Skip to content

feat(middleware)!: release v3 — migrate to Fiber v3, cut /v3 major#131

Merged
rodrigodh merged 30 commits into
mainfrom
develop
Jul 22, 2026
Merged

feat(middleware)!: release v3 — migrate to Fiber v3, cut /v3 major#131
rodrigodh merged 30 commits into
mainfrom
develop

Conversation

@rodrigodh

Copy link
Copy Markdown
Contributor

Description

Release of the v3 major to main (stable). Migrates to Fiber v3, cuts the /v3 module path (github.com/LerianStudio/lib-auth/v3), and depends on the stable lib-observability/v2 v2.0.0 + lib-commons/v6 v6.0.0.

Expected release: v2.9.0 → v3.0.0 (single major bump; breaking commit 024a909 feat(middleware)! present in range; .releaserc.yml maps breaking→major).

Type of Change

  • BREAKING CHANGE
  • feat / build

Breaking Changes

  • Module path is now /v3 — imports become github.com/LerianStudio/lib-auth/v3/...; run go mod tidy.
  • Fiber v3 — HTTP middleware targets gofiber/fiber/v3; consumers on Fiber v2 must migrate.

Note

Merge the stable-deps bump into develop first so this release carries observability v2.0.0 + commons v6.0.0.

rodrigodh and others added 28 commits July 15, 2026 16:10
Bump gofiber/fiber v2->v3.4.0 and cut the /v3 module major. Consume the
published betas lib-commons/v6 v6.0.0-beta.2 and lib-observability/v2
v2.0.0-beta.1 (no local replace). Authorize's handler is now func(c fiber.Ctx)
(interface by value) and uses c.Context(); merged with develop's product-param
rename. Bump golang.org/x/text to v0.39.0 (CVE-2026-56852).

BREAKING CHANGE: module path is now github.com/LerianStudio/lib-auth/v3 and the
Authorize middleware requires Fiber v3.

X-Lerian-Ref: 0x1
Fiber v3 pulls golang.org/x/crypto via acme/autocert; the openpgp advisory
has no fixed version and the package is not imported.

