jwt

package module
v3.7.0+incompatible Latest Latest
Warning

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

Go to latest
Published: Jan 14, 2019 License: MIT Imports: 16 Imported by: 0

README

JWT

Build Status

Authorization Middleware for Caddy

This middleware implements an authorization layer for Caddy based on JSON Web Tokens (JWT). You can learn more about using JWT in your application at jwt.io.

Basic Syntax

jwt [path]

By default every resource under path will be secured using JWT validation. To specify a list of resources that need to be secured, use multiple declarations:

jwt [path1]
jwt [path2]

Important You must set the secret used to construct your token in an environment variable named JWT_SECRET(HMAC) or JWT_PUBLIC_KEY(RSA or ECDSA). Otherwise, your tokens will silently fail validation. Caddy will start without this value set, but it must be present at the time of the request for the signature to be validated.

Advanced Syntax

You can optionally use claim information to further control access to your routes. In a jwt block you can specify rules to allow or deny access based on the value of a claim. If the claim is a json array of strings, the allow and deny directives will check if the array contains the specified string value. An allow or deny rule will be valid if any value in the array is a match.

jwt {
   path [path]
   redirect [location]
   allow [claim] [value]
   deny [claim] [value]
}

To authorize access based on a claim, use the allow syntax. To deny access, use the deny keyword. You can use multiple keywords to achieve complex access rules. If any allow access rule returns true, access will be allowed. If a deny rule is true, access will be denied. Deny rules will allow any other value for that claim.

For example, suppose you have a token with user: someone and role: member. If you have the following access block:

jwt {
   path /protected
   deny role member
   allow user someone
}

The middleware will deny everyone with role: member but will allow the specific user named someone. A different user with a role: admin or role: foo would be allowed because the deny rule will allow anyone that doesn't have role member.

If the optional redirect is set, the middleware will send a redirect to the supplied location (HTTP 303) instead of an access denied code, if the access is denied.

Ways of passing a token for validation

There are three ways to pass the token for validation: (1) in the Authorization header, (2) as a cookie, and (3) as a URL query parameter. The middleware will by default look in those places in the order listed and return 401 if it can't find any token.

Method Format
Authorization Header Authorization: Bearer <token>
Cookie "jwt_token": <token>
URL Query Parameter /protected?token=<token>

It is possible to customize what token sources should be used via the token_source rule. If at one or more token_source rules are specified, they will be used instead of the default in the given order. For example, to do the same validation as default, but with the different cookie and query param names, the user could use the following snippet:

jwt {
   ...
   token_source header
   token_source cookie my_cookie_name
   token_source query_param my_param_name
}

Constructing a valid token

JWTs consist of three parts: header, claims, and signature. To properly construct a JWT, it's recommended that you use a JWT library appropriate for your language. At a minimum, this authorization middleware expects the following fields to be present:

Header
{
"typ": "JWT",
"alg": "HS256|HS384|HS512|RS256|RS384|RS512|ES256|ES384|ES512"
}
Claims

If you want to limit the validity of your tokens to a certain time period, use the "exp" field to declare the expiry time of your token. This time should be a Unix timestamp in integer format.

{
"exp": 1460192076
}

Acting on claims in the token

You can of course add extra claims in the claim section. Once the token is validated, the claims you include will be passed as headers to a downstream resource. Since the token has been validated by Caddy, you can be assured that these headers represent valid claims from your token. For example, if you include the following claims in your token:

{
  "user": "test",
  "role": "admin",
  "logins": 10,
  "groups": ["user", "operator"],
  "data": {
    "payload": "something"
  }
}

The following headers will be added to the request that is proxied to your application:

Token-Claim-User: test
Token-Claim-Role: admin
Token-Claim-Logins: 10
Token-Claim-Groups: user,operator
Token-Claim-Data.payload: something

Token claims will always be converted to a string. If you expect your claim to be another type, remember to convert it back before you use it. Nested JSON objects will be flattened. In the example above, you can see that the nested payload field is flattened to data.payload.

All request headers with the prefix Token-Claim- are stripped from the request before being forwarded upstream, so users can't spoof them.

