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 5package google 6 7import ( 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/internal/externalaccount" 19 "golang.org/x/oauth2/jwt" 20) 21 22// Endpoint is Google's OAuth 2.0 default endpoint. 23var Endpoint = oauth2.Endpoint{ 24 AuthURL: "https://accounts.google.com/o/oauth2/auth", 25 TokenURL: "https://oauth2.googleapis.com/token", 26 AuthStyle: oauth2.AuthStyleInParams, 27} 28 29// JWTTokenURL is Google's OAuth 2.0 token URL to use with the JWT flow. 30const JWTTokenURL = "https://oauth2.googleapis.com/token" 31 32// ConfigFromJSON uses a Google Developers Console client_credentials.json 33// file to construct a config. 34// client_credentials.json can be downloaded from 35// https://console.developers.google.com, under "Credentials". Download the Web 36// application credentials in the JSON format and provide the contents of the 37// file as jsonKey. 38func ConfigFromJSON(jsonKey []byte, scope ...string) (*oauth2.Config, error) { 39 type cred struct { 40 ClientID string `json:"client_id"` 41 ClientSecret string `json:"client_secret"` 42 RedirectURIs []string `json:"redirect_uris"` 43 AuthURI string `json:"auth_uri"` 44 TokenURI string `json:"token_uri"` 45 } 46 var j struct { 47 Web *cred `json:"web"` 48 Installed *cred `json:"installed"` 49 } 50 if err := json.Unmarshal(jsonKey, &j); err != nil { 51 return nil, err 52 } 53 var c *cred 54 switch { 55 case j.Web != nil: 56 c = j.Web 57 case j.Installed != nil: 58 c = j.Installed 59 default: 60 return nil, fmt.Errorf("oauth2/google: no credentials found") 61 } 62 if len(c.RedirectURIs) < 1 { 63 return nil, errors.New("oauth2/google: missing redirect URL in the client_credentials.json") 64 } 65 return &oauth2.Config{ 66 ClientID: c.ClientID, 67 ClientSecret: c.ClientSecret, 68 RedirectURL: c.RedirectURIs[0], 69 Scopes: scope, 70 Endpoint: oauth2.Endpoint{ 71 AuthURL: c.AuthURI, 72 TokenURL: c.TokenURI, 73 }, 74 }, nil 75} 76 77// JWTConfigFromJSON uses a Google Developers service account JSON key file to read 78// the credentials that authorize and authenticate the requests. 79// Create a service account on "Credentials" for your project at 80// https://console.developers.google.com to download a JSON key file. 81func JWTConfigFromJSON(jsonKey []byte, scope ...string) (*jwt.Config, error) { 82 var f credentialsFile 83 if err := json.Unmarshal(jsonKey, &f); err != nil { 84 return nil, err 85 } 86 if f.Type != serviceAccountKey { 87 return nil, fmt.Errorf("google: read JWT from JSON credentials: 'type' field is %q (expected %q)", f.Type, serviceAccountKey) 88 } 89 scope = append([]string(nil), scope...) // copy 90 return f.jwtConfig(scope, ""), nil 91} 92 93// JSON key file types. 94const ( 95 serviceAccountKey = "service_account" 96 userCredentialsKey = "authorized_user" 97 externalAccountKey = "external_account" 98) 99 100// credentialsFile is the unmarshalled representation of a credentials file. 101type credentialsFile struct { 102 Type string `json:"type"` 103 104 // Service Account fields 105 ClientEmail string `json:"client_email"` 106 PrivateKeyID string `json:"private_key_id"` 107 PrivateKey string `json:"private_key"` 108 AuthURL string `json:"auth_uri"` 109 TokenURL string `json:"token_uri"` 110 ProjectID string `json:"project_id"` 111 112 // User Credential fields 113 // (These typically come from gcloud auth.) 114 ClientSecret string `json:"client_secret"` 115 ClientID string `json:"client_id"` 116 RefreshToken string `json:"refresh_token"` 117 118 // External Account fields 119 Audience string `json:"audience"` 120 SubjectTokenType string `json:"subject_token_type"` 121 TokenURLExternal string `json:"token_url"` 122 TokenInfoURL string `json:"token_info_url"` 123 ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"` 124 CredentialSource externalaccount.CredentialSource `json:"credential_source"` 125 QuotaProjectID string `json:"quota_project_id"` 126} 127 128func (f *credentialsFile) jwtConfig(scopes []string, subject string) *jwt.Config { 129 cfg := &jwt.Config{ 130 Email: f.ClientEmail, 131 PrivateKey: []byte(f.PrivateKey), 132 PrivateKeyID: f.PrivateKeyID, 133 Scopes: scopes, 134 TokenURL: f.TokenURL, 135 Subject: subject, // This is the user email to impersonate 136 } 137 if cfg.TokenURL == "" { 138 cfg.TokenURL = JWTTokenURL 139 } 140 return cfg 141} 142 143func (f *credentialsFile) tokenSource(ctx context.Context, params CredentialsParams) (oauth2.TokenSource, error) { 144 switch f.Type { 145 case serviceAccountKey: 146 cfg := f.jwtConfig(params.Scopes, params.Subject) 147 return cfg.TokenSource(ctx), nil 148 case userCredentialsKey: 149 cfg := &oauth2.Config{ 150 ClientID: f.ClientID, 151 ClientSecret: f.ClientSecret, 152 Scopes: params.Scopes, 153 Endpoint: oauth2.Endpoint{ 154 AuthURL: f.AuthURL, 155 TokenURL: f.TokenURL, 156 AuthStyle: oauth2.AuthStyleInParams, 157 }, 158 } 159 if cfg.Endpoint.AuthURL == "" { 160 cfg.Endpoint.AuthURL = Endpoint.AuthURL 161 } 162 if cfg.Endpoint.TokenURL == "" { 163 cfg.Endpoint.TokenURL = Endpoint.TokenURL 164 } 165 tok := &oauth2.Token{RefreshToken: f.RefreshToken} 166 return cfg.TokenSource(ctx, tok), nil 167 case externalAccountKey: 168 cfg := &externalaccount.Config{ 169 Audience: f.Audience, 170 SubjectTokenType: f.SubjectTokenType, 171 TokenURL: f.TokenURLExternal, 172 TokenInfoURL: f.TokenInfoURL, 173 ServiceAccountImpersonationURL: f.ServiceAccountImpersonationURL, 174 ClientSecret: f.ClientSecret, 175 ClientID: f.ClientID, 176 CredentialSource: f.CredentialSource, 177 QuotaProjectID: f.QuotaProjectID, 178 Scopes: params.Scopes, 179 } 180 return cfg.TokenSource(ctx), nil 181 case "": 182 return nil, errors.New("missing 'type' field in credentials") 183 default: 184 return nil, fmt.Errorf("unknown credential type: %q", f.Type) 185 } 186} 187 188// ComputeTokenSource returns a token source that fetches access tokens 189// from Google Compute Engine (GCE)'s metadata server. It's only valid to use 190// this token source if your program is running on a GCE instance. 191// If no account is specified, "default" is used. 192// If no scopes are specified, a set of default scopes are automatically granted. 193// Further information about retrieving access tokens from the GCE metadata 194// server can be found at https://cloud.google.com/compute/docs/authentication. 195func ComputeTokenSource(account string, scope ...string) oauth2.TokenSource { 196 return oauth2.ReuseTokenSource(nil, computeSource{account: account, scopes: scope}) 197} 198 199type computeSource struct { 200 account string 201 scopes []string 202} 203 204func (cs computeSource) Token() (*oauth2.Token, error) { 205 if !metadata.OnGCE() { 206 return nil, errors.New("oauth2/google: can't get a token from the metadata service; not running on GCE") 207 } 208 acct := cs.account 209 if acct == "" { 210 acct = "default" 211 } 212 tokenURI := "instance/service-accounts/" + acct + "/token" 213 if len(cs.scopes) > 0 { 214 v := url.Values{} 215 v.Set("scopes", strings.Join(cs.scopes, ",")) 216 tokenURI = tokenURI + "?" + v.Encode() 217 } 218 tokenJSON, err := metadata.Get(tokenURI) 219 if err != nil { 220 return nil, err 221 } 222 var res struct { 223 AccessToken string `json:"access_token"` 224 ExpiresInSec int `json:"expires_in"` 225 TokenType string `json:"token_type"` 226 } 227 err = json.NewDecoder(strings.NewReader(tokenJSON)).Decode(&res) 228 if err != nil { 229 return nil, fmt.Errorf("oauth2/google: invalid token JSON from metadata: %v", err) 230 } 231 if res.ExpiresInSec == 0 || res.AccessToken == "" { 232 return nil, fmt.Errorf("oauth2/google: incomplete token received from metadata") 233 } 234 tok := &oauth2.Token{ 235 AccessToken: res.AccessToken, 236 TokenType: res.TokenType, 237 Expiry: time.Now().Add(time.Duration(res.ExpiresInSec) * time.Second), 238 } 239 // NOTE(cbro): add hidden metadata about where the token is from. 240 // This is needed for detection by client libraries to know that credentials come from the metadata server. 241 // This may be removed in a future version of this library. 242 return tok.WithExtra(map[string]interface{}{ 243 "oauth2.google.tokenSource": "compute-metadata", 244 "oauth2.google.serviceAccount": acct, 245 }), nil 246} 247