Files
TREK/server/tests/unit/services/webauthnConfig.test.ts
T
Maurice a876fb2634 feat: Passkey (WebAuthn) login (#1111)
* feat(auth): passkey (WebAuthn) login — server endpoints, schema + admin toggle

Add @simplewebauthn/server registration and primary (discoverable) login ceremonies under /api/auth/passkey, a webauthn_credentials + single-use webauthn_challenges schema (migration), the instance-wide passkey_login toggle (default off) enforced before auth by a guard, and require_mfa satisfaction via a verified passkey. RP ID/origin come only from server config (webauthn_rp_id/origins -> APP_URL), never request headers.

* feat(auth): passkey enrolment, login button + admin settings UI

PasskeysSection in account settings (add/rename/remove with a current-password step-up), a 'Sign in with a passkey' button on the login page, the admin enable + RP-ID/origins controls, and a per-user admin reset action.

* i18n(auth): passkey strings across all locales

Add login/settings/admin passkey keys to en and all 19 translated locales.
2026-06-05 18:54:13 +02:00

104 lines
3.8 KiB
TypeScript

/**
* webauthnConfig.test.ts
*
* The RP-ID / allowed-origin resolver is the single highest-risk piece of the
* passkey feature: a wrong RP ID permanently bricks every enrolled credential.
* These tests pin the security-relevant rules — config wins over APP_URL, bare
* IPs are rejected, localhost dev uses the browser (Vite) origin, and the
* resolver NEVER reads request headers.
*/
const { settingsStore, appUrlRef } = vi.hoisted(() => ({
settingsStore: new Map<string, string>(),
appUrlRef: { value: '' },
}));
vi.mock('../../../src/db/database', () => ({
db: {
prepare: (_sql: string) => ({
get: (key: string) => {
const v = settingsStore.get(key);
return v === undefined ? undefined : { value: v };
},
}),
},
}));
vi.mock('../../../src/services/notifications', () => ({
getAppUrl: () => appUrlRef.value,
}));
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { resolveWebauthnConfig, isPasskeyConfigured } from '../../../src/services/webauthnConfig';
beforeEach(() => {
settingsStore.clear();
appUrlRef.value = '';
});
afterEach(() => {
vi.unstubAllEnvs();
});
describe('resolveWebauthnConfig', () => {
it('WAC-001: derives the RP ID and single origin from a real APP_URL domain', () => {
appUrlRef.value = 'https://trek.example.org';
const cfg = resolveWebauthnConfig();
expect(cfg).not.toBeNull();
expect(cfg!.rpID).toBe('trek.example.org');
expect(cfg!.origins).toEqual(['https://trek.example.org']);
expect(isPasskeyConfigured()).toBe(true);
});
it('WAC-002: returns null for a bare-IP host (IPs are not valid RP IDs)', () => {
appUrlRef.value = 'http://192.168.1.50:3001';
expect(resolveWebauthnConfig()).toBeNull();
expect(isPasskeyConfigured()).toBe(false);
});
it('WAC-003: returns null when nothing is configured', () => {
expect(resolveWebauthnConfig()).toBeNull();
expect(isPasskeyConfigured()).toBe(false);
});
it('WAC-004: localhost dev uses the browser (Vite :5173) origin, not just the API port', () => {
appUrlRef.value = 'http://localhost:3001';
const cfg = resolveWebauthnConfig();
expect(cfg!.rpID).toBe('localhost');
expect(cfg!.origins).toContain('http://localhost:5173');
expect(cfg!.origins).toContain('http://localhost:3001');
});
it('WAC-005: an explicit webauthn_rp_id app-setting overrides APP_URL', () => {
appUrlRef.value = 'https://internal.example.org';
settingsStore.set('webauthn_rp_id', 'public.example.org');
settingsStore.set('webauthn_origins', 'https://public.example.org');
const cfg = resolveWebauthnConfig();
expect(cfg!.rpID).toBe('public.example.org');
expect(cfg!.origins).toEqual(['https://public.example.org']);
});
it('WAC-006: webauthn_origins is parsed as a comma-separated, trimmed list', () => {
settingsStore.set('webauthn_rp_id', 'example.org');
settingsStore.set('webauthn_origins', 'https://a.example.org , https://b.example.org/');
const cfg = resolveWebauthnConfig();
expect(cfg!.origins).toEqual(['https://a.example.org', 'https://b.example.org']);
});
it('WAC-007: the WEBAUTHN_RP_ID env var takes priority', () => {
vi.stubEnv('WEBAUTHN_RP_ID', 'env.example.org');
vi.stubEnv('WEBAUTHN_ORIGINS', 'https://env.example.org');
appUrlRef.value = 'https://ignored.example.org';
const cfg = resolveWebauthnConfig();
expect(cfg!.rpID).toBe('env.example.org');
expect(cfg!.origins).toEqual(['https://env.example.org']);
});
it('WAC-008: a configured RP ID with no origins falls back to the APP_URL origin', () => {
appUrlRef.value = 'https://trek.example.org';
settingsStore.set('webauthn_rp_id', 'trek.example.org');
const cfg = resolveWebauthnConfig();
expect(cfg!.origins).toEqual(['https://trek.example.org']);
});
});