Skip to content

feat: forward auth support#2485

Open
soulteary wants to merge 1 commit intobasecamp:mainfrom
soulteary:feat/integrated-forward-auth
Open

feat: forward auth support#2485
soulteary wants to merge 1 commit intobasecamp:mainfrom
soulteary:feat/integrated-forward-auth

Conversation

@soulteary
Copy link

Fizzy × Forward Auth (Stargate) Integration Plan

Goal

When a request is authenticated by Stargate (or a similar Forward Auth gateway), the gateway injects these headers:

  • X-Auth-Email — Authenticated user email (required)
  • X-Auth-User — Gateway-side user identifier (optional; for logging or default display name)
  • X-Auth-Amr, X-Forwarded-For, X-Real-Ip, etc. — Optional, for auditing or IP logging

Fizzy only trusts these headers when the request comes from a trusted source, then uses them for identity resolution and authorization, without requiring magic-link login.

Architecture and Data Flow

sequenceDiagram
  participant Browser
  participant Traefik
  participant Stargate
  participant Fizzy

  Browser->>Traefik: GET /12345/boards
  Traefik->>Stargate: Forward Auth check
  Stargate->>Stargate: Validate session/OTP
  Stargate->>Traefik: 200 + inject X-Auth-Email etc.
  Traefik->>Fizzy: Request with X-Auth-* headers
  Fizzy->>Fizzy: Trust check + Identity/User resolution
  Fizzy->>Fizzy: Set Current.identity / Current.user
  Fizzy->>Browser: Response (user logged in)
Loading
  • Auth chain extension: After existing resume_session and authenticate_by_bearer_token, add authenticate_by_forward_auth; if all three fail, then request_authentication (redirect to login).
  • Header-based auth runs only for requests where Forward Auth is enabled and the request passes the trust check; all other requests behave as today.

Implementation Details

1. Auth chain extension

  • File: app/controllers/concerns/authentication.rb
  • Change: Update require_authentication from
    resume_session || authenticate_by_bearer_token || request_authentication
    to
    resume_session || authenticate_by_bearer_token || authenticate_by_forward_auth || request_authentication
    and add a private method authenticate_by_forward_auth.

Logic summary:

  • If Forward Auth is disabled (see config below) or the request fails the trust check, return false (do not set identity).
  • Read request.headers["X-Auth-Email"], normalize (strip, downcase), then find or create Identity (aligned with existing Identity normalization).
  • If Current.account is set (from URL via config/initializers/tenanting/account_slug.rb):
    • Find the User for this Identity in that Account.
    • If config allows auto-provision and no User exists, create one (default role configurable, e.g. member; name from email local part or X-Auth-User).
    • If auto-provision is off and there is no User, treat as unauthorized (do not set identity; flow continues to request_authentication).
  • Set Current.identity = identity.
  • Optional: On first successful Forward Auth auth, call start_new_session_for(identity) to create a Fizzy session and set a cookie, so ActionCable and page reloads still identify the user via cookie. If no session is created, every request must carry Forward Auth headers and ActionCable would need separate support (see below).

2. Trust and security

Requests must be trusted only when they actually come from our gateway; otherwise anyone could forge X-Auth-Email and impersonate users.

  • Option 1 (recommended): Trust by source IP
    Run Forward Auth auth only when request.remote_ip is in a configured trusted IP list (e.g. Traefik/Stargate internal network or 127.0.0.1). Fizzy already has config/initializers/true_client_ip.rb for True-Client-IPX-Forwarded-For; the effective IP is resolved by Rails RemoteIp etc., so use request.remote_ip for the trust check, consistent with existing auditing.

  • Option 2 (optional): Secret header from gateway
    If Stargate or Traefik sets a custom header after auth (e.g. X-Forward-Auth-Verified: <shared_secret>), Fizzy can verify that header against a configured secret before trusting X-Auth-Email. Can be used instead of or in addition to the IP allowlist.

Implement a “Forward Auth config” object that exposes trusted?(request) (check IP and/or secret header). Proceed with authenticate_by_forward_auth only when trusted?(request) is true and the auth header is present.

3. Configuration

  • Where: New config/initializers/forward_auth.rb (or in config/application.rb’s to_prepare), reading from ENV or Rails.application.config.
  • Suggested env vars (examples):
    • FORWARD_AUTH_ENABLED — Enable Forward Auth (e.g. true / 1).
    • FORWARD_AUTH_TRUSTED_IPS — Comma-separated trusted IPs or CIDRs (e.g. 127.0.0.1,10.0.0.0/8); if empty, rely only on secret header when used.
    • FORWARD_AUTH_SECRET_HEADER, FORWARD_AUTH_SECRET — Optional: header name and expected value set by the gateway for verification.
    • FORWARD_AUTH_AUTO_PROVISION — When Identity exists but there is no User in the current Account, auto-create User (e.g. true).
    • FORWARD_AUTH_DEFAULT_ROLE — Default role for auto-created Users (e.g. member).
    • FORWARD_AUTH_CREATE_SESSION — Create Fizzy session and set cookie on first successful Forward Auth (recommended true for WebSocket compatibility).

Expose this as Rails.application.config.forward_auth or a config singleton, and use it in authenticate_by_forward_auth and trust logic.

4. Identity / User and header mapping

  • Identity: Keyed by X-Auth-Email, aligned with email_address format and uniqueness in app/models/identity.rb. Look up with Identity.find_by(email_address: normalized_email); use find_or_create_by when auto-creation is enabled (and keep alignment with existing normalizes :email_address).
  • User: At most one User per Identity per Account (see unique index index_users_on_account_id_and_identity_id). With FORWARD_AUTH_AUTO_PROVISION, follow the pattern in db/seeds.rb’s find_or_create_user to create Users, set verified_at: Time.current and default role; name from email local part or a friendly value from X-Auth-User.

5. ActionCable (WebSocket)

  • Current behavior: app/channels/application_cable/connection.rb resolves the session only via cookies.signed[:session_token], then derives current_user.
  • If “create Fizzy session and set cookie after Forward Auth success”: No Connection changes; WebSocket still identifies the user via the existing cookie.
  • If “header-only auth, no session”: Add a branch in Connection’s set_current_user: when there is no valid session in the cookie, if the request has trusted Forward Auth headers (or a token set by the gateway), use the same trust and Identity/User resolution to set current_user. That requires WebSocket requests to go through the same gateway with the same headers (or optional token). Prefer the “create session” approach to limit scope of changes.

6. Testing and documentation

  • Tests:
    • In test/controllers/concerns/ or existing controller tests, add cases where Forward Auth is enabled, request is trusted, and a valid X-Auth-Email correctly sets Current.identity / Current.user.
    • Cover: disabled, untrusted IP, missing/wrong secret header, no User and auto-provision off, auto-created User, etc.
  • Docs: Add a section under docs/ or in the README on using Fizzy with Stargate or other Forward Auth gateways (how to enable, trust config, required headers, optional auto-provision and session strategy), and reference the Stargate repo (e.g. https://github.com/soulteary/stargate).

7. Compatibility with existing behavior

  • When FORWARD_AUTH_ENABLED is unset or the request fails the trust check, behavior is unchanged (Cookie session + Bearer + magic link only).
  • Requests already authenticated by Cookie or Bearer never hit authenticate_by_forward_auth because resume_session or authenticate_by_bearer_token returns true first.
  • Login/logout URLs, session menu, multi-Account choice, etc. stay the same; Forward Auth is only tried when the request has an account prefix (i.e. Current.account set) and authentication is required.

Suggested implementation order

  1. Add Forward Auth config (initializer + trust logic).
  2. Implement authenticate_by_forward_auth in app/controllers/concerns/authentication.rb (Identity/User lookup, optional auto-provision, optional session creation) and wire it into require_authentication.
  3. Add tests and update documentation.
  4. If “header-only, no session” is needed later, extend ActionCable Connection’s auth branch.

Risks and notes

  • Trust scope: Misconfiguration (e.g. trusting 0.0.0.0/0 or leaking the secret) allows arbitrary user impersonation; review before deployment.
  • Logging and auditing: Log Forward Auth success/failure (no passwords or tokens) for debugging and security review.
  • Multi-tenancy: Forward Auth only answers “who”; “which Account” is still determined by the account id in the URL and whether a User exists in Fizzy (and Access, etc.), consistent with existing multi-tenant behavior.

@soulteary soulteary force-pushed the feat/integrated-forward-auth branch from 81a3514 to 36dfcd9 Compare February 5, 2026 02:27
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.

1 participant