...

Source file src/golang.org/x/oauth2/google/google.go

Documentation: golang.org/x/oauth2/google

     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  	return computeTokenSource(account, 0, scope...)
   256  }
   257  
   258  func computeTokenSource(account string, earlyExpiry time.Duration, scope ...string) oauth2.TokenSource {
   259  	return oauth2.ReuseTokenSourceWithExpiry(nil, computeSource{account: account, scopes: scope}, earlyExpiry)
   260  }
   261  
   262  type computeSource struct {
   263  	account string
   264  	scopes  []string
   265  }
   266  
   267  func (cs computeSource) Token() (*oauth2.Token, error) {
   268  	if !metadata.OnGCE() {
   269  		return nil, errors.New("oauth2/google: can't get a token from the metadata service; not running on GCE")
   270  	}
   271  	acct := cs.account
   272  	if acct == "" {
   273  		acct = "default"
   274  	}
   275  	tokenURI := "instance/service-accounts/" + acct + "/token"
   276  	if len(cs.scopes) > 0 {
   277  		v := url.Values{}
   278  		v.Set("scopes", strings.Join(cs.scopes, ","))
   279  		tokenURI = tokenURI + "?" + v.Encode()
   280  	}
   281  	tokenJSON, err := metadata.Get(tokenURI)
   282  	if err != nil {
   283  		return nil, err
   284  	}
   285  	var res struct {
   286  		AccessToken  string `json:"access_token"`
   287  		ExpiresInSec int    `json:"expires_in"`
   288  		TokenType    string `json:"token_type"`
   289  	}
   290  	err = json.NewDecoder(strings.NewReader(tokenJSON)).Decode(&res)
   291  	if err != nil {
   292  		return nil, fmt.Errorf("oauth2/google: invalid token JSON from metadata: %v", err)
   293  	}
   294  	if res.ExpiresInSec == 0 || res.AccessToken == "" {
   295  		return nil, fmt.Errorf("oauth2/google: incomplete token received from metadata")
   296  	}
   297  	tok := &oauth2.Token{
   298  		AccessToken: res.AccessToken,
   299  		TokenType:   res.TokenType,
   300  		Expiry:      time.Now().Add(time.Duration(res.ExpiresInSec) * time.Second),
   301  	}
   302  	// NOTE(cbro): add hidden metadata about where the token is from.
   303  	// This is needed for detection by client libraries to know that credentials come from the metadata server.
   304  	// This may be removed in a future version of this library.
   305  	return tok.WithExtra(map[string]interface{}{
   306  		"oauth2.google.tokenSource":    "compute-metadata",
   307  		"oauth2.google.serviceAccount": acct,
   308  	}), nil
   309  }
   310  

View as plain text