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: &lt;script&gt;alert(&#39;hello&#39;)&lt;/script&gt;\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  }