Claims with special characters that aren't allowed in HTTP headers will be URL escaped. For example, Auth0 requires that claims be namespaced with the full URL such as

{
  "http://example.com/user": "test"
}

The URL escaping will lead to some ugly headers like

Token-Claim-Http:%2F%2Fexample.com%2Fuser: test

If you only care about the last section of the path, you can use the strip_header directive to strip everything before the last portion of the path.

jwt {
  path /
  strip_header
}

When combined with the claims above, it will result in a header:

Token-Claim-User: test

Allowing Public Access to Certain Paths

In some cases, you may want to allow public access to a particular path without a valid token. For example, you may want to protect all your routes except access to the /login path. You can do that with the except directive.

jwt {
  path /
  except /login
}

Every path that begins with /login will be excepted from the JWT token requirement. All other paths will be protected. In the case that you set your path to the root as in the example above, you also might want to allow access to the so-called naked or root domain while protecting everything else. You can use the directive allowroot which will allow access to the naked domain. For example, if you have the following config block:

jwt {
  path /
  except /login
  allowroot
}

Requests to https://example.com/login and https://example.com/ will both be allowed without a valid token. Any other path will require a valid token.

Allowing Public Access Regardless of Token

In some cases, a page should be accessible whether a valid token is present or not. An example might be the Github home page or a public repository, which should be visible even to logged-out users. In those cases, you would want to parse any valid token that might be present and pass the claims through to the application, leaving it to the application to decide whether the user has access. You can use the directive passthrough for this:

jwt {
  path /
  passthrough
}

It should be noted that passthrough will always allow access on the path provided, regardless of whether a token is present or valid, and regardless of allow/deny directives. The application would be responsible for acting on the parsed claims.

Specifying Keys for Use in Validating Tokens

There are two ways to specify key material used in validating tokens. If you run Caddy in a container or via an init system like Systemd, you can directly specify your keys using the environment variables JWT_SECRET for HMAC or JWT_PUBLIC_KEY for RSA or ECDSA (PEM-encoded public key). You cannot use both at the same time because it would open up a known security hole in the JWT specification. When you run multiple sites, all would have to use the same keys to validate tokens.

When you run multiple sites from one Caddyfile, you can specify the location of a file that contains your PEM-encoded public key or your HMAC secret. Once again, you cannot use both for the same site because it would cause a security hole. However, you can use different methods on different sites because the configurations are independent.

For RSA or ECDSA tokens:

jwt {
  path /
  publickey /path/to/key.pem
} 

For HMAC:

jwt {
  path /
  secret /path/to/secret.txt
}

When you store your key material in a file, this middleware will cache the result and use the modification time on the file to determine if the secret has changed since the last request. This should allow you to rotate your keys or invalidate tokens by writing a new key to the file without worrying about possible file locking problems (although you should still check that your write succeeded before issuing tokens with your new key.)

If you have multiple public keys or secrets that should be considered valid, use multiple declarations to the keys or secrets in different files. Authorization will be allowed if any of the keys validate the token.

jwt {
  path /
  publickey /path/to/key1.pem
  publickey /path/to/key2.pem
}

Possible Return Status Codes

Code Reason
401 Unauthorized - no token, token failed validation, token is expired
403 Forbidden - Token is valid but denied because of an ALLOW or DENY rule
303 A 401 or 403 was returned and the redirect is enabled. This takes precedence over a 401 or 403 status.

Caveats

JWT validation depends only on validating the correct signature and that the token is unexpired. You can also set the nbf field to prevent validation before a certain timestamp. Other fields in the specification, such as aud, iss, sub, iat, and jti will not affect the validation step.

Documentation

Overview

Flatten makes flat, one-dimensional maps from arbitrarily nested ones.

Map keys turn into compound names, like `a.b.1.c` (dotted style) or `a[b][1][c]` (Rails style). It takes input as either JSON strings or Go structures. It (only) knows how to traverse JSON types: maps, slices and scalars.

Or Go maps directly.

