# 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= │ ▼ HTTP 200 OK + Grant Vector Injection • X-Forwarded-User-Id: • X-Forwarded-User: • X-Forwarded-Scopes: • X-Forwarded-App-Id: ``` --- ## 3. Deep-Link & Parameter Preservation 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:** ```typescript 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 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: ```javascript 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: ```typescript 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) |