Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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>` ```
- Loading branch information
Showing
8 changed files
with
385 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
31 changes: 31 additions & 0 deletions
31
staging/src/k8s.io/apiserver/pkg/authentication/request/websocket/BUILD
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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", | ||
], | ||
) |
109 changes: 109 additions & 0 deletions
109
staging/src/k8s.io/apiserver/pkg/authentication/request/websocket/protocol.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
Oops, something went wrong.