t := map[string]interface{}{
	"a": "b",
	"c": map[string]interface{}{
		"d": "e",
		"f": "g",
	},
	"z": 1.4567,
}

flat, err := Flatten(nested, "", RailsStyle)

// output:
// map[string]interface{}{
//	"a":    "b",
//	"c[d]": "e",
//	"c[f]": "g",
//	"z":    1.4567,
// }

Index

Constants

View Source
const ENV_PUBLIC_KEY = "JWT_PUBLIC_KEY"
View Source
const ENV_SECRET = "JWT_SECRET"

Variables

View Source
var (
	// Default TokenSources to be applied in the given order if the
	// user did not explicitly configure them via the token_source option
	DefaultTokenSources = []TokenSource{
		&HeaderTokenSource{},
		&CookieTokenSource{
			CookieName: "jwt_token",
		},
		&QueryTokenSource{
			ParamName: "token",
		},
	}
)
View Source
var NotValidInputError = errors.New("Not a valid input: map or slice")

Nested input must be a map or slice

Functions

func AssertHmacToken

func AssertHmacToken(token *jwt.Token) error

func AssertPublicKeyAndTokenCombination

func AssertPublicKeyAndTokenCombination(publicKey interface{}, token *jwt.Token) error

func ExtractToken

func ExtractToken(tss []TokenSource, r *http.Request) (string, error)

ExtractToken will find a JWT token in the token sources specified. If tss is empty, the DefaultTokenSources are used.

func Flatten

func Flatten(nested map[string]interface{}, prefix string, style SeparatorStyle) (map[string]interface{}, error)

Flatten generates a flat map from a nested one. The original may include values of type map, slice and scalar, but not struct. Keys in the flat map will be a compound of descending map keys and slice iterations. The presentation of keys is set by style. A prefix is joined to each key.

func IsEcdsaPublicKey

func IsEcdsaPublicKey(key interface{}) bool

func IsEcdsaToken

func IsEcdsaToken(token *jwt.Token) bool

func IsHmacToken

func IsHmacToken(token *jwt.Token) bool

func IsRsaPublicKey

func IsRsaPublicKey(key interface{}) bool

func IsRsaToken

func IsRsaToken(token *jwt.Token) bool

func ParsePublicKey

func ParsePublicKey(pem []byte) (interface{}, error)

func ReadPublicKeyFile

func ReadPublicKeyFile(filepath string) (interface{}, error)

func Setup

func Setup(c *caddy.Controller) error

Setup is called by Caddy to parse the config block

func ValidateToken

func ValidateToken(uToken string, keyBackend KeyBackend) (*jwt.Token, error)

ValidateToken will return a parsed token if it passes validation, or an error if any part of the token fails validation. Possible errors include malformed tokens, unknown/unspecified signing algorithms, missing secret key, tokens that are not valid yet (i.e., 'nbf' field), tokens that are expired, and tokens that fail signature verification (forged)

Types

type AccessRule

type AccessRule struct {
	Authorize RuleType
	Claim     string
	Value     string
}

AccessRule represents a single ALLOW/DENY rule based on the value of a claim in a validated token

type Auth

type Auth struct {
	Rules []Rule
	Next  httpserver.Handler
	Realm string
}

Auth represents configuration information for the middleware

func (Auth) ServeHTTP

func (h Auth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error)

type CookieTokenSource

type CookieTokenSource struct {
	CookieName string
}

Extracts a token from a cookie named `CookieName`.

func (*CookieTokenSource) ExtractToken

func (cts *CookieTokenSource) ExtractToken(r *http.Request) string

type EncryptionType

type EncryptionType int

EncryptionType specifies the valid configuration for a path

const (
	// HS family of algorithms
	HMAC EncryptionType = iota + 1
	// RS and ES families of algorithms
	PKI
)

type HeaderTokenSource

type HeaderTokenSource struct{}

Extracts a token from the Authorization header in the form `Bearer <JWT Token>`

func (*HeaderTokenSource) ExtractToken

func (*HeaderTokenSource) ExtractToken(r *http.Request) string

type HmacKeyBackend

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

HmacKeyBacked is an HMAC-SHA key provider

