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 stsexchange
 16
 17import (
 18	"context"
 19	"encoding/base64"
 20	"encoding/json"
 21	"fmt"
 22	"log/slog"
 23	"net/http"
 24	"net/url"
 25	"strconv"
 26	"strings"
 27
 28	"cloud.google.com/go/auth"
 29	"cloud.google.com/go/auth/internal"
 30	"github.com/googleapis/gax-go/v2/internallog"
 31)
 32
 33const (
 34	// GrantType for a sts exchange.
 35	GrantType = "urn:ietf:params:oauth:grant-type:token-exchange"
 36	// TokenType for a sts exchange.
 37	TokenType = "urn:ietf:params:oauth:token-type:access_token"
 38
 39	jwtTokenType = "urn:ietf:params:oauth:token-type:jwt"
 40)
 41
 42// Options stores the configuration for making an sts exchange request.
 43type Options struct {
 44	Client         *http.Client
 45	Logger         *slog.Logger
 46	Endpoint       string
 47	Request        *TokenRequest
 48	Authentication ClientAuthentication
 49	Headers        http.Header
 50	// ExtraOpts are optional fields marshalled into the `options` field of the
 51	// request body.
 52	ExtraOpts    map[string]interface{}
 53	RefreshToken string
 54}
 55
 56// RefreshAccessToken performs the token exchange using a refresh token flow.
 57func RefreshAccessToken(ctx context.Context, opts *Options) (*TokenResponse, error) {
 58	data := url.Values{}
 59	data.Set("grant_type", "refresh_token")
 60	data.Set("refresh_token", opts.RefreshToken)
 61	return doRequest(ctx, opts, data)
 62}
 63
 64// ExchangeToken performs an oauth2 token exchange with the provided endpoint.
 65func ExchangeToken(ctx context.Context, opts *Options) (*TokenResponse, error) {
 66	data := url.Values{}
 67	data.Set("audience", opts.Request.Audience)
 68	data.Set("grant_type", GrantType)
 69	data.Set("requested_token_type", TokenType)
 70	data.Set("subject_token_type", opts.Request.SubjectTokenType)
 71	data.Set("subject_token", opts.Request.SubjectToken)
 72	data.Set("scope", strings.Join(opts.Request.Scope, " "))
 73	if opts.ExtraOpts != nil {
 74		opts, err := json.Marshal(opts.ExtraOpts)
 75		if err != nil {
 76			return nil, fmt.Errorf("credentials: failed to marshal additional options: %w", err)
 77		}
 78		data.Set("options", string(opts))
 79	}
 80	return doRequest(ctx, opts, data)
 81}
 82
 83func doRequest(ctx context.Context, opts *Options, data url.Values) (*TokenResponse, error) {
 84	opts.Authentication.InjectAuthentication(data, opts.Headers)
 85	encodedData := data.Encode()
 86	logger := internallog.New(opts.Logger)
 87
 88	req, err := http.NewRequestWithContext(ctx, "POST", opts.Endpoint, strings.NewReader(encodedData))
 89	if err != nil {
 90		return nil, fmt.Errorf("credentials: failed to properly build http request: %w", err)
 91
 92	}
 93	for key, list := range opts.Headers {
 94		for _, val := range list {
 95			req.Header.Add(key, val)
 96		}
 97	}
 98	req.Header.Set("Content-Length", strconv.Itoa(len(encodedData)))
 99
100	logger.DebugContext(ctx, "sts token request", "request", internallog.HTTPRequest(req, []byte(encodedData)))
101	resp, body, err := internal.DoRequest(opts.Client, req)
102	if err != nil {
103		return nil, fmt.Errorf("credentials: invalid response from Secure Token Server: %w", err)
104	}
105	logger.DebugContext(ctx, "sts token response", "response", internallog.HTTPResponse(resp, body))
106	if c := resp.StatusCode; c < http.StatusOK || c > http.StatusMultipleChoices {
107		return nil, fmt.Errorf("credentials: status code %d: %s", c, body)
108	}
109	var stsResp TokenResponse
110	if err := json.Unmarshal(body, &stsResp); err != nil {
111		return nil, fmt.Errorf("credentials: failed to unmarshal response body from Secure Token Server: %w", err)
112	}
113
114	return &stsResp, nil
115}
116
117// TokenRequest contains fields necessary to make an oauth2 token
118// exchange.
119type TokenRequest struct {
120	ActingParty struct {
121		ActorToken     string
122		ActorTokenType string
123	}
124	GrantType          string
125	Resource           string
126	Audience           string
127	Scope              []string
128	RequestedTokenType string
129	SubjectToken       string
130	SubjectTokenType   string
131}
132
133// TokenResponse is used to decode the remote server response during
134// an oauth2 token exchange.
135type TokenResponse struct {
136	AccessToken     string `json:"access_token"`
137	IssuedTokenType string `json:"issued_token_type"`
138	TokenType       string `json:"token_type"`
139	ExpiresIn       int    `json:"expires_in"`
140	Scope           string `json:"scope"`
141	RefreshToken    string `json:"refresh_token"`
142}
143
144// ClientAuthentication represents an OAuth client ID and secret and the
145// mechanism for passing these credentials as stated in rfc6749#2.3.1.
146type ClientAuthentication struct {
147	AuthStyle    auth.Style
148	ClientID     string
149	ClientSecret string
150}
151
152// InjectAuthentication is used to add authentication to a Secure Token Service
153// exchange request.  It modifies either the passed url.Values or http.Header
154// depending on the desired authentication format.
155func (c *ClientAuthentication) InjectAuthentication(values url.Values, headers http.Header) {
156	if c.ClientID == "" || c.ClientSecret == "" || values == nil || headers == nil {
157		return
158	}
159	switch c.AuthStyle {
160	case auth.StyleInHeader:
161		plainHeader := c.ClientID + ":" + c.ClientSecret
162		headers.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(plainHeader)))
163	default:
164		values.Set("client_id", c.ClientID)
165		values.Set("client_secret", c.ClientSecret)
166	}
167}