Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
9ed4d33
Draft showing WIP testing approach.
gIthuriel Oct 13, 2020
8ea3602
Tweaked some internal notes
gIthuriel Oct 13, 2020
58b935b
temp
gIthuriel Oct 19, 2020
b838bba
Add testing for ExchangeToken()
gIthuriel Oct 19, 2020
7f24b84
Go fmt
gIthuriel Oct 19, 2020
ddbe2ba
Fixed file names.
gIthuriel Oct 19, 2020
c19137f
Fixed directory structure.
gIthuriel Oct 19, 2020
13049af
Fixed import issues and test validity.
gIthuriel Oct 19, 2020
ed1c7b8
google: add ExchangeToken unit tests
gIthuriel Oct 29, 2020
717b310
google: fix error formatting, add ctx argument
gIthuriel Nov 3, 2020
4bcf502
google: fix more nits and return early in one error case.
gIthuriel Nov 3, 2020
74f89b5
google: update formatting and code structure
gIthuriel Nov 10, 2020
87e4e25
google: update error return format.
gIthuriel Nov 10, 2020
929f793
google: change json decoding layout
gIthuriel Nov 12, 2020
0dc8cd7
google: remove remaining cmp.Diff call
gIthuriel Nov 12, 2020
098ad88
google: add test to ensure optional field works as intended.
gIthuriel Nov 17, 2020
9432311
google: clean up tests and add godoc comments
gIthuriel Nov 18, 2020
c53b048
google: fix spelling and go fmt
gIthuriel Nov 18, 2020
532f2de
Merge pull request #1 from golang/master
gIthuriel Dec 2, 2020
358bcd3
Draft showing WIP testing approach.
gIthuriel Oct 13, 2020
2522fa4
Tweaked some internal notes
gIthuriel Oct 13, 2020
f5cea3e
temp
gIthuriel Oct 19, 2020
9f31b34
Add testing for ExchangeToken()
gIthuriel Oct 19, 2020
3231799
Go fmt
gIthuriel Oct 19, 2020
da03451
Fixed file names.
gIthuriel Oct 19, 2020
cde1045
Fixed directory structure.
gIthuriel Oct 19, 2020
520fe12
Fixed import issues and test validity.
gIthuriel Oct 19, 2020
5688b03
google: add ExchangeToken unit tests
gIthuriel Oct 29, 2020
01ea637
google: fix error formatting, add ctx argument
gIthuriel Nov 3, 2020
801e561
google: fix more nits and return early in one error case.
gIthuriel Nov 3, 2020
5908a54
google: update formatting and code structure
gIthuriel Nov 10, 2020
57d1f92
google: update error return format.
gIthuriel Nov 10, 2020
ccc73e0
google: change json decoding layout
gIthuriel Nov 12, 2020
97d4314
google: remove remaining cmp.Diff call
gIthuriel Nov 12, 2020
b1b1e51
google: add test to ensure optional field works as intended.
gIthuriel Nov 17, 2020
aa7010a
google: clean up tests and add godoc comments
gIthuriel Nov 18, 2020
a17de82
google: fix spelling and go fmt
gIthuriel Nov 18, 2020
3178b87
google: dummy commit to trigger Trybot after rebasing
gIthuriel Dec 2, 2020
1aa066d
google: resolve merge conflict from dummy commit changes
gIthuriel Dec 2, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions google/internal/externalaccount/sts_exchange.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package externalaccount

import (
"context"
"encoding/json"
"fmt"
"golang.org/x/oauth2"
"io"
"net/http"
"net/url"
"strconv"
"strings"
)

