externalaccountuser.go raw

   1  // Copyright 2023 Google LLC
   2  //
   3  // Licensed under the Apache License, Version 2.0 (the "License");
   4  // you may not use this file except in compliance with the License.
   5  // You may obtain a copy of the License at
   6  //
   7  //      http://www.apache.org/licenses/LICENSE-2.0
   8  //
   9  // Unless required by applicable law or agreed to in writing, software
  10  // distributed under the License is distributed on an "AS IS" BASIS,
  11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12  // See the License for the specific language governing permissions and
  13  // limitations under the License.
  14  
  15  package externalaccountuser
  16  
  17  import (
  18  	"context"
  19  	"errors"
  20  	"log/slog"
  21  	"net/http"
  22  	"time"
  23  
  24  	"cloud.google.com/go/auth"
  25  	"cloud.google.com/go/auth/credentials/internal/stsexchange"
  26  	"cloud.google.com/go/auth/internal"
  27  	"github.com/googleapis/gax-go/v2/internallog"
  28  )
  29  
  30  // Options stores the configuration for fetching tokens with external authorized
  31  // user credentials.
  32  type Options struct {
  33  	// Audience is the Secure Token Service (STS) audience which contains the
  34  	// resource name for the workforce pool and the provider identifier in that
  35  	// pool.
  36  	Audience string
  37  	// RefreshToken is the OAuth 2.0 refresh token.
  38  	RefreshToken string
  39  	// TokenURL is the STS token exchange endpoint for refresh.
  40  	TokenURL string
  41  	// TokenInfoURL is the STS endpoint URL for token introspection. Optional.
  42  	TokenInfoURL string
  43  	// ClientID is only required in conjunction with ClientSecret, as described
  44  	// below.
  45  	ClientID string
  46  	// ClientSecret is currently only required if token_info endpoint also needs
  47  	// to be called with the generated a cloud access token. When provided, STS
  48  	// will be called with additional basic authentication using client_id as
  49  	// username and client_secret as password.
  50  	ClientSecret string
  51  	// Scopes contains the desired scopes for the returned access token.
  52  	Scopes []string
  53  
  54  	// Client for token request.
  55  	Client *http.Client
  56  	// Logger for logging.
  57  	Logger *slog.Logger
  58  }
  59  
  60  func (c *Options) validate() bool {
  61  	return c.ClientID != "" && c.ClientSecret != "" && c.RefreshToken != "" && c.TokenURL != ""
  62  }
  63  
  64  // NewTokenProvider returns a [cloud.google.com/go/auth.TokenProvider]
  65  // configured with the provided options.
  66  func NewTokenProvider(opts *Options) (auth.TokenProvider, error) {
  67  	if !opts.validate() {
  68  		return nil, errors.New("credentials: invalid external_account_authorized_user configuration")
  69  	}
  70  
  71  	tp := &tokenProvider{
  72  		o: opts,
  73  	}
  74  	return auth.NewCachedTokenProvider(tp, nil), nil
  75  }
  76  
  77  type tokenProvider struct {
  78  	o *Options
  79  }
  80  
  81  func (tp *tokenProvider) Token(ctx context.Context) (*auth.Token, error) {
  82  	opts := tp.o
  83  
  84  	clientAuth := stsexchange.ClientAuthentication{
  85  		AuthStyle:    auth.StyleInHeader,
  86  		ClientID:     opts.ClientID,
  87  		ClientSecret: opts.ClientSecret,
  88  	}
  89  	headers := make(http.Header)
  90  	headers.Set("Content-Type", "application/x-www-form-urlencoded")
  91  	stsResponse, err := stsexchange.RefreshAccessToken(ctx, &stsexchange.Options{
  92  		Client:         opts.Client,
  93  		Endpoint:       opts.TokenURL,
  94  		RefreshToken:   opts.RefreshToken,
  95  		Authentication: clientAuth,
  96  		Headers:        headers,
  97  		Logger:         internallog.New(tp.o.Logger),
  98  	})
  99  	if err != nil {
 100  		return nil, err
 101  	}
 102  	if stsResponse.ExpiresIn < 0 {
 103  		return nil, errors.New("credentials: invalid expiry from security token service")
 104  	}
 105  
 106  	// guarded by the wrapping with CachedTokenProvider
 107  	if stsResponse.RefreshToken != "" {
 108  		opts.RefreshToken = stsResponse.RefreshToken
 109  	}
 110  	return &auth.Token{
 111  		Value:  stsResponse.AccessToken,
 112  		Expiry: time.Now().UTC().Add(time.Duration(stsResponse.ExpiresIn) * time.Second),
 113  		Type:   internal.TokenTypeBearer,
 114  	}, nil
 115  }
 116