github.com/argoproj/argo-cd/v3@v3.2.1/util/session/sessionmanager.go (about)

     1  package session
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"math"
     8  	"math/rand"
     9  	"net"
    10  	"net/http"
    11  	"os"
    12  	"strings"
    13  	"sync"
    14  	"time"
    15  
    16  	"github.com/coreos/go-oidc/v3/oidc"
    17  	"github.com/golang-jwt/jwt/v5"
    18  	"github.com/google/uuid"
    19  	log "github.com/sirupsen/logrus"
    20  	"google.golang.org/grpc/codes"
    21  	"google.golang.org/grpc/status"
    22  
    23  	"github.com/argoproj/argo-cd/v3/server/rbacpolicy"
    24  
    25  	"github.com/argoproj/argo-cd/v3/common"
    26  	"github.com/argoproj/argo-cd/v3/pkg/client/listers/application/v1alpha1"
    27  	"github.com/argoproj/argo-cd/v3/util/dex"
    28  	"github.com/argoproj/argo-cd/v3/util/env"
    29  	httputil "github.com/argoproj/argo-cd/v3/util/http"
    30  	jwtutil "github.com/argoproj/argo-cd/v3/util/jwt"
    31  	oidcutil "github.com/argoproj/argo-cd/v3/util/oidc"
    32  	passwordutil "github.com/argoproj/argo-cd/v3/util/password"
    33  	"github.com/argoproj/argo-cd/v3/util/settings"
    34  )
    35  
    36  // SessionManager generates and validates JWT tokens for login sessions.
    37  type SessionManager struct {
    38  	settingsMgr                   *settings.SettingsManager
    39  	projectsLister                v1alpha1.AppProjectNamespaceLister
    40  	client                        *http.Client
    41  	prov                          oidcutil.Provider
    42  	storage                       UserStateStorage
    43  	sleep                         func(d time.Duration)
    44  	verificationDelayNoiseEnabled bool
    45  	failedLock                    sync.RWMutex
    46  	metricsRegistry               MetricsRegistry
    47  }
    48  
    49  // LoginAttempts is a timestamped counter for failed login attempts
    50  type LoginAttempts struct {
    51  	// Time of the last failed login
    52  	LastFailed time.Time `json:"lastFailed"`
    53  	// Number of consecutive login failures
    54  	FailCount int `json:"failCount"`
    55  }
    56  
    57  type MetricsRegistry interface {
    58  	IncLoginRequestCounter(status string)
    59  }
    60  
    61  const (
    62  	// SessionManagerClaimsIssuer fills the "iss" field of the token.
    63  	SessionManagerClaimsIssuer = "argocd"
    64  	AuthErrorCtxKey            = "auth-error"
    65  
    66  	// invalidLoginError, for security purposes, doesn't say whether the username or password was invalid.  This does not mitigate the potential for timing attacks to determine which is which.
    67  	invalidLoginError           = "Invalid username or password"
    68  	blankPasswordError          = "Blank passwords are not allowed"
    69  	accountDisabled             = "Account %s is disabled"
    70  	usernameTooLongError        = "Username is too long (%d bytes max)"
    71  	userDoesNotHaveCapability   = "Account %s does not have %s capability"
    72  	autoRegenerateTokenDuration = time.Minute * 5
    73  )
    74  
    75  const (
    76  	// Maximum length of username, too keep the cache's memory signature low
    77  	maxUsernameLength = 32
    78  	// The default maximum session cache size
    79  	defaultMaxCacheSize = 10000
    80  	// The default number of maximum login failures before delay kicks in
    81  	defaultMaxLoginFailures = 5
    82  	// The default time in seconds for the failure window
    83  	defaultFailureWindow = 300
    84  	// The password verification delay max
    85  	verificationDelayNoiseMin = 500 * time.Millisecond
    86  	// The password verification delay max
    87  	verificationDelayNoiseMax = 1000 * time.Millisecond
    88  
    89  	// environment variables to control rate limiter behaviour:
    90  
    91  	// Max number of login failures before login delay kicks in
    92  	envLoginMaxFailCount = "ARGOCD_SESSION_FAILURE_MAX_FAIL_COUNT"
    93  
    94  	// Number of maximum seconds the login is allowed to delay for. Default: 300 (5 minutes).
    95  	envLoginFailureWindowSeconds = "ARGOCD_SESSION_FAILURE_WINDOW_SECONDS"
    96  
    97  	// Max number of stored usernames
    98  	envLoginMaxCacheSize = "ARGOCD_SESSION_MAX_CACHE_SIZE"
    99  )
   100  
   101  var InvalidLoginErr = status.Errorf(codes.Unauthenticated, invalidLoginError)
   102  
   103  // Returns the maximum cache size as number of entries
   104  func getMaximumCacheSize() int {
   105  	return env.ParseNumFromEnv(envLoginMaxCacheSize, defaultMaxCacheSize, 1, math.MaxInt32)
   106  }
   107  
   108  // Returns the maximum number of login failures before login delay kicks in
   109  func getMaxLoginFailures() int {
   110  	return env.ParseNumFromEnv(envLoginMaxFailCount, defaultMaxLoginFailures, 1, math.MaxInt32)
   111  }
   112  
   113  // Returns the number of maximum seconds the login is allowed to delay for
   114  func getLoginFailureWindow() time.Duration {
   115  	return time.Duration(env.ParseNumFromEnv(envLoginFailureWindowSeconds, defaultFailureWindow, 0, math.MaxInt32))
   116  }
   117  
   118  // NewSessionManager creates a new session manager from Argo CD settings
   119  func NewSessionManager(settingsMgr *settings.SettingsManager, projectsLister v1alpha1.AppProjectNamespaceLister, dexServerAddr string, dexTLSConfig *dex.DexTLSConfig, storage UserStateStorage) *SessionManager {
   120  	s := SessionManager{
   121  		settingsMgr:                   settingsMgr,
   122  		storage:                       storage,
   123  		sleep:                         time.Sleep,
   124  		projectsLister:                projectsLister,
   125  		verificationDelayNoiseEnabled: true,
   126  	}
   127  	settings, err := settingsMgr.GetSettings()
   128  	if err != nil {
   129  		panic(err)
   130  	}
   131  
   132  	transport := &http.Transport{
   133  		Proxy: http.ProxyFromEnvironment,
   134  		Dial: (&net.Dialer{
   135  			Timeout:   30 * time.Second,
   136  			KeepAlive: 30 * time.Second,
   137  		}).Dial,
   138  		TLSHandshakeTimeout:   10 * time.Second,
   139  		ExpectContinueTimeout: 1 * time.Second,
   140  	}
   141  
   142  	s.client = &http.Client{
   143  		Transport: transport,
   144  	}
   145  
   146  	if settings.DexConfig != "" {
   147  		transport.TLSClientConfig = dex.TLSConfig(dexTLSConfig)
   148  		addrWithProto := dex.DexServerAddressWithProtocol(dexServerAddr, dexTLSConfig)
   149  		s.client.Transport = dex.NewDexRewriteURLRoundTripper(addrWithProto, s.client.Transport)
   150  	} else {
   151  		transport.TLSClientConfig = settings.OIDCTLSConfig()
   152  	}
   153  	if os.Getenv(common.EnvVarSSODebug) == "1" {
   154  		s.client.Transport = httputil.DebugTransport{T: s.client.Transport}
   155  	}
   156  
   157  	return &s
   158  }
   159  
   160  // Create creates a new token for a given subject (user) and returns it as a string.
   161  // Passing a value of `0` for secondsBeforeExpiry creates a token that never expires.
   162  // The id parameter holds an optional unique JWT token identifier and stored as a standard claim "jti" in the JWT token.
   163  func (mgr *SessionManager) Create(subject string, secondsBeforeExpiry int64, id string) (string, error) {
   164  	now := time.Now().UTC()
   165  	claims := jwt.RegisteredClaims{
   166  		IssuedAt:  jwt.NewNumericDate(now),
   167  		Issuer:    SessionManagerClaimsIssuer,
   168  		NotBefore: jwt.NewNumericDate(now),
   169  		Subject:   subject,
   170  		ID:        id,
   171  	}
   172  	if secondsBeforeExpiry > 0 {
   173  		expires := now.Add(time.Duration(secondsBeforeExpiry) * time.Second)
   174  		claims.ExpiresAt = jwt.NewNumericDate(expires)
   175  	}
   176  
   177  	return mgr.signClaims(claims)
   178  }
   179  
   180  func (mgr *SessionManager) CollectMetrics(registry MetricsRegistry) {
   181  	mgr.metricsRegistry = registry
   182  	if mgr.metricsRegistry == nil {
   183  		log.Warn("Metrics registry is not set, metrics will not be collected")
   184  		return
   185  	}
   186  }
   187  
   188  func (mgr *SessionManager) IncLoginRequestCounter(status string) {
   189  	if mgr.metricsRegistry != nil {
   190  		mgr.metricsRegistry.IncLoginRequestCounter(status)
   191  	}
   192  }
   193  
   194  func (mgr *SessionManager) signClaims(claims jwt.Claims) (string, error) {
   195  	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
   196  	settings, err := mgr.settingsMgr.GetSettings()
   197  	if err != nil {
   198  		return "", err
   199  	}
   200  	return token.SignedString(settings.ServerSignature)
   201  }
   202  
   203  // GetSubjectAccountAndCapability analyzes Argo CD account token subject and extract account name
   204  // and the capability it was generated for (default capability is API Key).
   205  func GetSubjectAccountAndCapability(subject string) (string, settings.AccountCapability) {
   206  	capability := settings.AccountCapabilityApiKey
   207  	if parts := strings.Split(subject, ":"); len(parts) > 1 {
   208  		subject = parts[0]
   209  		switch parts[1] {
   210  		case string(settings.AccountCapabilityLogin):
   211  			capability = settings.AccountCapabilityLogin
   212  		case string(settings.AccountCapabilityApiKey):
   213  			capability = settings.AccountCapabilityApiKey
   214  		}
   215  	}
   216  	return subject, capability
   217  }
   218  
   219  // Parse tries to parse the provided string and returns the token claims for local login.
   220  func (mgr *SessionManager) Parse(tokenString string) (jwt.Claims, string, error) {
   221  	// Parse takes the token string and a function for looking up the key. The latter is especially
   222  	// useful if you use multiple keys for your application.  The standard is to use 'kid' in the
   223  	// head of the token to identify which key to use, but the parsed token (head and claims) is provided
   224  	// to the callback, providing flexibility.
   225  	var claims jwt.MapClaims
   226  	argoCDSettings, err := mgr.settingsMgr.GetSettings()
   227  	if err != nil {
   228  		return nil, "", err
   229  	}
   230  	token, err := jwt.ParseWithClaims(tokenString, &claims, func(token *jwt.Token) (any, error) {
   231  		// Don't forget to validate the alg is what you expect:
   232  		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
   233  			return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
   234  		}
   235  		return argoCDSettings.ServerSignature, nil
   236  	})
   237  	if err != nil {
   238  		return nil, "", err
   239  	}
   240  
   241  	issuedAt, err := jwtutil.IssuedAtTime(claims)
   242  	if err != nil {
   243  		return nil, "", err
   244  	}
   245  
   246  	subject := jwtutil.GetUserIdentifier(claims)
   247  	id := jwtutil.StringField(claims, "jti")
   248  
   249  	if projName, role, ok := rbacpolicy.GetProjectRoleFromSubject(subject); ok {
   250  		proj, err := mgr.projectsLister.Get(projName)
   251  		if err != nil {
   252  			return nil, "", err
   253  		}
   254  		_, _, err = proj.GetJWTToken(role, issuedAt.Unix(), id)
   255  		if err != nil {
   256  			return nil, "", err
   257  		}
   258  
   259  		return token.Claims, "", nil
   260  	}
   261  
   262  	subject, capability := GetSubjectAccountAndCapability(subject)
   263  	claims["sub"] = subject
   264  
   265  	account, err := mgr.settingsMgr.GetAccount(subject)
   266  	if err != nil {
   267  		return nil, "", err
   268  	}
   269  
   270  	if !account.Enabled {
   271  		return nil, "", fmt.Errorf("account %s is disabled", subject)
   272  	}
   273  
   274  	if !account.HasCapability(capability) {
   275  		return nil, "", fmt.Errorf("account %s does not have '%s' capability", subject, capability)
   276  	}
   277  
   278  	if id == "" || mgr.storage.IsTokenRevoked(id) {
   279  		return nil, "", errors.New("token is revoked, please re-login")
   280  	} else if capability == settings.AccountCapabilityApiKey && account.TokenIndex(id) == -1 {
   281  		return nil, "", fmt.Errorf("account %s does not have token with id %s", subject, id)
   282  	}
   283  
   284  	if account.PasswordMtime != nil && issuedAt.Before(*account.PasswordMtime) {
   285  		return nil, "", errors.New("account password has changed since token issued")
   286  	}
   287  
   288  	newToken := ""
   289  	if exp, err := jwtutil.ExpirationTime(claims); err == nil {
   290  		tokenExpDuration := exp.Sub(issuedAt)
   291  		remainingDuration := time.Until(exp)
   292  
   293  		if remainingDuration < autoRegenerateTokenDuration && capability == settings.AccountCapabilityLogin {
   294  			if uniqueId, err := uuid.NewRandom(); err == nil {
   295  				if val, err := mgr.Create(fmt.Sprintf("%s:%s", subject, settings.AccountCapabilityLogin), int64(tokenExpDuration.Seconds()), uniqueId.String()); err == nil {
   296  					newToken = val
   297  				}
   298  			}
   299  		}
   300  	}
   301  	return token.Claims, newToken, nil
   302  }
   303  
   304  // GetLoginFailures retrieves the login failure information from the cache. Any modifications to the LoginAttemps map must be done in a thread-safe manner.
   305  func (mgr *SessionManager) GetLoginFailures() map[string]LoginAttempts {
   306  	// Get failures from the cache
   307  	return mgr.storage.GetLoginAttempts()
   308  }
   309  
   310  func expireOldFailedAttempts(maxAge time.Duration, failures map[string]LoginAttempts) int {
   311  	expiredCount := 0
   312  	for key, attempt := range failures {
   313  		if time.Since(attempt.LastFailed) > maxAge*time.Second {
   314  			expiredCount++
   315  			delete(failures, key)
   316  		}
   317  	}
   318  	return expiredCount
   319  }
   320  
   321  // Protect admin user from login attempt reset caused by attempts to overflow cache in a brute force attack. Instead remove random non-admin to make room in cache.
   322  func pickRandomNonAdminLoginFailure(failures map[string]LoginAttempts, username string) *string {
   323  	idx := rand.Intn(len(failures) - 1)
   324  	i := 0
   325  	for key := range failures {
   326  		if i == idx {
   327  			if key == common.ArgoCDAdminUsername || key == username {
   328  				return pickRandomNonAdminLoginFailure(failures, username)
   329  			}
   330  			return &key
   331  		}
   332  		i++
   333  	}
   334  	return nil
   335  }
   336  
   337  // Updates the failure count for a given username. If failed is true, increases the counter. Otherwise, sets counter back to 0.
   338  func (mgr *SessionManager) updateFailureCount(username string, failed bool) {
   339  	mgr.failedLock.Lock()
   340  	defer mgr.failedLock.Unlock()
   341  
   342  	failures := mgr.GetLoginFailures()
   343  
   344  	// Expire old entries in the cache if we have a failure window defined.
   345  	if window := getLoginFailureWindow(); window > 0 {
   346  		count := expireOldFailedAttempts(window, failures)
   347  		if count > 0 {
   348  			log.Infof("Expired %d entries from session cache due to max age reached", count)
   349  		}
   350  	}
   351  
   352  	// If we exceed a certain cache size, we need to remove random entries to
   353  	// prevent overbloating the cache with fake entries, as this could lead to
   354  	// memory exhaustion and ultimately in a DoS. We remove a single entry to
   355  	// replace it with the new one.
   356  	if failed && len(failures) >= getMaximumCacheSize() {
   357  		log.Warnf("Session cache size exceeds %d entries, removing random entry", getMaximumCacheSize())
   358  		rmUser := pickRandomNonAdminLoginFailure(failures, username)
   359  		if rmUser != nil {
   360  			delete(failures, *rmUser)
   361  			log.Infof("Deleted entry for user %s from cache", *rmUser)
   362  		}
   363  	}
   364  
   365  	attempt, ok := failures[username]
   366  	if !ok {
   367  		attempt = LoginAttempts{FailCount: 0}
   368  	}
   369  
   370  	// On login failure, increase fail count and update last failed timestamp.
   371  	// On login success, remove the entry from the cache.
   372  	if failed {
   373  		attempt.FailCount++
   374  		attempt.LastFailed = time.Now()
   375  		failures[username] = attempt
   376  		log.Warnf("User %s failed login %d time(s)", username, attempt.FailCount)
   377  	} else if attempt.FailCount > 0 {
   378  		// Forget username for cache size enforcement, since entry in cache was deleted
   379  		delete(failures, username)
   380  	}
   381  
   382  	err := mgr.storage.SetLoginAttempts(failures)
   383  	if err != nil {
   384  		log.Errorf("Could not update login attempts: %v", err)
   385  	}
   386  }
   387  
   388  // Get the current login failure attempts for given username
   389  func (mgr *SessionManager) getFailureCount(username string) LoginAttempts {
   390  	mgr.failedLock.RLock()
   391  	defer mgr.failedLock.RUnlock()
   392  	failures := mgr.GetLoginFailures()
   393  	attempt, ok := failures[username]
   394  	if !ok {
   395  		attempt = LoginAttempts{FailCount: 0}
   396  	}
   397  	return attempt
   398  }
   399  
   400  // Calculate a login delay for the given login attempt
   401  func (mgr *SessionManager) exceededFailedLoginAttempts(attempt LoginAttempts) bool {
   402  	maxFails := getMaxLoginFailures()
   403  	failureWindow := getLoginFailureWindow()
   404  
   405  	// Whether we are in the failure window for given attempt
   406  	inWindow := func() bool {
   407  		if failureWindow == 0 || time.Since(attempt.LastFailed).Seconds() <= float64(failureWindow) {
   408  			return true
   409  		}
   410  		return false
   411  	}
   412  
   413  	// If we reached max failed attempts within failure window, we need to calc the delay
   414  	if attempt.FailCount >= maxFails && inWindow() {
   415  		return true
   416  	}
   417  
   418  	return false
   419  }
   420  
   421  // VerifyUsernamePassword verifies if a username/password combo is correct
   422  func (mgr *SessionManager) VerifyUsernamePassword(username string, password string) error {
   423  	if password == "" {
   424  		return status.Errorf(codes.Unauthenticated, blankPasswordError)
   425  	}
   426  	// Enforce maximum length of username on local accounts
   427  	if len(username) > maxUsernameLength {
   428  		return status.Errorf(codes.InvalidArgument, usernameTooLongError, maxUsernameLength)
   429  	}
   430  
   431  	start := time.Now()
   432  	if mgr.verificationDelayNoiseEnabled {
   433  		defer func() {
   434  			// introduces random delay to protect from timing-based user enumeration attack
   435  			delayNanoseconds := verificationDelayNoiseMin.Nanoseconds() +
   436  				int64(rand.Intn(int(verificationDelayNoiseMax.Nanoseconds()-verificationDelayNoiseMin.Nanoseconds())))
   437  			// take into account amount of time spent since the request start
   438  			delayNanoseconds = delayNanoseconds - time.Since(start).Nanoseconds()
   439  			if delayNanoseconds > 0 {
   440  				mgr.sleep(time.Duration(delayNanoseconds))
   441  			}
   442  		}()
   443  	}
   444  
   445  	attempt := mgr.getFailureCount(username)
   446  	if mgr.exceededFailedLoginAttempts(attempt) {
   447  		log.Warnf("User %s had too many failed logins (%d)", username, attempt.FailCount)
   448  		return InvalidLoginErr
   449  	}
   450  
   451  	account, err := mgr.settingsMgr.GetAccount(username)
   452  	if err != nil {
   453  		if errStatus, ok := status.FromError(err); ok && errStatus.Code() == codes.NotFound {
   454  			mgr.updateFailureCount(username, true)
   455  			err = InvalidLoginErr
   456  		}
   457  		// to prevent time-based user enumeration, we must perform a password
   458  		// hash cycle to keep response time consistent (if the function were
   459  		// to continue and not return here)
   460  		_, _ = passwordutil.HashPassword("for_consistent_response_time")
   461  		return err
   462  	}
   463  
   464  	valid, _ := passwordutil.VerifyPassword(password, account.PasswordHash)
   465  	if !valid {
   466  		mgr.updateFailureCount(username, true)
   467  		return InvalidLoginErr
   468  	}
   469  
   470  	if !account.Enabled {
   471  		return status.Errorf(codes.Unauthenticated, accountDisabled, username)
   472  	}
   473  
   474  	if !account.HasCapability(settings.AccountCapabilityLogin) {
   475  		return status.Errorf(codes.Unauthenticated, userDoesNotHaveCapability, username, settings.AccountCapabilityLogin)
   476  	}
   477  	mgr.updateFailureCount(username, false)
   478  	return nil
   479  }
   480  
   481  // AuthMiddlewareFunc returns a function that can be used as an
   482  // authentication middleware for HTTP requests.
   483  func (mgr *SessionManager) AuthMiddlewareFunc(disabled bool) func(http.Handler) http.Handler {
   484  	return func(h http.Handler) http.Handler {
   485  		return WithAuthMiddleware(disabled, mgr, h)
   486  	}
   487  }
   488  
   489  // TokenVerifier defines the contract to invoke token
   490  // verification logic
   491  type TokenVerifier interface {
   492  	VerifyToken(token string) (jwt.Claims, string, error)
   493  }
   494  
   495  // WithAuthMiddleware is an HTTP middleware used to ensure incoming
   496  // requests are authenticated before invoking the target handler. If
   497  // disabled is true, it will just invoke the next handler in the chain.
   498  func WithAuthMiddleware(disabled bool, authn TokenVerifier, next http.Handler) http.Handler {
   499  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   500  		if !disabled {
   501  			cookies := r.Cookies()
   502  			tokenString, err := httputil.JoinCookies(common.AuthCookieName, cookies)
   503  			if err != nil {
   504  				http.Error(w, "Auth cookie not found", http.StatusBadRequest)
   505  				return
   506  			}
   507  			claims, _, err := authn.VerifyToken(tokenString)
   508  			if err != nil {
   509  				http.Error(w, "Invalid token", http.StatusUnauthorized)
   510  				return
   511  			}
   512  			ctx := r.Context()
   513  			// Add claims to the context to inspect for RBAC
   514  			//nolint:staticcheck
   515  			ctx = context.WithValue(ctx, "claims", claims)
   516  			r = r.WithContext(ctx)
   517  		}
   518  		next.ServeHTTP(w, r)
   519  	})
   520  }
   521  
   522  // VerifyToken verifies if a token is correct. Tokens can be issued either from us or by an IDP.
   523  // We choose how to verify based on the issuer.
   524  func (mgr *SessionManager) VerifyToken(tokenString string) (jwt.Claims, string, error) {
   525  	parser := jwt.NewParser(jwt.WithoutClaimsValidation())
   526  	claims := jwt.MapClaims{}
   527  	_, _, err := parser.ParseUnverified(tokenString, &claims)
   528  	if err != nil {
   529  		return nil, "", err
   530  	}
   531  	// Get issuer from MapClaims
   532  	issuer, _ := claims["iss"].(string)
   533  	switch issuer {
   534  	case SessionManagerClaimsIssuer:
   535  		// Argo CD signed token
   536  		return mgr.Parse(tokenString)
   537  	default:
   538  		// IDP signed token
   539  		prov, err := mgr.provider()
   540  		if err != nil {
   541  			return nil, "", err
   542  		}
   543  
   544  		argoSettings, err := mgr.settingsMgr.GetSettings()
   545  		if err != nil {
   546  			return nil, "", fmt.Errorf("cannot access settings while verifying the token: %w", err)
   547  		}
   548  		if argoSettings == nil {
   549  			return nil, "", errors.New("settings are not available while verifying the token")
   550  		}
   551  
   552  		idToken, err := prov.Verify(tokenString, argoSettings)
   553  		// The token verification has failed. If the token has expired, we will
   554  		// return a dummy claims only containing a value for the issuer, so the
   555  		// UI can handle expired tokens appropriately.
   556  		if err != nil {
   557  			log.Warnf("Failed to verify token: %s", err)
   558  			tokenExpiredError := &oidc.TokenExpiredError{}
   559  			if errors.As(err, &tokenExpiredError) {
   560  				claims = jwt.MapClaims{
   561  					"iss": "sso",
   562  				}
   563  				return claims, "", common.ErrTokenVerification
   564  			}
   565  			return nil, "", common.ErrTokenVerification
   566  		}
   567  
   568  		var claims jwt.MapClaims
   569  		err = idToken.Claims(&claims)
   570  		if err != nil {
   571  			return nil, "", err
   572  		}
   573  		return claims, "", nil
   574  	}
   575  }
   576  
   577  func (mgr *SessionManager) provider() (oidcutil.Provider, error) {
   578  	if mgr.prov != nil {
   579  		return mgr.prov, nil
   580  	}
   581  	settings, err := mgr.settingsMgr.GetSettings()
   582  	if err != nil {
   583  		return nil, err
   584  	}
   585  	if !settings.IsSSOConfigured() {
   586  		return nil, errors.New("SSO is not configured")
   587  	}
   588  	mgr.prov = oidcutil.NewOIDCProvider(settings.IssuerURL(), mgr.client)
   589  	return mgr.prov, nil
   590  }
   591  
   592  func (mgr *SessionManager) RevokeToken(ctx context.Context, id string, expiringAt time.Duration) error {
   593  	return mgr.storage.RevokeToken(ctx, id, expiringAt)
   594  }
   595  
   596  func LoggedIn(ctx context.Context) bool {
   597  	return GetUserIdentifier(ctx) != "" && ctx.Value(AuthErrorCtxKey) == nil
   598  }
   599  
   600  // Username is a helper to extract a human readable username from a context
   601  func Username(ctx context.Context) string {
   602  	mapClaims, ok := mapClaims(ctx)
   603  	if !ok {
   604  		return ""
   605  	}
   606  	switch jwtutil.StringField(mapClaims, "iss") {
   607  	case SessionManagerClaimsIssuer:
   608  		return jwtutil.GetUserIdentifier(mapClaims)
   609  	default:
   610  		e := jwtutil.StringField(mapClaims, "email")
   611  		if e != "" {
   612  			return e
   613  		}
   614  		return jwtutil.GetUserIdentifier(mapClaims)
   615  	}
   616  }
   617  
   618  func Iss(ctx context.Context) string {
   619  	mapClaims, ok := mapClaims(ctx)
   620  	if !ok {
   621  		return ""
   622  	}
   623  	return jwtutil.StringField(mapClaims, "iss")
   624  }
   625  
   626  func Iat(ctx context.Context) (time.Time, error) {
   627  	mapClaims, ok := mapClaims(ctx)
   628  	if !ok {
   629  		return time.Time{}, errors.New("unable to extract token claims")
   630  	}
   631  	return jwtutil.IssuedAtTime(mapClaims)
   632  }
   633  
   634  // GetUserIdentifier returns the user identifier from context, prioritizing federated claims over subject
   635  func GetUserIdentifier(ctx context.Context) string {
   636  	mapClaims, ok := mapClaims(ctx)
   637  	if !ok {
   638  		return ""
   639  	}
   640  	return jwtutil.GetUserIdentifier(mapClaims)
   641  }
   642  
   643  func Groups(ctx context.Context, scopes []string) []string {
   644  	mapClaims, ok := mapClaims(ctx)
   645  	if !ok {
   646  		return nil
   647  	}
   648  	return jwtutil.GetGroups(mapClaims, scopes)
   649  }
   650  
   651  func mapClaims(ctx context.Context) (jwt.MapClaims, bool) {
   652  	claims, ok := ctx.Value("claims").(jwt.Claims)
   653  	if !ok {
   654  		return nil, false
   655  	}
   656  	mapClaims, err := jwtutil.MapClaims(claims)
   657  	if err != nil {
   658  		return nil, false
   659  	}
   660  	return mapClaims, true
   661  }