Rename realms to domains; object-keyed origins/related config format
Finish the realm→domain terminology removal across source, tests, e2e and docs. The stored config drops all lists: Config.domains is keyed by rp-id, DomainConfig.origins/related are objects keyed by host (https:// omitted), values True or OriginEntry(auth_host=True). The default/primary domain concept is gone; ordering is display-time. Tests and e2e updated to the new API shapes (not run). Database re-migrated from the legacy backup into the new format.
This commit is contained in:
@@ -11,33 +11,33 @@ import {
|
||||
} from './fixtures/remote-auth'
|
||||
|
||||
/**
|
||||
* Multi-realm E2E tests.
|
||||
* Multi-domain E2E tests.
|
||||
*
|
||||
* The server is bootstrapped with two realms: localhost (default) and
|
||||
* The server is bootstrapped with two domains: localhost (default) and
|
||||
* test.localhost. Chrome resolves any *.localhost hostname to loopback, so
|
||||
* both realms are reachable over real HTTP from the browser.
|
||||
* both domains 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
|
||||
* - 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 realm where the user has no passkey
|
||||
* - The profile enrollment prompt on a domain where the user has no passkey
|
||||
*/
|
||||
|
||||
test.describe('Multi-realm E2E', () => {
|
||||
test.describe('Multi-domain E2E', () => {
|
||||
test.describe.configure({ mode: 'serial' })
|
||||
|
||||
const baseUrl = process.env.BASE_URL || 'http://localhost:4404'
|
||||
const realmUrl = 'http://test.localhost:4404'
|
||||
const domainUrl = 'http://test.localhost:4404'
|
||||
|
||||
test('dispatches realms by host header', async ({ page }) => {
|
||||
test('dispatches domains 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 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)
|
||||
@@ -58,8 +58,8 @@ test.describe('Multi-realm E2E', () => {
|
||||
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)
|
||||
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/')
|
||||
@@ -68,18 +68,18 @@ test.describe('Multi-realm E2E', () => {
|
||||
|
||||
const headers = { Cookie: `${getSessionCookieName()}=${reg.session_token}` }
|
||||
|
||||
// List realms
|
||||
const list = await page.request.get(`${baseUrl}/auth/api/admin/realms/`, { headers })
|
||||
// List domains
|
||||
const list = await page.request.get(`${baseUrl}/auth/api/admin/domains/`, { 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)
|
||||
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 realm
|
||||
const patch = await page.request.patch(`${baseUrl}/auth/api/admin/realms/localhost`, {
|
||||
// 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: '', auth_host: '', origins: [], related_origins: ['https://app.example.com'] },
|
||||
data: { rp_name: '', origins: {}, related: { 'app.example.com': true } },
|
||||
})
|
||||
expect(patch.ok()).toBeTruthy()
|
||||
|
||||
@@ -90,16 +90,16 @@ test.describe('Multi-realm E2E', () => {
|
||||
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/realms/localhost`, {
|
||||
const restore = await page.request.patch(`${baseUrl}/auth/api/admin/domains/localhost`, {
|
||||
headers,
|
||||
data: { rp_name: '', auth_host: '', origins: [], related_origins: [] },
|
||||
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-realm remote login via pairing code', async ({ page, virtualAuthenticator }) => {
|
||||
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')
|
||||
@@ -107,19 +107,19 @@ test.describe('Multi-realm E2E', () => {
|
||||
const reg = await registerPasskey(page, baseUrl, { resetToken: deviceToken })
|
||||
expect(reg.session_token).toBeTruthy()
|
||||
|
||||
// Requester page on the other realm (no session there)
|
||||
// Requester page on the other domain (no session there)
|
||||
const reqPage = await page.context().newPage()
|
||||
await reqPage.goto(`${realmUrl}/auth/`)
|
||||
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 realm
|
||||
// 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 realm and the
|
||||
// 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)
|
||||
@@ -133,13 +133,13 @@ test.describe('Multi-realm E2E', () => {
|
||||
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')
|
||||
// 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-realm').first()).toHaveText('localhost')
|
||||
await expect(reqPage.locator('.badge-domain').first()).toHaveText('localhost')
|
||||
|
||||
await reqPage.close()
|
||||
})
|
||||
Vendored
+3
-3
@@ -32,7 +32,7 @@ const b64helpersSource = `
|
||||
|
||||
/**
|
||||
* Start a remote auth request on the given page (the device wanting to log in).
|
||||
* The page must already be navigated to the requesting realm's origin.
|
||||
* The page must already be navigated to the requesting domain's origin.
|
||||
* Keeps the WebSocket open on window.__raWs and collects later messages into
|
||||
* window.__raMsgs; resolves with the pairing code.
|
||||
*/
|
||||
@@ -107,8 +107,8 @@ export async function awaitRemoteAuthSession(page: Page, timeoutMs = 90000): Pro
|
||||
/**
|
||||
* Permit a remote auth request from the given page (the authenticating device).
|
||||
* The page must be on the approver's origin with a valid session cookie and a
|
||||
* virtual authenticator holding a credential for that realm.
|
||||
* Resolves with the "found" message (includes the requesting realm's rp_id).
|
||||
* virtual authenticator holding a credential for that domain.
|
||||
* Resolves with the "found" message (includes the requesting domain's rp_id).
|
||||
*/
|
||||
export async function permitRemoteAuth(page: Page, code: string): Promise<any> {
|
||||
return page.evaluate(async ({ code, powSrc, b64src }) => {
|
||||
|
||||
@@ -20,7 +20,7 @@ interface TestState {
|
||||
/**
|
||||
* Global setup for E2E tests.
|
||||
*
|
||||
* Bootstraps a fresh combined database (paskia.kantadb) with two realms —
|
||||
* Bootstraps a fresh combined database (paskia.kantadb) with two domains —
|
||||
* localhost (default) and test.localhost — then starts the server with the
|
||||
* test data directory as its working directory. Captures the bootstrap reset
|
||||
* token from 'paskia init' output for initial user registration.
|
||||
@@ -44,7 +44,7 @@ export default async function globalSetup() {
|
||||
|
||||
const state: TestState = {}
|
||||
|
||||
// Bootstrap the database: two realms, localhost (default) and test.localhost
|
||||
// Bootstrap the database: two domains, localhost (default) and test.localhost
|
||||
console.log(' Bootstrapping database with paskia init...')
|
||||
const initResult = spawnSync(
|
||||
'uv',
|
||||
@@ -125,7 +125,7 @@ export default async function globalSetup() {
|
||||
}
|
||||
state.sessionCookie = settings.session_cookie
|
||||
console.log(` ✅ Session cookie name: ${state.sessionCookie}`)
|
||||
console.log(` ✅ Realm: ${settings.rp_id} (${settings.rp_name})\n`)
|
||||
console.log(` ✅ Domain: ${settings.rp_id} (${settings.rp_name})\n`)
|
||||
|
||||
// Save state for tests
|
||||
writeFileSync(stateFile, JSON.stringify(state, null, 2))
|
||||
|
||||
Reference in New Issue
Block a user