mirror of
				https://github.com/optim-enterprises-bv/kubernetes.git
				synced 2025-11-04 04:08:16 +00:00 
			
		
		
		
	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/bearertoken:go_default_library",
 | 
				
			||||||
        "//vendor/k8s.io/apiserver/pkg/authentication/request/headerrequest: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/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/request/x509:go_default_library",
 | 
				
			||||||
        "//vendor/k8s.io/apiserver/pkg/authentication/token/tokenfile: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",
 | 
					        "//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/bearertoken"
 | 
				
			||||||
	"k8s.io/apiserver/pkg/authentication/request/headerrequest"
 | 
						"k8s.io/apiserver/pkg/authentication/request/headerrequest"
 | 
				
			||||||
	"k8s.io/apiserver/pkg/authentication/request/union"
 | 
						"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/request/x509"
 | 
				
			||||||
	"k8s.io/apiserver/pkg/authentication/token/tokenfile"
 | 
						"k8s.io/apiserver/pkg/authentication/token/tokenfile"
 | 
				
			||||||
	"k8s.io/apiserver/plugin/pkg/authenticator/password/keystone"
 | 
						"k8s.io/apiserver/plugin/pkg/authenticator/password/keystone"
 | 
				
			||||||
@@ -126,7 +127,7 @@ func (config AuthenticatorConfig) New() (authenticator.Request, *spec.SecurityDe
 | 
				
			|||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return nil, nil, err
 | 
								return nil, nil, err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		authenticators = append(authenticators, tokenAuth)
 | 
							authenticators = append(authenticators, bearertoken.New(tokenAuth), websocket.NewProtocolAuthenticator(tokenAuth))
 | 
				
			||||||
		hasTokenAuth = true
 | 
							hasTokenAuth = true
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if len(config.ServiceAccountKeyFiles) > 0 {
 | 
						if len(config.ServiceAccountKeyFiles) > 0 {
 | 
				
			||||||
@@ -134,13 +135,13 @@ func (config AuthenticatorConfig) New() (authenticator.Request, *spec.SecurityDe
 | 
				
			|||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return nil, nil, err
 | 
								return nil, nil, err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		authenticators = append(authenticators, serviceAccountAuth)
 | 
							authenticators = append(authenticators, bearertoken.New(serviceAccountAuth), websocket.NewProtocolAuthenticator(serviceAccountAuth))
 | 
				
			||||||
		hasTokenAuth = true
 | 
							hasTokenAuth = true
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if config.BootstrapToken {
 | 
						if config.BootstrapToken {
 | 
				
			||||||
		if config.BootstrapTokenAuthenticator != nil {
 | 
							if config.BootstrapTokenAuthenticator != nil {
 | 
				
			||||||
			// TODO: This can sometimes be nil because of
 | 
								// 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
 | 
								hasTokenAuth = true
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -155,7 +156,7 @@ func (config AuthenticatorConfig) New() (authenticator.Request, *spec.SecurityDe
 | 
				
			|||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return nil, nil, err
 | 
								return nil, nil, err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		authenticators = append(authenticators, oidcAuth)
 | 
							authenticators = append(authenticators, bearertoken.New(oidcAuth), websocket.NewProtocolAuthenticator(oidcAuth))
 | 
				
			||||||
		hasTokenAuth = true
 | 
							hasTokenAuth = true
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if len(config.WebhookTokenAuthnConfigFile) > 0 {
 | 
						if len(config.WebhookTokenAuthnConfigFile) > 0 {
 | 
				
			||||||
@@ -163,13 +164,13 @@ func (config AuthenticatorConfig) New() (authenticator.Request, *spec.SecurityDe
 | 
				
			|||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return nil, nil, err
 | 
								return nil, nil, err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		authenticators = append(authenticators, webhookTokenAuth)
 | 
							authenticators = append(authenticators, bearertoken.New(webhookTokenAuth), websocket.NewProtocolAuthenticator(webhookTokenAuth))
 | 
				
			||||||
		hasTokenAuth = true
 | 
							hasTokenAuth = true
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// always add anytoken last, so that every other token authenticator gets to try first
 | 
						// always add anytoken last, so that every other token authenticator gets to try first
 | 
				
			||||||
	if config.AnyToken {
 | 
						if config.AnyToken {
 | 
				
			||||||
		authenticators = append(authenticators, bearertoken.New(anytoken.AnyTokenAuthenticator{}))
 | 
							authenticators = append(authenticators, bearertoken.New(anytoken.AnyTokenAuthenticator{}), websocket.NewProtocolAuthenticator(anytoken.AnyTokenAuthenticator{}))
 | 
				
			||||||
		hasTokenAuth = true
 | 
							hasTokenAuth = true
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -234,17 +235,17 @@ func newAuthenticatorFromBasicAuthFile(basicAuthFile string) (authenticator.Requ
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// newAuthenticatorFromTokenFile returns an authenticator.Request or an error
 | 
					// 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)
 | 
						tokenAuthenticator, err := tokenfile.NewCSV(tokenAuthFile)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return bearertoken.New(tokenAuthenticator), nil
 | 
						return tokenAuthenticator, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// newAuthenticatorFromOIDCIssuerURL returns an authenticator.Request or an error.
 | 
					// 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{
 | 
						tokenAuthenticator, err := oidc.New(oidc.OIDCOptions{
 | 
				
			||||||
		IssuerURL:     issuerURL,
 | 
							IssuerURL:     issuerURL,
 | 
				
			||||||
		ClientID:      clientID,
 | 
							ClientID:      clientID,
 | 
				
			||||||
@@ -256,11 +257,11 @@ func newAuthenticatorFromOIDCIssuerURL(issuerURL, clientID, caFile, usernameClai
 | 
				
			|||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return bearertoken.New(tokenAuthenticator), nil
 | 
						return tokenAuthenticator, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// newServiceAccountAuthenticator returns an authenticator.Request or an error
 | 
					// 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{}{}
 | 
						allPublicKeys := []interface{}{}
 | 
				
			||||||
	for _, keyfile := range keyfiles {
 | 
						for _, keyfile := range keyfiles {
 | 
				
			||||||
		publicKeys, err := serviceaccount.ReadPublicKeys(keyfile)
 | 
							publicKeys, err := serviceaccount.ReadPublicKeys(keyfile)
 | 
				
			||||||
@@ -271,7 +272,7 @@ func newServiceAccountAuthenticator(keyfiles []string, lookup bool, serviceAccou
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	tokenAuthenticator := serviceaccount.JWTTokenAuthenticator(allPublicKeys, lookup, serviceAccountGetter)
 | 
						tokenAuthenticator := serviceaccount.JWTTokenAuthenticator(allPublicKeys, lookup, serviceAccountGetter)
 | 
				
			||||||
	return bearertoken.New(tokenAuthenticator), nil
 | 
						return tokenAuthenticator, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// newAuthenticatorFromClientCAFile returns an authenticator.Request or an error
 | 
					// newAuthenticatorFromClientCAFile returns an authenticator.Request or an error
 | 
				
			||||||
@@ -297,11 +298,11 @@ func newAuthenticatorFromKeystoneURL(keystoneURL string, keystoneCAFile string)
 | 
				
			|||||||
	return basicauth.New(keystoneAuthenticator), nil
 | 
						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)
 | 
						webhookTokenAuthenticator, err := webhook.New(webhookConfigFile, ttl)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, err
 | 
							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/bearertoken:go_default_library",
 | 
				
			||||||
        "//vendor/k8s.io/apiserver/pkg/authentication/request/headerrequest: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/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/request/x509:go_default_library",
 | 
				
			||||||
        "//vendor/k8s.io/apiserver/pkg/authentication/token/tokenfile:go_default_library",
 | 
					        "//vendor/k8s.io/apiserver/pkg/authentication/token/tokenfile:go_default_library",
 | 
				
			||||||
        "//vendor/k8s.io/apiserver/pkg/authentication/user: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/bearertoken"
 | 
				
			||||||
	"k8s.io/apiserver/pkg/authentication/request/headerrequest"
 | 
						"k8s.io/apiserver/pkg/authentication/request/headerrequest"
 | 
				
			||||||
	unionauth "k8s.io/apiserver/pkg/authentication/request/union"
 | 
						unionauth "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/request/x509"
 | 
				
			||||||
	webhooktoken "k8s.io/apiserver/plugin/pkg/authenticator/token/webhook"
 | 
						webhooktoken "k8s.io/apiserver/plugin/pkg/authenticator/token/webhook"
 | 
				
			||||||
	authenticationclient "k8s.io/client-go/kubernetes/typed/authentication/v1beta1"
 | 
						authenticationclient "k8s.io/client-go/kubernetes/typed/authentication/v1beta1"
 | 
				
			||||||
@@ -87,7 +88,7 @@ func (c DelegatingAuthenticatorConfig) New() (authenticator.Request, *spec.Secur
 | 
				
			|||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return nil, nil, err
 | 
								return nil, nil, err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		authenticators = append(authenticators, bearertoken.New(tokenAuth))
 | 
							authenticators = append(authenticators, bearertoken.New(tokenAuth), websocket.NewProtocolAuthenticator(tokenAuth))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		securityDefinitions["BearerToken"] = &spec.SecurityScheme{
 | 
							securityDefinitions["BearerToken"] = &spec.SecurityScheme{
 | 
				
			||||||
			SecuritySchemeProps: spec.SecuritySchemeProps{
 | 
								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
 | 
					// IsWebSocketRequest returns true if the incoming request contains connection upgrade headers
 | 
				
			||||||
// for WebSockets.
 | 
					// for WebSockets.
 | 
				
			||||||
func IsWebSocketRequest(req *http.Request) bool {
 | 
					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
 | 
					// IgnoreReceives reads from a WebSocket until it is closed, then returns. If timeout is set, the
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user