github.com/kiali/kiali@v1.84.0/business/authentication/openid_auth_controller.go (about) 1 package authentication 2 3 import ( 4 "context" 5 "crypto/sha256" 6 "crypto/tls" 7 "crypto/x509" 8 "encoding/json" 9 "errors" 10 "fmt" 11 "io" 12 "net/http" 13 "net/url" 14 "os" 15 "strings" 16 "time" 17 18 "github.com/go-jose/go-jose" 19 "github.com/go-jose/go-jose/jwt" 20 "github.com/gorilla/mux" 21 "golang.org/x/sync/singleflight" 22 "k8s.io/client-go/tools/clientcmd/api" 23 24 "github.com/kiali/kiali/business" 25 "github.com/kiali/kiali/config" 26 "github.com/kiali/kiali/kubernetes" 27 "github.com/kiali/kiali/kubernetes/cache" 28 "github.com/kiali/kiali/log" 29 "github.com/kiali/kiali/util" 30 "github.com/kiali/kiali/util/httputil" 31 ) 32 33 const ( 34 // OpenIdNonceCookieName is the cookie name used to store a nonce code 35 // when user is starting authentication with the external server. This code 36 // is used to mitigate replay attacks. 37 OpenIdNonceCookieName = config.TokenCookieName + "-openid-nonce" 38 39 // OpenIdServerCAFile is a certificate file used to connect to the OpenID server. 40 // This is for cases when the authentication server is using TLS with a self-signed 41 // certificate. 42 OpenIdServerCAFile = "/kiali-cabundle/openid-server-ca.crt" 43 ) 44 45 // cachedOpenIdKeySet stores the metadata obtained from the /.well-known/openid-configuration 46 // endpoint of the OpenId server. Once the metadata is obtained for the first time, subsequent 47 // retrievals are served from this cached value rather than doing another request to the 48 // metadata endpoint of the OpenId server. 49 var cachedOpenIdMetadata *openIdMetadata 50 51 // cachedOpenIdKeySet stores the public key sets used for verification of the received 52 // id_tokens from the OpenId server. Its purpose is to prevent repeated queries to the JWKS 53 // endpoint of the OpenId server. However, since the keys can rotate, this is refreshed 54 // each time an id_token is signed with a key that is not present in the cached key set. 55 var cachedOpenIdKeySet *jose.JSONWebKeySet 56 57 // openIdFlightGroup is used to synchronize different threads of different HTTP requests so 58 // that only one request active to the metadata or jwks endpoints of the OpenId server. This 59 // prevents fetching the same data twice at the same time. 60 var openIdFlightGroup singleflight.Group 61 62 // openIdMetadata is a helper struct to parse the response from the metadata 63 // endpoint /.well-known/openid-configuration of the OpenID server. 64 // This was borrowed from https://github.com/coreos/go-oidc/blob/8d771559cf6e5111c9b9159810d0e4538e7cdc82/oidc.go 65 // and some additional fields were added. 66 type openIdMetadata struct { 67 Issuer string `json:"issuer"` 68 AuthURL string `json:"authorization_endpoint"` 69 TokenURL string `json:"token_endpoint"` 70 JWKSURL string `json:"jwks_uri"` 71 UserInfoURL string `json:"userinfo_endpoint"` 72 Algorithms []string `json:"id_token_signing_alg_values_supported"` 73 74 // Some extra fields 75 ScopesSupported []string `json:"scopes_supported"` 76 ResponseTypesSupported []string `json:"response_types_supported"` 77 } 78 79 // oidcSessionPayload is a helper type used as session data storage. An instance 80 // of this type is used with the SessionPersistor for session creation and persistance. 81 type oidcSessionPayload struct { 82 // Subject is the resolved name of the user that logged into Kiali. 83 Subject string `json:"subject,omitempty"` 84 85 // Token is the string provided by the OpenId server. It can be the id_token or 86 // the access_token, depending on the Kiali configuration. If RBAC is enabled, 87 // this is the token that can be used against the Kubernetes API. 88 Token string `json:"token,omitempty"` 89 } 90 91 // badOidcRequest is a helper type implementing Go's error interface. It's used to assist in 92 // error handling on the OpenId authentication flow. Since authentication is initiated via 93 // Kiali's web_root, it is hard to differentiate between an auth callback versus a first user 94 // request to Kiali. So, if this error is raised, it indicates that the authentication 95 // is not going to be handled and the http request should be passed to the next handler in 96 // the chain of the web_root endpoint. 97 type badOidcRequest struct { 98 // Detail contains the description of the error. 99 Detail string 100 } 101 102 // Error returns the text representation of an badOidcRequest error. 103 func (e badOidcRequest) Error() string { 104 return e.Detail 105 } 106 107 // OpenIdAuthController contains the backing logic to implement 108 // Kiali's "openid" authentication strategy. Only 109 // the authorization code flow is implemented. 110 // 111 // RBAC is supported, although it requires that the cluster is configured 112 // with OpenId integration. Thus, it is possible to turn off RBAC 113 // for simpler setups. 114 type OpenIdAuthController struct { 115 // SessionStore persists the session between HTTP requests. 116 SessionStore SessionPersistor 117 kialiCache cache.KialiCache 118 clientFactory kubernetes.ClientFactory 119 conf *config.Config 120 } 121 122 // NewOpenIdAuthController initializes a new controller for handling openid authentication, with the 123 // given persistor and the given businessInstantiator. The businessInstantiator can be nil and 124 // the initialized contoller will use the business.Get function. 125 func NewOpenIdAuthController(persistor SessionPersistor, kialiCache cache.KialiCache, clientFactory kubernetes.ClientFactory, conf *config.Config) *OpenIdAuthController { 126 return &OpenIdAuthController{ 127 SessionStore: persistor, 128 kialiCache: kialiCache, 129 clientFactory: clientFactory, 130 conf: conf, 131 } 132 } 133 134 // Authenticate was the entry point to handle OpenId authentication using the implicit flow. Support 135 // for the implicit flow has been removed. This is left here, because the "Authenticate" function is required 136 // by the AuthController interface which must be implemented by all auth controllers. So, this simply 137 // returns an error. 138 func (c OpenIdAuthController) Authenticate(r *http.Request, w http.ResponseWriter) (*UserSessionData, error) { 139 return nil, fmt.Errorf("support for OpenID's implicit flow has been removed") 140 } 141 142 // GetAuthCallbackHandler returns a http handler for authentication requests done to Kiali's web_root. 143 // This handler catches callbacks from the OpenId server. If it cannot be determined that the request 144 // is a callback from the authentication server, the request is passed to the fallbackHandler. 145 func (c OpenIdAuthController) GetAuthCallbackHandler(fallbackHandler http.Handler) http.Handler { 146 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 147 c.authenticateWithAuthorizationCodeFlow(r, w, fallbackHandler) 148 }) 149 } 150 151 // PostRoutes adds the additional endpoints needed on the Kiali's router 152 // in order to properly enable OpenId authentication. Only one new route is added to 153 // do a redirection from Kiali to the OpenId server to initiate authentication. 154 func (c OpenIdAuthController) PostRoutes(router *mux.Router) { 155 // swagger:route GET /auth/openid_redirect auth openidRedirect 156 // --- 157 // Endpoint to redirect the browser of the user to the authentication 158 // endpoint of the configured OpenId provider. 159 // 160 // Consumes: 161 // - application/json 162 // 163 // Produces: 164 // - application/html 165 // 166 // Schemes: http, https 167 // 168 // responses: 169 // 500: internalError 170 // 200: noContent 171 router. 172 Methods("GET"). 173 Path("/api/auth/openid_redirect"). 174 Name("OpenIdRedirect"). 175 HandlerFunc(c.redirectToAuthServerHandler) 176 } 177 178 // ValidateSession restores a session previously created by the Authenticate function. A sanity check of 179 // the id_token is performed if Kiali is not configured to use the access_token. Also, if RBAC is enabled, 180 // a privilege check is performed to verify that the user still has privileges to use Kiali. 181 // If the session is still valid, a populated UserSessionData is returned. Otherwise, nil is returned. 182 func (c OpenIdAuthController) ValidateSession(r *http.Request, w http.ResponseWriter) (*UserSessionData, error) { 183 // Restore a previously started session. 184 sPayload := oidcSessionPayload{} 185 sData, err := c.SessionStore.ReadSession(r, w, &sPayload) 186 if err != nil { 187 log.Warningf("Could not read the session: %v", err) 188 return nil, nil 189 } 190 if sData == nil { 191 return nil, nil 192 } 193 194 // The OpenId token must be present in the session 195 if len(sPayload.Token) == 0 { 196 log.Warning("Session is invalid: the OIDC token is absent") 197 return nil, nil 198 } 199 200 // If the id_token is being used to make calls to the cluster API, it's known that 201 // this token is a JWT and some of its structure; so, it's possible to do some sanity 202 // checks on the token. However, if the access_token is being used, this token is opaque 203 // and these sanity checks must be skipped. 204 if c.conf.Auth.OpenId.ApiToken != "access_token" { 205 // Parse the sid claim (id_token) to check that the sub claim matches to the configured "username" claim of the id_token 206 parsedOidcToken, err := jwt.ParseSigned(sPayload.Token) 207 if err != nil { 208 log.Warningf("Cannot parse sid claim of the OIDC token!: %v", err) 209 return nil, fmt.Errorf("cannot parse sid claim of the OIDC token: %w", err) 210 } 211 212 var claims map[string]interface{} // generic map to store parsed token 213 err = parsedOidcToken.UnsafeClaimsWithoutVerification(&claims) 214 if err != nil { 215 log.Warningf("Cannot parse the payload of the id_token: %v", err) 216 return nil, fmt.Errorf("cannot parse the payload of the id_token: %w", err) 217 } 218 219 if userClaim, ok := claims[c.conf.Auth.OpenId.UsernameClaim]; ok && sPayload.Subject != userClaim { 220 log.Warning("Kiali token rejected because of subject claim mismatch") 221 return nil, nil 222 } 223 } 224 225 var token string 226 if !c.conf.Auth.OpenId.DisableRBAC { 227 // If RBAC is ENABLED, check that the user has privileges on the cluster. 228 authInfo := &api.AuthInfo{Token: sPayload.Token} 229 userClients, err := c.clientFactory.GetClients(authInfo) 230 if err != nil { 231 log.Warningf("Could not get the business layer!!: %v", err) 232 return nil, fmt.Errorf("unable to create a Kubernetes client from the auth token: %w", err) 233 } 234 235 namespaceService := business.NewNamespaceService(userClients, c.clientFactory.GetSAClients(), c.kialiCache, c.conf) 236 _, err = namespaceService.GetNamespaces(r.Context()) 237 if err != nil { 238 log.Warningf("Token error!: %v", err) 239 return nil, nil 240 } 241 242 token = sPayload.Token 243 } else { 244 // If RBAC is off, it's assumed that the kubernetes cluster will reject the OpenId token. 245 // Instead, we use the Kiali token and this has the side effect that all users will share the 246 // same privileges. 247 token = c.clientFactory.GetSAHomeClusterClient().GetToken() 248 } 249 250 // Internal header used to propagate the subject of the request for audit purposes 251 r.Header.Add("Kiali-User", sPayload.Subject) 252 253 return &UserSessionData{ 254 ExpiresOn: sData.ExpiresOn, 255 Username: sPayload.Subject, 256 AuthInfo: &api.AuthInfo{Token: token}, 257 }, nil 258 } 259 260 // TerminateSession unconditionally terminates any existing session without any validation. 261 func (c OpenIdAuthController) TerminateSession(r *http.Request, w http.ResponseWriter) error { 262 c.SessionStore.TerminateSession(r, w) 263 return nil 264 } 265 266 // authenticateWithAuthorizationCodeFlow is the entry point to handle OpenId authentication using the authorization 267 // code flow. The HTTP request should contain "code" and "state" as URL parameters. Kiali will exchange the code 268 // for a token by contacting the OpenId server. If RBAC is enabled, the id_token should be valid to be used in the 269 // Kubernetes API (thus, privileges are verified to allow login); else, only token validity is checked and users will 270 // share the same privileges. 271 // An AuthenticationFailureError is returned if the authentication failed. Any 272 // other kind of error means that something unexpected happened. 273 func (c OpenIdAuthController) authenticateWithAuthorizationCodeFlow(r *http.Request, w http.ResponseWriter, fallbackHandler http.Handler) { 274 webRoot := c.conf.Server.WebRoot 275 webRootWithSlash := webRoot + "/" 276 277 flow := openidFlowHelper{kialiCache: c.kialiCache, conf: c.conf, clientFactory: c.clientFactory} 278 flow. 279 extractOpenIdCallbackParams(r). 280 checkOpenIdAuthorizationCodeFlowParams(). 281 // We cannot do a cleanup if we are not handling the auth here. So, 282 // the callbackCleanup func cannot be called before checkOpenIdAuthorizationCodeFlowParams(). 283 // It may sound reasonable to do a cleanup as early as possible (i.e. delete cookies), however 284 // if we do it, we break the "implicit" flow, because the requried cookies will no longer exist. 285 callbackCleanup(r, w). 286 validateOpenIdState(). 287 requestOpenIdToken(httputil.GuessKialiURL(c.conf, r)). 288 parseOpenIdToken(). 289 validateOpenIdNonceCode(). 290 checkAllowedDomains(). 291 checkUserPrivileges(). 292 createSession(r, w, c.SessionStore) 293 294 if flow.Error != nil { 295 if err, ok := flow.Error.(*badOidcRequest); ok { 296 log.Debugf("Not handling OpenId code flow authentication: %s", err.Detail) 297 fallbackHandler.ServeHTTP(w, r) 298 } else { 299 if flow.ShouldTerminateSession { 300 c.SessionStore.TerminateSession(r, w) 301 } 302 log.Warningf("Authentication rejected: %s", flow.Error.Error()) 303 http.Redirect(w, r, fmt.Sprintf("%s?openid_error=%s", webRootWithSlash, url.QueryEscape(flow.Error.Error())), http.StatusFound) 304 } 305 return 306 } 307 308 // Let's redirect (remove the openid params) to let the Kiali-UI to boot 309 http.Redirect(w, r, webRootWithSlash, http.StatusFound) 310 } 311 312 // redirectToAuthServerHandler prepares the redirection to initiate authentication with an OpenId Server. 313 // It finds what's the authentication endpoint of the OpenId server to redirect the user to. Then, creates 314 // the "nonce" and the "state" codes and forms the final URL to reply with a "302 Found" HTTP status and 315 // post the redirection in a "Location" HTTP header, with the needed parameters given the OpenId server 316 // capabilities. A Cookie is set to store the source of the calculated codes and be able to verify the 317 // authentication intent when the OpenId server calls back. 318 func (c OpenIdAuthController) redirectToAuthServerHandler(w http.ResponseWriter, r *http.Request) { 319 // This endpoint should be available only if OpenId strategy is configured 320 if c.conf.Auth.Strategy != config.AuthStrategyOpenId { 321 w.Header().Set("Content-Type", "text/plain") 322 w.WriteHeader(http.StatusNotFound) 323 _, _ = w.Write([]byte("OpenId strategy is not enabled")) 324 return 325 } 326 327 // Kiali only supports the authorization code flow. 328 if !isOpenIdCodeFlowPossible(c.conf) { 329 w.Header().Set("Content-Type", "text/plain") 330 w.WriteHeader(http.StatusNotImplemented) 331 _, _ = w.Write([]byte("Cannot start authentication because it is not possible to use OpenId's authorization code flow. Check Kiali logs for more details.")) 332 return 333 } 334 335 // Build scopes string 336 scopes := strings.Join(getConfiguredOpenIdScopes(c.conf), " ") 337 338 // Determine authorization endpoint 339 authorizationEndpoint := c.conf.Auth.OpenId.AuthorizationEndpoint 340 if len(authorizationEndpoint) == 0 { 341 openIdMetadata, err := getOpenIdMetadata(c.conf) 342 if err != nil { 343 w.Header().Set("Content-Type", "text/plain") 344 w.WriteHeader(http.StatusInternalServerError) 345 _, _ = w.Write([]byte("Error fetching OpenID provider metadata: " + err.Error())) 346 return 347 } 348 authorizationEndpoint = openIdMetadata.AuthURL 349 } 350 351 // Create a "nonce" code and set a cookie with the code 352 // It was chosen 15 chars arbitrarily. Probably, it's not worth to make this value configurable. 353 nonceCode, err := util.CryptoRandomString(15) 354 if err != nil { 355 w.Header().Set("Content-Type", "text/plain") 356 w.WriteHeader(http.StatusInternalServerError) 357 _, _ = w.Write([]byte("Random number generator failed")) 358 return 359 } 360 361 guessedKialiURL := httputil.GuessKialiURL(c.conf, r) 362 secureFlag := c.conf.IsServerHTTPS() || strings.HasPrefix(guessedKialiURL, "https:") 363 nowTime := util.Clock.Now() 364 expirationTime := nowTime.Add(time.Duration(c.conf.Auth.OpenId.AuthenticationTimeout) * time.Second) 365 nonceCookie := http.Cookie{ 366 Expires: expirationTime, 367 HttpOnly: true, 368 Secure: secureFlag, 369 Name: OpenIdNonceCookieName, 370 Path: c.conf.Server.WebRoot, 371 SameSite: http.SameSiteLaxMode, 372 Value: nonceCode, 373 } 374 http.SetCookie(w, &nonceCookie) 375 376 // Instead of sending the nonce code to the IdP, send a cryptographic hash. 377 // This way, if an attacker manages to steal the id_token returned by the IdP, he still 378 // needs to craft the cookie (which is hopefully very, very hard to do). 379 nonceHash := sha256.Sum224([]byte(nonceCode)) 380 381 // OpenID spec recommends the use of "state" parameter. Although it's just a recommendation, 382 // some identity providers have chosen to require the "state" parameter, effectively blocking 383 // authentication with Kiali. 384 // The state parameter is to mitigate CSRF attacks. Mitigation is usually done with 385 // a token and it's implementation *could* be similar to the nonce code, but this would 386 // require a second cookie. 387 // To reduce the usage of cookies, let's use the already generated nonce as a session_id, 388 // and the "nowTime" to generate a hash and use it as CSRF token. The Kiali's signing key is also used to 389 // add a component that is not traveling over the network. 390 // Although this "binds" the id_token returned by the IdP with the CSRF mitigation, this should be OK 391 // because we are including a "secret" key (i.e. should an attacker steal the nonce code, he still needs to know 392 // the Kiali's signing key). 393 csrfHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", nonceCode, nowTime.UTC().Format("060102150405"), getSigningKey(c.conf)))) 394 395 // Send redirection to browser 396 responseType := "code" // Request for the "authorization code" flow 397 redirectUri := fmt.Sprintf("%s?client_id=%s&response_type=%s&redirect_uri=%s&scope=%s&nonce=%s&state=%s", 398 authorizationEndpoint, 399 url.QueryEscape(c.conf.Auth.OpenId.ClientId), 400 responseType, 401 url.QueryEscape(guessedKialiURL), 402 url.QueryEscape(scopes), 403 url.QueryEscape(fmt.Sprintf("%x", nonceHash)), 404 url.QueryEscape(fmt.Sprintf("%x-%s", csrfHash, nowTime.UTC().Format("060102150405"))), 405 ) 406 407 if len(c.conf.Auth.OpenId.AdditionalRequestParams) > 0 { 408 urlParams := make([]string, 0, len(c.conf.Auth.OpenId.AdditionalRequestParams)) 409 for k, v := range c.conf.Auth.OpenId.AdditionalRequestParams { 410 urlParams = append(urlParams, fmt.Sprintf("%s=%s", url.QueryEscape(k), url.QueryEscape(v))) 411 } 412 redirectUri = fmt.Sprintf("%s&%s", redirectUri, strings.Join(urlParams, "&")) 413 } 414 415 http.Redirect(w, r, redirectUri, http.StatusFound) 416 } 417 418 // openidFlowHelper is a helper type to implement both the authorization code and the implicit 419 // flows of the OpenId specification. This is mainly for de-duplicating code. Previously, the same 420 // code was copied on two functions: one to handle implicit flow and one to handle authorization 421 // code flow. The differences were mainly because of the way error handling is done on each flow (one 422 // had to return an http response with a JSON error, while the other did http redirects). This helper 423 // uses Go errors and let the caller do the appropriate response, depending on the situation. 424 // Fields in this struct are filled and read as needed. 425 type openidFlowHelper struct { 426 // AccessToken stores the access_token returned by the OpenId server, if Kiali is 427 // configured to use it instead of the id_token. 428 AccessToken string 429 430 // Code is the authorization code provided during the callback of the authorization code flow. 431 Code string 432 433 // ExpiresOn is the expiration time of the id_token. 434 ExpiresOn time.Time 435 436 // IdToken is the identity token provided by the OpenId server, either during the callback 437 // of the implicit flow, or on the request to exchange the authorization code. 438 IdToken string 439 440 // Nonce is the code used to mitigate replay attacks. It's read from an HTTP Cookie. 441 Nonce string 442 443 // NonceHash is the sha256 hash of the nonce code. It is calculated after reading the nonce from its http cookie. 444 NonceHash []byte 445 446 // ParsedIdToken is the parsed form of the id_token, since it's known that it is a JWT. 447 ParsedIdToken *jwt.JSONWebToken 448 449 // IdTokenPayload holds the claims part of the id_token. 450 IdTokenPayload map[string]interface{} 451 452 // State is the code used to mitigate CSRF attacks. 453 State string 454 455 // Subject is the resolved username of the person that authenticated through an OpenId server. 456 Subject string 457 458 // UseAccessToken stores whether to use the OpenId access_token against the cluster API instead 459 // of the id_token. 460 UseAccessToken bool 461 462 // Error is nil unless there was an error during some phase of the authentication. A non-nil 463 // value cancels the authentication request. 464 Error error 465 466 // ShouldTerminateSession is set to a true value if an existing user session should be terminated 467 // as a consequence of a failure of a new authentication attempt (i.e if the Error field is not nil). 468 ShouldTerminateSession bool 469 470 kialiCache cache.KialiCache 471 clientFactory kubernetes.ClientFactory 472 conf *config.Config 473 } 474 475 // callbackCleanup deletes the nonce cookie that was generated during the redirection from Kiali to 476 // the OpenId server to initiate authentication (see OpenIdAuthController.redirectToAuthServerHandler). 477 func (p *openidFlowHelper) callbackCleanup(r *http.Request, w http.ResponseWriter) *openidFlowHelper { 478 // Do nothing if there was an error in previous flow steps. 479 if p.Error != nil { 480 return p 481 } 482 483 secureFlag := p.conf.IsServerHTTPS() || strings.HasPrefix(httputil.GuessKialiURL(p.conf, r), "https:") 484 485 // Delete the nonce cookie since we no longer need it. 486 deleteNonceCookie := http.Cookie{ 487 Name: OpenIdNonceCookieName, 488 Expires: time.Unix(0, 0), 489 HttpOnly: true, 490 Secure: secureFlag, 491 Path: p.conf.Server.WebRoot, 492 SameSite: http.SameSiteStrictMode, 493 Value: "", 494 } 495 http.SetCookie(w, &deleteNonceCookie) 496 497 return p 498 } 499 500 // extractOpenIdCallbackParams reads callback parameters from the HTTP request, once the OpenId server 501 // redirects back to Kiali with the credentials. It also reads the nonce cookie with the code generated 502 // during the initial redirection from Kiali to the OpenId Server (see OpenIdAuthController.redirectToAuthServerHandler). 503 func (p *openidFlowHelper) extractOpenIdCallbackParams(r *http.Request) *openidFlowHelper { 504 // Do nothing if there was an error in previous flow steps. 505 if p.Error != nil { 506 return p 507 } 508 509 var err error 510 511 // Get the nonce code hash 512 var nonceCookie *http.Cookie 513 if nonceCookie, err = r.Cookie(OpenIdNonceCookieName); err == nil { 514 p.Nonce = nonceCookie.Value 515 516 hash := sha256.Sum224([]byte(nonceCookie.Value)) 517 p.NonceHash = make([]byte, sha256.Size224) 518 copy(p.NonceHash, hash[:]) 519 } 520 521 // Parse/fetch received form data 522 err = r.ParseForm() 523 if err != nil { 524 err = &AuthenticationFailureError{ 525 HttpStatus: http.StatusBadRequest, 526 Reason: "failed to read OpenId callback params", 527 Detail: fmt.Errorf("error parsing form info: %w", err), 528 } 529 } else { 530 // Read relevant form data parameters 531 p.Code = r.Form.Get("code") 532 p.State = r.Form.Get("state") 533 } 534 535 p.Error = err 536 537 return p 538 } 539 540 // checkOpenIdAuthorizationCodeFlowParams verifies that the callback parameters for the authorization 541 // code flow are all present, as required by Kiali. 542 func (p *openidFlowHelper) checkOpenIdAuthorizationCodeFlowParams() *openidFlowHelper { 543 // Do nothing if there was an error in previous flow steps. 544 if p.Error != nil { 545 return p 546 } 547 if p.NonceHash == nil { 548 p.Error = &badOidcRequest{Detail: "no nonce code present - login window may have timed out"} 549 } 550 if p.State == "" { 551 p.Error = &badOidcRequest{Detail: "state parameter is empty or invalid"} 552 } 553 554 if p.Code == "" { 555 p.Error = &badOidcRequest{Detail: "no authorization code is present"} 556 } 557 558 return p 559 } 560 561 // checkAllowedDomains verifies that the "hd" or the "email" claims of the id_token (with 562 // priority for the "hd" claim) contain a domain from a list of predefined domains that 563 // are allowed to login into Kiali. 564 // 565 // The list of allowed domains can be specified in the 566 // Kiali CR and is useful for public auth servers that accept credentials from any 567 // of their registered users (from any organization), even if Kiali was registered under a 568 // specific organization account. 569 func (p *openidFlowHelper) checkAllowedDomains() *openidFlowHelper { 570 // Do nothing if there was an error in previous flow steps. 571 if p.Error != nil { 572 return p 573 } 574 575 if len(p.conf.Auth.OpenId.AllowedDomains) > 0 { 576 if err := checkDomain(p.IdTokenPayload, p.conf.Auth.OpenId.AllowedDomains); err != nil { 577 p.Error = &AuthenticationFailureError{Reason: err.Error()} 578 } 579 } 580 581 return p 582 } 583 584 // checkUserPrivileges verifies the privileges of the OpenId token, or validity of the token, 585 // depending if RBAC is enabled. 586 // 587 // If RBAC is enabled, either the id_token or the access_token (as specified by the api_token in 588 // the config) is tested against the cluster API to check if the user has enough privileges 589 // to log in to Kiali. 590 // 591 // If RBAC is disabled, then only validity of the id_token is verified (see validateOpenIdTokenInHouse). 592 func (p *openidFlowHelper) checkUserPrivileges() *openidFlowHelper { 593 // Do nothing if there was an error in previous flow steps. 594 if p.Error != nil { 595 return p 596 } 597 598 p.UseAccessToken = false 599 if p.conf.Auth.OpenId.DisableRBAC { 600 // When RBAC is on, we delegate some validations to the Kubernetes cluster. However, if RBAC is off 601 // the token must be fully validated, as we no longer pass the OpenId token to the cluster API server. 602 // Since the configuration indicates RBAC is off, we do the validations: 603 err := validateOpenIdTokenInHouse(p) 604 if err != nil { 605 p.Error = &AuthenticationFailureError{ 606 HttpStatus: http.StatusForbidden, 607 Reason: "the OpenID token was rejected", 608 Detail: err, 609 } 610 return p 611 } 612 } else { 613 // Check if user trying to login has enough privileges to login. This check is only done if 614 // config indicates that RBAC is on. For cases where RBAC is off, we simply assume that the 615 // Kiali ServiceAccount token should have enough privileges and skip this privilege check. 616 apiToken := p.IdToken 617 if p.conf.Auth.OpenId.ApiToken == "access_token" { 618 apiToken = p.AccessToken 619 p.UseAccessToken = true 620 } 621 httpStatus, errMsg, detailedError := verifyOpenIdUserAccess(apiToken, p.clientFactory, p.kialiCache, p.conf) 622 if httpStatus != http.StatusOK { 623 p.Error = &AuthenticationFailureError{ 624 HttpStatus: httpStatus, 625 Reason: errMsg, 626 Detail: detailedError, 627 } 628 return p 629 } 630 } 631 632 return p 633 } 634 635 // createSession asks the SessionPersistor to start a session. 636 func (p *openidFlowHelper) createSession(r *http.Request, w http.ResponseWriter, sessionStore SessionPersistor) *oidcSessionPayload { 637 // Do nothing if there was an error in previous flow steps. 638 if p.Error != nil { 639 return nil 640 } 641 642 sPayload := buildSessionPayload(p) 643 err := sessionStore.CreateSession(r, w, config.AuthStrategyOpenId, p.ExpiresOn, sPayload) 644 if err != nil { 645 p.Error = err 646 } 647 648 return sPayload 649 } 650 651 // parseOpenIdToken parses the OpenId id_token which is a JWT. This is to extract it's claims 652 // and be able to process them in later steps of the authentication flow. 653 func (p *openidFlowHelper) parseOpenIdToken() *openidFlowHelper { 654 // Do nothing if there was an error in previous flow steps. 655 if p.Error != nil { 656 return p 657 } 658 659 // Parse the received id_token from the IdP (it is a JWT token) without validating its signature 660 parsedOidcToken, err := jwt.ParseSigned(p.IdToken) 661 if err != nil { 662 p.Error = &AuthenticationFailureError{ 663 Reason: "cannot parse received id_token from the OpenId provider", 664 Detail: err, 665 } 666 p.ShouldTerminateSession = true 667 return p 668 } 669 p.ParsedIdToken = parsedOidcToken 670 671 var claims map[string]interface{} // generic map to store parsed token 672 err = parsedOidcToken.UnsafeClaimsWithoutVerification(&claims) 673 if err != nil { 674 p.Error = &AuthenticationFailureError{ 675 Reason: "cannot parse the payload of the id_token from the OpenId provider", 676 Detail: err, 677 } 678 p.ShouldTerminateSession = true 679 return p 680 } 681 p.IdTokenPayload = claims 682 683 // Extract expiration date from the OpenId token 684 if expClaim, ok := claims["exp"]; !ok { 685 p.Error = &AuthenticationFailureError{ 686 Reason: "the received id_token from the OpenId provider has missing the required 'exp' claim", 687 } 688 p.ShouldTerminateSession = true 689 return p 690 } else { 691 // If the expiration date is present on the claim, we use that 692 expiresInNumber, err := parseTimeClaim(expClaim) 693 if err != nil { 694 p.Error = &AuthenticationFailureError{ 695 Reason: "token exp claim is present, but invalid", 696 Detail: err, 697 } 698 p.ShouldTerminateSession = true 699 return p 700 } 701 702 p.ExpiresOn = time.Unix(expiresInNumber, 0) 703 } 704 705 // Extract the name of the user from the id_token. The "subject" is passed to the front-end to be displayed. 706 p.Subject = "OpenId User" // Set a default value 707 if userClaim, ok := claims[p.conf.Auth.OpenId.UsernameClaim]; ok && len(userClaim.(string)) > 0 { 708 p.Subject = userClaim.(string) 709 } 710 711 return p 712 } 713 714 // validateOpenIdNonceCode checks that the nonce hash that is present in the id_token is the right 715 // hash, given the nonce code present in the http cookie. 716 // 717 // This is the replay attack mitigation. 718 func (p *openidFlowHelper) validateOpenIdNonceCode() *openidFlowHelper { 719 // Do nothing if there was an error in previous flow steps. 720 if p.Error != nil { 721 return p 722 } 723 724 // Parse the received id_token from the IdP and check nonce code 725 nonceHashHex := fmt.Sprintf("%x", p.NonceHash) 726 if nonceClaim, ok := p.IdTokenPayload["nonce"]; !ok || nonceHashHex != nonceClaim.(string) { 727 p.Error = &AuthenticationFailureError{ 728 HttpStatus: http.StatusForbidden, 729 Reason: "OpenId token rejected: nonce code mismatch", 730 } 731 } 732 return p 733 } 734 735 // validateOpenIdState verifies that the "state" parameter passed during the callback to Kiali 736 // has the expected value, given the value of the nonce cookie and Kiali's signing key. 737 // 738 // This is the CSRF attack mitigation. 739 func (p *openidFlowHelper) validateOpenIdState() *openidFlowHelper { 740 // Do nothing if there was an error in previous flow steps. 741 if p.Error != nil { 742 return p 743 } 744 745 state := p.State 746 747 separator := strings.LastIndexByte(state, '-') 748 if separator != -1 { 749 csrfToken, timestamp := state[:separator], state[separator+1:] 750 csrfHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", p.Nonce, timestamp, getSigningKey(p.conf)))) 751 752 if fmt.Sprintf("%x", csrfHash) != csrfToken { 753 p.Error = &AuthenticationFailureError{ 754 HttpStatus: http.StatusForbidden, 755 Reason: "Request rejected: CSRF mitigation", 756 } 757 } 758 } else { 759 p.Error = &AuthenticationFailureError{ 760 HttpStatus: http.StatusForbidden, 761 Reason: "Request rejected: State parameter is invalid", 762 } 763 } 764 765 return p 766 } 767 768 // requestOpenIdToken makes a request to the OpenId server to exchange the received code (of the 769 // authorization code flow) with a proper identity token (id_token) and an access_token (if applicable). 770 func (p *openidFlowHelper) requestOpenIdToken(redirect_uri string) *openidFlowHelper { 771 // Do nothing if there was an error in previous flow steps. 772 if p.Error != nil { 773 return p 774 } 775 776 oidcMeta, err := getOpenIdMetadata(p.conf) 777 if err != nil { 778 p.Error = err 779 return p 780 } 781 782 cfg := p.conf.Auth.OpenId 783 784 httpClient, err := createHttpClient(p.conf, oidcMeta.TokenURL) 785 if err != nil { 786 p.Error = fmt.Errorf("failure when creating http client to request open id token: %w", err) 787 return p 788 } 789 790 // Exchange authorization code for a token 791 requestParams := url.Values{} 792 requestParams.Set("code", p.Code) 793 requestParams.Set("grant_type", "authorization_code") 794 requestParams.Set("redirect_uri", redirect_uri) 795 if len(cfg.ClientSecret) == 0 { 796 requestParams.Set("client_id", cfg.ClientId) 797 } 798 799 tokenRequest, err := http.NewRequest(http.MethodPost, oidcMeta.TokenURL, strings.NewReader(requestParams.Encode())) 800 if err != nil { 801 p.Error = fmt.Errorf("failure when creating the token request: %w", err) 802 return p 803 } 804 805 if len(cfg.ClientSecret) > 0 { 806 tokenRequest.SetBasicAuth(url.QueryEscape(cfg.ClientId), url.QueryEscape(cfg.ClientSecret)) 807 } 808 809 tokenRequest.Header.Add("Content-Type", "application/x-www-form-urlencoded") 810 response, err := httpClient.Do(tokenRequest) 811 if err != nil { 812 p.Error = fmt.Errorf("failure when requesting token from IdP: %w", err) 813 return p 814 } 815 816 defer response.Body.Close() 817 rawTokenResponse, err := io.ReadAll(response.Body) 818 if err != nil { 819 p.Error = fmt.Errorf("failed to read token response from IdP: %w", err) 820 return p 821 } 822 823 if response.StatusCode != 200 { 824 log.Debugf("OpenId token request failed with response: %s", string(rawTokenResponse)) 825 p.Error = fmt.Errorf("request failed (HTTP response status = %s)", response.Status) 826 return p 827 } 828 829 // Parse token response 830 var tokenResponse struct { 831 IdToken string `json:"id_token"` 832 AccessToken string `json:"access_token"` 833 } 834 835 err = json.Unmarshal(rawTokenResponse, &tokenResponse) 836 if err != nil { 837 p.Error = fmt.Errorf("cannot parse OpenId token response: %w", err) 838 return p 839 } 840 841 if len(tokenResponse.IdToken) == 0 { 842 p.Error = errors.New("the IdP did not provide an id_token") 843 return p 844 } 845 846 p.IdToken = tokenResponse.IdToken 847 p.AccessToken = tokenResponse.AccessToken 848 return p 849 } 850 851 // buildSessionPayload returns a struct that should be used as a payload for a call to SessionPersistor.CreateSession. 852 // It contains enough data to restore a session started with the OpenId auth strategy. 853 func buildSessionPayload(openIdParams *openidFlowHelper) *oidcSessionPayload { 854 token := openIdParams.IdToken 855 if openIdParams.UseAccessToken { 856 token = openIdParams.AccessToken 857 } 858 859 return &oidcSessionPayload{ 860 Token: token, 861 Subject: openIdParams.Subject, 862 } 863 } 864 865 // checkDomain verifies that the "hd" or the "email" claims in tokenClaims contain a domain 866 // from the provided list in allowedDomains (with priority for the "hd" domain). 867 // 868 // See also: openidFlowHelper.checkAllowedDomains. 869 func checkDomain(tokenClaims map[string]interface{}, allowedDomains []string) error { 870 var hostedDomain string 871 foundDomain := false 872 if v, ok := tokenClaims["hd"]; ok { 873 hostedDomain = v.(string) 874 } else { 875 // domains like gmail.com don't have the hosted domain (hd) on claims 876 // fields, so we try to get the domain on email claim 877 var email string 878 if v, ok := tokenClaims["email"]; ok { 879 email = v.(string) 880 } 881 splitedEmail := strings.Split(email, "@") 882 if len(splitedEmail) < 2 { 883 return fmt.Errorf("cannot detect hosted domain on OpenID for the email %s ", email) 884 } 885 hostedDomain = splitedEmail[1] 886 } 887 for _, d := range allowedDomains { 888 if hostedDomain == d { 889 foundDomain = true 890 break 891 } 892 } 893 if !foundDomain { 894 return fmt.Errorf("domain %s not allowed to login", hostedDomain) 895 } 896 return nil 897 } 898 899 // createHttpClient is a helper for creating and configuring an http client that is ready 900 // to do requests to the url in toUrl, which should be and endpoint of the OpenId server. 901 func createHttpClient(conf *config.Config, toUrl string) (*http.Client, error) { 902 cfg := conf.Auth.OpenId 903 parsedUrl, err := url.Parse(toUrl) 904 if err != nil { 905 return nil, err 906 } 907 908 // Check if there is a user-configured custom certificate for the OpenID Server. Read it, if it exists 909 var cafile []byte 910 if _, customCaErr := os.Stat(OpenIdServerCAFile); customCaErr == nil { 911 var caReadErr error 912 if cafile, caReadErr = os.ReadFile(OpenIdServerCAFile); caReadErr != nil { 913 return nil, fmt.Errorf("failed to read the OpenId CA certificate: %w", caReadErr) 914 } 915 } else if !errors.Is(customCaErr, os.ErrNotExist) { 916 log.Warningf("Unable to read the provided OpenID Server CA file (%s). Ignoring...", customCaErr.Error()) 917 } 918 919 httpTransport := &http.Transport{} 920 if cfg.InsecureSkipVerifyTLS || cafile != nil { 921 var certPool *x509.CertPool 922 if cafile != nil { 923 certPool = x509.NewCertPool() 924 if ok := certPool.AppendCertsFromPEM(cafile); !ok { 925 return nil, fmt.Errorf("supplied OpenId CA file cannot be parsed") 926 } 927 } 928 929 httpTransport.TLSClientConfig = &tls.Config{ 930 InsecureSkipVerify: cfg.InsecureSkipVerifyTLS, 931 RootCAs: certPool, 932 } 933 } 934 935 if cfg.HTTPProxy != "" || cfg.HTTPSProxy != "" { 936 proxyFunc := getProxyForUrl(parsedUrl, cfg.HTTPProxy, cfg.HTTPSProxy) 937 httpTransport.Proxy = proxyFunc 938 } 939 940 httpClient := http.Client{ 941 Timeout: time.Second * 10, 942 Transport: httpTransport, 943 } 944 945 return &httpClient, nil 946 } 947 948 // isOpenIdCodeFlowPossible determines if the "authorization code" flow can be used 949 // to do user authentication. 950 func isOpenIdCodeFlowPossible(conf *config.Config) bool { 951 // Kiali's signing key length must be 16, 24 or 32 bytes in order to be able to use 952 // encoded cookies. 953 switch len(getSigningKey(conf)) { 954 case 16, 24, 32: 955 default: 956 log.Warningf("Cannot use OpenId authorization code flow because signing key is not 16, 24 nor 32 bytes long") 957 return false 958 } 959 960 // IdP provider's metadata must list "code" in it's supported response types 961 metadata, err := getOpenIdMetadata(conf) 962 if err != nil { 963 // On error, just inform that code flow is not possible 964 log.Warningf("Error when fetching OpenID provider's metadata: %s", err.Error()) 965 return false 966 } 967 968 for _, v := range metadata.ResponseTypesSupported { 969 if v == "code" { 970 return true 971 } 972 } 973 974 log.Warning("Cannot use the authorization code flow because the OpenID provider does not support the 'code' response type") 975 976 return false 977 } 978 979 // getConfiguredOpenIdScopes gets the list of scopes set in Kiali configuration making sure 980 // that the mandatory "openid" scope is present in the returned list. 981 func getConfiguredOpenIdScopes(conf *config.Config) []string { 982 cfg := conf.Auth.OpenId 983 scopes := cfg.Scopes 984 985 isOpenIdScopePresent := false 986 for _, s := range scopes { 987 if s == "openid" { 988 isOpenIdScopePresent = true 989 break 990 } 991 } 992 993 if !isOpenIdScopePresent { 994 scopes = append(scopes, "openid") 995 } 996 997 return scopes 998 } 999 1000 // getJwkFromKeySet retrieves the Key with the specified keyId from the OpenId server. The key 1001 // is used to verify the signature an id_token. 1002 // 1003 // The OpenId server publishes "key sets" which rotate constantly. This function fetches the currently 1004 // published key set and returns the key with the matching keyId, if found. 1005 // 1006 // The retrieved key sets are cached to prevent flooding the OpenId server. Key sets are 1007 // refreshed as needed, when the requested keyId is not available in the cached key set. 1008 // 1009 // See also getOpenIdJwks, validateOpenIdTokenInHouse. 1010 func getJwkFromKeySet(conf *config.Config, keyId string) (*jose.JSONWebKey, error) { 1011 // Helper function to find a key with a certain key id in a key-set. 1012 findJwkFunc := func(kid string, jwks *jose.JSONWebKeySet) *jose.JSONWebKey { 1013 for _, key := range jwks.Keys { 1014 if key.KeyID == kid { 1015 return &key 1016 } 1017 } 1018 return nil 1019 } 1020 1021 if cachedOpenIdKeySet != nil { 1022 // If key-set is cached, try to find the key in the cached key-set 1023 foundKey := findJwkFunc(keyId, cachedOpenIdKeySet) 1024 if foundKey != nil { 1025 return foundKey, nil 1026 } 1027 } 1028 1029 // If key-set is not cached, or if the requested key was not found in the 1030 // cached key-set, then fetch/refresh the key-set from the OpenId provider 1031 keySet, err := getOpenIdJwks(conf) 1032 if err != nil { 1033 return nil, err 1034 } 1035 1036 // Try to find the key in the fetched key-set 1037 foundKey := findJwkFunc(keyId, keySet) 1038 1039 // "foundKey" can be nil. That's acceptable if the key-set does not contain the requested key id 1040 return foundKey, nil 1041 } 1042 1043 // getOpenIdJwks fetches the currently published key set from the OpenId server. 1044 // It's better to use the getJwkFromKeySet function rather than this one. 1045 func getOpenIdJwks(conf *config.Config) (*jose.JSONWebKeySet, error) { 1046 fetchedKeySet, fetchError, _ := openIdFlightGroup.Do("jwks", func() (interface{}, error) { 1047 oidcMetadata, err := getOpenIdMetadata(conf) 1048 if err != nil { 1049 return nil, err 1050 } 1051 1052 // Create HTTP client 1053 httpClient, err := createHttpClient(conf, oidcMetadata.JWKSURL) 1054 if err != nil { 1055 return nil, fmt.Errorf("failed to create http client to fetch OpenId JWKS document: %w", err) 1056 } 1057 1058 // Fetch Keys document 1059 response, err := httpClient.Get(oidcMetadata.JWKSURL) 1060 if err != nil { 1061 return nil, err 1062 } 1063 1064 defer response.Body.Close() 1065 if response.StatusCode != 200 { 1066 return nil, fmt.Errorf("cannot fetch OpenId JWKS document (HTTP response status = %s)", response.Status) 1067 } 1068 1069 // Parse the Keys document 1070 var oidcKeys jose.JSONWebKeySet 1071 1072 rawMetadata, err := io.ReadAll(response.Body) 1073 if err != nil { 1074 return nil, fmt.Errorf("failed to read OpenId JWKS document: %s", err.Error()) 1075 } 1076 1077 err = json.Unmarshal(rawMetadata, &oidcKeys) 1078 if err != nil { 1079 return nil, fmt.Errorf("cannot parse OpenId JWKS document: %s", err.Error()) 1080 } 1081 1082 cachedOpenIdKeySet = &oidcKeys // Store the keyset in a "cache" 1083 return cachedOpenIdKeySet, nil 1084 }) 1085 1086 if fetchError != nil { 1087 return nil, fetchError 1088 } 1089 1090 return fetchedKeySet.(*jose.JSONWebKeySet), nil 1091 } 1092 1093 // getOpenIdMetadata fetches the OpenId metadata using the configured Issuer URI and 1094 // downloading the metadata from the well-known path '/.well-known/openid-configuration'. Some 1095 // validations are performed and the parsed metadata is returned. Since the metadata should be 1096 // rare to change, the retrieved metadata is cached on first call and subsequent calls return 1097 // the cached metadata. 1098 func getOpenIdMetadata(conf *config.Config) (*openIdMetadata, error) { 1099 if cachedOpenIdMetadata != nil { 1100 return cachedOpenIdMetadata, nil 1101 } 1102 1103 fetchedMetadata, fetchError, _ := openIdFlightGroup.Do("metadata", func() (interface{}, error) { 1104 cfg := conf.Auth.OpenId 1105 1106 // Remove trailing slash from issuer URI, if needed 1107 trimmedIssuerUri := strings.TrimRight(cfg.IssuerUri, "/") 1108 1109 httpClient, err := createHttpClient(conf, trimmedIssuerUri) 1110 if err != nil { 1111 return nil, fmt.Errorf("failed to create http client to fetch OpenId Metadata: %w", err) 1112 } 1113 1114 // Fetch IdP metadata 1115 response, err := httpClient.Get(trimmedIssuerUri + "/.well-known/openid-configuration") 1116 if err != nil { 1117 return nil, err 1118 } 1119 1120 defer response.Body.Close() 1121 if response.StatusCode != 200 { 1122 return nil, fmt.Errorf("cannot fetch OpenId Metadata (HTTP response status = %s)", response.Status) 1123 } 1124 1125 // Parse JSON document 1126 var metadata openIdMetadata 1127 1128 rawMetadata, err := io.ReadAll(response.Body) 1129 if err != nil { 1130 return nil, fmt.Errorf("failed to read OpenId Metadata: %s", err.Error()) 1131 } 1132 1133 err = json.Unmarshal(rawMetadata, &metadata) 1134 if err != nil { 1135 return nil, fmt.Errorf("cannot parse OpenId Metadata: %s", err.Error()) 1136 } 1137 1138 // Validate issuer == issuerUri 1139 if metadata.Issuer != cfg.IssuerUri { 1140 return nil, fmt.Errorf("mismatch between the configured issuer_uri (%s) and the exposed Issuer URI in OpenId provider metadata (%s)", cfg.IssuerUri, metadata.Issuer) 1141 } 1142 1143 // Validate there is an authorization endpoint 1144 if len(metadata.AuthURL) == 0 { 1145 return nil, errors.New("the OpenID provider does not expose an authorization endpoint") 1146 } 1147 1148 // Log warning if OpenId provider informs that some of the configured scopes are not supported 1149 // It's possible to try authentication. If metadata is right, the error will be evident to the user when trying to login. 1150 scopes := getConfiguredOpenIdScopes(conf) 1151 for _, scope := range scopes { 1152 isScopeSupported := false 1153 for _, supportedScope := range metadata.ScopesSupported { 1154 if scope == supportedScope { 1155 isScopeSupported = true 1156 break 1157 } 1158 } 1159 1160 if !isScopeSupported { 1161 log.Warning("Configured OpenID provider informs some of the configured scopes are unsupported. Users may not be able to login.") 1162 break 1163 } 1164 } 1165 1166 // Return parsed metadata 1167 cachedOpenIdMetadata = &metadata 1168 return cachedOpenIdMetadata, nil 1169 }) 1170 1171 if fetchError != nil { 1172 return nil, fetchError 1173 } 1174 1175 return fetchedMetadata.(*openIdMetadata), nil 1176 } 1177 1178 // getProxyForUrl returns a function which, in turn, returns the URL of the proxy server that should 1179 // be used to reach the targetURL. Both httpProxy and httpsProxy are URLs of proxy servers (can be the same). 1180 // The httpProxy is used if the targetURL has the plain HTTP protocol. The httpsProxy is used if the targetURL 1181 // has the secure HTTPS protocol. 1182 // 1183 // Proxies are used for environments where the cluster does not have direct access to the internet and 1184 // all out-of-cluster/non-internal traffic is required to go through a proxy server. 1185 func getProxyForUrl(targetURL *url.URL, httpProxy string, httpsProxy string) func(req *http.Request) (*url.URL, error) { 1186 return func(req *http.Request) (*url.URL, error) { 1187 var proxyUrl *url.URL 1188 var err error 1189 1190 if httpProxy != "" && targetURL.Scheme == "http" { 1191 proxyUrl, err = url.Parse(httpProxy) 1192 } else if httpsProxy != "" && targetURL.Scheme == "https" { 1193 proxyUrl, err = url.Parse(httpsProxy) 1194 } 1195 1196 if err != nil { 1197 return nil, err 1198 } 1199 1200 return proxyUrl, nil 1201 } 1202 } 1203 1204 // parseTimeClaim parses the "exp" claim of a JWT token. 1205 // 1206 // As it turns out, the response from time claims can be either a f64 and 1207 // a json.Number. With this, we take care of it, converting to the int64 1208 // that we need to use timestamps in go. 1209 func parseTimeClaim(claimValue interface{}) (int64, error) { 1210 var err error 1211 parsedTime := int64(0) 1212 1213 switch exp := claimValue.(type) { 1214 case float64: 1215 // This can not fail 1216 parsedTime = int64(exp) 1217 case json.Number: 1218 // This can fail, so we short-circuit if we get an invalid value. 1219 parsedTime, err = exp.Int64() 1220 if err != nil { 1221 return 0, err 1222 } 1223 default: 1224 return 0, errors.New("the 'exp' claim of the OpenId token has invalid type") 1225 } 1226 1227 return parsedTime, nil 1228 } 1229 1230 func verifyAudienceClaim(openIdParams *openidFlowHelper, oidCfg config.OpenIdConfig) error { 1231 if audienceClaim, ok := openIdParams.IdTokenPayload["aud"]; !ok { 1232 return errors.New("the OpenId token has no aud claim") 1233 } else { 1234 switch ac := audienceClaim.(type) { 1235 case string: 1236 if oidCfg.ClientId != ac { 1237 return fmt.Errorf("the OpenId token is not targeted for Kiali; got aud = '%s'", audienceClaim) 1238 } 1239 case []string: 1240 if len(ac) != 1 { 1241 return fmt.Errorf("the OpenId string token was rejected because it has more than one audience; got aud = %v", audienceClaim) 1242 } 1243 if oidCfg.ClientId != ac[0] { 1244 return fmt.Errorf("the OpenId string token is not targeted for Kiali; got []aud = '%v'", audienceClaim) 1245 } 1246 case []any: 1247 if len(audienceClaim.([]any)) != 1 { 1248 return fmt.Errorf("the OpenId token was rejected because it has more than one audience; got aud = %v", audienceClaim) 1249 } 1250 acStr := fmt.Sprintf("%v", audienceClaim.([]any)[0]) 1251 if oidCfg.ClientId != acStr { 1252 return fmt.Errorf("the OpenId token is not targeted for Kiali; got []aud = '%v'", acStr) 1253 } 1254 default: 1255 return fmt.Errorf("the OpenId token has an unexpected audience claim; value [%v] of type [%T]", audienceClaim, audienceClaim) 1256 } 1257 } 1258 1259 return nil 1260 } 1261 1262 // validateOpenIdTokenInHouse checks that the id_token provided by the OpenId server 1263 // is valid. Its claims are validated to check that the expected values are present. 1264 // If the claims look OK, the signature is checked against the key sets published by 1265 // the OpenId server. 1266 func validateOpenIdTokenInHouse(openIdParams *openidFlowHelper) error { 1267 oidCfg := openIdParams.conf.Auth.OpenId 1268 oidMetadata, err := getOpenIdMetadata(openIdParams.conf) 1269 if err != nil { 1270 return err 1271 } 1272 1273 // Check iss claim matches fetched metadata at discovery 1274 if issuerClaim, ok := openIdParams.IdTokenPayload["iss"].(string); !ok || issuerClaim != oidMetadata.Issuer { 1275 return fmt.Errorf("the OpenId token has unexpected issuer claim; got iss = '%s'", issuerClaim) 1276 } 1277 1278 // Check the aud claim contains our client-id 1279 if err := verifyAudienceClaim(openIdParams, oidCfg); err != nil { 1280 return err 1281 } 1282 1283 if len(openIdParams.ParsedIdToken.Headers) != 1 { 1284 return fmt.Errorf("the OpenId token has unexpected number of headers [%d]", len(openIdParams.ParsedIdToken.Headers)) 1285 } 1286 1287 // Currently, we only support tokens with an RSA signature with SHA-256, which is the default in the OIDC spec 1288 if openIdParams.ParsedIdToken.Headers[0].Algorithm != "RS256" { 1289 return fmt.Errorf("the OpenId token has unexpected alg header claim; got alg = '%s'", openIdParams.ParsedIdToken.Headers[0].Algorithm) 1290 } 1291 1292 // Check iat (issued at) claim 1293 if iatClaim, ok := openIdParams.IdTokenPayload["iat"]; !ok { 1294 return errors.New("the OpenId token has no iat claim or is invalid") 1295 } else { 1296 parsedIat, parseErr := parseTimeClaim(iatClaim) 1297 if parseErr != nil { 1298 return fmt.Errorf("the OpenId token has an invalid iat claim: %w", parseErr) 1299 } 1300 if parsedIat == 0 { 1301 // This is weird. This would mean an invalid type 1302 return fmt.Errorf("the OpenId token has an invalid value in the iat claim; got '%v'", iatClaim) 1303 } 1304 1305 // Let's do the minimal check to ensure that the token wasn't issued in the future 1306 // we add a little offset to "now" to add one minute tolerance 1307 iatTime := time.Unix(parsedIat, 0) 1308 nowTime := util.Clock.Now().Add(60 * time.Second) 1309 if iatTime.After(nowTime) { 1310 return fmt.Errorf("we don't like people living in the future - enjoy the present!; iat = '%d'", parsedIat) 1311 } 1312 } 1313 1314 // Check exp (expiration time) claim 1315 // The OIDC spec says: "The current time MUST be before the time represented by the exp Claim" 1316 // No tolerance for this check. 1317 if !util.Clock.Now().Before(openIdParams.ExpiresOn) { 1318 return fmt.Errorf("the OpenId token has expired; exp = '%s'", openIdParams.ExpiresOn.String()) 1319 } 1320 1321 // There are other claims that could be checked, but are not verified here: 1322 // - nonce: This should be verified regardless if RBAC is on/off. So, it's verified in 1323 // another part of the authentication flow. 1324 // - acr: we are not asking for this claim at authorization, so the IdP doesn't 1325 // need to provide it nor we need to verify it. 1326 // - auth_time: we are not asking for this claim at authorization, so the IdP doesn't 1327 // need to provide it nor we need to verify it. 1328 1329 // If execution flow reached this point, all claims look valid, but that won't guarantee that 1330 // the id_token hasn't been tampered. So, we check the signature to find if 1331 // the token is genuine 1332 if kidHeader := openIdParams.ParsedIdToken.Headers[0].KeyID; len(kidHeader) == 0 { 1333 return errors.New("the OpenId token is missing the kid header claim") 1334 } else { 1335 if jws, parseErr := jose.ParseSigned(openIdParams.IdToken); parseErr != nil { 1336 return fmt.Errorf("error when parsing the OpenId token: %w", parseErr) 1337 } else { 1338 if len(jws.Signatures) == 0 { 1339 return errors.New("an unsigned OpenId token is not acceptable") 1340 } 1341 1342 matchingKey, findKeyErr := getJwkFromKeySet(openIdParams.conf, kidHeader) 1343 if findKeyErr != nil { 1344 return fmt.Errorf("something went wrong when trying to find the key that signed the OpenId token: %w", findKeyErr) 1345 } 1346 if matchingKey == nil { 1347 return errors.New("the OpenId token is signed with an unknown key") 1348 } 1349 1350 _, signVerifyErr := jws.Verify(matchingKey) 1351 if signVerifyErr != nil { 1352 return fmt.Errorf("the signature of the OpenId token is invalid: %w", signVerifyErr) 1353 } 1354 } 1355 } 1356 1357 return nil 1358 } 1359 1360 // verifyOpenIdUserAccess checks that the provided token has enough privileges on the cluster to 1361 // allow a login to Kiali. 1362 func verifyOpenIdUserAccess(token string, clientFactory kubernetes.ClientFactory, kialiCache cache.KialiCache, conf *config.Config) (int, string, error) { 1363 authInfo := &api.AuthInfo{Token: token} 1364 userClients, err := clientFactory.GetClients(authInfo) 1365 if err != nil { 1366 return http.StatusInternalServerError, "Unable to create a Kubernetes client from the auth token", err 1367 } 1368 1369 namespaceService := business.NewNamespaceService(userClients, clientFactory.GetSAClients(), kialiCache, conf) 1370 1371 // Using the namespaces API to check if token is valid. In Kubernetes, the version API seems to allow 1372 // anonymous access, so it's not feasible to use the version API for token verification. 1373 nsList, err := namespaceService.GetNamespaces(context.TODO()) 1374 if err != nil { 1375 return http.StatusUnauthorized, "Token is not valid or is expired", err 1376 } 1377 1378 // If namespace list is empty, return unauthorized error 1379 if len(nsList) == 0 { 1380 return http.StatusUnauthorized, "Cannot view any namespaces. Please read Kiali's RBAC documentation for more details.", nil 1381 } 1382 1383 return http.StatusOK, "", nil 1384 }