jwt

package module
v1.12.0 Latest Latest
Warning

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

Go to latest
Published: Jun 19, 2022 License: CC0-1.0 Imports: 25 Imported by: 99

README

API Documentation Build Status

About

… a JSON Web Token (JWT) library for the Go programming language.

  • Feature complete
  • Full test coverage
  • Dependency free
  • Key management

The API enforces secure use by design. Unsigned tokens are rejected. No support for encrypted tokens either—use wire encryption instead.

This is free and unencumbered software released into the public domain.

Introduction

Tokens encapsulate signed statements called claims. A claim is a named JSON value. Applications using JWTs should define which specific claims they use and when they are required or optional.

var claims jwt.Claims
claims.Subject = "alice"
claims.Issued  = jwt.NewNumericTime(time.Now().Round(time.Second))
claims.Set     = map[string]interface{}{"email_verified": false}
// issue a JWT
token, err := claims.EdDSASign(JWTPrivateKey)

Tokens consists of printable ASCII characters, e.g., eyJhbGciOiJFUzI1NiJ9.eyJzdWIiOiJha3JpZWdlciIsInByZWZpeCI6IkRyLiJ9.RTOboYsLW7zXFJyXtIypOmXfuRGVT_FpDUTs2TOuK73qZKm56JcESfsl_etnBsl7W80TXE5l5qecrMizh3XYmw. Secured resources can use such tokens to determine the respective permissions. Note how the verification process is self-contained with just a public key.

// verify a JWT
claims, err := jwt.EdDSACheck(token, JWTPublicKey)
if err != nil {
	log.Print("credentials rejected: ", err)
	return
}
err = claims.AcceptTemporal(time.Now(), time.Second)
if err != nil {
	log.Print("credential constraints violated: ", err)
	return
}

// ready for use
log.Print("hello ", claims.Subject)
if verified, _ := claims.Set["email_verified"].(bool); !verified {
	log.Print("e-mail confirmation pending")
}

Commonly, agents receive a JWT uppon authentication/login. Then, that token is included with requests to the secured resources, as a proof of authority. Token access is “eyes only” in such scenario. Include and enforce more context detail with claims to further reduce risk. E.g., a session identifier or a fingerprint of the client's TLS key can prevent usage of any hijacked tokens.

High-Level API

Server-side security can be applied with a standard http.Handler setup. The following example denies requests to MyAPI when the JWT is not valid, or when the JWT is missing either the subject, formatted name or roles claim.

// define trusted credentials
var keys jwt.KeyRegister
n, err := keys.LoadPEM(text, nil)
if err != nil {
	log.Fatal(err)
}
log.Print("setup with ", n, " JWT keys")

http.Handle("/api/v1", &jwt.Handler{
	Target: MyAPI, // protected HTTP handler
	Keys:   &keys,

	// map two claims to HTTP headers
	HeaderPrefix: "X-Verified-",
	HeaderBinding: map[string]string{
		"sub": "X-Verified-User", // registered [standard] claim
		"fn":  "X-Verified-Name", // private [custom] claim
	},

	// map another claim with custom logic
	Func: func(w http.ResponseWriter, req *http.Request, claims *jwt.Claims) (pass bool) {
		log.Printf("got a valid JWT %q for %q", claims.ID, claims.Audiences)

		// map role enumeration
		s, ok := claims.String("roles")
		if !ok {
			http.Error(w, "jwt: want roles claim as a string", http.StatusForbidden)
			return false
		}
		req.Header["X-Verified-Roles"] = strings.Fields(s)

		return true
	},
})

When all applicable JWT claims are mapped to HTTP request headers, then the service logic can stay free of verification code, plus easier unit testing.

// Greeting is a standard HTTP handler fuction.
func Greeting(w http.ResponseWriter, req *http.Request) {
	fmt.Fprintf(w, "Hello %s!\n", req.Header.Get("X-Verified-Name"))
	fmt.Fprintf(w, "You are authorized as %s.\n", req.Header.Get("X-Verified-User"))
}

The validated Claims object may also be exposed through the request context.

Performance

The following results were measured with Go 1.19-beta1 on an Apple M1.

name                      time/op
ECDSA/sign-ES256-8        19.8µs ± 0%
ECDSA/sign-ES384-8         196µs ± 0%
ECDSA/check-ES256-8       58.7µs ± 0%
ECDSA/check-ES384-8        632µs ± 0%
EdDSA/sign-EdDSA-8        24.9µs ± 0%
EdDSA/check-EdDSA-8       54.0µs ± 0%
HMAC/sign-HS256-8          664ns ± 1%
HMAC/sign-HS256-reuse-8    461ns ± 0%
HMAC/sign-HS384-8         2.60µs ± 0%
HMAC/sign-HS384-reuse-8   1.40µs ± 0%
HMAC/sign-HS512-8         2.61µs ± 0%
HMAC/sign-HS512-reuse-8   1.41µs ± 0%
HMAC/check-HS256-8        1.81µs ± 1%
HMAC/check-HS256-reuse-8  1.61µs ± 1%
HMAC/check-HS384-8        3.75µs ± 0%
HMAC/check-HS384-reuse-8  2.54µs ± 1%
HMAC/check-HS512-8        3.76µs ± 0%
HMAC/check-HS512-reuse-8  2.56µs ± 0%
RSA/sign-1024-bit-8        226µs ± 1%
RSA/sign-2048-bit-8       1.10ms ± 2%
RSA/sign-4096-bit-8       6.03ms ± 3%
RSA/check-1024-bit-8      13.4µs ± 0%
RSA/check-2048-bit-8      30.7µs ± 1%
RSA/check-4096-bit-8      90.3µs ± 1%

EdDSA [Ed25519] produces small signatures and it performs well.

Standard Compliance

  • RFC 2617: “HTTP Authentication”
  • RFC 6750: “The OAuth 2.0 Authorization Framework: Bearer Token Usage”
  • RFC 7468: “Textual Encodings of PKIX, PKCS, and CMS Structures”
  • RFC 7515: “JSON Web Signature (JWS)”
  • RFC 7517: “JSON Web Key (JWK)”
  • RFC 7518: “JSON Web Algorithms (JWA)”
  • RFC 7519: “JSON Web Token (JWT)”
  • RFC 8037: “CFRG Elliptic Curve Diffie-Hellman (ECDH) and Signatures in JSON Object Signing and Encryption (JOSE)”

