feat(middleware): add opt-in local JWT signature verification#128
Conversation
The general authorization path parsed the bearer token with ParseUnverified and trusted its claims (type/owner/sub) to build the authz subject, relying solely on the network POST to /v1/authorize as the trust anchor. The M2M gate already verified tokens properly (RS256-pinned, expiry required, optional issuer) against a static injected cert. Converge on that hardened verifier instead of adding a second (JWKS-based) mechanism: extract m2m.go's crypto core into one package-level verifyToken used by BOTH RequireM2M and checkAuthorization. Hooking it at checkAuthorization covers Fiber and both gRPC interceptors from one audited place. Verification is opt-in and gated by presence of a cert, mirroring the M2M cert gate: - AUTH_JWT_VERIFY_CERT: newline-joined PEM bundle (or AUTH_JWT_VERIFY_CERT_PATH for a mounted file). Multiple certs enable zero-downtime key rotation — a token verified by ANY listed key is accepted (static-keyset-with-overlap). - AUTH_JWT_ISSUER: optional "iss" pin. When no cert is configured the historical ParseUnverified behavior is preserved exactly (backward compatible). When configured, the token is verified (signature/alg/exp/iss) before its claims are used; any failure denies with 401 (fail closed). A configured-but-unparseable cert is logged loud at ERROR and leaves verification disabled, never silently accepted, keeping NewAuthClient's no-error signature. Closes #106 Claude-Session: https://claude.ai/code/session_01RcQLK4qK1kbpvXWjuAR3Xh
Unify opt-in local JWT signature verification (#106) with the authz resilience layer (#108: decision cache, circuit breaker, bounded retry) that merged to develop first. Both features survive. checkAuthorization now: opens a per-request context deadline (#108), extracts claims via extractClaims (#106) — verify-then-parse when a verify cert is configured (RS256/exp/iss), ParseUnverified when unset — BEFORE the claims are trusted, then resolves the decision through the cache/breaker/retry layer whose fallbacks all deny (fail closed). Both the Fiber and gRPC paths route through this one function. AuthClient keeps both field sets (verifyKeys/verifyIssuer + Required/timeout/cache/breaker/retryMax); NewAuthClient wires both env configs; go.mod keeps gobreaker + backoff as direct deps. Claude-Session: https://claude.ai/code/session_01RcQLK4qK1kbpvXWjuAR3Xh
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughJWT authorization now supports optional local RS256 verification using inline or file-based PEM keys, issuer validation, and key rotation. Authorization and M2M flows use shared verification logic, while tests cover validation, configuration, integration, and fallback behavior. ChangesJWT Verification
Sequence Diagram(s)sequenceDiagram
participant Request
participant checkAuthorization
participant extractClaims
participant verifyToken
participant AuthBackend
Request->>checkAuthorization: Submit bearer token
checkAuthorization->>extractClaims: Extract authorization claims
extractClaims->>verifyToken: Verify token with configured RSA keys
verifyToken-->>extractClaims: Claims or HTTP 401
alt verified
extractClaims-->>checkAuthorization: Valid claims
checkAuthorization->>AuthBackend: Check authorization
else rejected
extractClaims-->>checkAuthorization: Deny request
end
Possibly related PRs
✨ Finishing Touches✨ Simplify code
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@auth/middleware/verification.go`:
- Around line 140-146: Update the parse-failure log in the parseRSAPublicKeys
error branch to use source-neutral wording instead of naming
AUTH_JWT_VERIFY_CERT, so failures from either certificate configuration source
are described accurately. Preserve the existing error details and return
behavior.
- Around line 156-158: Update the inline certificate handling in the
verification middleware to trim surrounding whitespace from AUTH_JWT_VERIFY_CERT
before checking whether it is empty and returning it. Preserve precedence for a
non-empty inline certificate so whitespace-only values fall through to the
existing AUTH_JWT_VERIFY_CERT_PATH handling.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 45c3cb3f-562e-456f-b191-0f4563a97ac9
📒 Files selected for processing (5)
README.mdauth/middleware/m2m.goauth/middleware/middleware.goauth/middleware/verification.goauth/middleware/verification_test.go
…Y_CERT Reword the cert-parse failure log to be source-neutral since the PEM data can come from either AUTH_JWT_VERIFY_CERT or AUTH_JWT_VERIFY_CERT_PATH. Trim surrounding whitespace on the inline AUTH_JWT_VERIFY_CERT value before PEM parsing so a stray trailing newline no longer breaks cert parsing, mirroring the existing AUTH_JWT_VERIFY_CERT_PATH handling. Claude-Session: https://claude.ai/code/session_01RcQLK4qK1kbpvXWjuAR3Xh
|
🎉 This PR is included in version 3.0.0-beta.8 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
## [3.0.0](v2.9.0...v3.0.0) (2026-07-22) ### ⚠ BREAKING CHANGES * **middleware:** module path is now github.com/LerianStudio/lib-auth/v3 and the Authorize middleware requires Fiber v3. X-Lerian-Ref: 0x1 ### Features * **middleware:** add authz cache, circuit breaker, and bounded retry ([c0469e6](c0469e6)), closes [#107](#107) [#127](#127) [#127](#127) [#108](#108) * **middleware:** add fail-closed AUTH_REQUIRED option ([f7fff6c](f7fff6c)), closes [#107](#107) * **middleware:** add M2M authentication gate ([2034198](2034198)) * **middleware:** add opt-in local JWT signature verification ([#128](#128)) ([48b81a3](48b81a3)), closes [#106](#106) * **middleware:** add optional issuer pinning to M2M gate ([5afd305](5afd305)) * **middleware:** expose M2M identity via request context ([c48393a](c48393a)), closes [#125](#125) * **middleware:** forward product on M2M auth (flag-gated) ([93c44db](93c44db)) * **middleware:** migrate to Fiber v3, cut /v3 major ([024a909](024a909)) * real M2M subject and token type whitelist ([e31ac53](e31ac53)) ### Bug Fixes * **middleware:** return zero M2MIdentity when reporting absent ([25fc4ec](25fc4ec)), closes [#126](#126)
|
🎉 This PR is included in version 3.0.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
What
Adds opt-in local JWT signature verification on the general authorization path, converging on the M2M gate's existing hardened RS256 verifier instead of introducing a second (JWKS-based) mechanism.
m2m.go's crypto core into one package-levelverifyToken(pubKeys, issuer, token)— RS256-pinned (WithValidMethods, closing alg substitution),WithExpirationRequired, optionalWithIssuer.RequireM2M(viaM2MAuthenticator.verify) and the generalcheckAuthorizationpath. Hooked atcheckAuthorization, so it covers Fiber + both gRPC interceptors from one audited place.Why
The general path used
ParseUnverifiedand trusted the token'stype/owner/subclaims to build the authz subject, with the networkPOST /v1/authorizeas the only trust anchor. This adds defense-in-depth: a forged token is rejected before its claims are ever used.Config (opt-in; gated by presence, like the M2M cert)
AUTH_JWT_VERIFY_CERT— newline-joined PEM bundle (PKIX pubkey or X.509 cert). Multiple certs → zero-downtime key rotation: a token verified by ANY listed key is accepted (static-keyset-with-overlap).AUTH_JWT_VERIFY_CERT_PATH— mounted PEM file (used only whenAUTH_JWT_VERIFY_CERTis empty).AUTH_JWT_ISSUER— optionalisspin.Behavior
ParseUnverifiedbehavior, byte-for-byte unchanged (backward compatible; all 112 pre-existing tests still pass).NewAuthClient's no-error signature. See "Design note" below.Tests
30 new tests: valid signed passes; wrong-key/forged → 401; expired → 401; missing-exp → 401; HS256/none alg-confusion → 401; issuer match/mismatch; multi-cert rotation bundle accepts either key, rejects a stranger; forged/expired tokens denied before the authz backend is reached; unset-cert path unchanged;
NewAuthClientenv wiring (inline, bundle, path, bad-cert-disables, precedence).Design note for reviewer
The design left the bad-cert constructor posture open (erroring constructor vs log-loud-and-disable). This PR implements log-loud-and-disable within the existing no-error
NewAuthClient— non-breaking for all consumers, and "not silent" (ERROR log). Since verification is env-gated, adopting it needs no consumer code change. If you want strict fail-closed-on-bad-cert at startup, that composes naturally with #107'sAUTH_REQUIREDand can be a follow-up.One
#nosec G304 G703sits on theAUTH_JWT_VERIFY_CERT_PATHfile read: the path is operator-supplied deployment config (a mounted secret), not request/user input..golangci.ymlalready excludes this G304 class repo-wide; the annotation covers the standalone GoSec job too.Verification
go build,go vet,golangci-lint run ./..., andgosec ./...all clean locally; 142 tests pass.Closes #106
https://claude.ai/code/session_01RcQLK4qK1kbpvXWjuAR3Xh