docs: add ForwardAuth Redirection & Deep-Link Protocol Specification (RFC-SPEC-2026-FA-01)

This commit is contained in:
Tyler Gillispie 2026-08-24 12:53:16 -07:00
parent 96bcf69179
commit b867a166e9
2 changed files with 176 additions and 0 deletions

View File

@ -49,6 +49,12 @@ workload identity.
freezes and in-flight WebAuthn re-auth. freezes and in-flight WebAuthn re-auth.
- _Raw Gitea Mirror:_ - _Raw Gitea Mirror:_
`https://git.atyg.org/tylerg/auth-yes/raw/branch/main/docs/GHOST_COCKPIT_SPEC.md` `https://git.atyg.org/tylerg/auth-yes/raw/branch/main/docs/GHOST_COCKPIT_SPEC.md`
- 🌐
**[ForwardAuth Browser Redirection & Deep-Link Spec](docs/FORWARDAUTH_REDIRECT_SPEC.md):**
Specification for browser 302 vs API 401 dual-response handling, open redirect
prevention (CWE-601), and deep-link query parameter preservation.
- _Raw Gitea Mirror:_
`https://git.atyg.org/tylerg/auth-yes/raw/branch/main/docs/FORWARDAUTH_REDIRECT_SPEC.md`
- 🛡️ **[Cryptographic Standards & Verification Dossier](docs/VERIFY.md):** Full - 🛡️ **[Cryptographic Standards & Verification Dossier](docs/VERIFY.md):** Full
mathematical and specification audits (W3C WebAuthn Level 3, RFC 9421, RFC mathematical and specification audits (W3C WebAuthn Level 3, RFC 9421, RFC
7638, RFC 6962, RFC 9106). 7638, RFC 6962, RFC 9106).

View File

@ -0,0 +1,170 @@
# 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>
```
---
## 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`<br>`X-Forwarded-Host: ed-droid.atyg.org` | `302 Found` | `Location: https://auth.atyg.org/login?redirect=...` |
| **API/cURL Unauthenticated** | `Accept: application/json` or `*/*`<br>`X-Forwarded-Host: ed-droid.atyg.org` | `401 Unauthorized` | Body: `"Unauthorized"` |
| **Browser Authenticated (Valid Grant)** | `Cookie: session_id=...`<br>`X-Forwarded-Host: ed-droid.atyg.org` | `200 OK` | `X-Forwarded-User: alice`<br>`X-Forwarded-Scopes: operator` |
| **Browser Authenticated (No Grant)** | `Cookie: session_id=...`<br>`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) |