- 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.
147 lines
6.1 KiB
TypeScript
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()
|
|
})
|
|
})
|