github.com/argoproj/argo-cd/v2@v2.10.9/util/oidc/oidc_test.go (about) 1 package oidc 2 3 import ( 4 "crypto/tls" 5 "encoding/hex" 6 "encoding/json" 7 "fmt" 8 "net/http" 9 "net/http/httptest" 10 "net/url" 11 "os" 12 "strings" 13 "testing" 14 "time" 15 16 gooidc "github.com/coreos/go-oidc/v3/oidc" 17 "github.com/golang-jwt/jwt/v4" 18 "github.com/stretchr/testify/assert" 19 "github.com/stretchr/testify/require" 20 "golang.org/x/oauth2" 21 22 "github.com/argoproj/argo-cd/v2/common" 23 "github.com/argoproj/argo-cd/v2/server/settings/oidc" 24 "github.com/argoproj/argo-cd/v2/util" 25 "github.com/argoproj/argo-cd/v2/util/cache" 26 "github.com/argoproj/argo-cd/v2/util/crypto" 27 "github.com/argoproj/argo-cd/v2/util/dex" 28 "github.com/argoproj/argo-cd/v2/util/settings" 29 "github.com/argoproj/argo-cd/v2/util/test" 30 ) 31 32 func TestInferGrantType(t *testing.T) { 33 for _, path := range []string{"dex", "okta", "auth0", "onelogin"} { 34 t.Run(path, func(t *testing.T) { 35 rawConfig, err := os.ReadFile("testdata/" + path + ".json") 36 assert.NoError(t, err) 37 var config OIDCConfiguration 38 err = json.Unmarshal(rawConfig, &config) 39 assert.NoError(t, err) 40 grantType := InferGrantType(&config) 41 assert.Equal(t, GrantTypeAuthorizationCode, grantType) 42 43 var noCodeResponseTypes []string 44 for _, supportedResponseType := range config.ResponseTypesSupported { 45 if supportedResponseType != ResponseTypeCode { 46 noCodeResponseTypes = append(noCodeResponseTypes, supportedResponseType) 47 } 48 } 49 50 config.ResponseTypesSupported = noCodeResponseTypes 51 grantType = InferGrantType(&config) 52 assert.Equal(t, GrantTypeImplicit, grantType) 53 }) 54 } 55 } 56 57 func TestIDTokenClaims(t *testing.T) { 58 oauth2Config := &oauth2.Config{ 59 ClientID: "DUMMY_OIDC_PROVIDER", 60 ClientSecret: "0987654321", 61 Endpoint: oauth2.Endpoint{AuthURL: "https://argocd-dev.onelogin.com/oidc/auth", TokenURL: "https://argocd-dev.onelogin.com/oidc/token"}, 62 Scopes: []string{"oidc", "profile", "groups"}, 63 RedirectURL: "https://argocd-dev.io/redirect_url", 64 } 65 66 var opts []oauth2.AuthCodeOption 67 requestedClaims := make(map[string]*oidc.Claim) 68 69 opts = AppendClaimsAuthenticationRequestParameter(opts, requestedClaims) 70 assert.Len(t, opts, 0) 71 72 requestedClaims["groups"] = &oidc.Claim{Essential: true} 73 opts = AppendClaimsAuthenticationRequestParameter(opts, requestedClaims) 74 assert.Len(t, opts, 1) 75 76 authCodeURL, err := url.Parse(oauth2Config.AuthCodeURL("TEST", opts...)) 77 assert.NoError(t, err) 78 79 values, err := url.ParseQuery(authCodeURL.RawQuery) 80 assert.NoError(t, err) 81 82 assert.Equal(t, "{\"id_token\":{\"groups\":{\"essential\":true}}}", values.Get("claims")) 83 } 84 85 type fakeProvider struct { 86 } 87 88 func (p *fakeProvider) Endpoint() (*oauth2.Endpoint, error) { 89 return &oauth2.Endpoint{}, nil 90 } 91 92 func (p *fakeProvider) ParseConfig() (*OIDCConfiguration, error) { 93 return nil, nil 94 } 95 96 func (p *fakeProvider) Verify(_ string, _ *settings.ArgoCDSettings) (*gooidc.IDToken, error) { 97 return nil, nil 98 } 99 100 func TestHandleCallback(t *testing.T) { 101 app := ClientApp{provider: &fakeProvider{}} 102 103 req := httptest.NewRequest(http.MethodGet, "http://example.com/foo", nil) 104 req.Form = url.Values{ 105 "error": []string{"login-failed"}, 106 "error_description": []string{"<script>alert('hello')</script>"}, 107 } 108 w := httptest.NewRecorder() 109 110 app.HandleCallback(w, req) 111 112 assert.Equal(t, "login-failed: <script>alert('hello')</script>\n", w.Body.String()) 113 } 114 115 func TestClientApp_HandleLogin(t *testing.T) { 116 oidcTestServer := test.GetOIDCTestServer(t) 117 t.Cleanup(oidcTestServer.Close) 118 119 dexTestServer := test.GetDexTestServer(t) 120 t.Cleanup(dexTestServer.Close) 121 122 t.Run("oidc certificate checking during login should toggle on config", func(t *testing.T) { 123 cdSettings := &settings.ArgoCDSettings{ 124 URL: "https://argocd.example.com", 125 OIDCConfigRAW: fmt.Sprintf(` 126 name: Test 127 issuer: %s 128 clientID: xxx 129 clientSecret: yyy 130 requestedScopes: ["oidc"]`, oidcTestServer.URL), 131 } 132 app, err := NewClientApp(cdSettings, dexTestServer.URL, nil, "https://argocd.example.com", cache.NewInMemoryCache(24*time.Hour)) 133 require.NoError(t, err) 134 135 req := httptest.NewRequest(http.MethodGet, "https://argocd.example.com/auth/login", nil) 136 137 w := httptest.NewRecorder() 138 139 app.HandleLogin(w, req) 140 141 if !strings.Contains(w.Body.String(), "certificate signed by unknown authority") && !strings.Contains(w.Body.String(), "certificate is not trusted") { 142 t.Fatal("did not receive expected certificate verification failure error") 143 } 144 145 cdSettings.OIDCTLSInsecureSkipVerify = true 146 147 app, err = NewClientApp(cdSettings, dexTestServer.URL, nil, "https://argocd.example.com", cache.NewInMemoryCache(24*time.Hour)) 148 require.NoError(t, err) 149 150 w = httptest.NewRecorder() 151 152 app.HandleLogin(w, req) 153 154 assert.NotContains(t, w.Body.String(), "certificate is not trusted") 155 assert.NotContains(t, w.Body.String(), "certificate signed by unknown authority") 156 }) 157 158 t.Run("dex certificate checking during login should toggle on config", func(t *testing.T) { 159 cdSettings := &settings.ArgoCDSettings{ 160 URL: "https://argocd.example.com", 161 DexConfig: `connectors: 162 - type: github 163 name: GitHub 164 config: 165 clientID: aabbccddeeff00112233 166 clientSecret: aabbccddeeff00112233`, 167 } 168 cert, err := tls.X509KeyPair(test.Cert, test.PrivateKey) 169 require.NoError(t, err) 170 cdSettings.Certificate = &cert 171 172 app, err := NewClientApp(cdSettings, dexTestServer.URL, nil, "https://argocd.example.com", cache.NewInMemoryCache(24*time.Hour)) 173 require.NoError(t, err) 174 175 req := httptest.NewRequest(http.MethodGet, "https://argocd.example.com/auth/login", nil) 176 177 w := httptest.NewRecorder() 178 179 app.HandleLogin(w, req) 180 181 if !strings.Contains(w.Body.String(), "certificate signed by unknown authority") && !strings.Contains(w.Body.String(), "certificate is not trusted") { 182 t.Fatal("did not receive expected certificate verification failure error") 183 } 184 185 app, err = NewClientApp(cdSettings, dexTestServer.URL, &dex.DexTLSConfig{StrictValidation: false}, "https://argocd.example.com", cache.NewInMemoryCache(24*time.Hour)) 186 require.NoError(t, err) 187 188 w = httptest.NewRecorder() 189 190 app.HandleLogin(w, req) 191 192 assert.NotContains(t, w.Body.String(), "certificate is not trusted") 193 assert.NotContains(t, w.Body.String(), "certificate signed by unknown authority") 194 }) 195 } 196 197 func Test_Login_Flow(t *testing.T) { 198 // Show that SSO login works when no redirect URL is provided, and we fall back to the configured base href for the 199 // Argo CD instance. 200 201 oidcTestServer := test.GetOIDCTestServer(t) 202 t.Cleanup(oidcTestServer.Close) 203 204 cdSettings := &settings.ArgoCDSettings{ 205 URL: "https://argocd.example.com", 206 OIDCConfigRAW: fmt.Sprintf(` 207 name: Test 208 issuer: %s 209 clientID: xxx 210 clientSecret: yyy 211 requestedScopes: ["oidc"]`, oidcTestServer.URL), 212 OIDCTLSInsecureSkipVerify: true, 213 } 214 215 // The base href (the last argument for NewClientApp) is what HandleLogin will fall back to when no explicit 216 // redirect URL is given. 217 app, err := NewClientApp(cdSettings, "", nil, "/", cache.NewInMemoryCache(24*time.Hour)) 218 require.NoError(t, err) 219 220 w := httptest.NewRecorder() 221 222 req := httptest.NewRequest(http.MethodGet, "https://argocd.example.com/auth/login", nil) 223 224 app.HandleLogin(w, req) 225 226 redirectUrl, err := w.Result().Location() 227 require.NoError(t, err) 228 229 state := redirectUrl.Query()["state"] 230 231 req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://argocd.example.com/auth/callback?state=%s&code=abc", state), nil) 232 for _, cookie := range w.Result().Cookies() { 233 req.AddCookie(cookie) 234 } 235 236 w = httptest.NewRecorder() 237 238 app.HandleCallback(w, req) 239 240 assert.NotContains(t, w.Body.String(), InvalidRedirectURLError.Error()) 241 } 242 243 func TestClientApp_HandleCallback(t *testing.T) { 244 oidcTestServer := test.GetOIDCTestServer(t) 245 t.Cleanup(oidcTestServer.Close) 246 247 dexTestServer := test.GetDexTestServer(t) 248 t.Cleanup(dexTestServer.Close) 249 250 t.Run("oidc certificate checking during oidc callback should toggle on config", func(t *testing.T) { 251 cdSettings := &settings.ArgoCDSettings{ 252 URL: "https://argocd.example.com", 253 OIDCConfigRAW: fmt.Sprintf(` 254 name: Test 255 issuer: %s 256 clientID: xxx 257 clientSecret: yyy 258 requestedScopes: ["oidc"]`, oidcTestServer.URL), 259 } 260 app, err := NewClientApp(cdSettings, dexTestServer.URL, nil, "https://argocd.example.com", cache.NewInMemoryCache(24*time.Hour)) 261 require.NoError(t, err) 262 263 req := httptest.NewRequest(http.MethodGet, "https://argocd.example.com/auth/callback", nil) 264 265 w := httptest.NewRecorder() 266 267 app.HandleCallback(w, req) 268 269 if !strings.Contains(w.Body.String(), "certificate signed by unknown authority") && !strings.Contains(w.Body.String(), "certificate is not trusted") { 270 t.Fatal("did not receive expected certificate verification failure error") 271 } 272 273 cdSettings.OIDCTLSInsecureSkipVerify = true 274 275 app, err = NewClientApp(cdSettings, dexTestServer.URL, nil, "https://argocd.example.com", cache.NewInMemoryCache(24*time.Hour)) 276 require.NoError(t, err) 277 278 w = httptest.NewRecorder() 279 280 app.HandleCallback(w, req) 281 282 assert.NotContains(t, w.Body.String(), "certificate is not trusted") 283 assert.NotContains(t, w.Body.String(), "certificate signed by unknown authority") 284 }) 285 286 t.Run("dex certificate checking during oidc callback should toggle on config", func(t *testing.T) { 287 cdSettings := &settings.ArgoCDSettings{ 288 URL: "https://argocd.example.com", 289 DexConfig: `connectors: 290 - type: github 291 name: GitHub 292 config: 293 clientID: aabbccddeeff00112233 294 clientSecret: aabbccddeeff00112233`, 295 } 296 cert, err := tls.X509KeyPair(test.Cert, test.PrivateKey) 297 require.NoError(t, err) 298 cdSettings.Certificate = &cert 299 300 app, err := NewClientApp(cdSettings, dexTestServer.URL, nil, "https://argocd.example.com", cache.NewInMemoryCache(24*time.Hour)) 301 require.NoError(t, err) 302 303 req := httptest.NewRequest(http.MethodGet, "https://argocd.example.com/auth/callback", nil) 304 305 w := httptest.NewRecorder() 306 307 app.HandleCallback(w, req) 308 309 if !strings.Contains(w.Body.String(), "certificate signed by unknown authority") && !strings.Contains(w.Body.String(), "certificate is not trusted") { 310 t.Fatal("did not receive expected certificate verification failure error") 311 } 312 313 app, err = NewClientApp(cdSettings, dexTestServer.URL, &dex.DexTLSConfig{StrictValidation: false}, "https://argocd.example.com", cache.NewInMemoryCache(24*time.Hour)) 314 require.NoError(t, err) 315 316 w = httptest.NewRecorder() 317 318 app.HandleCallback(w, req) 319 320 assert.NotContains(t, w.Body.String(), "certificate is not trusted") 321 assert.NotContains(t, w.Body.String(), "certificate signed by unknown authority") 322 }) 323 } 324 325 func TestIsValidRedirect(t *testing.T) { 326 var tests = []struct { 327 name string 328 valid bool 329 redirectURL string 330 allowedURLs []string 331 }{ 332 { 333 name: "Single allowed valid URL", 334 valid: true, 335 redirectURL: "https://localhost:4000", 336 allowedURLs: []string{"https://localhost:4000/"}, 337 }, 338 { 339 name: "Empty URL", 340 valid: true, 341 redirectURL: "", 342 allowedURLs: []string{"https://localhost:4000/"}, 343 }, 344 { 345 name: "Trailing single slash and empty suffix are handled the same", 346 valid: true, 347 redirectURL: "https://localhost:4000/", 348 allowedURLs: []string{"https://localhost:4000"}, 349 }, 350 { 351 name: "Multiple valid URLs with one allowed", 352 valid: true, 353 redirectURL: "https://localhost:4000", 354 allowedURLs: []string{"https://wherever:4000", "https://localhost:4000"}, 355 }, 356 { 357 name: "Multiple valid URLs with none allowed", 358 valid: false, 359 redirectURL: "https://localhost:4000", 360 allowedURLs: []string{"https://wherever:4000", "https://invalid:4000"}, 361 }, 362 { 363 name: "Invalid redirect URL because path prefix does not match", 364 valid: false, 365 redirectURL: "https://localhost:4000/applications", 366 allowedURLs: []string{"https://localhost:4000/argocd"}, 367 }, 368 { 369 name: "Valid redirect URL because prefix matches", 370 valid: true, 371 redirectURL: "https://localhost:4000/argocd/applications", 372 allowedURLs: []string{"https://localhost:4000/argocd"}, 373 }, 374 { 375 name: "Invalid redirect URL because resolved path does not match prefix", 376 valid: false, 377 redirectURL: "https://localhost:4000/argocd/../applications", 378 allowedURLs: []string{"https://localhost:4000/argocd"}, 379 }, 380 { 381 name: "Invalid redirect URL because scheme mismatch", 382 valid: false, 383 redirectURL: "http://localhost:4000", 384 allowedURLs: []string{"https://localhost:4000"}, 385 }, 386 { 387 name: "Invalid redirect URL because port mismatch", 388 valid: false, 389 redirectURL: "https://localhost", 390 allowedURLs: []string{"https://localhost:80"}, 391 }, 392 { 393 name: "Invalid redirect URL because of CRLF in path", 394 valid: false, 395 redirectURL: "https://localhost:80/argocd\r\n", 396 allowedURLs: []string{"https://localhost:80/argocd\r\n"}, 397 }, 398 } 399 400 for _, tt := range tests { 401 t.Run(tt.name, func(t *testing.T) { 402 res := isValidRedirectURL(tt.redirectURL, tt.allowedURLs) 403 assert.Equal(t, res, tt.valid) 404 }) 405 } 406 } 407 408 func TestGenerateAppState(t *testing.T) { 409 signature, err := util.MakeSignature(32) 410 require.NoError(t, err) 411 expectedReturnURL := "http://argocd.example.com/" 412 app, err := NewClientApp(&settings.ArgoCDSettings{ServerSignature: signature, URL: expectedReturnURL}, "", nil, "", cache.NewInMemoryCache(24*time.Hour)) 413 require.NoError(t, err) 414 generateResponse := httptest.NewRecorder() 415 state, err := app.generateAppState(expectedReturnURL, generateResponse) 416 require.NoError(t, err) 417 418 t.Run("VerifyAppState_Successful", func(t *testing.T) { 419 req := httptest.NewRequest(http.MethodGet, "/", nil) 420 for _, cookie := range generateResponse.Result().Cookies() { 421 req.AddCookie(cookie) 422 } 423 424 returnURL, err := app.verifyAppState(req, httptest.NewRecorder(), state) 425 assert.NoError(t, err) 426 assert.Equal(t, expectedReturnURL, returnURL) 427 }) 428 429 t.Run("VerifyAppState_Failed", func(t *testing.T) { 430 req := httptest.NewRequest(http.MethodGet, "/", nil) 431 for _, cookie := range generateResponse.Result().Cookies() { 432 req.AddCookie(cookie) 433 } 434 435 _, err := app.verifyAppState(req, httptest.NewRecorder(), "wrong state") 436 assert.Error(t, err) 437 }) 438 } 439 440 func TestGenerateAppState_XSS(t *testing.T) { 441 signature, err := util.MakeSignature(32) 442 require.NoError(t, err) 443 app, err := NewClientApp( 444 &settings.ArgoCDSettings{ 445 // Only return URLs starting with this base should be allowed. 446 URL: "https://argocd.example.com", 447 ServerSignature: signature, 448 }, 449 "", nil, "", cache.NewInMemoryCache(24*time.Hour), 450 ) 451 require.NoError(t, err) 452 453 t.Run("XSS fails", func(t *testing.T) { 454 // This attack assumes the attacker has compromised the server's secret key. We use `generateAppState` here for 455 // convenience, but an attacker with access to the server secret could write their own code to generate the 456 // malicious cookie. 457 458 expectedReturnURL := "javascript: alert('hi')" 459 generateResponse := httptest.NewRecorder() 460 state, err := app.generateAppState(expectedReturnURL, generateResponse) 461 require.NoError(t, err) 462 463 req := httptest.NewRequest(http.MethodGet, "/", nil) 464 for _, cookie := range generateResponse.Result().Cookies() { 465 req.AddCookie(cookie) 466 } 467 468 returnURL, err := app.verifyAppState(req, httptest.NewRecorder(), state) 469 assert.ErrorIs(t, err, InvalidRedirectURLError) 470 assert.Empty(t, returnURL) 471 }) 472 473 t.Run("valid return URL succeeds", func(t *testing.T) { 474 expectedReturnURL := "https://argocd.example.com/some/path" 475 generateResponse := httptest.NewRecorder() 476 state, err := app.generateAppState(expectedReturnURL, generateResponse) 477 require.NoError(t, err) 478 479 req := httptest.NewRequest(http.MethodGet, "/", nil) 480 for _, cookie := range generateResponse.Result().Cookies() { 481 req.AddCookie(cookie) 482 } 483 484 returnURL, err := app.verifyAppState(req, httptest.NewRecorder(), state) 485 assert.NoError(t, err, InvalidRedirectURLError) 486 assert.Equal(t, expectedReturnURL, returnURL) 487 }) 488 } 489 490 func TestGenerateAppState_NoReturnURL(t *testing.T) { 491 signature, err := util.MakeSignature(32) 492 require.NoError(t, err) 493 cdSettings := &settings.ArgoCDSettings{ServerSignature: signature} 494 key, err := cdSettings.GetServerEncryptionKey() 495 require.NoError(t, err) 496 497 req := httptest.NewRequest(http.MethodGet, "/", nil) 498 encrypted, err := crypto.Encrypt([]byte("123"), key) 499 require.NoError(t, err) 500 501 app, err := NewClientApp(cdSettings, "", nil, "/argo-cd", cache.NewInMemoryCache(24*time.Hour)) 502 require.NoError(t, err) 503 504 req.AddCookie(&http.Cookie{Name: common.StateCookieName, Value: hex.EncodeToString(encrypted)}) 505 returnURL, err := app.verifyAppState(req, httptest.NewRecorder(), "123") 506 assert.NoError(t, err) 507 assert.Equal(t, "/argo-cd", returnURL) 508 } 509 510 func TestGetUserInfo(t *testing.T) { 511 512 var tests = []struct { 513 name string 514 userInfoPath string 515 expectedOutput interface{} 516 expectError bool 517 expectUnauthenticated bool 518 expectedCacheItems []struct { // items to check in cache after function call 519 key string 520 value string 521 expectEncrypted bool 522 expectError bool 523 } 524 idpHandler func(w http.ResponseWriter, r *http.Request) 525 idpClaims jwt.MapClaims // as per specification sub and exp are REQUIRED fields 526 cache cache.CacheClient 527 cacheItems []struct { // items to put in cache before execution 528 key string 529 value string 530 encrypt bool 531 } 532 }{ 533 { 534 name: "call UserInfo with wrong userInfoPath", 535 userInfoPath: "/user", 536 expectedOutput: jwt.MapClaims(nil), 537 expectError: true, 538 expectUnauthenticated: false, 539 expectedCacheItems: []struct { 540 key string 541 value string 542 expectEncrypted bool 543 expectError bool 544 }{ 545 { 546 key: formatUserInfoResponseCacheKey(UserInfoResponseCachePrefix, "randomUser"), 547 expectError: true, 548 }, 549 }, 550 idpClaims: jwt.MapClaims{"sub": "randomUser", "exp": float64(time.Now().Add(5 * time.Minute).Unix())}, 551 idpHandler: func(w http.ResponseWriter, r *http.Request) { 552 w.WriteHeader(http.StatusNotFound) 553 }, 554 cache: cache.NewInMemoryCache(24 * time.Hour), 555 cacheItems: []struct { 556 key string 557 value string 558 encrypt bool 559 }{ 560 { 561 key: formatAccessTokenCacheKey(AccessTokenCachePrefix, "randomUser"), 562 value: "FakeAccessToken", 563 encrypt: true, 564 }, 565 }, 566 }, 567 { 568 name: "call UserInfo with bad accessToken", 569 userInfoPath: "/user-info", 570 expectedOutput: jwt.MapClaims(nil), 571 expectError: false, 572 expectUnauthenticated: true, 573 expectedCacheItems: []struct { 574 key string 575 value string 576 expectEncrypted bool 577 expectError bool 578 }{ 579 { 580 key: formatUserInfoResponseCacheKey(UserInfoResponseCachePrefix, "randomUser"), 581 expectError: true, 582 }, 583 }, 584 idpClaims: jwt.MapClaims{"sub": "randomUser", "exp": float64(time.Now().Add(5 * time.Minute).Unix())}, 585 idpHandler: func(w http.ResponseWriter, r *http.Request) { 586 w.WriteHeader(http.StatusUnauthorized) 587 }, 588 cache: cache.NewInMemoryCache(24 * time.Hour), 589 cacheItems: []struct { 590 key string 591 value string 592 encrypt bool 593 }{ 594 { 595 key: formatAccessTokenCacheKey(AccessTokenCachePrefix, "randomUser"), 596 value: "FakeAccessToken", 597 encrypt: true, 598 }, 599 }, 600 }, 601 { 602 name: "call UserInfo with garbage returned", 603 userInfoPath: "/user-info", 604 expectedOutput: jwt.MapClaims(nil), 605 expectError: true, 606 expectUnauthenticated: false, 607 expectedCacheItems: []struct { 608 key string 609 value string 610 expectEncrypted bool 611 expectError bool 612 }{ 613 { 614 key: formatUserInfoResponseCacheKey(UserInfoResponseCachePrefix, "randomUser"), 615 expectError: true, 616 }, 617 }, 618 idpClaims: jwt.MapClaims{"sub": "randomUser", "exp": float64(time.Now().Add(5 * time.Minute).Unix())}, 619 idpHandler: func(w http.ResponseWriter, r *http.Request) { 620 userInfoBytes := ` 621 notevenJsongarbage 622 ` 623 _, err := w.Write([]byte(userInfoBytes)) 624 if err != nil { 625 w.WriteHeader(http.StatusInternalServerError) 626 return 627 } 628 w.WriteHeader(http.StatusTeapot) 629 }, 630 cache: cache.NewInMemoryCache(24 * time.Hour), 631 cacheItems: []struct { 632 key string 633 value string 634 encrypt bool 635 }{ 636 { 637 key: formatAccessTokenCacheKey(AccessTokenCachePrefix, "randomUser"), 638 value: "FakeAccessToken", 639 encrypt: true, 640 }, 641 }, 642 }, 643 { 644 name: "call UserInfo without accessToken in cache", 645 userInfoPath: "/user-info", 646 expectedOutput: jwt.MapClaims(nil), 647 expectError: true, 648 expectUnauthenticated: true, 649 expectedCacheItems: []struct { 650 key string 651 value string 652 expectEncrypted bool 653 expectError bool 654 }{ 655 { 656 key: formatUserInfoResponseCacheKey(UserInfoResponseCachePrefix, "randomUser"), 657 expectError: true, 658 }, 659 }, 660 idpClaims: jwt.MapClaims{"sub": "randomUser", "exp": float64(time.Now().Add(5 * time.Minute).Unix())}, 661 idpHandler: func(w http.ResponseWriter, r *http.Request) { 662 userInfoBytes := ` 663 { 664 "groups":["githubOrg:engineers"] 665 }` 666 w.Header().Set("content-type", "application/json") 667 _, err := w.Write([]byte(userInfoBytes)) 668 if err != nil { 669 w.WriteHeader(http.StatusInternalServerError) 670 return 671 } 672 w.WriteHeader(http.StatusOK) 673 }, 674 cache: cache.NewInMemoryCache(24 * time.Hour), 675 }, 676 { 677 name: "call UserInfo with valid accessToken in cache", 678 userInfoPath: "/user-info", 679 expectedOutput: jwt.MapClaims{"groups": []interface{}{"githubOrg:engineers"}}, 680 expectError: false, 681 expectUnauthenticated: false, 682 expectedCacheItems: []struct { 683 key string 684 value string 685 expectEncrypted bool 686 expectError bool 687 }{ 688 { 689 key: formatUserInfoResponseCacheKey(UserInfoResponseCachePrefix, "randomUser"), 690 value: "{\"groups\":[\"githubOrg:engineers\"]}", 691 expectEncrypted: true, 692 expectError: false, 693 }, 694 }, 695 idpClaims: jwt.MapClaims{"sub": "randomUser", "exp": float64(time.Now().Add(5 * time.Minute).Unix())}, 696 idpHandler: func(w http.ResponseWriter, r *http.Request) { 697 userInfoBytes := ` 698 { 699 "groups":["githubOrg:engineers"] 700 }` 701 w.Header().Set("content-type", "application/json") 702 _, err := w.Write([]byte(userInfoBytes)) 703 if err != nil { 704 w.WriteHeader(http.StatusInternalServerError) 705 return 706 } 707 w.WriteHeader(http.StatusOK) 708 }, 709 cache: cache.NewInMemoryCache(24 * time.Hour), 710 cacheItems: []struct { 711 key string 712 value string 713 encrypt bool 714 }{ 715 { 716 key: formatAccessTokenCacheKey(AccessTokenCachePrefix, "randomUser"), 717 value: "FakeAccessToken", 718 encrypt: true, 719 }, 720 }, 721 }, 722 } 723 724 for _, tt := range tests { 725 t.Run(tt.name, func(t *testing.T) { 726 ts := httptest.NewServer(http.HandlerFunc(tt.idpHandler)) 727 defer ts.Close() 728 729 signature, err := util.MakeSignature(32) 730 require.NoError(t, err) 731 cdSettings := &settings.ArgoCDSettings{ServerSignature: signature} 732 encryptionKey, err := cdSettings.GetServerEncryptionKey() 733 assert.NoError(t, err) 734 a, _ := NewClientApp(cdSettings, "", nil, "/argo-cd", tt.cache) 735 736 for _, item := range tt.cacheItems { 737 var newValue []byte 738 newValue = []byte(item.value) 739 if item.encrypt { 740 newValue, err = crypto.Encrypt([]byte(item.value), encryptionKey) 741 assert.NoError(t, err) 742 } 743 err := a.clientCache.Set(&cache.Item{ 744 Key: item.key, 745 Object: newValue, 746 }) 747 require.NoError(t, err) 748 } 749 750 got, unauthenticated, err := a.GetUserInfo(tt.idpClaims, ts.URL, tt.userInfoPath) 751 assert.Equal(t, tt.expectedOutput, got) 752 assert.Equal(t, tt.expectUnauthenticated, unauthenticated) 753 if tt.expectError { 754 assert.Error(t, err) 755 } else { 756 assert.NoError(t, err) 757 } 758 for _, item := range tt.expectedCacheItems { 759 var tmpValue []byte 760 err := a.clientCache.Get(item.key, &tmpValue) 761 if item.expectError { 762 require.Error(t, err) 763 } else { 764 require.NoError(t, err) 765 if item.expectEncrypted { 766 tmpValue, err = crypto.Decrypt(tmpValue, encryptionKey) 767 require.NoError(t, err) 768 } 769 assert.Equal(t, item.value, string(tmpValue)) 770 } 771 } 772 }) 773 } 774 775 }