Skip to content

feat(middleware): add fail-closed AUTH_REQUIRED option - #127

Merged
fredcamaral merged 1 commit into
developfrom
feat/auth-required-fail-closed-107
Jul 21, 2026
Merged

feat(middleware): add fail-closed AUTH_REQUIRED option#127
fredcamaral merged 1 commit into
developfrom
feat/auth-required-fail-closed-107

Conversation

@fredcamaral

Copy link
Copy Markdown
Member

What

Adds an opt-in fail-closed switch to the auth middleware so a
disabled/misconfigured client refuses to serve instead of silently passing every
request through unauthenticated.

  • New AuthClient.Required bool field, read once from the AUTH_REQUIRED env var
    at construction (mirrors the existing AUTH_M2M_PRODUCT_FORWARD_ENABLED wiring).
  • When Required == true and auth cannot authorize (!Enabled || Address == ""),
    every protected route refuses to serve:
    • Fiber AuthorizeHTTP 503 Service Unavailable
    • gRPC unary & stream interceptors → codes.Unavailable
  • A loud error is logged at construction when AUTH_REQUIRED is set but auth is
    disabled/blank, so the misconfiguration is visible immediately rather than at
    first request.
  • Default Required == false preserves the prior pass-through behavior exactly.

Why

Per #107, the middleware fails open: if !auth.Enabled || auth.Address == ""
returns c.Next() (and the gRPC equivalents pass through). One missing/typo'd env
var silently downgrades a protected service to fully open, with no error, no boot
failure. Downstream (midaz) compensates with N hand-rolled per-service boot gates
that drift independently; the fail-closed posture belongs upstream in one place.

Design decision: handler-level refusal, not a constructor error

The issue suggested "an error from the constructor / a hard 503 from the handler."
I chose the 503 / codes.Unavailable at the handler, deliberately:

  • NewAuthClient currently returns *AuthClient with no error. Retrofitting a
    (*AuthClient, error) signature is a breaking API change to every call site
    (README examples and consumers call it positionally) — which directly contradicts
    the "opt-in, backward-compatible" requirement. NewM2MAuthenticator returning an
    error is not a precedent to match here; it was born with that signature.
  • Handler-level refusal is genuinely fail-closed: with Required=true and a
    misconfig, no route ever serves unauthenticated — it returns 503 on every request
    (loud, not silent). The added boot-time error log gives operators the deploy-time
    signal without a signature break.

Scope note

NewGRPCAuthUnaryPolicy was already at the repo's cognitive-complexity ceiling on
develop. To add the fail-closed branch without tripping the linter or resorting to
a //nolint, the tenant-claim propagation block that was duplicated verbatim in
both gRPC interceptors is extracted into a shared tenantContext helper. Existing
tenant-propagation tests cover the extraction (no behavior change).

Tests

Added, all passing (full package: 124 tests green; golangci-lint run ./... clean):

  • TestNewAuthClient_ReadsRequiredFlagAUTH_REQUIRED env → Required field
    (true / absent / non-true).
  • TestAuthorize_FailClosed (Fiber, via app.Test): required+disabled → 503;
    required+blank-address → 503; not-required+disabled → passes through (unchanged);
    required+enabled → normal authorize flow.
  • TestNewGRPCAuthUnaryPolicy / TestNewGRPCAuthStreamPolicy: required+disabled and
    required+blank-address → codes.Unavailable, handler not invoked.

Closes #107

https://claude.ai/code/session_01RcQLK4qK1kbpvXWjuAR3Xh

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
@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: d60089a5-276b-4dbc-ba1a-000450ad2ba7

📥 Commits

Reviewing files that changed from the base of the PR and between 9feddbb and f7fff6c.

📒 Files selected for processing (5)
  • README.md
  • auth/middleware/middleware.go
  • auth/middleware/middlewareGRPC.go
  • auth/middleware/middlewareGRPC_test.go
  • auth/middleware/middleware_test.go

📝 Walkthrough

Walkthrough

Changes

Required authentication enforcement

Layer / File(s) Summary
HTTP required-auth gating
auth/middleware/middleware.go, auth/middleware/middleware_test.go, README.md
AUTH_REQUIRED populates AuthClient.Required; unusable required authentication returns HTTP 503, while the default remains fail-open. Tests cover environment parsing and request outcomes, and the README documents the behavior.
gRPC enforcement and tenant context
auth/middleware/middlewareGRPC.go, auth/middleware/middlewareGRPC_test.go
Unary and stream interceptors return Unavailable when required authentication is unavailable. Tenant metadata propagation now uses shared context handling and wrapped streams where needed.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant AuthMiddleware
  participant AuthService
  participant ProtectedHandler
  Client->>AuthMiddleware: protected request or RPC
  AuthMiddleware->>AuthService: authorize when available
  alt required auth unavailable
    AuthMiddleware-->>Client: HTTP 503 or gRPC Unavailable
  else authorized
    AuthMiddleware->>ProtectedHandler: forward request with auth context
    ProtectedHandler-->>Client: response
  end
Loading

Possibly related PRs

  • LerianStudio/lib-auth#121: Modifies the same Fiber Authorize middleware while migrating tracing context extraction to Fiber v3.
✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch feat/auth-required-fail-closed-107

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

@fredcamaral
fredcamaral merged commit 7474d61 into develop Jul 21, 2026
3 checks passed
@fredcamaral
fredcamaral deleted the feat/auth-required-fail-closed-107 branch July 21, 2026 12:47
@lerian-studio-midaz-push-bot

Copy link
Copy Markdown

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

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 21, 2026
## [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)
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