From 6491681e3ed3d73fc05fa15e1025b18592e0977f Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Sat, 21 Mar 2026 22:31:13 -0700 Subject: [PATCH] fix(auth): force platform authenticator + redirect disabled modules Force authenticatorAttachment: 'platform' across all WebAuthn registration flows to prevent USB security key prompts. Redirect browser navigations to space root when accessing disabled modules instead of returning JSON error. Co-Authored-By: Claude Opus 4.6 --- lib/rspace-header.ts | 1 + server/index.ts | 5 +++++ shared/components/rstack-identity.ts | 4 ++-- src/encryptid/server.ts | 6 ++++-- src/encryptid/webauthn.ts | 4 ++-- 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/rspace-header.ts b/lib/rspace-header.ts index 5c02268..9ea66b2 100644 --- a/lib/rspace-header.ts +++ b/lib/rspace-header.ts @@ -673,6 +673,7 @@ export function showAuthModal(callbacks?: Partial): void { { alg: -257, type: 'public-key' as const }, ], authenticatorSelection: { + authenticatorAttachment: 'platform', residentKey: 'required', requireResidentKey: true, userVerification: 'required', diff --git a/server/index.ts b/server/index.ts index 3b4a7c1..a75c801 100644 --- a/server/index.ts +++ b/server/index.ts @@ -2056,6 +2056,11 @@ for (const mod of getAllModules()) { const doc = getDocumentData(space); if (mod.id !== "rspace") { if (doc?.meta?.enabledModules && !doc.meta.enabledModules.includes(mod.id)) { + // Redirect browser navigations to space root; return JSON error for API calls + const accept = c.req.header("Accept") || ""; + if (accept.includes("text/html")) { + return c.redirect(`https://${space}.rspace.online/`); + } return c.json({ error: "Module not enabled for this space" }, 404); } } diff --git a/shared/components/rstack-identity.ts b/shared/components/rstack-identity.ts index 7acb479..ffca412 100644 --- a/shared/components/rstack-identity.ts +++ b/shared/components/rstack-identity.ts @@ -621,7 +621,7 @@ export class RStackIdentity extends HTMLElement { { alg: -7, type: "public-key" as const }, { alg: -257, type: "public-key" as const }, ], - authenticatorSelection: { residentKey: "required", requireResidentKey: true, userVerification: "required" }, + authenticatorSelection: { authenticatorAttachment: "platform", residentKey: "required", requireResidentKey: true, userVerification: "required" }, attestation: "none", timeout: 60000, }, @@ -1127,7 +1127,7 @@ export class RStackIdentity extends HTMLElement { { alg: -7, type: "public-key" as const }, { alg: -257, type: "public-key" as const }, ], - authenticatorSelection: { residentKey: "required", requireResidentKey: true, userVerification: "required" }, + authenticatorSelection: { authenticatorAttachment: "platform", residentKey: "required", requireResidentKey: true, userVerification: "required" }, attestation: "none", timeout: 60000, }, diff --git a/src/encryptid/server.ts b/src/encryptid/server.ts index 988c1f1..50381c9 100644 --- a/src/encryptid/server.ts +++ b/src/encryptid/server.ts @@ -546,6 +546,7 @@ app.post('/api/register/start', async (c) => { { alg: -257, type: 'public-key' }, // RS256 ], authenticatorSelection: { + authenticatorAttachment: 'platform', residentKey: 'required', requireResidentKey: true, userVerification: 'required', @@ -1524,6 +1525,7 @@ app.post('/api/account/device/start', async (c) => { { alg: -257, type: 'public-key' }, ], authenticatorSelection: { + authenticatorAttachment: 'platform', residentKey: 'required', requireResidentKey: true, userVerification: 'required', @@ -2438,7 +2440,7 @@ app.get('/guardian', (c) => { { alg: -7, type: 'public-key' }, { alg: -257, type: 'public-key' }, ], - authenticatorSelection: { residentKey: 'required', requireResidentKey: true, userVerification: 'required' }, + authenticatorSelection: { authenticatorAttachment: 'platform', residentKey: 'required', requireResidentKey: true, userVerification: 'required' }, attestation: 'none', timeout: 60000, }, @@ -7083,7 +7085,7 @@ app.get('/', (c) => { { alg: -7, type: 'public-key' }, { alg: -257, type: 'public-key' }, ], - authenticatorSelection: { residentKey: 'required', requireResidentKey: true, userVerification: 'required' }, + authenticatorSelection: { authenticatorAttachment: 'platform', residentKey: 'required', requireResidentKey: true, userVerification: 'required' }, attestation: 'none', timeout: 60000, extensions: { credProps: true, ...prfExtension }, diff --git a/src/encryptid/webauthn.ts b/src/encryptid/webauthn.ts index d27af0a..a346752 100644 --- a/src/encryptid/webauthn.ts +++ b/src/encryptid/webauthn.ts @@ -178,8 +178,8 @@ export async function registerPasskey( // Require user verification (biometric/PIN) userVerification: cfg.userVerification, - // Prefer platform authenticator but allow cross-platform - authenticatorAttachment: platformAvailable ? 'platform' : undefined, + // Force platform authenticator (Windows Hello, Touch ID, etc.) + authenticatorAttachment: 'platform', }, // Don't request attestation (privacy)