From b64aaab40aa7aa975ad46105dcb7d8119fe12454 Mon Sep 17 00:00:00 2001 From: jay Date: Sat, 16 May 2026 03:12:43 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20WS=20upgrade=20auth=20=E2=80=94=20multi-?= =?UTF-8?q?token=20bearer=20not=20validated?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- extensions/remote-control/auth/tokens.ts | 24 ++++++++++++++++++++++ extensions/remote-control/server/server.ts | 11 +++++++--- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/extensions/remote-control/auth/tokens.ts b/extensions/remote-control/auth/tokens.ts index e75bdf2..a793257 100644 --- a/extensions/remote-control/auth/tokens.ts +++ b/extensions/remote-control/auth/tokens.ts @@ -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(); + +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 { + 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; } diff --git a/extensions/remote-control/server/server.ts b/extensions/remote-control/server/server.ts index 115b6d3..9e815fa 100644 --- a/extensions/remote-control/server/server.ts +++ b/extensions/remote-control/server/server.ts @@ -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(); 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;