Skip to content

feat(auth): forward-auth verification endpoint#34

Merged
polaz merged 2 commits into
mainfrom
feat/#33-forward-auth
Jun 20, 2026
Merged

feat(auth): forward-auth verification endpoint#34
polaz merged 2 commits into
mainfrom
feat/#33-forward-auth

Conversation

@polaz

@polaz polaz commented Jun 20, 2026

Copy link
Copy Markdown
Member

Summary

Implements the forward-auth verification endpoint so the proxy can act as an external auth gate for a fronting reverse proxy (nginx auth_request, Traefik forwardAuth). forward_auth config was parsed (its policies already drive the JWT route rules) but the endpoint it advertises wasn't served.

What's added (when auth.mode == jwt and forward_auth.enabled)

  • Serves forward_auth.path (default /auth/verify). It validates the request's Bearer token and evaluates the route policies against the original request, read from the fronting proxy's X-Forwarded-Method / X-Forwarded-Uri (falling back to X-Original-Method / X-Original-URI, then the verify request's own method/path).
  • 200 echoing the configured claim headers (e.g. x-forwarded-user) so the fronting proxy copies the verified identity upstream.
  • 401 when unauthenticated (adds Location: forward_auth.login_url when configured); 403 when authenticated but missing a required role.
  • The endpoint is mounted after the JWT middleware layer, so it is not itself gated by it.

Design

The token → policy → claims decision is factored into a single Auth::decide shared by the JWT middleware and the verify endpoint, so they can never drift. The route reuses the already-built Auth (same keys + policies). Original method/path parsing strips any query string.

Testing

4 endpoint tests (200 + echoed claim header, 401 + login Location, 403 missing role, unprotected original path) plus the existing 16 auth tests unchanged after the refactor. cargo nextest run --features redis: 109 passed. clippy (all-features) + fmt clean.

Out of scope (separate roadmap items)

External gRPC AuthZ (authz), BFF sessions (bff), and multi-application configs (applications_path).

Docs

README: forward-auth endpoint moved to Features; the roadmap line now tracks only External AuthZ / BFF.

Closes #33

Serve forward_auth.path (default /auth/verify) so a fronting reverse
proxy (nginx auth_request, Traefik forwardAuth) can delegate auth to the
proxy. The endpoint validates the request's Bearer token and evaluates
the configured route policies against the original request, taken from
the fronting proxy's X-Forwarded-Method / X-Forwarded-Uri (falling back
to X-Original-Method / X-Original-URI, then the verify request's own):

- 200 echoing the configured claim headers (verified identity) for the
  fronting proxy to copy upstream;
- 401 when unauthenticated (Location: login_url when configured);
- 403 when authenticated but missing a required role.

The token/policy/claims decision is factored into a single Auth::decide
shared by the JWT middleware and this endpoint, so both stay in lockstep.
The endpoint is mounted after the auth layer, so it is not itself gated.

Closes #33
@coderabbitai

coderabbitai Bot commented Jun 20, 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 Plus

Run ID: 2f7712d9-811f-4260-9525-0edfadb4a009

📥 Commits

Reviewing files that changed from the base of the PR and between 679429a and d0d9cd6.

📒 Files selected for processing (1)
  • src/auth/forward.rs

📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • New Features
    • Added a forward-auth verification endpoint for reverse proxies to authenticate requests.
    • When allowed, requests return verified claim headers; when missing/invalid, responses are 401 Unauthorized; when denied, 403 Forbidden.
  • Documentation
    • Updated the README roadmap to add Forward-auth support.
  • Tests
    • Added coverage for allow/deny/forbid behavior and optional login redirect handling.

Walkthrough

The PR refactors the JWT middleware's inline authorization logic into a shared Auth::decide method returning an AuthDecision enum, then uses that shared path to implement a new ForwardAuth endpoint (/auth/verify) for fronting reverse proxies. The endpoint is mounted outside the JWT middleware gate in the main router, and the README roadmap is updated.

Changes

Forward-auth verification endpoint

Layer / File(s) Summary
AuthDecision enum and Auth::decide refactor
src/auth/mod.rs
Adds AuthDecision (Allow, Unauthenticated, Forbidden) and Auth::decide that centralizes bearer-token parsing, JWT verification, route policy enforcement, and claim-header construction. Updates middleware to delegate to decide, insert verified claim headers on Allow, or return 401/403. Exports the new forward submodule.
ForwardAuth endpoint and tests
src/auth/forward.rs
Defines ForwardAuth with build (reads forward_auth config), routes (mounts any-method handler), and verify (reconstructs original method/path from forwarding headers, calls Auth::decide, returns 200 with claim headers, 401 with optional Location redirect, or 403). Adds private header extraction helpers and a full async test suite.
Router wiring and README
src/lib.rs, README.md
ProxyServer::router conditionally constructs ForwardAuth and merges its routes before the Shield and maintenance layers so the endpoint is public. README roadmap adds a Forward-auth bullet for the /auth/verify endpoint.

Sequence Diagram(s)

sequenceDiagram
  participant FrontingProxy
  participant ForwardAuth
  participant Auth
  participant RoutePolicy

  rect rgba(70, 130, 180, 0.5)
    Note over FrontingProxy,ForwardAuth: Public /auth/verify — not gated by JWT middleware
    FrontingProxy->>ForwardAuth: ANY /auth/verify<br/>(X-Forwarded-Method, X-Forwarded-Uri, Authorization)
    ForwardAuth->>ForwardAuth: extract original method + path from forwarding headers
  end

  rect rgba(100, 160, 100, 0.5)
    Note over ForwardAuth,RoutePolicy: Shared Auth::decide path (also used by middleware)
    ForwardAuth->>Auth: decide(headers, original_path, original_method)
    Auth->>Auth: parse + verify bearer token
    Auth->>RoutePolicy: match route, check require_auth / required_roles
    Auth-->>ForwardAuth: AuthDecision
  end

  alt Allow
    ForwardAuth-->>FrontingProxy: 200 OK + claim headers (e.g. x-forwarded-user)
  else Unauthenticated
    ForwardAuth-->>FrontingProxy: 401 Unauthorized (+ Location: login_url if set)
  else Forbidden
    ForwardAuth-->>FrontingProxy: 403 Forbidden
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • structured-world/structured-proxy#28: Introduced the JWT validation, route policy enforcement, and claim-to-header injection logic in src/auth/mod.rs that this PR refactors into the shared Auth::decide method.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and concisely summarizes the main change: implementing a forward-auth verification endpoint for JWT authentication.
Description check ✅ Passed The PR description is comprehensive, well-organized, and directly related to the changeset. It explains the purpose, implementation details, testing, and documentation updates.
Linked Issues check ✅ Passed The PR fully implements all requirements from issue #33: serves the verification endpoint at the configured path, validates Bearer tokens, evaluates route policies, returns appropriate status codes with claim headers and login redirects, refactors auth logic into a reusable decision function, includes comprehensive tests, and updates documentation.
Out of Scope Changes check ✅ Passed All changes are directly related to the forward-auth verification endpoint scope defined in issue #33. No out-of-scope changes were introduced; external AuthZ, BFF sessions, and multi-application configs remain out of scope as specified.
Docstring Coverage ✅ Passed Docstring coverage is 95.45% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#33-forward-auth

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

@greptile-apps

greptile-apps Bot commented Jun 20, 2026

Copy link
Copy Markdown

Greptile Summary

Implements the long-missing forward-auth verification endpoint (/auth/verify) so the proxy can serve as an external auth gate for nginx auth_request or Traefik forwardAuth. The core auth logic is refactored into a single Auth::decide() method shared by the JWT middleware and the new endpoint, ensuring the two paths can never drift.

  • src/auth/forward.rs — new ForwardAuth struct mounts the verify route, reads the original method/path from the fronting proxy's forwarding headers, calls Auth::decide(), and returns 200 (with echoed claim headers) / 401 (+ Location when a login URL is configured) / 403. Five tests cover the valid-token 200, no-token 401+Location, wrong-role 403, invalid-signature 401, and unprotected-path 200 paths.
  • src/auth/mod.rsAuth::decide() extracted from the middleware; the middleware is slimmed to call decide() and map the result, with identical observable behaviour to before the refactor.
  • src/lib.rs — forward-auth routes are merged into the router after the JWT middleware layer is applied and before Shield/Maintenance, so the endpoint is not gated by the JWT middleware but is still subject to rate limiting and maintenance mode.

Confidence Score: 5/5

Safe to merge; the forward-auth endpoint is correctly isolated from the JWT middleware and all auth decision paths are covered by the shared decide() method.

The refactor to Auth::decide() preserves the exact same token-validation and policy-enforcement behaviour that the middleware had before, verified by the 16 existing tests passing unchanged. The new endpoint correctly handles all five outcome paths (valid token, no token on protected route, wrong role, invalid token, no token on unprotected route) and each is covered by a dedicated test. Router layer ordering in lib.rs correctly places the forward-auth routes outside the JWT middleware layer but inside Shield and Maintenance.

No files require special attention.

Important Files Changed

Filename Overview
src/auth/forward.rs New file implementing the ForwardAuth verification endpoint with correct token validation, forwarded-header parsing, 200/401/403 response semantics, and five well-targeted tests covering valid, invalid, no-token, wrong-role, and unprotected-path cases
src/auth/mod.rs Refactors JWT middleware to delegate to a new shared Auth::decide() method; logic is preserved exactly and the new AuthDecision enum cleanly expresses all three outcomes (Allow, Unauthenticated, Forbidden)
src/lib.rs Adds ForwardAuth route mounting; the endpoint is correctly merged after the JWT middleware layer (so it is not gated by it) and before Shield/Maintenance layers (so rate limiting and maintenance mode still apply)
README.md Moves forward-auth from the roadmap to the implemented features list and narrows the roadmap to External AuthZ / BFF sessions only

Reviews (2): Last reviewed commit: "refactor(auth): simplify forward-auth qu..." | Re-trigger Greptile

Comment thread src/auth/forward.rs Outdated
Comment thread src/auth/forward.rs
…oken

- Use split_once('?') for the original-path query strip; the previous
  split().next().unwrap_or(..) had a dead fallback branch (split always
  yields at least one element).
- Add a verify-endpoint test that a token signed by the wrong key is
  rejected with 401, guarding the verification path against regressions
  (e.g. a dropped algorithm-confusion check).

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(auth): forward-auth verification endpoint

1 participant