docs: add ForwardAuth Redirection & Deep-Link Protocol Specification (RFC-SPEC-2026-FA-01)
This commit is contained in:
parent
96bcf69179
commit
b867a166e9
@ -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).
|
||||||
|
|||||||
170
docs/FORWARDAUTH_REDIRECT_SPEC.md
Normal file
170
docs/FORWARDAUTH_REDIRECT_SPEC.md
Normal 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) |
|
||||||
Loading…
x
Reference in New Issue
Block a user