jwt

package module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Aug 25, 2026 License: MIT Imports: 26 Imported by: 0

README

JWT validation

jwt validates signed compact JWTs at the authentication boundary. It owns JWT/JWS parsing, signature and claim policy, static JWK sets, and a bounded remote JWKS cache. It does not extract HTTP credentials, discover OIDC providers, issue tokens, manage sessions, or make authorization decisions.

Quick start

Construct a Validator with one issuer, one audience, an explicit algorithm allowlist, a deterministic clock.Clock, and exactly one key source:

validator, err := jwt.New(jwt.Config{
	Issuer:     "https://issuer.example.com",
	Audience:   "orders",
	Algorithms: []jwa.SignatureAlgorithm{jwa.RS256()},
	KeySet:     keys,
	Clock:      clock,
})
if err != nil {
	return err
}

principal, err := validator.ValidateBearer(ctx, compactToken)

The compiling package example shows a complete signed-token setup.

Validation policy

Every validator requires iss, aud, sub, iat, and exp. The subject, issuer, and audience must be non-empty; issuer and audience must match the configured values. Subjects optionally restricts subjects to an exact allowlist. RequiredClaims adds deployment-specific required claims to the mandatory registered set. exp, nbf, and iat must be integral JSON numbers; fractional and exponent forms are rejected to avoid parser-dependent clock rounding. They are checked against one instant read from the configured clock per validation with the configured non-negative Skew.

Tokens must use compact JWS serialization with exactly three non-empty, unpadded base64url segments. The protected header must contain non-empty alg and kid string values. The algorithm must be in the configured allowlist and must match the selected JWK's declared algorithm and key type. none, deprecated algorithms, unknown algorithms, duplicate JSON members, invalid UTF-8, malformed JSON, duplicate key IDs, all critical headers, and JSON or nested serialization are rejected. Token-provided key references (jku, jwk, and x5*) are rejected. Unpaired UTF-16 escapes and excessively long JSON numbers are rejected before claim decoding.

Non-critical typ and cty values are metadata and do not select validation rules. Deployments with multiple token kinds must use mutually exclusive validator profiles, normally with distinct issuer, audience, key, algorithm, and required-claim policy.

MaxTokenBytes, MaxClaims, MaxClaimDepth, and MaxKeys bound hostile inputs. Zero values select conservative defaults. Negative values and values above the authentication package's shared claim limits are invalid configuration. MaxClaims must reserve capacity for all five mandatory claims and every configured RequiredClaims entry.

Algorithms and keys

The validator can allow the non-deprecated signature algorithms registered by the pinned JWX version in these key families:

  • HS256, HS384, and HS512 with oct keys;
  • RS256, RS384, and RS512 with RSA keys;
  • PS256, PS384, and PS512 with RSA keys;
  • ES256, ES384, and ES512 with EC keys;
  • Ed25519 with OKP keys.

HMAC keys must contain at least 32, 48, or 64 bytes for HS256, HS384, or HS512. RSA keys must be public and have a modulus from 2048 through 8192 bits. EC keys must be public and use the algorithm's exact curve. none, deprecated generic EdDSA, ES256K, and algorithms outside the listed families are rejected.

Only algorithms explicitly supplied in Config.Algorithms are accepted. JWKs must have unique non-empty kid values and an alg in that allowlist. If present, use must be sig, and key_ops must contain only verify. Keep HMAC secrets separate from asymmetric public-key material; the key-type checks are an additional defense against algorithm confusion, not a substitute for key separation.

Local and remote key providers

Use Config.KeySet for a local set or Config.Provider for a narrow dynamic provider. Supplying both or neither is invalid. Sets are copied and validated before use, so callers retain ownership and cannot mutate the validator's static trust state.

NewRemote accepts HTTPS by default, completes successful initialization with exactly one bounded fetch, and then owns a JWX cache for that exact URL. The URL must not contain user info or a fragment. All redirects and compressed responses are denied. Response bodies, aggregate headers, key count, concurrent operations, and initialization time are bounded. Cache-Control: max-age and Expires determine refresh time within the configured minimum and maximum intervals; absent or unusable cache headers fall back to the minimum. no-cache, no-store, and must-revalidate force the configured minimum interval. Conflicting or malformed max-age directives also use the minimum, and Age reduces a max-age lifetime before bounds are applied. Each provider applies independent bounded refresh jitter (10 percent by default) to avoid synchronized fleet load.

This is an application trust cache, not an HTTP response cache. The directives control the next revalidation attempt but do not authorize HTTP reuse; no-cache, no-store, and must-revalidate therefore schedule the minimum interval while the separately validated key trust state remains available under the documented fail-stale policy.

A successful refresh atomically replaces the cached set. A failed refresh returns ErrAuthenticationUnavailable and retains the last successful set. That fail-stale policy preserves validation for already-known keys during an issuer outage; it never accepts an unknown key. Applications that require fail-closed freshness must stop using or close the provider after their own freshness deadline.

