mirror of
https://github.com/golang/oauth2.git
synced 2025-07-21 00:00:09 +08:00
With https://go.dev/issue/61417 implemented, we can use the token type directly to unmarshal the JSON fields for the wire format. While here, remove all uses of the deprecated ioutil package as suggested by gopls while making these changes. Change-Id: I79d82374643007a21b5b3d9a8117bed81273eca5 Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/614415 Reviewed-by: Sean Liao <sean@liao.dev> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Dmitri Shuralyov <dmitshur@google.com> Reviewed-by: Michael Pratt <mpratt@google.com>
309 lines
11 KiB
Go
309 lines
11 KiB
Go
// 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/externalaccount"
|
|
"golang.org/x/oauth2/google/internal/externalaccountauthorizeduser"
|
|
"golang.org/x/oauth2/google/internal/impersonate"
|
|
"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 externalaccount.NewTokenSource(ctx, *cfg)
|
|
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 := impersonate.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 {
|
|
// refresh 3 minutes and 45 seconds early. The shortest MDS cache is currently 4 minutes, so any
|
|
// refreshes earlier are a waste of compute.
|
|
earlyExpirySecs := 225 * time.Second
|
|
return computeTokenSource(account, earlyExpirySecs, 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 oauth2.Token
|
|
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.ExpiresIn == 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.ExpiresIn) * 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
|
|
}
|