jwt_middleware

package module
v1.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 11, 2023 License: Apache-2.0, MIT Imports: 19 Imported by: 0

README

Go Coverage

Dynamic JWT Validation Middleware

This is a middleware plugin for Traefik with the following features:

  • Validation of JSON Web Tokens in cookies, headers, and/or query string parameters for access control.
  • Dynamic lookup of public keys from the well-known JWKS endpoint of whitelisted issuers.
  • HTTP redirects for unauthorized and forbidden calls when configured in interactive mode.
  • Flexible claim checks, including optional wildcards and Go template interpolation.

Configuration

1a. Add the plugin to traefik, either in your static traefik config file:

experimental:
  plugins:
    jwt:
      moduleName: github.com/Brainnwave/jwt-middleware
      version: v1.1.5

1b. or with command-line options:

command:
  ...
  - "--experimental.plugins.jwt.modulename=github.com/Brainnwave/jwt-middleware"
  - "--experimental.plugins.jwt.version=v1.1.5"
  1. Configure and activate the plugin as a middleware in your dynamic traefik config:
http:
  middlewares:
    secure-api:
      plugin:
        jwt:
          issuers:
            - https://auth.example.com
          require:
            aud: test.example.com
  1. use the middleware in services via docker-compose labels
  labels:
    - "traefik.http.routers.my-service.middlewares=secure-api@file"
Options

The plugin supports the following configuration options.

Name Description
issuers A list of trusted issuers to fetch JWKs from. Keys will be prefetched from these issuers on startup. If a token contains a kid that is not known and the iss claim matches one of the issuers, a call will be made to refresh the keys in the plugin. Any keys previously fetched from the issuer that are no longer retrieved will be removed from the plugin's cache on each fetch. fnmatch-style wildcards are supported to accommodate some multitenancy scenarios (e.g. https://*.example.com). It is not recommended to use wildcard issuers unless you understand the implication that any webserver on your domain could be used to spoof a JWK endpoint unless you have full confidence in your DNS security and what is running on all servers within the domain in question.
secret A shared secret or a fixed public key to use for signature validation. A fixed secret may be used in conjunction with issuers to combine dynamic and static keys. This can be useful when transitioning from earlier systems or for machine-to-machine tokens signed with internal keys. Note that if a dynamic key is not matched but a static secret is configured, the static secret will be used as a fallback key. If this secret is not of the correct type for the presented key, an error such as token signature is invalid: key is of invalid type will be returned to the user, which may be confusing.
require A map of zero or more claims that must be present and match against one or more values. If not claims are specified, all tokens that are validly signed by the trusted issuers or with the shared secret will pass. If more than one claim is specified, each is required (i.e an AND relationship exists for all the specified claims). For each claim, multiple values may be specified and the claim will be valid if any matches (i.e. an OR relations exists for values within a claim). fnmatch-style wildcards are supported for claim values. Go template interpolation is support for access to (full) URL, Host, Scheme, Path (including query string).
headerMap A map in the form of Header: claim. Headers will be added (or overwritten) by claim values from the token. If the claim is not present, no action for that value is taken (and any existing header will remain unchanged).
cookieName Name of the cookie to retrieve the token from if present. Default: Authorization. If token retrieval from cookies must be disabled for some reason, set to an empty string. If forwardAuth is false, the cookie will be removed before forwarding to the backend.
headerName Name of the Header to retrieve the token from if present. Default: Authorization. If token retrieval from headers must be disabled for some reason, set to an empty string. Tokens are supported either with or without a Bearer prefix. If forwardAuth is false, the header will be removed before forwarding to the backend.
parameterName Name of the query string parameter to retrieve the token from if present. Default: disabled. If forwardAuth is false, the query string parameter will be removed before forwarding to the backend.
redirectUnauthorized URL to redirect Unauthorized (401) claims to instead of returning a 401 status code. This is intended for interactive requests where the user should be redirected to login and then returned to the page that access was attempted to. Go template interpolation may be used to construct a return_to parameter for the redirection. See examples and template elements below.
redirectForbidden URL to redirect Unauthorized (403) claims to instead of returning a 403 status code. As above, this is intended for interactive requests and the same template interpolation applies. This is most useful to redirect a user to explain that they do not have access to the resource, even though they are authenticated. Such pages may, for example, offer explanations of how access may be obtained or may offer to allow the user to try using a different identity. If redirectUnauthorized is given but not redirectForbidden the URL for redirectUnauthorized will be used, rather than returning an HTTP status to an interactive session.
freshness Integeter value in seconds to consider a token as "fresh" based on its iat claim, if present. If a token is not within this freshness window, the plugin allows that a user may have recently had new permissions and thus new claims granted since last logging in, and will issue a 401 in place of a 403 (as well as redirecting interactive sessions as if Unauthorized). Once a user as logged in again, their token will be within the freshness window and a definitive 403 can be returned or not. Default 3600 = 1 hour. Set freshness = 0 to disable.
forwardToken Boolean indicating whether the token should be removed from where it is found before passing to backend. Default false. If multiple tokens are present in different locations (e.g. cookie and header), only the token used will be removed.
optional Validate tokens according to the normal rules but don't require that a token be present. If specific claim requirements are specified in require but with optional set to true and a token is not present, access will be permitted even though the requirements are obviously not met, which may not be what you want or expect. In this case, no headers will be set from claims (as there aren't any).