// ExchangeToken performs an oauth2 token exchange with the provided endpoint.
// The first 4 fields are all mandatory. headers can be used to pass additional
// headers beyond the bare minimum required by the token exchange. options can
// be used to pass additional JSON-structured options to the remote server.
func ExchangeToken(ctx context.Context, endpoint string, request *STSTokenExchangeRequest, authentication ClientAuthentication, headers http.Header, options map[string]interface{}) (*STSTokenExchangeResponse, error) {

client := oauth2.NewClient(ctx, nil)

data := url.Values{}
data.Set("audience", request.Audience)
data.Set("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange")
data.Set("requested_token_type", "urn:ietf:params:oauth:token-type:access_token")
data.Set("subject_token_type", request.SubjectTokenType)
data.Set("subject_token", request.SubjectToken)
data.Set("scope", strings.Join(request.Scope, " "))
opts, err := json.Marshal(options)
if err != nil {
return nil, fmt.Errorf("oauth2/google: failed to marshal additional options: %v", err)
}
data.Set("options", string(opts))

authentication.InjectAuthentication(data, headers)
encodedData := data.Encode()

req, err := http.NewRequestWithContext(ctx, "POST", endpoint, strings.NewReader(encodedData))
if err != nil {
return nil, fmt.Errorf("oauth2/google: failed to properly build http request: %v", err)

}
for key, list := range headers {
for _, val := range list {
req.Header.Add(key, val)
}
}
req.Header.Add("Content-Length", strconv.Itoa(len(encodedData)))

resp, err := client.Do(req)

if err != nil {
return nil, fmt.Errorf("oauth2/google: invalid response from Secure Token Server: %v", err)
}
defer resp.Body.Close()

bodyJson := json.NewDecoder(io.LimitReader(resp.Body, 1<<20))
var stsResp STSTokenExchangeResponse
err = bodyJson.Decode(&stsResp)
if err != nil {
return nil, fmt.Errorf("oauth2/google: failed to unmarshal response body from Secure Token Server: %v", err)

}

return &stsResp, nil
}

// STSTokenExchangeRequest contains fields necessary to make an oauth2 token exchange.
type STSTokenExchangeRequest struct {
ActingParty struct {
ActorToken string
ActorTokenType string
}
GrantType string
Resource string
Audience string
Scope []string
RequestedTokenType string
SubjectToken string
SubjectTokenType string
}

// STSTokenExchangeResponse is used to decode the remote server response during an oauth2 token exchange.
type STSTokenExchangeResponse struct {
AccessToken string `json:"access_token"`
IssuedTokenType string `json:"issued_token_type"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
Scope string `json:"scope"`
RefreshToken string `json:"refresh_token"`
}
183 changes: 183 additions & 0 deletions google/internal/externalaccount/sts_exchange_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
// Copyright 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package externalaccount

import (
"context"
"encoding/json"
"golang.org/x/oauth2"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"testing"
)

var auth = ClientAuthentication{
AuthStyle: oauth2.AuthStyleInHeader,
ClientID: clientID,
ClientSecret: clientSecret,
}

var tokenRequest = STSTokenExchangeRequest{
ActingParty: struct {
ActorToken string
ActorTokenType string
}{},
GrantType: "urn:ietf:params:oauth:grant-type:token-exchange",
Resource: "",
Audience: "32555940559.apps.googleusercontent.com", //TODO: Make sure audience is correct in this test (might be mismatched)
Scope: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
RequestedTokenType: "urn:ietf:params:oauth:token-type:access_token",
SubjectToken: "Sample.Subject.Token",
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
}

var requestbody = "audience=32555940559.apps.googleusercontent.com&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&options=null&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdevstorage.full_control&subject_token=Sample.Subject.Token&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Ajwt"
var responseBody = `{"access_token":"Sample.Access.Token","issued_token_type":"urn:ietf:params:oauth:token-type:access_token","token_type":"Bearer","expires_in":3600,"scope":"https://www.googleapis.com/auth/cloud-platform"}`
var expectedToken = STSTokenExchangeResponse{
AccessToken: "Sample.Access.Token",
IssuedTokenType: "urn:ietf:params:oauth:token-type:access_token",
TokenType: "Bearer",
ExpiresIn: 3600,
Scope: "https://www.googleapis.com/auth/cloud-platform",
RefreshToken: "",
}

func TestExchangeToken(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
t.Errorf("Unexpected request method, %v is found", r.Method)
}
if r.URL.String() != "/" {
t.Errorf("Unexpected request URL, %v is found", r.URL)
}
if got, want := r.Header.Get("Authorization"), "Basic cmJyZ25vZ25yaG9uZ28zYmk0Z2I5Z2hnOWc6bm90c29zZWNyZXQ="; got != want {
t.Errorf("Unexpected authorization header, got %v, want %v", got, want)
}
if got, want := r.Header.Get("Content-Type"), "application/x-www-form-urlencoded"; got != want {
t.Errorf("Unexpected Content-Type header, got %v, want %v", got, want)
}
body, err := ioutil.ReadAll(r.Body)
if err != nil {
t.Errorf("Failed reading request body: %v.", err)
}
if got, want := string(body), requestbody; got != want {
t.Errorf("Unexpected exchange payload, got %v but want %v", got, want)
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(responseBody))
}))
defer ts.Close()

headers := http.Header{}
headers.Add("Content-Type", "application/x-www-form-urlencoded")

resp, err := ExchangeToken(context.Background(), ts.URL, &tokenRequest, auth, headers, nil)
if err != nil {
t.Fatalf("ExchangeToken failed with error: %v", err)
}

if expectedToken != *resp {
t.Errorf("mismatched messages received by mock server. \nWant: \n%v\n\nGot:\n%v", expectedToken, *resp)
}

}

func TestExchangeToken_Err(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte("what's wrong with this response?"))
}))
defer ts.Close()

headers := http.Header{}
headers.Add("Content-Type", "application/x-www-form-urlencoded")
_, err := ExchangeToken(context.Background(), ts.URL, &tokenRequest, auth, headers, nil)
if err == nil {
t.Errorf("Expected handled error; instead got nil.")
}
}

/* Lean test specifically for options, as the other features are tested earlier. */
type testOpts struct {
First string `json:"first"`
Second string `json:"second"`
}

var optsValues = [][]string{{"foo", "bar"}, {"cat", "pan"}}

func TestExchangeToken_Opts(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
t.Fatalf("Failed reading request body: %v.", err)
}
data, err := url.ParseQuery(string(body))
if err != nil {
t.Fatalf("Failed to parse request body: %v", err)
}
strOpts, ok := data["options"]
if !ok {
t.Errorf("Server didn't recieve an \"options\" field.")
} else if len(strOpts) < 1 {
t.Errorf("\"options\" field has length 0.")
}
var opts map[string]interface{}
err = json.Unmarshal([]byte(strOpts[0]), &opts)
if len(opts) < 2 {
t.Errorf("Too few options received.")
}

val, ok := opts["one"]
if !ok {
t.Errorf("Couldn't find first option parameter.")
} else {
tOpts1, ok := val.(map[string]interface{})
if !ok {
t.Errorf("Failed to assert the first option parameter as type testOpts.")
} else {
if got, want := tOpts1["first"].(string), optsValues[0][0]; got != want {
t.Errorf("First value in first options field is incorrect; got %v but want %v", got, want)
}
if got, want := tOpts1["second"].(string), optsValues[0][1]; got != want {
t.Errorf("Second value in first options field is incorrect; got %v but want %v", got, want)
}
}
}

val2, ok := opts["two"]
if !ok {
t.Errorf("Couldn't find second option parameter.")
} else {
tOpts2, ok := val2.(map[string]interface{})
if !ok {
t.Errorf("Failed to assert the second option parameter as type testOpts.")
} else {
if got, want := tOpts2["first"].(string), optsValues[1][0]; got != want {
t.Errorf("First value in second options field is incorrect; got %v but want %v", got, want)
}
if got, want := tOpts2["second"].(string), optsValues[1][1]; got != want {
t.Errorf("Second value in second options field is incorrect; got %v but want %v", got, want)
}
}
}

// Send a proper reply so that no other errors crop up.
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(responseBody))

}))
defer ts.Close()
headers := http.Header{}
headers.Add("Content-Type", "application/x-www-form-urlencoded")

firstOption := testOpts{optsValues[0][0], optsValues[0][1]}
secondOption := testOpts{optsValues[1][0], optsValues[1][1]}
inputOpts := make(map[string]interface{})
inputOpts["one"] = firstOption
inputOpts["two"] = secondOption
ExchangeToken(context.Background(), ts.URL, &tokenRequest, auth, headers, inputOpts)
}