JWT.io

Documentation

Overview

Package jwt implements “JSON Web Token (JWT)” RFC 7519. Signatures only; no unsecured nor encrypted tokens.

Example (Extend)

Demo use of a non-standard algorithm and custom JOSE heading.

package main

import (
	"crypto"
	_ "crypto/md5" // link into binary
	"encoding/json"
	"fmt"

	"github.com/pascaldekloe/jwt"
)

// JWTHeaders defines custom JOSE heading for token production.
var JWTHeaders = json.RawMessage(`{"lan": "XL9", "tcode": 102}`)

func init() {
	// static algorithm registration
	jwt.HMACAlgs["HM5"] = crypto.MD5
}

// Demo use of a non-standard algorithm and custom JOSE heading.
func main() {
	// issue a JWT
	c := jwt.Claims{KeyID: "№1"}
	token, err := c.HMACSign("HM5", []byte("guest"), JWTHeaders)
	if err != nil {
		fmt.Println("sign error:", err)
		return
	}
	fmt.Println("token:", string(token))

	// verify a JWT
	claims, err := jwt.HMACCheck(token, []byte("guest"))
	if err != nil {
		fmt.Println("check error:", err)
		return
	}
	fmt.Println("header:", string(claims.RawHeader))
}
Output:

token: eyJhbGciOiJITTUiLCJraWQiOiLihJYxIiwibGFuIjoiWEw5IiwidGNvZGUiOjEwMn0.e30.8i8eLO5fHTv1ucdUWtBRMA
header: {"alg":"HM5","kid":"№1","lan":"XL9","tcode":102}

Index

Examples

Constants

View Source
const (
	EdDSA = "EdDSA" // EdDSA signature algorithms
	ES256 = "ES256" // ECDSA using P-256 and SHA-256
	ES384 = "ES384" // ECDSA using P-384 and SHA-384
	// Deprecated: Use ES384 instead.
	// “Go just implemented all specified curves from FIPS 186. If I were to
	// make that choice today we'd have only P-256 and P-384.
	//
	// In 12 years, they will either all be broken by quantum computers, or
	// P-384 and P-521 will both stand.”
	// — Filippo Valsorda
	ES512 = "ES512" // ECDSA using P-521 and SHA-512
	HS256 = "HS256" // HMAC using SHA-256
	HS384 = "HS384" // HMAC using SHA-384
	HS512 = "HS512" // HMAC using SHA-512
	PS256 = "PS256" // RSASSA-PSS using SHA-256 and MGF1 with SHA-256
	PS384 = "PS384" // RSASSA-PSS using SHA-384 and MGF1 with SHA-384
	PS512 = "PS512" // RSASSA-PSS using SHA-512 and MGF1 with SHA-512
	RS256 = "RS256" // RSASSA-PKCS1-v1_5 using SHA-256
	RS384 = "RS384" // RSASSA-PKCS1-v1_5 using SHA-384
	RS512 = "RS512" // RSASSA-PKCS1-v1_5 using SHA-512
)

Algorithm Identification Tokens

View Source
const ErrUnsecured = AlgError("none")

ErrUnsecured signals a token without a signature, as described in RFC 7519, section 6.

View Source
const MIMEType = "application/jwt"

MIMEType is the IANA registered media type.

View Source
const OAuthURN = "urn:ietf:params:oauth:token-type:jwt"

OAuthURN is the IANA registered OAuth URI.

Variables

Algorithm support is configured with hash registrations. Any modifications should be made before first use to prevent data races in the Check and Sign functions, i.e., customise from either main or init.

View Source
var ErrNoHeader = errors.New("jwt: no HTTP authorization header")

ErrNoHeader signals an HTTP request without authorization.

View Source
var ErrSigMiss = errors.New("jwt: signature mismatch")

ErrSigMiss means the signature check failed.

View Source
var EvalCrit = func(token []byte, crit []string, header json.RawMessage) error {
	return fmt.Errorf("jwt: unsupported critical extension in JOSE header: %q", crit)
}

EvalCrit is invoked by the Check functions for each token with one or more JOSE extensions. The crit slice has the JSON field names (for header) which “MUST be understood and processed” according to RFC 7515, subsection 4.1.11. “If any of the listed extension Header Parameters are not understood and supported by the recipient, then the JWS is invalid.” The respective Check function returns any error from EvalCrit as is.

Functions

func BearerToken added in v1.11.0

func BearerToken(h http.Header) (token string, err error)

Bearer extracts the token from an HTTP header.

Types

type AlgError added in v1.4.0

type AlgError string

AlgError signals that the specified algorithm is not in use.

func (AlgError) Error added in v1.4.0

func (e AlgError) Error() string

Error honors the error interface.

type Claims

type Claims struct {
	// Registered field values take precedence over Set.
	Registered

	// Set maps claims by name, for usecases beyond the Registered fields.
	// The Sign methods copy each non-zero Registered value into Set when
	// the map is not nil. The Check methods map claims in Set if the name
	// doesn't match any of the Registered, or if the data type won't fit.
	// Entries are treated conform the encoding/json package.
	//
	//	bool, for JSON booleans
	//	float64, for JSON numbers
	//	string, for JSON strings
	//	[]interface{}, for JSON arrays
	//	map[string]interface{}, for JSON objects
	//	nil for JSON null
	//
	Set map[string]interface{}

	// Raw encoding as is within the token. This field is read-only.
	Raw json.RawMessage
	// RawHeader encoding as is within the token. This field is read-only.
	RawHeader json.RawMessage

	// “The "kid" (key ID) Header Parameter is a hint indicating which key
	// was used to secure the JWS. This parameter allows originators to
	// explicitly signal a change of key to recipients. The structure of the
	// "kid" value is unspecified. Its value MUST be a case-sensitive
	// string. Use of this Header Parameter is OPTIONAL.”
	// — “JSON Web Signature (JWS)” RFC 7515, subsection 4.1.4
	KeyID string
}

