github.com/argoproj/argo-cd/v3@v3.2.1/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  	"path/filepath"
    13  	"strings"
    14  	"sync"
    15  	"testing"
    16  	"time"
    17  
    18  	gooidc "github.com/coreos/go-oidc/v3/oidc"
    19  	"github.com/golang-jwt/jwt/v5"
    20  	"github.com/stretchr/testify/assert"
    21  	"github.com/stretchr/testify/require"
    22  	"golang.org/x/oauth2"
    23  	"sigs.k8s.io/yaml"
    24  
    25  	"github.com/argoproj/argo-cd/v3/common"
    26  	"github.com/argoproj/argo-cd/v3/server/settings/oidc"
    27  	"github.com/argoproj/argo-cd/v3/util"
    28  	"github.com/argoproj/argo-cd/v3/util/cache"
    29  	"github.com/argoproj/argo-cd/v3/util/crypto"
    30  	"github.com/argoproj/argo-cd/v3/util/dex"
    31  	"github.com/argoproj/argo-cd/v3/util/settings"
    32  	"github.com/argoproj/argo-cd/v3/util/test"
    33  )
    34  
    35  func setupAzureIdentity(t *testing.T) {
    36  	t.Helper()
    37  
    38  	tempDir := t.TempDir()
    39  	tokenFilePath := filepath.Join(tempDir, "token.txt")
    40  	tempFile, err := os.Create(tokenFilePath)
    41  	require.NoError(t, err)
    42  	_, err = tempFile.WriteString("serviceAccountToken")
    43  	require.NoError(t, err)
    44  	t.Setenv("AZURE_FEDERATED_TOKEN_FILE", tokenFilePath)
    45  }
    46  
    47  func TestInferGrantType(t *testing.T) {
    48  	for _, path := range []string{"dex", "okta", "auth0", "onelogin"} {
    49  		t.Run(path, func(t *testing.T) {
    50  			rawConfig, err := os.ReadFile("testdata/" + path + ".json")
    51  			require.NoError(t, err)
    52  			var config OIDCConfiguration
    53  			err = json.Unmarshal(rawConfig, &config)
    54  			require.NoError(t, err)
    55  			grantType := InferGrantType(&config)
    56  			assert.Equal(t, GrantTypeAuthorizationCode, grantType)
    57  
    58  			var noCodeResponseTypes []string
    59  			for _, supportedResponseType := range config.ResponseTypesSupported {
    60  				if supportedResponseType != ResponseTypeCode {
    61  					noCodeResponseTypes = append(noCodeResponseTypes, supportedResponseType)
    62  				}
    63  			}
    64  
    65  			config.ResponseTypesSupported = noCodeResponseTypes
    66  			grantType = InferGrantType(&config)
    67  			assert.Equal(t, GrantTypeImplicit, grantType)
    68  		})
    69  	}
    70  }
    71  
    72  func TestIDTokenClaims(t *testing.T) {
    73  	oauth2Config := &oauth2.Config{
    74  		ClientID:     "DUMMY_OIDC_PROVIDER",
    75  		ClientSecret: "0987654321",
    76  		Endpoint:     oauth2.Endpoint{AuthURL: "https://argocd-dev.onelogin.com/oidc/auth", TokenURL: "https://argocd-dev.onelogin.com/oidc/token"},
    77  		Scopes:       []string{"oidc", "profile", "groups"},
    78  		RedirectURL:  "https://argocd-dev.io/redirect_url",
    79  	}
    80  
    81  	var opts []oauth2.AuthCodeOption
    82  	requestedClaims := make(map[string]*oidc.Claim)
    83  
    84  	opts = AppendClaimsAuthenticationRequestParameter(opts, requestedClaims)
    85  	assert.Empty(t, opts)
    86  
    87  	requestedClaims["groups"] = &oidc.Claim{Essential: true}
    88  	opts = AppendClaimsAuthenticationRequestParameter(opts, requestedClaims)
    89  	assert.Len(t, opts, 1)
    90  
    91  	authCodeURL, err := url.Parse(oauth2Config.AuthCodeURL("TEST", opts...))
    92  	require.NoError(t, err)
    93  
    94  	values, err := url.ParseQuery(authCodeURL.RawQuery)
    95  	require.NoError(t, err)
    96  
    97  	assert.JSONEq(t, "{\"id_token\":{\"groups\":{\"essential\":true}}}", values.Get("claims"))
    98  }
    99  
   100  type fakeProvider struct{}
   101  
   102  func (p *fakeProvider) Endpoint() (*oauth2.Endpoint, error) {
   103  	return &oauth2.Endpoint{}, nil
   104  }
   105  
   106  func (p *fakeProvider) ParseConfig() (*OIDCConfiguration, error) {
   107  	return nil, nil
   108  }
   109  
   110  func (p *fakeProvider) Verify(_ string, _ *settings.ArgoCDSettings) (*gooidc.IDToken, error) {
   111  	return nil, nil
   112  }
   113  
   114  func TestHandleCallback(t *testing.T) {
   115  	app := ClientApp{provider: &fakeProvider{}, settings: &settings.ArgoCDSettings{}}
   116  
   117  	req := httptest.NewRequest(http.MethodGet, "http://example.com/foo", http.NoBody)
   118  	req.Form = url.Values{
   119  		"error":             []string{"login-failed"},
   120  		"error_description": []string{"<script>alert('hello')</script>"},
   121  	}
   122  	w := httptest.NewRecorder()
   123  
   124  	app.HandleCallback(w, req)
   125  
   126  	assert.Equal(t, "login-failed: &lt;script&gt;alert(&#39;hello&#39;)&lt;/script&gt;\n", w.Body.String())
   127  }
   128  
   129  func TestClientApp_HandleLogin(t *testing.T) {
   130  	oidcTestServer := test.GetOIDCTestServer(t, nil)
   131  	t.Cleanup(oidcTestServer.Close)
   132  
   133  	dexTestServer := test.GetDexTestServer(t)
   134  	t.Cleanup(dexTestServer.Close)
   135  
   136  	t.Run("oidc certificate checking during login should toggle on config", func(t *testing.T) {
   137  		cdSettings := &settings.ArgoCDSettings{
   138  			URL: "https://argocd.example.com",
   139  			OIDCConfigRAW: fmt.Sprintf(`
   140  name: Test
   141  issuer: %s
   142  clientID: xxx
   143  clientSecret: yyy
   144  requestedScopes: ["oidc"]`, oidcTestServer.URL),
   145  		}
   146  		app, err := NewClientApp(cdSettings, dexTestServer.URL, nil, "https://argocd.example.com", cache.NewInMemoryCache(24*time.Hour))
   147  		require.NoError(t, err)
   148  
   149  		req := httptest.NewRequest(http.MethodGet, "https://argocd.example.com/auth/login", http.NoBody)
   150  
   151  		w := httptest.NewRecorder()
   152  
   153  		app.HandleLogin(w, req)
   154  
   155  		assert.Contains(t, w.Body.String(), "certificate signed by unknown authority")
   156  
   157  		cdSettings.OIDCTLSInsecureSkipVerify = true
   158  
   159  		app, err = NewClientApp(cdSettings, dexTestServer.URL, nil, "https://argocd.example.com", cache.NewInMemoryCache(24*time.Hour))
   160  		require.NoError(t, err)
   161  
   162  		w = httptest.NewRecorder()
   163  
   164  		app.HandleLogin(w, req)
   165  
   166  		assert.NotContains(t, w.Body.String(), "certificate is not trusted")
   167  		assert.NotContains(t, w.Body.String(), "certificate signed by unknown authority")
   168  	})
   169  
   170  	t.Run("dex certificate checking during login should toggle on config", func(t *testing.T) {
   171  		cdSettings := &settings.ArgoCDSettings{
   172  			URL: "https://argocd.example.com",
   173  			DexConfig: `connectors:
   174  - type: github
   175    name: GitHub
   176    config:
   177      clientID: aabbccddeeff00112233
   178      clientSecret: aabbccddeeff00112233`,
   179  		}
   180  		cert, err := tls.X509KeyPair(test.Cert, test.PrivateKey)
   181  		require.NoError(t, err)
   182  		cdSettings.Certificate = &cert
   183  		app, err := NewClientApp(cdSettings, dexTestServer.URL, nil, "https://argocd.example.com", cache.NewInMemoryCache(24*time.Hour))
   184  		require.NoError(t, err)
   185  
   186  		req := httptest.NewRequest(http.MethodGet, "https://argocd.example.com/auth/login", http.NoBody)
   187  
   188  		w := httptest.NewRecorder()
   189  
   190  		app.HandleLogin(w, req)
   191  
   192  		if !strings.Contains(w.Body.String(), "certificate signed by unknown authority") && !strings.Contains(w.Body.String(), "certificate is not trusted") {
   193  			t.Fatal("did not receive expected certificate verification failure error")
   194  		}
   195  
   196  		app, err = NewClientApp(cdSettings, dexTestServer.URL, &dex.DexTLSConfig{StrictValidation: false}, "https://argocd.example.com", cache.NewInMemoryCache(24*time.Hour))
   197  		require.NoError(t, err)
   198  
   199  		w = httptest.NewRecorder()
   200  
   201  		app.HandleLogin(w, req)
   202  
   203  		assert.NotContains(t, w.Body.String(), "certificate is not trusted")
   204  		assert.NotContains(t, w.Body.String(), "certificate signed by unknown authority")
   205  	})
   206  
   207  	t.Run("OIDC auth", func(t *testing.T) {
   208  		cdSettings := &settings.ArgoCDSettings{
   209  			URL:                       "https://argocd.example.com",
   210  			OIDCTLSInsecureSkipVerify: true,
   211  		}
   212  		oidcConfig := settings.OIDCConfig{
   213  			Name:         "Test",
   214  			Issuer:       oidcTestServer.URL,
   215  			ClientID:     "xxx",
   216  			ClientSecret: "yyy",
   217  		}
   218  		oidcConfigRaw, err := yaml.Marshal(oidcConfig)
   219  		require.NoError(t, err)
   220  		cdSettings.OIDCConfigRAW = string(oidcConfigRaw)
   221  
   222  		app, err := NewClientApp(cdSettings, dexTestServer.URL, &dex.DexTLSConfig{StrictValidation: false}, "https://argocd.example.com", cache.NewInMemoryCache(24*time.Hour))
   223  		require.NoError(t, err)
   224  
   225  		req := httptest.NewRequest(http.MethodGet, "https://argocd.example.com/auth/login", http.NoBody)
   226  		w := httptest.NewRecorder()
   227  		app.HandleLogin(w, req)
   228  
   229  		assert.Equal(t, http.StatusSeeOther, w.Code)
   230  		location, err := url.Parse(w.Header().Get("Location"))
   231  		require.NoError(t, err)
   232  		values, err := url.ParseQuery(location.RawQuery)
   233  		require.NoError(t, err)
   234  		assert.Equal(t, []string{"openid", "profile", "email", "groups"}, strings.Split(values.Get("scope"), " "))
   235  		assert.Equal(t, "xxx", values.Get("client_id"))
   236  		assert.Equal(t, "code", values.Get("response_type"))
   237  	})
   238  
   239  	t.Run("OIDC auth with custom scopes", func(t *testing.T) {
   240  		cdSettings := &settings.ArgoCDSettings{
   241  			URL:                       "https://argocd.example.com",
   242  			OIDCTLSInsecureSkipVerify: true,
   243  		}
   244  		oidcConfig := settings.OIDCConfig{
   245  			Name:            "Test",
   246  			Issuer:          oidcTestServer.URL,
   247  			ClientID:        "xxx",
   248  			ClientSecret:    "yyy",
   249  			RequestedScopes: []string{"oidc"},
   250  		}
   251  		oidcConfigRaw, err := yaml.Marshal(oidcConfig)
   252  		require.NoError(t, err)
   253  		cdSettings.OIDCConfigRAW = string(oidcConfigRaw)
   254  
   255  		app, err := NewClientApp(cdSettings, dexTestServer.URL, &dex.DexTLSConfig{StrictValidation: false}, "https://argocd.example.com", cache.NewInMemoryCache(24*time.Hour))
   256  		require.NoError(t, err)
   257  
   258  		req := httptest.NewRequest(http.MethodGet, "https://argocd.example.com/auth/login", http.NoBody)
   259  		w := httptest.NewRecorder()
   260  		app.HandleLogin(w, req)
   261  
   262  		assert.Equal(t, http.StatusSeeOther, w.Code)
   263  		location, err := url.Parse(w.Header().Get("Location"))
   264  		require.NoError(t, err)
   265  		values, err := url.ParseQuery(location.RawQuery)
   266  		require.NoError(t, err)
   267  		assert.Equal(t, []string{"oidc"}, strings.Split(values.Get("scope"), " "))
   268  		assert.Equal(t, "xxx", values.Get("client_id"))
   269  		assert.Equal(t, "code", values.Get("response_type"))
   270  	})
   271  
   272  	t.Run("Dex auth", func(t *testing.T) {
   273  		cdSettings := &settings.ArgoCDSettings{
   274  			URL: dexTestServer.URL,
   275  		}
   276  		dexConfig := map[string]any{
   277  			"connectors": []map[string]any{
   278  				{
   279  					"type": "github",
   280  					"name": "GitHub",
   281  					"config": map[string]any{
   282  						"clientId":     "aabbccddeeff00112233",
   283  						"clientSecret": "aabbccddeeff00112233",
   284  					},
   285  				},
   286  			},
   287  		}
   288  		dexConfigRaw, err := yaml.Marshal(dexConfig)
   289  		require.NoError(t, err)
   290  		cdSettings.DexConfig = string(dexConfigRaw)
   291  
   292  		app, err := NewClientApp(cdSettings, dexTestServer.URL, &dex.DexTLSConfig{StrictValidation: false}, "https://argocd.example.com", cache.NewInMemoryCache(24*time.Hour))
   293  		require.NoError(t, err)
   294  
   295  		req := httptest.NewRequest(http.MethodGet, "https://argocd.example.com/auth/login", http.NoBody)
   296  		w := httptest.NewRecorder()
   297  		app.HandleLogin(w, req)
   298  
   299  		assert.Equal(t, http.StatusSeeOther, w.Code)
   300  		location, err := url.Parse(w.Header().Get("Location"))
   301  		require.NoError(t, err)
   302  		values, err := url.ParseQuery(location.RawQuery)
   303  		require.NoError(t, err)
   304  		assert.Equal(t, []string{"openid", "profile", "email", "groups", common.DexFederatedScope}, strings.Split(values.Get("scope"), " "))
   305  		assert.Equal(t, common.ArgoCDClientAppID, values.Get("client_id"))
   306  		assert.Equal(t, "code", values.Get("response_type"))
   307  	})
   308  
   309  	t.Run("with additional base URL", func(t *testing.T) {
   310  		cdSettings := &settings.ArgoCDSettings{
   311  			URL:                       "https://argocd.example.com",
   312  			AdditionalURLs:            []string{"https://localhost:8080", "https://other.argocd.example.com"},
   313  			OIDCTLSInsecureSkipVerify: true,
   314  			DexConfig: `connectors:
   315  			- type: github
   316  			  name: GitHub
   317  			  config:
   318  			    clientID: aabbccddeeff00112233
   319  			    clientSecret: aabbccddeeff00112233`,
   320  			OIDCConfigRAW: fmt.Sprintf(`
   321  name: Test
   322  issuer: %s
   323  clientID: xxx
   324  clientSecret: yyy
   325  requestedScopes: ["oidc"]`, oidcTestServer.URL),
   326  		}
   327  		cert, err := tls.X509KeyPair(test.Cert, test.PrivateKey)
   328  		require.NoError(t, err)
   329  		cdSettings.Certificate = &cert
   330  		app, err := NewClientApp(cdSettings, dexTestServer.URL, &dex.DexTLSConfig{StrictValidation: false}, "https://argocd.example.com", cache.NewInMemoryCache(24*time.Hour))
   331  		require.NoError(t, err)
   332  
   333  		t.Run("should accept login redirecting on the main domain", func(t *testing.T) {
   334  			req := httptest.NewRequest(http.MethodGet, "https://argocd.example.com/auth/login", http.NoBody)
   335  
   336  			req.URL.RawQuery = url.Values{
   337  				"return_url": []string{"https://argocd.example.com/applications"},
   338  			}.Encode()
   339  
   340  			w := httptest.NewRecorder()
   341  
   342  			app.HandleLogin(w, req)
   343  
   344  			assert.Equal(t, http.StatusSeeOther, w.Code)
   345  			location, err := url.Parse(w.Header().Get("Location"))
   346  			require.NoError(t, err)
   347  			assert.Equal(t, fmt.Sprintf("%s://%s", location.Scheme, location.Host), oidcTestServer.URL)
   348  			assert.Equal(t, "/auth", location.Path)
   349  			assert.Equal(t, "https://argocd.example.com/auth/callback", location.Query().Get("redirect_uri"))
   350  		})
   351  
   352  		t.Run("should accept login redirecting on the alternative domains", func(t *testing.T) {
   353  			req := httptest.NewRequest(http.MethodGet, "https://localhost:8080/auth/login", http.NoBody)
   354  
   355  			req.URL.RawQuery = url.Values{
   356  				"return_url": []string{"https://localhost:8080/applications"},
   357  			}.Encode()
   358  
   359  			w := httptest.NewRecorder()
   360  
   361  			app.HandleLogin(w, req)
   362  
   363  			assert.Equal(t, http.StatusSeeOther, w.Code)
   364  			location, err := url.Parse(w.Header().Get("Location"))
   365  			require.NoError(t, err)
   366  			assert.Equal(t, fmt.Sprintf("%s://%s", location.Scheme, location.Host), oidcTestServer.URL)
   367  			assert.Equal(t, "/auth", location.Path)
   368  			assert.Equal(t, "https://localhost:8080/auth/callback", location.Query().Get("redirect_uri"))
   369  		})
   370  
   371  		t.Run("should accept login redirecting on the alternative domains", func(t *testing.T) {
   372  			req := httptest.NewRequest(http.MethodGet, "https://other.argocd.example.com/auth/login", http.NoBody)
   373  
   374  			req.URL.RawQuery = url.Values{
   375  				"return_url": []string{"https://other.argocd.example.com/applications"},
   376  			}.Encode()
   377  
   378  			w := httptest.NewRecorder()
   379  
   380  			app.HandleLogin(w, req)
   381  
   382  			assert.Equal(t, http.StatusSeeOther, w.Code)
   383  			location, err := url.Parse(w.Header().Get("Location"))
   384  			require.NoError(t, err)
   385  			assert.Equal(t, fmt.Sprintf("%s://%s", location.Scheme, location.Host), oidcTestServer.URL)
   386  			assert.Equal(t, "/auth", location.Path)
   387  			assert.Equal(t, "https://other.argocd.example.com/auth/callback", location.Query().Get("redirect_uri"))
   388  		})
   389  
   390  		t.Run("should deny login redirecting on the alternative domains", func(t *testing.T) {
   391  			req := httptest.NewRequest(http.MethodGet, "https://not-argocd.example.com/auth/login", http.NoBody)
   392  
   393  			req.URL.RawQuery = url.Values{
   394  				"return_url": []string{"https://not-argocd.example.com/applications"},
   395  			}.Encode()
   396  
   397  			w := httptest.NewRecorder()
   398  
   399  			app.HandleLogin(w, req)
   400  
   401  			assert.Equal(t, http.StatusBadRequest, w.Code)
   402  			assert.Empty(t, w.Header().Get("Location"))
   403  		})
   404  	})
   405  }
   406  
   407  func Test_Login_Flow(t *testing.T) {
   408  	// Show that SSO login works when no redirect URL is provided, and we fall back to the configured base href for the
   409  	// Argo CD instance.
   410  
   411  	oidcTestServer := test.GetOIDCTestServer(t, nil)
   412  	t.Cleanup(oidcTestServer.Close)
   413  
   414  	cdSettings := &settings.ArgoCDSettings{
   415  		URL: "https://argocd.example.com",
   416  		OIDCConfigRAW: fmt.Sprintf(`
   417  name: Test
   418  issuer: %s
   419  clientID: test-client-id
   420  clientSecret: test-client-secret
   421  requestedScopes: ["oidc"]`, oidcTestServer.URL),
   422  		OIDCTLSInsecureSkipVerify: true,
   423  	}
   424  	// The base href (the last argument for NewClientApp) is what HandleLogin will fall back to when no explicit
   425  	// redirect URL is given.
   426  	app, err := NewClientApp(cdSettings, "", nil, "/", cache.NewInMemoryCache(24*time.Hour))
   427  	require.NoError(t, err)
   428  
   429  	w := httptest.NewRecorder()
   430  
   431  	req := httptest.NewRequest(http.MethodGet, "https://argocd.example.com/auth/login", http.NoBody)
   432  
   433  	app.HandleLogin(w, req)
   434  
   435  	redirectURL, err := w.Result().Location()
   436  	require.NoError(t, err)
   437  
   438  	state := redirectURL.Query().Get("state")
   439  
   440  	req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://argocd.example.com/auth/callback?state=%s&code=abc", state), http.NoBody)
   441  	for _, cookie := range w.Result().Cookies() {
   442  		req.AddCookie(cookie)
   443  	}
   444  
   445  	w = httptest.NewRecorder()
   446  
   447  	app.HandleCallback(w, req)
   448  
   449  	assert.Equal(t, 303, w.Code)
   450  	assert.NotContains(t, w.Body.String(), ErrInvalidRedirectURL.Error())
   451  }
   452  
   453  func Test_Login_Flow_With_PKCE(t *testing.T) {
   454  	var codeChallenge string
   455  
   456  	oidcTestServer := test.GetOIDCTestServer(t, func(r *http.Request) {
   457  		codeVerifier := r.FormValue("code_verifier")
   458  		assert.NotEmpty(t, codeVerifier)
   459  		assert.Equal(t, oauth2.S256ChallengeFromVerifier(codeVerifier), codeChallenge)
   460  	})
   461  	t.Cleanup(oidcTestServer.Close)
   462  
   463  	cdSettings := &settings.ArgoCDSettings{
   464  		URL: "https://example.com/argocd",
   465  		OIDCConfigRAW: fmt.Sprintf(`
   466  name: Test
   467  issuer: %s
   468  clientID: test-client-id
   469  clientSecret: test-client-secret
   470  requestedScopes: ["oidc"]
   471  enablePKCEAuthentication: true`, oidcTestServer.URL),
   472  		OIDCTLSInsecureSkipVerify: true,
   473  	}
   474  	app, err := NewClientApp(cdSettings, "", nil, "/", cache.NewInMemoryCache(24*time.Hour))
   475  	require.NoError(t, err)
   476  
   477  	w := httptest.NewRecorder()
   478  
   479  	req := httptest.NewRequest(http.MethodGet, "https://example.com/argocd/auth/login", http.NoBody)
   480  
   481  	app.HandleLogin(w, req)
   482  
   483  	redirectURL, err := w.Result().Location()
   484  	require.NoError(t, err)
   485  
   486  	codeChallenge = redirectURL.Query().Get("code_challenge")
   487  
   488  	assert.NotEmpty(t, codeChallenge)
   489  	assert.Equal(t, "S256", redirectURL.Query().Get("code_challenge_method"))
   490  
   491  	state := redirectURL.Query().Get("state")
   492  
   493  	req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://example.com/argocd/auth/callback?state=%s&code=abc", state), http.NoBody)
   494  	for _, cookie := range w.Result().Cookies() {
   495  		req.AddCookie(cookie)
   496  	}
   497  
   498  	w = httptest.NewRecorder()
   499  
   500  	app.HandleCallback(w, req)
   501  
   502  	assert.Equal(t, 303, w.Code)
   503  	assert.NotContains(t, w.Body.String(), ErrInvalidRedirectURL.Error())
   504  }
   505  
   506  func TestClientApp_HandleCallback(t *testing.T) {
   507  	oidcTestServer := test.GetOIDCTestServer(t, nil)
   508  	t.Cleanup(oidcTestServer.Close)
   509  
   510  	dexTestServer := test.GetDexTestServer(t)
   511  	t.Cleanup(dexTestServer.Close)
   512  
   513  	t.Run("oidc certificate checking during oidc callback should toggle on config", func(t *testing.T) {
   514  		cdSettings := &settings.ArgoCDSettings{
   515  			URL: "https://argocd.example.com",
   516  			OIDCConfigRAW: fmt.Sprintf(`
   517  name: Test
   518  issuer: %s
   519  clientID: xxx
   520  clientSecret: yyy
   521  requestedScopes: ["oidc"]`, oidcTestServer.URL),
   522  		}
   523  		app, err := NewClientApp(cdSettings, dexTestServer.URL, nil, "https://argocd.example.com", cache.NewInMemoryCache(24*time.Hour))
   524  		require.NoError(t, err)
   525  
   526  		req := httptest.NewRequest(http.MethodGet, "https://argocd.example.com/auth/callback", http.NoBody)
   527  
   528  		w := httptest.NewRecorder()
   529  
   530  		app.HandleCallback(w, req)
   531  
   532  		if !strings.Contains(w.Body.String(), "certificate signed by unknown authority") && !strings.Contains(w.Body.String(), "certificate is not trusted") {
   533  			t.Fatal("did not receive expected certificate verification failure error")
   534  		}
   535  
   536  		cdSettings.OIDCTLSInsecureSkipVerify = true
   537  
   538  		app, err = NewClientApp(cdSettings, dexTestServer.URL, nil, "https://argocd.example.com", cache.NewInMemoryCache(24*time.Hour))
   539  		require.NoError(t, err)
   540  
   541  		w = httptest.NewRecorder()
   542  
   543  		app.HandleCallback(w, req)
   544  
   545  		assert.NotContains(t, w.Body.String(), "certificate is not trusted")
   546  		assert.NotContains(t, w.Body.String(), "certificate signed by unknown authority")
   547  	})
   548  
   549  	t.Run("dex certificate checking during oidc callback should toggle on config", func(t *testing.T) {
   550  		cdSettings := &settings.ArgoCDSettings{
   551  			URL: "https://argocd.example.com",
   552  			DexConfig: `connectors:
   553  - type: github
   554    name: GitHub
   555    config:
   556      clientID: aabbccddeeff00112233
   557      clientSecret: aabbccddeeff00112233`,
   558  		}
   559  		cert, err := tls.X509KeyPair(test.Cert, test.PrivateKey)
   560  		require.NoError(t, err)
   561  		cdSettings.Certificate = &cert
   562  		app, err := NewClientApp(cdSettings, dexTestServer.URL, nil, "https://argocd.example.com", cache.NewInMemoryCache(24*time.Hour))
   563  		require.NoError(t, err)
   564  
   565  		req := httptest.NewRequest(http.MethodGet, "https://argocd.example.com/auth/callback", http.NoBody)
   566  
   567  		w := httptest.NewRecorder()
   568  
   569  		app.HandleCallback(w, req)
   570  
   571  		if !strings.Contains(w.Body.String(), "certificate signed by unknown authority") && !strings.Contains(w.Body.String(), "certificate is not trusted") {
   572  			t.Fatal("did not receive expected certificate verification failure error")
   573  		}
   574  
   575  		app, err = NewClientApp(cdSettings, dexTestServer.URL, &dex.DexTLSConfig{StrictValidation: false}, "https://argocd.example.com", cache.NewInMemoryCache(24*time.Hour))
   576  		require.NoError(t, err)
   577  
   578  		w = httptest.NewRecorder()
   579  
   580  		app.HandleCallback(w, req)
   581  
   582  		assert.NotContains(t, w.Body.String(), "certificate is not trusted")
   583  		assert.NotContains(t, w.Body.String(), "certificate signed by unknown authority")
   584  	})
   585  }
   586  
   587  func Test_azureApp_getFederatedServiceAccountToken(t *testing.T) {
   588  	app := azureApp{mtx: &sync.RWMutex{}}
   589  
   590  	setupAzureIdentity(t)
   591  
   592  	t.Run("before the method call assertion should be empty.", func(t *testing.T) {
   593  		assert.Empty(t, app.assertion)
   594  	})
   595  
   596  	t.Run("Fetch the token value from the file", func(t *testing.T) {
   597  		_, err := app.getFederatedServiceAccountToken(t.Context())
   598  		require.NoError(t, err)
   599  		assert.Equal(t, "serviceAccountToken", app.assertion)
   600  	})
   601  
   602  	t.Run("Workload Identity Not enabled.", func(t *testing.T) {
   603  		t.Setenv("AZURE_FEDERATED_TOKEN_FILE", "")
   604  		_, err := app.getFederatedServiceAccountToken(t.Context())
   605  		assert.ErrorContains(t, err, "AZURE_FEDERATED_TOKEN_FILE env variable not found, make sure workload identity is enabled on the cluster")
   606  	})
   607  
   608  	t.Run("Workload Identity invalid file", func(t *testing.T) {
   609  		t.Setenv("AZURE_FEDERATED_TOKEN_FILE", filepath.Join(t.TempDir(), "invalid.txt"))
   610  		_, err := app.getFederatedServiceAccountToken(t.Context())
   611  		assert.ErrorContains(t, err, "AZURE_FEDERATED_TOKEN_FILE specified file does not exist")
   612  	})
   613  
   614  	t.Run("Concurrent access to the function", func(t *testing.T) {
   615  		currentExpiryTime := app.expires
   616  
   617  		var wg sync.WaitGroup
   618  		numGoroutines := 10
   619  		wg.Add(numGoroutines)
   620  		for i := 0; i < numGoroutines; i++ {
   621  			go func() {
   622  				defer wg.Done()
   623  				_, err := app.getFederatedServiceAccountToken(t.Context())
   624  				require.NoError(t, err)
   625  				assert.Equal(t, "serviceAccountToken", app.assertion)
   626  			}()
   627  		}
   628  		wg.Wait()
   629  
   630  		// Event with multiple concurrent calls the expiry time should not change untile it passes.
   631  		assert.Equal(t, currentExpiryTime, app.expires)
   632  	})
   633  
   634  	t.Run("Concurrent access to the function when the current token expires", func(t *testing.T) {
   635  		var wg sync.WaitGroup
   636  		currentExpiryTime := app.expires
   637  		app.expires = time.Now()
   638  		numGoroutines := 10
   639  		wg.Add(numGoroutines)
   640  		for i := 0; i < numGoroutines; i++ {
   641  			go func() {
   642  				defer wg.Done()
   643  				_, err := app.getFederatedServiceAccountToken(t.Context())
   644  				require.NoError(t, err)
   645  				assert.Equal(t, "serviceAccountToken", app.assertion)
   646  			}()
   647  		}
   648  		wg.Wait()
   649  
   650  		assert.NotEqual(t, currentExpiryTime, app.expires)
   651  	})
   652  }
   653  
   654  func TestClientAppWithAzureWorkloadIdentity_HandleCallback(t *testing.T) {
   655  	tokenRequestAssertions := func(r *http.Request) {
   656  		err := r.ParseForm()
   657  		require.NoError(t, err)
   658  
   659  		formData := r.Form
   660  		clientAssertion := formData.Get("client_assertion")
   661  		clientAssertionType := formData.Get("client_assertion_type")
   662  		assert.Equal(t, "serviceAccountToken", clientAssertion)
   663  		assert.Equal(t, "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", clientAssertionType)
   664  	}
   665  
   666  	oidcTestServer := test.GetAzureOIDCTestServer(t, tokenRequestAssertions)
   667  	t.Cleanup(oidcTestServer.Close)
   668  
   669  	dexTestServer := test.GetDexTestServer(t)
   670  	t.Cleanup(dexTestServer.Close)
   671  	signature, err := util.MakeSignature(32)
   672  	require.NoError(t, err)
   673  
   674  	setupAzureIdentity(t)
   675  
   676  	t.Run("oidc certificate checking during oidc callback should toggle on config", func(t *testing.T) {
   677  		cdSettings := &settings.ArgoCDSettings{
   678  			URL:             "https://argocd.example.com",
   679  			ServerSignature: signature,
   680  			OIDCConfigRAW: fmt.Sprintf(`
   681  name: Test
   682  issuer: %s
   683  clientID: xxx
   684  azure:
   685    useWorkloadIdentity: true
   686  skipAudienceCheckWhenTokenHasNoAudience: true
   687  requestedScopes: ["oidc"]`, oidcTestServer.URL),
   688  		}
   689  
   690  		app, err := NewClientApp(cdSettings, dexTestServer.URL, nil, "https://argocd.example.com", cache.NewInMemoryCache(24*time.Hour))
   691  		require.NoError(t, err)
   692  
   693  		req := httptest.NewRequest(http.MethodGet, "https://argocd.example.com/auth/callback", http.NoBody)
   694  		req.Form = url.Values{
   695  			"code":  {"abc"},
   696  			"state": {"123"},
   697  		}
   698  		w := httptest.NewRecorder()
   699  
   700  		app.HandleCallback(w, req)
   701  
   702  		if !strings.Contains(w.Body.String(), "certificate signed by unknown authority") && !strings.Contains(w.Body.String(), "certificate is not trusted") {
   703  			t.Fatal("did not receive expected certificate verification failure error")
   704  		}
   705  
   706  		cdSettings.OIDCTLSInsecureSkipVerify = true
   707  
   708  		app, err = NewClientApp(cdSettings, dexTestServer.URL, nil, "https://argocd.example.com", cache.NewInMemoryCache(24*time.Hour))
   709  		require.NoError(t, err)
   710  
   711  		w = httptest.NewRecorder()
   712  
   713  		key, err := cdSettings.GetServerEncryptionKey()
   714  		require.NoError(t, err)
   715  		encrypted, _ := crypto.Encrypt([]byte("123"), key)
   716  		req.AddCookie(&http.Cookie{Name: common.StateCookieName, Value: hex.EncodeToString(encrypted)})
   717  
   718  		app.HandleCallback(w, req)
   719  
   720  		assert.NotContains(t, w.Body.String(), "certificate is not trusted")
   721  		assert.NotContains(t, w.Body.String(), "certificate signed by unknown authority")
   722  	})
   723  }
   724  
   725  func TestIsValidRedirect(t *testing.T) {
   726  	tests := []struct {
   727  		name        string
   728  		valid       bool
   729  		redirectURL string
   730  		allowedURLs []string
   731  	}{
   732  		{
   733  			name:        "Single allowed valid URL",
   734  			valid:       true,
   735  			redirectURL: "https://localhost:4000",
   736  			allowedURLs: []string{"https://localhost:4000/"},
   737  		},
   738  		{
   739  			name:        "Empty URL",
   740  			valid:       true,
   741  			redirectURL: "",
   742  			allowedURLs: []string{"https://localhost:4000/"},
   743  		},
   744  		{
   745  			name:        "Trailing single slash and empty suffix are handled the same",
   746  			valid:       true,
   747  			redirectURL: "https://localhost:4000/",
   748  			allowedURLs: []string{"https://localhost:4000"},
   749  		},
   750  		{
   751  			name:        "Multiple valid URLs with one allowed",
   752  			valid:       true,
   753  			redirectURL: "https://localhost:4000",
   754  			allowedURLs: []string{"https://wherever:4000", "https://localhost:4000"},
   755  		},
   756  		{
   757  			name:        "Multiple valid URLs with none allowed",
   758  			valid:       false,
   759  			redirectURL: "https://localhost:4000",
   760  			allowedURLs: []string{"https://wherever:4000", "https://invalid:4000"},
   761  		},
   762  		{
   763  			name:        "Invalid redirect URL because path prefix does not match",
   764  			valid:       false,
   765  			redirectURL: "https://localhost:4000/applications",
   766  			allowedURLs: []string{"https://localhost:4000/argocd"},
   767  		},
   768  		{
   769  			name:        "Valid redirect URL because prefix matches",
   770  			valid:       true,
   771  			redirectURL: "https://localhost:4000/argocd/applications",
   772  			allowedURLs: []string{"https://localhost:4000/argocd"},
   773  		},
   774  		{
   775  			name:        "Invalid redirect URL because resolved path does not match prefix",
   776  			valid:       false,
   777  			redirectURL: "https://localhost:4000/argocd/../applications",
   778  			allowedURLs: []string{"https://localhost:4000/argocd"},
   779  		},
   780  		{
   781  			name:        "Invalid redirect URL because scheme mismatch",
   782  			valid:       false,
   783  			redirectURL: "http://localhost:4000",
   784  			allowedURLs: []string{"https://localhost:4000"},
   785  		},
   786  		{
   787  			name:        "Invalid redirect URL because port mismatch",
   788  			valid:       false,
   789  			redirectURL: "https://localhost",
   790  			allowedURLs: []string{"https://localhost:80"},
   791  		},
   792  		{
   793  			name:        "Invalid redirect URL because of CRLF in path",
   794  			valid:       false,
   795  			redirectURL: "https://localhost:80/argocd\r\n",
   796  			allowedURLs: []string{"https://localhost:80/argocd\r\n"},
   797  		},
   798  	}
   799  
   800  	for _, tt := range tests {
   801  		t.Run(tt.name, func(t *testing.T) {
   802  			res := isValidRedirectURL(tt.redirectURL, tt.allowedURLs)
   803  			assert.Equal(t, res, tt.valid)
   804  		})
   805  	}
   806  }
   807  
   808  func TestGenerateAppState(t *testing.T) {
   809  	signature, err := util.MakeSignature(32)
   810  	require.NoError(t, err)
   811  	expectedReturnURL := "http://argocd.example.com/"
   812  	app, err := NewClientApp(&settings.ArgoCDSettings{ServerSignature: signature, URL: expectedReturnURL}, "", nil, "", cache.NewInMemoryCache(24*time.Hour))
   813  	require.NoError(t, err)
   814  	generateResponse := httptest.NewRecorder()
   815  	expectedPKCEVerifier := oauth2.GenerateVerifier()
   816  	state, err := app.generateAppState(expectedReturnURL, expectedPKCEVerifier, generateResponse)
   817  	require.NoError(t, err)
   818  
   819  	t.Run("VerifyAppState_Successful", func(t *testing.T) {
   820  		req := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
   821  		for _, cookie := range generateResponse.Result().Cookies() {
   822  			req.AddCookie(cookie)
   823  		}
   824  
   825  		returnURL, pkceVerifier, err := app.verifyAppState(req, httptest.NewRecorder(), state)
   826  		require.NoError(t, err)
   827  		assert.Equal(t, expectedReturnURL, returnURL)
   828  		assert.Equal(t, expectedPKCEVerifier, pkceVerifier)
   829  	})
   830  
   831  	t.Run("VerifyAppState_Failed", func(t *testing.T) {
   832  		req := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
   833  		for _, cookie := range generateResponse.Result().Cookies() {
   834  			req.AddCookie(cookie)
   835  		}
   836  
   837  		_, _, err := app.verifyAppState(req, httptest.NewRecorder(), "wrong state")
   838  		require.Error(t, err)
   839  	})
   840  }
   841  
   842  func TestGenerateAppState_XSS(t *testing.T) {
   843  	signature, err := util.MakeSignature(32)
   844  	require.NoError(t, err)
   845  	app, err := NewClientApp(
   846  		&settings.ArgoCDSettings{
   847  			// Only return URLs starting with this base should be allowed.
   848  			URL:             "https://argocd.example.com",
   849  			ServerSignature: signature,
   850  		},
   851  		"", nil, "", cache.NewInMemoryCache(24*time.Hour),
   852  	)
   853  	require.NoError(t, err)
   854  
   855  	t.Run("XSS fails", func(t *testing.T) {
   856  		// This attack assumes the attacker has compromised the server's secret key. We use `generateAppState` here for
   857  		// convenience, but an attacker with access to the server secret could write their own code to generate the
   858  		// malicious cookie.
   859  
   860  		expectedReturnURL := "javascript: alert('hi')"
   861  		generateResponse := httptest.NewRecorder()
   862  		state, err := app.generateAppState(expectedReturnURL, "", generateResponse)
   863  		require.NoError(t, err)
   864  
   865  		req := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
   866  		for _, cookie := range generateResponse.Result().Cookies() {
   867  			req.AddCookie(cookie)
   868  		}
   869  
   870  		returnURL, _, err := app.verifyAppState(req, httptest.NewRecorder(), state)
   871  		require.ErrorIs(t, err, ErrInvalidRedirectURL)
   872  		assert.Empty(t, returnURL)
   873  	})
   874  
   875  	t.Run("valid return URL succeeds", func(t *testing.T) {
   876  		expectedReturnURL := "https://argocd.example.com/some/path"
   877  		generateResponse := httptest.NewRecorder()
   878  		state, err := app.generateAppState(expectedReturnURL, "", generateResponse)
   879  		require.NoError(t, err)
   880  
   881  		req := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
   882  		for _, cookie := range generateResponse.Result().Cookies() {
   883  			req.AddCookie(cookie)
   884  		}
   885  
   886  		returnURL, _, err := app.verifyAppState(req, httptest.NewRecorder(), state)
   887  		require.NoError(t, err)
   888  		assert.Equal(t, expectedReturnURL, returnURL)
   889  	})
   890  }
   891  
   892  func TestGenerateAppState_NoReturnURL(t *testing.T) {
   893  	signature, err := util.MakeSignature(32)
   894  	require.NoError(t, err)
   895  	cdSettings := &settings.ArgoCDSettings{ServerSignature: signature}
   896  	key, err := cdSettings.GetServerEncryptionKey()
   897  	require.NoError(t, err)
   898  
   899  	req := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
   900  	encrypted, err := crypto.Encrypt([]byte("123"), key)
   901  	require.NoError(t, err)
   902  	app, err := NewClientApp(cdSettings, "", nil, "/argo-cd", cache.NewInMemoryCache(24*time.Hour))
   903  	require.NoError(t, err)
   904  
   905  	req.AddCookie(&http.Cookie{Name: common.StateCookieName, Value: hex.EncodeToString(encrypted)})
   906  	returnURL, _, err := app.verifyAppState(req, httptest.NewRecorder(), "123")
   907  	require.NoError(t, err)
   908  	assert.Equal(t, "/argo-cd", returnURL)
   909  }
   910  
   911  func TestGetUserInfo(t *testing.T) {
   912  	tests := []struct {
   913  		name                  string
   914  		userInfoPath          string
   915  		expectedOutput        any
   916  		expectError           bool
   917  		expectUnauthenticated bool
   918  		expectedCacheItems    []struct { // items to check in cache after function call
   919  			key             string
   920  			value           string
   921  			expectEncrypted bool
   922  			expectError     bool
   923  		}
   924  		idpHandler func(w http.ResponseWriter, r *http.Request)
   925  		idpClaims  jwt.MapClaims // as per specification sub and exp are REQUIRED fields
   926  		cache      cache.CacheClient
   927  		cacheItems []struct { // items to put in cache before execution
   928  			key     string
   929  			value   string
   930  			encrypt bool
   931  		}
   932  	}{
   933  		{
   934  			name:                  "call UserInfo with wrong userInfoPath",
   935  			userInfoPath:          "/user",
   936  			expectedOutput:        jwt.MapClaims(nil),
   937  			expectError:           true,
   938  			expectUnauthenticated: false,
   939  			expectedCacheItems: []struct {
   940  				key             string
   941  				value           string
   942  				expectEncrypted bool
   943  				expectError     bool
   944  			}{
   945  				{
   946  					key:         formatUserInfoResponseCacheKey("randomUser"),
   947  					expectError: true,
   948  				},
   949  			},
   950  			idpClaims: jwt.MapClaims{"sub": "randomUser", "exp": float64(time.Now().Add(5 * time.Minute).Unix())},
   951  			idpHandler: func(w http.ResponseWriter, _ *http.Request) {
   952  				w.WriteHeader(http.StatusNotFound)
   953  			},
   954  			cache: cache.NewInMemoryCache(24 * time.Hour),
   955  			cacheItems: []struct {
   956  				key     string
   957  				value   string
   958  				encrypt bool
   959  			}{
   960  				{
   961  					key:     formatAccessTokenCacheKey("randomUser"),
   962  					value:   "FakeAccessToken",
   963  					encrypt: true,
   964  				},
   965  			},
   966  		},
   967  		{
   968  			name:                  "call UserInfo with bad accessToken",
   969  			userInfoPath:          "/user-info",
   970  			expectedOutput:        jwt.MapClaims(nil),
   971  			expectError:           false,
   972  			expectUnauthenticated: true,
   973  			expectedCacheItems: []struct {
   974  				key             string
   975  				value           string
   976  				expectEncrypted bool
   977  				expectError     bool
   978  			}{
   979  				{
   980  					key:         formatUserInfoResponseCacheKey("randomUser"),
   981  					expectError: true,
   982  				},
   983  			},
   984  			idpClaims: jwt.MapClaims{"sub": "randomUser", "exp": float64(time.Now().Add(5 * time.Minute).Unix())},
   985  			idpHandler: func(w http.ResponseWriter, _ *http.Request) {
   986  				w.WriteHeader(http.StatusUnauthorized)
   987  			},
   988  			cache: cache.NewInMemoryCache(24 * time.Hour),
   989  			cacheItems: []struct {
   990  				key     string
   991  				value   string
   992  				encrypt bool
   993  			}{
   994  				{
   995  					key:     formatAccessTokenCacheKey("randomUser"),
   996  					value:   "FakeAccessToken",
   997  					encrypt: true,
   998  				},
   999  			},
  1000  		},
  1001  		{
  1002  			name:                  "call UserInfo with garbage returned",
  1003  			userInfoPath:          "/user-info",
  1004  			expectedOutput:        jwt.MapClaims(nil),
  1005  			expectError:           true,
  1006  			expectUnauthenticated: false,
  1007  			expectedCacheItems: []struct {
  1008  				key             string
  1009  				value           string
  1010  				expectEncrypted bool
  1011  				expectError     bool
  1012  			}{
  1013  				{
  1014  					key:         formatUserInfoResponseCacheKey("randomUser"),
  1015  					expectError: true,
  1016  				},
  1017  			},
  1018  			idpClaims: jwt.MapClaims{"sub": "randomUser", "exp": float64(time.Now().Add(5 * time.Minute).Unix())},
  1019  			idpHandler: func(w http.ResponseWriter, _ *http.Request) {
  1020  				userInfoBytes := `
  1021  			  notevenJsongarbage
  1022  				`
  1023  				_, err := w.Write([]byte(userInfoBytes))
  1024  				if err != nil {
  1025  					w.WriteHeader(http.StatusInternalServerError)
  1026  					return
  1027  				}
  1028  				w.WriteHeader(http.StatusTeapot)
  1029  			},
  1030  			cache: cache.NewInMemoryCache(24 * time.Hour),
  1031  			cacheItems: []struct {
  1032  				key     string
  1033  				value   string
  1034  				encrypt bool
  1035  			}{
  1036  				{
  1037  					key:     formatAccessTokenCacheKey("randomUser"),
  1038  					value:   "FakeAccessToken",
  1039  					encrypt: true,
  1040  				},
  1041  			},
  1042  		},
  1043  		{
  1044  			name:                  "call UserInfo without accessToken in cache",
  1045  			userInfoPath:          "/user-info",
  1046  			expectedOutput:        jwt.MapClaims(nil),
  1047  			expectError:           true,
  1048  			expectUnauthenticated: true,
  1049  			expectedCacheItems: []struct {
  1050  				key             string
  1051  				value           string
  1052  				expectEncrypted bool
  1053  				expectError     bool
  1054  			}{
  1055  				{
  1056  					key:         formatUserInfoResponseCacheKey("randomUser"),
  1057  					expectError: true,
  1058  				},
  1059  			},
  1060  			idpClaims: jwt.MapClaims{"sub": "randomUser", "exp": float64(time.Now().Add(5 * time.Minute).Unix())},
  1061  			idpHandler: func(w http.ResponseWriter, _ *http.Request) {
  1062  				userInfoBytes := `
  1063  				{
  1064  					"groups":["githubOrg:engineers"]
  1065  				}`
  1066  				w.Header().Set("content-type", "application/json")
  1067  				_, err := w.Write([]byte(userInfoBytes))
  1068  				if err != nil {
  1069  					w.WriteHeader(http.StatusInternalServerError)
  1070  					return
  1071  				}
  1072  				w.WriteHeader(http.StatusOK)
  1073  			},
  1074  			cache: cache.NewInMemoryCache(24 * time.Hour),
  1075  		},
  1076  		{
  1077  			name:                  "call UserInfo with valid accessToken in cache",
  1078  			userInfoPath:          "/user-info",
  1079  			expectedOutput:        jwt.MapClaims{"groups": []any{"githubOrg:engineers"}},
  1080  			expectError:           false,
  1081  			expectUnauthenticated: false,
  1082  			expectedCacheItems: []struct {
  1083  				key             string
  1084  				value           string
  1085  				expectEncrypted bool
  1086  				expectError     bool
  1087  			}{
  1088  				{
  1089  					key:             formatUserInfoResponseCacheKey("randomUser"),
  1090  					value:           "{\"groups\":[\"githubOrg:engineers\"]}",
  1091  					expectEncrypted: true,
  1092  					expectError:     false,
  1093  				},
  1094  			},
  1095  			idpClaims: jwt.MapClaims{"sub": "randomUser", "exp": float64(time.Now().Add(5 * time.Minute).Unix())},
  1096  			idpHandler: func(w http.ResponseWriter, _ *http.Request) {
  1097  				userInfoBytes := `
  1098  				{
  1099  					"groups":["githubOrg:engineers"]
  1100  				}`
  1101  				w.Header().Set("content-type", "application/json")
  1102  				_, err := w.Write([]byte(userInfoBytes))
  1103  				if err != nil {
  1104  					w.WriteHeader(http.StatusInternalServerError)
  1105  					return
  1106  				}
  1107  				w.WriteHeader(http.StatusOK)
  1108  			},
  1109  			cache: cache.NewInMemoryCache(24 * time.Hour),
  1110  			cacheItems: []struct {
  1111  				key     string
  1112  				value   string
  1113  				encrypt bool
  1114  			}{
  1115  				{
  1116  					key:     formatAccessTokenCacheKey("randomUser"),
  1117  					value:   "FakeAccessToken",
  1118  					encrypt: true,
  1119  				},
  1120  			},
  1121  		},
  1122  	}
  1123  
  1124  	for _, tt := range tests {
  1125  		t.Run(tt.name, func(t *testing.T) {
  1126  			ts := httptest.NewServer(http.HandlerFunc(tt.idpHandler))
  1127  			defer ts.Close()
  1128  
  1129  			signature, err := util.MakeSignature(32)
  1130  			require.NoError(t, err)
  1131  			cdSettings := &settings.ArgoCDSettings{ServerSignature: signature}
  1132  			encryptionKey, err := cdSettings.GetServerEncryptionKey()
  1133  			require.NoError(t, err)
  1134  			a, _ := NewClientApp(cdSettings, "", nil, "/argo-cd", tt.cache)
  1135  
  1136  			for _, item := range tt.cacheItems {
  1137  				var newValue []byte
  1138  				newValue = []byte(item.value)
  1139  				if item.encrypt {
  1140  					newValue, err = crypto.Encrypt([]byte(item.value), encryptionKey)
  1141  					require.NoError(t, err)
  1142  				}
  1143  				err := a.clientCache.Set(&cache.Item{
  1144  					Key:    item.key,
  1145  					Object: newValue,
  1146  				})
  1147  				require.NoError(t, err)
  1148  			}
  1149  
  1150  			got, unauthenticated, err := a.GetUserInfo(tt.idpClaims, ts.URL, tt.userInfoPath)
  1151  			assert.Equal(t, tt.expectedOutput, got)
  1152  			assert.Equal(t, tt.expectUnauthenticated, unauthenticated)
  1153  			if tt.expectError {
  1154  				require.Error(t, err)
  1155  			} else {
  1156  				require.NoError(t, err)
  1157  			}
  1158  			for _, item := range tt.expectedCacheItems {
  1159  				var tmpValue []byte
  1160  				err := a.clientCache.Get(item.key, &tmpValue)
  1161  				if item.expectError {
  1162  					require.Error(t, err)
  1163  				} else {
  1164  					require.NoError(t, err)
  1165  					if item.expectEncrypted {
  1166  						tmpValue, err = crypto.Decrypt(tmpValue, encryptionKey)
  1167  						require.NoError(t, err)
  1168  					}
  1169  					assert.Equal(t, item.value, string(tmpValue))
  1170  				}
  1171  			}
  1172  		})
  1173  	}
  1174  }