Remote is safe for concurrent validation and refresh. Automatic and explicit refreshes use the same hardened client and never overlap remote work; overlapping explicit refreshes share one in-process result. Returned sets are deep copies. Canceling a refresh waiter stops that caller waiting. The remote request admitted by the cache continues under the provider lifecycle until it finishes or Close cancels the provider. Canceling the context passed to a successful NewRemote does not close the provider. Once Close begins, new work remains rejected. It cancels in-flight provider operations, waits for them to leave the provider, and shuts down cache-owned goroutines. A canceled close stops that caller waiting and reports the context error; if shutdown did not finish, a later Close may retry. The caller that creates a Remote owns it and must close it.

Results and errors

ValidateBearer returns an immutable authentication principal. Registered claims and configured scope/tenant claims populate typed principal fields; remaining private claims are defensively copied into Principal.Claims. Private JSON numbers retain their exact encoded value as json.Number. Authenticate additionally accepts only authentication.BearerCredential.

Failures use stable authentication categories:

  • malformed or oversized credentials: ErrCredentialsInvalid;
  • failed signature, claims, or trust policy: ErrCredentialsRejected;
  • provider, cancellation, or lifecycle failures: ErrAuthenticationUnavailable.

Standards-backed parse and claim failures retain the safe JWX sentinels such as jwt.ParseError(), jwt.TokenExpiredError(), and jwt.MissingRequiredClaimError() in the error chain. Key-source failures use the redacted ErrKeyProviderUnavailable sentinel. Raw provider, transport, endpoint-query, key, token, signature, and claim values are not retained.

Use errors.Is and errors.As; do not match strings. Public error text is the stable category and does not include token, signature, key, claim, endpoint, or remote error text. Only safe standard categories and redacted provider causes remain available through the standard error chain.

Adoption and migration

Keep transport extraction in authentication/authhttp or another adapter and pass only the bearer credential to this package. Keep OIDC discovery and ID token policy in authentication/oidc. Migrate permissive JWT parsers by first inventorying issuers, audiences, algorithms, key IDs, clock skew, and required claims; then configure the narrowest observed policy and reject legacy tokens that do not satisfy it.

The module follows stable v1 compatibility. Public API compatibility is checked against api/baseline.txt; breaking changes require a new major release and explicit migration guidance. The package follows the pinned JWX algorithm registry; review that dependency and this algorithm list when upgrading it.

The RFC-derived acceptance, remote-boundary, and error matrix is recorded in docs/hardening.md. The stable interpretation and defensive-policy register is recorded in docs/specification-decisions.md. The complete exported surface and defaults are recorded in docs/api.md.

Security notes and tradeoffs

  • Treat tokens, JWKs, provider URLs, and wrapped causes as secrets in logs and telemetry.
  • Prefer asymmetric verification for independently operated issuers. HMAC requires every verifier to possess signing-capable secret material.
  • Keep leeway as small as operational clock accuracy permits.
  • A larger token, claim, depth, key, body, or refresh bound increases resource exposure.
  • Remote caching improves availability and rotation behavior but introduces a deliberate fail-stale window after the last successful refresh.

FAQ

Does this package issue tokens or perform authorization? No. It only validates authentication evidence and constructs a principal.

Does an unknown kid trigger an unbounded fetch? No. Validation uses the current bounded provider snapshot. Refresh is controlled by the cache schedule or an explicit bounded Refresh call.

Can HTTP be enabled for JWKS? WithInsecureHTTP exists for isolated tests and trusted development networks. Production endpoints should use HTTPS.

Can callers mutate returned key sets? A provider owns its returned set; the validator copies and validates it for each validation attempt. Remote retains ownership of its cached set.

What happens during a JWKS outage? Refresh reports unavailable, while the last successful key set remains usable according to the documented fail-stale policy.

Ecosystem

Use the Golib documentation portal to choose companion packages, supported stacks, recipes, and operations guidance.

Documentation

Overview

Package jwt provides strict JWT and JWK authentication using lestrrat-go/jwx.

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrKeyProviderUnavailable = errors.New("jwt: key provider unavailable")

ErrKeyProviderUnavailable is the redacted cause for key-provider failures.

Functions

This section is empty.

Types

type Clock deprecated

type Clock interface {
	clockpkg.Clock
}

Clock supplies validation time and permits deterministic tests.

Deprecated: depend on clock.Clock in new code. This named compatibility contract remains available throughout v1.

type Config

type Config struct {
	Issuer         string
	Audience       string
	Algorithms     []jwa.SignatureAlgorithm
	KeySet         jwk.Set
	Provider       KeyProvider
	Clock          Clock
	Skew           time.Duration
	MaxTokenBytes  int
	MaxClaims      int
	MaxClaimDepth  int
	MaxKeys        int
	Subjects       []string
	RequiredClaims []string
	ScopeClaim     string
	TenantClaim    string
}

Config defines a strict JWT trust boundary.

type KeyProvider

type KeyProvider interface {
	KeySet(context.Context) (jwk.Set, error)
}

