3 Commits

Author SHA1 Message Date
2f946d28b5
ajwt: redesign API — immutable Issuer, Signer, JWKsFetcher
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
2026-03-13 11:23:50 -06:00
3f7985317f
ajwt: implement redesigned API from REDESIGN.md
Rename ValidateParams → Validator, make Issuer immutable after construction.

Key changes:
- StandardClaims.GetStandardClaims() + StandardClaimsSource interface: any
  struct embedding StandardClaims satisfies the interface for free via
  Go's method promotion — zero boilerplate for callers
- Issuer is now immutable after construction; keys and validator are
  unexported; Params field removed
- New constructors: New, NewWithJWKs, NewWithOIDC, NewWithOAuth2
- UnsafeVerify(tokenStr string) (*JWS, error): Decode + sig verify + iss
  check; "unsafe" means exp/aud/etc. are NOT checked
- VerifyAndValidate(tokenStr, claims, now): full pipeline requiring non-nil
  Validator; fails loudly with nil Validator
- FetchJWKs(ctx, url), FetchJWKsFromOIDC(ctx, base),
  FetchJWKsFromOAuth2(ctx, base): standalone fetch functions with context
- PublicJWK.Thumbprint(): RFC 7638 SHA-256 thumbprint, canonical field
  ordering per spec (EC: crv/kty/x/y, RSA: e/kty/n, OKP: crv/kty/x)
- DecodePublicJWKsJSON: auto-populates KID from Thumbprint when absent
- Tests: 14 pass, covering VerifyAndValidate, UnsafeVerify, nil-validator
  error, all alg round trips, tampered alg, Thumbprint, auto-KID
2026-03-13 10:28:47 -06:00
1f0b36fc6d
feat(auth/ajwt): add first-principles JWT/JWS/JWK package
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.
2026-03-12 19:40:18 -06:00