File size: 3,863 Bytes
1f5ea39
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
/**
 * 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']);
  });
});