golang.org/x/oauth2@v0.18.0/google/externalaccount/basecredentials_test.go (about) 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 5 package externalaccount 6 7 import ( 8 "context" 9 "fmt" 10 "io/ioutil" 11 "net/http" 12 "net/http/httptest" 13 "testing" 14 "time" 15 16 "golang.org/x/oauth2" 17 ) 18 19 const ( 20 textBaseCredPath = "testdata/3pi_cred.txt" 21 jsonBaseCredPath = "testdata/3pi_cred.json" 22 baseImpersonateCredsReqBody = "audience=32555940559.apps.googleusercontent.com&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcloud-platform&subject_token=street123&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Ajwt" 23 baseImpersonateCredsRespBody = `{"accessToken":"Second.Access.Token","expireTime":"2020-12-28T15:01:23Z"}` 24 ) 25 26 var testBaseCredSource = CredentialSource{ 27 File: textBaseCredPath, 28 Format: Format{Type: fileTypeText}, 29 } 30 31 var testConfig = Config{ 32 Audience: "32555940559.apps.googleusercontent.com", 33 SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", 34 TokenInfoURL: "http://localhost:8080/v1/tokeninfo", 35 ClientSecret: "notsosecret", 36 ClientID: "rbrgnognrhongo3bi4gb9ghg9g", 37 CredentialSource: &testBaseCredSource, 38 Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"}, 39 } 40 41 var ( 42 baseCredsRequestBody = "audience=32555940559.apps.googleusercontent.com&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdevstorage.full_control&subject_token=street123&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aid_token" 43 baseCredsResponseBody = `{"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"}` 44 workforcePoolRequestBodyWithClientId = "audience=%2F%2Fiam.googleapis.com%2Flocations%2Feu%2FworkforcePools%2Fpool-id%2Fproviders%2Fprovider-id&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdevstorage.full_control&subject_token=street123&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aid_token" 45 workforcePoolRequestBodyWithoutClientId = "audience=%2F%2Fiam.googleapis.com%2Flocations%2Feu%2FworkforcePools%2Fpool-id%2Fproviders%2Fprovider-id&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&options=%7B%22userProject%22%3A%22myProject%22%7D&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdevstorage.full_control&subject_token=street123&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aid_token" 46 correctAT = "Sample.Access.Token" 47 expiry int64 = 234852 48 ) 49 var ( 50 testNow = func() time.Time { return time.Unix(expiry, 0) } 51 ) 52 53 type testExchangeTokenServer struct { 54 url string 55 authorization string 56 contentType string 57 metricsHeader string 58 body string 59 response string 60 } 61 62 func run(t *testing.T, config *Config, tets *testExchangeTokenServer) (*oauth2.Token, error) { 63 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 64 if got, want := r.URL.String(), tets.url; got != want { 65 t.Errorf("URL.String(): got %v but want %v", got, want) 66 } 67 headerAuth := r.Header.Get("Authorization") 68 if got, want := headerAuth, tets.authorization; got != want { 69 t.Errorf("got %v but want %v", got, want) 70 } 71 headerContentType := r.Header.Get("Content-Type") 72 if got, want := headerContentType, tets.contentType; got != want { 73 t.Errorf("got %v but want %v", got, want) 74 } 75 headerMetrics := r.Header.Get("x-goog-api-client") 76 if got, want := headerMetrics, tets.metricsHeader; got != want { 77 t.Errorf("got %v but want %v", got, want) 78 } 79 body, err := ioutil.ReadAll(r.Body) 80 if err != nil { 81 t.Fatalf("Failed reading request body: %s.", err) 82 } 83 if got, want := string(body), tets.body; got != want { 84 t.Errorf("Unexpected exchange payload: got %v but want %v", got, want) 85 } 86 w.Header().Set("Content-Type", "application/json") 87 w.Write([]byte(tets.response)) 88 })) 89 defer server.Close() 90 config.TokenURL = server.URL 91 92 oldNow := now 93 defer func() { now = oldNow }() 94 now = testNow 95 96 ts := tokenSource{ 97 ctx: context.Background(), 98 conf: config, 99 } 100 101 return ts.Token() 102 } 103 104 func validateToken(t *testing.T, tok *oauth2.Token) { 105 if got, want := tok.AccessToken, correctAT; got != want { 106 t.Errorf("Unexpected access token: got %v, but wanted %v", got, want) 107 } 108 if got, want := tok.TokenType, "Bearer"; got != want { 109 t.Errorf("Unexpected TokenType: got %v, but wanted %v", got, want) 110 } 111 112 if got, want := tok.Expiry, testNow().Add(time.Duration(3600)*time.Second); got != want { 113 t.Errorf("Unexpected Expiry: got %v, but wanted %v", got, want) 114 } 115 } 116 117 func createImpersonationServer(urlWanted, authWanted, bodyWanted, response string, t *testing.T) *httptest.Server { 118 return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 119 if got, want := r.URL.String(), urlWanted; got != want { 120 t.Errorf("URL.String(): got %v but want %v", got, want) 121 } 122 headerAuth := r.Header.Get("Authorization") 123 if got, want := headerAuth, authWanted; got != want { 124 t.Errorf("got %v but want %v", got, want) 125 } 126 headerContentType := r.Header.Get("Content-Type") 127 if got, want := headerContentType, "application/json"; got != want { 128 t.Errorf("got %v but want %v", got, want) 129 } 130 body, err := ioutil.ReadAll(r.Body) 131 if err != nil { 132 t.Fatalf("Failed reading request body: %v.", err) 133 } 134 if got, want := string(body), bodyWanted; got != want { 135 t.Errorf("Unexpected impersonation payload: got %v but want %v", got, want) 136 } 137 w.Header().Set("Content-Type", "application/json") 138 w.Write([]byte(response)) 139 })) 140 } 141 142 func createTargetServer(metricsHeaderWanted string, t *testing.T) *httptest.Server { 143 return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 144 if got, want := r.URL.String(), "/"; got != want { 145 t.Errorf("URL.String(): got %v but want %v", got, want) 146 } 147 headerAuth := r.Header.Get("Authorization") 148 if got, want := headerAuth, "Basic cmJyZ25vZ25yaG9uZ28zYmk0Z2I5Z2hnOWc6bm90c29zZWNyZXQ="; got != want { 149 t.Errorf("got %v but want %v", got, want) 150 } 151 headerContentType := r.Header.Get("Content-Type") 152 if got, want := headerContentType, "application/x-www-form-urlencoded"; got != want { 153 t.Errorf("got %v but want %v", got, want) 154 } 155 headerMetrics := r.Header.Get("x-goog-api-client") 156 if got, want := headerMetrics, metricsHeaderWanted; got != want { 157 t.Errorf("got %v but want %v", got, want) 158 } 159 body, err := ioutil.ReadAll(r.Body) 160 if err != nil { 161 t.Fatalf("Failed reading request body: %v.", err) 162 } 163 if got, want := string(body), baseImpersonateCredsReqBody; got != want { 164 t.Errorf("Unexpected exchange payload: got %v but want %v", got, want) 165 } 166 w.Header().Set("Content-Type", "application/json") 167 w.Write([]byte(baseCredsResponseBody)) 168 })) 169 } 170 171 func getExpectedMetricsHeader(source string, saImpersonation bool, configLifetime bool) string { 172 return fmt.Sprintf("gl-go/%s auth/unknown google-byoid-sdk source/%s sa-impersonation/%t config-lifetime/%t", goVersion(), source, saImpersonation, configLifetime) 173 } 174 175 func TestToken(t *testing.T) { 176 config := Config{ 177 Audience: "32555940559.apps.googleusercontent.com", 178 SubjectTokenType: "urn:ietf:params:oauth:token-type:id_token", 179 ClientSecret: "notsosecret", 180 ClientID: "rbrgnognrhongo3bi4gb9ghg9g", 181 CredentialSource: &testBaseCredSource, 182 Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"}, 183 } 184 185 server := testExchangeTokenServer{ 186 url: "/", 187 authorization: "Basic cmJyZ25vZ25yaG9uZ28zYmk0Z2I5Z2hnOWc6bm90c29zZWNyZXQ=", 188 contentType: "application/x-www-form-urlencoded", 189 metricsHeader: getExpectedMetricsHeader("file", false, false), 190 body: baseCredsRequestBody, 191 response: baseCredsResponseBody, 192 } 193 194 tok, err := run(t, &config, &server) 195 196 if err != nil { 197 t.Fatalf("Unexpected error: %e", err) 198 } 199 validateToken(t, tok) 200 } 201 202 func TestWorkforcePoolTokenWithClientID(t *testing.T) { 203 config := Config{ 204 Audience: "//iam.googleapis.com/locations/eu/workforcePools/pool-id/providers/provider-id", 205 SubjectTokenType: "urn:ietf:params:oauth:token-type:id_token", 206 ClientSecret: "notsosecret", 207 ClientID: "rbrgnognrhongo3bi4gb9ghg9g", 208 CredentialSource: &testBaseCredSource, 209 Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"}, 210 WorkforcePoolUserProject: "myProject", 211 } 212 213 server := testExchangeTokenServer{ 214 url: "/", 215 authorization: "Basic cmJyZ25vZ25yaG9uZ28zYmk0Z2I5Z2hnOWc6bm90c29zZWNyZXQ=", 216 contentType: "application/x-www-form-urlencoded", 217 metricsHeader: getExpectedMetricsHeader("file", false, false), 218 body: workforcePoolRequestBodyWithClientId, 219 response: baseCredsResponseBody, 220 } 221 222 tok, err := run(t, &config, &server) 223 224 if err != nil { 225 t.Fatalf("Unexpected error: %e", err) 226 } 227 validateToken(t, tok) 228 } 229 230 func TestWorkforcePoolTokenWithoutClientID(t *testing.T) { 231 config := Config{ 232 Audience: "//iam.googleapis.com/locations/eu/workforcePools/pool-id/providers/provider-id", 233 SubjectTokenType: "urn:ietf:params:oauth:token-type:id_token", 234 ClientSecret: "notsosecret", 235 CredentialSource: &testBaseCredSource, 236 Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"}, 237 WorkforcePoolUserProject: "myProject", 238 } 239 240 server := testExchangeTokenServer{ 241 url: "/", 242 authorization: "", 243 contentType: "application/x-www-form-urlencoded", 244 metricsHeader: getExpectedMetricsHeader("file", false, false), 245 body: workforcePoolRequestBodyWithoutClientId, 246 response: baseCredsResponseBody, 247 } 248 249 tok, err := run(t, &config, &server) 250 251 if err != nil { 252 t.Fatalf("Unexpected error: %e", err) 253 } 254 validateToken(t, tok) 255 } 256 257 func TestNonworkforceWithWorkforcePoolUserProject(t *testing.T) { 258 config := Config{ 259 Audience: "32555940559.apps.googleusercontent.com", 260 SubjectTokenType: "urn:ietf:params:oauth:token-type:id_token", 261 TokenURL: "https://sts.googleapis.com", 262 ClientSecret: "notsosecret", 263 ClientID: "rbrgnognrhongo3bi4gb9ghg9g", 264 CredentialSource: &testBaseCredSource, 265 Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"}, 266 WorkforcePoolUserProject: "myProject", 267 } 268 269 _, err := NewTokenSource(context.Background(), config) 270 271 if err == nil { 272 t.Fatalf("Expected error but found none") 273 } 274 if got, want := err.Error(), "oauth2/google/externalaccount: Workforce pool user project should not be set for non-workforce pool credentials"; got != want { 275 t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got) 276 } 277 } 278 279 func TestWorkforcePoolCreation(t *testing.T) { 280 var audienceValidatyTests = []struct { 281 audience string 282 expectSuccess bool 283 }{ 284 {"//iam.googleapis.com/locations/global/workforcePools/pool-id/providers/provider-id", true}, 285 {"//iam.googleapis.com/locations/eu/workforcePools/pool-id/providers/provider-id", true}, 286 {"//iam.googleapis.com/locations/eu/workforcePools/workloadIdentityPools/providers/provider-id", true}, 287 {"identitynamespace:1f12345:my_provider", false}, 288 {"//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/pool-id/providers/provider-id", false}, 289 {"//iam.googleapis.com/projects/123456/locations/eu/workloadIdentityPools/pool-id/providers/provider-id", false}, 290 {"//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/workforcePools/providers/provider-id", false}, 291 {"//iamgoogleapis.com/locations/eu/workforcePools/pool-id/providers/provider-id", false}, 292 {"//iam.googleapiscom/locations/eu/workforcePools/pool-id/providers/provider-id", false}, 293 {"//iam.googleapis.com/locations/workforcePools/pool-id/providers/provider-id", false}, 294 {"//iam.googleapis.com/locations/eu/workforcePool/pool-id/providers/provider-id", false}, 295 {"//iam.googleapis.com/locations//workforcePool/pool-id/providers/provider-id", false}, 296 } 297 298 ctx := context.Background() 299 for _, tt := range audienceValidatyTests { 300 t.Run(" "+tt.audience, func(t *testing.T) { // We prepend a space ahead of the test input when outputting for sake of readability. 301 config := testConfig 302 config.TokenURL = "https://sts.googleapis.com" // Setting the most basic acceptable tokenURL 303 config.ServiceAccountImpersonationURL = "https://iamcredentials.googleapis.com" 304 config.Audience = tt.audience 305 config.WorkforcePoolUserProject = "myProject" 306 _, err := NewTokenSource(ctx, config) 307 308 if tt.expectSuccess && err != nil { 309 t.Errorf("got %v but want nil", err) 310 } else if !tt.expectSuccess && err == nil { 311 t.Errorf("got nil but expected an error") 312 } 313 }) 314 } 315 } 316 317 var impersonationTests = []struct { 318 name string 319 config Config 320 expectedImpersonationBody string 321 expectedMetricsHeader string 322 }{ 323 { 324 name: "Base Impersonation", 325 config: Config{ 326 Audience: "32555940559.apps.googleusercontent.com", 327 SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", 328 TokenInfoURL: "http://localhost:8080/v1/tokeninfo", 329 ClientSecret: "notsosecret", 330 ClientID: "rbrgnognrhongo3bi4gb9ghg9g", 331 CredentialSource: &testBaseCredSource, 332 Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"}, 333 }, 334 expectedImpersonationBody: "{\"lifetime\":\"3600s\",\"scope\":[\"https://www.googleapis.com/auth/devstorage.full_control\"]}", 335 expectedMetricsHeader: getExpectedMetricsHeader("file", true, false), 336 }, 337 { 338 name: "With TokenLifetime Set", 339 config: Config{ 340 Audience: "32555940559.apps.googleusercontent.com", 341 SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", 342 TokenInfoURL: "http://localhost:8080/v1/tokeninfo", 343 ClientSecret: "notsosecret", 344 ClientID: "rbrgnognrhongo3bi4gb9ghg9g", 345 CredentialSource: &testBaseCredSource, 346 Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"}, 347 ServiceAccountImpersonationLifetimeSeconds: 10000, 348 }, 349 expectedImpersonationBody: "{\"lifetime\":\"10000s\",\"scope\":[\"https://www.googleapis.com/auth/devstorage.full_control\"]}", 350 expectedMetricsHeader: getExpectedMetricsHeader("file", true, true), 351 }, 352 } 353 354 func TestImpersonation(t *testing.T) { 355 for _, tt := range impersonationTests { 356 t.Run(tt.name, func(t *testing.T) { 357 testImpersonateConfig := tt.config 358 impersonateServer := createImpersonationServer("/", "Bearer Sample.Access.Token", tt.expectedImpersonationBody, baseImpersonateCredsRespBody, t) 359 defer impersonateServer.Close() 360 testImpersonateConfig.ServiceAccountImpersonationURL = impersonateServer.URL 361 362 targetServer := createTargetServer(tt.expectedMetricsHeader, t) 363 defer targetServer.Close() 364 testImpersonateConfig.TokenURL = targetServer.URL 365 366 ourTS, err := testImpersonateConfig.tokenSource(context.Background(), "http") 367 if err != nil { 368 t.Fatalf("Failed to create TokenSource: %v", err) 369 } 370 371 oldNow := now 372 defer func() { now = oldNow }() 373 now = testNow 374 375 tok, err := ourTS.Token() 376 if err != nil { 377 t.Fatalf("Unexpected error: %e", err) 378 } 379 if got, want := tok.AccessToken, "Second.Access.Token"; got != want { 380 t.Errorf("Unexpected access token: got %v, but wanted %v", got, want) 381 } 382 if got, want := tok.TokenType, "Bearer"; got != want { 383 t.Errorf("Unexpected TokenType: got %v, but wanted %v", got, want) 384 } 385 }) 386 } 387 } 388 389 var newTokenTests = []struct { 390 name string 391 config Config 392 }{ 393 { 394 name: "Missing Audience", 395 config: Config{ 396 SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", 397 TokenInfoURL: "http://localhost:8080/v1/tokeninfo", 398 ClientSecret: "notsosecret", 399 ClientID: "rbrgnognrhongo3bi4gb9ghg9g", 400 CredentialSource: &testBaseCredSource, 401 Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"}, 402 ServiceAccountImpersonationLifetimeSeconds: 10000, 403 }, 404 }, 405 { 406 name: "Missing Subject Token Type", 407 config: Config{ 408 Audience: "32555940559.apps.googleusercontent.com", 409 TokenInfoURL: "http://localhost:8080/v1/tokeninfo", 410 ClientSecret: "notsosecret", 411 ClientID: "rbrgnognrhongo3bi4gb9ghg9g", 412 CredentialSource: &testBaseCredSource, 413 Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"}, 414 ServiceAccountImpersonationLifetimeSeconds: 10000, 415 }, 416 }, 417 { 418 name: "No Cred Source", 419 config: Config{ 420 Audience: "32555940559.apps.googleusercontent.com", 421 SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", 422 TokenInfoURL: "http://localhost:8080/v1/tokeninfo", 423 ClientSecret: "notsosecret", 424 ClientID: "rbrgnognrhongo3bi4gb9ghg9g", 425 Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"}, 426 ServiceAccountImpersonationLifetimeSeconds: 10000, 427 }, 428 }, 429 { 430 name: "Cred Source and Supplier", 431 config: Config{ 432 Audience: "32555940559.apps.googleusercontent.com", 433 SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", 434 TokenInfoURL: "http://localhost:8080/v1/tokeninfo", 435 CredentialSource: &testBaseCredSource, 436 AwsSecurityCredentialsSupplier: testAwsSupplier{}, 437 ClientSecret: "notsosecret", 438 ClientID: "rbrgnognrhongo3bi4gb9ghg9g", 439 Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"}, 440 ServiceAccountImpersonationLifetimeSeconds: 10000, 441 }, 442 }, 443 } 444 445 func TestNewToken(t *testing.T) { 446 for _, tt := range newTokenTests { 447 t.Run(tt.name, func(t *testing.T) { 448 testConfig := tt.config 449 450 _, err := NewTokenSource(context.Background(), testConfig) 451 if err == nil { 452 t.Fatalf("expected error when calling NewToken()") 453 } 454 }) 455 } 456 } 457 458 func TestConfig_TokenURL(t *testing.T) { 459 tests := []struct { 460 tokenURL string 461 universeDomain string 462 want string 463 }{ 464 { 465 tokenURL: "https://sts.googleapis.com/v1/token", 466 universeDomain: "", 467 want: "https://sts.googleapis.com/v1/token", 468 }, 469 { 470 tokenURL: "", 471 universeDomain: "", 472 want: "https://sts.googleapis.com/v1/token", 473 }, 474 { 475 tokenURL: "", 476 universeDomain: "googleapis.com", 477 want: "https://sts.googleapis.com/v1/token", 478 }, 479 { 480 tokenURL: "", 481 universeDomain: "example.com", 482 want: "https://sts.example.com/v1/token", 483 }, 484 } 485 for _, tt := range tests { 486 config := &Config{ 487 Audience: "//iam.googleapis.com/locations/eu/workforcePools/pool-id/providers/provider-id", 488 SubjectTokenType: "urn:ietf:params:oauth:token-type:id_token", 489 CredentialSource: &testBaseCredSource, 490 Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"}, 491 } 492 config.TokenURL = tt.tokenURL 493 config.UniverseDomain = tt.universeDomain 494 config.parse(context.Background()) 495 if got := config.TokenURL; got != tt.want { 496 t.Errorf("got %q, want %q", got, tt.want) 497 } 498 } 499 }