Skip to content

Commit

Permalink
Merge pull request #47740 from liggitt/websocket-protocol
Browse files Browse the repository at this point in the history
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>`
```
  • Loading branch information
Kubernetes Submit Queue committed Jun 24, 2017
2 parents 8dabdf7 + 6a872c0 commit 714f97d
Show file tree
Hide file tree
Showing 8 changed files with 385 additions and 16 deletions.
1 change: 1 addition & 0 deletions pkg/kubeapiserver/authenticator/BUILD
Expand Up @@ -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",
Expand Down
29 changes: 15 additions & 14 deletions pkg/kubeapiserver/authenticator/config.go
Expand Up @@ -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"
Expand Down Expand Up @@ -126,21 +127,21 @@ 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 {
serviceAccountAuth, err := newServiceAccountAuthenticator(config.ServiceAccountKeyFiles, config.ServiceAccountLookup, config.ServiceAccountTokenGetter)
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
}
}
Expand All @@ -155,21 +156,21 @@ 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 {
webhookTokenAuth, err := newWebhookTokenAuthenticator(config.WebhookTokenAuthnConfigFile, config.WebhookTokenAuthnCacheTTL)
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
}

Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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
}
Expand Up @@ -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",
Expand Down
Expand Up @@ -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"
Expand Down Expand Up @@ -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{
Expand Down
@@ -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 comments on commit 714f97d

Please sign in to comment.