...

Source file src/google.golang.org/api/idtoken/idtoken.go

Documentation: google.golang.org/api/idtoken

     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  
     5  package idtoken
     6  
     7  import (
     8  	"context"
     9  	"encoding/json"
    10  	"fmt"
    11  	"net/http"
    12  	"path/filepath"
    13  	"strings"
    14  
    15  	"cloud.google.com/go/compute/metadata"
    16  	"golang.org/x/oauth2"
    17  	"golang.org/x/oauth2/google"
    18  
    19  	newidtoken "cloud.google.com/go/auth/credentials/idtoken"
    20  	"cloud.google.com/go/auth/oauth2adapt"
    21  	"google.golang.org/api/impersonate"
    22  	"google.golang.org/api/internal"
    23  	"google.golang.org/api/option"
    24  	"google.golang.org/api/option/internaloption"
    25  	htransport "google.golang.org/api/transport/http"
    26  )
    27  
    28  // ClientOption is aliased so relevant options are easily found in the docs.
    29  
    30  // ClientOption is for configuring a Google API client or transport.
    31  type ClientOption = option.ClientOption
    32  
    33  type credentialsType int
    34  
    35  const (
    36  	unknownCredType credentialsType = iota
    37  	serviceAccount
    38  	impersonatedServiceAccount
    39  	externalAccount
    40  )
    41  
    42  // NewClient creates a HTTP Client that automatically adds an ID token to each
    43  // request via an Authorization header. The token will have the audience
    44  // provided and be configured with the supplied options. The parameter audience
    45  // may not be empty.
    46  func NewClient(ctx context.Context, audience string, opts ...ClientOption) (*http.Client, error) {
    47  	var ds internal.DialSettings
    48  	for _, opt := range opts {
    49  		opt.Apply(&ds)
    50  	}
    51  	if err := ds.Validate(); err != nil {
    52  		return nil, err
    53  	}
    54  	if ds.NoAuth {
    55  		return nil, fmt.Errorf("idtoken: option.WithoutAuthentication not supported")
    56  	}
    57  	if ds.APIKey != "" {
    58  		return nil, fmt.Errorf("idtoken: option.WithAPIKey not supported")
    59  	}
    60  	if ds.TokenSource != nil {
    61  		return nil, fmt.Errorf("idtoken: option.WithTokenSource not supported")
    62  	}
    63  
    64  	ts, err := NewTokenSource(ctx, audience, opts...)
    65  	if err != nil {
    66  		return nil, err
    67  	}
    68  	// Skip DialSettings validation so added TokenSource will not conflict with user
    69  	// provided credentials.
    70  	opts = append(opts, option.WithTokenSource(ts), internaloption.SkipDialSettingsValidation())
    71  	httpTransport := http.DefaultTransport.(*http.Transport).Clone()
    72  	httpTransport.MaxIdleConnsPerHost = 100
    73  	t, err := htransport.NewTransport(ctx, httpTransport, opts...)
    74  	if err != nil {
    75  		return nil, err
    76  	}
    77  	return &http.Client{Transport: t}, nil
    78  }
    79  
    80  // NewTokenSource creates a TokenSource that returns ID tokens with the audience
    81  // provided and configured with the supplied options. The parameter audience may
    82  // not be empty.
    83  func NewTokenSource(ctx context.Context, audience string, opts ...ClientOption) (oauth2.TokenSource, error) {
    84  	if audience == "" {
    85  		return nil, fmt.Errorf("idtoken: must supply a non-empty audience")
    86  	}
    87  	var ds internal.DialSettings
    88  	for _, opt := range opts {
    89  		opt.Apply(&ds)
    90  	}
    91  	if err := ds.Validate(); err != nil {
    92  		return nil, err
    93  	}
    94  	if ds.TokenSource != nil {
    95  		return nil, fmt.Errorf("idtoken: option.WithTokenSource not supported")
    96  	}
    97  	if ds.ImpersonationConfig != nil {
    98  		return nil, fmt.Errorf("idtoken: option.WithImpersonatedCredentials not supported")
    99  	}
   100  	if ds.IsNewAuthLibraryEnabled() {
   101  		return newTokenSourceNewAuth(ctx, audience, &ds)
   102  	}
   103  	return newTokenSource(ctx, audience, &ds)
   104  }
   105  
   106  func newTokenSourceNewAuth(ctx context.Context, audience string, ds *internal.DialSettings) (oauth2.TokenSource, error) {
   107  	if ds.AuthCredentials != nil {
   108  		return nil, fmt.Errorf("idtoken: option.WithTokenProvider not supported")
   109  	}
   110  	creds, err := newidtoken.NewCredentials(&newidtoken.Options{
   111  		Audience:        audience,
   112  		CustomClaims:    ds.CustomClaims,
   113  		CredentialsFile: ds.CredentialsFile,
   114  		CredentialsJSON: ds.CredentialsJSON,
   115  		Client:          oauth2.NewClient(ctx, nil),
   116  	})
   117  	if err != nil {
   118  		return nil, err
   119  	}
   120  	return oauth2adapt.TokenSourceFromTokenProvider(creds), nil
   121  }
   122  
   123  func newTokenSource(ctx context.Context, audience string, ds *internal.DialSettings) (oauth2.TokenSource, error) {
   124  	creds, err := internal.Creds(ctx, ds)
   125  	if err != nil {
   126  		return nil, err
   127  	}
   128  	if len(creds.JSON) > 0 {
   129  		return tokenSourceFromBytes(ctx, creds.JSON, audience, ds)
   130  	}
   131  	// If internal.Creds did not return a response with JSON fallback to the
   132  	// metadata service as the creds.TokenSource is not an ID token.
   133  	if metadata.OnGCE() {
   134  		return computeTokenSource(audience, ds)
   135  	}
   136  	return nil, fmt.Errorf("idtoken: couldn't find any credentials")
   137  }
   138  
   139  func tokenSourceFromBytes(ctx context.Context, data []byte, audience string, ds *internal.DialSettings) (oauth2.TokenSource, error) {
   140  	allowedType, err := getAllowedType(data)
   141  	if err != nil {
   142  		return nil, err
   143  	}
   144  	switch allowedType {
   145  	case serviceAccount:
   146  		cfg, err := google.JWTConfigFromJSON(data, ds.GetScopes()...)
   147  		if err != nil {
   148  			return nil, err
   149  		}
   150  		customClaims := ds.CustomClaims
   151  		if customClaims == nil {
   152  			customClaims = make(map[string]interface{})
   153  		}
   154  		customClaims["target_audience"] = audience
   155  
   156  		cfg.PrivateClaims = customClaims
   157  		cfg.UseIDToken = true
   158  
   159  		ts := cfg.TokenSource(ctx)
   160  		tok, err := ts.Token()
   161  		if err != nil {
   162  			return nil, err
   163  		}
   164  		return oauth2.ReuseTokenSource(tok, ts), nil
   165  	case impersonatedServiceAccount, externalAccount:
   166  		type url struct {
   167  			ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"`
   168  		}
   169  		var accountURL *url
   170  		if err := json.Unmarshal(data, &accountURL); err != nil {
   171  			return nil, err
   172  		}
   173  		account := filepath.Base(accountURL.ServiceAccountImpersonationURL)
   174  		account = strings.Split(account, ":")[0]
   175  
   176  		config := impersonate.IDTokenConfig{
   177  			Audience:        audience,
   178  			TargetPrincipal: account,
   179  			IncludeEmail:    true,
   180  		}
   181  		ts, err := impersonate.IDTokenSource(ctx, config, option.WithCredentialsJSON(data))
   182  		if err != nil {
   183  			return nil, err
   184  		}
   185  		return ts, nil
   186  	default:
   187  		return nil, fmt.Errorf("idtoken: unsupported credentials type")
   188  	}
   189  }
   190  
   191  // getAllowedType returns the credentials type of type credentialsType, and an error.
   192  // allowed types are "service_account" and "impersonated_service_account"
   193  func getAllowedType(data []byte) (credentialsType, error) {
   194  	var t credentialsType
   195  	if len(data) == 0 {
   196  		return t, fmt.Errorf("idtoken: credential provided is 0 bytes")
   197  	}
   198  	var f struct {
   199  		Type string `json:"type"`
   200  	}
   201  	if err := json.Unmarshal(data, &f); err != nil {
   202  		return t, err
   203  	}
   204  	t = parseCredType(f.Type)
   205  	return t, nil
   206  }
   207  
   208  func parseCredType(typeString string) credentialsType {
   209  	switch typeString {
   210  	case "service_account":
   211  		return serviceAccount
   212  	case "impersonated_service_account":
   213  		return impersonatedServiceAccount
   214  	case "external_account":
   215  		return externalAccount
   216  	default:
   217  		return unknownCredType
   218  	}
   219  }
   220  
   221  // WithCustomClaims optionally specifies custom private claims for an ID token.
   222  func WithCustomClaims(customClaims map[string]interface{}) ClientOption {
   223  	return withCustomClaims(customClaims)
   224  }
   225  
   226  type withCustomClaims map[string]interface{}
   227  
   228  func (w withCustomClaims) Apply(o *internal.DialSettings) {
   229  	o.CustomClaims = w
   230  }
   231  
   232  // WithCredentialsFile returns a ClientOption that authenticates
   233  // API calls with the given service account or refresh token JSON
   234  // credentials file.
   235  func WithCredentialsFile(filename string) ClientOption {
   236  	return option.WithCredentialsFile(filename)
   237  }
   238  
   239  // WithCredentialsJSON returns a ClientOption that authenticates
   240  // API calls with the given service account or refresh token JSON
   241  // credentials.
   242  func WithCredentialsJSON(p []byte) ClientOption {
   243  	return option.WithCredentialsJSON(p)
   244  }
   245  
   246  // WithHTTPClient returns a ClientOption that specifies the HTTP client to use
   247  // as the basis of communications. This option may only be used with services
   248  // that support HTTP as their communication transport. When used, the
   249  // WithHTTPClient option takes precedent over all other supplied options.
   250  func WithHTTPClient(client *http.Client) ClientOption {
   251  	return option.WithHTTPClient(client)
   252  }
   253  

View as plain text