Claims are the (signed) statements of a JWT.

Example

Note how the security model is flawed without any purpose claims. The bare minimum should include time constraints like Expires.

package main

import (
	"crypto/rsa"
	"crypto/x509"
	"encoding/pem"
	"fmt"
	"time"

	"github.com/pascaldekloe/jwt"
)

func main() {
	var c jwt.Claims
	c.Issuer = "malory"
	c.Subject = "sterling"
	c.Audiences = []string{"armory"}

	// Approval is a custom claim element.
	type Approval struct {
		Name  string `json:"name"`
		Count int    `json:"count"`
	}
	c.Set = map[string]interface{}{
		"approved": []Approval{{"RPG-7", 1}},
	}

	// issue a JWT
	token, err := c.RSASign(jwt.RS256, RSAKey)
	if err != nil {
		fmt.Println("token creation failed on", err)
		return
	}

	// verify a JWT
	claims, err := jwt.RSACheck(token, &RSAKey.PublicKey)
	if err != nil {
		fmt.Println("credentials rejected:", err)
		return
	}
	err = claims.AcceptTemporal(time.Now(), time.Second)
	if err != nil {
		fmt.Println("credential constraints violated:", err)
		return
	}
	if !claims.AcceptAudience("armory") {
		fmt.Println("credentials not for armory:", claims.Audiences)
		return
	}
	fmt.Println(string(claims.Raw))
}

// RSAPEM is a PKCS #1 form of RSAJWK.
const RSAPEM = `-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAofgWCuLjybRlzo0tZWJjNiuSfb4p4fAkd/wWJcyQoTbji9k0
l8W26mPddxHmfHQp+Vaw+4qPCJrcS2mJPMEzP1Pt0Bm4d4QlL+yRT+SFd2lZS+pC
gNMsD1W/YpRPEwOWvG6b32690r2jZ47soMZo9wGzjb/7OMg0LOL+bSf63kpaSHSX
ndS5z5rexMdbBYUsLA9e+KXBdQOS+UTo7WTBEMa2R2CapHg665xsmtdVMTBQY4uD
Zlxvb3qCo5ZwKh9kG4LT6/I5IhlJH7aGhyxXFvUK+DWNmoudF8NAco9/h9iaGNj8
q2ethFkMLs91kzk2PAcDTW9gb54h4FRWyuXpoQIDAQABAoIBABKucaRpzQorw35S
bEUAVx8dYXUdZOlJcHtiWQ+dC6V8ljxAHj/PLyzTveyI5QO/xkObCyjIL303l2cf
UhPu2MFaJdjVzqACXuOrLot/eSFvxjvqVidTtAZExqFRJ9mylUVAoLvhowVWmC1O
n95fZCXxTUtxNEG1Xcc7m0rtzJKs45J+N/V9DP1edYH6USyPSWGp6wuA+KgHRnKK
Vf9GRx80JQY7nVNkL17eHoTWEwga+lwi0FEoW9Y7lDtWXYmKBWhUE+U8PGxlJf8f
40493HDw1WRQ/aSLoS4QTp3rn7gYgeHEvfJdkkf0UMhlknlo53M09EFPdadQ4TlU
bjqKc50CgYEA4BzEEOtIpmVdVEZNCqS7baC4crd0pqnRH/5IB3jw3bcxGn6QLvnE
tfdUdiYrqBdss1l58BQ3KhooKeQTa9AB0Hw/Py5PJdTJNPY8cQn7ouZ2KKDcmnPG
BY5t7yLc1QlQ5xHdwW1VhvKn+nXqhJTBgIPgtldC+KDV5z+y2XDwGUcCgYEAuQPE
fgmVtjL0Uyyx88GZFF1fOunH3+7cepKmtH4pxhtCoHqpWmT8YAmZxaewHgHAjLYs
p1ZSe7zFYHj7C6ul7TjeLQeZD/YwD66t62wDmpe/HlB+TnBA+njbglfIsRLtXlnD
zQkv5dTltRJ11BKBBypeeF6689rjcJIDEz9RWdcCgYAHAp9XcCSrn8wVkMVkKdb7
DOX4IKjzdahm+ctDAJN4O/y7OW5FKebvUjdAIt2GuoTZ71iTG+7F0F+lP88jtjP4
U4qe7VHoewl4MKOfXZKTe+YCS1XbNvfgwJ3Ltyl1OH9hWvu2yza7q+d5PCsDzqtm
27kxuvULVeya+TEdAB1ijQKBgQCH/3r6YrVH/uCWGy6bzV1nGNOdjKc9tmkfOJmN
54dxdixdpozCQ6U4OxZrsj3FcOhHBsqAHvX2uuYjagqvo3cOj1TRqNocX40omfCC
Mx3bD1yPPf/6TI2XECva/ggqEY2mYzmIiA5LVVmc5nrybr+lssFKneeyxN2Wq93S
0iJMdQKBgCGHewxzoa1r8ZMD0LETNrToK423K377UCYqXfg5XMclbrjPbEC3YI1Z
NqMtuhdBJqUnBi6tjKMF+34Xf0CUN8ncuXGO2CAYvO8PdyCixHX52ybaDjy1FtCE
6yUXjoKNXKvUm7MWGsAYH6f4IegOetN5NvmUMFStCSkh7ixZLkN1
-----END RSA PRIVATE KEY-----`

var RSAKey = mustParseRSAKey(RSAPEM)

func mustParseRSAKey(s string) *rsa.PrivateKey {
	block, _ := pem.Decode([]byte(s))
	if block == nil {
		panic("no PEM block")
	}

	key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
	if err != nil {
		panic(err)
	}
	return key
}
Output:

{"approved":[{"name":"RPG-7","count":1}],"aud":["armory"],"iss":"malory","sub":"sterling"}
Example (ByName)

Typed Claim Lookups

package main

import (
	"fmt"
	"time"

	"github.com/pascaldekloe/jwt"
)

