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

     1  package session
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"math"
     9  	"math/rand"
    10  	"net"
    11  	"net/http"
    12  	"os"
    13  	"time"
    14  
    15  	oidc "github.com/coreos/go-oidc"
    16  	"github.com/dgrijalva/jwt-go/v4"
    17  	log "github.com/sirupsen/logrus"
    18  	"google.golang.org/grpc/codes"
    19  	"google.golang.org/grpc/status"
    20  
    21  	"github.com/argoproj/argo-cd/common"
    22  	"github.com/argoproj/argo-cd/pkg/client/listers/application/v1alpha1"
    23  	"github.com/argoproj/argo-cd/server/rbacpolicy"
    24  	"github.com/argoproj/argo-cd/util/cache/appstate"
    25  	"github.com/argoproj/argo-cd/util/dex"
    26  	"github.com/argoproj/argo-cd/util/env"
    27  	httputil "github.com/argoproj/argo-cd/util/http"
    28  	jwtutil "github.com/argoproj/argo-cd/util/jwt"
    29  	oidcutil "github.com/argoproj/argo-cd/util/oidc"
    30  	passwordutil "github.com/argoproj/argo-cd/util/password"
    31  	"github.com/argoproj/argo-cd/util/settings"
    32  )
    33  
    34  // SessionManager generates and validates JWT tokens for login sessions.
    35  type SessionManager struct {
    36  	settingsMgr                   *settings.SettingsManager
    37  	projectsLister                v1alpha1.AppProjectNamespaceLister
    38  	client                        *http.Client
    39  	prov                          oidcutil.Provider
    40  	storage                       UserStateStorage
    41  	sleep                         func(d time.Duration)
    42  	verificationDelayNoiseEnabled bool
    43  }
    44  
    45  type inMemoryUserStateStorage struct {
    46  	attempts map[string]LoginAttempts
    47  }
    48  
    49  func NewInMemoryUserStateStorage() *inMemoryUserStateStorage {
    50  	return &inMemoryUserStateStorage{attempts: map[string]LoginAttempts{}}
    51  }
    52  
    53  func (storage *inMemoryUserStateStorage) GetLoginAttempts(attempts *map[string]LoginAttempts) error {
    54  	*attempts = storage.attempts
    55  	return nil
    56  }
    57  
    58  func (storage *inMemoryUserStateStorage) SetLoginAttempts(attempts map[string]LoginAttempts) error {
    59  	storage.attempts = attempts
    60  	return nil
    61  }
    62  
    63  type UserStateStorage interface {
    64  	GetLoginAttempts(attempts *map[string]LoginAttempts) error
    65  	SetLoginAttempts(attempts map[string]LoginAttempts) error
    66  }
    67  
    68  // LoginAttempts is a timestamped counter for failed login attempts
    69  type LoginAttempts struct {
    70  	// Time of the last failed login
    71  	LastFailed time.Time `json:"lastFailed"`
    72  	// Number of consecutive login failures
    73  	FailCount int `json:"failCount"`
    74  }
    75  
    76  const (
    77  	// SessionManagerClaimsIssuer fills the "iss" field of the token.
    78  	SessionManagerClaimsIssuer = "argocd"
    79  
    80  	// 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.
    81  	invalidLoginError         = "Invalid username or password"
    82  	blankPasswordError        = "Blank passwords are not allowed"
    83  	accountDisabled           = "Account %s is disabled"
    84  	usernameTooLongError      = "Username is too long (%d bytes max)"
    85  	userDoesNotHaveCapability = "Account %s does not have %s capability"
    86  )
    87  
    88  const (
    89  	// Maximum length of username, too keep the cache's memory signature low
    90  	maxUsernameLength = 32
    91  	// The default maximum session cache size
    92  	defaultMaxCacheSize = 1000
    93  	// The default number of maximum login failures before delay kicks in
    94  	defaultMaxLoginFailures = 5
    95  	// The default time in seconds for the failure window
    96  	defaultFailureWindow = 300
    97  	// The password verification delay max
    98  	verificationDelayNoiseMin = 500 * time.Millisecond
    99  	// The password verification delay max
   100  	verificationDelayNoiseMax = 1000 * time.Millisecond
   101  
   102  	// environment variables to control rate limiter behaviour:
   103  
   104  	// Max number of login failures before login delay kicks in
   105  	envLoginMaxFailCount = "ARGOCD_SESSION_FAILURE_MAX_FAIL_COUNT"
   106  
   107  	// Number of maximum seconds the login is allowed to delay for. Default: 300 (5 minutes).
   108  	envLoginFailureWindowSeconds = "ARGOCD_SESSION_FAILURE_WINDOW_SECONDS"
   109  
   110  	// Max number of stored usernames
   111  	envLoginMaxCacheSize = "ARGOCD_SESSION_MAX_CACHE_SIZE"
   112  )
   113  
   114  var (
   115  	InvalidLoginErr = status.Errorf(codes.Unauthenticated, invalidLoginError)
   116  )
   117  
   118  // Returns the maximum cache size as number of entries
   119  func getMaximumCacheSize() int {
   120  	return env.ParseNumFromEnv(envLoginMaxCacheSize, defaultMaxCacheSize, 1, math.MaxInt32)
   121  }
   122  
   123  // Returns the maximum number of login failures before login delay kicks in
   124  func getMaxLoginFailures() int {
   125  	return env.ParseNumFromEnv(envLoginMaxFailCount, defaultMaxLoginFailures, 1, math.MaxInt32)
   126  }
   127  
   128  // Returns the number of maximum seconds the login is allowed to delay for
   129  func getLoginFailureWindow() time.Duration {
   130  	return time.Duration(env.ParseNumFromEnv(envLoginFailureWindowSeconds, defaultFailureWindow, 0, math.MaxInt32))
   131  }
   132  
   133  // NewSessionManager creates a new session manager from Argo CD settings
   134  func NewSessionManager(settingsMgr *settings.SettingsManager, projectsLister v1alpha1.AppProjectNamespaceLister, dexServerAddr string, storage UserStateStorage) *SessionManager {
   135  	s := SessionManager{
   136  		settingsMgr:                   settingsMgr,
   137  		storage:                       storage,
   138  		sleep:                         time.Sleep,
   139  		projectsLister:                projectsLister,
   140  		verificationDelayNoiseEnabled: true,
   141  	}
   142  	settings, err := settingsMgr.GetSettings()
   143  	if err != nil {
   144  		panic(err)
   145  	}
   146  	tlsConfig := settings.TLSConfig()
   147  	if tlsConfig != nil {
   148  		tlsConfig.InsecureSkipVerify = true
   149  	}
   150  	s.client = &http.Client{
   151  		Transport: &http.Transport{
   152  			TLSClientConfig: tlsConfig,
   153  			Proxy:           http.ProxyFromEnvironment,
   154  			Dial: (&net.Dialer{
   155  				Timeout:   30 * time.Second,
   156  				KeepAlive: 30 * time.Second,
   157  			}).Dial,
   158  			TLSHandshakeTimeout:   10 * time.Second,
   159  			ExpectContinueTimeout: 1 * time.Second,
   160  		},
   161  	}
   162  	if settings.DexConfig != "" {
   163  		s.client.Transport = dex.NewDexRewriteURLRoundTripper(dexServerAddr, s.client.Transport)
   164  	}
   165  	if os.Getenv(common.EnvVarSSODebug) == "1" {
   166  		s.client.Transport = httputil.DebugTransport{T: s.client.Transport}
   167  	}
   168  
   169  	return &s
   170  }
   171  
   172  // Create creates a new token for a given subject (user) and returns it as a string.
   173  // Passing a value of `0` for secondsBeforeExpiry creates a token that never expires.
   174  // The id parameter holds an optional unique JWT token identifier and stored as a standard claim "jti" in the JWT token.
   175  func (mgr *SessionManager) Create(subject string, secondsBeforeExpiry int64, id string) (string, error) {
   176  	// Create a new token object, specifying signing method and the claims
   177  	// you would like it to contain.
   178  	now := time.Now().UTC()
   179  	claims := jwt.StandardClaims{
   180  		IssuedAt:  jwt.At(now),
   181  		Issuer:    SessionManagerClaimsIssuer,
   182  		NotBefore: jwt.At(now),
   183  		Subject:   subject,
   184  		ID:        id,
   185  	}
   186  	if secondsBeforeExpiry > 0 {
   187  		expires := now.Add(time.Duration(secondsBeforeExpiry) * time.Second)
   188  		claims.ExpiresAt = jwt.At(expires)
   189  	}
   190  
   191  	return mgr.signClaims(claims)
   192  }
   193  
   194  type standardClaims struct {
   195  	Audience  jwt.ClaimStrings `json:"aud,omitempty"`
   196  	ExpiresAt int64            `json:"exp,omitempty"`
   197  	ID        string           `json:"jti,omitempty"`
   198  	IssuedAt  int64            `json:"iat,omitempty"`
   199  	Issuer    string           `json:"iss,omitempty"`
   200  	NotBefore int64            `json:"nbf,omitempty"`
   201  	Subject   string           `json:"sub,omitempty"`
   202  }
   203  
   204  func unixTimeOrZero(t *jwt.Time) int64 {
   205  	if t == nil {
   206  		return 0
   207  	}
   208  	return t.Unix()
   209  }
   210  
   211  func (mgr *SessionManager) signClaims(claims jwt.Claims) (string, error) {
   212  	// log.Infof("Issuing claims: %v", claims)
   213  	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
   214  	settings, err := mgr.settingsMgr.GetSettings()
   215  	if err != nil {
   216  		return "", err
   217  	}
   218  	// workaround for https://github.com/argoproj/argo-cd/issues/5217
   219  	// According to https://tools.ietf.org/html/rfc7519#section-4.1.6 "iat" and other time fields must contain
   220  	// number of seconds from 1970-01-01T00:00:00Z UTC until the specified UTC date/time.
   221  	// The https://github.com/dgrijalva/jwt-go marshals time as non integer.
   222  	return token.SignedString(settings.ServerSignature, jwt.WithMarshaller(func(ctx jwt.CodingContext, v interface{}) ([]byte, error) {
   223  		if std, ok := v.(jwt.StandardClaims); ok {
   224  			return json.Marshal(standardClaims{
   225  				Audience:  std.Audience,
   226  				ExpiresAt: unixTimeOrZero(std.ExpiresAt),
   227  				ID:        std.ID,
   228  				IssuedAt:  unixTimeOrZero(std.IssuedAt),
   229  				Issuer:    std.Issuer,
   230  				NotBefore: unixTimeOrZero(std.NotBefore),
   231  				Subject:   std.Subject,
   232  			})
   233  		}
   234  		return json.Marshal(v)
   235  	}))
   236  }
   237  
   238  // Parse tries to parse the provided string and returns the token claims for local login.
   239  func (mgr *SessionManager) Parse(tokenString string) (jwt.Claims, error) {
   240  	// Parse takes the token string and a function for looking up the key. The latter is especially
   241  	// useful if you use multiple keys for your application.  The standard is to use 'kid' in the
   242  	// head of the token to identify which key to use, but the parsed token (head and claims) is provided
   243  	// to the callback, providing flexibility.
   244  	var claims jwt.MapClaims
   245  	argoCDSettings, err := mgr.settingsMgr.GetSettings()
   246  	if err != nil {
   247  		return nil, err
   248  	}
   249  	token, err := jwt.ParseWithClaims(tokenString, &claims, func(token *jwt.Token) (interface{}, error) {
   250  		// Don't forget to validate the alg is what you expect:
   251  		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
   252  			return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
   253  		}
   254  		return argoCDSettings.ServerSignature, nil
   255  	})
   256  	if err != nil {
   257  		return nil, err
   258  	}
   259  
   260  	issuedAt, err := jwtutil.IssuedAtTime(claims)
   261  	if err != nil {
   262  		return nil, err
   263  	}
   264  
   265  	subject := jwtutil.StringField(claims, "sub")
   266  	id := jwtutil.StringField(claims, "jti")
   267  
   268  	if projName, role, ok := rbacpolicy.GetProjectRoleFromSubject(subject); ok {
   269  		proj, err := mgr.projectsLister.Get(projName)
   270  		if err != nil {
   271  			return nil, err
   272  		}
   273  		_, _, err = proj.GetJWTToken(role, issuedAt.Unix(), id)
   274  		if err != nil {
   275  			return nil, err
   276  		}
   277  
   278  		return token.Claims, nil
   279  	}
   280  
   281  	account, err := mgr.settingsMgr.GetAccount(subject)
   282  	if err != nil {
   283  		return nil, err
   284  	}
   285  
   286  	if !account.Enabled {
   287  		return nil, fmt.Errorf("account %s is disabled", subject)
   288  	}
   289  
   290  	var capability settings.AccountCapability
   291  	if id != "" {
   292  		capability = settings.AccountCapabilityApiKey
   293  	} else {
   294  		capability = settings.AccountCapabilityLogin
   295  	}
   296  	if !account.HasCapability(capability) {
   297  		return nil, fmt.Errorf("account %s does not have '%s' capability", subject, capability)
   298  	}
   299  
   300  	if id != "" && account.TokenIndex(id) == -1 {
   301  		return nil, fmt.Errorf("account %s does not have token with id %s", subject, id)
   302  	}
   303  
   304  	if account.PasswordMtime != nil && issuedAt.Before(*account.PasswordMtime) {
   305  		return nil, fmt.Errorf("Account password has changed since token issued")
   306  	}
   307  	return token.Claims, nil
   308  }
   309  
   310  // GetLoginFailures retrieves the login failure information from the cache
   311  func (mgr *SessionManager) GetLoginFailures() map[string]LoginAttempts {
   312  	// Get failures from the cache
   313  	var failures map[string]LoginAttempts
   314  	err := mgr.storage.GetLoginAttempts(&failures)
   315  	if err != nil {
   316  		if err != appstate.ErrCacheMiss {
   317  			log.Errorf("Could not retrieve login attempts: %v", err)
   318  		}
   319  		failures = make(map[string]LoginAttempts)
   320  	}
   321  
   322  	return failures
   323  }
   324  
   325  func expireOldFailedAttempts(maxAge time.Duration, failures *map[string]LoginAttempts) int {
   326  	expiredCount := 0
   327  	for key, attempt := range *failures {
   328  		if time.Since(attempt.LastFailed) > maxAge*time.Second {
   329  			expiredCount += 1
   330  			delete(*failures, key)
   331  		}
   332  	}
   333  	return expiredCount
   334  }
   335  
   336  // Updates the failure count for a given username. If failed is true, increases the counter. Otherwise, sets counter back to 0.
   337  func (mgr *SessionManager) updateFailureCount(username string, failed bool) {
   338  
   339  	failures := mgr.GetLoginFailures()
   340  
   341  	// Expire old entries in the cache if we have a failure window defined.
   342  	if window := getLoginFailureWindow(); window > 0 {
   343  		count := expireOldFailedAttempts(window, &failures)
   344  		if count > 0 {
   345  			log.Infof("Expired %d entries from session cache due to max age reached", count)
   346  		}
   347  	}
   348  
   349  	// If we exceed a certain cache size, we need to remove random entries to
   350  	// prevent overbloating the cache with fake entries, as this could lead to
   351  	// memory exhaustion and ultimately in a DoS. We remove a single entry to
   352  	// replace it with the new one.
   353  	//
   354  	// Chances are that we remove the one that is under active attack, but this
   355  	// chance is low (1:cache_size)
   356  	if failed && len(failures) >= getMaximumCacheSize() {
   357  		log.Warnf("Session cache size exceeds %d entries, removing random entry", getMaximumCacheSize())
   358  		idx := rand.Intn(len(failures) - 1)
   359  		var rmUser string
   360  		i := 0
   361  		for key := range failures {
   362  			if i == idx {
   363  				rmUser = key
   364  				delete(failures, key)
   365  				break
   366  			}
   367  			i++
   368  		}
   369  		log.Infof("Deleted entry for user %s from cache", rmUser)
   370  	}
   371  
   372  	attempt, ok := failures[username]
   373  	if !ok {
   374  		attempt = LoginAttempts{FailCount: 0}
   375  	}
   376  
   377  	// On login failure, increase fail count and update last failed timestamp.
   378  	// On login success, remove the entry from the cache.
   379  	if failed {
   380  		attempt.FailCount += 1
   381  		attempt.LastFailed = time.Now()
   382  		failures[username] = attempt
   383  		log.Warnf("User %s failed login %d time(s)", username, attempt.FailCount)
   384  	} else {
   385  		if attempt.FailCount > 0 {
   386  			// Forget username for cache size enforcement, since entry in cache was deleted
   387  			delete(failures, username)
   388  		}
   389  	}
   390  
   391  	err := mgr.storage.SetLoginAttempts(failures)
   392  	if err != nil {
   393  		log.Errorf("Could not update login attempts: %v", err)
   394  	}
   395  
   396  }
   397  
   398  // Get the current login failure attempts for given username
   399  func (mgr *SessionManager) getFailureCount(username string) LoginAttempts {
   400  	failures := mgr.GetLoginFailures()
   401  	attempt, ok := failures[username]
   402  	if !ok {
   403  		attempt = LoginAttempts{FailCount: 0}
   404  	}
   405  	return attempt
   406  }
   407  
   408  // Calculate a login delay for the given login attempt
   409  func (mgr *SessionManager) exceededFailedLoginAttempts(attempt LoginAttempts) bool {
   410  	maxFails := getMaxLoginFailures()
   411  	failureWindow := getLoginFailureWindow()
   412  
   413  	// Whether we are in the failure window for given attempt
   414  	inWindow := func() bool {
   415  		if failureWindow == 0 || time.Since(attempt.LastFailed).Seconds() <= float64(failureWindow) {
   416  			return true
   417  		}
   418  		return false
   419  	}
   420  
   421  	// If we reached max failed attempts within failure window, we need to calc the delay
   422  	if attempt.FailCount >= maxFails && inWindow() {
   423  		return true
   424  	}
   425  
   426  	return false
   427  }
   428  
   429  // VerifyUsernamePassword verifies if a username/password combo is correct
   430  func (mgr *SessionManager) VerifyUsernamePassword(username string, password string) error {
   431  	if password == "" {
   432  		return status.Errorf(codes.Unauthenticated, blankPasswordError)
   433  	}
   434  	// Enforce maximum length of username on local accounts
   435  	if len(username) > maxUsernameLength {
   436  		return status.Errorf(codes.InvalidArgument, usernameTooLongError, maxUsernameLength)
   437  	}
   438  
   439  	start := time.Now()
   440  	if mgr.verificationDelayNoiseEnabled {
   441  		defer func() {
   442  			// introduces random delay to protect from timing-based user enumeration attack
   443  			delayNanoseconds := verificationDelayNoiseMin.Nanoseconds() +
   444  				int64(rand.Intn(int(verificationDelayNoiseMax.Nanoseconds()-verificationDelayNoiseMin.Nanoseconds())))
   445  				// take into account amount of time spent since the request start
   446  			delayNanoseconds = delayNanoseconds - time.Since(start).Nanoseconds()
   447  			if delayNanoseconds > 0 {
   448  				mgr.sleep(time.Duration(delayNanoseconds))
   449  			}
   450  		}()
   451  	}
   452  
   453  	attempt := mgr.getFailureCount(username)
   454  	if mgr.exceededFailedLoginAttempts(attempt) {
   455  		log.Warnf("User %s had too many failed logins (%d)", username, attempt.FailCount)
   456  		return InvalidLoginErr
   457  	}
   458  
   459  	account, err := mgr.settingsMgr.GetAccount(username)
   460  	if err != nil {
   461  		if errStatus, ok := status.FromError(err); ok && errStatus.Code() == codes.NotFound {
   462  			mgr.updateFailureCount(username, true)
   463  			err = InvalidLoginErr
   464  		}
   465  		// to prevent time-based user enumeration, we must perform a password
   466  		// hash cycle to keep response time consistent (if the function were
   467  		// to continue and not return here)
   468  		_, _ = passwordutil.HashPassword("for_consistent_response_time")
   469  		return err
   470  	}
   471  
   472  	valid, _ := passwordutil.VerifyPassword(password, account.PasswordHash)
   473  	if !valid {
   474  		mgr.updateFailureCount(username, true)
   475  		return InvalidLoginErr
   476  	}
   477  
   478  	if !account.Enabled {
   479  		return status.Errorf(codes.Unauthenticated, accountDisabled, username)
   480  	}
   481  
   482  	if !account.HasCapability(settings.AccountCapabilityLogin) {
   483  		return status.Errorf(codes.Unauthenticated, userDoesNotHaveCapability, username, settings.AccountCapabilityLogin)
   484  	}
   485  	mgr.updateFailureCount(username, false)
   486  	return nil
   487  }
   488  
   489  // VerifyToken verifies if a token is correct. Tokens can be issued either from us or by an IDP.
   490  // We choose how to verify based on the issuer.
   491  func (mgr *SessionManager) VerifyToken(tokenString string) (jwt.Claims, error) {
   492  	parser := &jwt.Parser{
   493  		ValidationHelper: jwt.NewValidationHelper(jwt.WithoutClaimsValidation(), jwt.WithoutAudienceValidation()),
   494  	}
   495  	var claims jwt.StandardClaims
   496  	_, _, err := parser.ParseUnverified(tokenString, &claims)
   497  	if err != nil {
   498  		return nil, err
   499  	}
   500  	switch claims.Issuer {
   501  	case SessionManagerClaimsIssuer:
   502  		// Argo CD signed token
   503  		return mgr.Parse(tokenString)
   504  	default:
   505  		// IDP signed token
   506  		prov, err := mgr.provider()
   507  		if err != nil {
   508  			return claims, err
   509  		}
   510  
   511  		// Token must be verified for at least one audience
   512  		// TODO(jannfis): Is this the right way? Shouldn't we know our audience and only validate for the correct one?
   513  		var idToken *oidc.IDToken
   514  		for _, aud := range claims.Audience {
   515  			idToken, err = prov.Verify(aud, tokenString)
   516  			if err == nil {
   517  				break
   518  			}
   519  		}
   520  		if err != nil {
   521  			return claims, err
   522  		}
   523  		var claims jwt.MapClaims
   524  		err = idToken.Claims(&claims)
   525  		return claims, err
   526  	}
   527  }
   528  
   529  func (mgr *SessionManager) provider() (oidcutil.Provider, error) {
   530  	if mgr.prov != nil {
   531  		return mgr.prov, nil
   532  	}
   533  	settings, err := mgr.settingsMgr.GetSettings()
   534  	if err != nil {
   535  		return nil, err
   536  	}
   537  	if !settings.IsSSOConfigured() {
   538  		return nil, fmt.Errorf("SSO is not configured")
   539  	}
   540  	mgr.prov = oidcutil.NewOIDCProvider(settings.IssuerURL(), mgr.client)
   541  	return mgr.prov, nil
   542  }
   543  
   544  func LoggedIn(ctx context.Context) bool {
   545  	return Sub(ctx) != ""
   546  }
   547  
   548  // Username is a helper to extract a human readable username from a context
   549  func Username(ctx context.Context) string {
   550  	mapClaims, ok := mapClaims(ctx)
   551  	if !ok {
   552  		return ""
   553  	}
   554  	switch jwtutil.StringField(mapClaims, "iss") {
   555  	case SessionManagerClaimsIssuer:
   556  		return jwtutil.StringField(mapClaims, "sub")
   557  	default:
   558  		return jwtutil.StringField(mapClaims, "email")
   559  	}
   560  }
   561  
   562  func Iss(ctx context.Context) string {
   563  	mapClaims, ok := mapClaims(ctx)
   564  	if !ok {
   565  		return ""
   566  	}
   567  	return jwtutil.StringField(mapClaims, "iss")
   568  }
   569  
   570  func Iat(ctx context.Context) (time.Time, error) {
   571  	mapClaims, ok := mapClaims(ctx)
   572  	if !ok {
   573  		return time.Time{}, errors.New("unable to extract token claims")
   574  	}
   575  	return jwtutil.IssuedAtTime(mapClaims)
   576  }
   577  
   578  func Sub(ctx context.Context) string {
   579  	mapClaims, ok := mapClaims(ctx)
   580  	if !ok {
   581  		return ""
   582  	}
   583  	return jwtutil.StringField(mapClaims, "sub")
   584  }
   585  
   586  func Groups(ctx context.Context, scopes []string) []string {
   587  	mapClaims, ok := mapClaims(ctx)
   588  	if !ok {
   589  		return nil
   590  	}
   591  	return jwtutil.GetGroups(mapClaims, scopes)
   592  }
   593  
   594  func mapClaims(ctx context.Context) (jwt.MapClaims, bool) {
   595  	claims, ok := ctx.Value("claims").(jwt.Claims)
   596  	if !ok {
   597  		return nil, false
   598  	}
   599  	mapClaims, err := jwtutil.MapClaims(claims)
   600  	if err != nil {
   601  		return nil, false
   602  	}
   603  	return mapClaims, true
   604  }