// 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) }