func main() {
	now := time.Unix(1537622794, 0)
	c := jwt.Claims{
		Registered: jwt.Registered{
			Issuer:    "a",
			Subject:   "b",
			Audiences: []string{"c"},
			Expires:   jwt.NewNumericTime(now.Add(time.Minute)),
			NotBefore: jwt.NewNumericTime(now.Add(-time.Second)),
			Issued:    jwt.NewNumericTime(now),
			ID:        "d",
		},
		Set: map[string]interface{}{
			"ext": "e",
			"nde": true,
		},
	}

	for _, name := range []string{"iss", "sub", "aud", "exp", "nbf", "iat", "jti", "ext", "nde"} {
		if s, ok := c.String(name); ok {
			fmt.Printf("%q: %q\n", name, s)
		}
		if n, ok := c.Number(name); ok {
			fmt.Printf("%q: %0.f\n", name, n)
		}
		if b, ok := c.Set[name].(bool); ok {
			fmt.Printf("%q: %t\n", name, b)
		}
	}
}
Output:

"iss": "a"
"sub": "b"
"aud": "c"
"exp": 1537622854
"nbf": 1537622793
"iat": 1537622794
"jti": "d"
"ext": "e"
"nde": true

func ECDSACheck

func ECDSACheck(token []byte, key *ecdsa.PublicKey) (*Claims, error)

ECDSACheck parses a JWT if, and only if, the signature checks out. The return is an AlgError when the algorithm is not in ECDSAAlgs. Use Valid to complete the verification.

func ECDSACheckHeader

func ECDSACheckHeader(r *http.Request, key *ecdsa.PublicKey) (*Claims, error)

ECDSACheckHeader applies ECDSACheck on an HTTP request. Specifically it looks for a bearer token in the Authorization header.

func EdDSACheck added in v1.6.0

func EdDSACheck(token []byte, key ed25519.PublicKey) (*Claims, error)

EdDSACheck parses a JWT if, and only if, the signature checks out. Use Valid to complete the verification.

func EdDSACheckHeader added in v1.6.0

func EdDSACheckHeader(r *http.Request, key ed25519.PublicKey) (*Claims, error)

EdDSACheckHeader applies EdDSACheck on an HTTP request. Specifically it looks for a bearer token in the Authorization header.

func HMACCheck

func HMACCheck(token, secret []byte) (*Claims, error)

HMACCheck parses a JWT if, and only if, the signature checks out. The return is an AlgError when the algorithm is not in HMACAlgs. Use Valid to complete the verification.

func HMACCheckHeader

func HMACCheckHeader(r *http.Request, secret []byte) (*Claims, error)

HMACCheckHeader applies HMACCheck on an HTTP request. Specifically it looks for a bearer token in the Authorization header.

func ParseWithoutCheck added in v1.6.0

func ParseWithoutCheck(token []byte) (*Claims, error)

ParseWithoutCheck skips the signature validation.

func RSACheck

func RSACheck(token []byte, key *rsa.PublicKey) (*Claims, error)

RSACheck parses a JWT if, and only if, the signature checks out. The return is an AlgError when the algorithm is not in RSAAlgs. Use Valid to complete the verification.

func RSACheckHeader

func RSACheckHeader(r *http.Request, key *rsa.PublicKey) (*Claims, error)

RSACheckHeader applies RSACheck on an HTTP request. Specifically it looks for a bearer token in the Authorization header.

func (*Claims) ECDSASign

func (c *Claims) ECDSASign(alg string, key *ecdsa.PrivateKey, extraHeaders ...json.RawMessage) (token []byte, err error)

ECDSASign updates the Raw fields and returns a new JWT. The return is an AlgError when alg is not in ECDSAAlgs. The caller must use the correct key for the respective algorithm (P-256 for ES256, P-384 for ES384 and P-521 for ES512) or risk malformed token production.

The JOSE header (content) can be extended with extraHeaders, in the form of JSON objects. Redundant and/or duplicate keys are applied as provided.

func (*Claims) ECDSASignHeader

func (c *Claims) ECDSASignHeader(r *http.Request, alg string, key *ecdsa.PrivateKey) error

ECDSASignHeader applies ECDSASign on an HTTP request. Specifically it sets a bearer token in the Authorization header.

func (*Claims) EdDSASign added in v1.6.0

func (c *Claims) EdDSASign(key ed25519.PrivateKey, extraHeaders ...json.RawMessage) (token []byte, err error)

EdDSASign updates the Raw fields and returns a new JWT.

The JOSE header (content) can be extended with extraHeaders, in the form of JSON objects. Redundant and/or duplicate keys are applied as provided.

func (*Claims) EdDSASignHeader added in v1.6.0

func (c *Claims) EdDSASignHeader(r *http.Request, key ed25519.PrivateKey) error

EdDSASignHeader applies ECDSASign on an HTTP request. Specifically it sets a bearer token in the Authorization header.

func (*Claims) FormatWithoutSign added in v1.7.0

func (c *Claims) FormatWithoutSign(alg string, extraHeaders ...json.RawMessage) (tokenWithoutSignature []byte, err error)

FormatWithoutSign updates the Raw fields and returns a new JWT, with only the first two parts.

tokenWithoutSignature :≡ header-base64 '.' payload-base64
token                 :≡ tokenWithoutSignature '.' signature-base64

The JOSE header (content) can be extended with extraHeaders, in the form of JSON objects. Redundant and/or duplicate keys are applied as provided.

func (*Claims) HMACSign

func (c *Claims) HMACSign(alg string, secret []byte, extraHeaders ...json.RawMessage) (token []byte, err error)

HMACSign updates the Raw fields and returns a new JWT. The return is an AlgError when alg is not in HMACAlgs.

The JOSE header (content) can be extended with extraHeaders, in the form of JSON objects. Redundant and/or duplicate keys are applied as provided.

func (*Claims) HMACSignHeader

func (c *Claims) HMACSignHeader(r *http.Request, alg string, secret []byte) error

HMACSignHeader applies HMACSign on an HTTP request. Specifically it sets a bearer token in the Authorization header.

func (*Claims) Number

func (c *Claims) Number(name string) (value float64, ok bool)

Number returns the claim when present and if the representation is a JSON number. Note that null is not a number.

