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 }