The following variables are available in Go template for interpolation:

Name Description
{{.URL}} Full request URL including scheme and any query string parameters.
{{.Scheme}} https or http
{{.Host}} Host name only, without scheme, including port if any
{{.Path}} Path and any query string parameters
Claim Matching

The following config snippet / JWT example pairs illustrate requirements and claims that satisfy them:

Simple
require:
  aud: "customer.example.com"
{
  "iss": "auth.example.com",
  "aud": "customer.example.com"
}
Dynamic Requirement

E.g. for requiring that a token's audience matches the domain being accessed

Will suceed when called on https://customer.example.com/example but fail on https://other.example.com/example Note that it is necessary to escape the Go template with to prevent traefik from attempting to interpret it.

require:
  aud: "{{`{{.Host}}`}}"
{
  "iss": "auth.example.com",
  "aud": "customer.example.com"
}
Wildcard Claim

Will suceed when called on https://customer.example.com/example

require:
  aud: "customer.example.com"
{
  "iss": "auth.example.com",
  "aud": "*.example.com"
}

Note that the wildcard is granted to the user in their claim, not asked for in the requirements.

Custom Nested Claims
require:
  authority:
    app1.example.com: ["admin", "superuser"]
{
  "iss": "auth.example.com",
  "authority": {
    "app1.example.com": ["user", "admin"],
    "app2.example.com": ["user"]
  }
}
Examples
Interactive webserver with redirection to login and error pages
http:
  middlewares:
    secure-interactive:
      plugin:
        jwt:
          issuers:
            - https://auth.example.com
          require:
            aud: test.example.com
          redirectUnauthorized: "https://example.com/login?return_to={{`{{.URL}}`}}"
          redirectForbidden: "https://example.com/unauthorized"
Specifying a fixed ECDSA public key
http:
  middlewares:
    secure-interactive:
      plugin:
        jwt:
          secret: |
            -----BEGIN PUBLIC KEY-----
            MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEmHp6e95bmF2NqgziUMZP/+x8r2dH
            m/UisMRtEhOY6vE92LCdFpyqtwYwPVryYexT0ITtQldD5O091QcuIOm6KQ==
            -----END PUBLIC KEY-----
          require:
            aud: test.example.com

Acknowledgements

Inspired by code from https://github.com/legege/jwt-validation-middleware, https://github.com/23deg/jwt-middleware and https://github.com/team-carepay/traefik-jwt-plugin

Documentation

Overview

This file contains code taken from github.com/team-carepay/traefik-jwt-plugin We would like to simply use github.com/go-jose/go-jose/v3 for the JWKS instead but traefik's yaegi interpreter messes up the unmarshalling.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func FetchJWKS

func FetchJWKS(url string) (map[string]interface{}, error)

func JWKThumbprint

func JWKThumbprint(jwk JSONWebKey) string

JWKThumbprint creates a JWK thumbprint out of pub as specified in https://tools.ietf.org/html/rfc7638.

func New

func New(_ context.Context, next http.Handler, config *Config, name string) (http.Handler, error)

New creates a new JWTPlugin.

func SetupSecret

func SetupSecret(secret string) (interface{}, error)

Types

type Config

