jwt

package
v0.17.0 Latest Latest
Warning

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

Go to latest
Published: Jun 14, 2026 License: AGPL-3.0 Imports: 10 Imported by: 0

Documentation

Overview

Package jwt provides RS256 JWT signing, verification, and JWKS publishing. It defines the pluggable Signer / KeyProvider interfaces that the identity service uses for access tokens, plus access-token claim types and a verifier that consumes any KeyProvider.

Concrete backends live in subpackages:

  • pkg/jwt/file — file-backed signer (default). Reads a JSON keys file at startup, reloads on SIGHUP. Suitable for any deployment that does not require an external KMS.
  • pkg/jwt/kmsaws — AWS KMS-backed signer. Delegates the signing operation to AWS KMS; private key material never leaves KMS.

Adding a new backend (GCP KMS, HashiCorp Vault, hardware HSM, …) is a matter of implementing Signer. The verifier, the JWKS HTTP handler, and every caller in this repo speak only to the interface.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AssertJWKSIncludesActiveKIDs added in v0.8.0

func AssertJWKSIncludesActiveKIDs(s Signer, now time.Time) error

AssertJWKSIncludesActiveKIDs is a startup sanity check: every currently-active kid the signer reports must appear in the JWKS document the verifier publishes. Drift here means a third-party service that fetched JWKS once cannot validate the next token we mint.

Returns an error when (a) JWKS rendering fails, or (b) any active kid is missing from the rendered JWKS. Pass the rendered JWKS into a startup assertion (cmd/identity/main.go panics on error). Inactive (expired-but-still-publishable) keys are NOT required to appear.

func JWKS added in v0.8.0

func JWKS(kp KeyProvider) ([]byte, error)

JWKS renders the KeyProvider's public keys as a JWKS document (RFC 7517) suitable for serving at /.well-known/jwks.json. Every key the provider exposes is included so verifiers can validate tokens minted before a rotation completed.

Types

type Claims

type Claims struct {
	Sub    string `json:"sub"`
	Email  string `json:"email"`
	Name   string `json:"name"`
	Role   string `json:"role"`
	Tenant string `json:"tenant"`
	// Project is the control-plane project the token was minted for. It
	// scopes the token to one project so a token minted under project A is
	// rejected on a request resolved to project B (cross-project reuse).
	// Omitted from tokens minted before the project model (empty).
	Project   string   `json:"project,omitempty"`
	AvatarURL string   `json:"avatar_url"`
	SID       string   `json:"sid,omitempty"`
	Audience  []string `json:"aud,omitempty"`
	IssuedAt  int64    `json:"iat"`
	ExpiresAt int64    `json:"exp"`
}

Claims holds the fields embedded in an access token.

SID is the session id added when the service is running in `GATEWAY_REVOCATION_MODE=session`. It is the empty string in mode=ttl deployments, in which case the verification middleware skips the session lookup entirely (the hot path keeps its zero cost). The claim is JSON-named `sid` so it lines up with OAuth / OIDC tooling that already understands the `sid` convention.

func VerifyAccessToken

func VerifyAccessToken(tokenStr string, kp KeyProvider, expectedTenant, expectedAudience string, requireAudience bool) (*Claims, error)

VerifyAccessToken verifies an RS256 access token against the supplied KeyProvider and returns its claims. The token must carry a "kid" header that matches a key the provider publishes. Tokens without "kid" or with an unknown "kid" are rejected.

If expectedTenant is non-empty, the token's "tenant" claim must match it exactly; otherwise the token is rejected. Passing an empty expectedTenant disables the cross-tenant check.

Audience handling:

  • If expectedAudience is empty, the "aud" claim is not inspected.
  • If expectedAudience is non-empty and the token's "aud" claim is present, it must contain expectedAudience (a token MAY carry multiple audiences — the check passes when any of them matches).
  • If expectedAudience is non-empty and the token has no "aud" claim, the token is rejected only when requireAudience is true. This gives callers a one-deploy migration window: ship the verifier with requireAudience=false, wait for all minted tokens to carry "aud", then flip requireAudience=true.
  • A token whose "aud" claim is present but does not contain expectedAudience is ALWAYS rejected, regardless of requireAudience.

Tokens with a missing or zero "exp" claim are explicitly rejected: the underlying lestrrat-go jwt library treats an absent exp as "no expiration", which would otherwise produce unbounded-lifetime tokens.