func (*Claims) RSASign

func (c *Claims) RSASign(alg string, key *rsa.PrivateKey, extraHeaders ...json.RawMessage) (token []byte, err error)

RSASign updates the Raw fields and returns a new JWT. The return is an AlgError when alg is not in RSAAlgs.

The JOSE header (content) can be extended with extraHeaders, in the form of JSON objects. Redundant and/or duplicate keys are applied as provided.

func (*Claims) RSASignHeader

func (c *Claims) RSASignHeader(r *http.Request, alg string, key *rsa.PrivateKey) error

RSASignHeader applies RSASign on an HTTP request. Specifically it sets a bearer token in the Authorization header.

func (*Claims) String

func (c *Claims) String(name string) (value string, ok bool)

String returns the claim when present and if the representation is a JSON string. Note that null is not a string.

type HMAC added in v1.10.0

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

HMAC is a reusable instance, optimized for high usage scenarios.

Multiple goroutines may invoke methods on an HMAC simultaneously.

func NewHMAC added in v1.10.0

func NewHMAC(alg string, secret []byte) (*HMAC, error)

NewHMAC returns a new reusable instance.

func (*HMAC) Check added in v1.10.0

func (h *HMAC) Check(token []byte) (*Claims, error)

Check parses a JWT if, and only if, the signature checks out. The return is an AlgError when the algorithm does not match. Use Valid to complete the verification.

func (*HMAC) CheckHeader added in v1.10.0

func (h *HMAC) CheckHeader(r *http.Request) (*Claims, error)

CheckHeader applies Check on an HTTP request. Specifically it looks for a bearer token in the Authorization header.

func (*HMAC) Sign added in v1.10.0

func (h *HMAC) Sign(c *Claims, extraHeaders ...json.RawMessage) (token []byte, err error)

Sign updates the Raw fields on c and returns a new JWT.

The JOSE header (content) can be extended with extraHeaders, in the form of JSON objects. Redundant and/or duplicate keys are applied as provided.

func (*HMAC) SignHeader added in v1.10.0

func (h *HMAC) SignHeader(c *Claims, r *http.Request) error

SignHeader applies Sign on an HTTP request. Specifically it sets a bearer token in the Authorization header.

type Handler

type Handler struct {
	// Target is the secured service.
	Target http.Handler

	// Keys defines the trusted credentials.
	Keys *KeyRegister

	// HeaderBinding maps JWT claim names to HTTP header names.
	// All requests passed to Target have these headers set. In
	// case of failure the request is rejected with status code
	// 401 (Unauthorized) and a description.
	HeaderBinding map[string]string

	// HeaderPrefix is an optional constraint for JWT claim binding.
	// Any client headers that match the prefix are removed from the
	// request.
	HeaderPrefix string

	// ContextKey places the validated Claims in the context of
	// each respective request passed to Target when set. See
	// http.Request.Context and context.Context.Value.
	ContextKey interface{}

	// TemporalLeeway controls the tolerance with time constraints.
	TemporalLeeway time.Duration

	// When not nil, then Func is called after the JWT validation
	// succeeds and before any header bindings. Target is skipped
	// [request drop] when the return is false.
	// This feature may be used to further customise requests or
	// as a filter or as an extended http.HandlerFunc.
	Func func(http.ResponseWriter, *http.Request, *Claims) (pass bool)

	// Error sends a custom response. Nil defaults to http.Error.
	// The appropriate WWW-Authenticate value is already present.
	Error func(w http.ResponseWriter, error string, statusCode int)
}

Handler protects an http.Handler with security enforcements. Requests are only passed to Target if the JWT checks out.

Example
package main

import (
	"crypto/ed25519"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"os"

	"github.com/pascaldekloe/jwt"
)

func main() {
	// standard HTTP handler
	http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
		fmt.Fprintf(w, "Hello %s!\n", req.Header.Get("X-Verified-Name"))
		fmt.Fprintf(w, "You are authorized as %s.\n", req.Header.Get("X-Verified-User"))
	})

	// secure service configuration
	srv := httptest.NewTLSServer(&jwt.Handler{
		Target:       http.DefaultServeMux,
		Keys:         &jwt.KeyRegister{EdDSAs: []ed25519.PublicKey{EdPublicKey}},
		HeaderPrefix: "X-Verified-",
		HeaderBinding: map[string]string{
			"sub": "X-Verified-User", // registered [standard] claim name
			"fn":  "X-Verified-Name", // private [custom] claim name
		},
	})
	defer srv.Close()

	// call service
	req, _ := http.NewRequest("GET", srv.URL, nil)
	req.Header.Set("Authorization", "Bearer eyJhbGciOiJFZERTQSJ9.eyJmbiI6IkxhbmEgQW50aG9ueSBLYW5lIiwic3ViIjoibGFrYW5lIn0.B0DpTbticlRJN8y867gmylujJdfHRnnrFF_nTPkpYbVt9-1Ne1-YawzQxzOQXyZa7HwoU-Um8jOI_Fh8xubjAg")
	resp, _ := srv.Client().Do(req)
	fmt.Println("HTTP", resp.Status)
	io.Copy(os.Stdout, resp.Body)
}

// EdPublicKey is an example key from RFC 8037, appendix A.1.
var EdPublicKey = ed25519.PublicKey{
	0xd7, 0x5a, 0x98, 0x01, 0x82, 0xb1, 0x0a, 0xb7,
	0xd5, 0x4b, 0xfe, 0xd3, 0xc9, 0x64, 0x07, 0x3a,
	0x0e, 0xe1, 0x72, 0xf3, 0xda, 0xa6, 0x23, 0x25,
	0xaf, 0x02, 0x1a, 0x68, 0xf7, 0x07, 0x51, 0x1a,
}
Output:

HTTP 200 OK
Hello Lana Anthony Kane!
You are authorized as lakane.
Example (Context)

Claims From Request Context

package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"github.com/pascaldekloe/jwt"
)

