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