import { test, expect } from './fixtures/virtual-authenticator' import { registerPasskey, getSessionCookieName, popDeviceToken, } from './fixtures/passkey-helpers' import { startRemoteAuthRequest, awaitRemoteAuthSession, permitRemoteAuth, } from './fixtures/remote-auth' /** * Multi-domain E2E tests. * * The server is bootstrapped with two domains: localhost (default) and * test.localhost. Chrome resolves any *.localhost hostname to loopback, so * both domains are reachable over real HTTP from the browser. * * Covers: * - Host-based domain dispatch (settings, 421 for unknown hosts) * - Related Origin Requests well-known endpoint + admin domain API * - 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 */ test.describe('Multi-domain E2E', () => { test.describe.configure({ mode: 'serial' }) const baseUrl = process.env.BASE_URL || 'http://localhost:4404' const domainUrl = 'http://test.localhost:4404' test('dispatches domains by host header', async ({ page }) => { // Browser navigation: Chrome maps *.localhost to loopback const domainResp = await page.goto(`${domainUrl}/auth/api/settings`) expect(domainResp?.status()).toBe(200) const domainSettings = await domainResp?.json() expect(domainSettings.rp_id).toBe('test.localhost') expect(domainSettings.own_auth_host).toBeNull() 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') // Unknown host is rejected with 421 Misdirected Request. // page.request is Node-side, so target loopback with an explicit Host. const unknownResp = await page.request.get(`${baseUrl}/auth/api/settings`, { headers: { Host: 'unknown.example.org' }, }) expect(unknownResp.status()).toBe(421) }) test('well-known webauthn endpoint reflects related origins', async ({ page }) => { // No related origins configured initially → 404 const before = await page.request.get(`${baseUrl}/.well-known/webauthn`) expect(before.status()).toBe(404) }) test('master admin manages domains and related origins via API', 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}` } // List domains const list = await page.request.get(`${baseUrl}/auth/api/admin/domains/`, { headers }) expect(list.ok()).toBeTruthy() const domains = await list.json() expect(domains.map((r: any) => r.rp_id).sort()).toEqual(['localhost', 'test.localhost']) const localhostDomain = domains.find((r: any) => r.rp_id === 'localhost') expect(localhostDomain.origins).toEqual({}) // Add a related origin (unrelated domain) to the localhost domain const patch = await page.request.patch(`${baseUrl}/auth/api/admin/domains/localhost`, { headers, data: { rp_name: '', origins: {}, related: { 'app.example.com': true } }, }) expect(patch.ok()).toBeTruthy() // The well-known endpoint now lists it const wk = await page.request.get(`${baseUrl}/.well-known/webauthn`) expect(wk.ok()).toBeTruthy() const wkJson = await wk.json() expect(wkJson.origins).toContain('https://app.example.com') // 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, data: { rp_name: '', origins: {}, related: {} }, }) expect(restore.ok()).toBeTruthy() const after = await page.request.get(`${baseUrl}/.well-known/webauthn`) expect(after.status()).toBe(404) }) test('cross-domain remote login via pairing code', async ({ page, virtualAuthenticator }) => { // Register a fresh passkey on localhost (this test's virtual authenticator) 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() // Requester page on the other domain (no session there) const reqPage = await page.context().newPage() await reqPage.goto(`${domainUrl}/auth/`) const pairingCode = await startRemoteAuthRequest(reqPage) expect(pairingCode.split('.')).toHaveLength(3) // Approver permits with the localhost passkey; the "found" message names // the requesting domain const found = await permitRemoteAuth(page, pairingCode) expect(found.rp_id).toBe('test.localhost') // The requester redeems the exchange code on its own domain and the // session validates there for the same user const validation = await awaitRemoteAuthSession(reqPage) expect(validation.ctx.user.uuid).toBe(reg.user) // The session is recorded with the requesting host const userInfo = await reqPage.evaluate(async () => { const resp = await fetch('/auth/api/user-info') if (!resp.ok) throw new Error(`user-info failed: ${resp.status}`) return resp.json() }) const current = Object.values(userInfo.sessions as any[]).find((s: any) => s.is_current) as any expect(current.host).toContain('test.localhost') // The profile on test.localhost prompts adding a passkey for this domain, // and the existing localhost passkey carries a domain badge await reqPage.goto(`${domainUrl}/auth/`) const notice = reqPage.locator('.domain-enroll-notice') await expect(notice).toBeVisible({ timeout: 15000 }) await expect(notice).toContainText('test.localhost') await expect(reqPage.locator('.badge-domain').first()).toHaveText('localhost') await reqPage.close() }) })