X-Lerian-Ref: 0x1
go get -u upgrades all of the consumer's dependencies; pin to @latest so
the install snippet only affects lib-auth. (CodeRabbit #121)

X-Lerian-Ref: 0x1
feat(middleware)!: migrate to Fiber v3, cut /v3 major
## [3.0.0-beta.1](v2.9.0...v3.0.0-beta.1) (2026-07-15)

### ⚠ 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:** migrate to Fiber v3, cut /v3 major ([024a909](024a909))
deriveSubject now switches on the token type. application (M2M)
tokens use their real sub claim instead of the fabricated
admin/{product}-editor-role; the enforcer ignores that subject and
resolves roles dynamically. Any type outside {normal-user,
application}, including empty or absent, fails closed with 401 before
the authorize call, closing the asymmetry where an empty type fell
into the M2M path. normal-user is unchanged; product is forwarded
only for normal-user flows. gRPC telemetry mirrors the request body.

Part of Item 17 (Tier A), Phase 1.

X-Lerian-Ref: 0x1
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat: real M2M subject and token type whitelist
## [3.0.0-beta.2](v3.0.0-beta.1...v3.0.0-beta.2) (2026-07-16)

### Features

* real M2M subject and token type whitelist ([e31ac53](e31ac53))
M2M (application-token) authorization calls sent no product, so once Custom permission resources are prefixed with "{product}/" (Item 17 F2), a bare M2M request stops matching the stored resource and the auth service denies it (403).

Forward the route product for application tokens too, gated by AUTH_M2M_PRODUCT_FORWARD_ENABLED (default off) so deploy != release: prefix isolation activates by flipping the flag, with no consumer code change. Normal-user behavior is unchanged. A shared shouldForwardProduct helper keeps the request body and telemetry payload in lockstep.

X-Lerian-Ref: 0x1
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rward

feat(middleware): forward product on M2M auth (flag-gated)
## [3.0.0-beta.3](v3.0.0-beta.2...v3.0.0-beta.3) (2026-07-17)

### Features

* **middleware:** forward product on M2M auth (flag-gated) ([93c44db](93c44db))
Add M2MAuthenticator, a Fiber v3 middleware (RequireM2M) that
authenticates machine-to-machine callers by verifying a Casdoor-issued
RS256 application token offline against an injected RSA public key (the
issuer certificate), so any service can reuse it by supplying its own
cert. No network round-trip and no Casdoor SDK dependency.

It authenticates only: RS256 is pinned (closing none/HS256 alg
substitution), expiry is required, and the token type must be
"application". It does not authorize and does not bind the caller to a
target slug; that ownership check is intentionally out of scope, with
possession of a valid, manually-provisioned M2M token as the trust
boundary. The authenticated identity (sub, azp) is exposed to downstream
handlers via M2MSubjectFromContext / M2MClientIDFromContext. Every
failure path fails closed (401/403); when disabled the middleware is a
pass-through, and an enabled authenticator requires a parseable cert at
construction.

X-Lerian-Ref: 0x1
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The M2M gate verified the RS256 signature but ignored the "iss"
claim, so any application token signed by the same key passed regardless
of issuer. This is safe in a single-issuer deployment but becomes
cross-issuer token reuse if one signing key ever serves multiple
issuers/orgs.

NewM2MAuthenticator now takes an expectedIssuer: when non-empty it pins
the token issuer via jwt.WithIssuer; empty keeps the prior behavior
(no issuer check) for single-issuer/dev setups. Audience is deliberately
not pinned here — a Casdoor client_credentials token carries the caller's
own clientId as aud, so pinning specific audiences would be the app<->slug
ownership binding that is intentionally out of scope (Option B).

X-Lerian-Ref: 0x1
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eware

feat(middleware): add M2M authentication gate
## [3.0.0-beta.4](v3.0.0-beta.3...v3.0.0-beta.4) (2026-07-20)

### Features

* **middleware:** add M2M authentication gate ([2034198](2034198))
* **middleware:** add optional issuer pinning to M2M gate ([5afd305](5afd305))
RequireM2M now stores the authenticated application identity on the request
Go context (context.Context) via c.SetContext, in addition to setting the
existing span attributes. Downstream consumers read it with a single
framework-agnostic accessor, M2MIdentityFromContext(ctx), which works for
fiber-native handlers (c.Context()), humafiber-derived Huma handlers, and gRPC
alike.

Motivation: a consumer (plugin-access-manager) registers a Huma operation via
humafiber, whose handler receives a Go context derived from Fiber's c.Context().
The previous Fiber-Locals-only accessors (M2MSubjectFromContext /
M2MClientIDFromContext, keyed by c.Locals) could not be reached cleanly from a
Huma handler. Propagating the identity on the Go context resolves this with one
typed accessor.

The identity is added onto c.Context() (not the tracing/span context), so this
change carries only the identity value and does not alter span topology.

The Fiber-Locals accessors were introduced in this same beta line (3.0.0-beta.x,
PR #125) and have no production consumers, so they are removed in favor of the
single M2MIdentityFromContext(ctx) API. A fiber-native caller reads the identity
via M2MIdentityFromContext(c.Context()). A typed, unexported context key avoids
string-key collisions.

Verification logic (RS256/issuer/expiry/type/fail-closed) is unchanged.

X-Lerian-Ref: 0x1
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
M2MIdentityFromContext returned the stored (possibly half-populated) value alongside ok==false when the subject was empty, contradicting the documented (zero, false) contract. Return M2MIdentity{} on the absent/empty-subject path so a caller that ignores ok cannot observe partial identity data. Addresses a CodeRabbit review comment on PR #126.

X-Lerian-Ref: 0x1
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(middleware): expose M2M identity via request context
## [3.0.0-beta.5](v3.0.0-beta.4...v3.0.0-beta.5) (2026-07-20)

### Features

* **middleware:** expose M2M identity via request context ([c48393a](c48393a)), closes [#125](#125)

### Bug Fixes

* **middleware:** return zero M2MIdentity when reporting absent ([25fc4ec](25fc4ec)), closes [#126](#126)
Adds an opt-in AUTH_REQUIRED switch (AuthClient.Required, read from the
AUTH_REQUIRED env var at construction) that inverts the default fail-open
posture. When set and auth is disabled or misconfigured (!Enabled ||
Address == ""), every protected route refuses to serve — HTTP 503 on the
Fiber path and gRPC codes.Unavailable on the unary and stream interceptors —
instead of silently passing the request through unauthenticated. A loud
error is also logged at construction so the misconfiguration is visible.

Default (false) preserves the prior pass-through behavior exactly, so this
is backward-compatible. Enforcement lives at the handler/interceptor rather
than as a constructor error, to avoid a breaking change to NewAuthClient's
existing (non-erroring) signature.

The tenant-claim propagation duplicated across the two gRPC interceptors is
extracted into a shared tenantContext helper, keeping cognitive complexity
under the linter threshold after the added fail-closed branch.

Closes #107

Claude-Session: https://claude.ai/code/session_01RcQLK4qK1kbpvXWjuAR3Xh
Every authorized request made a synchronous POST /v1/authorize with no cache,
breaker, or retry, so an authz-service blip or outage amplified straight into
the request path. The call was also built with http.NewRequest (no context),
so a caller's deadline/cancellation never reached it.

Add three opt-in resilience layers (all default-off, so default behavior is
byte-for-byte unchanged) plus the context fix, composed as
breaker(retry(httpCall(ctx))) with the cache checked before the breaker:

- Prereq fix: build the authz request with http.NewRequestWithContext, and wrap
  each call in context.WithTimeout(ctx, AUTH_TIMEOUT) (default 30s =
  behavior-neutral). The caller's cancellation now aborts the call, and the
  deadline caps the retry budget.
- TTL decision cache (AUTH_CACHE_TTL > 0) keyed by (sub, resource, action,
  product) — never the raw token. Bounded, sharded, no-dependency map with lazy
  expiry (no background goroutine to leak). get never returns an expired entry.
  Documented staleness/revocation-lag window = TTL.
- Circuit breaker (AUTH_BREAKER_ENABLED, sony/gobreaker promoted to direct). On
  open it serves ONLY a fresh positive cache hit (checked before the breaker),
  otherwise denies — never a stale grant.
- Bounded retry (AUTH_RETRY_MAX, cenkalti/backoff/v5) on TRANSIENT failures only
  (network/timeout/5xx); authoritative 401/403 are never retried.

Every fallback — breaker open, retry exhausted, timeout — denies (fail closed,
403 / PermissionDenied). This complements #107: misconfig at construction ->
503 (never mounts open); runtime outage -> deny. Two triggers, both fail-closed.

Stacks on #107 (PR #127): built on its Required fail-closed flag. Must merge
after #127; rebase onto develop and retarget base to develop once #127 lands.

Closes #108

Claude-Session: https://claude.ai/code/session_01RcQLK4qK1kbpvXWjuAR3Xh
…sed-107

feat(middleware): add fail-closed AUTH_REQUIRED option
## [3.0.0-beta.6](v3.0.0-beta.5...v3.0.0-beta.6) (2026-07-21)

### Features

* **middleware:** add fail-closed AUTH_REQUIRED option ([f7fff6c](f7fff6c)), closes [#107](#107)
feat(middleware): add authz cache, circuit breaker, and bounded retry
## [3.0.0-beta.7](v3.0.0-beta.6...v3.0.0-beta.7) (2026-07-21)

### Features

* **middleware:** add authz cache, circuit breaker, and bounded retry ([c0469e6](c0469e6)), closes [#107](#107) [#127](#127) [#127](#127) [#108](#108)
* feat(middleware): add opt-in local JWT signature verification

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

* refactor(middleware): source-neutral cert error + trim AUTH_JWT_VERIFY_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
## [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)
… v6.0.0

Pin the observability and commons dependencies to their stable v2.0.0/v6.0.0
releases (were v2.0.0-beta.1 / v6.0.0-beta.2). No code changes; build passes.

X-Lerian-Ref: 0x1
@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: 4b873dfb-01cc-4f25-b4d2-d85825f36ee2

📥 Commits

Reviewing files that changed from the base of the PR and between 467caac and 469b81a.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (2)
  • CHANGELOG.md
  • go.mod

📝 Walkthrough

Walkthrough

Changes

Auth v3 middleware

Layer / File(s) Summary
Module migration and configuration documentation
go.mod, README.md, CHANGELOG.md, .trivyignore
Moves the module and middleware examples to v3/Fiber v3, documents configuration and releases, and records the Trivy exception.
M2M authentication gate
auth/middleware/m2m.go, auth/middleware/m2m_test.go
Adds RS256-verified M2M authentication, issuer and claim validation, context identity propagation, and tests.
JWT verification and authorization controls
auth/middleware/middleware.go, auth/middleware/verification.go, auth/middleware/middlewareGRPC.go, auth/middleware/*_test.go
Adds local JWT verification, claim validation, fail-closed behavior, product forwarding, tenant propagation, and Fiber/gRPC coverage.
Authorization caching and resilience
auth/middleware/decisioncache.go, auth/middleware/resilience.go, auth/middleware/resilience_test.go
Adds bounded TTL caching, timeouts, retries, circuit breaking, response classification, and environment-based configuration.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant AuthMiddleware
  participant JWTVerifier
  participant AuthorizationService
  Client->>AuthMiddleware: Request with JWT
  AuthMiddleware->>JWTVerifier: Extract and optionally verify claims
  JWTVerifier-->>AuthMiddleware: Claims or unauthorized
  AuthMiddleware->>AuthorizationService: POST authorization payload
  AuthorizationService-->>AuthMiddleware: Authorization decision
  AuthMiddleware-->>Client: Continue or authorization response
Loading

Possibly related PRs

✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch develop

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: 3

🤖 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 `@CHANGELOG.md`:
- Around line 1-8: Align all release artifacts for the intended v3.0.0 release:
update CHANGELOG.md lines 1-8 with the final 3.0.0 entry and revise lines 56-68
so the progression starts from beta.8; replace beta internal dependencies in
go.mod lines 22-23 with stable versions; and update README.md line 10 to use the
stable `@latest` installation only after publishing that tag, otherwise pin the
intended beta explicitly.

In `@README.md`:
- Around line 54-65: Update the authorization resilience documentation near
AUTH_CACHE_TTL to explicitly state that fresh positive decision-cache hits are
the exception to the fail-closed fallback rule and may be served during
authorization outages. Preserve the existing explanation of revocation lag and
availability tradeoffs.
- Around line 29-41: Update the JWT verification configuration flow described in
the README so malformed AUTH_JWT_VERIFY_CERT or AUTH_JWT_VERIFY_CERT_PATH
configuration cannot disable verifyKeys and fall back to ParseUnverified. Ensure
invalid configured keys fail closed with unauthorized requests, or require an
explicit, clearly monitored opt-out before allowing unverified parsing; preserve
the existing optional behavior only when verification is genuinely unset.
🪄 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: 87c0b8ea-37e8-4f7e-8c6f-fe202f1fc45b

📥 Commits

Reviewing files that changed from the base of the PR and between 2c2cf15 and 467caac.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (15)
  • .trivyignore
  • CHANGELOG.md
  • README.md
  • auth/middleware/decisioncache.go
  • auth/middleware/m2m.go
  • auth/middleware/m2m_test.go
  • auth/middleware/middleware.go
  • auth/middleware/middlewareGRPC.go
  • auth/middleware/middlewareGRPC_test.go
  • auth/middleware/middleware_test.go
  • auth/middleware/resilience.go
  • auth/middleware/resilience_test.go
  • auth/middleware/verification.go
  • auth/middleware/verification_test.go
  • go.mod

Comment thread CHANGELOG.md
Comment thread README.md
Comment thread README.md
chore(deps): point to stable observability v2.0.0 and commons v6.0.0
@lerian-studio-midaz-push-bot

Copy link
Copy Markdown

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

The release is available on GitHub release

Your semantic-release bot 📦🚀

@rodrigodh
rodrigodh merged commit f00d53e into main Jul 22, 2026
3 checks passed
@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.

4 participants