1// Copyright 2023 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package credentials
16
17import (
18 "context"
19 "encoding/json"
20 "errors"
21 "fmt"
22 "log/slog"
23 "net/http"
24 "os"
25 "time"
26
27 "cloud.google.com/go/auth"
28 "cloud.google.com/go/auth/internal"
29 "cloud.google.com/go/auth/internal/credsfile"
30 "cloud.google.com/go/compute/metadata"
31 "github.com/googleapis/gax-go/v2/internallog"
32)
33
34const (
35 // jwtTokenURL is Google's OAuth 2.0 token URL to use with the JWT(2LO) flow.
36 jwtTokenURL = "https://oauth2.googleapis.com/token"
37
38 // Google's OAuth 2.0 default endpoints.
39 googleAuthURL = "https://accounts.google.com/o/oauth2/auth"
40 googleTokenURL = "https://oauth2.googleapis.com/token"
41
42 // GoogleMTLSTokenURL is Google's default OAuth2.0 mTLS endpoint.
43 GoogleMTLSTokenURL = "https://oauth2.mtls.googleapis.com/token"
44
45 // Help on default credentials
46 adcSetupURL = "https://cloud.google.com/docs/authentication/external/set-up-adc"
47)
48
49var (
50 // for testing
51 allowOnGCECheck = true
52)
53
54// OnGCE reports whether this process is running in Google Cloud.
55func OnGCE() bool {
56 // TODO(codyoss): once all libs use this auth lib move metadata check here
57 return allowOnGCECheck && metadata.OnGCE()
58}
59
60// DetectDefault searches for "Application Default Credentials" and returns
61// a credential based on the [DetectOptions] provided.
62//
63// It looks for credentials in the following places, preferring the first
64// location found:
65//
66// - A JSON file whose path is specified by the GOOGLE_APPLICATION_CREDENTIALS
67// environment variable. For workload identity federation, refer to
68// https://cloud.google.com/iam/docs/how-to#using-workload-identity-federation
69// on how to generate the JSON configuration file for on-prem/non-Google
70// cloud platforms.
71// - A JSON file in a location known to the gcloud command-line tool. On
72// Windows, this is %APPDATA%/gcloud/application_default_credentials.json. On
73// other systems, $HOME/.config/gcloud/application_default_credentials.json.
74// - On Google Compute Engine, Google App Engine standard second generation
75// runtimes, and Google App Engine flexible environment, it fetches
76// credentials from the metadata server.
77func DetectDefault(opts *DetectOptions) (*auth.Credentials, error) {
78 if err := opts.validate(); err != nil {
79 return nil, err
80 }
81 if len(opts.CredentialsJSON) > 0 {
82 return readCredentialsFileJSON(opts.CredentialsJSON, opts)
83 }
84 if opts.CredentialsFile != "" {
85 return readCredentialsFile(opts.CredentialsFile, opts)
86 }
87 if filename := os.Getenv(credsfile.GoogleAppCredsEnvVar); filename != "" {
88 creds, err := readCredentialsFile(filename, opts)
89 if err != nil {
90 return nil, err
91 }
92 return creds, nil
93 }
94
95 fileName := credsfile.GetWellKnownFileName()
96 if b, err := os.ReadFile(fileName); err == nil {
97 return readCredentialsFileJSON(b, opts)
98 }
99
100 if OnGCE() {
101 metadataClient := metadata.NewWithOptions(&metadata.Options{
102 Logger: opts.logger(),
103 })
104 return auth.NewCredentials(&auth.CredentialsOptions{
105 TokenProvider: computeTokenProvider(opts, metadataClient),
106 ProjectIDProvider: auth.CredentialsPropertyFunc(func(ctx context.Context) (string, error) {
107 return metadataClient.ProjectIDWithContext(ctx)
108 }),
109 UniverseDomainProvider: &internal.ComputeUniverseDomainProvider{
110 MetadataClient: metadataClient,
111 },
112 }), nil
113 }
114
115 return nil, fmt.Errorf("credentials: could not find default credentials. See %v for more information", adcSetupURL)
116}
117
118// DetectOptions provides configuration for [DetectDefault].
119type DetectOptions struct {
120 // Scopes that credentials tokens should have. Example:
121 // https://www.googleapis.com/auth/cloud-platform. Required if Audience is
122 // not provided.
123 Scopes []string
124 // Audience that credentials tokens should have. Only applicable for 2LO
125 // flows with service accounts. If specified, scopes should not be provided.
126 Audience string
127 // Subject is the user email used for [domain wide delegation](https://developers.google.com/identity/protocols/oauth2/service-account#delegatingauthority).
128 // Optional.
129 Subject string
130 // EarlyTokenRefresh configures how early before a token expires that it
131 // should be refreshed. Once the tokenβs time until expiration has entered
132 // this refresh window the token is considered valid but stale. If unset,
133 // the default value is 3 minutes and 45 seconds. Optional.
134 EarlyTokenRefresh time.Duration
135 // DisableAsyncRefresh configures a synchronous workflow that refreshes
136 // stale tokens while blocking. The default is false. Optional.
137 DisableAsyncRefresh bool
138 // AuthHandlerOptions configures an authorization handler and other options
139 // for 3LO flows. It is required, and only used, for client credential
140 // flows.
141 AuthHandlerOptions *auth.AuthorizationHandlerOptions
142 // TokenURL allows to set the token endpoint for user credential flows. If
143 // unset the default value is: https://oauth2.googleapis.com/token.
144 // Optional.
145 TokenURL string
146 // STSAudience is the audience sent to when retrieving an STS token.
147 // Currently this only used for GDCH auth flow, for which it is required.
148 STSAudience string
149 // CredentialsFile overrides detection logic and sources a credential file
150 // from the provided filepath. If provided, CredentialsJSON must not be.
151 // Optional.
152 CredentialsFile string
153 // CredentialsJSON overrides detection logic and uses the JSON bytes as the
154 // source for the credential. If provided, CredentialsFile must not be.
155 // Optional.
156 CredentialsJSON []byte
157 // UseSelfSignedJWT directs service account based credentials to create a
158 // self-signed JWT with the private key found in the file, skipping any
159 // network requests that would normally be made. Optional.
160 UseSelfSignedJWT bool
161 // Client configures the underlying client used to make network requests
162 // when fetching tokens. Optional.
163 Client *http.Client
164 // UniverseDomain is the default service domain for a given Cloud universe.
165 // The default value is "googleapis.com". This option is ignored for
166 // authentication flows that do not support universe domain. Optional.
167 UniverseDomain string
168 // Logger is used for debug logging. If provided, logging will be enabled
169 // at the loggers configured level. By default logging is disabled unless
170 // enabled by setting GOOGLE_SDK_GO_LOGGING_LEVEL in which case a default
171 // logger will be used. Optional.
172 Logger *slog.Logger
173}
174
175func (o *DetectOptions) validate() error {
176 if o == nil {
177 return errors.New("credentials: options must be provided")
178 }
179 if len(o.Scopes) > 0 && o.Audience != "" {
180 return errors.New("credentials: both scopes and audience were provided")
181 }
182 if len(o.CredentialsJSON) > 0 && o.CredentialsFile != "" {
183 return errors.New("credentials: both credentials file and JSON were provided")
184 }
185 return nil
186}
187
188func (o *DetectOptions) tokenURL() string {
189 if o.TokenURL != "" {
190 return o.TokenURL
191 }
192 return googleTokenURL
193}
194
195func (o *DetectOptions) scopes() []string {
196 scopes := make([]string, len(o.Scopes))
197 copy(scopes, o.Scopes)
198 return scopes
199}
200
201func (o *DetectOptions) client() *http.Client {
202 if o.Client != nil {
203 return o.Client
204 }
205 return internal.DefaultClient()
206}
207
208func (o *DetectOptions) logger() *slog.Logger {
209 return internallog.New(o.Logger)
210}
211
212func readCredentialsFile(filename string, opts *DetectOptions) (*auth.Credentials, error) {
213 b, err := os.ReadFile(filename)
214 if err != nil {
215 return nil, err
216 }
217 return readCredentialsFileJSON(b, opts)
218}
219
220func readCredentialsFileJSON(b []byte, opts *DetectOptions) (*auth.Credentials, error) {
221 // attempt to parse jsonData as a Google Developers Console client_credentials.json.
222 config := clientCredConfigFromJSON(b, opts)
223 if config != nil {
224 if config.AuthHandlerOpts == nil {
225 return nil, errors.New("credentials: auth handler must be specified for this credential filetype")
226 }
227 tp, err := auth.New3LOTokenProvider(config)
228 if err != nil {
229 return nil, err
230 }
231 return auth.NewCredentials(&auth.CredentialsOptions{
232 TokenProvider: tp,
233 JSON: b,
234 }), nil
235 }
236 return fileCredentials(b, opts)
237}
238
239func clientCredConfigFromJSON(b []byte, opts *DetectOptions) *auth.Options3LO {
240 var creds credsfile.ClientCredentialsFile
241 var c *credsfile.Config3LO
242 if err := json.Unmarshal(b, &creds); err != nil {
243 return nil
244 }
245 switch {
246 case creds.Web != nil:
247 c = creds.Web
248 case creds.Installed != nil:
249 c = creds.Installed
250 default:
251 return nil
252 }
253 if len(c.RedirectURIs) < 1 {
254 return nil
255 }
256 var handleOpts *auth.AuthorizationHandlerOptions
257 if opts.AuthHandlerOptions != nil {
258 handleOpts = &auth.AuthorizationHandlerOptions{
259 Handler: opts.AuthHandlerOptions.Handler,
260 State: opts.AuthHandlerOptions.State,
261 PKCEOpts: opts.AuthHandlerOptions.PKCEOpts,
262 }
263 }
264 return &auth.Options3LO{
265 ClientID: c.ClientID,
266 ClientSecret: c.ClientSecret,
267 RedirectURL: c.RedirectURIs[0],
268 Scopes: opts.scopes(),
269 AuthURL: c.AuthURI,
270 TokenURL: c.TokenURI,
271 Client: opts.client(),
272 Logger: opts.logger(),
273 EarlyTokenExpiry: opts.EarlyTokenRefresh,
274 AuthHandlerOpts: handleOpts,
275 // TODO(codyoss): refactor this out. We need to add in auto-detection
276 // for this use case.
277 AuthStyle: auth.StyleInParams,
278 }
279}