detect.go

  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}