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 }