creds.go raw
1 // Copyright 2017 Google LLC.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
4
5 package internal
6
7 import (
8 "context"
9 "crypto/tls"
10 "encoding/json"
11 "errors"
12 "fmt"
13 "net"
14 "net/http"
15 "os"
16 "time"
17
18 "cloud.google.com/go/auth"
19 "cloud.google.com/go/auth/credentials"
20 "cloud.google.com/go/auth/oauth2adapt"
21 "golang.org/x/oauth2"
22 "google.golang.org/api/internal/cert"
23 "google.golang.org/api/internal/credentialstype"
24 "google.golang.org/api/internal/impersonate"
25
26 "golang.org/x/oauth2/google"
27 )
28
29 const quotaProjectEnvVar = "GOOGLE_CLOUD_QUOTA_PROJECT"
30
31 // Creds returns credential information obtained from DialSettings, or if none, then
32 // it returns default credential information.
33 func Creds(ctx context.Context, ds *DialSettings) (*google.Credentials, error) {
34 if ds.IsNewAuthLibraryEnabled() {
35 return credsNewAuth(ds)
36 }
37 creds, err := baseCreds(ctx, ds)
38 if err != nil {
39 return nil, err
40 }
41 if ds.ImpersonationConfig != nil {
42 return impersonateCredentials(ctx, creds, ds)
43 }
44 return creds, nil
45 }
46
47 // AuthCreds returns [cloud.google.com/go/auth.Credentials] based on credentials
48 // options provided via [option.ClientOption], including legacy oauth2/google
49 // options. If there are no applicable options, then it returns the result of
50 // [cloud.google.com/go/auth/credentials.DetectDefault].
51 // Note: If NoAuth is true, when [google.golang.org/api/option.WithoutAuthentication]
52 // is passed, then no authentication will be performed and this function will
53 // return nil, nil.
54 func AuthCreds(ctx context.Context, settings *DialSettings) (*auth.Credentials, error) {
55 if settings.NoAuth {
56 return nil, nil
57 }
58 if settings.AuthCredentials != nil {
59 return settings.AuthCredentials, nil
60 }
61 // Support oauth2/google options
62 var oauth2Creds *google.Credentials
63 if settings.InternalCredentials != nil {
64 oauth2Creds = settings.InternalCredentials
65 } else if settings.Credentials != nil {
66 oauth2Creds = settings.Credentials
67 } else if settings.TokenSource != nil {
68 oauth2Creds = &google.Credentials{TokenSource: settings.TokenSource}
69 }
70 if oauth2Creds != nil {
71 return oauth2adapt.AuthCredentialsFromOauth2Credentials(oauth2Creds), nil
72 }
73
74 return detectDefaultFromDialSettings(settings)
75 }
76
77 // GetOAuth2Configuration determines configurations for the OAuth2 transport, which is separate from the API transport.
78 // The OAuth2 transport and endpoint will be configured for mTLS if applicable.
79 func GetOAuth2Configuration(ctx context.Context, settings *DialSettings) (string, *http.Client, error) {
80 clientCertSource, err := getClientCertificateSource(settings)
81 if err != nil {
82 return "", nil, err
83 }
84 tokenURL := oAuth2Endpoint(clientCertSource)
85 var oauth2Client *http.Client
86 if clientCertSource != nil {
87 tlsConfig := &tls.Config{
88 GetClientCertificate: clientCertSource,
89 }
90 oauth2Client = customHTTPClient(tlsConfig)
91 } else {
92 oauth2Client = oauth2.NewClient(ctx, nil)
93 }
94 return tokenURL, oauth2Client, nil
95 }
96
97 func credsNewAuth(settings *DialSettings) (*google.Credentials, error) {
98 // Preserve old options behavior
99 if settings.InternalCredentials != nil {
100 return settings.InternalCredentials, nil
101 } else if settings.Credentials != nil {
102 return settings.Credentials, nil
103 } else if settings.TokenSource != nil {
104 return &google.Credentials{TokenSource: settings.TokenSource}, nil
105 }
106
107 if settings.AuthCredentials != nil {
108 return oauth2adapt.Oauth2CredentialsFromAuthCredentials(settings.AuthCredentials), nil
109 }
110
111 creds, err := detectDefaultFromDialSettings(settings)
112 if err != nil {
113 return nil, err
114 }
115 return oauth2adapt.Oauth2CredentialsFromAuthCredentials(creds), nil
116 }
117
118 func detectDefaultFromDialSettings(settings *DialSettings) (*auth.Credentials, error) {
119 var useSelfSignedJWT bool
120 var aud string
121 var scopes []string
122 // If scoped JWTs are enabled user provided an aud, allow self-signed JWT.
123 if settings.EnableJwtWithScope || len(settings.Audiences) > 0 {
124 useSelfSignedJWT = true
125 }
126
127 if len(settings.Scopes) > 0 {
128 scopes = make([]string, len(settings.Scopes))
129 copy(scopes, settings.Scopes)
130 }
131 if len(settings.Audiences) > 0 {
132 aud = settings.Audiences[0]
133 }
134 // Only default scopes if user did not also set an audience.
135 if len(settings.Scopes) == 0 && aud == "" && len(settings.DefaultScopes) > 0 {
136 scopes = make([]string, len(settings.DefaultScopes))
137 copy(scopes, settings.DefaultScopes)
138 }
139 if len(scopes) == 0 && aud == "" {
140 aud = settings.DefaultAudience
141 }
142
143 credsFile, _ := settings.GetAuthCredentialsFile()
144 credsJSON, _ := settings.GetAuthCredentialsJSON()
145 return credentials.DetectDefault(&credentials.DetectOptions{
146 Scopes: scopes,
147 Audience: aud,
148 CredentialsFile: credsFile,
149 CredentialsJSON: credsJSON,
150 UseSelfSignedJWT: useSelfSignedJWT,
151 Logger: settings.Logger,
152 })
153 }
154
155 func baseCreds(ctx context.Context, ds *DialSettings) (*google.Credentials, error) {
156 if ds.InternalCredentials != nil {
157 return ds.InternalCredentials, nil
158 }
159 if ds.Credentials != nil {
160 return ds.Credentials, nil
161 }
162 if credsJSON, checkCredType := ds.GetAuthCredentialsJSON(); len(credsJSON) > 0 {
163 return credentialsFromJSON(ctx, credsJSON, ds, checkCredType)
164 }
165 if credsFile, checkCredType := ds.GetAuthCredentialsFile(); credsFile != "" {
166 data, err := os.ReadFile(credsFile)
167 if err != nil {
168 return nil, fmt.Errorf("cannot read credentials file: %v", err)
169 }
170 return credentialsFromJSON(ctx, data, ds, checkCredType)
171 }
172 if ds.TokenSource != nil {
173 return &google.Credentials{TokenSource: ds.TokenSource}, nil
174 }
175 cred, err := google.FindDefaultCredentials(ctx, ds.GetScopes()...)
176 if err != nil {
177 return nil, err
178 }
179 if len(cred.JSON) > 0 {
180 return credentialsFromJSON(ctx, cred.JSON, ds, credentialstype.Unknown)
181 }
182 // For GAE and GCE, the JSON is empty so return the default credentials directly.
183 return cred, nil
184 }
185
186 // JSON key file type.
187 const (
188 serviceAccountKey = "service_account"
189 )
190
191 // credentialsFromJSON returns a google.Credentials from the JSON data
192 //
193 // - A self-signed JWT flow will be executed if the following conditions are
194 // met:
195 //
196 // (1) At least one of the following is true:
197 // (a) Scope for self-signed JWT flow is enabled
198 // (b) Audiences are explicitly provided by users
199 // (2) No service account impersontation
200 //
201 // - Otherwise, executes standard OAuth 2.0 flow
202 // More details: google.aip.dev/auth/4111
203 func credentialsFromJSON(ctx context.Context, data []byte, ds *DialSettings, checkCredType credentialstype.CredType) (*google.Credentials, error) {
204 if checkCredType != credentialstype.Unknown {
205 if err := credentialstype.CheckCredentialType(data, checkCredType); err != nil {
206 return nil, err
207 }
208 }
209 var params google.CredentialsParams
210 params.Scopes = ds.GetScopes()
211
212 tokenURL, oauth2Client, err := GetOAuth2Configuration(ctx, ds)
213 if err != nil {
214 return nil, err
215 }
216 params.TokenURL = tokenURL
217 ctx = context.WithValue(ctx, oauth2.HTTPClient, oauth2Client)
218
219 // By default, a standard OAuth 2.0 token source is created
220 cred, err := google.CredentialsFromJSONWithParams(ctx, data, params)
221 if err != nil {
222 return nil, err
223 }
224
225 // Override the token source to use self-signed JWT if conditions are met
226 isJWTFlow, err := isSelfSignedJWTFlow(data, ds)
227 if err != nil {
228 return nil, err
229 }
230 if isJWTFlow {
231 ts, err := selfSignedJWTTokenSource(data, ds)
232 if err != nil {
233 return nil, err
234 }
235 cred.TokenSource = ts
236 }
237
238 return cred, err
239 }
240
241 func oAuth2Endpoint(clientCertSource cert.Source) string {
242 if isMTLS(clientCertSource) {
243 return google.MTLSTokenURL
244 }
245 return google.Endpoint.TokenURL
246 }
247
248 func isSelfSignedJWTFlow(data []byte, ds *DialSettings) (bool, error) {
249 // For non-GDU universe domains, token exchange is impossible and services
250 // must support self-signed JWTs with scopes.
251 if !ds.IsUniverseDomainGDU() {
252 return typeServiceAccount(data)
253 }
254 if (ds.EnableJwtWithScope || ds.HasCustomAudience()) && ds.ImpersonationConfig == nil {
255 return typeServiceAccount(data)
256 }
257 return false, nil
258 }
259
260 // typeServiceAccount checks if JSON data is for a service account.
261 func typeServiceAccount(data []byte) (bool, error) {
262 var f struct {
263 Type string `json:"type"`
264 // The remaining JSON fields are omitted because they are not used.
265 }
266 if err := json.Unmarshal(data, &f); err != nil {
267 return false, err
268 }
269 return f.Type == serviceAccountKey, nil
270 }
271
272 func selfSignedJWTTokenSource(data []byte, ds *DialSettings) (oauth2.TokenSource, error) {
273 if len(ds.GetScopes()) > 0 && !ds.HasCustomAudience() {
274 // Scopes are preferred in self-signed JWT unless the scope is not available
275 // or a custom audience is used.
276 return google.JWTAccessTokenSourceWithScope(data, ds.GetScopes()...)
277 } else if ds.GetAudience() != "" {
278 // Fallback to audience if scope is not provided
279 return google.JWTAccessTokenSourceFromJSON(data, ds.GetAudience())
280 } else {
281 return nil, errors.New("neither scopes or audience are available for the self-signed JWT")
282 }
283 }
284
285 // GetQuotaProject retrieves quota project with precedence being: client option,
286 // environment variable, creds file.
287 func GetQuotaProject(creds *google.Credentials, clientOpt string) string {
288 if clientOpt != "" {
289 return clientOpt
290 }
291 if env := os.Getenv(quotaProjectEnvVar); env != "" {
292 return env
293 }
294 if creds == nil {
295 return ""
296 }
297 var v struct {
298 QuotaProject string `json:"quota_project_id"`
299 }
300 if err := json.Unmarshal(creds.JSON, &v); err != nil {
301 return ""
302 }
303 return v.QuotaProject
304 }
305
306 func impersonateCredentials(ctx context.Context, creds *google.Credentials, ds *DialSettings) (*google.Credentials, error) {
307 if len(ds.ImpersonationConfig.Scopes) == 0 {
308 ds.ImpersonationConfig.Scopes = ds.GetScopes()
309 }
310 ts, err := impersonate.TokenSource(ctx, creds.TokenSource, ds.ImpersonationConfig)
311 if err != nil {
312 return nil, err
313 }
314 return &google.Credentials{
315 TokenSource: ts,
316 ProjectID: creds.ProjectID,
317 }, nil
318 }
319
320 // customHTTPClient constructs an HTTPClient using the provided tlsConfig, to support mTLS.
321 func customHTTPClient(tlsConfig *tls.Config) *http.Client {
322 trans := baseTransport()
323 trans.TLSClientConfig = tlsConfig
324 return &http.Client{Transport: trans}
325 }
326
327 func baseTransport() *http.Transport {
328 return &http.Transport{
329 Proxy: http.ProxyFromEnvironment,
330 DialContext: (&net.Dialer{
331 Timeout: 30 * time.Second,
332 KeepAlive: 30 * time.Second,
333 DualStack: true,
334 }).DialContext,
335 MaxIdleConns: 100,
336 MaxIdleConnsPerHost: 100,
337 IdleConnTimeout: 90 * time.Second,
338 TLSHandshakeTimeout: 10 * time.Second,
339 ExpectContinueTimeout: 1 * time.Second,
340 }
341 }
342