Review fixes: validation hardening, dead code, stale comments

- validate_config: reject multiple auth-host marks per domain;
  sanitize_config clears extras (first wins) and coerces junk entry
  values to presence-only
- origin_key: lowercase keys, strip trailing dots (bare hosts/wildcards)
- Passkey._allowlisted: tolerate trailing-dot wildcard bases
- wschat: stamp remote-flow sessions with the session host's domain,
  not the approver's
- auth_host redirects: keep the port (redirect to the configured auth
  host instead of the normalized, port-less current host)
- update_domain: required fields (wholesale replace) — no silent wipes
- admin: fix pre-existing lockout-guard order in org permission removal;
  permission PATCH keeps domain restriction when omitted; 400 instead of
  500 on unknown permission UUIDs
- Drop dead code: db.update_config/set_session_host/delete_reset_token,
  Session.metadata, oidjwt.clear_key, background aliases,
  avatar.current_avatar_url/media_root, wsutil.require_pow
- Prune stale/duplicated comments and docstrings
This commit is contained in:
2026-09-07 07:53:27 +00:00
parent 2039c47e46
commit 3ff41ca354
24 changed files with 224 additions and 176 deletions
+18 -7
View File
@@ -75,18 +75,22 @@ Runs tests with Playwright Inspector for step-by-step debugging.
```
e2e/
├── playwright.config.ts # Playwright configuration
├── playwright.config.js # Playwright configuration
├── package.json
├── tsconfig.json
├── test-data/ # Test database (created at runtime)
│ └── test.sqlite
│ └── paskia.kantadb
└── tests/
├── global-setup.ts # Creates fresh DB, captures reset token
├── global-setup.ts # Creates fresh DB (localhost + test.localhost domains), captures reset token
├── global-teardown.ts # Cleanup
├── passkey.spec.ts # Main E2E tests
├── 10-passkey.spec.ts # Registration, authentication, session tests
├── 20-api-auth.spec.ts # API-mode iframe flows (401/403/reauth)
├── 50-multidomain.spec.ts# Multi-domain dispatch, related origins, auth hosts, remote login
├── 99-logout.spec.ts # Logout (runs last)
└── fixtures/
├── virtual-authenticator.ts # Virtual authenticator setup
── passkey-helpers.ts # WebSocket helpers
── passkey-helpers.ts # WebSocket helpers
└── remote-auth.ts # Pairing-code remote auth helpers
```
## What's Tested
@@ -107,6 +111,13 @@ e2e/
- Logout (`/auth/api/logout`)
- Invalid/missing token rejection
### Multi-Domain
- Host-based domain dispatch (`localhost` vs `test.localhost`, 421 for unknown hosts)
- Related Origin Requests well-known endpoint and admin domain API
- Per-domain auth hosts (UI at the site root)
- WebSocket cross-domain rules
- Cross-domain remote login via pairing code
## How Virtual Authenticator Works
The tests use Chrome DevTools Protocol (CDP) to create a virtual authenticator:
@@ -142,12 +153,12 @@ This creates an in-browser authenticator that:
## Limitations
1. **Chromium only**: Virtual authenticator is a Chrome DevTools feature
2. **No cross-origin**: Tests run on localhost; production-like origins need additional setup
2. **Multi-domain via `*.localhost`**: Chrome resolves any `*.localhost` hostname to loopback, which the tests use for cross-domain scenarios; non-localhost domains are exercised only via explicit Host headers (Node-side requests)
3. **Single user per run**: Bootstrap creates one admin user; additional users need admin API
## Debugging Tips
1. **Check test database**: `e2e/test-data/test.sqlite` persists after tests
1. **Check test database**: `e2e/test-data/paskia.kantadb` is removed during teardown; comment out the cleanup in `global-teardown.ts` to inspect it after a run
2. **View server output**: Global setup echoes server bootstrap to console
3. **Use trace viewer**: `npx playwright show-trace` on failure traces
+111 -1
View File
@@ -19,7 +19,11 @@ import {
*
* Covers:
* - Host-based domain dispatch (settings, 421 for unknown hosts)
* - Related Origin Requests well-known endpoint + admin domain API
* - Related Origin Requests well-known endpoint + admin domain API,
* including HTTP dispatch to a related hostname
* - Per-domain auth hosts: settings, UI at the site root, /auth/ redirect
* - WebSocket cross-domain rule: rejected unless the Host is the origin
* domain's own auth host
* - Cross-domain remote login: a passkey registered on localhost permits a
* session on test.localhost via pairing code
* - The profile enrollment prompt on a domain where the user has no passkey
@@ -38,11 +42,15 @@ test.describe('Multi-domain E2E', () => {
const domainSettings = await domainResp?.json()
expect(domainSettings.rp_id).toBe('test.localhost')
expect(domainSettings.own_auth_host).toBeNull()
expect(domainSettings.auth_host).toBeNull()
expect(domainSettings.ui_base_path).toBe('/auth/')
const defaultResp = await page.goto(`${baseUrl}/auth/api/settings`)
expect(defaultResp?.status()).toBe(200)
const defaultSettings = await defaultResp?.json()
expect(defaultSettings.rp_id).toBe('localhost')
expect(defaultSettings.auth_host).toBeNull()
expect(defaultSettings.ui_base_path).toBe('/auth/')
// Unknown host is rejected with 421 Misdirected Request.
// page.request is Node-side, so target loopback with an explicit Host.
@@ -89,6 +97,14 @@ test.describe('Multi-domain E2E', () => {
const wkJson = await wk.json()
expect(wkJson.origins).toContain('https://app.example.com')
// The related hostname now dispatches to the listing domain (HTTP).
// page.request is Node-side, so target loopback with an explicit Host.
const relResp = await page.request.get(`${baseUrl}/auth/api/settings`, {
headers: { Host: 'app.example.com' },
})
expect(relResp.ok()).toBeTruthy()
expect((await relResp.json()).rp_id).toBe('localhost')
// Restore: remove related origins again so later tests see the pristine state
const restore = await page.request.patch(`${baseUrl}/auth/api/admin/domains/localhost`, {
headers,
@@ -97,6 +113,100 @@ test.describe('Multi-domain E2E', () => {
expect(restore.ok()).toBeTruthy()
const after = await page.request.get(`${baseUrl}/.well-known/webauthn`)
expect(after.status()).toBe(404)
// ...and the related hostname is unknown again
const relGone = await page.request.get(`${baseUrl}/auth/api/settings`, {
headers: { Host: 'app.example.com' },
})
expect(relGone.status()).toBe(421)
})
test('per-domain auth host serves the domain UI at its site root', async ({ page, virtualAuthenticator }) => {
// Fresh session via device token (domain writes require recent auth)
const deviceToken = popDeviceToken()
test.skip(!deviceToken, 'No device tokens available')
await page.goto('/auth/')
const reg = await registerPasskey(page, baseUrl, { resetToken: deviceToken })
expect(reg.session_token).toBeTruthy()
const headers = { Cookie: `${getSessionCookieName()}=${reg.session_token}` }
const authHost = 'auth.test.localhost:4404'
try {
// Mark an auth host on the test.localhost domain. Chrome resolves any
// *.localhost hostname to loopback, so the auth host is reachable.
const patch = await page.request.patch(`${baseUrl}/auth/api/admin/domains/test.localhost`, {
headers,
data: { rp_name: '', origins: { [`http://${authHost}`]: { auth_host: true } }, related: {} },
})
expect(patch.ok()).toBeTruthy()
// The auth host dispatches to its domain and reports itself in settings
const settingsResp = await page.goto(`http://${authHost}/auth/api/settings`)
expect(settingsResp?.status()).toBe(200)
const settings = await settingsResp?.json()
expect(settings.rp_id).toBe('test.localhost')
expect(settings.auth_host).toBe(authHost)
expect(settings.own_auth_host).toBe(authHost)
expect(settings.ui_base_path).toBe('/')
// The UI lives at the site root on the auth host
const rootResp = await page.goto(`http://${authHost}/`)
expect(rootResp?.status()).toBe(200)
expect(rootResp?.headers()['content-type']).toContain('text/html')
// /auth/ on the auth host redirects to the root
const redir = await page.request.get(`${baseUrl}/auth/`, {
headers: { Host: authHost },
maxRedirects: 0,
})
expect(redir.status()).toBe(307)
expect(redir.headers()['location']).toMatch(/^http:\/\/auth\.test\.localhost(:\d+)?\/$/)
} finally {
// Restore: no origins, no auth host (later tests expect pristine state)
const restore = await page.request.patch(`${baseUrl}/auth/api/admin/domains/test.localhost`, {
headers,
data: { rp_name: '', origins: {}, related: {} },
})
expect(restore.ok()).toBeTruthy()
}
const after = await page.request.get(`${baseUrl}/auth/api/settings`, {
headers: { Host: 'test.localhost:4404' },
})
expect((await after.json()).auth_host).toBeNull()
})
test('WebSocket cross-domain connections require the origin domain\'s own auth host', async ({ page }) => {
await page.goto(`${domainUrl}/auth/`)
// Same-domain WebSocket receives authentication options...
const sameDomain: any = await page.evaluate(async () => {
return new Promise((resolve) => {
const ws = new WebSocket(`ws://${location.host}/auth/ws/authenticate`)
const timer = setTimeout(() => { ws.close(); resolve({ message: false }) }, 5000)
ws.onmessage = () => { clearTimeout(timer); ws.close(); resolve({ message: true }) }
ws.onerror = () => { clearTimeout(timer); resolve({ message: false }) }
})
})
expect(sameDomain.message).toBe(true)
// ...but a cross-domain connection is closed pre-accept: test.localhost
// has no auth host of its own, so no other host may serve its logins
const crossDomain: any = await page.evaluate(async (host) => {
return new Promise((resolve) => {
const ws = new WebSocket(`ws://${host}/auth/ws/authenticate`)
let message = false
const timer = setTimeout(() => { ws.close(); resolve({ message, code: -1 }) }, 5000)
ws.onmessage = () => { message = true }
ws.onclose = (event) => {
clearTimeout(timer)
resolve({ message, code: event.code, wasClean: event.wasClean })
}
})
}, new URL(baseUrl).host)
expect(crossDomain.message).toBe(false)
expect(crossDomain.wasClean).toBe(false)
})
test('cross-domain remote login via pairing code', async ({ page, virtualAuthenticator }) => {