github.com/kiali/kiali@v1.84.0/business/authentication/openshift_auth_controller.go (about) 1 package authentication 2 3 import ( 4 "fmt" 5 "net/http" 6 "net/url" 7 "strings" 8 "time" 9 10 "github.com/gorilla/mux" 11 "golang.org/x/oauth2" 12 "k8s.io/client-go/tools/clientcmd/api" 13 14 "github.com/kiali/kiali/business" 15 "github.com/kiali/kiali/config" 16 "github.com/kiali/kiali/log" 17 "github.com/kiali/kiali/util" 18 ) 19 20 // openshiftSessionPayload holds the data that will be persisted in the SessionStore 21 // in order to be able to maintain the session of the user across requests. 22 type openshiftSessionPayload struct { 23 oauth2.Token 24 } 25 26 // OpenshiftAuthController contains the backing logic to implement 27 // Kiali's "openshift" authentication strategy. This authentication 28 // strategy is basically an implementation of OAuth's authorization 29 // code flow with the specifics of OpenShift. 30 // 31 // Alternatively, it is possible that 3rd-parties are controlling 32 // the session. For these cases, Kiali can receive an OpenShift token 33 // via the "Authorization" HTTP Header or via the "oauth_token" 34 // URL parameter. Token received from 3rd parties are not persisted 35 // with the active Kiali's persistor, because that would collide and 36 // replace an existing Kiali session. So, it is assumed that the 3rd-party 37 // has its own persistence system (similarly to how 'header' auth works). 38 type OpenshiftAuthController struct { 39 conf *config.Config 40 openshiftOAuth *business.OpenshiftOAuthService 41 // SessionStore persists the session between HTTP requests. 42 SessionStore SessionPersistor 43 } 44 45 // NewOpenshiftAuthController initializes a new controller for handling OpenShift authentication, with the 46 // given persistor and the given businessInstantiator. The businessInstantiator can be nil and 47 // the initialized contoller will use the business.Get function. 48 func NewOpenshiftAuthController(persistor SessionPersistor, openshiftOAuth *business.OpenshiftOAuthService, conf *config.Config) (*OpenshiftAuthController, error) { 49 return &OpenshiftAuthController{ 50 conf: conf, 51 openshiftOAuth: openshiftOAuth, 52 // TODO: Multi-cluster support. 53 SessionStore: persistor, 54 }, nil 55 } 56 57 // PostRoutes adds the additional endpoints needed on the Kiali's router 58 // in order to properly enable Openshift authentication. Only one new route is added to 59 // do a redirection from Kiali to the Openshift OAuth server to initiate authentication. 60 func (c OpenshiftAuthController) PostRoutes(router *mux.Router) { 61 // swagger:route GET /auth/openshift_redirect auth openshiftRedirect 62 // --- 63 // Endpoint to redirect the browser of the user to the authentication 64 // endpoint of the configured openshift provider. 65 // 66 // Consumes: 67 // - application/json 68 // 69 // Produces: 70 // - application/html 71 // 72 // Schemes: http, https 73 // 74 // responses: 75 // 500: internalError 76 // 200: noContent 77 router. 78 Methods("GET"). 79 Path("/api/auth/openshift_redirect"). 80 Name("OpenShiftAuthRedirect"). 81 HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 82 verifier := oauth2.GenerateVerifier() // Store in the session cookie 83 84 // Redirect user to consent page to ask for permission 85 // for the scopes specified above. 86 // TODO: Include cluster 87 url, err := c.openshiftOAuth.AuthCodeURL(verifier, c.conf.KubernetesConfig.ClusterName) 88 if err != nil { 89 log.Errorf("Error getting the AuthCodeURL: %v", err) 90 http.Error(w, "Internal error", http.StatusInternalServerError) 91 return 92 } 93 94 oAuthConfig, err := c.openshiftOAuth.OAuthConfig(c.conf.KubernetesConfig.ClusterName) 95 if err != nil { 96 log.Errorf("Error getting the OAuthConfig: %v", err) 97 http.Error(w, "Internal error", http.StatusInternalServerError) 98 return 99 } 100 101 // If redirect url is https, then we can assume that the endpoint is accepting https traffic 102 // and the cookie should be secure. 103 secureFlag := c.conf.IsServerHTTPS() || strings.HasPrefix(url, "https:") 104 105 nowTime := util.Clock.Now() 106 expirationTime := nowTime.Add(time.Duration(oAuthConfig.TokenAgeInSeconds) * time.Second) 107 108 // nonce cookie stores the verifier. 109 nonceCookie := http.Cookie{ 110 Expires: expirationTime, 111 HttpOnly: true, 112 Secure: secureFlag, 113 Name: OpenIdNonceCookieName, 114 Path: c.conf.Server.WebRoot, 115 // TODO: Can this be strict? 116 SameSite: http.SameSiteLaxMode, 117 // TODO: Possibly store cluster and any other state we need about the request in the cookie. 118 Value: verifier, 119 } 120 http.SetCookie(w, &nonceCookie) 121 http.Redirect(w, r, url, http.StatusFound) 122 }) 123 } 124 125 // GetAuthCallbackHandler will attempt to extract the nonce cookie and the code from the request. 126 // If neither one is present then it is assumed that the request is not a callback from the OAuth provider 127 // and the fallbackHandler is called instead. 128 // TODO: Supporting a separate login route for Kiali would obviate the need for the fallbackHandler. 129 func (c OpenshiftAuthController) GetAuthCallbackHandler(fallbackHandler http.Handler) http.Handler { 130 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 131 nonceCookie, err := r.Cookie(OpenIdNonceCookieName) 132 if err != nil { 133 log.Debugf("Not handling OAuth code flow authentication: could not get the nonce cookie: %v", err) 134 fallbackHandler.ServeHTTP(w, r) 135 return 136 } 137 138 code := r.FormValue("code") 139 if code == "" { 140 log.Debugf("Not handling OAuth code flow authentication: could not get the code: %v", err) 141 fallbackHandler.ServeHTTP(w, r) 142 return 143 } 144 145 // If we get here then the request IS a callback from the OpenId provider. 146 147 webRoot := c.conf.Server.WebRoot 148 webRootWithSlash := webRoot + "/" 149 150 // TODO: We probably need the redirect from the oauth server to include the cluster name in the path. 151 tok, err := c.openshiftOAuth.Exchange(r.Context(), code, nonceCookie.Value, c.conf.KubernetesConfig.ClusterName) 152 if err != nil { 153 log.Errorf("Authentication rejected: Unable to exchange the code for a token: %v", err) 154 http.Redirect(w, r, fmt.Sprintf("%s?openshift_error=%s", webRootWithSlash, url.QueryEscape(err.Error())), http.StatusFound) 155 return 156 } 157 158 if err := c.SessionStore.CreateSession(r, w, config.AuthStrategyOpenshift, tok.Expiry, tok); err != nil { 159 log.Errorf("Authentication rejected: Could not create the session: %v", err) 160 http.Redirect(w, r, fmt.Sprintf("%s?openshift_error=%s", webRootWithSlash, url.QueryEscape(err.Error())), http.StatusFound) 161 return 162 } 163 164 // Delete the nonce cookie since we no longer need it. 165 deleteNonceCookie := http.Cookie{ 166 Expires: time.Unix(0, 0), 167 HttpOnly: true, 168 Name: OpenIdNonceCookieName, 169 Path: c.conf.Server.WebRoot, 170 Secure: nonceCookie.Secure, 171 SameSite: http.SameSiteStrictMode, 172 Value: "", 173 } 174 http.SetCookie(w, &deleteNonceCookie) 175 176 // Use the authorization code that is pushed to the redirect 177 // Let's redirect (remove the openid params) to let the Kiali-UI to boot 178 http.Redirect(w, r, webRootWithSlash, http.StatusFound) 179 }) 180 } 181 182 // Authenticate handles an HTTP request that contains the access_token, expires_in URL parameters. The access_token 183 // should be the token that was obtained from the OpenShift OAuth server and expires_in is the expiration date-time 184 // of the token. The token is validated by obtaining the information user tied to it. Although RBAC is always assumed 185 // when using OpenShift, privileges are not checked here. 186 func (o OpenshiftAuthController) Authenticate(r *http.Request, w http.ResponseWriter) (*UserSessionData, error) { 187 return nil, fmt.Errorf("support for OAuth's implicit flow has been removed") 188 } 189 190 // ValidateSession restores a session previously created by the Authenticate function. The user token (access_token) 191 // is revalidated by re-fetching user info from the cluster, to ensure that the token hasn't been revoked. 192 // If the session is still valid, a populated UserSessionData is returned. Otherwise, nil is returned. 193 func (o OpenshiftAuthController) ValidateSession(r *http.Request, w http.ResponseWriter) (*UserSessionData, error) { 194 var token string 195 var expires time.Time 196 197 // In OpenShift auth, it is possible that a session is started by a 3rd party. If that's the case, Kiali 198 // can receive the OpenShift token of the session via HTTP Headers of via a URL Query string parameter. 199 // HTTP Headers have priority over URL parameters. If a token is received via some of these means, 200 // then the received session has priority over the Kiali initiated session (stored in cookies). 201 if authHeader := r.Header.Get("Authorization"); len(authHeader) != 0 && strings.HasPrefix(authHeader, "Bearer ") { 202 token = strings.TrimPrefix(authHeader, "Bearer ") 203 expires = util.Clock.Now().Add(time.Second * time.Duration(config.Get().LoginToken.ExpirationSeconds)) 204 } else if authToken := r.URL.Query().Get("oauth_token"); len(authToken) != 0 { 205 token = strings.TrimSpace(authToken) 206 expires = util.Clock.Now().Add(time.Second * time.Duration(config.Get().LoginToken.ExpirationSeconds)) 207 } else { 208 sPayload := openshiftSessionPayload{} 209 sData, err := o.SessionStore.ReadSession(r, w, &sPayload) 210 if err != nil { 211 log.Warningf("Could not read the openshift session: %v", err) 212 return nil, nil 213 } 214 if sData == nil { 215 return nil, nil 216 } 217 218 // The Openshift token must be present 219 if len(sPayload.AccessToken) == 0 { 220 log.Warning("Session is invalid: the Openshift token is absent") 221 return nil, nil 222 } 223 224 token = sPayload.AccessToken 225 expires = sData.ExpiresOn 226 } 227 228 user, err := o.openshiftOAuth.GetUserInfo(r.Context(), token) 229 if err == nil { 230 // Internal header used to propagate the subject of the request for audit purposes 231 r.Header.Add("Kiali-User", user.Name) 232 return &UserSessionData{ 233 ExpiresOn: expires, 234 Username: user.Name, 235 AuthInfo: &api.AuthInfo{Token: token}, 236 }, nil 237 } 238 239 log.Warningf("Token error: %v", err) 240 return nil, nil 241 } 242 243 // TerminateSession session created by the Authenticate function. 244 // To properly clean the session, the OpenShift access_token is revoked/deleted by making a call 245 // to the relevant OpenShift API. If this process fails, the session is not cleared and an error 246 // is returned. 247 // The cleanup is done assuming the access_token was issued to be used only in Kiali. 248 func (o OpenshiftAuthController) TerminateSession(r *http.Request, w http.ResponseWriter) error { 249 sPayload := openshiftSessionPayload{} 250 sData, err := o.SessionStore.ReadSession(r, w, &sPayload) 251 if err != nil { 252 return TerminateSessionError{ 253 Message: fmt.Sprintf("There is no active openshift session: %v", err), 254 HttpStatus: http.StatusUnauthorized, 255 } 256 } 257 if sData == nil { 258 return TerminateSessionError{ 259 Message: "logout problem: no session exists.", 260 HttpStatus: http.StatusInternalServerError, 261 } 262 } 263 264 // The Openshift token must be present 265 if len(sPayload.AccessToken) == 0 { 266 return TerminateSessionError{ 267 Message: "Cannot logout: the Openshift token is absent from the session", 268 HttpStatus: http.StatusInternalServerError, 269 } 270 } 271 272 // TODO: Support multi-cluster. A single logout should termiante all sessions. 273 err = o.openshiftOAuth.Logout(r.Context(), sPayload.AccessToken, o.conf.KubernetesConfig.ClusterName) 274 if err != nil { 275 return TerminateSessionError{ 276 Message: fmt.Sprintf("Could not log out of OpenShift: %v", err), 277 HttpStatus: http.StatusInternalServerError, 278 } 279 } 280 281 o.SessionStore.TerminateSession(r, w) 282 return nil 283 }