Skip to content

feat(middleware): add opt-in local JWT signature verification#128

Merged
fredcamaral merged 3 commits into
developfrom
feat/local-jwt-verification-106
Jul 21, 2026
Merged

feat(middleware): add opt-in local JWT signature verification#128
fredcamaral merged 3 commits into
developfrom
feat/local-jwt-verification-106

Conversation

@fredcamaral

Copy link
Copy Markdown
Member

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.

  • Extracts m2m.go's crypto core into one package-level verifyToken(pubKeys, issuer, token) — RS256-pinned (WithValidMethods, closing alg substitution), WithExpirationRequired, optional WithIssuer.
  • Used by both RequireM2M (via M2MAuthenticator.verify) and the general checkAuthorization path. Hooked at checkAuthorization, so it covers Fiber + both gRPC interceptors from one audited place.
  • REJECTS JWKS per the approved design: JWKS reintroduces the hot-path network coupling the M2M design deliberately avoided and that Add authz decision cache, circuit breaker, and bounded retry #108 exists to remove. Rotation is handled by a multi-cert bundle instead.

Why

The general path used ParseUnverified and trusted the token's type/owner/sub claims to build the authz subject, with the network POST /v1/authorize as 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 when AUTH_JWT_VERIFY_CERT is empty).
  • AUTH_JWT_ISSUER — optional iss pin.

Behavior

  • Unset cert → historical ParseUnverified behavior, byte-for-byte unchanged (backward compatible; all 112 pre-existing tests still pass).
  • Configured → verify signature/alg/exp/iss BEFORE claim extraction; any failure denies with 401, fail closed.
  • Configured-but-unparseable cert → logged loud at ERROR, verification left disabled (never silently accepted), keeping 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; NewAuthClient env 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's AUTH_REQUIRED and can be a follow-up.

One #nosec G304 G703 sits on the AUTH_JWT_VERIFY_CERT_PATH file read: the path is operator-supplied deployment config (a mounted secret), not request/user input. .golangci.yml already excludes this G304 class repo-wide; the annotation covers the standalone GoSec job too.

Verification

go build, go vet, golangci-lint run ./..., and gosec ./... all clean locally; 142 tests pass.

Closes #106

https://claude.ai/code/session_01RcQLK4qK1kbpvXWjuAR3Xh

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
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b63ad024-60a5-4210-b303-ca8d0c7ad851

📥 Commits

Reviewing files that changed from the base of the PR and between d13faf1 and 6ecb0d9.

📒 Files selected for processing (1)
  • auth/middleware/verification.go

📝 Walkthrough

Walkthrough

JWT 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.

Changes

JWT Verification

Layer / File(s) Summary
Verification and key parsing
auth/middleware/verification.go, auth/middleware/verification_test.go
Adds RS256 verification with required expiry, optional issuer checks, multi-key support, and parsing tests.
Configuration and authorization wiring
auth/middleware/middleware.go, auth/middleware/verification.go, auth/middleware/verification_test.go, README.md
Loads verification settings during client construction, applies them during authorization, tests fallback and failure behavior, and documents the environment variables.
M2M verifier integration
auth/middleware/m2m.go
Routes M2M token validation through the shared verifier while preserving token type and subject checks.

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
Loading

Possibly related PRs

✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch feat/local-jwt-verification-106

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 64dd89c and d13faf1.

📒 Files selected for processing (5)
  • README.md
  • auth/middleware/m2m.go
  • auth/middleware/middleware.go
  • auth/middleware/verification.go
  • auth/middleware/verification_test.go

Comment thread auth/middleware/verification.go
Comment thread auth/middleware/verification.go Outdated
…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
@fredcamaral
fredcamaral merged commit 48b81a3 into develop Jul 21, 2026
3 checks passed
@fredcamaral
fredcamaral deleted the feat/local-jwt-verification-106 branch July 21, 2026 16:20
lerian-studio-midaz-push-bot Bot pushed a commit that referenced this pull request Jul 21, 2026
## [3.0.0-beta.8](v3.0.0-beta.7...v3.0.0-beta.8) (2026-07-21)

### Features

* **middleware:** add opt-in local JWT signature verification ([#128](#128)) ([48b81a3](48b81a3)), closes [#106](#106)
@lerian-studio-midaz-push-bot

Copy link
Copy Markdown

🎉 This PR is included in version 3.0.0-beta.8 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

lerian-studio-midaz-push-bot Bot pushed a commit that referenced this pull request Jul 22, 2026
## [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)
@lerian-studio-midaz-push-bot

Copy link
Copy Markdown

🎉 This PR is included in version 3.0.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant