google.go raw

   1  // Copyright 2014 The Go Authors. All rights reserved.
   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 google
   6  
   7  import (
   8  	"context"
   9  	"encoding/json"
  10  	"errors"
  11  	"fmt"
  12  	"net/url"
  13  	"strings"
  14  	"time"
  15  
  16  	"cloud.google.com/go/compute/metadata"
  17  	"golang.org/x/oauth2"
  18  	"golang.org/x/oauth2/google/externalaccount"
  19  	"golang.org/x/oauth2/google/internal/externalaccountauthorizeduser"
  20  	"golang.org/x/oauth2/google/internal/impersonate"
  21  	"golang.org/x/oauth2/jwt"
  22  )
  23  
  24  // Endpoint is Google's OAuth 2.0 default endpoint.
  25  var Endpoint = oauth2.Endpoint{
  26  	AuthURL:       "https://accounts.google.com/o/oauth2/auth",
  27  	TokenURL:      "https://oauth2.googleapis.com/token",
  28  	DeviceAuthURL: "https://oauth2.googleapis.com/device/code",
  29  	AuthStyle:     oauth2.AuthStyleInParams,
  30  }
  31  
  32  // MTLSTokenURL is Google's OAuth 2.0 default mTLS endpoint.
  33  const MTLSTokenURL = "https://oauth2.mtls.googleapis.com/token"
  34  
  35  // JWTTokenURL is Google's OAuth 2.0 token URL to use with the JWT flow.
  36  const JWTTokenURL = "https://oauth2.googleapis.com/token"
  37  
  38  // ConfigFromJSON uses a Google Developers Console client_credentials.json
  39  // file to construct a config.
  40  // client_credentials.json can be downloaded from
  41  // https://console.developers.google.com, under "Credentials". Download the Web
  42  // application credentials in the JSON format and provide the contents of the
  43  // file as jsonKey.
  44  func ConfigFromJSON(jsonKey []byte, scope ...string) (*oauth2.Config, error) {
  45  	type cred struct {
  46  		ClientID     string   `json:"client_id"`
  47  		ClientSecret string   `json:"client_secret"`
  48  		RedirectURIs []string `json:"redirect_uris"`
  49  		AuthURI      string   `json:"auth_uri"`
  50  		TokenURI     string   `json:"token_uri"`
  51  	}
  52  	var j struct {
  53  		Web       *cred `json:"web"`
  54  		Installed *cred `json:"installed"`
  55  	}
  56  	if err := json.Unmarshal(jsonKey, &j); err != nil {
  57  		return nil, err
  58  	}
  59  	var c *cred
  60  	switch {
  61  	case j.Web != nil:
  62  		c = j.Web
  63  	case j.Installed != nil:
  64  		c = j.Installed
  65  	default:
  66  		return nil, fmt.Errorf("oauth2/google: no credentials found")
  67  	}
  68  	if len(c.RedirectURIs) < 1 {
  69  		return nil, errors.New("oauth2/google: missing redirect URL in the client_credentials.json")
  70  	}
  71  	return &oauth2.Config{
  72  		ClientID:     c.ClientID,
  73  		ClientSecret: c.ClientSecret,
  74  		RedirectURL:  c.RedirectURIs[0],
  75  		Scopes:       scope,
  76  		Endpoint: oauth2.Endpoint{
  77  			AuthURL:  c.AuthURI,
  78  			TokenURL: c.TokenURI,
  79  		},
  80  	}, nil
  81  }
  82  
  83  // JWTConfigFromJSON uses a Google Developers service account JSON key file to read
  84  // the credentials that authorize and authenticate the requests.
  85  // Create a service account on "Credentials" for your project at
  86  // https://console.developers.google.com to download a JSON key file.
  87  func JWTConfigFromJSON(jsonKey []byte, scope ...string) (*jwt.Config, error) {
  88  	var f credentialsFile
  89  	if err := json.Unmarshal(jsonKey, &f); err != nil {
  90  		return nil, err
  91  	}
  92  	if f.Type != serviceAccountKey {
  93  		return nil, fmt.Errorf("google: read JWT from JSON credentials: 'type' field is %q (expected %q)", f.Type, serviceAccountKey)
  94  	}
  95  	scope = append([]string(nil), scope...) // copy
  96  	return f.jwtConfig(scope, ""), nil
  97  }
  98  
  99  // JSON key file types.
 100  const (
 101  	serviceAccountKey                = "service_account"
 102  	userCredentialsKey               = "authorized_user"
 103  	externalAccountKey               = "external_account"
 104  	externalAccountAuthorizedUserKey = "external_account_authorized_user"
 105  	impersonatedServiceAccount       = "impersonated_service_account"
 106  )
 107  
 108  // credentialsFile is the unmarshalled representation of a credentials file.
 109  type credentialsFile struct {
 110  	Type string `json:"type"`
 111  
 112  	// Service Account fields
 113  	ClientEmail    string `json:"client_email"`
 114  	PrivateKeyID   string `json:"private_key_id"`
 115  	PrivateKey     string `json:"private_key"`
 116  	AuthURL        string `json:"auth_uri"`
 117  	TokenURL       string `json:"token_uri"`
 118  	ProjectID      string `json:"project_id"`
 119  	UniverseDomain string `json:"universe_domain"`
 120  
 121  	// User Credential fields
 122  	// (These typically come from gcloud auth.)
 123  	ClientSecret string `json:"client_secret"`
 124  	ClientID     string `json:"client_id"`
 125  	RefreshToken string `json:"refresh_token"`
 126  
 127  	// External Account fields
 128  	Audience                       string                           `json:"audience"`
 129  	SubjectTokenType               string                           `json:"subject_token_type"`
 130  	TokenURLExternal               string                           `json:"token_url"`
 131  	TokenInfoURL                   string                           `json:"token_info_url"`
 132  	ServiceAccountImpersonationURL string                           `json:"service_account_impersonation_url"`
 133  	ServiceAccountImpersonation    serviceAccountImpersonationInfo  `json:"service_account_impersonation"`
 134  	Delegates                      []string                         `json:"delegates"`
 135  	CredentialSource               externalaccount.CredentialSource `json:"credential_source"`
 136  	QuotaProjectID                 string                           `json:"quota_project_id"`
 137  	WorkforcePoolUserProject       string                           `json:"workforce_pool_user_project"`
 138  
 139  	// External Account Authorized User fields
 140  	RevokeURL string `json:"revoke_url"`
 141  
 142  	// Service account impersonation
 143  	SourceCredentials *credentialsFile `json:"source_credentials"`
 144  }
 145  
 146  type serviceAccountImpersonationInfo struct {
 147  	TokenLifetimeSeconds int `json:"token_lifetime_seconds"`
 148  }
 149  
 150  func (f *credentialsFile) jwtConfig(scopes []string, subject string) *jwt.Config {
 151  	cfg := &jwt.Config{
 152  		Email:        f.ClientEmail,
 153  		PrivateKey:   []byte(f.PrivateKey),
 154  		PrivateKeyID: f.PrivateKeyID,
 155  		Scopes:       scopes,
 156  		TokenURL:     f.TokenURL,
 157  		Subject:      subject, // This is the user email to impersonate
 158  		Audience:     f.Audience,
 159  	}
 160  	if cfg.TokenURL == "" {
 161  		cfg.TokenURL = JWTTokenURL
 162  	}
 163  	return cfg
 164  }
 165  
 166  func (f *credentialsFile) tokenSource(ctx context.Context, params CredentialsParams) (oauth2.TokenSource, error) {
 167  	switch f.Type {
 168  	case serviceAccountKey:
 169  		cfg := f.jwtConfig(params.Scopes, params.Subject)
 170  		return cfg.TokenSource(ctx), nil
 171  	case userCredentialsKey:
 172  		cfg := &oauth2.Config{
 173  			ClientID:     f.ClientID,
 174  			ClientSecret: f.ClientSecret,
 175  			Scopes:       params.Scopes,
 176  			Endpoint: oauth2.Endpoint{
 177  				AuthURL:   f.AuthURL,
 178  				TokenURL:  f.TokenURL,
 179  				AuthStyle: oauth2.AuthStyleInParams,
 180  			},
 181  		}
 182  		if cfg.Endpoint.AuthURL == "" {
 183  			cfg.Endpoint.AuthURL = Endpoint.AuthURL
 184  		}
 185  		if cfg.Endpoint.TokenURL == "" {
 186  			if params.TokenURL != "" {
 187  				cfg.Endpoint.TokenURL = params.TokenURL
 188  			} else {
 189  				cfg.Endpoint.TokenURL = Endpoint.TokenURL
 190  			}
 191  		}
 192  		tok := &oauth2.Token{RefreshToken: f.RefreshToken}
 193  		return cfg.TokenSource(ctx, tok), nil
 194  	case externalAccountKey:
 195  		cfg := &externalaccount.Config{
 196  			Audience:                       f.Audience,
 197  			SubjectTokenType:               f.SubjectTokenType,
 198  			TokenURL:                       f.TokenURLExternal,
 199  			TokenInfoURL:                   f.TokenInfoURL,
 200  			ServiceAccountImpersonationURL: f.ServiceAccountImpersonationURL,
 201  			ServiceAccountImpersonationLifetimeSeconds: f.ServiceAccountImpersonation.TokenLifetimeSeconds,
 202  			ClientSecret:             f.ClientSecret,
 203  			ClientID:                 f.ClientID,
 204  			CredentialSource:         &f.CredentialSource,
 205  			QuotaProjectID:           f.QuotaProjectID,
 206  			Scopes:                   params.Scopes,
 207  			WorkforcePoolUserProject: f.WorkforcePoolUserProject,
 208  		}
 209  		return externalaccount.NewTokenSource(ctx, *cfg)
 210  	case externalAccountAuthorizedUserKey:
 211  		cfg := &externalaccountauthorizeduser.Config{
 212  			Audience:       f.Audience,
 213  			RefreshToken:   f.RefreshToken,
 214  			TokenURL:       f.TokenURLExternal,
 215  			TokenInfoURL:   f.TokenInfoURL,
 216  			ClientID:       f.ClientID,
 217  			ClientSecret:   f.ClientSecret,
 218  			RevokeURL:      f.RevokeURL,
 219  			QuotaProjectID: f.QuotaProjectID,
 220  			Scopes:         params.Scopes,
 221  		}
 222  		return cfg.TokenSource(ctx)
 223  	case impersonatedServiceAccount:
 224  		if f.ServiceAccountImpersonationURL == "" || f.SourceCredentials == nil {
 225  			return nil, errors.New("missing 'source_credentials' field or 'service_account_impersonation_url' in credentials")
 226  		}
 227  
 228  		ts, err := f.SourceCredentials.tokenSource(ctx, params)
 229  		if err != nil {
 230  			return nil, err
 231  		}
 232  		imp := impersonate.ImpersonateTokenSource{
 233  			Ctx:       ctx,
 234  			URL:       f.ServiceAccountImpersonationURL,
 235  			Scopes:    params.Scopes,
 236  			Ts:        ts,
 237  			Delegates: f.Delegates,
 238  		}
 239  		return oauth2.ReuseTokenSource(nil, imp), nil
 240  	case "":
 241  		return nil, errors.New("missing 'type' field in credentials")
 242  	default:
 243  		return nil, fmt.Errorf("unknown credential type: %q", f.Type)
 244  	}
 245  }
 246  
 247  // ComputeTokenSource returns a token source that fetches access tokens
 248  // from Google Compute Engine (GCE)'s metadata server. It's only valid to use
 249  // this token source if your program is running on a GCE instance.
 250  // If no account is specified, "default" is used.
 251  // If no scopes are specified, a set of default scopes are automatically granted.
 252  // Further information about retrieving access tokens from the GCE metadata
 253  // server can be found at https://cloud.google.com/compute/docs/authentication.
 254  func ComputeTokenSource(account string, scope ...string) oauth2.TokenSource {
 255  	// Refresh 3 minutes and 45 seconds early. The shortest MDS cache is currently 4 minutes, so any
 256  	// refreshes earlier are a waste of compute.
 257  	earlyExpirySecs := 225 * time.Second
 258  	return computeTokenSource(account, earlyExpirySecs, scope...)
 259  }
 260  
 261  func computeTokenSource(account string, earlyExpiry time.Duration, scope ...string) oauth2.TokenSource {
 262  	return oauth2.ReuseTokenSourceWithExpiry(nil, computeSource{account: account, scopes: scope}, earlyExpiry)
 263  }
 264  
 265  type computeSource struct {
 266  	account string
 267  	scopes  []string
 268  }
 269  
 270  func (cs computeSource) Token() (*oauth2.Token, error) {
 271  	if !metadata.OnGCE() {
 272  		return nil, errors.New("oauth2/google: can't get a token from the metadata service; not running on GCE")
 273  	}
 274  	acct := cs.account
 275  	if acct == "" {
 276  		acct = "default"
 277  	}
 278  	tokenURI := "instance/service-accounts/" + acct + "/token"
 279  	if len(cs.scopes) > 0 {
 280  		v := url.Values{}
 281  		v.Set("scopes", strings.Join(cs.scopes, ","))
 282  		tokenURI = tokenURI + "?" + v.Encode()
 283  	}
 284  	tokenJSON, err := metadata.Get(tokenURI)
 285  	if err != nil {
 286  		return nil, err
 287  	}
 288  	var res oauth2.Token
 289  	err = json.NewDecoder(strings.NewReader(tokenJSON)).Decode(&res)
 290  	if err != nil {
 291  		return nil, fmt.Errorf("oauth2/google: invalid token JSON from metadata: %v", err)
 292  	}
 293  	if res.ExpiresIn == 0 || res.AccessToken == "" {
 294  		return nil, fmt.Errorf("oauth2/google: incomplete token received from metadata")
 295  	}
 296  	tok := &oauth2.Token{
 297  		AccessToken: res.AccessToken,
 298  		TokenType:   res.TokenType,
 299  		Expiry:      time.Now().Add(time.Duration(res.ExpiresIn) * time.Second),
 300  	}
 301  	// NOTE(cbro): add hidden metadata about where the token is from.
 302  	// This is needed for detection by client libraries to know that credentials come from the metadata server.
 303  	// This may be removed in a future version of this library.
 304  	return tok.WithExtra(map[string]any{
 305  		"oauth2.google.tokenSource":    "compute-metadata",
 306  		"oauth2.google.serviceAccount": acct,
 307  	}), nil
 308  }
 309