diff --git a/internal/request/http.go b/internal/request/http.go index 08374d0b..9a354313 100644 --- a/internal/request/http.go +++ b/internal/request/http.go @@ -4,6 +4,7 @@ package request import ( + "encoding/base64" "fmt" h "net/http" "regexp" @@ -127,9 +128,14 @@ func (h http) GetUserAndGroups() (username string, groups []string, err error) { } func (h http) processBearerToken() (username string, groups []string, err error) { + token, err := h.bearerToken() + if err != nil { + return "", nil, err + } + tr := &authenticationv1.TokenReview{ Spec: authenticationv1.TokenReviewSpec{ - Token: h.bearerToken(), + Token: token, }, } @@ -148,8 +154,33 @@ func (h http) processBearerToken() (username string, groups []string, err error) return tr.Status.User.Username, tr.Status.User.Groups, nil } -func (h http) bearerToken() string { - return strings.ReplaceAll(h.Header.Get("Authorization"), "Bearer ", "") +// Get the JWT from headers +// If there is no Authorizaion Bearer, then try finding the Bearer in Websocket Protocols header. This is for browser support. +func (h http) bearerToken() (string, error) { + tradBearer := strings.ReplaceAll(h.Header.Get("Authorization"), "Bearer ", "") + wsHeader := h.Header.Get("Sec-Websocket-Protocol") + + switch { + case tradBearer != "": + return tradBearer, nil + case wsHeader != "": + re := regexp.MustCompile(`base64url\.bearer\.authorization\.k8s\.io\.([^,]*)`) + + match := re.FindStringSubmatch(wsHeader)[1] + if match != "" { + // our token is base64 encoded without padding + b64decode, err := base64.RawStdEncoding.DecodeString(match) + if err != nil { + return "", NewErrUnauthorized("failed to decode websocket auth bearer: " + err.Error()) + } + + return string(b64decode), nil + } + + return "", NewErrUnauthorized("Websocket Protocol token is undefined") + default: + return "", NewErrUnauthorized("unauthenticated users are not supported") + } } type authenticationFn func() (username string, groups []string, err error) @@ -161,13 +192,7 @@ func (h http) authenticationFns() []authenticationFn { //nolint:exhaustive switch authType { case BearerToken: - fns = append(fns, func() (username string, groups []string, err error) { - if len(h.bearerToken()) == 0 { - return "", nil, NewErrUnauthorized("unauthenticated users not supported") - } - - return h.processBearerToken() - }) + fns = append(fns, h.processBearerToken) case TLSCertificate: // If the proxy is handling a non TLS connection, we have to skip the authentication strategy, // since the TLS section of the request would be nil. diff --git a/internal/request/http_test.go b/internal/request/http_test.go index b5dc71a5..6615e63f 100644 --- a/internal/request/http_test.go +++ b/internal/request/http_test.go @@ -137,7 +137,7 @@ func Test_http_GetUserAndGroups(t *testing.T) { wantErr: false, }, { - name: "Bearer", + name: "InvalidBearer", fields: fields{ Request: &http.Request{ Header: map[string][]string{ @@ -157,6 +157,61 @@ func Test_http_GetUserAndGroups(t *testing.T) { wantGroups: nil, wantErr: true, }, + { + name: "TraditionalBearer", + fields: fields{ + Request: &http.Request{ + Header: map[string][]string{ + "Authorization": {fmt.Sprintf("Bearer %s", "asdf")}, + }, + }, + authTypes: []request.AuthType{ + request.BearerToken, + }, + usernameClaimField: "", + client: testClient(func(ctx context.Context, obj client.Object) error { + tr := obj.(*authenticationv1.TokenReview) + + if tr.Spec.Token == "asdf" { + tr.Status.Authenticated = true + + return nil + } + + return fmt.Errorf("failed to match token") + }), + }, + wantUsername: "", + wantGroups: nil, + wantErr: false, + }, + { + name: "WebsocketBearer", + fields: fields{ + Request: &http.Request{ + Header: map[string][]string{ + "Sec-Websocket-Protocol": {"base64url.bearer.authorization.k8s.io.YXNkZg"}, + }, + }, + authTypes: []request.AuthType{ + request.BearerToken, + }, + usernameClaimField: "", + client: testClient(func(ctx context.Context, obj client.Object) error { + tr := obj.(*authenticationv1.TokenReview) + if tr.Spec.Token == "fdsa" { + tr.Status.Authenticated = true + + return nil + } + + return fmt.Errorf("failed to match token or decode") + }), + }, + wantUsername: "", + wantGroups: nil, + wantErr: false, + }, { name: "Certificate-Impersonation", fields: fields{