fix: WS upgrade auth — multi-token bearer not validated

Problem: isAuthenticated() for WS upgrade only checked legacy single token.
iOS bearer token (from POST /pair → createToken()) was rejected → 403 on WS.

Fix:
- warmTokenCache(): pre-load all multi-tokens into a sync Set on startup
- validateBearerSync(): O(1) sync lookup against the cache
- createToken(): adds to cache immediately on creation
- isAuthenticated(): checks validateBearerSync() as third fallback
This commit is contained in:
Johannes Merz 2026-05-16 03:12:43 +02:00
parent 38cad794e2
commit b64aaab40a
2 changed files with 32 additions and 3 deletions

View File

@ -20,6 +20,29 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
// ---------------------------------------------------------------------------
// In-memory cache for sync validation (WS upgrade can't await)
// ---------------------------------------------------------------------------
const _tokenCache = new Set<string>();
function cacheToken(token: string): void {
_tokenCache.add(token);
}
/** Sync bearer validation — checks in-memory cache populated at runtime. */
export function validateBearerSync(bearer: string): boolean {
return _tokenCache.has(bearer);
}
/** Warm the cache from disk on startup. */
export async function warmTokenCache(stateDir?: string): Promise<void> {
const entries = await loadTokens(stateDir);
for (const e of entries) _tokenCache.add(e.token);
}
// ---------------------------------------------------------------------------
export interface TokenEntry {
id: string;
token: string;
@ -70,6 +93,7 @@ export async function createToken(
};
entries.push(entry);
await saveTokens(entries, stateDir);
cacheToken(entry.token);
return entry;
}

View File

@ -34,7 +34,12 @@ import {
generatePairingToken,
printPairingQr,
} from "../auth/pairing.js";
import { createToken, validateBearer } from "../auth/tokens.js";
import {
createToken,
validateBearer,
validateBearerSync,
warmTokenCache,
} from "../auth/tokens.js";
import {
generateSessionId,
loadOrCreateToken,
@ -81,6 +86,7 @@ export async function startServer(
const clientChangeListeners: Array<() => void> = [];
const clients = new Set<WsClient>();
const token = await loadOrCreateToken();
await warmTokenCache(); // pre-load multi-tokens into sync cache
// Map of valid session IDs → expiry timestamp (ms since epoch)
const SESSION_TTL_MS = 86_400_000; // 24 h — matches cookie Max-Age
@ -112,8 +118,7 @@ export async function startServer(
const bearer = extractBearer(req);
if (bearer) {
if (validateToken(bearer, token)) return true;
// async validateBearer checked in asyncHandler for API routes;
// for WS upgrade we do a sync fallback: legacy token only here.
if (validateBearerSync(bearer)) return true; // multi-token sync cache
}
return false;