externalaccountuser.go

  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
 15package externalaccountuser
 16
 17import (
 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.
 32type 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
 60func (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.
 66func 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
 77type tokenProvider struct {
 78	o *Options
 79}
 80
 81func (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}