Documentation
¶
Overview ¶
Package jwt issues short-lived JSON Web Tokens from a session, for other services that must know who is calling without sharing the auth database.
The session stays an opaque, revocable token; a JWT is a copy of "who" for a few minutes, signed with Ed25519 (alg EdDSA) and verifiable with the public keys this plugin publishes as a JWKS. Its short life is its revocation: a signed token cannot be withdrawn, so it expires before a revoked session would matter.
Index ¶
- Constants
- Variables
- func GenerateKey(alg Alg) (crypto.Signer, error)
- func HashOf(alg Alg) crypto.Hash
- func KeyID(pub crypto.PublicKey) string
- func KeysFromJWKS(set JWKS) map[string]crypto.PublicKey
- func NewSessionCache(p *Plugin) auth.SessionCache
- func PublicKeyForAlg(k JWK, alg Alg) (crypto.PublicKey, bool)
- func PublicKeyOfJWK(k JWK) (crypto.PublicKey, bool)
- func RSAKey(k crypto.Signer, alg Alg) (crypto.Signer, error)
- func SessionJWT(ctx context.Context) (string, bool)
- func Thumbprint(k JWK) (string, error)
- func VerifyHS256(token string, secret []byte, types ...string) ([]byte, error)
- func VerifySignature(token string, pub crypto.PublicKey, types ...string) (header, payload []byte, err error)
- type Alg
- type ClaimsFunc
- type JWK
- type JWKS
- type Option
- func AlgorithmKeys(keys ...crypto.Signer) Option
- func Audience(aud ...string) Option
- func ClaimSource(fn ClaimsFunc) Option
- func Claims(fn func(ctx context.Context, u auth.User) map[string]any) Option
- func IssueOnSession() Option
- func Issuer(iss string) Option
- func JWKSPath(path string) Option
- func Payload(fn PayloadFunc) Option
- func PreviousKeys(keys ...crypto.PublicKey) Option
- func RemoteJWKS(rawURL string) Option
- func Subject(fn SubjectFunc) Option
- func WithSigner(s Signer) Option
- func WithSigningKey(k crypto.Signer) Option
- func WithTTL(d time.Duration) Option
- func WithTemplate(name string, t Template) Option
- type PayloadFunc
- type Plugin
- func (p *Plugin) Init(a *auth.Auth) error
- func (p *Plugin) JWKSRoute() string
- func (p *Plugin) JWKSURL() string
- func (p *Plugin) Name() string
- func (p *Plugin) OperationHooks() []auth.OperationHook
- func (p *Plugin) Operations() []auth.Operation
- func (p *Plugin) OptionErr() error
- func (p *Plugin) PublicKeys() JWKS
- func (p *Plugin) Sign(ctx context.Context, in SignRequest) (TokenResult, error)
- func (p *Plugin) TemplateNames() []string
- func (p *Plugin) TemplateToken(ctx context.Context, name string) (TokenResult, error)
- func (p *Plugin) Token(ctx context.Context) (TokenResult, error)
- func (p *Plugin) Validate(c *auth.Config) error
- type RSAPublicKey
- type RemoteKeys
- type SignRequest
- type Signer
- type SubjectFunc
- type Template
- type TemplateKey
- type TemplateRequest
- type TokenResult
- type Verified
- type Verifier
Constants ¶
const ( // KeyTypeOKP is the JWK "kty" of an Edwards-curve key. //ds:def id=keytypeokp-cx2p5h5q owner=@khanakia stability=stable KeyTypeOKP = "OKP" // KeyTypeRSA is the JWK "kty" of an RSA key. KeyTypeRSA = "RSA" // KeyTypeEC is the JWK "kty" of an elliptic-curve (ECDSA) key. KeyTypeEC = "EC" // CurveEd25519 is the JWK "crv" of an EdDSA key. //ds:def id=curveed25519-w6kd4wru owner=@khanakia stability=stable CurveEd25519 = "Ed25519" // CurveP256 is the JWK "crv" of an ES256 key. CurveP256 = "P-256" // CurveP384 is the JWK "crv" of an ES384 key. CurveP384 = "P-384" // CurveP521 is the JWK "crv" of an ES512 key. CurveP521 = "P-521" // KeyUseSignature is the JWK "use" of a key that only signs. //ds:def id=keyusesignature-azygjgrb owner=@khanakia stability=stable KeyUseSignature = "sig" )
JWK and JWS header values for each key type (RFC 7517, RFC 7518, RFC 8037).
const ( // SessionCacheType is the JWS "typ" header of a session-cache snapshot // (better-auth's "better-auth.session-cache+jwt"). Verify refuses it and // the cache refuses every other typ, so an access token can never stand // in for a cached session, nor a snapshot for an access token. //ds:def id=sessioncachetype-w8d3kq6n owner=@khanakia stability=stable SessionCacheType = "ubgo-auth.session-cache+jwt" // SessionCacheAudience is the "aud" claim of a snapshot (better-auth's // "better-auth:session-cache"): a service whose Verifier checks its own // audience refuses a snapshot on that ground too. //ds:def id=sessioncacheaudience-v4m7zt2h owner=@khanakia stability=stable SessionCacheAudience = "ubgo-auth:session-cache" )
Session-cache constants.
const ( EndpointToken auth.EndpointID = "jwt.token" EndpointJWKS auth.EndpointID = "jwt.jwks" )
Operation ids.
const ( //ds:def id=defaultttl-kqd34f84 owner=@khanakia stability=stable DefaultTTL = 5 * time.Minute // MaxTTL caps a token: past an hour, a revoked session would stay usable // through its JWTs for longer than anyone expects. //ds:def id=maxttl-n53tvy7p owner=@khanakia stability=stable MaxTTL = time.Hour )
Lifetimes.
const ( // RemoteMinRefresh bounds how often an unknown kid may trigger a fetch, // so a flood of forged tokens cannot make a verifying service hammer the // issuer. //ds:def id=remoteminrefresh-c6vw2kzp owner=@khanakia stability=stable RemoteMinRefresh = time.Minute // RemoteMaxAge is how long a fetched set is trusted before the next // lookup refetches it, so a retired key disappears from the verifier // within the day even when no unknown kid forces a fetch. //ds:def id=remotemaxage-t9hr4nxb owner=@khanakia stability=stable RemoteMaxAge = 24 * time.Hour // DefaultRemoteTimeout bounds one key-set fetch when the caller passes // no client. //ds:def id=defaultremotetimeout-w3jq8mdf owner=@khanakia stability=stable DefaultRemoteTimeout = 10 * time.Second )
Remote key-set constants.
const ( // DefaultJWKSPath is where the key set is served under the plugin's // prefix (GET <mount>/jwt/jwks) unless JWKSPath moves it. //ds:def id=defaultjwkspath-t3vk9q2w owner=@khanakia stability=stable DefaultJWKSPath = "/jwks" // SetJWTHeader is the response header IssueOnSession puts a fresh token // in on the session read, better-auth's set-auth-jwt. The transport also // lists it in Access-Control-Expose-Headers, so a script on another // origin can read it. //ds:def id=setjwtheader-p6xr2m4c owner=@khanakia stability=stable SetJWTHeader = "Set-Auth-JWT" )
Route and header constants.
const ( // RotationSeedSize is the length of the seed Rotating derives keys from: // 256 bits, so the seed is as strong as the keys it makes. //ds:def id=rotationseedsize-f4wq9bkm owner=@khanakia stability=stable RotationSeedSize = 32 // MinRotationInterval bounds how often keys change: below an hour the // key set churns faster than verifiers refetch it, and every unknown kid // becomes a fetch. //ds:def id=minrotationinterval-h7zt2vcp owner=@khanakia stability=stable MinRotationInterval = time.Hour )
Rotation constants.
const ( // HS256 is HMAC with SHA-256 (RFC 7518 section 3.2). It is not an Alg: // a Verifier never accepts it and no key set ever lists its key, because // the key is a secret shared with one receiver. Only a template with // HMACKey signs it, for a vendor that verifies with a shared secret // (Supabase's legacy JWT secret, Hasura's HS256 key). //ds:def id=hs256-t5mq8wzk owner=@khanakia stability=stable HS256 = "HS256" // MinHMACSecret is the shortest HMACKey secret: RFC 7518 section 3.2 // requires a key at least as long as the hash output, 32 bytes for // SHA-256, because a shorter one is easier to guess than to forge. //ds:def id=minhmacsecret-r8kw2vnd owner=@khanakia stability=stable MinHMACSecret = 32 )
Template limits and wire values.
const DefaultAlgorithm = EdDSA
DefaultAlgorithm is the algorithm of the key GenerateKey makes when given "", and of every token signed with an Ed25519 key, which is what the plugin was built on.
const DefaultLeeway = 30 * time.Second
DefaultLeeway absorbs clock differences between servers.
const EndpointTemplate auth.EndpointID = "jwt.template"
EndpointTemplate is the operation that mints a token from a named template, Clerk's getToken({ template }).
const MaxAudienceValues = 64
MaxAudienceValues is the most aud entries a token may carry.
const ( // MinRSABits is the smallest RSA modulus signed or accepted. NIST SP // 800-131A has disallowed anything smaller since 2013, and a verifier // that took a 1024-bit key would trust a modulus that can be factored. //ds:def id=minrsabits-q8w2nd5k owner=@khanakia stability=stable MinRSABits = 2048 )
Key sizes.
const PluginName = "jwt"
PluginName is the plugin's permanent identity.
Variables ¶
var ( // ErrNoAlgorithmKey is a Sign whose SignRequest.Alg names an algorithm // that neither the signing key nor an AlgorithmKeys key signs, better- // auth's descriptive error for an algorithm absent from keyPairConfig and // keyPairConfigs. ErrNoAlgorithmKey = errors.New("jwt: no signing key for this algorithm; add one with jwt.AlgorithmKeys") // ErrAlgorithmKeys is a configuration error: an AlgorithmKeys key that is // nil or a short Ed25519 key, or one key given twice (counting the // signing key), which would publish one key id twice. ErrAlgorithmKeys = errors.New("jwt: AlgorithmKeys holds valid keys, each once and none the signing key") // ErrNoSigningKey is a Sign whose SignRequest.KeyID names no key the // plugin signs with (the signing key or an AlgorithmKeys key), or one // of another algorithm than SignRequest.Alg. ErrNoSigningKey = errors.New("jwt: no signing key with this key id") )
Errors of AlgorithmKeys.
var ( ErrNoKey = errors.New("jwt: pass jwt.WithSigningKey with an Ed25519, RSA or P-256 private key, or jwt.WithSigner") ErrReservedClaim = errors.New("jwt: Claims may not set a registered claim") ErrNoIssuer = errors.New("jwt: needs auth.WithBaseURL or jwt.Issuer") )
Errors.
var ( // ErrJWKSPath is a JWKSPath that does not start with "/", is "/" alone, // contains "..", "//" or a character outside RFC 3986's unreserved set, // or is the token or template route's path. ErrJWKSPath = errors.New(`jwt: JWKSPath must start with "/", hold only letters, digits and "-._~/", contain no ".." or "//", and not be "/token" or "/template"`) // ErrRemoteJWKS is a RemoteJWKS URL that is not an absolute https URL // (http is allowed for a loopback host), or carries a fragment or user // information. ErrRemoteJWKS = errors.New("jwt: RemoteJWKS needs an absolute https URL (http only for a loopback host)") )
Route and remote key-set errors, returned by auth.New.
var ( // ErrRotationSeed is a seed that is not RotationSeedSize bytes. ErrRotationSeed = errors.New("jwt: Rotating needs a 32-byte seed") // ErrRotationInterval is an interval below MinRotationInterval. ErrRotationInterval = errors.New("jwt: Rotating interval is at least an hour") // ErrRotationGrace is a grace period shorter than the token TTL: a // token could then outlive its key's publication and fail to verify // while still valid. ErrRotationGrace = errors.New("jwt: Rotating grace period is at least the token TTL") )
Rotation errors, returned by auth.New.
var ( // ErrUnknownTemplate is a request for a template name the app did not // register. Templates are an allowlist kept on the server: a client // picks one by name and can never shape the claims. ErrUnknownTemplate = auth.NewError("jwt.unknown_template", http.StatusNotFound, "This token template does not exist.") // ErrTemplateName is a template name that is empty, longer than 64 // characters, or holds a character outside a-z, 0-9, "-" and "_", or a // name given twice. ErrTemplateName = errors.New(`jwt: a template name is 1-64 characters of a-z, 0-9, "-" and "_", each registered once`) // ErrHMACSecret is an HMACKey secret shorter than MinHMACSecret bytes. ErrHMACSecret = errors.New("jwt: an HMAC template secret is at least 32 bytes") // ErrTemplateKey is a template key that cannot sign: a PrivateKey of an // unsupported type, a key id with a character outside the base64url // alphabet, or a key whose algorithm is not in the template's Algs. ErrTemplateKey = errors.New("jwt: the template's key cannot sign for it") )
Template errors. ErrUnknownTemplate is what a client sees; the others are configuration errors, returned by auth.New.
var AlgValues = []Alg{EdDSA, RS256, ES256, ES384, ES512, RS384, RS512, PS256, PS384, PS512}
AlgValues is every algorithm the plugin signs and verifies, in the order the documentation lists them: the three original ones first, so a list built from it keeps its old prefix.
var ErrBadType = errors.New(`jwt: a token type holds only letters, digits and "+-." and is not the session-cache type`)
ErrBadType is a SignRequest.Type or Verifier.Types entry that holds a character outside letters, digits and "+-.", or is SessionCacheType.
var ErrCacheNotWired = errors.New("jwt: NewSessionCache's plugin is not in auth.WithPlugins")
ErrCacheNotWired is a session cache whose plugin was not passed to auth.WithPlugins, so it has no issuer and no clock: every Put fails, and the first request says why instead of caching nothing silently.
var ErrEmptySubject = errors.New("jwt: Subject returned an empty subject")
ErrEmptySubject is a SubjectFunc that returned "": a token without a subject names nobody, so the call fails rather than mint it.
var ErrInvalid = errors.New("jwt: token is not valid")
ErrInvalid covers every token that must not be trusted: malformed, a typ other than JWT (a session-cache snapshot), an alg other than the one of the key its kid names (including "none"), an unknown key, a bad signature, expired, not yet valid, or for another issuer or audience.
var ErrRemoteKeys = errors.New("jwt: remote key set could not be read")
ErrRemoteKeys is a key-set fetch that failed or returned something that is not a key set; Lookup then reports the kid unknown, and the error is kept for LastError.
var ErrSignature = errors.New("jwt: the Signer's signature does not verify with its published key")
ErrSignature is a Signer whose signature does not verify against the key it published: a KMS that answered with the wrong key or the wrong encoding (an ASN.1 ECDSA signature where JWS wants r||s). The token is not minted, because no verifier could accept it.
var ErrSignerKey = errors.New("jwt: Signer has no published key to sign with")
ErrSignerKey is a Signer with no key to sign with, or a Rotating signer asked to sign with a key outside its grace: the token would carry a kid no verifier could find, so the call fails instead of minting it.
var ErrUnsupportedKey = errors.New("jwt: the key is not Ed25519, RSA of at least 2048 bits, or ECDSA on P-256, P-384 or P-521")
ErrUnsupportedKey is a key that is not Ed25519, RSA of at least MinRSABits or ECDSA on P-256, P-384 or P-521: WithSigningKey, PreviousKeys or a Signer's PublicKeys gave one, and auth.New refuses it, or GenerateKey or RSAKey was asked for an algorithm it does not know.
Functions ¶
func GenerateKey ¶
GenerateKey makes a new private key for alg ("" means DefaultAlgorithm): Ed25519, RSA of MinRSABits (bound to alg by RSAKey for an algorithm other than RS256), or ECDSA on the algorithm's curve. Pass it to WithSigningKey or AlgorithmKeys, and keep it in the app's secret store: a key generated at every start would sign out every verifier holding the old key set.
func HashOf ¶
HashOf is the hash an algorithm signs over, and the one OpenID Connect's at_hash and c_hash use (Core section 3.1.3.6): SHA-256, SHA-384 or SHA-512 by the algorithm's suffix, and SHA-512 for EdDSA (Ed25519 hashes with it internally). 0 for an unknown algorithm.
func KeyID ¶
KeyID names a public key: the first 16 base64url characters of a SHA-256. For Ed25519 the hash is of the raw 32-byte key, as before RSA and ECDSA existed, so the ids of keys already published never change; for RSA and ECDSA it is of the key's PKIX (SubjectPublicKeyInfo) encoding, followed, for an RSAPublicKey, by its algorithm, so one modulus bound to two algorithms has two ids. An unsupported key has no id, "".
func KeysFromJWKS ¶
KeysFromJWKS turns a published key set into Verifier.Keys, keeping the Ed25519, RSA (at least MinRSABits) and P-256 keys whose alg, when given, is the one of their type, and skipping every other key.
func NewSessionCache ¶
func NewSessionCache(p *Plugin) auth.SessionCache
NewSessionCache returns an auth.SessionCache whose client copy is a JWT signed with p's key, better-auth's sessionCookieCache: a value with typ SessionCacheType and kid in the header, and iss, aud (SessionCacheAudience), sub (the user id), sid (the session id), iat, exp (when the entry stops being used), the session, the user, and a binding to the session token among its claims. Pass it to auth.WithSessionCache, with p in auth.WithPlugins.
It keeps every rule of auth.NewSnapshotCache, which stays the default: an entry is used only with the token it was made for (compared in constant time), only until its expiry, never past MaxSessionCacheTTL, and the core never renews one without reading the store. Get verifies the signature with the published key its kid names, under that key's algorithm and never the header's, and refuses every other typ; anything that does not verify is a miss, so the store is asked.
The difference that matters: the claims are signed, not encrypted. Anyone holding the value (the browser, a bearer client, a log line) can read the session's IP, user agent and app data and the user's name and image; nobody can change them. Use NewSnapshotCache when that is not acceptable. With Rotating, a grace shorter than the cache TTL only turns a snapshot signed with a retired key into a miss.
func PublicKeyForAlg ¶
PublicKeyForAlg parses another party's JWK for a signature made under alg, the header of a token that key signed: a DPoP proof's embedded key, or a private_key_jwt client's registered key. RFC 7517 section 4.4 makes a JWK's alg optional, and such keys often omit it; then the key's type decides the family (an RSA key verifies any RSA algorithm, an EC key only its curve's, an Ed25519 key only EdDSA), and the result is bound to alg so VerifySignature still pins the algorithm to the key. A JWK that names an alg must name this one. false for anything PublicKeyOfJWK refuses, and for an alg outside the key's family, so no header can turn a public key into an HMAC secret.
func PublicKeyOfJWK ¶
PublicKeyOfJWK parses a JWK into a public key with the rules KeysFromJWKS applies: Ed25519, RSA of at least MinRSABits, P-256 on the curve, and an alg, when given, that is the key's. false for anything else.
func RSAKey ¶
RSAKey binds an RSA signer (an *rsa.PrivateKey, or a KMS key behind crypto.Signer) to one RSA algorithm, better-auth's keyPairConfig { alg: "PS256" }: the result signs alg, and its public key (an RSAPublicKey) is published with alg. RS256 returns the signer unchanged, since a bare RSA key already signs it. Anything other than an RSA key of at least MinRSABits and an RSA algorithm is ErrUnsupportedKey.
func SessionJWT ¶
SessionJWT returns the token IssueOnSession handed out during the request in ctx, for a Go caller of Auth.Session, which has no response headers to read; false when there is none (the option is off, the caller is signed out, or the session was not read).
func Thumbprint ¶
Thumbprint is the RFC 7638 JWK thumbprint, base64url of SHA-256 over the key's required members in lexicographic order with no whitespace: what a DPoP-bound token's cnf.jkt names. It refuses a key PublicKeyOfJWK would refuse, so a thumbprint always names a usable key.
func VerifyHS256 ¶
VerifyHS256 checks an HS256 compact JWS against secret, for an app that verifies its own HMACKey template tokens (a test, or a service that shares the secret): the header's alg must be HS256, typ one of types ("JWT" when none is given), and the MAC equal in constant time. It returns the decoded payload JSON and checks no claim; exp, iss and aud are the caller's. A token signed with a public-key algorithm never passes here, and an HS256 token never passes a Verifier, so neither path can be confused for the other.
func VerifySignature ¶
func VerifySignature(token string, pub crypto.PublicKey, types ...string) (header, payload []byte, err error)
VerifySignature checks a compact JWS against one known key, for a token that is not one of this plugin's (a DPoP proof carries its key in its header and has no sub or exp, so Verifier does not fit). The typ must be one of types, the alg the key's, and the signature valid; it returns the decoded header and payload JSON for the caller's own claim checks, and checks no claim itself.
Types ¶
type Alg ¶
type Alg string
Alg is a JWS signature algorithm (RFC 7518 section 3, RFC 8037) the plugin signs and verifies with. The algorithm is a property of the key, never of the token: an Ed25519 key signs EdDSA, an RSA key RS256 (or the algorithm RSAKey names), and an ECDSA key the algorithm of its curve (P-256 ES256, P-384 ES384, P-521 ES512); a verifier accepts a token only when its alg header names the algorithm of the key its kid points at. So a token cannot pick its own algorithm, which is how "alg: none" and the RS256-to-HS256 confusion work.
const ( // EdDSA is Ed25519 (RFC 8037): small keys, fast, deterministic // signatures, and the default. EdDSA Alg = "EdDSA" // RS256 is RSASSA-PKCS1-v1_5 with SHA-256, for a consumer that accepts // RSA only, such as the JWT authorizer of AWS API Gateway. RS256 Alg = "RS256" // RS384 and RS512 are RSASSA-PKCS1-v1_5 with SHA-384 and SHA-512; an // RSA key signs them only through RSAKey. RS384 Alg = "RS384" RS512 Alg = "RS512" // PS256, PS384 and PS512 are RSASSA-PSS with SHA-256, SHA-384 and // SHA-512, the salt as long as the hash (RFC 7518 section 3.5); an RSA // key signs them only through RSAKey. PS256 Alg = "PS256" PS384 Alg = "PS384" PS512 Alg = "PS512" // ES256 is ECDSA on P-256 with SHA-256, for a consumer or a KMS that // offers ECDSA and not Ed25519. ES256 Alg = "ES256" // ES384 is ECDSA on P-384 with SHA-384. ES384 Alg = "ES384" // ES512 is ECDSA on P-521 with SHA-512 (the name is the hash's, the // curve is 521 bits). ES512 Alg = "ES512" )
The algorithms. Symmetric (HS*) algorithms are deliberately absent: a key set is public, and a shared secret in one is a forgery waiting to happen.
type ClaimsFunc ¶
ClaimsFunc builds app claims from the user and the session, and can fail: a claim read from the database (the active organization, a plan) has an error to report, and a token missing a claim its receiver relies on must not be minted, so the error fails the token call instead.
func UserClaim ¶
func UserClaim(name string, read func(ctx context.Context, userID string) (json.RawMessage, error)) ClaimsFunc
UserClaim is a ClaimsFunc that writes one claim, name, from a JSON document read per user: for the public metadata plugin/metadata keeps, jwt.ClaimSource(jwt.UserClaim("public_metadata", meta.PublicOf)), or the same in a template's Claims. It takes a func rather than the metadata plugin, so any per-user JSON source fits and this package imports no other feature. Only a document meant for every receiver of the token belongs here: a JWT is signed, not encrypted, so private metadata must never be a claim. A failing read, or a document that is not valid JSON, fails the token call.
type JWK ¶
type JWK struct {
KeyType string `json:"kty"`
Curve string `json:"crv,omitempty"`
X string `json:"x,omitempty"`
Y string `json:"y,omitempty"`
N string `json:"n,omitempty"`
E string `json:"e,omitempty"`
KeyID string `json:"kid"`
Use string `json:"use"`
Alg string `json:"alg"`
}
JWK is one public key in JSON Web Key form. Which fields are set depends on KeyType: OKP (Ed25519, RFC 8037) has Curve and X; RSA (RFC 7518 section 6.3) has N and E; EC (P-256, RFC 7518 section 6.2) has Curve, X and Y. The unused ones are omitted from the JSON, so an Ed25519 key's JSON is what it was before RSA and ECDSA existed.
type JWKS ¶
type JWKS struct {
Keys []JWK `json:"keys"`
}
JWKS is the public key set: the current key first, then the AlgorithmKeys keys, then PreviousKeys.
type Option ¶
type Option func(*Plugin)
Option configures the plugin; pass options to New.
func AlgorithmKeys ¶
AlgorithmKeys adds signing keys beside the signing key, better-auth's jwks.keyPairConfigs: a token signed by Sign with SignRequest.Alg set to an algorithm uses the first key of it (the signing key before these), SignRequest.KeyID picks one by key id (better-auth's per-resource signingKeyId), and every one is published in the key set (after the signer's keys, before PreviousKeys), so a verifier finds it by kid. The session token route and Token keep signing with the main key. Several keys may share an algorithm; one key given twice (the signing key included) fails auth.New with ErrAlgorithmKeys. better-auth makes such a key the first time it is asked for and stores it; this plugin stores no keys, so the app passes them (GenerateKey makes one) from its secret store.
func Audience ¶
Audience sets the aud claim: the service, or services, the tokens are for. One value is written as a string, several as an array (RFC 7519 section 4.1.3); Verifier accepts either form.
func ClaimSource ¶
func ClaimSource(fn ClaimsFunc) Option
ClaimSource is Payload for claims whose lookup can fail, such as orgjwt's organization claim: its claims are added after Claims' and Payload's, replacing any of the same name, a registered claim fails the call like there, and so does an error.
func Claims ¶
Claims adds app claims, such as a role or a tenant. A registered claim returned here fails the call rather than overwriting the plugin's. The values are any because a claim is any JSON value; one with no JSON form fails the call.
func IssueOnSession ¶
func IssueOnSession() Option
IssueOnSession makes the session read (GET <mount>/session, Auth.Session, every transport) also mint a token and hand it to the client in the SetJWTHeader response header: better-auth's set-auth-jwt, which it sends unless disableSettingJwtHeader. Off by default here, because a token in every session read is a bearer credential handed out on every page load whether the client wants one or not. The token is exactly the one POST <mount>/jwt/token returns at that instant (the same claims, TTL, signer and audience), there is none when the caller is signed out (the session read answers 401), and a failure to mint fails the session read, as a failing after-hook does in better-auth. A Go caller reads it with SessionJWT(ctx) after calling Auth.Session.
func JWKSPath ¶
JWKSPath moves the key-set route from DefaultJWKSPath to path, under the plugin's prefix: JWKSPath("/.well-known/jwks.json") serves GET <mount>/jwt/.well-known/jwks.json, and the old path answers 404. better-auth's jwks.jwksPath; the same rules apply (it starts with "/" and has no ".."), plus the characters a route may hold here (ErrJWKSPath).
func Payload ¶
func Payload(fn PayloadFunc) Option
Payload is Claims with the session too, better-auth's definePayload: a claim from the session (its organization, its device) can ride in the token. Its claims are added after Claims', replacing any of the same name; a registered claim (sub, sid, iss, aud, exp, iat, jti) still fails the call, because a receiver trusts those. better-auth's default payload is the whole user; this plugin's is the user id only, so no personal data rides in a bearer token unless the app returns it here.
func PreviousKeys ¶
PreviousKeys keeps publishing old public keys after a rotation, so tokens signed just before it still verify until they expire. A key may be of another algorithm than the signing key, which is how a deployment moves from EdDSA to RS256 without failing tokens in flight.
func RemoteJWKS ¶
RemoteJWKS says the key set is published somewhere else, such as a KMS's own JWKS endpoint or a CDN: the local key-set route is not served (404), and JWKSURL returns rawURL for the app to hand to its verifiers (jwt.NewRemoteKeys(url, nil) in Go, createRemoteJWKSet in jose, the "JWKS URI" field of an API gateway). better-auth's jwks.remoteUrl, which it advertises in its OpenID discovery document; this library has no discovery document, and no token carries the URL (no "jku" header): a verifier that followed a URL named by the token it is verifying would let the token choose its own keys. The keys at rawURL must include every key the plugin's Signer publishes, or the tokens will not verify; the session cache keeps verifying against the plugin's own keys either way.
func Subject ¶
func Subject(fn SubjectFunc) Option
Subject replaces the default sub claim, the user id, for a service that keys users differently (an external id stored on the user, say). The function must return a non-empty value, or the token call fails with ErrEmptySubject.
func WithSigner ¶
WithSigner replaces WithSigningKey with a custom Signer: a KMS, an HSM, or Rotating. One of the two is required.
func WithSigningKey ¶
WithSigningKey sets the key that signs new tokens, and so the algorithm: an ed25519.PrivateKey signs EdDSA, an *rsa.PrivateKey (at least MinRSABits) RS256, an *ecdsa.PrivateKey on P-256 ES256 (GenerateKey makes any of them). Any crypto.Signer works, so a KMS client that implements it keeps its private key in the KMS. Required, unless WithSigner names a Signer instead. An Ed25519 key of the wrong length is ignored, so auth.New fails with ErrNoKey; another unsupported key fails it with ErrUnsupportedKey, and nil with auth.ErrNilOption.
func WithTTL ¶
WithTTL overrides DefaultTTL. A value that is not positive or exceeds MaxTTL becomes MaxTTL, so a misconfiguration never issues an unbounded token.
func WithTemplate ¶
WithTemplate registers a template under name. Names are 1-64 characters of a-z, 0-9, "-" and "_", each registered once (ErrTemplateName).
type PayloadFunc ¶
PayloadFunc builds a token's app claims from the user and the session it is minted from, better-auth's jwt.definePayload({ user, session }).
type Plugin ¶
type Plugin struct {
// contains filtered or unexported fields
}
Plugin issues JWTs from sessions and publishes the public key set. Build it with New; WithSigningKey is required.
func New ¶
New builds the plugin and defines its token and key-set operations. The TTL is clamped here, so every later reader sees a value within MaxTTL.
func (*Plugin) Init ¶
Init captures the core and fills the issuer from the base URL's origin when Issuer was not set. A Rotating signer takes the core's clock, so its periods and the tokens' timestamps come from one source.
func (*Plugin) JWKSRoute ¶
JWKSRoute is the key-set route under the app's mount prefix ("/jwt/jwks" by default, the JWKSPath otherwise), or "" when RemoteJWKS publishes the keys elsewhere and the route is not served.
func (*Plugin) JWKSURL ¶
JWKSURL is where verifiers fetch the key set when it is published elsewhere: the RemoteJWKS URL, or "" when the plugin serves it itself, at JWKSRoute under the app's mount prefix.
func (*Plugin) OperationHooks ¶
func (p *Plugin) OperationHooks() []auth.OperationHook
OperationHooks adds, under IssueOnSession, the after-hook on the session read that mints the header token; nothing otherwise.
func (*Plugin) Operations ¶
Operations lists token and template (signed in) and jwks (public), for the transports to mount; jwks is left out under RemoteJWKS, so its route answers 404 as better-auth's does. The template route is served with no template registered too, answering ErrUnknownTemplate, so a client's method exists before the app adds its first template.
func (*Plugin) OptionErr ¶
OptionErr is every error the options recorded, such as auth.ErrNilOption naming an option given nil; auth.New refuses to start while it is not nil (auth.OptionChecker).
func (*Plugin) PublicKeys ¶
PublicKeys returns the key set, for a service in the same process: the signer's keys (the signing key first), then the AlgorithmKeys keys, then PreviousKeys.
func (*Plugin) Sign ¶
func (p *Plugin) Sign(ctx context.Context, in SignRequest) (TokenResult, error)
Sign mints a token that is not tied to a session: a service token for a job, a token for a user the app looked up itself, a token to hand a webhook receiver. It carries sub, iss, aud, iat, nbf, exp and jti, but no sid, so Verified.SessionID is "" for a receiver; the plugin's Subject and Claims options do not apply, since the caller names both. Go only, for trusted code: anything minted here is as good as a session token to the receiving service, which is why no operation offers it.
func (*Plugin) TemplateNames ¶
TemplateNames lists the registered template names, sorted, for a settings page or a generated client.
func (*Plugin) TemplateToken ¶
TemplateToken mints a token from the named template for the signed-in user: ErrUnknownTemplate for a name the app did not register, and an API key is refused like Token.
func (*Plugin) Token ¶
func (p *Plugin) Token(ctx context.Context) (TokenResult, error)
Token issues a JWT for the signed-in user. An API key cannot mint one: it is an auth operation, refused with auth.ErrAPIKeyRefused.
func (*Plugin) Validate ¶
Validate needs a signer and an issuer: without a key nothing can be signed, and a token without an issuer cannot be told apart from another app's. A Rotating signer's seed, interval and grace are checked here too, every published key must be of a supported algorithm (ErrUnsupportedKey), and JWKSPath and RemoteJWKS must be well formed.
type RSAPublicKey ¶
RSAPublicKey is the public half of an RSA key that signs an algorithm other than RS256 (RS384, RS512, PS256, PS384, PS512): the public key of an RSAKey signer, and what PublicKeyOfJWK answers for an RSA JWK whose alg is one of them. Keeping the algorithm on the key keeps the rule that a token never chooses how its own signature is checked: the same modulus published as RS256 and as PS256 is two keys with two key ids.
type RemoteKeys ¶
type RemoteKeys struct {
// Now is the clock; nil means time.Now.
Now func() time.Time
// contains filtered or unexported fields
}
RemoteKeys fetches an issuer's JWKS over HTTP and caches it, refetching when a token names a key it does not know (at most once per RemoteMinRefresh) or when the set is older than RemoteMaxAge. Give it to a Verifier as Lookup, so a service that trusts an app's keys picks up a rotation without a deploy. It is safe for concurrent use.
func NewRemoteKeys ¶
func NewRemoteKeys(url string, hc *http.Client) *RemoteKeys
NewRemoteKeys builds a RemoteKeys for the JWKS at url. A nil client gets one with DefaultRemoteTimeout. Nothing is fetched until the first Lookup.
func (*RemoteKeys) LastError ¶
func (r *RemoteKeys) LastError() error
LastError is the error of the most recent fetch, nil when it succeeded, for a health check or a log line.
type SignRequest ¶
type SignRequest struct {
// Subject is the sub claim; required.
Subject string
// Audience is the aud claim; nil takes the plugin's Audience.
Audience []string
// TTL is the token's lifetime; 0 takes the plugin's, and more than MaxTTL
// becomes MaxTTL, like WithTTL.
TTL time.Duration
// Claims are app claims; a registered claim here is ErrReservedClaim.
Claims map[string]any
// Type is the JWS "typ" header; "" is "JWT". An OAuth provider's access
// token uses "at+jwt" (RFC 9068). It is a media-type name: letters,
// digits and "+-." only (ErrBadType otherwise), and never
// SessionCacheType, so no Sign call can mint a session-cache snapshot.
Type string
// SessionID is the sid claim, for an ID token that a logout request can
// later bind to the browser's session; "" leaves it out.
SessionID string
// Alg picks the signing algorithm, better-auth's signingAlgorithm: ""
// or the signing key's algorithm signs with it, another one with the
// AlgorithmKeys key of that algorithm, and an algorithm with no key is
// ErrNoAlgorithmKey.
Alg Alg
// KeyID picks the signing key by its key id among the signing key and
// the AlgorithmKeys keys, better-auth's signingKeyId; a key id the
// plugin does not sign with, or one of another algorithm than Alg, is
// ErrNoSigningKey.
KeyID string
}
SignRequest is what Sign mints a token from: a subject the app chose and the claims it wants, under the plugin's key, issuer and lifetime cap.
type Signer ¶
type Signer interface {
// PublicKeys returns every key a token in flight may carry, the key
// that signs now first: its KeyID becomes the token's kid header, and
// the JWKS publishes the list in this order. An empty list means the
// signer cannot sign, and Token fails with ErrSignerKey.
PublicKeys() []crypto.PublicKey
// Sign returns the JWS signature of the signing input (header dot
// payload) with the private half of key, the first key PublicKeys
// returned when the token's header was built: for EdDSA the Ed25519
// signature of the input, for RS256 the PKCS #1 v1.5 signature of its
// SHA-256, for ES256 the 64-byte r||s of its SHA-256 (RFC 7518 section
// 3.4, not the ASN.1 form crypto.Signer returns). The key is passed
// back, rather than assumed current, so a signer whose key changes
// between the two calls, at the instant of a rotation, still signs with
// the key the header names.
Sign(ctx context.Context, key crypto.PublicKey, signingInput []byte) ([]byte, error)
}
Signer signs tokens. The plugin ships two: the key passed to WithSigningKey, and Rotating, which derives a fresh Ed25519 key every interval. An app whose private key must never leave a KMS implements it (or, simpler, passes a KMS crypto.Signer to WithSigningKey): Sign asks the service for the signature and PublicKeys returns what the JWKS must list. The algorithm follows the first key: ed25519.PublicKey is EdDSA, *rsa.PublicKey RS256, *ecdsa.PublicKey on P-256 ES256; any other key is ErrUnsupportedKey. The signature the plugin receives is checked against that key before a token is minted (ErrSignature).
func Rotating ¶
Rotating is a Signer that changes Ed25519 (EdDSA) keys automatically. RS256 and ES256 keys rotate by hand (WithSigningKey with the new key, PreviousKeys with the old), because the standard library derives no RSA key from a seed and only Go 1.25 parses a raw ECDSA scalar. The key of each period (interval-wide, counted from the Unix epoch) is derived from the seed with HKDF-SHA256, so every instance of the app derives the same key for the same period with no storage and no coordination, and a restart changes nothing. The JWKS publishes the current key first, then the keys of the periods that ended within grace, so a token signed just before a rotation verifies until it expires; grace must be at least the TTL.
The seed is the one secret: whoever holds it can derive every key, past and future, which is also true of a stored private key. Rotating buys a bounded lifetime per key and an automatic schedule, not protection against a leaked seed; rotate the seed itself for that.
type SubjectFunc ¶
SubjectFunc chooses the sub claim for a token. It receives the user and the session the token is minted from.
type Template ¶
type Template struct {
// Claims builds the template's app claims from the user and the
// session; nil adds none, and an error fails the call.
Claims ClaimsFunc
// Subject chooses sub; nil takes the plugin's Subject option, else the
// user id. An empty result fails the call with ErrEmptySubject.
Subject SubjectFunc
// Issuer is iss; "" takes the plugin's issuer.
Issuer string
// Audience is aud; nil writes none. A template meant for one receiver
// should name it, so the receiver's audience check keeps a token from
// another template out.
Audience []string
// TTL is the lifetime; 0 takes the plugin's, and more than MaxTTL
// becomes MaxTTL. A token never outlives its session either way.
TTL time.Duration
// Key signs the tokens; nil is the plugin's signing key. See
// PublishedKey, HMACKey and PrivateKey.
Key TemplateKey
// Algs, when not empty, are the algorithms the receiver accepts: Convex
// verifies RS256 and ES256 only, Firebase RS256 only. auth.New fails
// with ErrTemplateKey when Key signs another one, rather than minting
// tokens the receiver refuses.
Algs []string
// Type is the typ header; "" is "JWT". Same rules as SignRequest.Type.
Type string
}
Template is a named token shape the app registers with WithTemplate and a signed-in client asks for by name (POST <mount>/jwt/template), Clerk's JWT templates: a token for a third party (a database, a GraphQL engine, a backend-as-a-service) that wants claims in its own shape. Everything here is server configuration; the client sends the name only.
The registered claims stay the plugin's: iss, sub, iat, nbf, exp and jti are always written, sid names the session, act an impersonating admin, and aud is Audience. Claims returning one of them fails the call (ErrReservedClaim), so a template can rename nothing a receiver trusts; Issuer and Subject are where a vendor's iss and sub are set. The plugin's Claims, Payload and Subject options do not apply to a template.
type TemplateKey ¶
type TemplateKey interface {
// contains filtered or unexported methods
}
TemplateKey is what signs a template's tokens: PublishedKey, HMACKey or PrivateKey. The set is closed, so a template key is always one whose publication rule is known: published keys are in the JWKS, the other two never are.
func HMACKey ¶
func HMACKey(secret []byte) TemplateKey
HMACKey signs a template with HS256 and a secret shared with its one receiver, for a vendor that verifies with a shared secret (Supabase's legacy JWT secret, Hasura's HS256 key). The secret is never published: the JWKS lists public keys only, a Verifier refuses HS256 outright, and the secret must be at least MinHMACSecret bytes (ErrHMACSecret). Prefer PublishedKey wherever the receiver can fetch a key set: whoever holds a shared secret can mint tokens, the receiver included.
func PrivateKey ¶
func PrivateKey(k crypto.Signer, kid string) TemplateKey
PrivateKey signs a template with a key the receiver issued, such as a Firebase service account's RSA key, whose public half the receiver already holds. It is never added to the plugin's key set. kid is the header's key id, the receiver's name for the key ("" writes no kid).
func PublishedKey ¶
func PublishedKey(alg Alg, kid string) TemplateKey
PublishedKey signs a template with one of the plugin's published keys, chosen like SignRequest: alg picks the signing key or an AlgorithmKeys key of that algorithm, kid one key by id; "" for both is the signing key. The receiver verifies through the plugin's JWKS route, which is how Convex's customJwt provider and Hasura's jwk_url work.
type TemplateRequest ¶
type TemplateRequest struct {
Template string `json:"template"`
}
TemplateRequest names the template a token is minted from.
type TokenResult ¶
TokenResult is a signed token and its expiry, which is the earlier of the TTL and the session's own expiry.
type Verified ¶
type Verified struct {
Subject string
SessionID string
// Actor is the admin behind an impersonation, or "".
Actor string
ExpiresAt time.Time
// Extra holds every claim the app added with Claims, raw.
Extra map[string]json.RawMessage
}
Verified is a verified token's claims. Only a Verify that returned no error produces one, so every field is trustworthy. ExpiresAt is in UTC.
type Verifier ¶
type Verifier struct {
// Keys maps key ids to public keys; build it with KeysFromJWKS or add
// keys with KeyID.
Keys map[string]crypto.PublicKey
// Lookup finds a key Keys does not hold, such as one published after a
// rotation; RemoteKeys.Lookup fetches the issuer's JWKS. Nil means Keys
// is the whole set.
Lookup func(kid string) (crypto.PublicKey, bool)
Issuer string
Audience string
// Types are the JWS "typ" headers accepted; nil accepts "JWT" only, as
// before. An OAuth resource server verifying access tokens sets
// []string{"at+jwt"}. The session-cache type is never accepted.
Types []string
Leeway time.Duration
// Now is the clock; nil means time.Now.
Now func() time.Time
}
Verifier checks tokens this plugin issued, for a Go service that receives them. It needs no database and no JWT library.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package jwtpreset holds example jwt.Template conventions for third-party services that accept a token from your app: Supabase, Hasura, Convex and Firebase custom tokens.
|
Package jwtpreset holds example jwt.Template conventions for third-party services that accept a token from your app: Supabase, Hasura, Convex and Firebase custom tokens. |