Files
paskia/e2e/tests/50-multirealm.spec.ts
T
LeoVasanko 8ccc257f44 E2E: two-realm setup, host dispatch, related origins, cross-realm remote login
- global-setup bootstraps via one-shot 'paskia init --rp-id
  localhost,test.localhost' in the test-data directory (which doubles as
  the server cwd, dropping the removed PASKIA_DB), captures the reset
  token from init output (stdout+stderr), then spawns plain serve.
- New 50-multirealm spec: per-host settings dispatch, 421 for unknown
  hosts, /.well-known/webauthn 404 until a related origin is added via
  the admin realm API (and removed again), and a full cross-realm remote
  login: requester on test.localhost, permit on localhost with a fresh
  virtual-authenticator passkey, session validated on test.localhost.
  Asserts the profile enrollment prompt and realm badge render.
- New fixtures/remote-auth.ts drives the remote-auth WS protocol in
  browser context, including the PBKDF2 PoW.
2026-09-06 15:06:20 +00:00

147 lines
6.1 KiB
TypeScript

import { test, expect } from './fixtures/virtual-authenticator'
import {
registerPasskey,
getSessionCookieName,
popDeviceToken,
} from './fixtures/passkey-helpers'
import {
startRemoteAuthRequest,
awaitRemoteAuthSession,
permitRemoteAuth,
} from './fixtures/remote-auth'
/**
* Multi-realm E2E tests.
*
* The server is bootstrapped with two realms: localhost (default) and
* test.localhost. Chrome resolves any *.localhost hostname to loopback, so
* both realms are reachable over real HTTP from the browser.
*
* Covers:
* - Host-based realm dispatch (settings, 421 for unknown hosts)
* - Related Origin Requests well-known endpoint + admin realm API
* - Cross-realm remote login: a passkey registered on localhost permits a
* session on test.localhost via pairing code
* - The profile enrollment prompt on a realm where the user has no passkey
*/
test.describe('Multi-realm E2E', () => {
test.describe.configure({ mode: 'serial' })
const baseUrl = process.env.BASE_URL || 'http://localhost:4404'
const realmUrl = 'http://test.localhost:4404'
test('dispatches realms by host header', async ({ page }) => {
// Browser navigation: Chrome maps *.localhost to loopback
const realmResp = await page.goto(`${realmUrl}/auth/api/settings`)
expect(realmResp?.status()).toBe(200)
const realmSettings = await realmResp?.json()
expect(realmSettings.rp_id).toBe('test.localhost')
expect(realmSettings.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 realms and related origins via API', async ({ page, virtualAuthenticator }) => {
// Fresh session via device token (realm 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 realms
const list = await page.request.get(`${baseUrl}/auth/api/admin/realms/`, { headers })
expect(list.ok()).toBeTruthy()
const realms = await list.json()
expect(realms.map((r: any) => r.rp_id).sort()).toEqual(['localhost', 'test.localhost'])
const localhostRealm = realms.find((r: any) => r.rp_id === 'localhost')
expect(localhostRealm.is_default).toBe(true)
// Add a related origin (unrelated domain) to the localhost realm
const patch = await page.request.patch(`${baseUrl}/auth/api/admin/realms/localhost`, {
headers,
data: { rp_name: '', auth_host: '', origins: ['https://app.example.com'] },
})
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 origins again so later tests see the pristine state
const restore = await page.request.patch(`${baseUrl}/auth/api/admin/realms/localhost`, {
headers,
data: { rp_name: '', auth_host: '', origins: [] },
})
expect(restore.ok()).toBeTruthy()
const after = await page.request.get(`${baseUrl}/.well-known/webauthn`)
expect(after.status()).toBe(404)
})
test('cross-realm 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 realm (no session there)
const reqPage = await page.context().newPage()
await reqPage.goto(`${realmUrl}/auth/`)
const pairingCode = await startRemoteAuthRequest(reqPage)
expect(pairingCode.split('.')).toHaveLength(3)
// Approver permits with the localhost passkey; the "found" message names
// the requesting realm
const found = await permitRemoteAuth(page, pairingCode)
expect(found.rp_id).toBe('test.localhost')
// The requester redeems the exchange code on its own realm 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 realm,
// and the existing localhost passkey carries a realm badge
await reqPage.goto(`${realmUrl}/auth/`)
const notice = reqPage.locator('.realm-enroll-notice')
await expect(notice).toBeVisible({ timeout: 15000 })
await expect(notice).toContainText('test.localhost')
await expect(reqPage.locator('.badge-realm').first()).toHaveText('localhost')
await reqPage.close()
})
})