golang.org/x/oauth2@v0.18.0/google/google.go (about) 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 }