From 1ce71d7200be0ee1d39f7653b951856d08d16434 Mon Sep 17 00:00:00 2001 From: Brandon Corbett Date: Tue, 20 Oct 2026 21:18:37 -0400 Subject: [PATCH] test(verify): send the OAuth state binding with harness callbacks --- .changeset/harness-oauth-binding.md | 5 +++++ verify/harness/lib/cutoverFlows.ts | 4 +++- verify/harness/lib/flows.ts | 4 ++-- 3 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 .changeset/harness-oauth-binding.md diff --git a/.changeset/harness-oauth-binding.md b/.changeset/harness-oauth-binding.md new file mode 100644 index 0000000..6c733ee --- /dev/null +++ b/.changeset/harness-oauth-binding.md @@ -0,0 +1,5 @@ +--- +"seamless-cli": patch +--- + +`seamless verify` now forwards the `binding` that `POST /oauth/:providerId/start` returns in the body of its OAuth callbacks, so the OAuth specs pass against an auth API that ties the callback to the browser that started the flow. Older API images ignore the extra field. diff --git a/verify/harness/lib/cutoverFlows.ts b/verify/harness/lib/cutoverFlows.ts index 781ab87..117fc39 100644 --- a/verify/harness/lib/cutoverFlows.ts +++ b/verify/harness/lib/cutoverFlows.ts @@ -87,7 +87,8 @@ export async function legacySignIn( }); expect(start.ok(), `oauth start -> ${start.status()} ${await start.text()}`).toBeTruthy(); - const authorizationUrl = new URL((await start.json()).authorizationUrl); + const { authorizationUrl: startUrl, binding } = await start.json(); + const authorizationUrl = new URL(startUrl); authorizationUrl.searchParams.set('mock_sub', profile.sub); authorizationUrl.searchParams.set('mock_oid', profile.oid); authorizationUrl.searchParams.set('mock_email', profile.email); @@ -100,6 +101,7 @@ export async function legacySignIn( data: { code: redirected.searchParams.get('code'), state: redirected.searchParams.get('state'), + binding, }, }); } diff --git a/verify/harness/lib/flows.ts b/verify/harness/lib/flows.ts index 4040f64..78d9e92 100644 --- a/verify/harness/lib/flows.ts +++ b/verify/harness/lib/flows.ts @@ -149,7 +149,7 @@ export async function oauthLogin( ): Promise { const start = await ctx.post(`${pathPrefix}/oauth/${providerId}/start`, { data: {} }); expect(start.ok(), `oauth start -> ${start.status()} ${await start.text()}`).toBeTruthy(); - const { authorizationUrl } = await start.json(); + const { authorizationUrl, binding } = await start.json(); const authorize = await ctx.get(authorizationUrl, { maxRedirects: 0 }); expect(authorize.status(), `authorize redirects with a code (got ${authorize.status()})`).toBe( @@ -161,7 +161,7 @@ export async function oauthLogin( expect(code, 'authorize returns an auth code').toBeTruthy(); const callback = await ctx.post(`${pathPrefix}/oauth/${providerId}/callback`, { - data: { code, state }, + data: { code, state, binding }, }); expect(callback.ok(), `oauth callback -> ${callback.status()} ${await callback.text()}`).toBeTruthy(); const body = await callback.json();