| // Copyright 2014 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package google |
| |
| import ( |
| "context" |
| "encoding/json" |
| "errors" |
| "fmt" |
| "net/url" |
| "strings" |
| "time" |
| |
| "cloud.google.com/go/compute/metadata" |
| "golang.org/x/oauth2" |
| "golang.org/x/oauth2/google/internal/externalaccount" |
| "golang.org/x/oauth2/google/internal/externalaccountauthorizeduser" |
| "golang.org/x/oauth2/jwt" |
| ) |
| |
| // Endpoint is Google's OAuth 2.0 default endpoint. |
| var Endpoint = oauth2.Endpoint{ |
| AuthURL: "https://accounts.google.com/o/oauth2/auth", |
| TokenURL: "https://oauth2.googleapis.com/token", |
| DeviceAuthURL: "https://oauth2.googleapis.com/device/code", |
| AuthStyle: oauth2.AuthStyleInParams, |
| } |
| |
| // MTLSTokenURL is Google's OAuth 2.0 default mTLS endpoint. |
| const MTLSTokenURL = "https://oauth2.mtls.googleapis.com/token" |
| |
| // JWTTokenURL is Google's OAuth 2.0 token URL to use with the JWT flow. |
| const JWTTokenURL = "https://oauth2.googleapis.com/token" |
| |
| // ConfigFromJSON uses a Google Developers Console client_credentials.json |
| // file to construct a config. |
| // client_credentials.json can be downloaded from |
| // https://console.developers.google.com, under "Credentials". Download the Web |
| // application credentials in the JSON format and provide the contents of the |
| // file as jsonKey. |
| func ConfigFromJSON(jsonKey []byte, scope ...string) (*oauth2.Config, error) { |
| type cred struct { |
| ClientID string `json:"client_id"` |
| ClientSecret string `json:"client_secret"` |
| RedirectURIs []string `json:"redirect_uris"` |
| AuthURI string `json:"auth_uri"` |
| TokenURI string `json:"token_uri"` |
| } |
| var j struct { |
| Web *cred `json:"web"` |
| Installed *cred `json:"installed"` |
| } |
| if err := json.Unmarshal(jsonKey, &j); err != nil { |
| return nil, err |
| } |
| var c *cred |
| switch { |
| case j.Web != nil: |
| c = j.Web |
| case j.Installed != nil: |
| c = j.Installed |
| default: |
| return nil, fmt.Errorf("oauth2/google: no credentials found") |
| } |
| if len(c.RedirectURIs) < 1 { |
| return nil, errors.New("oauth2/google: missing redirect URL in the client_credentials.json") |
| } |
| return &oauth2.Config{ |
| ClientID: c.ClientID, |
| ClientSecret: c.ClientSecret, |
| RedirectURL: c.RedirectURIs[0], |
| Scopes: scope, |
| Endpoint: oauth2.Endpoint{ |
| AuthURL: c.AuthURI, |
| TokenURL: c.TokenURI, |
| }, |
| }, nil |
| } |
| |
| // JWTConfigFromJSON uses a Google Developers service account JSON key file to read |
| // the credentials that authorize and authenticate the requests. |
| // Create a service account on "Credentials" for your project at |
| // https://console.developers.google.com to download a JSON key file. |
| func JWTConfigFromJSON(jsonKey []byte, scope ...string) (*jwt.Config, error) { |
| var f credentialsFile |
| if err := json.Unmarshal(jsonKey, &f); err != nil { |
| return nil, err |
| } |
| if f.Type != serviceAccountKey { |
| return nil, fmt.Errorf("google: read JWT from JSON credentials: 'type' field is %q (expected %q)", f.Type, serviceAccountKey) |
| } |
| scope = append([]string(nil), scope...) // copy |
| return f.jwtConfig(scope, ""), nil |
| } |
| |
| // JSON key file types. |
| const ( |
| serviceAccountKey = "service_account" |
| userCredentialsKey = "authorized_user" |
| externalAccountKey = "external_account" |
| externalAccountAuthorizedUserKey = "external_account_authorized_user" |
| impersonatedServiceAccount = "impersonated_service_account" |
| ) |
| |
| // credentialsFile is the unmarshalled representation of a credentials file. |
| type credentialsFile struct { |
| Type string `json:"type"` |
| |
| // Service Account fields |
| ClientEmail string `json:"client_email"` |
| PrivateKeyID string `json:"private_key_id"` |
| PrivateKey string `json:"private_key"` |
| AuthURL string `json:"auth_uri"` |
| TokenURL string `json:"token_uri"` |
| ProjectID string `json:"project_id"` |
| UniverseDomain string `json:"universe_domain"` |
| |
| // User Credential fields |
| // (These typically come from gcloud auth.) |
| ClientSecret string `json:"client_secret"` |
| ClientID string `json:"client_id"` |
| RefreshToken string `json:"refresh_token"` |
| |
| // External Account fields |
| Audience string `json:"audience"` |
| SubjectTokenType string `json:"subject_token_type"` |
| TokenURLExternal string `json:"token_url"` |
| TokenInfoURL string `json:"token_info_url"` |
| ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"` |
| ServiceAccountImpersonation serviceAccountImpersonationInfo `json:"service_account_impersonation"` |
| Delegates []string `json:"delegates"` |
| CredentialSource externalaccount.CredentialSource `json:"credential_source"` |
| QuotaProjectID string `json:"quota_project_id"` |
| WorkforcePoolUserProject string `json:"workforce_pool_user_project"` |
| |
| // External Account Authorized User fields |
| RevokeURL string `json:"revoke_url"` |
| |
| // Service account impersonation |
| SourceCredentials *credentialsFile `json:"source_credentials"` |
| } |
| |
| type serviceAccountImpersonationInfo struct { |
| TokenLifetimeSeconds int `json:"token_lifetime_seconds"` |
| } |
| |
| func (f *credentialsFile) jwtConfig(scopes []string, subject string) *jwt.Config { |
| cfg := &jwt.Config{ |
| Email: f.ClientEmail, |
| PrivateKey: []byte(f.PrivateKey), |
| PrivateKeyID: f.PrivateKeyID, |
| Scopes: scopes, |
| TokenURL: f.TokenURL, |
| Subject: subject, // This is the user email to impersonate |
| Audience: f.Audience, |
| } |
| if cfg.TokenURL == "" { |
| cfg.TokenURL = JWTTokenURL |
| } |
| return cfg |
| } |
| |
| func (f *credentialsFile) tokenSource(ctx context.Context, params CredentialsParams) (oauth2.TokenSource, error) { |
| switch f.Type { |
| case serviceAccountKey: |
| cfg := f.jwtConfig(params.Scopes, params.Subject) |
| return cfg.TokenSource(ctx), nil |
| case userCredentialsKey: |
| cfg := &oauth2.Config{ |
| ClientID: f.ClientID, |
| ClientSecret: f.ClientSecret, |
| Scopes: params.Scopes, |
| Endpoint: oauth2.Endpoint{ |
| AuthURL: f.AuthURL, |
| TokenURL: f.TokenURL, |
| AuthStyle: oauth2.AuthStyleInParams, |
| }, |
| } |
| if cfg.Endpoint.AuthURL == "" { |
| cfg.Endpoint.AuthURL = Endpoint.AuthURL |
| } |
| if cfg.Endpoint.TokenURL == "" { |
| if params.TokenURL != "" { |
| cfg.Endpoint.TokenURL = params.TokenURL |
| } else { |
| cfg.Endpoint.TokenURL = Endpoint.TokenURL |
| } |
| } |
| tok := &oauth2.Token{RefreshToken: f.RefreshToken} |
| return cfg.TokenSource(ctx, tok), nil |
| case externalAccountKey: |
| cfg := &externalaccount.Config{ |
| Audience: f.Audience, |
| SubjectTokenType: f.SubjectTokenType, |
| TokenURL: f.TokenURLExternal, |
| TokenInfoURL: f.TokenInfoURL, |
| ServiceAccountImpersonationURL: f.ServiceAccountImpersonationURL, |
| ServiceAccountImpersonationLifetimeSeconds: f.ServiceAccountImpersonation.TokenLifetimeSeconds, |
| ClientSecret: f.ClientSecret, |
| ClientID: f.ClientID, |
| CredentialSource: f.CredentialSource, |
| QuotaProjectID: f.QuotaProjectID, |
| Scopes: params.Scopes, |
| WorkforcePoolUserProject: f.WorkforcePoolUserProject, |
| } |
| return cfg.TokenSource(ctx) |
| case externalAccountAuthorizedUserKey: |
| cfg := &externalaccountauthorizeduser.Config{ |
| Audience: f.Audience, |
| RefreshToken: f.RefreshToken, |
| TokenURL: f.TokenURLExternal, |
| TokenInfoURL: f.TokenInfoURL, |
| ClientID: f.ClientID, |
| ClientSecret: f.ClientSecret, |
| RevokeURL: f.RevokeURL, |
| QuotaProjectID: f.QuotaProjectID, |
| Scopes: params.Scopes, |
| } |
| return cfg.TokenSource(ctx) |
| case impersonatedServiceAccount: |
| if f.ServiceAccountImpersonationURL == "" || f.SourceCredentials == nil { |
| return nil, errors.New("missing 'source_credentials' field or 'service_account_impersonation_url' in credentials") |
| } |
| |
| ts, err := f.SourceCredentials.tokenSource(ctx, params) |
| if err != nil { |
| return nil, err |
| } |
| imp := externalaccount.ImpersonateTokenSource{ |
| Ctx: ctx, |
| URL: f.ServiceAccountImpersonationURL, |
| Scopes: params.Scopes, |
| Ts: ts, |
| Delegates: f.Delegates, |
| } |
| return oauth2.ReuseTokenSource(nil, imp), nil |
| case "": |
| return nil, errors.New("missing 'type' field in credentials") |
| default: |
| return nil, fmt.Errorf("unknown credential type: %q", f.Type) |
| } |
| } |
| |
| // ComputeTokenSource returns a token source that fetches access tokens |
| // from Google Compute Engine (GCE)'s metadata server. It's only valid to use |
| // this token source if your program is running on a GCE instance. |
| // If no account is specified, "default" is used. |
| // If no scopes are specified, a set of default scopes are automatically granted. |
| // Further information about retrieving access tokens from the GCE metadata |
| // server can be found at https://cloud.google.com/compute/docs/authentication. |
| func ComputeTokenSource(account string, scope ...string) oauth2.TokenSource { |
| return computeTokenSource(account, 0, scope...) |
| } |
| |
| func computeTokenSource(account string, earlyExpiry time.Duration, scope ...string) oauth2.TokenSource { |
| return oauth2.ReuseTokenSourceWithExpiry(nil, computeSource{account: account, scopes: scope}, earlyExpiry) |
| } |
| |
| type computeSource struct { |
| account string |
| scopes []string |
| } |
| |
| func (cs computeSource) Token() (*oauth2.Token, error) { |
| if !metadata.OnGCE() { |
| return nil, errors.New("oauth2/google: can't get a token from the metadata service; not running on GCE") |
| } |
| acct := cs.account |
| if acct == "" { |
| acct = "default" |
| } |
| tokenURI := "instance/service-accounts/" + acct + "/token" |
| if len(cs.scopes) > 0 { |
| v := url.Values{} |
| v.Set("scopes", strings.Join(cs.scopes, ",")) |
| tokenURI = tokenURI + "?" + v.Encode() |
| } |
| tokenJSON, err := metadata.Get(tokenURI) |
| if err != nil { |
| return nil, err |
| } |
| var res struct { |
| AccessToken string `json:"access_token"` |
| ExpiresInSec int `json:"expires_in"` |
| TokenType string `json:"token_type"` |
| } |
| err = json.NewDecoder(strings.NewReader(tokenJSON)).Decode(&res) |
| if err != nil { |
| return nil, fmt.Errorf("oauth2/google: invalid token JSON from metadata: %v", err) |
| } |
| if res.ExpiresInSec == 0 || res.AccessToken == "" { |
| return nil, fmt.Errorf("oauth2/google: incomplete token received from metadata") |
| } |
| tok := &oauth2.Token{ |
| AccessToken: res.AccessToken, |
| TokenType: res.TokenType, |
| Expiry: time.Now().Add(time.Duration(res.ExpiresInSec) * time.Second), |
| } |
| // NOTE(cbro): add hidden metadata about where the token is from. |
| // This is needed for detection by client libraries to know that credentials come from the metadata server. |
| // This may be removed in a future version of this library. |
| return tok.WithExtra(map[string]interface{}{ |
| "oauth2.google.tokenSource": "compute-metadata", |
| "oauth2.google.serviceAccount": acct, |
| }), nil |
| } |