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  }