Key changes from previous design:
- Issuer is now immutable after construction (no mutex, no SetKeys)
- New(keys []PublicJWK) — no issURL or Validator baked in
- Verify returns (nil, err) on any failure; UnsafeVerify returns (*JWS, err)
even on sig failure so callers can inspect kid/iss for multi-issuer routing
- VerifyAndValidate takes ClaimsValidator per-call instead of baking it into
the Issuer; soft errors in errs, hard errors in err, nil sentinel discarded
- ClaimsValidator interface implemented by *Validator and *MultiValidator
- MultiValidator: []string for iss, aud, azp (multi-tenant)
- Signer: round-robin across NamedSigner keys via atomic.Uint64; auto-KID
from RFC 7638 thumbprint; Issuer() returns *Issuer with signer's public keys
- JWKsFetcher: lazy, no background goroutine; Issuer(ctx) checks freshness
per call and creates new *Issuer on cache miss; KeepOnError + StaleAge for
serving stale keys on fetch failure
- pub.go: add EncodePublicJWK and MarshalPublicJWKs (encode counterparts)
- Remove NewWithJWKs, NewWithOIDC, NewWithOAuth2 constructors from Issuer
Design goals from first principles:
- JWS holds only parsed structure (header, payload, sig) — no Claims
interface, no Verified flag. Removes footguns from the simpler packages.
- Issuer owns key management and verification. Verify does key lookup by
kid, sig verification, and iss claim check — in that order, so sig is
always authenticated before any payload data is trusted.
- ValidateParams is a stable config object with Validate(StandardClaims,
time.Time) as a method. Time is passed at the call site, not stored in
the params struct, so the same config object can be reused across requests.
- UnmarshalClaims(v any) accepts any type — no Claims interface to
implement. Custom validation is a plain function call, not a method
satisfying an interface.
- Sign uses crypto.Signer, supporting ES256/ES384/ES512 (ECDSA), RS256
(RSA PKCS#1 v1.5), and EdDSA (Ed25519, RFC 8037).
- PublicJWK uses crypto.PublicKey (not generics) since JWKS returns
heterogeneous key types at runtime. Typed accessors ECDSA(), RSA(), and
EdDSA() replace TypedKeys[K] filtering.
- JWKS parsing handles kty: "EC", "RSA", and "OKP" (Ed25519).
10 tests: ES256/RS256/EdDSA round trips, custom validation, wrong key,
unknown kid, iss mismatch, tampered alg, PublicJWK accessors, JWKS JSON.