KeyProvider returns a current read-only JWK set for one validation attempt.

type KeyProviderFunc

type KeyProviderFunc func(context.Context) (jwk.Set, error)

KeyProviderFunc adapts a function to KeyProvider.

func (KeyProviderFunc) KeySet

func (f KeyProviderFunc) KeySet(ctx context.Context) (jwk.Set, error)

KeySet calls f.

type Remote

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

Remote owns a bounded JWK cache and all of its background goroutines.

func NewRemote

func NewRemote(ctx context.Context, rawURL string, options ...RemoteOption) (*Remote, error)

NewRemote registers and initially fetches one exact JWK URL. The caller owns the returned provider and must call Close.

func (*Remote) Close

func (r *Remote) Close(ctx context.Context) error

Close cancels and joins all cache-owned background work.

func (*Remote) KeySet

func (r *Remote) KeySet(ctx context.Context) (jwk.Set, error)

KeySet returns the current cached JWK set without transferring ownership.

func (*Remote) Refresh

func (r *Remote) Refresh(ctx context.Context) error

Refresh synchronously refreshes the cached JWK set. A failed refresh keeps the previously cached set available.

type RemoteOption

type RemoteOption func(*remoteConfig)

RemoteOption configures a network-backed JWK provider.

func WithHTTPClient

func WithHTTPClient(client *http.Client) RemoteOption

WithHTTPClient supplies an HTTP client. JWX timeout and redirect hardening is layered onto a shallow copy of the client.

func WithInitializationTimeout

func WithInitializationTimeout(timeout time.Duration) RemoteOption

WithInitializationTimeout bounds the initial fetch and cache registration.

func WithInsecureHTTP

func WithInsecureHTTP() RemoteOption

WithInsecureHTTP permits an HTTP JWK URL. It is intended only for isolated tests and trusted development networks.

func WithMaxJWKBodyBytes

func WithMaxJWKBodyBytes(maximum int64) RemoteOption

WithMaxJWKBodyBytes bounds a JWK HTTP response body.

func WithMaxJWKHeaderBytes

func WithMaxJWKHeaderBytes(maximum int64) RemoteOption

WithMaxJWKHeaderBytes bounds the aggregate response-header bytes accepted from the JWK endpoint.

func WithMaxJWKKeys

func WithMaxJWKKeys(maximum int) RemoteOption

WithMaxJWKKeys bounds the number of keys accepted in a remote JWK set.

func WithRefreshBounds

func WithRefreshBounds(minimum, maximum time.Duration) RemoteOption

WithRefreshBounds configures minimum and maximum automatic refresh intervals.

func WithRefreshJitter

func WithRefreshJitter(maximumFraction float64) RemoteOption

WithRefreshJitter configures the maximum fractional deviation applied to provider refresh intervals. Zero disables jitter; values must be below one.

type Validator

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

Validator authenticates signed compact JWT bearer credentials.

func New

func New(configuration Config) (*Validator, error)

New validates and defensively copies a static JWK trust configuration.

Example
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/faustbrian/go-authentication/authtest"
	authjwt "github.com/faustbrian/go-authentication/jwt"
	"github.com/lestrrat-go/jwx/v3/jwa"
	"github.com/lestrrat-go/jwx/v3/jwk"

	upstreamjwt "github.com/lestrrat-go/jwx/v3/jwt"
)

func main() {
	key, err := jwk.Import([]byte("01234567890123456789012345678901"))
	if err != nil {
		return
	}
	_ = key.Set(jwk.KeyIDKey, "key")
	_ = key.Set(jwk.AlgorithmKey, jwa.HS256())
	keys := jwk.NewSet()
	_ = keys.AddKey(key)
	now := time.Unix(1_800_000_000, 0).UTC()
	validator, err := authjwt.New(authjwt.Config{
		Issuer: "https://issuer.example.test", Audience: "service",
		Algorithms: []jwa.SignatureAlgorithm{jwa.HS256()}, KeySet: keys,
		Clock: authtest.NewClock(now),
	})
	if err != nil {
		return
	}
	token := upstreamjwt.New()
	_ = token.Set("sub", "service")
	_ = token.Set("iss", "https://issuer.example.test")
	_ = token.Set("aud", "service")
	_ = token.Set("iat", now)
	_ = token.Set("exp", now.Add(time.Hour))
	signed, _ := upstreamjwt.Sign(token, upstreamjwt.WithKey(jwa.HS256(), key))
	principal, err := validator.ValidateBearer(context.Background(), string(signed))
	fmt.Println(err, principal.Subject())
}
Output:
<nil> service

func (*Validator) Authenticate

func (v *Validator) Authenticate(ctx context.Context, credential authentication.Credential) (authentication.Result, error)

Authenticate validates a bearer credential and returns a JWT principal.

func (*Validator) ValidateBearer

func (v *Validator) ValidateBearer(ctx context.Context, token string) (authentication.Principal, error)

ValidateBearer verifies a bounded compact JWT and constructs an immutable principal.

Jump to

Keyboard shortcuts

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