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:
@@ -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 }) => {
|
||||
|
||||
Reference in New Issue
Block a user