Skip to content

Commit

Permalink
feat: add support for bearer tokens in websocket protocols (#533)
Browse files Browse the repository at this point in the history
* feat: add support for bearer tokens in websocket protocols

Signed-off-by: David Kovari <[email protected]>

* bearerToken returns string, error
errors on failing to get token

Signed-off-by: David Kovari <[email protected]>

* code cleanup and add tests

Signed-off-by: David Kovari <[email protected]>

* style(golangci-lint): adressing meta linters errors

Signed-off-by: Dario Tranchitella <[email protected]>

---------

Signed-off-by: David Kovari <[email protected]>
Signed-off-by: Dario Tranchitella <[email protected]>
Co-authored-by: Dario Tranchitella <[email protected]>
  • Loading branch information
CrimsonFez and prometherion authored Nov 14, 2024
1 parent 2a97211 commit 4ae014c
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 11 deletions.
45 changes: 35 additions & 10 deletions internal/request/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package request

import (
"encoding/base64"
"fmt"
h "net/http"
"regexp"
Expand Down Expand Up @@ -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,
},
}

Expand All @@ -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)
Expand All @@ -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.
Expand Down
57 changes: 56 additions & 1 deletion internal/request/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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{
Expand Down

0 comments on commit 4ae014c

Please sign in to comment.