auth-yes/docs/FORWARDAUTH_REDIRECT_SPEC.md

7.1 KiB

ForwardAuth Browser Redirection & Deep-Link Protocol Specification

Specification ID: RFC-SPEC-2026-FA-01
Classification: Ingress Security & UX Protocol
Applies To: Traefik Ingress Proxy, Auth-Yes Core Gateway (/api/forward-auth), WebAuthn Client (auth-client.js)
Status: Canonical / Implemented


1. Executive Summary & Problem Statement

When edge ingress proxies (e.g. Traefik) intercept traffic via ForwardAuth, unauthenticated requests must be handled differently depending on the nature of the client:

  1. Interactive Web Browsers (Humans): Navigating to a protected web application (e.g. https://ed-droid.atyg.org/control-panel?tab=telemetry) must not display a blank 401 Unauthorized text error. The proxy must seamlessly issue a 302 Found redirect to the WebAuthn Passkey authority (https://auth.atyg.org/login?redirect=...) and automatically return the user to their exact requested URL/tab upon biometric touch.
  2. Headless Daemons & REST APIs (Machines): Automated daemons, cURL scripts, and microservice RPCs must never receive an HTML redirect loop. They must receive an immediate, machine-parseable HTTP 401 Unauthorized (or 403 Forbidden).

2. The Dual-Response Protocol (Browser 302 vs API 401)

Detection Logic in GET /api/forward-auth

When Auth-Yes receives an unauthenticated ForwardAuth sub-request from Traefik:

[ Incoming Request to Ingress ]
              │
              ▼
[ Traefik ForwardAuth Sub-request to auth-api ]
  - Headers evaluated:
    • X-Forwarded-Host:  "ed-droid.atyg.org"
    • X-Forwarded-Uri:   "/control-panel?tab=telemetry"
    • X-Forwarded-Proto: "https"
    • Accept:            "text/html,application/xhtml+xml,..."
              │
              ▼
    Is Session Valid in Valkey/DB?
              │
      ┌───────┴───────┐
      │               │
     YES              NO
      │               │
      │       ┌───────┴────────────────────────┐
      │       ▼                                ▼
      │   Accept header contains           Accept header does NOT
      │   "text/html" (Browser)            contain "text/html" (API)
      │       │                                │
      │       ▼                                ▼
      │   HTTP 302 Redirect                HTTP 401 Unauthorized
      │   Location:                        Body: "Unauthorized"
      │   https://auth.atyg.org/login?
      │   redirect=<encoded_target_url>
      │
      ▼
HTTP 200 OK + Grant Vector Injection
  • X-Forwarded-User-Id: <uuid>
  • X-Forwarded-User:    <username>
  • X-Forwarded-Scopes:  <scopes>
  • X-Forwarded-App-Id:  <app_id>

To prevent user friction, the full destination URL—including protocol, host, path, and all query parameters—is preserved throughout the authentication ceremony:

  1. URL Reconstruction at Ingress:
    const host = req.headers.get("X-Forwarded-Host") || "atyg.org";
    const proto = req.headers.get("X-Forwarded-Proto") || "https";
    const uri = req.headers.get("X-Forwarded-Uri") || "/";
    const targetUrl = `${proto}://${host}${uri}`;
    
  2. Encoding into Redirect Location:
    HTTP/1.1 302 Found
    Location: https://auth.atyg.org/login?redirect=https%3A%2F%2Fed-droid.atyg.org%2Fcontrol-panel%3Ftab%3Dtelemetry
    
  3. Post-Authentication Dispatch: Immediately following WebAuthn passkey verification, auth-client.js extracts the redirect search parameter and executes:
    globalThis.location.href = targetRedirect;
    

4. Open Redirect Security (CWE-601 Prevention)

Allowing arbitrary redirect parameters poses severe phishing risks (e.g. https://auth.atyg.org/login?redirect=https://evil-attacker.com).

Strict Whitelist Validation Rules:

Before dispatching the user post-login, the redirect URL MUST pass strict origin validation:

function isSafeRedirectUrl(rawUrl: string): boolean {
  try {
    // 1. Relative paths within the same origin are always safe
    if (rawUrl.startsWith("/") && !rawUrl.startsWith("//")) {
      return true;
    }

    const parsed = new URL(rawUrl);

    // 2. Only allow explicit wildcard *.atyg.org subdomains or localhost
    if (
      parsed.hostname === "atyg.org" ||
      parsed.hostname.endsWith(".atyg.org") ||
      parsed.hostname === "localhost"
    ) {
      return true;
    }
  } catch (_e) {
    // Malformed URL
    return false;
  }
  return false;
}
  • Violation Behavior: If an invalid or foreign domain is supplied in the redirect parameter, the client silently falls back to /dashboard, preventing malicious redirects.

5. Infinite Redirect Loop Prevention & 403 Forbidden Handling

A critical failure mode in ForwardAuth occurs when an authenticated user attempts to access an application for which they lack an active RBAC grant:

  • The Anti-Pattern (Infinite Loop): Returning 302 Redirect to /login when a user is already logged in causes the browser to loop infinitely between app.atyg.org and auth.atyg.org.
  • The Auth-Yes Rule:
    • Unauthenticated (!auth): Return 302 to /login (for browsers) or 401 (for APIs).
    • Authenticated but Inactive / Suspended (account_status !== 'active'): Return 403 Forbidden ("Forbidden: Account inactive").
    • Authenticated but Lacking Role Grant (!grantRole && !isGlobalAdmin): Return 403 Forbidden ("Forbidden: Access denied to this application").

6. Verification Test Cases

Scenario Request Headers Expected HTTP Status Response Header / Body
Browser Unauthenticated Accept: text/html
X-Forwarded-Host: ed-droid.atyg.org
302 Found Location: https://auth.atyg.org/login?redirect=...
API/cURL Unauthenticated Accept: application/json or */*
X-Forwarded-Host: ed-droid.atyg.org
401 Unauthorized Body: "Unauthorized"
Browser Authenticated (Valid Grant) Cookie: session_id=...
X-Forwarded-Host: ed-droid.atyg.org
200 OK X-Forwarded-User: alice
X-Forwarded-Scopes: operator
Browser Authenticated (No Grant) Cookie: session_id=...
X-Forwarded-Host: ungranted-app.atyg.org
403 Forbidden Body: "Forbidden: Access denied..." (No redirect loop)
Open Redirect Exploit Attempt ?redirect=https://phishing-site.com 200 OK \rightarrow Client Navigates to /dashboard (Phishing domain discarded)