func main() {
	const claimsKey = "verified-jwt"

	// secure service configuration
	h := &jwt.Handler{
		Target: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
			claims := req.Context().Value(claimsKey).(*jwt.Claims)
			if n, ok := claims.Number("deadline"); !ok {
				fmt.Fprintln(w, "no deadline")
			} else {
				fmt.Fprintln(w, "deadline at", (*jwt.NumericTime)(&n))
			}
		}),
		Keys:       &jwt.KeyRegister{Secrets: [][]byte{[]byte("killarcherdie")}},
		ContextKey: claimsKey,
	}

	// call service
	req := httptest.NewRequest("GET", "/status", nil)
	req.Header.Set("Authorization", "Bearer eyJhbGciOiJIUzM4NCJ9.eyJkZWFkbGluZSI6NjcxNTAwNzk5fQ.HS3mmHVfgP9EMpV4LLzagc6BB1P9J9Yh5TRA9DQHS4GeEejqMaBX0N4LAsMPgW0G")
	resp := httptest.NewRecorder()
	h.ServeHTTP(resp, req)
	fmt.Println("HTTP", resp.Code)
	fmt.Println(resp.Body)
}
Output:

HTTP 200
deadline at 1991-04-12T23:59:59Z
Example (Error)
package main

import (
	"crypto/ecdsa"
	"crypto/x509"
	"encoding/pem"
	"fmt"
	"net/http"
	"net/http/httptest"
	"time"

	"github.com/pascaldekloe/jwt"
)

func main() {
	// secure service configuration
	h := &jwt.Handler{
		Target: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			fmt.Fprintln(w, "My plan is to crowdsource a plan!")
		}),
		Keys: &jwt.KeyRegister{ECDSAs: []*ecdsa.PublicKey{&ECKey.PublicKey}},
		// customise with JSON messages
		Error: func(w http.ResponseWriter, error string, statusCode int) {
			w.Header().Set("Content-Type", "application/json;charset=UTF-8")
			w.WriteHeader(statusCode)
			fmt.Fprintf(w, `{"msg": %q}`, error)
		},
	}

	// call service with expired token
	req := httptest.NewRequest("GET", "/had-something-for-this", nil)
	var c jwt.Claims
	c.Expires = jwt.NewNumericTime(time.Now().Add(-time.Second))
	if err := c.ECDSASignHeader(req, jwt.ES512, ECKey); err != nil {
		fmt.Println("sign error:", err)
	}
	resp := httptest.NewRecorder()
	h.ServeHTTP(resp, req)
	fmt.Println("HTTP", resp.Code)
	fmt.Println(resp.Header().Get("WWW-Authenticate"))
	fmt.Println(resp.Body)
}

// ECPEM is a PKCS #8 form of ECJWK.
const ECPEM = `-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgjpsQnnGQmL+YBIff
H1136cspYG6+0iY7X1fCE9+E9LKhRANCAAR/zc4ncPbEXUGDy+5v20t7WAczNXvp
7xO6z248e9FURcfxRM0bvZt+hyzf7bnuufSzaV1uqQskrYpGIyiFiOWt
-----END PRIVATE KEY-----`

var ECKey = mustParseECKey(ECPEM)

func mustParseECKey(s string) *ecdsa.PrivateKey {
	block, _ := pem.Decode([]byte(s))
	if block == nil {
		panic("no PEM block")
	}

	key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
	if err != nil {
		panic(err)
	}
	return key.(*ecdsa.PrivateKey)
}
Output:

HTTP 401
Bearer error="invalid_token", error_description="jwt: expiration time [\"exp\"] passed"
{"msg": "jwt: expiration time [\"exp\"] passed"}
Example (Filter)

Func As A Request Filter

package main

import (
	"crypto/rsa"
	"crypto/x509"
	"encoding/pem"
	"fmt"
	"net/http"
	"net/http/httptest"

	"github.com/pascaldekloe/jwt"
)

func main() {
	// secure service configuration
	h := &jwt.Handler{
		Target: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			fmt.Fprintln(w, "Elaborate voicemail hoax!")
		}),
		Keys: &jwt.KeyRegister{RSAs: []*rsa.PublicKey{&RSAKey.PublicKey}},
		Func: func(w http.ResponseWriter, req *http.Request, claims *jwt.Claims) (pass bool) {
			if claims.Subject != "marcher" {
				http.Error(w, "Ring, ring!", http.StatusServiceUnavailable)
				return false
			}

			return true
		},
	}

	// call service
	req := httptest.NewRequest("GET", "/urgent", nil)
	req.Header.Set("Authorization", "Bearer eyJhbGciOiJQUzI1NiJ9.e30.KSfpjI7WFxGjI0t7NEqPFpcOkEQfR9YiK0nRxDqA7Eoz3X2Af4MhDTgHy4tTXwNBCpW0K-fjMfRG0E34nnsFWsUqFLuMq-geftUUf9aA7E2jrfcZUgi5-FlvOCk8P-iAbqfX0rTIyEBQ21huv75NdYnlfg_2RNd8YqhtxyqTPEjlb0_oLigGEYM6T0eySjNv8V-W2w97HBABHjEaP9aNqj2q_ZB5qERJ-qKP--JYGNx-rTaydFnDAIyWgbRIG2X9IaCRWKe-R8Qz3t76OkZIm7lXiDuYk7aMmfhtSrDL80bpTWGqyQ9AOxAKOTVNTRoTr3Z5cGxrg6B6p3fs4thvFw")
	resp := httptest.NewRecorder()
	h.ServeHTTP(resp, req)
	fmt.Println("HTTP", resp.Code)
	fmt.Println(resp.Body)
}