type Config struct {
	ValidMethods         []string
	Issuers              []string
	Secret               string                 `json:"secret,omitempty"`
	Require              map[string]interface{} `json:"require,omitempty"`
	Optional             bool                   `json:"optional,omitempty"`
	RedirectUnauthorized string                 `json:"redirectUnauthorized,omitempty"`
	RedirectForbidden    string                 `json:"redirectForbidden,omitempty"`
	CookieName           string                 `json:"cookieName,omitempty"`
	HeaderName           string                 `json:"headerName,omitempty"`
	ParameterName        string                 `json:"parameterName,omitempty"`
	HeaderMap            map[string]string      `json:"headerMap,omitempty"`
	ForwardToken         bool                   `json:"forwardToken,omitempty"`
	Freshness            int64                  `json:"freshness,omitempty"`
}

Config is the configuration for the plugin.

func CreateConfig

func CreateConfig() *Config

CreateConfig creates the default plugin configuration.

type JSONWebKey

type JSONWebKey struct {
	Kid string   `json:"kid"`
	Kty string   `json:"kty"`
	Alg string   `json:"alg"`
	Use string   `json:"use"`
	X5c []string `json:"x5c"`
	X5t string   `json:"x5t"`
	N   string   `json:"n"`
	E   string   `json:"e"`
	K   string   `json:"k,omitempty"`
	X   string   `json:"x,omitempty"`
	Y   string   `json:"y,omitempty"`
	D   string   `json:"d,omitempty"`
	P   string   `json:"p,omitempty"`
	Q   string   `json:"q,omitempty"`
	Dp  string   `json:"dp,omitempty"`
	Dq  string   `json:"dq,omitempty"`
	Qi  string   `json:"qi,omitempty"`
	Crv string   `json:"crv,omitempty"`
}

JSONWebKey is a JSON web key returned by the JWKS request.

type JSONWebKeySet

type JSONWebKeySet struct {
	Keys []JSONWebKey `json:"keys"`
}

JSONWebKeySet represents a set of JSON web keys.

type JWTPlugin

type JWTPlugin struct {
	// contains filtered or unexported fields
}

JWTPlugin is a traefik middleware plugin that authorizes access based on JWT tokens.

func (*JWTPlugin) GetKey

func (plugin *JWTPlugin) GetKey(token *jwt.Token) (interface{}, error)

GetKey gets the key for the given key ID from the plugin's key cache. If the key isn't present and the iss is valid according to the plugin's configuration, all keys for the iss are fetched and the key is looked up again.

func (*JWTPlugin) IsValidIssuer

func (plugin *JWTPlugin) IsValidIssuer(issuer string) bool

IsValidIssuer returns true if the issuer is allowed by the Issers configuration.

func (*JWTPlugin) ServeHTTP

func (plugin *JWTPlugin) ServeHTTP(response http.ResponseWriter, request *http.Request)

ServeHTTP is the middleware entry point.

func (*JWTPlugin) Validate

func (plugin *JWTPlugin) Validate(request *http.Request, variables *TemplateVariables) (int, error)

Validate validates the request and returns the HTTP status code or an error if the request is not valid. It also sets any headers that should be forwarded to the backend.

func (*JWTPlugin) ValidateClaim

func (plugin *JWTPlugin) ValidateClaim(claim string, claims jwt.MapClaims, requirements []Requirement, variables *TemplateVariables) bool

ValidateClaim

type Requirement

type Requirement interface {
	Validate(value interface{}, variables *TemplateVariables) bool
}

Requirement is a requirement for a claim.

type TemplateRequirement

type TemplateRequirement struct {
	// contains filtered or unexported fields
}

TemplateRequirement is a dynamic requirement for a claim that uses a template that needs interpolating per request.

func (TemplateRequirement) Validate

func (requirement TemplateRequirement) Validate(value interface{}, variables *TemplateVariables) bool

Validate interpolates the requirement template with the given variables and then delegates to ValueRequirement.

type TemplateVariables

type TemplateVariables struct {
	URL    string
	Scheme string
	Host   string
	Path   string
}

TemplateVariables are the per-request variables passed to Go templates for interpolation, such as the require and redirect templates.

type ValueRequirement

type ValueRequirement struct {
	// contains filtered or unexported fields
}

ValueRequirement is a requirement for a claim that is a known value.

func (ValueRequirement) Validate

func (requirement ValueRequirement) Validate(value interface{}, variables *TemplateVariables) bool

Validate checks value against the requirement, calling ourself recursively for object and array values. variables is required in the interface and passed on recusrively by ultimately ignored bu ValueRequirement having been already interpolated by TemplateRequirement

func (ValueRequirement) ValidateNested

func (requirement ValueRequirement) ValidateNested(value interface{}) bool

ValidateNested checks value against the nested requirement

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL