1// Copyright 2020 Google LLC. 2// Use of this source code is governed by a BSD-style 3// license that can be found in the LICENSE file. 4 5package idtoken 6 7import ( 8 "context" 9 "encoding/json" 10 "fmt" 11 "net/http" 12 13 "cloud.google.com/go/compute/metadata" 14 "golang.org/x/oauth2" 15 "golang.org/x/oauth2/google" 16 17 "google.golang.org/api/internal" 18 "google.golang.org/api/option" 19 "google.golang.org/api/option/internaloption" 20 htransport "google.golang.org/api/transport/http" 21) 22 23// ClientOption is aliased so relevant options are easily found in the docs. 24 25// ClientOption is for configuring a Google API client or transport. 26type ClientOption = option.ClientOption 27 28// NewClient creates a HTTP Client that automatically adds an ID token to each 29// request via an Authorization header. The token will have have the audience 30// provided and be configured with the supplied options. The parameter audience 31// may not be empty. 32func NewClient(ctx context.Context, audience string, opts ...ClientOption) (*http.Client, error) { 33 var ds internal.DialSettings 34 for _, opt := range opts { 35 opt.Apply(&ds) 36 } 37 if err := ds.Validate(); err != nil { 38 return nil, err 39 } 40 if ds.NoAuth { 41 return nil, fmt.Errorf("idtoken: option.WithoutAuthentication not supported") 42 } 43 if ds.APIKey != "" { 44 return nil, fmt.Errorf("idtoken: option.WithAPIKey not supported") 45 } 46 if ds.TokenSource != nil { 47 return nil, fmt.Errorf("idtoken: option.WithTokenSource not supported") 48 } 49 50 ts, err := NewTokenSource(ctx, audience, opts...) 51 if err != nil { 52 return nil, err 53 } 54 // Skip DialSettings validation so added TokenSource will not conflict with user 55 // provided credentials. 56 opts = append(opts, option.WithTokenSource(ts), internaloption.SkipDialSettingsValidation()) 57 t, err := htransport.NewTransport(ctx, http.DefaultTransport, opts...) 58 if err != nil { 59 return nil, err 60 } 61 return &http.Client{Transport: t}, nil 62} 63 64// NewTokenSource creates a TokenSource that returns ID tokens with the audience 65// provided and configured with the supplied options. The parameter audience may 66// not be empty. 67func NewTokenSource(ctx context.Context, audience string, opts ...ClientOption) (oauth2.TokenSource, error) { 68 if audience == "" { 69 return nil, fmt.Errorf("idtoken: must supply a non-empty audience") 70 } 71 var ds internal.DialSettings 72 for _, opt := range opts { 73 opt.Apply(&ds) 74 } 75 if err := ds.Validate(); err != nil { 76 return nil, err 77 } 78 if ds.TokenSource != nil { 79 return nil, fmt.Errorf("idtoken: option.WithTokenSource not supported") 80 } 81 if ds.ImpersonationConfig != nil { 82 return nil, fmt.Errorf("idtoken: option.WithImpersonatedCredentials not supported") 83 } 84 return newTokenSource(ctx, audience, &ds) 85} 86 87func newTokenSource(ctx context.Context, audience string, ds *internal.DialSettings) (oauth2.TokenSource, error) { 88 creds, err := internal.Creds(ctx, ds) 89 if err != nil { 90 return nil, err 91 } 92 if len(creds.JSON) > 0 { 93 return tokenSourceFromBytes(ctx, creds.JSON, audience, ds) 94 } 95 // If internal.Creds did not return a response with JSON fallback to the 96 // metadata service as the creds.TokenSource is not an ID token. 97 if metadata.OnGCE() { 98 return computeTokenSource(audience, ds) 99 } 100 return nil, fmt.Errorf("idtoken: couldn't find any credentials") 101} 102 103func tokenSourceFromBytes(ctx context.Context, data []byte, audience string, ds *internal.DialSettings) (oauth2.TokenSource, error) { 104 if err := isServiceAccount(data); err != nil { 105 return nil, err 106 } 107 cfg, err := google.JWTConfigFromJSON(data, ds.GetScopes()...) 108 if err != nil { 109 return nil, err 110 } 111 112 customClaims := ds.CustomClaims 113 if customClaims == nil { 114 customClaims = make(map[string]interface{}) 115 } 116 customClaims["target_audience"] = audience 117 118 cfg.PrivateClaims = customClaims 119 cfg.UseIDToken = true 120 121 ts := cfg.TokenSource(ctx) 122 tok, err := ts.Token() 123 if err != nil { 124 return nil, err 125 } 126 return oauth2.ReuseTokenSource(tok, ts), nil 127} 128 129func isServiceAccount(data []byte) error { 130 if len(data) == 0 { 131 return fmt.Errorf("idtoken: credential provided is 0 bytes") 132 } 133 var f struct { 134 Type string `json:"type"` 135 } 136 if err := json.Unmarshal(data, &f); err != nil { 137 return err 138 } 139 if f.Type != "service_account" { 140 return fmt.Errorf("idtoken: credential must be service_account, found %q", f.Type) 141 } 142 return nil 143} 144 145// WithCustomClaims optionally specifies custom private claims for an ID token. 146func WithCustomClaims(customClaims map[string]interface{}) ClientOption { 147 return withCustomClaims(customClaims) 148} 149 150type withCustomClaims map[string]interface{} 151 152func (w withCustomClaims) Apply(o *internal.DialSettings) { 153 o.CustomClaims = w 154} 155 156// WithCredentialsFile returns a ClientOption that authenticates 157// API calls with the given service account or refresh token JSON 158// credentials file. 159func WithCredentialsFile(filename string) ClientOption { 160 return option.WithCredentialsFile(filename) 161} 162 163// WithCredentialsJSON returns a ClientOption that authenticates 164// API calls with the given service account or refresh token JSON 165// credentials. 166func WithCredentialsJSON(p []byte) ClientOption { 167 return option.WithCredentialsJSON(p) 168} 169 170// WithHTTPClient returns a ClientOption that specifies the HTTP client to use 171// as the basis of communications. This option may only be used with services 172// that support HTTP as their communication transport. When used, the 173// WithHTTPClient option takes precedent over all other supplied options. 174func WithHTTPClient(client *http.Client) ClientOption { 175 return option.WithHTTPClient(client) 176} 177