func (Claims) ClaimsMap added in v0.8.0

func (c Claims) ClaimsMap(now time.Time, expiry time.Duration) map[string]any

ClaimsMap converts the access-token claims plus standard iat/exp into the generic claim map Signer.SignClaims consumes. Used internally by concrete signers; exposed so future backends in other repositories can reuse the canonical claim layout.

type KeyProvider added in v0.8.0

type KeyProvider interface {
	// Keys returns every public key the provider currently advertises,
	// including keys past their ExpiresAt (so in-flight tokens still
	// verify). The slice MUST NOT be nil; it MAY be empty during
	// rotation only if the provider has no usable keys (which the
	// caller treats as a fatal condition at startup).
	//
	// Order is implementation-defined but stable for a given snapshot.
	Keys() []PublicKey

	// Get returns the public key for the supplied kid, or false when
	// no such key is published. Includes expired-but-not-yet-removed
	// keys, so the verifier can still validate tokens minted before
	// rotation completed.
	Get(kid string) (*rsa.PublicKey, bool)
}

KeyProvider exposes the public-key view of a signer's key store. It is the surface used by the verifier and by the JWKS HTTP handler.

type PublicKey added in v0.8.0

type PublicKey struct {
	// KID is the JWS "kid" header value stamped on tokens this key signs.
	KID string

	// Key is the RSA public key. Always non-nil for keys returned from
	// [KeyProvider.Keys].
	Key *rsa.PublicKey

	// NotBefore is the earliest moment this key is allowed to sign new
	// tokens. Zero means "no lower bound".
	NotBefore time.Time

	// ExpiresAt is the moment after which the key must no longer sign
	// new tokens. Zero means "never expires". A key past its ExpiresAt
	// still participates in verification (so tokens minted before
	// expiry remain valid until their own [Claims.ExpiresAt]) until the
	// key is removed from the provider entirely.
	ExpiresAt time.Time
}

PublicKey describes one signing key's public half plus its rotation metadata. Returned by KeyProvider so the JWKS endpoint can publish every key whose tokens may still be in flight.

func (PublicKey) IsActive added in v0.8.0

func (k PublicKey) IsActive(now time.Time) bool

IsActive reports whether the key may sign new tokens at the supplied time: NotBefore has elapsed (or is zero) and ExpiresAt has not yet arrived.

func (PublicKey) IsExpired added in v0.8.0

func (k PublicKey) IsExpired(now time.Time) bool

IsExpired reports whether the key's ExpiresAt has passed at the supplied time. Keys with a zero ExpiresAt are never expired.

type Signer added in v0.8.0

type Signer interface {
	KeyProvider

	// ActiveKID returns the kid the signer stamps on new tokens. It is
	// the kid of the currently-active key. Empty result indicates a
	// misconfigured signer (no usable active key); callers must treat
	// this as a fatal condition at startup.
	ActiveKID() string

	// SignAccessToken builds a standard access-token JWT from claims,
	// stamps iat/exp, signs with the active key, and returns the
	// compact-serialized JWS string.
	SignAccessToken(ctx context.Context, claims Claims, expiry time.Duration) (string, error)

	// SignClaims is the generic primitive: serialize the supplied claim
	// map as a JWT, sign with the active key, return the compact JWS.
	// Use this for non-access-token JWTs (e.g. OAuth state tokens).
	// Claim names follow the standard JWT/JWS conventions ("iat",
	// "exp", custom names).
	SignClaims(ctx context.Context, claims map[string]any) (string, error)
}

Signer issues RS256-signed JWTs using the active key in whatever key store the deployer wired up. Implementations MUST also expose the KeyProvider surface so that signing and verification stay in sync without per-backend branches in the HTTP handler.

Directories

Path Synopsis
Package file implements the default file-backed jwt.Signer for the identity service.
Package file implements the default file-backed jwt.Signer for the identity service.
Package jwttest provides an in-process jwt.Signer for tests.
Package jwttest provides an in-process jwt.Signer for tests.
Package kmsaws implements a jwt.Signer that delegates the signature operation to AWS KMS.
Package kmsaws implements a jwt.Signer that delegates the signature operation to AWS KMS.

Jump to

Keyboard shortcuts

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