1// Copyright 2020 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package externalaccount
6
7import (
8	"context"
9	"fmt"
10	"golang.org/x/oauth2"
11	"net/http"
12	"strconv"
13	"time"
14)
15
16// now aliases time.Now for testing
17var now = func() time.Time {
18	return time.Now().UTC()
19}
20
21// Config stores the configuration for fetching tokens with external credentials.
22type Config struct {
23	Audience                       string
24	SubjectTokenType               string
25	TokenURL                       string
26	TokenInfoURL                   string
27	ServiceAccountImpersonationURL string
28	ClientSecret                   string
29	ClientID                       string
30	CredentialSource               CredentialSource
31	QuotaProjectID                 string
32	Scopes                         []string
33}
34
35// TokenSource Returns an external account TokenSource struct. This is to be called by package google to construct a google.Credentials.
36func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource {
37	ts := tokenSource{
38		ctx:  ctx,
39		conf: c,
40	}
41	if c.ServiceAccountImpersonationURL == "" {
42		return oauth2.ReuseTokenSource(nil, ts)
43	}
44	scopes := c.Scopes
45	ts.conf.Scopes = []string{"https://www.googleapis.com/auth/cloud-platform"}
46	imp := impersonateTokenSource{
47		ctx:    ctx,
48		url:    c.ServiceAccountImpersonationURL,
49		scopes: scopes,
50		ts:     oauth2.ReuseTokenSource(nil, ts),
51	}
52	return oauth2.ReuseTokenSource(nil, imp)
53}
54
55// Subject token file types.
56const (
57	fileTypeText = "text"
58	fileTypeJSON = "json"
59)
60
61type format struct {
62	// Type is either "text" or "json".  When not provided "text" type is assumed.
63	Type string `json:"type"`
64	// SubjectTokenFieldName is only required for JSON format.  This would be "access_token" for azure.
65	SubjectTokenFieldName string `json:"subject_token_field_name"`
66}
67
68// CredentialSource stores the information necessary to retrieve the credentials for the STS exchange.
69type CredentialSource struct {
70	File string `json:"file"`
71
72	URL     string            `json:"url"`
73	Headers map[string]string `json:"headers"`
74
75	EnvironmentID               string `json:"environment_id"`
76	RegionURL                   string `json:"region_url"`
77	RegionalCredVerificationURL string `json:"regional_cred_verification_url"`
78	CredVerificationURL         string `json:"cred_verification_url"`
79	Format                      format `json:"format"`
80}
81
82// parse determines the type of CredentialSource needed
83func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) {
84	if len(c.CredentialSource.EnvironmentID) > 3 && c.CredentialSource.EnvironmentID[:3] == "aws" {
85		if awsVersion, err := strconv.Atoi(c.CredentialSource.EnvironmentID[3:]); err == nil {
86			if awsVersion != 1 {
87				return nil, fmt.Errorf("oauth2/google: aws version '%d' is not supported in the current build", awsVersion)
88			}
89			return awsCredentialSource{
90				EnvironmentID:               c.CredentialSource.EnvironmentID,
91				RegionURL:                   c.CredentialSource.RegionURL,
92				RegionalCredVerificationURL: c.CredentialSource.RegionalCredVerificationURL,
93				CredVerificationURL:         c.CredentialSource.URL,
94				TargetResource:              c.Audience,
95				ctx:                         ctx,
96			}, nil
97		}
98	} else if c.CredentialSource.File != "" {
99		return fileCredentialSource{File: c.CredentialSource.File, Format: c.CredentialSource.Format}, nil
100	} else if c.CredentialSource.URL != "" {
101		return urlCredentialSource{URL: c.CredentialSource.URL, Headers: c.CredentialSource.Headers, Format: c.CredentialSource.Format, ctx: ctx}, nil
102	}
103	return nil, fmt.Errorf("oauth2/google: unable to parse credential source")
104}
105
106type baseCredentialSource interface {
107	subjectToken() (string, error)
108}
109
110// tokenSource is the source that handles external credentials.
111type tokenSource struct {
112	ctx  context.Context
113	conf *Config
114}
115
116// Token allows tokenSource to conform to the oauth2.TokenSource interface.
117func (ts tokenSource) Token() (*oauth2.Token, error) {
118	conf := ts.conf
119
120	credSource, err := conf.parse(ts.ctx)
121	if err != nil {
122		return nil, err
123	}
124	subjectToken, err := credSource.subjectToken()
125
126	if err != nil {
127		return nil, err
128	}
129	stsRequest := stsTokenExchangeRequest{
130		GrantType:          "urn:ietf:params:oauth:grant-type:token-exchange",
131		Audience:           conf.Audience,
132		Scope:              conf.Scopes,
133		RequestedTokenType: "urn:ietf:params:oauth:token-type:access_token",
134		SubjectToken:       subjectToken,
135		SubjectTokenType:   conf.SubjectTokenType,
136	}
137	header := make(http.Header)
138	header.Add("Content-Type", "application/x-www-form-urlencoded")
139	clientAuth := clientAuthentication{
140		AuthStyle:    oauth2.AuthStyleInHeader,
141		ClientID:     conf.ClientID,
142		ClientSecret: conf.ClientSecret,
143	}
144	stsResp, err := exchangeToken(ts.ctx, conf.TokenURL, &stsRequest, clientAuth, header, nil)
145	if err != nil {
146		return nil, err
147	}
148
149	accessToken := &oauth2.Token{
150		AccessToken: stsResp.AccessToken,
151		TokenType:   stsResp.TokenType,
152	}
153	if stsResp.ExpiresIn < 0 {
154		return nil, fmt.Errorf("oauth2/google: got invalid expiry from security token service")
155	} else if stsResp.ExpiresIn >= 0 {
156		accessToken.Expiry = now().Add(time.Duration(stsResp.ExpiresIn) * time.Second)
157	}
158
159	if stsResp.RefreshToken != "" {
160		accessToken.RefreshToken = stsResp.RefreshToken
161	}
162	return accessToken, nil
163}
164