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:
- 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 blank401 Unauthorizedtext error. The proxy must seamlessly issue a302 Foundredirect 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. - 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(or403 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>
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:
- 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}`; - 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 - Post-Authentication Dispatch: Immediately following WebAuthn passkey
verification,
auth-client.jsextracts theredirectsearch 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
redirectparameter, 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 Redirectto/loginwhen a user is already logged in causes the browser to loop infinitely betweenapp.atyg.organdauth.atyg.org. - The Auth-Yes Rule:
- Unauthenticated (
!auth): Return302to/login(for browsers) or401(for APIs). - Authenticated but Inactive / Suspended (
account_status !== 'active'): Return403 Forbidden("Forbidden: Account inactive"). - Authenticated but Lacking Role Grant (
!grantRole && !isGlobalAdmin): Return403 Forbidden("Forbidden: Access denied to this application").
- Unauthenticated (
6. Verification Test Cases
| Scenario | Request Headers | Expected HTTP Status | Response Header / Body |
|---|---|---|---|
| Browser Unauthenticated | Accept: text/htmlX-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: aliceX-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) |