Merge pull request #47740 from liggitt/websocket-protocol
Automatic merge from submit-queue Add token authentication method for websocket browser clients Closes #47967 Browser clients do not have the ability to set an `Authorization` header programatically on websocket requests. All they have control over is the URL and the websocket subprotocols sent (see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) This PR adds support for specifying a bearer token via a websocket subprotocol, with the format `base64url.bearer.authorization.k8s.io.<encoded-token>` 1. The client must specify at least one other subprotocol, since the server must echo a selected subprotocol back 2. `<encoded-token>` is `base64url-without-padding(token)` This enables web consoles to use websocket-based APIs (like watch, exec, logs, etc) using bearer token authentication. For example, to authenticate with the bearer token `mytoken`, the client could do: ```js var ws = new WebSocket( "wss://<server>/api/v1/namespaces/myns/pods/mypod/logs?follow=true", [ "base64url.bearer.authorization.k8s.io.bXl0b2tlbg", "base64.binary.k8s.io" ] ); ``` This results in the following headers: ``` Sec-WebSocket-Protocol: base64url.bearer.authorization.k8s.io.bXl0b2tlbg, base64.binary.k8s.io ``` Which this authenticator would recognize as the token `mytoken`, and if authentication succeeded, hand off to the rest of the API server with the headers ``` Sec-WebSocket-Protocol: base64.binary.k8s.io ``` Base64-encoding the token is required, since bearer tokens can contain characters a websocket protocol may not (`/` and `=`) ```release-note Websocket requests may now authenticate to the API server by passing a bearer token in a websocket subprotocol of the form `base64url.bearer.authorization.k8s.io.<base64url-encoded-bearer-token>` ```
This commit is contained in:
		@@ -21,6 +21,7 @@ go_library(
 | 
			
		||||
        "//vendor/k8s.io/apiserver/pkg/authentication/request/bearertoken:go_default_library",
 | 
			
		||||
        "//vendor/k8s.io/apiserver/pkg/authentication/request/headerrequest:go_default_library",
 | 
			
		||||
        "//vendor/k8s.io/apiserver/pkg/authentication/request/union:go_default_library",
 | 
			
		||||
        "//vendor/k8s.io/apiserver/pkg/authentication/request/websocket:go_default_library",
 | 
			
		||||
        "//vendor/k8s.io/apiserver/pkg/authentication/request/x509:go_default_library",
 | 
			
		||||
        "//vendor/k8s.io/apiserver/pkg/authentication/token/tokenfile:go_default_library",
 | 
			
		||||
        "//vendor/k8s.io/apiserver/plugin/pkg/authenticator/password/keystone:go_default_library",
 | 
			
		||||
 
 | 
			
		||||
@@ -28,6 +28,7 @@ import (
 | 
			
		||||
	"k8s.io/apiserver/pkg/authentication/request/bearertoken"
 | 
			
		||||
	"k8s.io/apiserver/pkg/authentication/request/headerrequest"
 | 
			
		||||
	"k8s.io/apiserver/pkg/authentication/request/union"
 | 
			
		||||
	"k8s.io/apiserver/pkg/authentication/request/websocket"
 | 
			
		||||
	"k8s.io/apiserver/pkg/authentication/request/x509"
 | 
			
		||||
	"k8s.io/apiserver/pkg/authentication/token/tokenfile"
 | 
			
		||||
	"k8s.io/apiserver/plugin/pkg/authenticator/password/keystone"
 | 
			
		||||
@@ -126,7 +127,7 @@ func (config AuthenticatorConfig) New() (authenticator.Request, *spec.SecurityDe
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, nil, err
 | 
			
		||||
		}
 | 
			
		||||
		authenticators = append(authenticators, tokenAuth)
 | 
			
		||||
		authenticators = append(authenticators, bearertoken.New(tokenAuth), websocket.NewProtocolAuthenticator(tokenAuth))
 | 
			
		||||
		hasTokenAuth = true
 | 
			
		||||
	}
 | 
			
		||||
	if len(config.ServiceAccountKeyFiles) > 0 {
 | 
			
		||||
@@ -134,13 +135,13 @@ func (config AuthenticatorConfig) New() (authenticator.Request, *spec.SecurityDe
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, nil, err
 | 
			
		||||
		}
 | 
			
		||||
		authenticators = append(authenticators, serviceAccountAuth)
 | 
			
		||||
		authenticators = append(authenticators, bearertoken.New(serviceAccountAuth), websocket.NewProtocolAuthenticator(serviceAccountAuth))
 | 
			
		||||
		hasTokenAuth = true
 | 
			
		||||
	}
 | 
			
		||||
	if config.BootstrapToken {
 | 
			
		||||
		if config.BootstrapTokenAuthenticator != nil {
 | 
			
		||||
			// TODO: This can sometimes be nil because of
 | 
			
		||||
			authenticators = append(authenticators, bearertoken.New(config.BootstrapTokenAuthenticator))
 | 
			
		||||
			authenticators = append(authenticators, bearertoken.New(config.BootstrapTokenAuthenticator), websocket.NewProtocolAuthenticator(config.BootstrapTokenAuthenticator))
 | 
			
		||||
			hasTokenAuth = true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
@@ -155,7 +156,7 @@ func (config AuthenticatorConfig) New() (authenticator.Request, *spec.SecurityDe
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, nil, err
 | 
			
		||||
		}
 | 
			
		||||
		authenticators = append(authenticators, oidcAuth)
 | 
			
		||||
		authenticators = append(authenticators, bearertoken.New(oidcAuth), websocket.NewProtocolAuthenticator(oidcAuth))
 | 
			
		||||
		hasTokenAuth = true
 | 
			
		||||
	}
 | 
			
		||||
	if len(config.WebhookTokenAuthnConfigFile) > 0 {
 | 
			
		||||
@@ -163,13 +164,13 @@ func (config AuthenticatorConfig) New() (authenticator.Request, *spec.SecurityDe
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, nil, err
 | 
			
		||||
		}
 | 
			
		||||
		authenticators = append(authenticators, webhookTokenAuth)
 | 
			
		||||
		authenticators = append(authenticators, bearertoken.New(webhookTokenAuth), websocket.NewProtocolAuthenticator(webhookTokenAuth))
 | 
			
		||||
		hasTokenAuth = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// always add anytoken last, so that every other token authenticator gets to try first
 | 
			
		||||
	if config.AnyToken {
 | 
			
		||||
		authenticators = append(authenticators, bearertoken.New(anytoken.AnyTokenAuthenticator{}))
 | 
			
		||||
		authenticators = append(authenticators, bearertoken.New(anytoken.AnyTokenAuthenticator{}), websocket.NewProtocolAuthenticator(anytoken.AnyTokenAuthenticator{}))
 | 
			
		||||
		hasTokenAuth = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -234,17 +235,17 @@ func newAuthenticatorFromBasicAuthFile(basicAuthFile string) (authenticator.Requ
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// newAuthenticatorFromTokenFile returns an authenticator.Request or an error
 | 
			
		||||
func newAuthenticatorFromTokenFile(tokenAuthFile string) (authenticator.Request, error) {
 | 
			
		||||
func newAuthenticatorFromTokenFile(tokenAuthFile string) (authenticator.Token, error) {
 | 
			
		||||
	tokenAuthenticator, err := tokenfile.NewCSV(tokenAuthFile)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return bearertoken.New(tokenAuthenticator), nil
 | 
			
		||||
	return tokenAuthenticator, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// newAuthenticatorFromOIDCIssuerURL returns an authenticator.Request or an error.
 | 
			
		||||
func newAuthenticatorFromOIDCIssuerURL(issuerURL, clientID, caFile, usernameClaim, groupsClaim string) (authenticator.Request, error) {
 | 
			
		||||
func newAuthenticatorFromOIDCIssuerURL(issuerURL, clientID, caFile, usernameClaim, groupsClaim string) (authenticator.Token, error) {
 | 
			
		||||
	tokenAuthenticator, err := oidc.New(oidc.OIDCOptions{
 | 
			
		||||
		IssuerURL:     issuerURL,
 | 
			
		||||
		ClientID:      clientID,
 | 
			
		||||
@@ -256,11 +257,11 @@ func newAuthenticatorFromOIDCIssuerURL(issuerURL, clientID, caFile, usernameClai
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return bearertoken.New(tokenAuthenticator), nil
 | 
			
		||||
	return tokenAuthenticator, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// newServiceAccountAuthenticator returns an authenticator.Request or an error
 | 
			
		||||
func newServiceAccountAuthenticator(keyfiles []string, lookup bool, serviceAccountGetter serviceaccount.ServiceAccountTokenGetter) (authenticator.Request, error) {
 | 
			
		||||
func newServiceAccountAuthenticator(keyfiles []string, lookup bool, serviceAccountGetter serviceaccount.ServiceAccountTokenGetter) (authenticator.Token, error) {
 | 
			
		||||
	allPublicKeys := []interface{}{}
 | 
			
		||||
	for _, keyfile := range keyfiles {
 | 
			
		||||
		publicKeys, err := serviceaccount.ReadPublicKeys(keyfile)
 | 
			
		||||
@@ -271,7 +272,7 @@ func newServiceAccountAuthenticator(keyfiles []string, lookup bool, serviceAccou
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tokenAuthenticator := serviceaccount.JWTTokenAuthenticator(allPublicKeys, lookup, serviceAccountGetter)
 | 
			
		||||
	return bearertoken.New(tokenAuthenticator), nil
 | 
			
		||||
	return tokenAuthenticator, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// newAuthenticatorFromClientCAFile returns an authenticator.Request or an error
 | 
			
		||||
@@ -297,11 +298,11 @@ func newAuthenticatorFromKeystoneURL(keystoneURL string, keystoneCAFile string)
 | 
			
		||||
	return basicauth.New(keystoneAuthenticator), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func newWebhookTokenAuthenticator(webhookConfigFile string, ttl time.Duration) (authenticator.Request, error) {
 | 
			
		||||
func newWebhookTokenAuthenticator(webhookConfigFile string, ttl time.Duration) (authenticator.Token, error) {
 | 
			
		||||
	webhookTokenAuthenticator, err := webhook.New(webhookConfigFile, ttl)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return bearertoken.New(webhookTokenAuthenticator), nil
 | 
			
		||||
	return webhookTokenAuthenticator, nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,7 @@ go_library(
 | 
			
		||||
        "//vendor/k8s.io/apiserver/pkg/authentication/request/bearertoken:go_default_library",
 | 
			
		||||
        "//vendor/k8s.io/apiserver/pkg/authentication/request/headerrequest:go_default_library",
 | 
			
		||||
        "//vendor/k8s.io/apiserver/pkg/authentication/request/union:go_default_library",
 | 
			
		||||
        "//vendor/k8s.io/apiserver/pkg/authentication/request/websocket:go_default_library",
 | 
			
		||||
        "//vendor/k8s.io/apiserver/pkg/authentication/request/x509:go_default_library",
 | 
			
		||||
        "//vendor/k8s.io/apiserver/pkg/authentication/token/tokenfile:go_default_library",
 | 
			
		||||
        "//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library",
 | 
			
		||||
 
 | 
			
		||||
@@ -29,6 +29,7 @@ import (
 | 
			
		||||
	"k8s.io/apiserver/pkg/authentication/request/bearertoken"
 | 
			
		||||
	"k8s.io/apiserver/pkg/authentication/request/headerrequest"
 | 
			
		||||
	unionauth "k8s.io/apiserver/pkg/authentication/request/union"
 | 
			
		||||
	"k8s.io/apiserver/pkg/authentication/request/websocket"
 | 
			
		||||
	"k8s.io/apiserver/pkg/authentication/request/x509"
 | 
			
		||||
	webhooktoken "k8s.io/apiserver/plugin/pkg/authenticator/token/webhook"
 | 
			
		||||
	authenticationclient "k8s.io/client-go/kubernetes/typed/authentication/v1beta1"
 | 
			
		||||
@@ -87,7 +88,7 @@ func (c DelegatingAuthenticatorConfig) New() (authenticator.Request, *spec.Secur
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, nil, err
 | 
			
		||||
		}
 | 
			
		||||
		authenticators = append(authenticators, bearertoken.New(tokenAuth))
 | 
			
		||||
		authenticators = append(authenticators, bearertoken.New(tokenAuth), websocket.NewProtocolAuthenticator(tokenAuth))
 | 
			
		||||
 | 
			
		||||
		securityDefinitions["BearerToken"] = &spec.SecurityScheme{
 | 
			
		||||
			SecuritySchemeProps: spec.SecuritySchemeProps{
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,31 @@
 | 
			
		||||
package(default_visibility = ["//visibility:public"])
 | 
			
		||||
 | 
			
		||||
licenses(["notice"])
 | 
			
		||||
 | 
			
		||||
load(
 | 
			
		||||
    "@io_bazel_rules_go//go:def.bzl",
 | 
			
		||||
    "go_library",
 | 
			
		||||
    "go_test",
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
go_library(
 | 
			
		||||
    name = "go_default_library",
 | 
			
		||||
    srcs = ["protocol.go"],
 | 
			
		||||
    tags = ["automanaged"],
 | 
			
		||||
    deps = [
 | 
			
		||||
        "//vendor/k8s.io/apiserver/pkg/authentication/authenticator:go_default_library",
 | 
			
		||||
        "//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library",
 | 
			
		||||
        "//vendor/k8s.io/apiserver/pkg/util/wsstream:go_default_library",
 | 
			
		||||
    ],
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
go_test(
 | 
			
		||||
    name = "go_default_test",
 | 
			
		||||
    srcs = ["protocol_test.go"],
 | 
			
		||||
    library = ":go_default_library",
 | 
			
		||||
    tags = ["automanaged"],
 | 
			
		||||
    deps = [
 | 
			
		||||
        "//vendor/k8s.io/apiserver/pkg/authentication/authenticator:go_default_library",
 | 
			
		||||
        "//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library",
 | 
			
		||||
    ],
 | 
			
		||||
)
 | 
			
		||||
@@ -0,0 +1,109 @@
 | 
			
		||||
/*
 | 
			
		||||
Copyright 2017 The Kubernetes Authors.
 | 
			
		||||
 | 
			
		||||
Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
you may not use this file except in compliance with the License.
 | 
			
		||||
You may obtain a copy of the License at
 | 
			
		||||
 | 
			
		||||
    http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 | 
			
		||||
Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
See the License for the specific language governing permissions and
 | 
			
		||||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
package websocket
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/textproto"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"unicode/utf8"
 | 
			
		||||
 | 
			
		||||
	"k8s.io/apiserver/pkg/authentication/authenticator"
 | 
			
		||||
	"k8s.io/apiserver/pkg/authentication/user"
 | 
			
		||||
	"k8s.io/apiserver/pkg/util/wsstream"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const bearerProtocolPrefix = "base64url.bearer.authorization.k8s.io."
 | 
			
		||||
 | 
			
		||||
var protocolHeader = textproto.CanonicalMIMEHeaderKey("Sec-WebSocket-Protocol")
 | 
			
		||||
 | 
			
		||||
var invalidToken = errors.New("invalid bearer token")
 | 
			
		||||
 | 
			
		||||
// ProtocolAuthenticator allows a websocket connection to provide a bearer token as a subprotocol
 | 
			
		||||
// in the format "base64url.bearer.authorization.<base64url-without-padding(bearer-token)>"
 | 
			
		||||
type ProtocolAuthenticator struct {
 | 
			
		||||
	// auth is the token authenticator to use to validate the token
 | 
			
		||||
	auth authenticator.Token
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewProtocolAuthenticator(auth authenticator.Token) *ProtocolAuthenticator {
 | 
			
		||||
	return &ProtocolAuthenticator{auth}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *ProtocolAuthenticator) AuthenticateRequest(req *http.Request) (user.Info, bool, error) {
 | 
			
		||||
	// Only accept websocket connections
 | 
			
		||||
	if !wsstream.IsWebSocketRequest(req) {
 | 
			
		||||
		return nil, false, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	token := ""
 | 
			
		||||
	sawTokenProtocol := false
 | 
			
		||||
	filteredProtocols := []string{}
 | 
			
		||||
	for _, protocolHeader := range req.Header[protocolHeader] {
 | 
			
		||||
		for _, protocol := range strings.Split(protocolHeader, ",") {
 | 
			
		||||
			protocol = strings.TrimSpace(protocol)
 | 
			
		||||
 | 
			
		||||
			if !strings.HasPrefix(protocol, bearerProtocolPrefix) {
 | 
			
		||||
				filteredProtocols = append(filteredProtocols, protocol)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if sawTokenProtocol {
 | 
			
		||||
				return nil, false, errors.New("multiple base64.bearer.authorization tokens specified")
 | 
			
		||||
			}
 | 
			
		||||
			sawTokenProtocol = true
 | 
			
		||||
 | 
			
		||||
			encodedToken := strings.TrimPrefix(protocol, bearerProtocolPrefix)
 | 
			
		||||
			decodedToken, err := base64.RawURLEncoding.DecodeString(encodedToken)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, false, errors.New("invalid base64.bearer.authorization token encoding")
 | 
			
		||||
			}
 | 
			
		||||
			if !utf8.Valid(decodedToken) {
 | 
			
		||||
				return nil, false, errors.New("invalid base64.bearer.authorization token")
 | 
			
		||||
			}
 | 
			
		||||
			token = string(decodedToken)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Must pass at least one other subprotocol so that we can remove the one containing the bearer token,
 | 
			
		||||
	// and there is at least one to echo back to the client
 | 
			
		||||
	if len(token) > 0 && len(filteredProtocols) == 0 {
 | 
			
		||||
		return nil, false, errors.New("missing additional subprotocol")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(token) == 0 {
 | 
			
		||||
		return nil, false, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	user, ok, err := a.auth.AuthenticateToken(token)
 | 
			
		||||
 | 
			
		||||
	// on success, remove the protocol with the token
 | 
			
		||||
	if ok {
 | 
			
		||||
		// https://tools.ietf.org/html/rfc6455#section-11.3.4 indicates the Sec-WebSocket-Protocol header may appear multiple times
 | 
			
		||||
		// in a request, and is logically the same as a single Sec-WebSocket-Protocol header field that contains all values
 | 
			
		||||
		req.Header.Set(protocolHeader, strings.Join(filteredProtocols, ","))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// If the token authenticator didn't error, provide a default error
 | 
			
		||||
	if !ok && err == nil {
 | 
			
		||||
		err = invalidToken
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return user, ok, err
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,222 @@
 | 
			
		||||
/*
 | 
			
		||||
Copyright 2017 The Kubernetes Authors.
 | 
			
		||||
 | 
			
		||||
Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
you may not use this file except in compliance with the License.
 | 
			
		||||
You may obtain a copy of the License at
 | 
			
		||||
 | 
			
		||||
    http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 | 
			
		||||
Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
See the License for the specific language governing permissions and
 | 
			
		||||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
package websocket
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"k8s.io/apiserver/pkg/authentication/authenticator"
 | 
			
		||||
	"k8s.io/apiserver/pkg/authentication/user"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestAuthenticateRequest(t *testing.T) {
 | 
			
		||||
	auth := NewProtocolAuthenticator(authenticator.TokenFunc(func(token string) (user.Info, bool, error) {
 | 
			
		||||
		if token != "token" {
 | 
			
		||||
			t.Errorf("unexpected token: %s", token)
 | 
			
		||||
		}
 | 
			
		||||
		return &user.DefaultInfo{Name: "user"}, true, nil
 | 
			
		||||
	}))
 | 
			
		||||
	user, ok, err := auth.AuthenticateRequest(&http.Request{
 | 
			
		||||
		Header: http.Header{
 | 
			
		||||
			"Connection":             []string{"upgrade"},
 | 
			
		||||
			"Upgrade":                []string{"websocket"},
 | 
			
		||||
			"Sec-Websocket-Protocol": []string{"base64url.bearer.authorization.k8s.io.dG9rZW4,dummy"},
 | 
			
		||||
		},
 | 
			
		||||
	})
 | 
			
		||||
	if !ok || user == nil || err != nil {
 | 
			
		||||
		t.Errorf("expected valid user")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestAuthenticateRequestTokenInvalid(t *testing.T) {
 | 
			
		||||
	auth := NewProtocolAuthenticator(authenticator.TokenFunc(func(token string) (user.Info, bool, error) {
 | 
			
		||||
		return nil, false, nil
 | 
			
		||||
	}))
 | 
			
		||||
	user, ok, err := auth.AuthenticateRequest(&http.Request{
 | 
			
		||||
		Header: http.Header{
 | 
			
		||||
			"Connection":             []string{"upgrade"},
 | 
			
		||||
			"Upgrade":                []string{"websocket"},
 | 
			
		||||
			"Sec-Websocket-Protocol": []string{"base64url.bearer.authorization.k8s.io.dG9rZW4,dummy"},
 | 
			
		||||
		},
 | 
			
		||||
	})
 | 
			
		||||
	if ok || user != nil {
 | 
			
		||||
		t.Errorf("expected not authenticated user")
 | 
			
		||||
	}
 | 
			
		||||
	if err != invalidToken {
 | 
			
		||||
		t.Errorf("expected invalidToken error, got %v", err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestAuthenticateRequestTokenInvalidCustomError(t *testing.T) {
 | 
			
		||||
	customError := errors.New("custom")
 | 
			
		||||
	auth := NewProtocolAuthenticator(authenticator.TokenFunc(func(token string) (user.Info, bool, error) {
 | 
			
		||||
		return nil, false, customError
 | 
			
		||||
	}))
 | 
			
		||||
	user, ok, err := auth.AuthenticateRequest(&http.Request{
 | 
			
		||||
		Header: http.Header{
 | 
			
		||||
			"Connection":             []string{"upgrade"},
 | 
			
		||||
			"Upgrade":                []string{"websocket"},
 | 
			
		||||
			"Sec-Websocket-Protocol": []string{"base64url.bearer.authorization.k8s.io.dG9rZW4,dummy"},
 | 
			
		||||
		},
 | 
			
		||||
	})
 | 
			
		||||
	if ok || user != nil {
 | 
			
		||||
		t.Errorf("expected not authenticated user")
 | 
			
		||||
	}
 | 
			
		||||
	if err != customError {
 | 
			
		||||
		t.Errorf("expected custom error, got %v", err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestAuthenticateRequestTokenError(t *testing.T) {
 | 
			
		||||
	auth := NewProtocolAuthenticator(authenticator.TokenFunc(func(token string) (user.Info, bool, error) {
 | 
			
		||||
		return nil, false, errors.New("error")
 | 
			
		||||
	}))
 | 
			
		||||
	user, ok, err := auth.AuthenticateRequest(&http.Request{
 | 
			
		||||
		Header: http.Header{
 | 
			
		||||
			"Connection":             []string{"upgrade"},
 | 
			
		||||
			"Upgrade":                []string{"websocket"},
 | 
			
		||||
			"Sec-Websocket-Protocol": []string{"base64url.bearer.authorization.k8s.io.dG9rZW4,dummy"},
 | 
			
		||||
		},
 | 
			
		||||
	})
 | 
			
		||||
	if ok || user != nil || err == nil {
 | 
			
		||||
		t.Errorf("expected error")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestAuthenticateRequestBadValue(t *testing.T) {
 | 
			
		||||
	testCases := []struct {
 | 
			
		||||
		Req *http.Request
 | 
			
		||||
	}{
 | 
			
		||||
		{Req: &http.Request{}},
 | 
			
		||||
		{Req: &http.Request{Header: http.Header{
 | 
			
		||||
			"Connection":             []string{"upgrade"},
 | 
			
		||||
			"Upgrade":                []string{"websocket"},
 | 
			
		||||
			"Sec-Websocket-Protocol": []string{"other-protocol"}}},
 | 
			
		||||
		},
 | 
			
		||||
		{Req: &http.Request{Header: http.Header{
 | 
			
		||||
			"Connection":             []string{"upgrade"},
 | 
			
		||||
			"Upgrade":                []string{"websocket"},
 | 
			
		||||
			"Sec-Websocket-Protocol": []string{"base64url.bearer.authorization.k8s.io."}}},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	for i, testCase := range testCases {
 | 
			
		||||
		auth := NewProtocolAuthenticator(authenticator.TokenFunc(func(token string) (user.Info, bool, error) {
 | 
			
		||||
			t.Errorf("authentication should not have been called")
 | 
			
		||||
			return nil, false, nil
 | 
			
		||||
		}))
 | 
			
		||||
		user, ok, err := auth.AuthenticateRequest(testCase.Req)
 | 
			
		||||
		if ok || user != nil || err != nil {
 | 
			
		||||
			t.Errorf("%d: expected not authenticated (no token)", i)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestBearerToken(t *testing.T) {
 | 
			
		||||
	tests := map[string]struct {
 | 
			
		||||
		ProtocolHeaders []string
 | 
			
		||||
		TokenAuth       authenticator.Token
 | 
			
		||||
 | 
			
		||||
		ExpectedUserName        string
 | 
			
		||||
		ExpectedOK              bool
 | 
			
		||||
		ExpectedErr             bool
 | 
			
		||||
		ExpectedProtocolHeaders []string
 | 
			
		||||
	}{
 | 
			
		||||
		"no header": {
 | 
			
		||||
			ProtocolHeaders:         nil,
 | 
			
		||||
			ExpectedUserName:        "",
 | 
			
		||||
			ExpectedOK:              false,
 | 
			
		||||
			ExpectedErr:             false,
 | 
			
		||||
			ExpectedProtocolHeaders: nil,
 | 
			
		||||
		},
 | 
			
		||||
		"empty header": {
 | 
			
		||||
			ProtocolHeaders:         []string{""},
 | 
			
		||||
			ExpectedUserName:        "",
 | 
			
		||||
			ExpectedOK:              false,
 | 
			
		||||
			ExpectedErr:             false,
 | 
			
		||||
			ExpectedProtocolHeaders: []string{""},
 | 
			
		||||
		},
 | 
			
		||||
		"non-bearer header": {
 | 
			
		||||
			ProtocolHeaders:         []string{"undefined"},
 | 
			
		||||
			ExpectedUserName:        "",
 | 
			
		||||
			ExpectedOK:              false,
 | 
			
		||||
			ExpectedErr:             false,
 | 
			
		||||
			ExpectedProtocolHeaders: []string{"undefined"},
 | 
			
		||||
		},
 | 
			
		||||
		"empty bearer token": {
 | 
			
		||||
			ProtocolHeaders:         []string{"base64url.bearer.authorization.k8s.io."},
 | 
			
		||||
			ExpectedUserName:        "",
 | 
			
		||||
			ExpectedOK:              false,
 | 
			
		||||
			ExpectedErr:             false,
 | 
			
		||||
			ExpectedProtocolHeaders: []string{"base64url.bearer.authorization.k8s.io."},
 | 
			
		||||
		},
 | 
			
		||||
		"valid bearer token removing header": {
 | 
			
		||||
			ProtocolHeaders:         []string{"base64url.bearer.authorization.k8s.io.dG9rZW4", "dummy, dummy2"},
 | 
			
		||||
			TokenAuth:               authenticator.TokenFunc(func(t string) (user.Info, bool, error) { return &user.DefaultInfo{Name: "myuser"}, true, nil }),
 | 
			
		||||
			ExpectedUserName:        "myuser",
 | 
			
		||||
			ExpectedOK:              true,
 | 
			
		||||
			ExpectedErr:             false,
 | 
			
		||||
			ExpectedProtocolHeaders: []string{"dummy,dummy2"},
 | 
			
		||||
		},
 | 
			
		||||
		"invalid bearer token": {
 | 
			
		||||
			ProtocolHeaders:         []string{"base64url.bearer.authorization.k8s.io.dG9rZW4,dummy"},
 | 
			
		||||
			TokenAuth:               authenticator.TokenFunc(func(t string) (user.Info, bool, error) { return nil, false, nil }),
 | 
			
		||||
			ExpectedUserName:        "",
 | 
			
		||||
			ExpectedOK:              false,
 | 
			
		||||
			ExpectedErr:             true,
 | 
			
		||||
			ExpectedProtocolHeaders: []string{"base64url.bearer.authorization.k8s.io.dG9rZW4,dummy"},
 | 
			
		||||
		},
 | 
			
		||||
		"error bearer token": {
 | 
			
		||||
			ProtocolHeaders:         []string{"base64url.bearer.authorization.k8s.io.dG9rZW4,dummy"},
 | 
			
		||||
			TokenAuth:               authenticator.TokenFunc(func(t string) (user.Info, bool, error) { return nil, false, errors.New("error") }),
 | 
			
		||||
			ExpectedUserName:        "",
 | 
			
		||||
			ExpectedOK:              false,
 | 
			
		||||
			ExpectedErr:             true,
 | 
			
		||||
			ExpectedProtocolHeaders: []string{"base64url.bearer.authorization.k8s.io.dG9rZW4,dummy"},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for k, tc := range tests {
 | 
			
		||||
		req, _ := http.NewRequest("GET", "/", nil)
 | 
			
		||||
		req.Header.Set("Connection", "upgrade")
 | 
			
		||||
		req.Header.Set("Upgrade", "websocket")
 | 
			
		||||
		for _, h := range tc.ProtocolHeaders {
 | 
			
		||||
			req.Header.Add("Sec-Websocket-Protocol", h)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		bearerAuth := NewProtocolAuthenticator(tc.TokenAuth)
 | 
			
		||||
		u, ok, err := bearerAuth.AuthenticateRequest(req)
 | 
			
		||||
		if tc.ExpectedErr != (err != nil) {
 | 
			
		||||
			t.Errorf("%s: Expected err=%v, got %v", k, tc.ExpectedErr, err)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		if ok != tc.ExpectedOK {
 | 
			
		||||
			t.Errorf("%s: Expected ok=%v, got %v", k, tc.ExpectedOK, ok)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		if ok && u.GetName() != tc.ExpectedUserName {
 | 
			
		||||
			t.Errorf("%s: Expected username=%v, got %v", k, tc.ExpectedUserName, u.GetName())
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		if !reflect.DeepEqual(req.Header["Sec-Websocket-Protocol"], tc.ExpectedProtocolHeaders) {
 | 
			
		||||
			t.Errorf("%s: Expected headers=%#v, got %#v", k, tc.ExpectedProtocolHeaders, req.Header["Sec-Websocket-Protocol"])
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -87,7 +87,10 @@ var (
 | 
			
		||||
// IsWebSocketRequest returns true if the incoming request contains connection upgrade headers
 | 
			
		||||
// for WebSockets.
 | 
			
		||||
func IsWebSocketRequest(req *http.Request) bool {
 | 
			
		||||
	return connectionUpgradeRegex.MatchString(strings.ToLower(req.Header.Get("Connection"))) && strings.ToLower(req.Header.Get("Upgrade")) == "websocket"
 | 
			
		||||
	if !strings.EqualFold(req.Header.Get("Upgrade"), "websocket") {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	return connectionUpgradeRegex.MatchString(strings.ToLower(req.Header.Get("Connection")))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IgnoreReceives reads from a WebSocket until it is closed, then returns. If timeout is set, the
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user