k8s.io/apiserver@v0.31.1/pkg/authentication/request/websocket/protocol.go (about)

     1  /*
     2  Copyright 2017 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package websocket
    18  
    19  import (
    20  	"encoding/base64"
    21  	"errors"
    22  	"net/http"
    23  	"net/textproto"
    24  	"strings"
    25  	"unicode/utf8"
    26  
    27  	"k8s.io/apimachinery/pkg/util/httpstream/wsstream"
    28  	"k8s.io/apiserver/pkg/authentication/authenticator"
    29  )
    30  
    31  const bearerProtocolPrefix = "base64url.bearer.authorization.k8s.io."
    32  
    33  var protocolHeader = textproto.CanonicalMIMEHeaderKey("Sec-WebSocket-Protocol")
    34  
    35  var errInvalidToken = errors.New("invalid bearer token")
    36  
    37  // ProtocolAuthenticator allows a websocket connection to provide a bearer token as a subprotocol
    38  // in the format "base64url.bearer.authorization.<base64url-without-padding(bearer-token)>"
    39  type ProtocolAuthenticator struct {
    40  	// auth is the token authenticator to use to validate the token
    41  	auth authenticator.Token
    42  }
    43  
    44  func NewProtocolAuthenticator(auth authenticator.Token) *ProtocolAuthenticator {
    45  	return &ProtocolAuthenticator{auth}
    46  }
    47  
    48  func (a *ProtocolAuthenticator) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) {
    49  	// Only accept websocket connections
    50  	if !wsstream.IsWebSocketRequest(req) {
    51  		return nil, false, nil
    52  	}
    53  
    54  	token := ""
    55  	sawTokenProtocol := false
    56  	filteredProtocols := []string{}
    57  	for _, protocolHeader := range req.Header[protocolHeader] {
    58  		for _, protocol := range strings.Split(protocolHeader, ",") {
    59  			protocol = strings.TrimSpace(protocol)
    60  
    61  			if !strings.HasPrefix(protocol, bearerProtocolPrefix) {
    62  				filteredProtocols = append(filteredProtocols, protocol)
    63  				continue
    64  			}
    65  
    66  			if sawTokenProtocol {
    67  				return nil, false, errors.New("multiple base64.bearer.authorization tokens specified")
    68  			}
    69  			sawTokenProtocol = true
    70  
    71  			encodedToken := strings.TrimPrefix(protocol, bearerProtocolPrefix)
    72  			decodedToken, err := base64.RawURLEncoding.DecodeString(encodedToken)
    73  			if err != nil {
    74  				return nil, false, errors.New("invalid base64.bearer.authorization token encoding")
    75  			}
    76  			if !utf8.Valid(decodedToken) {
    77  				return nil, false, errors.New("invalid base64.bearer.authorization token")
    78  			}
    79  			token = string(decodedToken)
    80  		}
    81  	}
    82  
    83  	// Must pass at least one other subprotocol so that we can remove the one containing the bearer token,
    84  	// and there is at least one to echo back to the client
    85  	if len(token) > 0 && len(filteredProtocols) == 0 {
    86  		return nil, false, errors.New("missing additional subprotocol")
    87  	}
    88  
    89  	if len(token) == 0 {
    90  		return nil, false, nil
    91  	}
    92  
    93  	resp, ok, err := a.auth.AuthenticateToken(req.Context(), token)
    94  
    95  	// on success, remove the protocol with the token
    96  	if ok {
    97  		// https://tools.ietf.org/html/rfc6455#section-11.3.4 indicates the Sec-WebSocket-Protocol header may appear multiple times
    98  		// in a request, and is logically the same as a single Sec-WebSocket-Protocol header field that contains all values
    99  		req.Header.Set(protocolHeader, strings.Join(filteredProtocols, ","))
   100  	}
   101  
   102  	// If the token authenticator didn't error, provide a default error
   103  	if !ok && err == nil {
   104  		err = errInvalidToken
   105  	}
   106  
   107  	return resp, ok, err
   108  }