Bumps [github.com/openshift-online/ocm-sdk-go](https://github.com/openshift-online/ocm-sdk-go) from 0.1.204 to 0.1.208. - [Release notes](https://github.com/openshift-online/ocm-sdk-go/releases) - [Changelog](https://github.com/openshift-online/ocm-sdk-go/blob/master/CHANGES.adoc) - [Commits](https://github.com/openshift-online/ocm-sdk-go/compare/v0.1.204...v0.1.208) --- updated-dependencies: - dependency-name: github.com/openshift-online/ocm-sdk-go dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com>
1026 lines
27 KiB
Go
1026 lines
27 KiB
Go
/*
|
|
Copyright (c) 2019 Red Hat, Inc.
|
|
|
|
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 authentication
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rsa"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"math/big"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/ghodss/yaml"
|
|
"github.com/golang-jwt/jwt"
|
|
|
|
"github.com/openshift-online/ocm-sdk-go/errors"
|
|
"github.com/openshift-online/ocm-sdk-go/logging"
|
|
)
|
|
|
|
// HandlerBuilder contains the data and logic needed to create a new authentication handler. Don't
|
|
// create objects of this type directly, use the NewHandler function instead.
|
|
type HandlerBuilder struct {
|
|
logger logging.Logger
|
|
publicPaths []string
|
|
keysFiles []string
|
|
keysURLs []string
|
|
keysCAs *x509.CertPool
|
|
keysInsecure bool
|
|
aclFiles []string
|
|
service string
|
|
error string
|
|
operationID func(*http.Request) string
|
|
tolerance time.Duration
|
|
cookie string
|
|
next http.Handler
|
|
}
|
|
|
|
// Handler is an HTTP handler that checks authentication using the JWT tokens from the authorization
|
|
// header.
|
|
type Handler struct {
|
|
logger logging.Logger
|
|
publicPaths []*regexp.Regexp
|
|
tokenParser *jwt.Parser
|
|
keysFiles []string
|
|
keysURLs []string
|
|
keysClient *http.Client
|
|
keys *sync.Map
|
|
lastKeyReload time.Time
|
|
aclItems map[string]*regexp.Regexp
|
|
service string
|
|
error string
|
|
operationID func(*http.Request) string
|
|
tolerance time.Duration
|
|
cookie string
|
|
next http.Handler
|
|
}
|
|
|
|
// NewHandler creates a builder that can then be configured and used to create authentication
|
|
// handlers.
|
|
func NewHandler() *HandlerBuilder {
|
|
return &HandlerBuilder{
|
|
cookie: defaultCookie,
|
|
}
|
|
}
|
|
|
|
// Logger sets the logger that the middleware will use to send messages to the log. This is
|
|
// mandatory.
|
|
func (b *HandlerBuilder) Logger(value logging.Logger) *HandlerBuilder {
|
|
b.logger = value
|
|
return b
|
|
}
|
|
|
|
// Public sets a regular expression that defines the parts of the URL space that considered public,
|
|
// and therefore require no authentication. This method may be called multiple times and then all
|
|
// the given regular expressions will be used to check what parts of the URL space are public.
|
|
func (b *HandlerBuilder) Public(value string) *HandlerBuilder {
|
|
b.publicPaths = append(b.publicPaths, value)
|
|
return b
|
|
}
|
|
|
|
// KeysFile sets the location of a file containing a JSON web key set that will be used to verify
|
|
// the signatures of the tokens. The keys from this file will be loaded when a token is received
|
|
// containing an unknown key identifier.
|
|
//
|
|
// At least one keys file or one keys URL is mandatory.
|
|
func (b *HandlerBuilder) KeysFile(value string) *HandlerBuilder {
|
|
if value != "" {
|
|
b.keysFiles = append(b.keysFiles, value)
|
|
}
|
|
return b
|
|
}
|
|
|
|
// KeysURL sets the URL containing a JSON web key set that will be used to verify the signatures of
|
|
// the tokens. The keys from these URLs will be loaded when a token is received containing an
|
|
// unknown key identifier.
|
|
//
|
|
// At least one keys file or one keys URL is mandatory.
|
|
func (b *HandlerBuilder) KeysURL(value string) *HandlerBuilder {
|
|
if value != "" {
|
|
b.keysURLs = append(b.keysURLs, value)
|
|
}
|
|
return b
|
|
}
|
|
|
|
// KeysCAs sets the certificate authorities that will be trusted when verifying the certificate of
|
|
// the web server where keys are loaded from.
|
|
func (b *HandlerBuilder) KeysCAs(value *x509.CertPool) *HandlerBuilder {
|
|
b.keysCAs = value
|
|
return b
|
|
}
|
|
|
|
// KeysInsecure sets the flag that indicates that the certificate of the web server where the keys
|
|
// are loaded from should not be checked. The default is false and changing it to true makes the
|
|
// token verification insecure, so refrain from doing that in security sensitive environments.
|
|
func (b *HandlerBuilder) KeysInsecure(value bool) *HandlerBuilder {
|
|
b.keysInsecure = value
|
|
return b
|
|
}
|
|
|
|
// ACLFile sets a file that contains items of the access control list. This should be a YAML file
|
|
// with the following format:
|
|
//
|
|
// - claim: email
|
|
// pattern: ^.*@redhat\.com$
|
|
//
|
|
// - claim: sub
|
|
// pattern: ^f:b3f7b485-7184-43c8-8169-37bd6d1fe4aa:myuser$
|
|
//
|
|
// The claim field is the name of the claim of the JWT token that will be checked. The pattern field
|
|
// is a regular expression. If the claim matches the regular expression then access will be allowed.
|
|
//
|
|
// If the ACL is empty then access will be allowed to all JWT tokens.
|
|
//
|
|
// If the ACL has at least one item then access will be allowed only to tokens that match at least
|
|
// one of the items.
|
|
func (b *HandlerBuilder) ACLFile(value string) *HandlerBuilder {
|
|
if value != "" {
|
|
b.aclFiles = append(b.aclFiles, value)
|
|
}
|
|
return b
|
|
}
|
|
|
|
// Next sets the HTTP handler that will be called when the authentication handler has authenticated
|
|
// correctly the request. This is mandatory.
|
|
func (b *HandlerBuilder) Next(value http.Handler) *HandlerBuilder {
|
|
b.next = value
|
|
return b
|
|
}
|
|
|
|
// Service sets the identifier of the service that will be used to generate error codes. For
|
|
// example, if the value is `my_service` then the JSON for error responses will be like this:
|
|
//
|
|
// {
|
|
// "kind": "Error",
|
|
// "id": "401",
|
|
// "href": "/api/clusters_mgmt/v1/errors/401",
|
|
// "code": "MY-SERVICE-401",
|
|
// "reason": "Bearer token is expired"
|
|
// }
|
|
//
|
|
// When this isn't explicitly provided the value will be extracted from the second segment of the
|
|
// request path. For example, if the request URL is `/api/clusters_mgmt/v1/cluster` the value will
|
|
// be `clusters_mgmt`.
|
|
func (b *HandlerBuilder) Service(value string) *HandlerBuilder {
|
|
b.service = value
|
|
return b
|
|
}
|
|
|
|
// Error sets the error identifier that will be used to generate JSON error responses. For example,
|
|
// if the value is `123` then the JSON for error responses will be like this:
|
|
//
|
|
// {
|
|
// "kind": "Error",
|
|
// "id": "11",
|
|
// "href": "/api/clusters_mgmt/v1/errors/11",
|
|
// "code": "CLUSTERS-MGMT-11",
|
|
// "reason": "Bearer token is expired"
|
|
// }
|
|
//
|
|
// When this isn't explicitly provided the value will be `401`. Note that changing this doesn't
|
|
// change the HTTP response status, that will always be 401.
|
|
func (b *HandlerBuilder) Error(value string) *HandlerBuilder {
|
|
b.error = value
|
|
return b
|
|
}
|
|
|
|
// OperationID sets a function that will be called each time an error is detected, passing the
|
|
// details of the request that caused the error. The value returned by the function will be included
|
|
// in the `operation_id` field of the JSON error response. For example, if the function returns
|
|
// `123` the generated JSON error response will be like this:
|
|
//
|
|
// {
|
|
// "kind": "Error",
|
|
// "id": "401",
|
|
// "href": "/api/clusters_mgmt/v1/errors/401",
|
|
// "code": "CLUSTERS-MGMT-401",
|
|
// "reason": "Bearer token is expired".
|
|
// "operation_id": "123"
|
|
// }
|
|
//
|
|
// For example, if the operation identifier is available in an HTTP header named `X-Operation-ID`
|
|
// then the handler can be configured like this to use it:
|
|
//
|
|
// handler, err := authentication.NewHandler().
|
|
// Logger(logger).
|
|
// KeysURL("https://...").
|
|
// OperationID(func(r *http.Request) string {
|
|
// return r.Header.Get("X-Operation-ID")
|
|
// }).
|
|
// Next(next).
|
|
// Build()
|
|
// if err != nil {
|
|
// ...
|
|
// }
|
|
//
|
|
// If the function returns an empty string then the `operation_id` field will not be added.
|
|
//
|
|
// By default there is no function configured for this, so no `operation_id` field will be added.
|
|
func (b *HandlerBuilder) OperationID(value func(r *http.Request) string) *HandlerBuilder {
|
|
b.operationID = value
|
|
return b
|
|
}
|
|
|
|
// Tolerance sets the maximum time that a token will be considered valid after it has expired. For
|
|
// example, to accept requests with tokens that have expired up to five minutes ago:
|
|
//
|
|
// handler, err := authentication.NewHandler().
|
|
// Logger(logger).
|
|
// KeysURL("https://...").
|
|
// Tolerance(5 * time.Minute).
|
|
// Next(next).
|
|
// Build()
|
|
// if err != nil {
|
|
// ...
|
|
// }
|
|
//
|
|
// The default value is zero tolerance.
|
|
func (b *HandlerBuilder) Tolerance(value time.Duration) *HandlerBuilder {
|
|
b.tolerance = value
|
|
return b
|
|
}
|
|
|
|
// Cookie sets the name of the cookie where the bearer token will be extracted from when the
|
|
// `Authorization` header isn't present. The default is `cs_jwt`.
|
|
func (b *HandlerBuilder) Cookie(value string) *HandlerBuilder {
|
|
b.cookie = value
|
|
return b
|
|
}
|
|
|
|
// Build uses the data stored in the builder to create a new authentication handler.
|
|
func (b *HandlerBuilder) Build() (handler *Handler, err error) {
|
|
// Check parameters:
|
|
if b.logger == nil {
|
|
err = fmt.Errorf("logger is mandatory")
|
|
return
|
|
}
|
|
if b.tolerance < 0 {
|
|
err = fmt.Errorf("tolerance must be zero or positive")
|
|
return
|
|
}
|
|
if b.next == nil {
|
|
err = fmt.Errorf("next handler is mandatory")
|
|
return
|
|
}
|
|
|
|
// Check that there is at least one keys source:
|
|
if len(b.keysFiles)+len(b.keysURLs) == 0 {
|
|
err = fmt.Errorf("at least one keys file or one keys URL must be configured")
|
|
return
|
|
}
|
|
|
|
// Check that all the configured keys files exist:
|
|
for _, file := range b.keysFiles {
|
|
var info os.FileInfo
|
|
info, err = os.Stat(file)
|
|
if err != nil {
|
|
err = fmt.Errorf("keys file '%s' doesn't exist: %w", file, err)
|
|
return
|
|
}
|
|
if !info.Mode().IsRegular() {
|
|
err = fmt.Errorf("keys file '%s' isn't a regular file", file)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Check that all the configured keys URLs are valid HTTPS URLs:
|
|
for _, addr := range b.keysURLs {
|
|
var parsed *url.URL
|
|
parsed, err = url.Parse(addr)
|
|
if err != nil {
|
|
err = fmt.Errorf("keys URL '%s' isn't a valid URL: %w", addr, err)
|
|
return
|
|
}
|
|
if !strings.EqualFold(parsed.Scheme, "https") {
|
|
err = fmt.Errorf(
|
|
"keys URL '%s' doesn't use the HTTPS protocol: %w",
|
|
addr, err,
|
|
)
|
|
}
|
|
}
|
|
|
|
// Create the HTTP client that will be used to load the keys:
|
|
keysClient := &http.Client{
|
|
Transport: &http.Transport{
|
|
TLSClientConfig: &tls.Config{
|
|
RootCAs: b.keysCAs,
|
|
InsecureSkipVerify: b.keysInsecure, // nolint
|
|
},
|
|
},
|
|
}
|
|
|
|
// Try to compile the regular expressions that define the parts of the URL space that are
|
|
// public:
|
|
public := make([]*regexp.Regexp, len(b.publicPaths))
|
|
for i, expr := range b.publicPaths {
|
|
public[i], err = regexp.Compile(expr)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
// Create the bearer token parser:
|
|
tokenParser := &jwt.Parser{}
|
|
|
|
// Make copies of the lists of keys files and URLs:
|
|
keysFiles := make([]string, len(b.keysFiles))
|
|
copy(keysFiles, b.keysFiles)
|
|
keysURLs := make([]string, len(b.keysURLs))
|
|
copy(keysURLs, b.keysURLs)
|
|
|
|
// Create the initial empty map of keys:
|
|
keys := &sync.Map{}
|
|
|
|
// Load the ACL files:
|
|
aclItems := map[string]*regexp.Regexp{}
|
|
for _, file := range b.aclFiles {
|
|
err = b.loadACLFile(file, aclItems)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
// Create and populate the object:
|
|
handler = &Handler{
|
|
logger: b.logger,
|
|
publicPaths: public,
|
|
tokenParser: tokenParser,
|
|
keysFiles: keysFiles,
|
|
keysURLs: keysURLs,
|
|
keysClient: keysClient,
|
|
keys: keys,
|
|
aclItems: aclItems,
|
|
service: b.service,
|
|
error: b.error,
|
|
operationID: b.operationID,
|
|
tolerance: b.tolerance,
|
|
cookie: b.cookie,
|
|
next: b.next,
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// aclItem is the type used to read a single ACL item from a YAML document.
|
|
type aclItem struct {
|
|
Claim string `json:"claim"`
|
|
Pattern string `json:"pattern"`
|
|
}
|
|
|
|
// loadACLFile loads the given ACL file into the given map of ACL items.
|
|
func (b *HandlerBuilder) loadACLFile(file string, items map[string]*regexp.Regexp) error {
|
|
// Load the YAML data:
|
|
yamlData, err := ioutil.ReadFile(file) // nolint
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Parse the YAML data:
|
|
var listData []aclItem
|
|
err = yaml.Unmarshal(yamlData, &listData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Process the items:
|
|
for _, itemData := range listData {
|
|
items[itemData.Claim], err = regexp.Compile(itemData.Pattern)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ServeHTTP is the implementation of the HTTP handler interface.
|
|
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
// Get the context:
|
|
ctx := r.Context()
|
|
|
|
// Check if the requested path is public, and skip authentication if it is:
|
|
for _, expr := range h.publicPaths {
|
|
if expr.MatchString(r.URL.Path) {
|
|
h.next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Try to extract the credentials from the `Authorization` header:
|
|
var bearer string
|
|
header := r.Header.Get("Authorization")
|
|
if header != "" {
|
|
matches := bearerRE.FindStringSubmatch(header)
|
|
if len(matches) != 3 {
|
|
h.sendError(
|
|
w, r,
|
|
"Authorization header '%s' is malformed",
|
|
header,
|
|
)
|
|
return
|
|
}
|
|
scheme := matches[1]
|
|
if !strings.EqualFold(scheme, "Bearer") {
|
|
h.sendError(
|
|
w, r,
|
|
"Authentication type '%s' isn't supported",
|
|
scheme,
|
|
)
|
|
return
|
|
}
|
|
bearer = matches[2]
|
|
}
|
|
|
|
// If it wasn't possible to extract the credentials from the `Authorization` header then try
|
|
// to get them from the cookies:
|
|
if bearer == "" && h.cookie != "" {
|
|
for _, cookie := range r.Cookies() {
|
|
if cookie.Name == h.cookie {
|
|
bearer = cookie.Value
|
|
}
|
|
}
|
|
}
|
|
|
|
// Report an error if after tying headers and cookies we still don't have credentials:
|
|
if bearer == "" {
|
|
if h.cookie != "" {
|
|
h.sendError(
|
|
w, r,
|
|
"Request doesn't contain the 'Authorization' header or "+
|
|
"the '%s' cookie",
|
|
h.cookie,
|
|
)
|
|
} else {
|
|
h.sendError(
|
|
w, r,
|
|
"Request doesn't contain the 'Authorization' header",
|
|
)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Use the JWT library to verify that the token is correctly signed and that the basic
|
|
// claims are correct:
|
|
token, claims, ok := h.checkToken(w, r, bearer)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
// The library that we use considers tokens valid if the claims that it checks don't exist,
|
|
// but we want to reject those tokens, so we need to do some additional validations:
|
|
ok = h.checkClaims(w, r, claims)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
// Check if the claims match at least one of the ACL items:
|
|
ok = h.checkACL(w, r, claims)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
// Add the token to the context:
|
|
ctx = ContextWithToken(ctx, token.object)
|
|
r = r.WithContext(ctx)
|
|
|
|
// Call the next handler:
|
|
h.next.ServeHTTP(w, r)
|
|
}
|
|
|
|
// selectKey selects the key that should be used to verify the given token.
|
|
func (h *Handler) selectKey(ctx context.Context, token *jwt.Token) (key interface{}, err error) {
|
|
// Get the key identifier:
|
|
value, ok := token.Header["kid"]
|
|
if !ok {
|
|
err = fmt.Errorf("token doesn't have a 'kid' field in the header")
|
|
return
|
|
}
|
|
kid, ok := value.(string)
|
|
if !ok {
|
|
err = fmt.Errorf(
|
|
"token has a 'kid' field, but it is a %T instead of a string",
|
|
value,
|
|
)
|
|
return
|
|
}
|
|
|
|
// Get the key for that key identifier. If there is no such key and we didn't reload keys
|
|
// recently then we try to reload them now.
|
|
key, ok = h.keys.Load(kid)
|
|
if !ok && time.Since(h.lastKeyReload) > 1*time.Minute {
|
|
err = h.loadKeys(ctx)
|
|
if err != nil {
|
|
return
|
|
}
|
|
h.lastKeyReload = time.Now()
|
|
key, ok = h.keys.Load(kid)
|
|
}
|
|
if !ok {
|
|
err = fmt.Errorf("there is no key for key identifier '%s'", kid)
|
|
return
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// keyData is the type used to read a single key from a JSON document.
|
|
type keyData struct {
|
|
Kid string `json:"kid"`
|
|
Kty string `json:"kty"`
|
|
Alg string `json:"alg"`
|
|
Use string `json:"use"`
|
|
N string `json:"n"`
|
|
E string `json:"e"`
|
|
}
|
|
|
|
// setData is the type used to read a collection of keys from a JSON document.
|
|
type setData struct {
|
|
Keys []keyData `json:"keys"`
|
|
}
|
|
|
|
// loadKeys loads the JSON web key set from the URLs specified in the configuration.
|
|
func (h *Handler) loadKeys(ctx context.Context) error {
|
|
// Load keys from the files given in the configuration:
|
|
for _, keysFile := range h.keysFiles {
|
|
h.logger.Info(ctx, "Loading keys from file '%s'", keysFile)
|
|
err := h.loadKeysFile(ctx, keysFile)
|
|
if err != nil {
|
|
h.logger.Error(ctx, "Can't load keys from file '%s': %v", keysFile, err)
|
|
}
|
|
}
|
|
|
|
// Load keys from URLs given in the configuration:
|
|
for _, keysURL := range h.keysURLs {
|
|
h.logger.Info(ctx, "Loading keys from URL '%s'", keysURL)
|
|
err := h.loadKeysURL(ctx, keysURL)
|
|
if err != nil {
|
|
h.logger.Error(ctx, "Can't load keys from URL '%s': %v", keysURL, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// loadKeysFile loads a JSON we key set from a file.
|
|
func (h *Handler) loadKeysFile(ctx context.Context, file string) error {
|
|
reader, err := os.Open(file) // nolint
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return h.readKeys(ctx, reader)
|
|
}
|
|
|
|
// loadKeysURL loads a JSON we key set from an URL.
|
|
func (h *Handler) loadKeysURL(ctx context.Context, addr string) error {
|
|
request, err := http.NewRequest(http.MethodGet, addr, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
request = request.WithContext(ctx)
|
|
response, err := h.keysClient.Do(request)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
err := response.Body.Close()
|
|
if err != nil {
|
|
h.logger.Error(
|
|
ctx,
|
|
"Can't close response body for request to '%s': %v",
|
|
addr, err,
|
|
)
|
|
}
|
|
}()
|
|
return h.readKeys(ctx, response.Body)
|
|
}
|
|
|
|
// readKeys reads the keys from JSON web key set available in the given reader.
|
|
func (h *Handler) readKeys(ctx context.Context, reader io.Reader) error {
|
|
// Read the JSON data:
|
|
jsonData, err := ioutil.ReadAll(reader)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Parse the JSON data:
|
|
var setData setData
|
|
err = json.Unmarshal(jsonData, &setData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Convert the key data to actual keys that can be used to verify the signatures of the
|
|
// tokens:
|
|
for _, keyData := range setData.Keys {
|
|
if h.logger.DebugEnabled() {
|
|
h.logger.Debug(ctx, "Value of 'kid' is '%s'", keyData.Kid)
|
|
h.logger.Debug(ctx, "Value of 'kty' is '%s'", keyData.Kty)
|
|
h.logger.Debug(ctx, "Value of 'alg' is '%s'", keyData.Alg)
|
|
h.logger.Debug(ctx, "Value of 'e' is '%s'", keyData.E)
|
|
h.logger.Debug(ctx, "Value of 'n' is '%s'", keyData.N)
|
|
}
|
|
if keyData.Kid == "" {
|
|
h.logger.Error(ctx, "Can't read key because 'kid' is empty")
|
|
continue
|
|
}
|
|
if keyData.Kty == "" {
|
|
h.logger.Error(
|
|
ctx,
|
|
"Can't read key '%s' because 'kty' is empty",
|
|
keyData.Kid,
|
|
)
|
|
continue
|
|
}
|
|
if keyData.Alg == "" {
|
|
h.logger.Error(
|
|
ctx,
|
|
"Can't read key '%s' because 'alg' is empty",
|
|
keyData.Kid,
|
|
)
|
|
continue
|
|
}
|
|
if keyData.E == "" {
|
|
h.logger.Error(
|
|
ctx,
|
|
"Can't read key '%s' because 'e' is empty",
|
|
keyData.Kid,
|
|
)
|
|
continue
|
|
}
|
|
if keyData.E == "" {
|
|
h.logger.Error(
|
|
ctx,
|
|
"Can't read key '%s' because 'n' is empty",
|
|
keyData.Kid,
|
|
)
|
|
continue
|
|
}
|
|
var key interface{}
|
|
key, err = h.parseKey(keyData)
|
|
if err != nil {
|
|
h.logger.Error(
|
|
ctx,
|
|
"Key '%s' will be ignored because it can't be parsed",
|
|
keyData.Kid,
|
|
)
|
|
continue
|
|
}
|
|
h.keys.Store(keyData.Kid, key)
|
|
h.logger.Info(ctx, "Loaded key '%s'", keyData.Kid)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// parseKey converts the key data loaded from the JSON document to an actual key that can be used
|
|
// to verify the signatures of tokens.
|
|
func (h *Handler) parseKey(data keyData) (key interface{}, err error) {
|
|
// Check key type:
|
|
if data.Kty != "RSA" {
|
|
err = fmt.Errorf("key type '%s' isn't supported", data.Kty)
|
|
return
|
|
}
|
|
|
|
// Decode the e and n values:
|
|
nb, err := base64.RawURLEncoding.DecodeString(data.N)
|
|
if err != nil {
|
|
return
|
|
}
|
|
eb, err := base64.RawURLEncoding.DecodeString(data.E)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Create the key:
|
|
key = &rsa.PublicKey{
|
|
N: new(big.Int).SetBytes(nb),
|
|
E: int(new(big.Int).SetBytes(eb).Int64()),
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// checkToken checks if the token is valid. If it is valid it returns the parsed token, the
|
|
// claims and true. If it isn't valid it sends an error response to the client and returns false.
|
|
func (h *Handler) checkToken(w http.ResponseWriter, r *http.Request,
|
|
bearer string) (token *tokenInfo, claims jwt.MapClaims, ok bool) {
|
|
// Get the context:
|
|
ctx := r.Context()
|
|
|
|
// Parse the token:
|
|
claims = jwt.MapClaims{}
|
|
object, err := h.tokenParser.ParseWithClaims(
|
|
bearer, claims,
|
|
func(token *jwt.Token) (key interface{}, err error) {
|
|
return h.selectKey(ctx, token)
|
|
},
|
|
)
|
|
token = &tokenInfo{
|
|
text: bearer,
|
|
object: object,
|
|
}
|
|
if err != nil {
|
|
switch typed := err.(type) {
|
|
case *jwt.ValidationError:
|
|
switch {
|
|
case typed.Errors&jwt.ValidationErrorMalformed != 0:
|
|
h.sendError(
|
|
w, r,
|
|
"Bearer token is malformed",
|
|
)
|
|
ok = false
|
|
case typed.Errors&jwt.ValidationErrorUnverifiable != 0:
|
|
h.sendError(
|
|
w, r,
|
|
"Bearer token can't be verified",
|
|
)
|
|
ok = false
|
|
case typed.Errors&jwt.ValidationErrorSignatureInvalid != 0:
|
|
h.sendError(
|
|
w, r,
|
|
"Signature of bearer token isn't valid",
|
|
)
|
|
ok = false
|
|
case typed.Errors&jwt.ValidationErrorExpired != 0:
|
|
// When the token is expired according to the JWT library we may
|
|
// still want to accept it if we have a configured tolerance:
|
|
if h.tolerance > 0 {
|
|
var remaining time.Duration
|
|
_, remaining, err = tokenRemaining(token, time.Now())
|
|
if err != nil {
|
|
h.logger.Error(
|
|
ctx,
|
|
"Can't check token duration: %v",
|
|
err,
|
|
)
|
|
remaining = 0
|
|
}
|
|
if -remaining <= h.tolerance {
|
|
ok = true
|
|
} else {
|
|
h.sendError(
|
|
w, r,
|
|
"Bearer token is expired",
|
|
)
|
|
ok = false
|
|
}
|
|
} else {
|
|
h.sendError(
|
|
w, r,
|
|
"Bearer token is expired",
|
|
)
|
|
ok = false
|
|
}
|
|
case typed.Errors&jwt.ValidationErrorIssuedAt != 0:
|
|
h.sendError(
|
|
w, r,
|
|
"Bearer token was issued in the future",
|
|
)
|
|
ok = false
|
|
case typed.Errors&jwt.ValidationErrorNotValidYet != 0:
|
|
h.sendError(
|
|
w, r,
|
|
"Bearer token isn't valid yet",
|
|
)
|
|
ok = false
|
|
default:
|
|
h.sendError(
|
|
w, r,
|
|
"Bearer token isn't valid",
|
|
)
|
|
ok = false
|
|
}
|
|
default:
|
|
h.sendError(
|
|
w, r,
|
|
"Bearer token is malformed",
|
|
)
|
|
ok = false
|
|
}
|
|
return
|
|
}
|
|
ok = true
|
|
return
|
|
}
|
|
|
|
// checkClaims checks that the required claims are present and that they have valid values. If
|
|
// something is wrong it sends an error response to the client and returns false.
|
|
func (h *Handler) checkClaims(w http.ResponseWriter, r *http.Request,
|
|
claims jwt.MapClaims) bool {
|
|
// The `typ` claim is optional, but if it exists the value must be `Bearer`:
|
|
value, ok := claims["typ"]
|
|
if ok {
|
|
typ, ok := value.(string)
|
|
if !ok {
|
|
h.sendError(
|
|
w, r,
|
|
"Bearer token type claim contains incorrect string value '%v'",
|
|
value,
|
|
)
|
|
return false
|
|
}
|
|
if !strings.EqualFold(typ, "Bearer") {
|
|
h.sendError(
|
|
w, r,
|
|
"Bearer token type '%s' isn't allowed",
|
|
typ,
|
|
)
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Check the format of the issue and expiration date claims:
|
|
_, ok = h.checkTimeClaim(w, r, claims, "iat")
|
|
if !ok {
|
|
return false
|
|
}
|
|
_, ok = h.checkTimeClaim(w, r, claims, "exp")
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
// Make sure that the impersonation flag claim doesn't exist, or is `false`:
|
|
value, ok = claims["impersonated"]
|
|
if ok {
|
|
flag, ok := value.(bool)
|
|
if !ok {
|
|
h.sendError(
|
|
w, r,
|
|
"Impersonation claim contains incorrect boolean value '%v'",
|
|
value,
|
|
)
|
|
return false
|
|
}
|
|
if flag {
|
|
h.sendError(
|
|
w, r,
|
|
"Impersonation isn't allowed",
|
|
)
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// checkTimeClaim checks that the given claim exists and that the value is a time. If it doesn't
|
|
// exist or it has a wrong type it sends an error response to the client and returns false. If it
|
|
// exists it returns its value and true.
|
|
func (h *Handler) checkTimeClaim(w http.ResponseWriter, r *http.Request,
|
|
claims jwt.MapClaims, name string) (result time.Time, ok bool) {
|
|
value, ok := h.checkClaim(w, r, claims, name)
|
|
if !ok {
|
|
return
|
|
}
|
|
seconds, ok := value.(float64)
|
|
if !ok {
|
|
h.sendError(
|
|
w, r,
|
|
"Bearer token claim '%s' contains incorrect time value '%v'",
|
|
name, value,
|
|
)
|
|
return
|
|
}
|
|
result = time.Unix(int64(seconds), 0)
|
|
return
|
|
}
|
|
|
|
// checkClaim checks that the given claim exists. If it doesn't exist it sends an error response to
|
|
// the client and returns false. If it exists it returns its value and true.
|
|
func (h *Handler) checkClaim(w http.ResponseWriter, r *http.Request, claims jwt.MapClaims,
|
|
name string) (value interface{}, ok bool) {
|
|
value, ok = claims[name]
|
|
if !ok {
|
|
h.sendError(
|
|
w, r,
|
|
"Bearer token doesn't contain required claim '%s'",
|
|
name,
|
|
)
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
// checkACL checks if the given set of claims match at least one of the items of the access control
|
|
// list. If there is no match it sends an error response to the client and returns false. If there
|
|
// is a match or the ACL is empty it returns true.
|
|
func (h *Handler) checkACL(w http.ResponseWriter, r *http.Request, claims jwt.MapClaims) bool {
|
|
// If there are no ACL items we consider that there are no restrictions, therefore we
|
|
// return true immediately:
|
|
if len(h.aclItems) == 0 {
|
|
return true
|
|
}
|
|
|
|
// Check all the ACL items:
|
|
for claim, pattern := range h.aclItems {
|
|
value, ok := claims[claim]
|
|
if !ok {
|
|
continue
|
|
}
|
|
text, ok := value.(string)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if pattern.MatchString(text) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
// No match, so the access is denied:
|
|
h.sendError(
|
|
w, r,
|
|
"Access denied",
|
|
)
|
|
return false
|
|
}
|
|
|
|
// sendError sends an error response to the client with the given status code and with a message
|
|
// compossed using the given format and arguments as the fmt.Sprintf function does.
|
|
func (h *Handler) sendError(w http.ResponseWriter, r *http.Request, format string, args ...interface{}) {
|
|
// Get the context:
|
|
ctx := r.Context()
|
|
|
|
// Prepare the body:
|
|
segments := strings.Split(r.URL.Path, "/")
|
|
realm := ""
|
|
builder := errors.NewError()
|
|
id := h.error
|
|
if id == "" {
|
|
id = fmt.Sprintf("%d", http.StatusUnauthorized)
|
|
}
|
|
builder.ID(id)
|
|
if len(segments) >= 4 {
|
|
service := h.service
|
|
if h.service == "" {
|
|
service = segments[2]
|
|
}
|
|
version := segments[3]
|
|
builder.HREF(fmt.Sprintf(
|
|
"/%s/%s/%s/errors/%s",
|
|
segments[1], segments[2], segments[3], id,
|
|
))
|
|
builder.Code(fmt.Sprintf(
|
|
"%s-%s",
|
|
strings.ToUpper(strings.ReplaceAll(service, "_", "-")),
|
|
id,
|
|
))
|
|
realm = fmt.Sprintf("%s/%s", service, version)
|
|
}
|
|
builder.Reason(fmt.Sprintf(format, args...))
|
|
if h.operationID != nil {
|
|
operationID := h.operationID(r)
|
|
if operationID != "" {
|
|
builder.OperationID(operationID)
|
|
}
|
|
}
|
|
body, err := builder.Build()
|
|
if err != nil {
|
|
h.logger.Error(ctx, "Can't build error response: %v", err)
|
|
errors.SendPanic(w, r)
|
|
}
|
|
|
|
// Send the response:
|
|
w.Header().Set("WWW-Authenticate", fmt.Sprintf("Bearer realm=\"%s\"", realm))
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
err = errors.MarshalError(body, w)
|
|
if err != nil {
|
|
h.logger.Error(
|
|
r.Context(),
|
|
"Can't send response body for request '%s': %v",
|
|
r.URL.Path,
|
|
err,
|
|
)
|
|
}
|
|
}
|
|
|
|
// Regular expression used to extract the bearer token from the authorization header:
|
|
var bearerRE = regexp.MustCompile(`^([a-zA-Z0-9]+)\s+(.*)$`)
|
|
|
|
// Name of the cookie used to extract the bearer token when the `Authorization` header isn't
|
|
// part of the request
|
|
var defaultCookie = "cs_jwt"
|