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  }