diff --git a/verify/CONFORMANCE.md b/verify/CONFORMANCE.md index 3f2eed4..8f58a2b 100644 --- a/verify/CONFORMANCE.md +++ b/verify/CONFORMANCE.md @@ -75,8 +75,9 @@ the `Authorization` header, and no cookies are set. The access token is accepted the pair, and a used refresh token answers `401` after the reuse window. Logout ends the session. -**Guard.** `/api/me` answers `401` with no session or an altered access cookie, and the -signed-in user's id with a valid one. +**Guard.** `/api/me` answers `401` with no session, an altered access cookie, or the +sign-in flow's ephemeral cookie presented as the access cookie, and the signed-in user's +id with a valid one. **Errors.** An API validation failure keeps its `400` and body (`error: "invalid_request"` with `details.issues`). No session answers `401` with an `error` diff --git a/verify/harness/adapter/protectedEndpoint.spec.ts b/verify/harness/adapter/protectedEndpoint.spec.ts index ceae886..af034c4 100644 --- a/verify/harness/adapter/protectedEndpoint.spec.ts +++ b/verify/harness/adapter/protectedEndpoint.spec.ts @@ -1,6 +1,7 @@ import { request as playwrightRequest } from '@playwright/test'; import { cookieSignup, currentUserId } from '../lib/conformanceFlows'; +import { cookieNamed } from '../lib/cookies'; import { expect, test } from '../lib/fixtures'; // GET /api/me is the reference app's own route behind the adapter's guard, so these @@ -38,4 +39,29 @@ test.describe('protected app endpoint (adapter, cookies)', () => { await browser.dispose(); } }); + + // Every adapter cookie can be signed with the same secret, and the sign-in flow + // hands out the ephemeral cookie before any factor is proven. It must never pass + // for a session. + test('refuses the sign-in flow cookie presented as the session cookie', async ({ + adapterActor, + adapterUrl, + }) => { + await cookieSignup(adapterActor.ctx, adapterActor.email); + const login = await adapterActor.ctx.post('/auth/login', { + data: { identifier: adapterActor.email }, + }); + const ephemeral = cookieNamed(login, 'seamless-ephemeral'); + expect(ephemeral, 'login set the ephemeral cookie').toBeDefined(); + + const browser = await playwrightRequest.newContext({ + baseURL: adapterUrl, + extraHTTPHeaders: { cookie: `seamless-access=${ephemeral!.value}` }, + }); + try { + expect((await browser.get('/api/me')).status()).toBe(401); + } finally { + await browser.dispose(); + } + }); });