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	"encoding/json"
10	"fmt"
11	"io"
12	"io/ioutil"
13	"net/http"
14	"net/url"
15	"strconv"
16	"strings"
17
18	"golang.org/x/oauth2"
19)
20
21// exchangeToken performs an oauth2 token exchange with the provided endpoint.
22// The first 4 fields are all mandatory.  headers can be used to pass additional
23// headers beyond the bare minimum required by the token exchange.  options can
24// be used to pass additional JSON-structured options to the remote server.
25func exchangeToken(ctx context.Context, endpoint string, request *stsTokenExchangeRequest, authentication clientAuthentication, headers http.Header, options map[string]interface{}) (*stsTokenExchangeResponse, error) {
26
27	client := oauth2.NewClient(ctx, nil)
28
29	data := url.Values{}
30	data.Set("audience", request.Audience)
31	data.Set("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange")
32	data.Set("requested_token_type", "urn:ietf:params:oauth:token-type:access_token")
33	data.Set("subject_token_type", request.SubjectTokenType)
34	data.Set("subject_token", request.SubjectToken)
35	data.Set("scope", strings.Join(request.Scope, " "))
36	if options != nil {
37		opts, err := json.Marshal(options)
38		if err != nil {
39			return nil, fmt.Errorf("oauth2/google: failed to marshal additional options: %v", err)
40		}
41		data.Set("options", string(opts))
42	}
43
44	authentication.InjectAuthentication(data, headers)
45	encodedData := data.Encode()
46
47	req, err := http.NewRequest("POST", endpoint, strings.NewReader(encodedData))
48	if err != nil {
49		return nil, fmt.Errorf("oauth2/google: failed to properly build http request: %v", err)
50
51	}
52	req = req.WithContext(ctx)
53	for key, list := range headers {
54		for _, val := range list {
55			req.Header.Add(key, val)
56		}
57	}
58	req.Header.Add("Content-Length", strconv.Itoa(len(encodedData)))
59
60	resp, err := client.Do(req)
61
62	if err != nil {
63		return nil, fmt.Errorf("oauth2/google: invalid response from Secure Token Server: %v", err)
64	}
65	defer resp.Body.Close()
66
67	body, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20))
68	if c := resp.StatusCode; c < 200 || c > 299 {
69		return nil, fmt.Errorf("oauth2/google: status code %d: %s", c, body)
70	}
71	var stsResp stsTokenExchangeResponse
72	err = json.Unmarshal(body, &stsResp)
73	if err != nil {
74		return nil, fmt.Errorf("oauth2/google: failed to unmarshal response body from Secure Token Server: %v", err)
75
76	}
77
78	return &stsResp, nil
79}
80
81// stsTokenExchangeRequest contains fields necessary to make an oauth2 token exchange.
82type stsTokenExchangeRequest struct {
83	ActingParty struct {
84		ActorToken     string
85		ActorTokenType string
86	}
87	GrantType          string
88	Resource           string
89	Audience           string
90	Scope              []string
91	RequestedTokenType string
92	SubjectToken       string
93	SubjectTokenType   string
94}
95
96// stsTokenExchangeResponse is used to decode the remote server response during an oauth2 token exchange.
97type stsTokenExchangeResponse struct {
98	AccessToken     string `json:"access_token"`
99	IssuedTokenType string `json:"issued_token_type"`
100	TokenType       string `json:"token_type"`
101	ExpiresIn       int    `json:"expires_in"`
102	Scope           string `json:"scope"`
103	RefreshToken    string `json:"refresh_token"`
104}
105