github.com/kiali/kiali@v1.84.0/business/authentication/token_auth_controller.go (about) 1 package authentication 2 3 import ( 4 "errors" 5 "fmt" 6 "net/http" 7 "strings" 8 "time" 9 10 "github.com/go-jose/go-jose/jwt" 11 "k8s.io/client-go/tools/clientcmd/api" 12 13 "github.com/kiali/kiali/business" 14 "github.com/kiali/kiali/config" 15 "github.com/kiali/kiali/kubernetes" 16 "github.com/kiali/kiali/kubernetes/cache" 17 "github.com/kiali/kiali/log" 18 "github.com/kiali/kiali/util" 19 ) 20 21 // tokenAuthController contains the backing logic to implement 22 // Kiali's "token" authentication strategy. It assumes that the 23 // user will use a token that is valid to be used against the Cluster API. 24 // In it's simplest form, it can be a ServiceAccount token. However, it can 25 // be any kind of token that can be passed using HTTP Bearer authentication 26 // in requests to the Kubernetes API. 27 type tokenAuthController struct { 28 conf *config.Config 29 kialiCache cache.KialiCache 30 clientFactory kubernetes.ClientFactory 31 // SessionStore persists the session between HTTP requests. 32 SessionStore SessionPersistor 33 } 34 35 type tokenSessionPayload struct { 36 // Token is the string that the user entered in the Kiali login screen. It should be 37 // a token that can be used against the Kubernetes API 38 Token string `json:"token,omitempty"` 39 } 40 41 // NewTokenAuthController initializes a new controller for handling token authentication, with the 42 // given persistor and the given businessInstantiator. The businessInstantiator can be nil and 43 // the initialized contoller will use the business.Get function. 44 func NewTokenAuthController(persistor SessionPersistor, clientFactory kubernetes.ClientFactory, kialiCache cache.KialiCache, conf *config.Config) *tokenAuthController { 45 return &tokenAuthController{ 46 clientFactory: clientFactory, 47 SessionStore: persistor, 48 kialiCache: kialiCache, 49 conf: conf, 50 } 51 } 52 53 // Authenticate handles an HTTP request that contains a token passed in the "token" field of form data of 54 // the body of the request (POST, PATCH or PUT methods). The token should be valid to be used in the 55 // Kubernetes API, thus the token is verified by trying a request to the Kubernetes API. 56 // If the Kubernetes API rejects the token, authentication fails with an invalid/expired token error. If 57 // the token is accepted, privileges to read some namespace is checked. If some namespace is readable, 58 // authentication succeeds and a session is started; else, authentication is rejected because the 59 // user won't be able to see any data in Kiali. 60 // An AuthenticationFailureError is returned if the authentication request is rejected (unauthorized). Any 61 // other kind of error means that something unexpected happened. 62 func (c tokenAuthController) Authenticate(r *http.Request, w http.ResponseWriter) (*UserSessionData, error) { 63 // Get the token from HTTP form data 64 err := r.ParseForm() 65 if err != nil { 66 return nil, fmt.Errorf("error parsing form data from client: %w", err) 67 } 68 69 token := r.PostForm.Get("token") 70 if token == "" { 71 return nil, errors.New("token is empty") 72 } 73 74 // Need client factory to create a client for the namespace service 75 // Create a bs layer with the received token to check its validity. 76 clients, err := c.clientFactory.GetClients(&api.AuthInfo{Token: token}) 77 if err != nil { 78 return nil, fmt.Errorf("could not get the clients: %w", err) 79 } 80 81 namespaceService := business.NewNamespaceService(clients, c.clientFactory.GetSAClients(), c.kialiCache, c.conf) 82 83 // Using the namespaces API to check if token is valid. In Kubernetes, the version API seems to allow 84 // anonymous access, so it's not feasible to use the version API for token verification. 85 nsList, err := namespaceService.GetNamespaces(r.Context()) 86 if err != nil { 87 c.SessionStore.TerminateSession(r, w) 88 return nil, &AuthenticationFailureError{Reason: "token is not valid or is expired", Detail: err} 89 } 90 91 // If namespace list is empty, return authentication failure. 92 if len(nsList) == 0 { 93 c.SessionStore.TerminateSession(r, w) 94 return nil, &AuthenticationFailureError{Reason: "not enough privileges to login"} 95 } 96 97 // Token was valid against the Kubernetes API, and it has privileges to read some namespace. 98 // Accept the token. Create the user session. 99 timeExpire := util.Clock.Now().Add(time.Second * time.Duration(c.conf.LoginToken.ExpirationSeconds)) 100 err = c.SessionStore.CreateSession(r, w, config.AuthStrategyToken, timeExpire, tokenSessionPayload{Token: token}) 101 if err != nil { 102 return nil, err 103 } 104 105 return &UserSessionData{ 106 ExpiresOn: timeExpire, 107 Username: extractSubjectFromK8sToken(token), 108 AuthInfo: &api.AuthInfo{Token: token}, 109 }, nil 110 } 111 112 // ValidateSession restores a session previously created by the Authenticate function. A minimal re-validation 113 // is done: only token validity is re-checked by making a request to the Kubernetes API, like in the Authenticate 114 // function. However, privileges are not re-checked. 115 // If the session is still valid, a populated UserSessionData is returned. Otherwise, nil is returned. 116 func (c tokenAuthController) ValidateSession(r *http.Request, w http.ResponseWriter) (*UserSessionData, error) { 117 // Restore a previously started session. 118 sPayload := tokenSessionPayload{} 119 sData, err := c.SessionStore.ReadSession(r, w, &sPayload) 120 if err != nil { 121 log.Warningf("Could not read the session: %v", err) 122 return nil, err 123 } 124 if sData == nil { 125 return nil, nil 126 } 127 128 // Check token validity. 129 clients, err := c.clientFactory.GetClients(&api.AuthInfo{Token: sPayload.Token}) 130 if err != nil { 131 return nil, fmt.Errorf("could create user clients from token: %w", err) 132 } 133 134 namespaceService := business.NewNamespaceService(clients, c.clientFactory.GetSAClients(), c.kialiCache, c.conf) 135 _, err = namespaceService.GetNamespaces(r.Context()) 136 if err != nil { 137 // The Kubernetes API rejected the token. 138 // Return no data (which means no active session). 139 log.Warningf("Token error!!: %v", err) 140 return nil, nil 141 } 142 143 // If we are here, the session looks valid. Return the session details. 144 r.Header.Add("Kiali-User", extractSubjectFromK8sToken(sPayload.Token)) // Internal header used to propagate the subject of the request for audit purposes 145 return &UserSessionData{ 146 ExpiresOn: sData.ExpiresOn, 147 Username: extractSubjectFromK8sToken(sPayload.Token), 148 AuthInfo: &api.AuthInfo{Token: sPayload.Token}, 149 }, nil 150 } 151 152 // TerminateSession unconditionally terminates any existing session without any validation. 153 func (c tokenAuthController) TerminateSession(r *http.Request, w http.ResponseWriter) error { 154 c.SessionStore.TerminateSession(r, w) 155 return nil 156 } 157 158 // extractSubjectFromK8sToken returns the string stored in the "sub" claim of a JWT. 159 // If the sub claim is prefixed with the "system:serviceaccount:" this prefix is removed. 160 // If the token is not a JWT, or if it does not have a "sub" claim, a generic "token" string 161 // is returned. 162 func extractSubjectFromK8sToken(token string) string { 163 subject := "token" // Set a default value 164 165 // Decode the Kubernetes token (it is a JWT token) without validating its signature 166 var claims map[string]interface{} // generic map to store parsed token 167 parsedJWSToken, err := jwt.ParseSigned(token) 168 if err == nil { 169 err = parsedJWSToken.UnsafeClaimsWithoutVerification(&claims) 170 if err == nil { 171 subject = strings.TrimPrefix(claims["sub"].(string), "system:serviceaccount:") // Shorten the subject displayed in UI. 172 } 173 } 174 175 return subject 176 }