Harden forward auth: store redirect URIs server-side, eliminate client control
Replace client-controlled redirectUri with server-side redirect intents. The portal server component validates the ?rd= hostname against isForwardAuthDomain, stores the URI in a new forward_auth_redirect_intents table, and passes only an opaque rid (128-bit random, SHA-256 hashed) to the client. Login endpoints consume the intent atomically (one-time use, 10-minute TTL) and retrieve the stored URI — the client never sends the redirect URL to any API endpoint. Security properties: - Redirect URI is never client-controlled in API requests - rid is 128-bit random, stored as SHA-256 hash (not reversible from DB) - Atomic one-time consumption prevents replay - 10-minute TTL limits attack window for OAuth round-trip - Immediate deletion after consumption - Expired intents cleaned up opportunistically - Hostname validated against registered forward-auth domains before storage Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -428,6 +428,22 @@ export const forwardAuthExchanges = sqliteTable(
|
||||
})
|
||||
);
|
||||
|
||||
export const forwardAuthRedirectIntents = sqliteTable(
|
||||
"forward_auth_redirect_intents",
|
||||
{
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
ridHash: text("rid_hash").notNull(),
|
||||
redirectUri: text("redirect_uri").notNull(),
|
||||
expiresAt: text("expires_at").notNull(),
|
||||
consumed: integer("consumed", { mode: "boolean" }).notNull().default(false),
|
||||
createdAt: text("created_at").notNull()
|
||||
},
|
||||
(table) => ({
|
||||
ridHashUnique: uniqueIndex("fari_rid_hash_unique").on(table.ridHash),
|
||||
expiresIdx: index("fari_expires_idx").on(table.expiresAt)
|
||||
})
|
||||
);
|
||||
|
||||
// ── L4 Proxy Hosts ───────────────────────────────────────────────────
|
||||
|
||||
export const l4ProxyHosts = sqliteTable("l4_proxy_hosts", {
|
||||
|
||||
Reference in New Issue
Block a user