Introduces code.nochebuena.dev/einherjar/auth-jwt — JWT authentication middleware and token lifecycle management for the Einherjar framework. Absorbs httpauth-jwt from micro-lib with three changes: logger parameter on AuthMiddleware, httputil.Error for consistent 401 responses, and added ECDSA support. Signers and Verifiers (one file per implementation — CT-6 compliant): - Verifier interface — Verify(tokenString string) (*jwt.Token, error) - Signer interface — extends Verifier; adds Sign(claims jwt.Claims) (string, error) - signer_hmac.go — NewHMACSigner(secret) → HS256; jwt.WithJSONNumber() on Verify - signer_rsa.go — NewRSASigner(key) + NewRSASignerFromPEM(pem) → RS256 - verifier_rsa.go — NewRSAPublicKeyVerifier + NewRSAPublicKeyVerifierFromPEM → RS256 verify-only - signer_ec.go — NewECSigner(key) + NewECSignerFromPEM(pem) → ES256/384/512; algorithm auto-detected from key curve (P-256→ES256, P-384→ES384, P-521→ES512) - verifier_ec.go — NewECPublicKeyVerifier + NewECPublicKeyVerifierFromPEM → EC verify-only Token lifecycle: - TokenConfig struct — AccessTTL, RefreshTTL, Issuer - TokenPair struct — AccessToken, RefreshToken, ExpiresIn - IssueTokenPair — access + refresh pair; customClaims merged at top level; refresh carries only sub/iss/iat/exp/jti/fam; jwt.WithJSONNumber() preserves int64 bitmasks - Blacklist interface — IsRevoked + Revoke; satisfied by cache-valkey via duck typing - ErrTokenRevoked — errors.New sentinel; errors.Is pattern for replay-attack detection - RefreshTokenPair — verifies token, checks blacklist, revokes old JTI, issues new pair HTTP middleware: - AuthMiddleware(logger, verifier, publicPaths) — verifies Bearer tokens; calls authmw.SetTokenData on success; 401 routed through httputil.Error (Warn level); publicPaths use path.Match wildcards; accepts Verifier (not Signer) to enforce narrowest-interface principle for verify-only services Compliance test (package authjwt_test) enforces CT-6 (≤1 exported TypeSpec/file), compile-time interface satisfaction, and behavioural coverage: HMAC/RSA/EC sign+verify, algorithm mismatch rejection, IssueTokenPair claims/jti/fam, RefreshTokenPair success/ revoked/blacklist-error/custom-claims, MaxInt64 json.Number precision, AuthMiddleware valid/invalid/expired/missing/public-path/wildcard/JSON-body/RSA-verifier/SetTokenData. Depends on auth v1.0.0, contracts v1.0.0, core v1.0.0, web v1.0.0, jwt/v5 v5.2.1. - identifiable.go: package-level Module variable (observability.Identifiable) for version identification — auth-jwt is a function library; not registered with the launcher
3.5 KiB
Architecture Decision Records — einherjar/auth-jwt
ADR-001: No root factory
Status: Accepted
Context: The question arose whether auth-jwt should expose a root factory
(e.g. authjwt.New(secret, cfg)) analogous to web.New.
Decision: No root factory. Callers construct Signer/Verifier directly and
wire AuthMiddleware themselves.
Rationale: HMAC vs RSA vs EC is a fundamental architectural choice that cannot
be encapsulated in a universal default. The TokenConfig, publicPaths, and
the Blacklist for refresh must all be supplied by the application. A root factory
would either accept all of these (producing a Config struct as complex as the raw API)
or make fixed choices that are wrong for some callers.
ADR-002: Logger added to AuthMiddleware
Status: Accepted
Context: The micro-lib httpauth-jwt AuthMiddleware wrote 401 responses
silently with no log emission.
Decision: AuthMiddleware accepts logging.Logger and routes all 401 responses
through httputil.Error(logger, w, r, xerrors.Unauthorized(...)).
Rationale: Consistent with auth ADR-002. 401 responses are logged at Warn level
by httputil.Error — token expiry, missing headers, and invalid signatures are all
client-visible failures that benefit from operator visibility. Silent 401s are invisible
in production; the cost of one Warn log per rejected request is negligible.
ADR-003: web dependency for httputil.Error
Status: Accepted
Context: auth-jwt error responses could be written with a local JSON helper
to avoid a dependency on web.
Decision: auth-jwt depends on einherjar/web solely for httputil.Error.
Rationale: Consistent with auth ADR-003. Error response shape must be identical
to all other API errors. auth-jwt is a transport-layer package; a web dependency
is expected and correct. Duplicating the error writer in auth-jwt would create
divergence over time.
ADR-004: ErrTokenRevoked as plain sentinel
Status: Accepted
Context: RefreshTokenPair could return a *xerrors.Err with code
ErrUnauthenticated instead of a plain errors.New sentinel.
Decision: ErrTokenRevoked = errors.New("token revoked") — idiomatic Go sentinel.
Rationale: errors.Is(err, authjwt.ErrTokenRevoked) is how callers distinguish
replay attacks from infrastructure failures. A plain sentinel keeps this check simple
and allocation-free. Using xerrors.Unauthorized(...) returns a pointer value whose
errors.Is semantics require an Is() method implementation. The sentinel pattern
(as used for io.EOF, sql.ErrNoRows) is the established Go idiom for this case.
The HTTP 401 response is the caller's responsibility, not RefreshTokenPair's.
ADR-005: EC algorithm auto-detected from key curve
Status: Accepted
Context: ECDSA supports three standardized curves (P-256, P-384, P-521) mapped to three JWT algorithms (ES256, ES384, ES512). The API could require callers to specify the algorithm explicitly.
Decision: NewECSigner detects the algorithm from key.Curve.Params().Name:
P-256→ES256, P-384→ES384, P-521→ES512. Default (unknown curve) is ES256.
Rationale: The algorithm is determined by the key — there is no valid scenario
where a P-256 key should sign with ES384. Requiring the caller to pass it explicitly
would create a new class of misconfiguration errors. Auto-detection eliminates that
class entirely. The same logic is applied in ecSigner.Verify to validate the
incoming token's algorithm header.