github.com/kiali/kiali@v1.84.0/business/authentication/header_auth_controller.go (about) 1 package authentication 2 3 import ( 4 "net/http" 5 "strings" 6 "time" 7 8 "k8s.io/client-go/tools/clientcmd/api" 9 10 "github.com/kiali/kiali/config" 11 "github.com/kiali/kiali/kubernetes" 12 "github.com/kiali/kiali/log" 13 "github.com/kiali/kiali/util" 14 ) 15 16 // headerAuthController contains the backing logic to implement 17 // Kiali's "header" authentication strategy. It assumes that authentication 18 // is fully done by an external system and Kiali does not participate. Kiali 19 // receives already valid credentials through HTTP headers on each request. 20 // Because of this, only minimal validation of the received credentials is 21 // performed. 22 type headerAuthController struct { 23 homeClusterSAClient kubernetes.ClientInterface 24 // SessionStore persists the session between HTTP requests. 25 SessionStore SessionPersistor 26 } 27 28 // headerSessionPayload is a helper type used as session data storage. An instance 29 // of this type is used with the SessionPersistor for session creation and persistence. 30 type headerSessionPayload struct { 31 // The resolved username associated with the received credentials. 32 Subject string `json:"subject,omitempty"` 33 34 // Token is the Bearer token that the upstream client sent on the HTTP Authorization 35 // header at the initial authentication. 36 Token string `json:"token,omitempty"` 37 } 38 39 // NewHeaderAuthController initializes a new controller for allowing already authenticated requests, with the 40 // given persistor and the given businessInstantiator. The businessInstantiator can be nil and 41 // the initialized controller will use the business.Get function. 42 func NewHeaderAuthController(persistor SessionPersistor, homeClusterSAClient kubernetes.ClientInterface) *headerAuthController { 43 return &headerAuthController{ 44 homeClusterSAClient: homeClusterSAClient, 45 SessionStore: persistor, 46 } 47 } 48 49 // Authenticate handles an HTTP request that contains credentials passed in HTTP headers. 50 // It is assumed that some external system is fully controlling authentication. Thus, it is 51 // assumed that the received credentials should be valid. Nevertheless, a minimal verification 52 // is done by trying to fetch the account/user name from the cluster. If account/user name information 53 // cannot be fetched, authentication is rejected. 54 // An error is returned if the authentication failed. 55 func (c headerAuthController) Authenticate(r *http.Request, w http.ResponseWriter) (*UserSessionData, error) { 56 authInfo := c.getTokenStringFromHeader(r) 57 58 if authInfo == nil || authInfo.Token == "" { 59 c.SessionStore.TerminateSession(r, w) 60 return nil, &AuthenticationFailureError{ 61 HttpStatus: http.StatusUnauthorized, 62 Reason: "Token is missing", 63 } 64 } 65 66 // Get the subject for the token to validate it as a valid token 67 subjectFromToken, err := c.homeClusterSAClient.GetTokenSubject(authInfo) 68 if err != nil { 69 return nil, err 70 } 71 72 // The token has been validated via k8s TokenReview, extract the subject for the ui to display 73 // from either the subject (via the TokenReview) or the impersonation header 74 var tokenSubject string 75 76 if authInfo.Impersonate == "" { 77 tokenSubject = subjectFromToken 78 tokenSubject = strings.TrimPrefix(tokenSubject, "system:serviceaccount:") // Shorten the subject displayed in UI. 79 } else { 80 tokenSubject = authInfo.Impersonate 81 } 82 83 // Create the session 84 timeExpire := util.Clock.Now().Add(time.Second * time.Duration(config.Get().LoginToken.ExpirationSeconds)) 85 err = c.SessionStore.CreateSession(r, w, config.AuthStrategyHeader, timeExpire, headerSessionPayload{Token: authInfo.Token, Subject: tokenSubject}) 86 if err != nil { 87 return nil, err 88 } 89 90 return &UserSessionData{ 91 ExpiresOn: timeExpire, 92 Username: tokenSubject, 93 AuthInfo: authInfo, 94 }, nil 95 } 96 97 // ValidateSession checks if credentials are available in HTTP headers. If they are present, a populated 98 // UserSessionData is returned. Otherwise, nil is returned. 99 func (c headerAuthController) ValidateSession(r *http.Request, w http.ResponseWriter) (*UserSessionData, error) { 100 log.Tracef("Using header for authentication, Url: [%s]", r.URL.String()) 101 102 sPayload := headerSessionPayload{} 103 sData, err := c.SessionStore.ReadSession(r, w, &sPayload) 104 if err != nil { 105 log.Warningf("Could not read the session: %v", err) 106 return nil, err 107 } 108 109 authInfo := c.getTokenStringFromHeader(r) 110 if authInfo == nil || authInfo.Token == "" { 111 // No token in HTTP headers, means no session. 112 return nil, nil 113 } 114 115 // A token in HTTP headers means there is a valid session, even if our cookies have 116 // expired. So, if we have cookies, we can recover the subject. Else, send empty subject. 117 // Expiration time is probably irrelevant for this auth strategy, but to keep the so-so same behavior 118 // before the auth refactor, we set expiration time to "now" if we don't have cookies. 119 var expiration time.Time 120 var subject string 121 if sData == nil { 122 expiration = util.Clock.Now() 123 subject = "" 124 } else { 125 expiration = sData.ExpiresOn 126 subject = sPayload.Subject 127 } 128 129 return &UserSessionData{ 130 ExpiresOn: expiration, 131 Username: subject, 132 AuthInfo: authInfo, 133 }, nil 134 } 135 136 // TerminateSession unconditionally terminates any existing session without any validation. 137 func (c headerAuthController) TerminateSession(r *http.Request, w http.ResponseWriter) error { 138 c.SessionStore.TerminateSession(r, w) 139 return nil 140 } 141 142 // getTokenStringFromHeader builds a Kubernetes api.AuthInfo object that contains user credentials 143 // and any other credential attributes received through HTTP headers. Minimally, the standard HTTP 144 // Authorization header is required to be present in the request containing a Bearer token that 145 // can be used to make requests to the cluster API. Additionally, Kubernetes Impersonation 146 // headers are allowed. Since all these headers are going to be used against the cluster API, here 147 // we read passively the data and let the cluster do its own validations on the credentials. 148 func (c headerAuthController) getTokenStringFromHeader(r *http.Request) *api.AuthInfo { 149 tokenString := "" // Default to no token. 150 151 // Extract token from the Authorization HTTP header sent from the reverse proxy 152 if headerValue := r.Header.Get("Authorization"); strings.Contains(headerValue, "Bearer") { 153 tokenString = strings.TrimPrefix(headerValue, "Bearer ") 154 } 155 156 authInfo := &api.AuthInfo{Token: tokenString} 157 158 impersonationHeader := r.Header.Get("Impersonate-User") 159 if len(impersonationHeader) > 0 { 160 // there's an impersonation header, lets make sure to add it 161 authInfo.Impersonate = impersonationHeader 162 163 // Check for impersonated groups 164 if groupsImpersonationHeader := r.Header["Impersonate-Group"]; len(groupsImpersonationHeader) > 0 { 165 authInfo.ImpersonateGroups = groupsImpersonationHeader 166 } 167 168 // check for extra fields 169 for headerName, headerValues := range r.Header { 170 if strings.HasPrefix(headerName, "Impersonate-Extra-") { 171 extraName := headerName[len("Impersonate-Extra-"):] 172 if authInfo.ImpersonateUserExtra == nil { 173 authInfo.ImpersonateUserExtra = make(map[string][]string) 174 } 175 authInfo.ImpersonateUserExtra[extraName] = headerValues 176 } 177 } 178 } 179 180 return authInfo 181 }