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}