- PublicKey.Key → PublicKey.Pub (CryptoPublicKey) - PrivateKey.privKey → PrivateKey.Priv (crypto.Signer, now public) - Update all internal usages in jwk.go, sign.go, verify.go - Update all test usages in jwt_test.go, coverage_test.go, edge_test.go - Update interop tests (round-trip-go-jose, round-trip-go-jwt, nuance) - Update SKILL.md documentation Breaking change: PublicKey.Key renamed to PublicKey.Pub
6.9 KiB
name, description
| name | description |
|---|---|
| jwt | Go JWT/JWS/JWK library patterns. Use when writing code that signs, verifies, or validates JWTs, implements OIDC/OAuth flows, builds JWKS endpoints, creates ACME JWS messages, or works with custom JOSE headers. |
Core flow
Issuer: NewSigner -> Signer.SignToString (or Sign + Encode)
Relying party: Decode -> Verifier.Verify -> RawJWT.UnmarshalClaims -> Validator.Validate
Signing JWTs
pk, _ := jwt.NewPrivateKey() // Ed25519 by default
signer, _ := jwt.NewSigner([]*jwt.PrivateKey{pk})
token, _ := signer.SignToString(&jwt.TokenClaims{
Iss: "https://auth.example.com",
Sub: "user123",
Aud: jwt.Listish{"https://api.example.com"},
Exp: time.Now().Add(time.Hour).Unix(),
IAt: time.Now().Unix(),
})
SignToString is the one-step path. Use Sign when you need the JWT object.
Verifying + validating
verifier, _ := jwt.NewVerifier(pubKeys)
jws, _ := verifier.VerifyJWT(tokenStr) // decode + verify in one step
var claims MyCustomClaims
_ = jws.UnmarshalClaims(&claims)
validator := jwt.NewIDTokenValidator(
[]string{"https://auth.example.com"}, // iss (nil=skip, ["*"]=any)
[]string{"https://api.example.com"}, // aud (nil=skip, ["*"]=any)
nil, // azp (nil=skip)
0, // grace period (0 = 2s default)
)
_ = validator.Validate(nil, &claims, time.Now())
Two-step alternative: jwt.Decode(tokenStr) then verifier.Verify(jws).
Claims embedding pattern
Embed TokenClaims (minimal) or StandardClaims (with name/email/picture) to satisfy the Claims interface for free:
type MyClaims struct {
jwt.TokenClaims
OrgID string `json:"org_id"`
Roles []string `json:"roles"`
}
TokenClaims fields: Iss, Sub, Aud, Exp, NBf, IAt, JTI, AuthTime, Nonce, AMR, AzP, ClientID, Scope (SpaceDelimited).
StandardClaims adds: Name, GivenName, FamilyName, Email, EmailVerified, Picture, and more.
Custom JOSE headers
Embed RFCHeader in your header struct and implement SignableJWT:
type DPoPHeader struct {
jwt.RFCHeader
Nonce string `json:"nonce,omitempty"`
}
type DPoPJWT struct {
jwt.RawJWT
Header DPoPHeader
}
func (d *DPoPJWT) GetHeader() jwt.RFCHeader { return d.Header.RFCHeader }
func (d *DPoPJWT) SetHeader(hdr jwt.Header) error {
d.Header.RFCHeader = *hdr.GetRFCHeader()
data, _ := json.Marshal(d.Header)
d.Protected = []byte(base64.RawURLEncoding.EncodeToString(data))
return nil
}
func (d *DPoPJWT) SetSignature(sig []byte) { d.Signature = sig }
Then sign with signer.SignJWT(dpopJWT).
Raw JWS signing (ACME, non-JWT protocols)
SignRaw signs arbitrary headers + payload without JWT semantics. Only alg is set from the key; KID is caller-controlled.
hdr := &AcmeHeader{
URL: "https://acme.example.com/acme/new-account",
Nonce: "server-nonce",
JWK: jwkBytes,
}
raw, _ := signer.SignRaw(hdr, payloadJSON)
flatJSON, _ := json.Marshal(raw) // flattened JWS JSON
Header structs must embed jwt.RFCHeader and satisfy the Header interface (which RFCHeader provides via GetRFCHeader()). RFCHeader uses omitempty on KID and Typ so they are absent when empty.
JWKS endpoint
Signer has WellKnownJWKs. Serve it directly:
json.Marshal(&signer.WellKnownJWKs) // {"keys":[...]}
Validators
NewIDTokenValidator(iss, aud, azp, gracePeriod)-- OIDC ID tokens (checks sub, exp, iat, auth_time)NewAccessTokenValidator(iss, aud, gracePeriod)-- RFC 9068 access tokens (checks sub, exp, iat, jti, client_id)- Struct literal
&Validator{Checks: ...}withChecksbitmask for custom validation - A zero-value
ValidatorreturnsErrMisconfigured-- always use a constructor or set flags
Iss/Aud/AzP slice semantics: nil=unchecked, []string{}=misconfigured, []string{"*"}=any, []string{"x"}=must match.
Listish (JSON quirk)
Listish handles the JWT "aud" claim quirk: RFC 7519 allows it to be either a single string "x" or an array ["x","y"]. Unmarshal accepts both; Marshal outputs string for single values, array for multiple.
SpaceDelimited (trinary)
SpaceDelimited is a slice that marshals as a space-separated string in JSON. Three states: nil (absent, omitted via omitzero), SpaceDelimited{} (present empty string ""), SpaceDelimited{"a","b"} (populated "a b").
Typ header validation
Call hdr.IsAllowedTyp(errs, allowed) between Verify and Validate. Case-insensitive per RFC 7515.
Constants: DefaultTokenTyp = "JWT", AccessTokenTyp = "at+jwt" (RFC 9068).
Key types
NewPrivateKey()-- generates Ed25519 (default, recommended)PrivateKey-- holdscrypto.Signerin.Priv, JWK metadata (KID, Use, Alg, KeyOps)PublicKey-- holdscrypto.PublicKeyin.Pub, JWK metadata (KID, Use, Alg, KeyOps)- Algorithm derived from key type automatically (EdDSA, ES256, ES384, ES512, RS256)
- Type-switch on
.Pubto access raw key:*ecdsa.PublicKey,*rsa.PublicKey,ed25519.PublicKey
CLI tool (cmd/jwt)
jwt sign --key <key> [claims-json] sign claims into a compact JWT
jwt inspect [token] decode and display (with OIDC/OAuth2 discovery)
jwt verify --key <key> [token] verify signature and validate claims
jwt keygen [--alg EdDSA] generate a fresh private key (JWK)
Key sources: --key flag, JWT_PRIVATE_KEY / JWT_PRIVATE_KEY_FILE / JWT_PUBLIC_JWK env vars.
Time claims: --exp 1h, --nbf -5s, --iat +0s (relative to --time), or absolute Unix epoch.
File layout
| File | Contents |
|---|---|
jwt.go |
Interfaces (VerifiableJWT, SignableJWT), RawJWT, JWT, Header, RFCHeader, Encode/Decode |
sign.go |
Signer, SignJWT, SignRaw, key validation |
verify.go |
Verifier, NewVerifier, Verify, VerifyJWT |
validate.go |
Validator, Checks bitmask, constructors, per-claim check methods |
claims.go |
TokenClaims, StandardClaims, Claims interface |
types.go |
NullBool, Listish, SpaceDelimited, token type constants |
jwk.go |
PrivateKey, PublicKey, NewPrivateKey, JWK marshal/unmarshal, thumbprint |
errors.go |
Sentinel errors, ValidationError, GetOAuth2Error |
keyfetch/ |
KeyFetcher (lazy JWKS fetch + cache), FetchURL, FetchOIDC, FetchOAuth2 |
keyfile/ |
Load/Save keys from local files (JWK, PEM, DER) |
Examples
See examples/ for complete working code:
oidc-id-token-- standard OIDC flow with NewIDTokenValidatoroauth-access-token-- RFC 9068 access tokens with NewAccessTokenValidatorhttp-middleware-- bearer token middlewaremcp-server-auth-- MCP agent authacme-jws-- ACME JWS with SignRaw (with tests)custom-header-- reading custom JOSE header fieldsdpop-jws-- DPoP proof tokens with custom typ and noncecached-keys-- remote JWKS fetching with disk persistencemfa-validator-- auth_time + MaxAge validationrfc-claims-- standard claims usage