// RSAPEM is a PKCS #1 form of RSAJWK.
const RSAPEM = `-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAofgWCuLjybRlzo0tZWJjNiuSfb4p4fAkd/wWJcyQoTbji9k0
l8W26mPddxHmfHQp+Vaw+4qPCJrcS2mJPMEzP1Pt0Bm4d4QlL+yRT+SFd2lZS+pC
gNMsD1W/YpRPEwOWvG6b32690r2jZ47soMZo9wGzjb/7OMg0LOL+bSf63kpaSHSX
ndS5z5rexMdbBYUsLA9e+KXBdQOS+UTo7WTBEMa2R2CapHg665xsmtdVMTBQY4uD
Zlxvb3qCo5ZwKh9kG4LT6/I5IhlJH7aGhyxXFvUK+DWNmoudF8NAco9/h9iaGNj8
q2ethFkMLs91kzk2PAcDTW9gb54h4FRWyuXpoQIDAQABAoIBABKucaRpzQorw35S
bEUAVx8dYXUdZOlJcHtiWQ+dC6V8ljxAHj/PLyzTveyI5QO/xkObCyjIL303l2cf
UhPu2MFaJdjVzqACXuOrLot/eSFvxjvqVidTtAZExqFRJ9mylUVAoLvhowVWmC1O
n95fZCXxTUtxNEG1Xcc7m0rtzJKs45J+N/V9DP1edYH6USyPSWGp6wuA+KgHRnKK
Vf9GRx80JQY7nVNkL17eHoTWEwga+lwi0FEoW9Y7lDtWXYmKBWhUE+U8PGxlJf8f
40493HDw1WRQ/aSLoS4QTp3rn7gYgeHEvfJdkkf0UMhlknlo53M09EFPdadQ4TlU
bjqKc50CgYEA4BzEEOtIpmVdVEZNCqS7baC4crd0pqnRH/5IB3jw3bcxGn6QLvnE
tfdUdiYrqBdss1l58BQ3KhooKeQTa9AB0Hw/Py5PJdTJNPY8cQn7ouZ2KKDcmnPG
BY5t7yLc1QlQ5xHdwW1VhvKn+nXqhJTBgIPgtldC+KDV5z+y2XDwGUcCgYEAuQPE
fgmVtjL0Uyyx88GZFF1fOunH3+7cepKmtH4pxhtCoHqpWmT8YAmZxaewHgHAjLYs
p1ZSe7zFYHj7C6ul7TjeLQeZD/YwD66t62wDmpe/HlB+TnBA+njbglfIsRLtXlnD
zQkv5dTltRJ11BKBBypeeF6689rjcJIDEz9RWdcCgYAHAp9XcCSrn8wVkMVkKdb7
DOX4IKjzdahm+ctDAJN4O/y7OW5FKebvUjdAIt2GuoTZ71iTG+7F0F+lP88jtjP4
U4qe7VHoewl4MKOfXZKTe+YCS1XbNvfgwJ3Ltyl1OH9hWvu2yza7q+d5PCsDzqtm
27kxuvULVeya+TEdAB1ijQKBgQCH/3r6YrVH/uCWGy6bzV1nGNOdjKc9tmkfOJmN
54dxdixdpozCQ6U4OxZrsj3FcOhHBsqAHvX2uuYjagqvo3cOj1TRqNocX40omfCC
Mx3bD1yPPf/6TI2XECva/ggqEY2mYzmIiA5LVVmc5nrybr+lssFKneeyxN2Wq93S
0iJMdQKBgCGHewxzoa1r8ZMD0LETNrToK423K377UCYqXfg5XMclbrjPbEC3YI1Z
NqMtuhdBJqUnBi6tjKMF+34Xf0CUN8ncuXGO2CAYvO8PdyCixHX52ybaDjy1FtCE
6yUXjoKNXKvUm7MWGsAYH6f4IegOetN5NvmUMFStCSkh7ixZLkN1
-----END RSA PRIVATE KEY-----`

var RSAKey = mustParseRSAKey(RSAPEM)

func mustParseRSAKey(s string) *rsa.PrivateKey {
	block, _ := pem.Decode([]byte(s))
	if block == nil {
		panic("no PEM block")
	}

	key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
	if err != nil {
		panic(err)
	}
	return key
}
Output:

HTTP 503
Ring, ring!

func (*Handler) ServeHTTP

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP honors the http.Handler interface.

type KeyRegister

type KeyRegister struct {
	ECDSAs  []*ecdsa.PublicKey  // ECDSA credentials
	EdDSAs  []ed25519.PublicKey // EdDSA credentials
	RSAs    []*rsa.PublicKey    // RSA credentials
	HMACs   []*HMAC             // HMAC credentials
	Secrets [][]byte            // HMAC credentials

	// Optional key identification. See Claims.KeyID for details.
	// Non-empty strings match the respective key or secret by index.
	ECDSAIDs  []string // ECDSAs key ID mapping
	EdDSAIDs  []string // EdDSA key ID mapping
	RSAIDs    []string // RSAs key ID mapping
	HMACIDs   []string // HMACs key ID mapping
	SecretIDs []string // Secrets key ID mapping
}

KeyRegister is a collection of recognized credentials.

func (*KeyRegister) Check

func (keys *KeyRegister) Check(token []byte) (*Claims, error)

Check parses a JWT if, and only if, the signature checks out. Use Claims.Valid to complete the verification.

func (*KeyRegister) CheckHeader

func (keys *KeyRegister) CheckHeader(r *http.Request) (*Claims, error)

CheckHeader applies KeyRegister.Check on an HTTP request. Specifically it looks for a bearer token in the Authorization header.

func (*KeyRegister) LoadJWK added in v1.6.0

func (keys *KeyRegister) LoadJWK(data []byte) (keysAdded int, err error)

LoadJWK adds keys from the JSON data to the register, including the key ID, a.k.a "kid", when present. If the object has a "keys" attribute, then data is read as a JWKS (JSON Web Key Set). Otherwise, data is read as a single JWK.

Example

JWKS With Key IDs

package main

import (
	"fmt"

	"github.com/pascaldekloe/jwt"
)

func main() {
	const json = `{
  "keys": [
    {"kty": "OKP", "crv":"Ed25519", "kid": "kazak",
      "d":"nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A",
      "x":"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"},
    {"kty":"oct", "k":"a29mdGE", "kid": "good old"}
  ]
}`

	var keys jwt.KeyRegister
	_, err := keys.LoadJWK([]byte(json))
	if err != nil {
		fmt.Println("load error:", err)
	}
	fmt.Printf("got %d EdDSA %q", len(keys.EdDSAs), keys.EdDSAIDs)
	fmt.Printf(" + %d secret %q", len(keys.Secrets), keys.SecretIDs)
}
Output:

got 1 EdDSA ["kazak"] + 1 secret ["good old"]

func (*KeyRegister) LoadPEM

func (keys *KeyRegister) LoadPEM(text, password []byte) (keysAdded int, err error)

LoadPEM scans text for PEM-encoded keys. Each occurrence found is then added to the register. Extraction works with certificates, public keys and private keys. PEM encryption is enforced with a non-empty password to ensure security when ordered.

Example (Encrypted)

PEM With Password Protection

package main

import (
	"fmt"

	"github.com/pascaldekloe/jwt"
)

func main() {
	const pem = `Keep it private! ✨

-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-128-CBC,65789712555A3E9FECD1D5E235B97B0C

o0Dz8S6QjGVq59yQdlakuKkoO0jKDN0PDu2L05ZLXwBQSGdIbRXtAOBRCNEME0V1
IF9pM6uRU7tqFoVneNTHD3XySJG8AHrTPSKC3Xw31pjEolMfoNDBAu1bYR6XxM2X
oDu2UNVB9vd/3b4bwTH9q5ISWdCVhS/ky0lC9lHXman/F/7MsemiVVCQ4XTIi9CR
nitMxJuXvkNBMtsyv+inmFMegKU6dj1DU93B9JpsFRRvy3TCfj9kRjhKWEpyindo
yaZMH3EGOA3ALW5kWyr+XegyYznQbVdDlo/ikO9BAywBOx+DdRG4xYxRdxYt8/HH
qXwPAGQe2veMlR7Iq3GjwHLebyuVc+iHbC7feRmNBpAT1RR7J+RIGlDPOBMUpuDT
A8HbNzPkoXPGh9vMsREXtR5aPCaZISdcm8DTlNiZCPaX5VHL4SRJ5XjI2rnahaOE
rhCFy0mxqQaKnEI9kCWWFmhX/MqzzfiW3yg0qFIAVLDQZZMFJr3jMHIvkxPk09rP
nQIjMRBalFXmSiksx8UEhAzyriqiXwwgEI0gJVHcs3EIQGD5jNqvIYTX67/rqSF2
OXoYuq0MHrAJgEfDncXvZFFMuAS/5KMvzSXfWr5/L0ncCU9UykjdPrFvetG/7IXQ
BT1TX4pOeW15a6fg6KwSZ5KPrt3o8qtRfW4Ov49hPD2EhnCTMbkCRBbW8F13+9YF
xzvC4Vm1r/Oa4TTUbf5tVto7ua/lZvwnu5DIWn2zy5ZUPrtn22r1ymVui7Iuhl0b
SRcADdHh3NgrjDjalhLDB95ho5omG39l7qBKBTlBAYJhDuAk9rIk1FCfCB8upztt
-----END RSA PRIVATE KEY-----`

	var keys jwt.KeyRegister
	n, err := keys.LoadPEM([]byte(pem), []byte("dangerzone"))
	if err != nil {
		fmt.Println("load error:", err)
	}
	fmt.Println(n, "keys added")
}
Output:

1 keys added

func (*KeyRegister) PEM added in v1.6.0

func (keys *KeyRegister) PEM() ([]byte, error)

PEM exports the (public) keys as PEM-encoded PKIX. Elements from the Secret field, if any, are not included.

type NumericTime

type NumericTime float64

NumericTime implements NumericDate: “A JSON numeric value representing the number of seconds from 1970-01-01T00:00:00Z UTC until the specified UTC date/time, ignoring leap seconds.”

func NewNumericTime

func NewNumericTime(t time.Time) *NumericTime

NewNumericTime returns the the corresponding representation with nil for the zero value. Do t.Round(time.Second) for slightly smaller token production and compatibility. See the bugs section for details.

func (*NumericTime) String

func (n *NumericTime) String() string

String returns the ISO representation or the empty string for nil.

func (*NumericTime) Time

func (n *NumericTime) Time() time.Time

Time returns the Go mapping with the zero value for nil.

type Registered

type Registered struct {
	// Issuer identifies the principal that issued the JWT.
	Issuer string `json:"iss,omitempty"`

	// Subject identifies the principal that is the subject of the JWT.
	Subject string `json:"sub,omitempty"`

	// Audiences identifies the recipients that the JWT is intended for.
	Audiences []string `json:"aud,omitempty"`

	// Expires identifies the expiration time on or after which the JWT
	// must not be accepted for processing.
	Expires *NumericTime `json:"exp,omitempty"`

	// NotBefore identifies the time before which the JWT must not be
	// accepted for processing.
	NotBefore *NumericTime `json:"nbf,omitempty"`

	// Issued identifies the time at which the JWT was issued.
	Issued *NumericTime `json:"iat,omitempty"`

	// ID provides a unique identifier for the JWT.
	ID string `json:"jti,omitempty"`
}

Registered “JSON Web Token Claims” has a subset of the IANA registration. See <https://www.iana.org/assignments/jwt/claims.csv> for the full listing.

Each field is optional—there are no required claims. The string values are case sensitive.

func (*Registered) AcceptAudience added in v1.5.0

func (r *Registered) AcceptAudience(stringOrURI string) bool

AcceptAudience verifies the applicability of an audience identified as stringOrURI. Any stringOrURI is accepted on absence of the aud(ience) claim.

func (*Registered) AcceptTemporal added in v1.12.0

func (r *Registered) AcceptTemporal(t time.Time, leeway time.Duration) error

AcceptTemporal verifies Issued, NotBefore and Expires each against t when the respective claim is present, i.e., when the NumericTime pointer is not nil.

func (*Registered) Valid added in v1.8.1

func (r *Registered) Valid(t time.Time) bool

Valid returns whether the claims set may be accepted for processing at the given moment in time. If the time is zero, then Valid returns whether there are no time constraints ("nbf" & "exp").

Notes

Bugs

  • Some broken JWT implementations fail to parse tokens with fractions in Registered.Expires, .NotBefore or .Issued. Round to seconds—like NewNumericDate(time.Now().Round(time.Second))—for compatibility.

Jump to

Keyboard shortcuts

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