github.com/argoproj/argo-cd/v2@v2.10.9/util/oidc/oidc.go (about)

     1  package oidc
     2  
     3  import (
     4  	"encoding/hex"
     5  	"encoding/json"
     6  	"fmt"
     7  	"html"
     8  	"html/template"
     9  	"io"
    10  	"net"
    11  	"net/http"
    12  	"net/url"
    13  	"os"
    14  	"path"
    15  	"strings"
    16  	"time"
    17  
    18  	gooidc "github.com/coreos/go-oidc/v3/oidc"
    19  	"github.com/golang-jwt/jwt/v4"
    20  	log "github.com/sirupsen/logrus"
    21  	"golang.org/x/oauth2"
    22  
    23  	"github.com/argoproj/argo-cd/v2/common"
    24  	"github.com/argoproj/argo-cd/v2/server/settings/oidc"
    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  
    29  	httputil "github.com/argoproj/argo-cd/v2/util/http"
    30  	jwtutil "github.com/argoproj/argo-cd/v2/util/jwt"
    31  	"github.com/argoproj/argo-cd/v2/util/rand"
    32  	"github.com/argoproj/argo-cd/v2/util/settings"
    33  )
    34  
    35  var InvalidRedirectURLError = fmt.Errorf("invalid return URL")
    36  
    37  const (
    38  	GrantTypeAuthorizationCode  = "authorization_code"
    39  	GrantTypeImplicit           = "implicit"
    40  	ResponseTypeCode            = "code"
    41  	UserInfoResponseCachePrefix = "userinfo_response"
    42  	AccessTokenCachePrefix      = "access_token"
    43  )
    44  
    45  // OIDCConfiguration holds a subset of interested fields from the OIDC configuration spec
    46  type OIDCConfiguration struct {
    47  	Issuer                 string   `json:"issuer"`
    48  	ScopesSupported        []string `json:"scopes_supported"`
    49  	ResponseTypesSupported []string `json:"response_types_supported"`
    50  	GrantTypesSupported    []string `json:"grant_types_supported,omitempty"`
    51  }
    52  
    53  type ClaimsRequest struct {
    54  	IDToken map[string]*oidc.Claim `json:"id_token"`
    55  }
    56  
    57  type ClientApp struct {
    58  	// OAuth2 client ID of this application (e.g. argo-cd)
    59  	clientID string
    60  	// OAuth2 client secret of this application
    61  	clientSecret string
    62  	// Callback URL for OAuth2 responses (e.g. https://argocd.example.com/auth/callback)
    63  	redirectURI string
    64  	// URL of the issuer (e.g. https://argocd.example.com/api/dex)
    65  	issuerURL string
    66  	// the path where the issuer providers user information (e.g /user-info for okta)
    67  	userInfoPath string
    68  	// The URL endpoint at which the ArgoCD server is accessed.
    69  	baseHRef string
    70  	// client is the HTTP client which is used to query the IDp
    71  	client *http.Client
    72  	// secureCookie indicates if the cookie should be set with the Secure flag, meaning it should
    73  	// only ever be sent over HTTPS. This value is inferred by the scheme of the redirectURI.
    74  	secureCookie bool
    75  	// settings holds Argo CD settings
    76  	settings *settings.ArgoCDSettings
    77  	// encryptionKey holds server encryption key
    78  	encryptionKey []byte
    79  	// provider is the OIDC provider
    80  	provider Provider
    81  	// clientCache represent a cache of sso artifact
    82  	clientCache cache.CacheClient
    83  }
    84  
    85  func GetScopesOrDefault(scopes []string) []string {
    86  	if len(scopes) == 0 {
    87  		return []string{"openid", "profile", "email", "groups"}
    88  	}
    89  	return scopes
    90  }
    91  
    92  // NewClientApp will register the Argo CD client app (either via Dex or external OIDC) and return an
    93  // object which has HTTP handlers for handling the HTTP responses for login and callback
    94  func NewClientApp(settings *settings.ArgoCDSettings, dexServerAddr string, dexTlsConfig *dex.DexTLSConfig, baseHRef string, cacheClient cache.CacheClient) (*ClientApp, error) {
    95  	redirectURL, err := settings.RedirectURL()
    96  	if err != nil {
    97  		return nil, err
    98  	}
    99  	encryptionKey, err := settings.GetServerEncryptionKey()
   100  	if err != nil {
   101  		return nil, err
   102  	}
   103  	a := ClientApp{
   104  		clientID:      settings.OAuth2ClientID(),
   105  		clientSecret:  settings.OAuth2ClientSecret(),
   106  		redirectURI:   redirectURL,
   107  		issuerURL:     settings.IssuerURL(),
   108  		userInfoPath:  settings.UserInfoPath(),
   109  		baseHRef:      baseHRef,
   110  		encryptionKey: encryptionKey,
   111  		clientCache:   cacheClient,
   112  	}
   113  	log.Infof("Creating client app (%s)", a.clientID)
   114  	u, err := url.Parse(settings.URL)
   115  	if err != nil {
   116  		return nil, fmt.Errorf("parse redirect-uri: %v", err)
   117  	}
   118  
   119  	transport := &http.Transport{
   120  		Proxy: http.ProxyFromEnvironment,
   121  		Dial: (&net.Dialer{
   122  			Timeout:   30 * time.Second,
   123  			KeepAlive: 30 * time.Second,
   124  		}).Dial,
   125  		TLSHandshakeTimeout:   10 * time.Second,
   126  		ExpectContinueTimeout: 1 * time.Second,
   127  	}
   128  	a.client = &http.Client{
   129  		Transport: transport,
   130  	}
   131  
   132  	if settings.DexConfig != "" && settings.OIDCConfigRAW == "" {
   133  		transport.TLSClientConfig = dex.TLSConfig(dexTlsConfig)
   134  		addrWithProto := dex.DexServerAddressWithProtocol(dexServerAddr, dexTlsConfig)
   135  		a.client.Transport = dex.NewDexRewriteURLRoundTripper(addrWithProto, a.client.Transport)
   136  	} else {
   137  		transport.TLSClientConfig = settings.OIDCTLSConfig()
   138  	}
   139  	if os.Getenv(common.EnvVarSSODebug) == "1" {
   140  		a.client.Transport = httputil.DebugTransport{T: a.client.Transport}
   141  	}
   142  
   143  	a.provider = NewOIDCProvider(a.issuerURL, a.client)
   144  	// NOTE: if we ever have replicas of Argo CD, this needs to switch to Redis cache
   145  	a.secureCookie = bool(u.Scheme == "https")
   146  	a.settings = settings
   147  	return &a, nil
   148  }
   149  
   150  func (a *ClientApp) oauth2Config(scopes []string) (*oauth2.Config, error) {
   151  	endpoint, err := a.provider.Endpoint()
   152  	if err != nil {
   153  		return nil, err
   154  	}
   155  	return &oauth2.Config{
   156  		ClientID:     a.clientID,
   157  		ClientSecret: a.clientSecret,
   158  		Endpoint:     *endpoint,
   159  		Scopes:       scopes,
   160  		RedirectURL:  a.redirectURI,
   161  	}, nil
   162  }
   163  
   164  // generateAppState creates an app state nonce
   165  func (a *ClientApp) generateAppState(returnURL string, w http.ResponseWriter) (string, error) {
   166  	// According to the spec (https://www.rfc-editor.org/rfc/rfc6749#section-10.10), this must be guessable with
   167  	// probability <= 2^(-128). The following call generates one of 52^24 random strings, ~= 2^136 possibilities.
   168  	randStr, err := rand.String(24)
   169  	if err != nil {
   170  		return "", fmt.Errorf("failed to generate app state: %w", err)
   171  	}
   172  	if returnURL == "" {
   173  		returnURL = a.baseHRef
   174  	}
   175  	cookieValue := fmt.Sprintf("%s:%s", randStr, returnURL)
   176  	if encrypted, err := crypto.Encrypt([]byte(cookieValue), a.encryptionKey); err != nil {
   177  		return "", err
   178  	} else {
   179  		cookieValue = hex.EncodeToString(encrypted)
   180  	}
   181  
   182  	http.SetCookie(w, &http.Cookie{
   183  		Name:     common.StateCookieName,
   184  		Value:    cookieValue,
   185  		Expires:  time.Now().Add(common.StateCookieMaxAge),
   186  		HttpOnly: true,
   187  		SameSite: http.SameSiteLaxMode,
   188  		Secure:   a.secureCookie,
   189  	})
   190  	return randStr, nil
   191  }
   192  
   193  func (a *ClientApp) verifyAppState(r *http.Request, w http.ResponseWriter, state string) (string, error) {
   194  	c, err := r.Cookie(common.StateCookieName)
   195  	if err != nil {
   196  		return "", err
   197  	}
   198  	val, err := hex.DecodeString(c.Value)
   199  	if err != nil {
   200  		return "", err
   201  	}
   202  	val, err = crypto.Decrypt(val, a.encryptionKey)
   203  	if err != nil {
   204  		return "", err
   205  	}
   206  	cookieVal := string(val)
   207  	redirectURL := a.baseHRef
   208  	parts := strings.SplitN(cookieVal, ":", 2)
   209  	if len(parts) == 2 && parts[1] != "" {
   210  		if !isValidRedirectURL(parts[1], []string{a.settings.URL, a.baseHRef}) {
   211  			sanitizedUrl := parts[1]
   212  			if len(sanitizedUrl) > 100 {
   213  				sanitizedUrl = sanitizedUrl[:100]
   214  			}
   215  			log.Warnf("Failed to verify app state - got invalid redirectURL %q", sanitizedUrl)
   216  			return "", fmt.Errorf("failed to verify app state: %w", InvalidRedirectURLError)
   217  		}
   218  		redirectURL = parts[1]
   219  	}
   220  	if parts[0] != state {
   221  		return "", fmt.Errorf("invalid state in '%s' cookie", common.AuthCookieName)
   222  	}
   223  	// set empty cookie to clear it
   224  	http.SetCookie(w, &http.Cookie{
   225  		Name:     common.StateCookieName,
   226  		Value:    "",
   227  		HttpOnly: true,
   228  		SameSite: http.SameSiteLaxMode,
   229  		Secure:   a.secureCookie,
   230  	})
   231  	return redirectURL, nil
   232  }
   233  
   234  // isValidRedirectURL checks whether the given redirectURL matches on of the
   235  // allowed URLs to redirect to.
   236  //
   237  // In order to be considered valid,the protocol and host (including port) have
   238  // to match and if allowed path is not "/", redirectURL's path must be within
   239  // allowed URL's path.
   240  func isValidRedirectURL(redirectURL string, allowedURLs []string) bool {
   241  	if redirectURL == "" {
   242  		return true
   243  	}
   244  	r, err := url.Parse(redirectURL)
   245  	if err != nil {
   246  		return false
   247  	}
   248  	// We consider empty path the same as "/" for redirect URL
   249  	if r.Path == "" {
   250  		r.Path = "/"
   251  	}
   252  	// Prevent CRLF in the redirectURL
   253  	if strings.ContainsAny(r.Path, "\r\n") {
   254  		return false
   255  	}
   256  	for _, baseURL := range allowedURLs {
   257  		b, err := url.Parse(baseURL)
   258  		if err != nil {
   259  			continue
   260  		}
   261  		// We consider empty path the same as "/" for allowed URL
   262  		if b.Path == "" {
   263  			b.Path = "/"
   264  		}
   265  		// scheme and host are mandatory to match.
   266  		if b.Scheme == r.Scheme && b.Host == r.Host {
   267  			// If path of redirectURL and allowedURL match, redirectURL is allowed
   268  			//if b.Path == r.Path {
   269  			//	return true
   270  			//}
   271  			// If path of redirectURL is within allowed URL's path, redirectURL is allowed
   272  			if strings.HasPrefix(path.Clean(r.Path), b.Path) {
   273  				return true
   274  			}
   275  		}
   276  	}
   277  	// No match - redirect URL is not allowed
   278  	return false
   279  }
   280  
   281  // HandleLogin formulates the proper OAuth2 URL (auth code or implicit) and redirects the user to
   282  // the IDp login & consent page
   283  func (a *ClientApp) HandleLogin(w http.ResponseWriter, r *http.Request) {
   284  	oidcConf, err := a.provider.ParseConfig()
   285  	if err != nil {
   286  		http.Error(w, err.Error(), http.StatusInternalServerError)
   287  		return
   288  	}
   289  	scopes := make([]string, 0)
   290  	var opts []oauth2.AuthCodeOption
   291  	if config := a.settings.OIDCConfig(); config != nil {
   292  		scopes = config.RequestedScopes
   293  		opts = AppendClaimsAuthenticationRequestParameter(opts, config.RequestedIDTokenClaims)
   294  	}
   295  	oauth2Config, err := a.oauth2Config(GetScopesOrDefault(scopes))
   296  	if err != nil {
   297  		http.Error(w, err.Error(), http.StatusInternalServerError)
   298  		return
   299  	}
   300  	returnURL := r.FormValue("return_url")
   301  	// Check if return_url is valid, otherwise abort processing (see https://github.com/argoproj/argo-cd/pull/4780)
   302  	if !isValidRedirectURL(returnURL, []string{a.settings.URL}) {
   303  		http.Error(w, "Invalid redirect URL: the protocol and host (including port) must match and the path must be within allowed URLs if provided", http.StatusBadRequest)
   304  		return
   305  	}
   306  	stateNonce, err := a.generateAppState(returnURL, w)
   307  	if err != nil {
   308  		log.Errorf("Failed to initiate login flow: %v", err)
   309  		http.Error(w, "Failed to initiate login flow", http.StatusInternalServerError)
   310  		return
   311  	}
   312  	grantType := InferGrantType(oidcConf)
   313  	var url string
   314  	switch grantType {
   315  	case GrantTypeAuthorizationCode:
   316  		url = oauth2Config.AuthCodeURL(stateNonce, opts...)
   317  	case GrantTypeImplicit:
   318  		url, err = ImplicitFlowURL(oauth2Config, stateNonce, opts...)
   319  		if err != nil {
   320  			log.Errorf("Failed to initiate implicit login flow: %v", err)
   321  			http.Error(w, "Failed to initiate implicit login flow", http.StatusInternalServerError)
   322  			return
   323  		}
   324  	default:
   325  		http.Error(w, fmt.Sprintf("Unsupported grant type: %v", grantType), http.StatusInternalServerError)
   326  		return
   327  	}
   328  	log.Infof("Performing %s flow login: %s", grantType, url)
   329  	http.Redirect(w, r, url, http.StatusSeeOther)
   330  }
   331  
   332  // HandleCallback is the callback handler for an OAuth2 login flow
   333  func (a *ClientApp) HandleCallback(w http.ResponseWriter, r *http.Request) {
   334  	oauth2Config, err := a.oauth2Config(nil)
   335  	if err != nil {
   336  		http.Error(w, err.Error(), http.StatusInternalServerError)
   337  		return
   338  	}
   339  	log.Infof("Callback: %s", r.URL)
   340  	if errMsg := r.FormValue("error"); errMsg != "" {
   341  		errorDesc := r.FormValue("error_description")
   342  		http.Error(w, html.EscapeString(errMsg)+": "+html.EscapeString(errorDesc), http.StatusBadRequest)
   343  		return
   344  	}
   345  	code := r.FormValue("code")
   346  	state := r.FormValue("state")
   347  	if code == "" {
   348  		// If code was not given, it implies implicit flow
   349  		a.handleImplicitFlow(r, w, state)
   350  		return
   351  	}
   352  	returnURL, err := a.verifyAppState(r, w, state)
   353  	if err != nil {
   354  		http.Error(w, err.Error(), http.StatusBadRequest)
   355  		return
   356  	}
   357  	ctx := gooidc.ClientContext(r.Context(), a.client)
   358  	token, err := oauth2Config.Exchange(ctx, code)
   359  	if err != nil {
   360  		http.Error(w, fmt.Sprintf("failed to get token: %v", err), http.StatusInternalServerError)
   361  		return
   362  	}
   363  	idTokenRAW, ok := token.Extra("id_token").(string)
   364  	if !ok {
   365  		http.Error(w, "no id_token in token response", http.StatusInternalServerError)
   366  		return
   367  	}
   368  
   369  	idToken, err := a.provider.Verify(idTokenRAW, a.settings)
   370  
   371  	if err != nil {
   372  		log.Warnf("Failed to verify token: %s", err)
   373  		http.Error(w, common.TokenVerificationError, http.StatusInternalServerError)
   374  		return
   375  	}
   376  	path := "/"
   377  	if a.baseHRef != "" {
   378  		path = strings.TrimRight(strings.TrimLeft(a.baseHRef, "/"), "/")
   379  	}
   380  	cookiePath := fmt.Sprintf("path=/%s", path)
   381  	flags := []string{cookiePath, "SameSite=lax", "httpOnly"}
   382  	if a.secureCookie {
   383  		flags = append(flags, "Secure")
   384  	}
   385  	var claims jwt.MapClaims
   386  	err = idToken.Claims(&claims)
   387  	if err != nil {
   388  		http.Error(w, err.Error(), http.StatusInternalServerError)
   389  		return
   390  	}
   391  	// save the accessToken in memory for later use
   392  	encToken, err := crypto.Encrypt([]byte(token.AccessToken), a.encryptionKey)
   393  	if err != nil {
   394  		claimsJSON, _ := json.Marshal(claims)
   395  		http.Error(w, "failed encrypting token", http.StatusInternalServerError)
   396  		log.Errorf("cannot encrypt accessToken: %v (claims=%s)", err, claimsJSON)
   397  		return
   398  	}
   399  	sub := jwtutil.StringField(claims, "sub")
   400  	err = a.clientCache.Set(&cache.Item{
   401  		Key:        formatAccessTokenCacheKey(AccessTokenCachePrefix, sub),
   402  		Object:     encToken,
   403  		Expiration: getTokenExpiration(claims),
   404  	})
   405  	if err != nil {
   406  		claimsJSON, _ := json.Marshal(claims)
   407  		http.Error(w, fmt.Sprintf("claims=%s, err=%v", claimsJSON, err), http.StatusInternalServerError)
   408  		return
   409  	}
   410  
   411  	if idTokenRAW != "" {
   412  		cookies, err := httputil.MakeCookieMetadata(common.AuthCookieName, idTokenRAW, flags...)
   413  		if err != nil {
   414  			claimsJSON, _ := json.Marshal(claims)
   415  			http.Error(w, fmt.Sprintf("claims=%s, err=%v", claimsJSON, err), http.StatusInternalServerError)
   416  			return
   417  		}
   418  
   419  		for _, cookie := range cookies {
   420  			w.Header().Add("Set-Cookie", cookie)
   421  		}
   422  	}
   423  
   424  	claimsJSON, _ := json.Marshal(claims)
   425  	log.Infof("Web login successful. Claims: %s", claimsJSON)
   426  	if os.Getenv(common.EnvVarSSODebug) == "1" {
   427  		claimsJSON, _ := json.MarshalIndent(claims, "", "  ")
   428  		renderToken(w, a.redirectURI, idTokenRAW, token.RefreshToken, claimsJSON)
   429  	} else {
   430  		http.Redirect(w, r, returnURL, http.StatusSeeOther)
   431  	}
   432  }
   433  
   434  var implicitFlowTmpl = template.Must(template.New("implicit.html").Parse(`<script>
   435  var hash = window.location.hash.substr(1);
   436  var result = hash.split('&').reduce(function (result, item) {
   437  	var parts = item.split('=');
   438  	result[parts[0]] = parts[1];
   439  	return result;
   440  }, {});
   441  var idToken = result['id_token'];
   442  var state = result['state'];
   443  var returnURL = "{{ .ReturnURL }}";
   444  if (state != "" && returnURL == "") {
   445  	window.location.href = window.location.href.split("#")[0] + "?state=" + result['state'] + window.location.hash;
   446  } else if (returnURL != "") {
   447  	document.cookie = "{{ .CookieName }}=" + idToken + "; path=/";
   448  	window.location.href = returnURL;
   449  }
   450  </script>`))
   451  
   452  // handleImplicitFlow completes an implicit OAuth2 flow. The id_token and state will be contained
   453  // in the URL fragment. The javascript client first redirects to the callback URL, supplying the
   454  // state nonce for verification, as well as looking up the return URL. Once verified, the client
   455  // stores the id_token from the fragment as a cookie. Finally it performs the final redirect back to
   456  // the return URL.
   457  func (a *ClientApp) handleImplicitFlow(r *http.Request, w http.ResponseWriter, state string) {
   458  	type implicitFlowValues struct {
   459  		CookieName string
   460  		ReturnURL  string
   461  	}
   462  	vals := implicitFlowValues{
   463  		CookieName: common.AuthCookieName,
   464  	}
   465  	if state != "" {
   466  		returnURL, err := a.verifyAppState(r, w, state)
   467  		if err != nil {
   468  			http.Error(w, err.Error(), http.StatusBadRequest)
   469  			return
   470  		}
   471  		vals.ReturnURL = returnURL
   472  	}
   473  	renderTemplate(w, implicitFlowTmpl, vals)
   474  }
   475  
   476  // ImplicitFlowURL is an adaptation of oauth2.Config::AuthCodeURL() which returns a URL
   477  // appropriate for an OAuth2 implicit login flow (as opposed to authorization code flow).
   478  func ImplicitFlowURL(c *oauth2.Config, state string, opts ...oauth2.AuthCodeOption) (string, error) {
   479  	opts = append(opts, oauth2.SetAuthURLParam("response_type", "id_token"))
   480  	randString, err := rand.String(24)
   481  	if err != nil {
   482  		return "", fmt.Errorf("failed to generate nonce for implicit flow URL: %w", err)
   483  	}
   484  	opts = append(opts, oauth2.SetAuthURLParam("nonce", randString))
   485  	return c.AuthCodeURL(state, opts...), nil
   486  }
   487  
   488  // OfflineAccess returns whether or not 'offline_access' is a supported scope
   489  func OfflineAccess(scopes []string) bool {
   490  	if len(scopes) == 0 {
   491  		// scopes_supported is a "RECOMMENDED" discovery claim, not a required
   492  		// one. If missing, assume that the provider follows the spec and has
   493  		// an "offline_access" scope.
   494  		return true
   495  	}
   496  	// See if scopes_supported has the "offline_access" scope.
   497  	for _, scope := range scopes {
   498  		if scope == gooidc.ScopeOfflineAccess {
   499  			return true
   500  		}
   501  	}
   502  	return false
   503  }
   504  
   505  // InferGrantType infers the proper grant flow depending on the OAuth2 client config and OIDC configuration.
   506  // Returns either: "authorization_code" or "implicit"
   507  func InferGrantType(oidcConf *OIDCConfiguration) string {
   508  	// Check the supported response types. If the list contains the response type 'code',
   509  	// then grant type is 'authorization_code'. This is preferred over the implicit
   510  	// grant type since refresh tokens cannot be issued that way.
   511  	for _, supportedType := range oidcConf.ResponseTypesSupported {
   512  		if supportedType == ResponseTypeCode {
   513  			return GrantTypeAuthorizationCode
   514  		}
   515  	}
   516  
   517  	// Assume implicit otherwise
   518  	return GrantTypeImplicit
   519  }
   520  
   521  // AppendClaimsAuthenticationRequestParameter appends a OIDC claims authentication request parameter
   522  // to `opts` with the `requestedClaims`
   523  func AppendClaimsAuthenticationRequestParameter(opts []oauth2.AuthCodeOption, requestedClaims map[string]*oidc.Claim) []oauth2.AuthCodeOption {
   524  	if len(requestedClaims) == 0 {
   525  		return opts
   526  	}
   527  	log.Infof("RequestedClaims: %s\n", requestedClaims)
   528  	claimsRequestParameter, err := createClaimsAuthenticationRequestParameter(requestedClaims)
   529  	if err != nil {
   530  		log.Errorf("Failed to create OIDC claims authentication request parameter from config: %s", err)
   531  		return opts
   532  	}
   533  	return append(opts, claimsRequestParameter)
   534  }
   535  
   536  func createClaimsAuthenticationRequestParameter(requestedClaims map[string]*oidc.Claim) (oauth2.AuthCodeOption, error) {
   537  	claimsRequest := ClaimsRequest{IDToken: requestedClaims}
   538  	claimsRequestRAW, err := json.Marshal(claimsRequest)
   539  	if err != nil {
   540  		return nil, err
   541  	}
   542  	return oauth2.SetAuthURLParam("claims", string(claimsRequestRAW)), nil
   543  }
   544  
   545  // GetUserInfo queries the IDP userinfo endpoint for claims
   546  func (a *ClientApp) GetUserInfo(actualClaims jwt.MapClaims, issuerURL, userInfoPath string) (jwt.MapClaims, bool, error) {
   547  	sub := jwtutil.StringField(actualClaims, "sub")
   548  	var claims jwt.MapClaims
   549  	var encClaims []byte
   550  
   551  	// in case we got it in the cache, we just return the item
   552  	clientCacheKey := formatUserInfoResponseCacheKey(UserInfoResponseCachePrefix, sub)
   553  	if err := a.clientCache.Get(clientCacheKey, &encClaims); err == nil {
   554  		claimsRaw, err := crypto.Decrypt(encClaims, a.encryptionKey)
   555  		if err != nil {
   556  			log.Errorf("decrypting the cached claims failed (sub=%s): %s", sub, err)
   557  		} else {
   558  			err = json.Unmarshal(claimsRaw, &claims)
   559  			if err != nil {
   560  				log.Errorf("cannot unmarshal cached claims structure: %s", err)
   561  			} else {
   562  				// return the cached claims since they are not yet expired, were successfully decrypted and unmarshaled
   563  				return claims, false, err
   564  			}
   565  		}
   566  	}
   567  
   568  	// check if the accessToken for the user is still present
   569  	var encAccessToken []byte
   570  	err := a.clientCache.Get(formatAccessTokenCacheKey(AccessTokenCachePrefix, sub), &encAccessToken)
   571  	// without an accessToken we can't query the user info endpoint
   572  	// thus the user needs to reauthenticate for argocd to get a new accessToken
   573  	if err == cache.ErrCacheMiss {
   574  		return claims, true, fmt.Errorf("no accessToken for %s: %w", sub, err)
   575  	} else if err != nil {
   576  		return claims, true, fmt.Errorf("couldn't read accessToken from cache for %s: %w", sub, err)
   577  	}
   578  
   579  	accessToken, err := crypto.Decrypt(encAccessToken, a.encryptionKey)
   580  	if err != nil {
   581  		return claims, true, fmt.Errorf("couldn't decrypt accessToken for %s: %w", sub, err)
   582  	}
   583  
   584  	url := issuerURL + userInfoPath
   585  	request, err := http.NewRequest("GET", url, nil)
   586  
   587  	if err != nil {
   588  		err = fmt.Errorf("failed creating new http request: %w", err)
   589  		return claims, false, err
   590  	}
   591  
   592  	bearer := fmt.Sprintf("Bearer %s", accessToken)
   593  	request.Header.Set("Authorization", bearer)
   594  
   595  	response, err := a.client.Do(request)
   596  	if err != nil {
   597  		return claims, false, fmt.Errorf("failed to query userinfo endpoint of IDP: %w", err)
   598  	}
   599  	defer response.Body.Close()
   600  	if response.StatusCode == http.StatusUnauthorized {
   601  		return claims, true, err
   602  	}
   603  
   604  	// according to https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponseValidation
   605  	// the response should be validated
   606  	header := response.Header.Get("content-type")
   607  	rawBody, err := io.ReadAll(response.Body)
   608  	if err != nil {
   609  		return claims, false, fmt.Errorf("got error reading response body: %w", err)
   610  	}
   611  	switch header {
   612  	case "application/jwt":
   613  		// if body is JWT, first validate it before extracting claims
   614  		idToken, err := a.provider.Verify(string(rawBody), a.settings)
   615  		if err != nil {
   616  			return claims, false, fmt.Errorf("user info response in jwt format not valid: %w", err)
   617  		}
   618  		err = idToken.Claims(claims)
   619  		if err != nil {
   620  			return claims, false, fmt.Errorf("cannot get claims from userinfo jwt: %w", err)
   621  		}
   622  	default:
   623  		// if body is json, unsigned and unencrypted claims can be deserialized
   624  		err = json.Unmarshal(rawBody, &claims)
   625  		if err != nil {
   626  			return claims, false, fmt.Errorf("failed to decode response body to struct: %w", err)
   627  		}
   628  	}
   629  
   630  	// in case response was successfully validated and there was no error, put item in cache
   631  	// but first let's determine the expiry of the cache
   632  	var cacheExpiry time.Duration
   633  	settingExpiry := a.settings.UserInfoCacheExpiration()
   634  	tokenExpiry := getTokenExpiration(claims)
   635  
   636  	// only use configured expiry if the token lives longer and the expiry is configured
   637  	// if the token has no expiry, use the expiry of the actual token
   638  	// otherwise use the expiry of the token
   639  	if settingExpiry < tokenExpiry && settingExpiry != 0 {
   640  		cacheExpiry = settingExpiry
   641  	} else if tokenExpiry < 0 {
   642  		cacheExpiry = getTokenExpiration(actualClaims)
   643  	} else {
   644  		cacheExpiry = tokenExpiry
   645  	}
   646  
   647  	rawClaims, err := json.Marshal(claims)
   648  	if err != nil {
   649  		return claims, false, fmt.Errorf("couldn't marshal claim to json: %w", err)
   650  	}
   651  	encClaims, err = crypto.Encrypt(rawClaims, a.encryptionKey)
   652  	if err != nil {
   653  		return claims, false, fmt.Errorf("couldn't encrypt user info response: %w", err)
   654  	}
   655  
   656  	err = a.clientCache.Set(&cache.Item{
   657  		Key:        clientCacheKey,
   658  		Object:     encClaims,
   659  		Expiration: cacheExpiry,
   660  	})
   661  	if err != nil {
   662  		return claims, false, fmt.Errorf("couldn't put item to cache: %w", err)
   663  	}
   664  
   665  	return claims, false, nil
   666  }
   667  
   668  // getTokenExpiration returns a time.Duration until the token expires
   669  func getTokenExpiration(claims jwt.MapClaims) time.Duration {
   670  	// get duration until token expires
   671  	exp := jwtutil.Float64Field(claims, "exp")
   672  	tm := time.Unix(int64(exp), 0)
   673  	tokenExpiry := time.Until(tm)
   674  	return tokenExpiry
   675  }
   676  
   677  // formatUserInfoResponseCacheKey returns the key which is used to store userinfo of user in cache
   678  func formatUserInfoResponseCacheKey(prefix, sub string) string {
   679  	return fmt.Sprintf("%s_%s", UserInfoResponseCachePrefix, sub)
   680  }
   681  
   682  // formatAccessTokenCacheKey returns the key which is used to store the accessToken of a user in cache
   683  func formatAccessTokenCacheKey(prefix, sub string) string {
   684  	return fmt.Sprintf("%s_%s", prefix, sub)
   685  }