func (*HmacKeyBackend) ProvideKey

func (instance *HmacKeyBackend) ProvideKey(token *jwt.Token) (interface{}, error)

ProvideKey will assert that the token signing algorithm and the configured key match

type KeyBackend

type KeyBackend interface {
	ProvideKey(token *jwt.Token) (interface{}, error)
}

KeyBackend provides a generic interface for providing key material for HS, RS, and ES algorithms

func NewDefaultKeyBackends

func NewDefaultKeyBackends() ([]KeyBackend, error)

NewDefaultKeyBackends will read from the environment and return key backends based on values from environment variables JWT_SECRET or JWT_PUBLIC_KEY. An error is returned if the keys are not able to be parsed or if an inconsistent configuration is found.

type LazyHmacKeyBackend

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

LazyHmacKeyBackend contains state to manage lazy key loading for HS family algorithms

func NewLazyHmacKeyBackend

func NewLazyHmacKeyBackend(value string) (*LazyHmacKeyBackend, error)

NewLazyHmacKeyBackend creates a new LazyHmacKeyBackend

func (*LazyHmacKeyBackend) ProvideKey

func (instance *LazyHmacKeyBackend) ProvideKey(token *jwt.Token) (interface{}, error)

ProvideKey will lazily load a secret key in a file, using a cached value if the key material has not changed. An error is returned if the token does not match the expected signing algorithm.

type LazyPublicKeyBackend

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

LazyPublicKeyBackend contains state to manage lazy key loading for RS and ES family algorithms

func NewLazyPublicKeyFileBackend

func NewLazyPublicKeyFileBackend(value string) (*LazyPublicKeyBackend, error)

NewLazyPublicKeyFileBackend returns a new LazyPublicKeyBackend

func (*LazyPublicKeyBackend) ProvideKey

func (instance *LazyPublicKeyBackend) ProvideKey(token *jwt.Token) (interface{}, error)

ProvideKey will lazily load a secret key in a file, using a cached value if the key material has not changed. An error is returned if the token does not match the expected signing algorithm.

type NoopKeyBackend

type NoopKeyBackend struct{}

NoopKeyBackend always returns an error when no key signing method is specified

func (*NoopKeyBackend) ProvideKey

func (instance *NoopKeyBackend) ProvideKey(token *jwt.Token) (interface{}, error)

ProvideKey always returns an error when no key signing method is specified

type PublicKeyBackend

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

PublicKeyBackend is an RSA or ECDSA key provider

func (*PublicKeyBackend) ProvideKey

func (instance *PublicKeyBackend) ProvideKey(token *jwt.Token) (interface{}, error)

ProvideKey will asssert that the token signing algorithm and the configured key match

type QueryTokenSource

type QueryTokenSource struct {
	ParamName string
}

Extracts a token from a URL query parameter of the form https://example.com?ParamName=<JWT token>

func (*QueryTokenSource) ExtractToken

func (qts *QueryTokenSource) ExtractToken(r *http.Request) string

type Rule

type Rule struct {
	Path          string
	ExceptedPaths []string
	AccessRules   []AccessRule
	Redirect      string
	AllowRoot     bool
	KeyBackends   []KeyBackend
	Passthrough   bool
	StripHeader   bool
	TokenSources  []TokenSource
}

Rule represents the configuration for a site

type RuleType

type RuleType int

RuleType distinguishes between ALLOW and DENY rules

const (
	// ALLOW represents a rule that should allow access based on claim value
	ALLOW RuleType = iota

	// DENY represents a rule that should deny access based on claim value
	DENY
)

type SeparatorStyle

type SeparatorStyle int

The presentation style of keys.

const (

	// Separate nested key components with dots, e.g. "a.b.1.c.d"
	DotStyle SeparatorStyle

	// Separate ala Rails, e.g. "a[b][c][1][d]"
	RailsStyle
)

type TokenSource

type TokenSource interface {
	// If the returned string is empty, the token was not found.
	// So far any implementation does not return errors.
	ExtractToken(r *http.Request) string
}

Jump to

Keyboard shortcuts

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