From 6db02040aa45eb788bc3b1ae819b4d3587feee70 Mon Sep 17 00:00:00 2001 From: Eiji Kitamura Date: Tue, 5 May 2026 17:44:57 +0900 Subject: [PATCH 1/8] feat: implement cross-origin authentication verification with topOrigin support --- .../verifyAuthenticationResponse.test.ts | 226 ++++++++++++++++++ .../verifyAuthenticationResponse.ts | 29 ++- .../src/helpers/decodeClientDataJSON.test.ts | 15 ++ .../src/helpers/decodeClientDataJSON.ts | 1 + 4 files changed, 270 insertions(+), 1 deletion(-) diff --git a/packages/server/src/authentication/verifyAuthenticationResponse.test.ts b/packages/server/src/authentication/verifyAuthenticationResponse.test.ts index f74fe579..6917f977 100644 --- a/packages/server/src/authentication/verifyAuthenticationResponse.test.ts +++ b/packages/server/src/authentication/verifyAuthenticationResponse.test.ts @@ -578,6 +578,232 @@ Deno.test('should return user verified flag after successful auth', async () => assertFalse(verification.authenticationInfo?.userVerified); }); +// Deno.test('should throw when crossOrigin is true but topOrigin is missing', async () => { +// const mockDecodeClientData = stub( +// _decodeClientDataJSONInternals, +// 'stubThis', +// returnsNext([ +// { +// type: 'webauthn.get', +// origin: assertionOrigin, +// challenge: assertionChallenge, +// crossOrigin: true, +// }, +// ]), +// ); +// +// await assertRejects( +// () => +// verifyAuthenticationResponse({ +// response: assertionResponse, +// expectedChallenge: assertionChallenge, +// expectedOrigin: assertionOrigin, +// expectedRPID: 'dev.dontneeda.pw', +// credential, +// }), +// Error, +// 'Invalid cross-origin authentication response - missing topOrigin', +// ); +// +// mockDecodeClientData.restore(); +// }); + +Deno.test('should verify when crossOrigin is true and topOrigin matches expectedTopOrigin (string)', async () => { + const mockDecodeClientData = stub( + _decodeClientDataJSONInternals, + 'stubThis', + returnsNext([ + { + type: 'webauthn.get', + origin: assertionOrigin, + challenge: assertionChallenge, + crossOrigin: true, + topOrigin: 'https://top.origin.com', + }, + ]), + ); + + try { + const verification = await verifyAuthenticationResponse({ + response: assertionResponse, + expectedChallenge: assertionChallenge, + expectedOrigin: assertionOrigin, + expectedRPID: 'dev.dontneeda.pw', + expectedTopOrigin: 'https://top.origin.com', + credential, + requireUserVerification: false, + }); + + assertEquals(verification.verified, true); + } finally { + mockDecodeClientData.restore(); + } +}); + +Deno.test('should throw when crossOrigin is true and topOrigin does not match expectedTopOrigin (string)', async () => { + const mockDecodeClientData = stub( + _decodeClientDataJSONInternals, + 'stubThis', + returnsNext([ + { + type: 'webauthn.get', + origin: assertionOrigin, + challenge: assertionChallenge, + crossOrigin: true, + topOrigin: 'https://wrong.top.origin.com', + }, + ]), + ); + + try { + await assertRejects( + () => + verifyAuthenticationResponse({ + response: assertionResponse, + expectedChallenge: assertionChallenge, + expectedOrigin: assertionOrigin, + expectedRPID: 'dev.dontneeda.pw', + expectedTopOrigin: 'https://top.origin.com', + credential, + }), + Error, + 'Unexpected cross-origin authentication within "https://wrong.top.origin.com", expected: https://top.origin.com', + ); + } finally { + mockDecodeClientData.restore(); + } +}); + +Deno.test('should verify when crossOrigin is true and topOrigin matches one of expectedTopOrigin (array)', async () => { + const mockDecodeClientData = stub( + _decodeClientDataJSONInternals, + 'stubThis', + returnsNext([ + { + type: 'webauthn.get', + origin: assertionOrigin, + challenge: assertionChallenge, + crossOrigin: true, + topOrigin: 'https://top.origin.com', + }, + ]), + ); + + try { + const verification = await verifyAuthenticationResponse({ + response: assertionResponse, + expectedChallenge: assertionChallenge, + expectedOrigin: assertionOrigin, + expectedRPID: 'dev.dontneeda.pw', + expectedTopOrigin: ['https://other.origin.com', 'https://top.origin.com'], + credential, + requireUserVerification: false, + }); + + assertEquals(verification.verified, true); + } finally { + mockDecodeClientData.restore(); + } +}); + +Deno.test('should throw when crossOrigin is true and topOrigin does not match any of expectedTopOrigin (array)', async () => { + const mockDecodeClientData = stub( + _decodeClientDataJSONInternals, + 'stubThis', + returnsNext([ + { + type: 'webauthn.get', + origin: assertionOrigin, + challenge: assertionChallenge, + crossOrigin: true, + topOrigin: 'https://wrong.top.origin.com', + }, + ]), + ); + + try { + await assertRejects( + () => + verifyAuthenticationResponse({ + response: assertionResponse, + expectedChallenge: assertionChallenge, + expectedOrigin: assertionOrigin, + expectedRPID: 'dev.dontneeda.pw', + expectedTopOrigin: ['https://top.origin.com', 'https://other.origin.com'], + credential, + }), + Error, + 'Unexpected cross-origin authentication within "https://wrong.top.origin.com", expected one of: https://top.origin.com, https://other.origin.com', + ); + } finally { + mockDecodeClientData.restore(); + } +}); + +Deno.test('should throw when crossOrigin is true but expectedTopOrigin is not specified', async () => { + const mockDecodeClientData = stub( + _decodeClientDataJSONInternals, + 'stubThis', + returnsNext([ + { + type: 'webauthn.get', + origin: assertionOrigin, + challenge: assertionChallenge, + crossOrigin: true, + topOrigin: 'https://top.origin.com', + }, + ]), + ); + + try { + await assertRejects( + () => + verifyAuthenticationResponse({ + response: assertionResponse, + expectedChallenge: assertionChallenge, + expectedOrigin: assertionOrigin, + expectedRPID: 'dev.dontneeda.pw', + credential, + }), + Error, + 'Unexpected cross-origin authentication within "https://top.origin.com", expected: undefined', + ); + } finally { + mockDecodeClientData.restore(); + } +}); + +Deno.test('should NOT check topOrigin when crossOrigin is false', async () => { + const mockDecodeClientData = stub( + _decodeClientDataJSONInternals, + 'stubThis', + returnsNext([ + { + type: 'webauthn.get', + origin: assertionOrigin, + challenge: assertionChallenge, + crossOrigin: false, + topOrigin: 'https://some.top.origin.com', + }, + ]), + ); + + try { + const verification = await verifyAuthenticationResponse({ + response: assertionResponse, + expectedChallenge: assertionChallenge, + expectedOrigin: assertionOrigin, + expectedRPID: 'dev.dontneeda.pw', + credential, + requireUserVerification: false, + }); + + assertEquals(verification.verified, true); + } finally { + mockDecodeClientData.restore(); + } +}); + /** * Assertion examples below */ diff --git a/packages/server/src/authentication/verifyAuthenticationResponse.ts b/packages/server/src/authentication/verifyAuthenticationResponse.ts index 86fce0ef..13910fca 100644 --- a/packages/server/src/authentication/verifyAuthenticationResponse.ts +++ b/packages/server/src/authentication/verifyAuthenticationResponse.ts @@ -42,6 +42,7 @@ export async function verifyAuthenticationResponse( expectedRPID: string | string[]; credential: WebAuthnCredential; expectedType?: string | string[]; + expectedTopOrigin?: string | string[]; requireUserVerification?: boolean; advancedFIDOConfig?: { userVerification?: UserVerificationRequirement; @@ -54,6 +55,7 @@ export async function verifyAuthenticationResponse( expectedOrigin, expectedRPID, expectedType, + expectedTopOrigin, credential, requireUserVerification = true, advancedFIDOConfig, @@ -87,7 +89,7 @@ export async function verifyAuthenticationResponse( const clientDataJSON = decodeClientDataJSON(assertionResponse.clientDataJSON); - const { type, origin, challenge, tokenBinding } = clientDataJSON; + const { type, origin, challenge, tokenBinding, crossOrigin, topOrigin } = clientDataJSON; // Make sure we're handling an authentication if (Array.isArray(expectedType)) { @@ -120,6 +122,31 @@ export async function verifyAuthenticationResponse( ); } + // Check that the authentication response is within an expected iframe + if (crossOrigin) { + // TODO: Since Safari doesn't support `topOrigin` as of May 2026, only check this when `topOrigin` is available for now. + if (topOrigin) { + if (Array.isArray(expectedTopOrigin)) { + if (!expectedTopOrigin.includes(topOrigin)) { + const joinedExpectedTopOrigin = expectedTopOrigin.join(', '); + throw new Error( + `Unexpected cross-origin authentication within "${topOrigin}", expected one of: ${joinedExpectedTopOrigin}`, + ); + } + } else { + if (topOrigin !== expectedTopOrigin) { + throw new Error( + `Unexpected cross-origin authentication within "${topOrigin}", expected: ${expectedTopOrigin}`, + ); + } + } + // } else { + // throw new Error( + // 'Invalid cross-origin authentication response - missing topOrigin', + // ); + } + } + // Check that the origin is our site if (Array.isArray(expectedOrigin)) { if (!expectedOrigin.includes(origin)) { diff --git a/packages/server/src/helpers/decodeClientDataJSON.test.ts b/packages/server/src/helpers/decodeClientDataJSON.test.ts index eefecb0d..cd946995 100644 --- a/packages/server/src/helpers/decodeClientDataJSON.test.ts +++ b/packages/server/src/helpers/decodeClientDataJSON.test.ts @@ -15,3 +15,18 @@ Deno.test('should convert base64url-encoded attestation clientDataJSON to JSON', }, ); }); + +Deno.test('should convert base64url-encoded clientDataJSON with crossOrigin and topOrigin to JSON', () => { + assertEquals( + decodeClientDataJSON( + 'eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiY2hhbGxlbmdlIiwib3JpZ2luIjoiaHR0cHM6Ly9vcmlnaW4uY29tIiwiY3Jvc3NPcmlnaW4iOnRydWUsInRvcE9yaWdpbiI6Imh0dHBzOi8vdG9wLm9yaWdpbi5jb20ifQ', + ), + { + type: 'webauthn.get', + challenge: 'challenge', + origin: 'https://origin.com', + crossOrigin: true, + topOrigin: 'https://top.origin.com', + }, + ); +}); diff --git a/packages/server/src/helpers/decodeClientDataJSON.ts b/packages/server/src/helpers/decodeClientDataJSON.ts index 22fef212..171ae297 100644 --- a/packages/server/src/helpers/decodeClientDataJSON.ts +++ b/packages/server/src/helpers/decodeClientDataJSON.ts @@ -16,6 +16,7 @@ export type ClientDataJSON = { challenge: string; origin: string; crossOrigin?: boolean; + topOrigin?: string; tokenBinding?: { id?: string; status: 'present' | 'supported' | 'not-supported'; From 6f36b371f89a4b9690273f6e5e89f14ef890454a Mon Sep 17 00:00:00 2001 From: Eiji Kitamura Date: Wed, 6 May 2026 17:02:28 +0900 Subject: [PATCH 2/8] fix: throw error for unexpected cross-origin requests when expectedTopOrigin is missing --- .../verifyAuthenticationResponse.test.ts | 91 +++++++++++++------ .../verifyAuthenticationResponse.ts | 9 +- 2 files changed, 67 insertions(+), 33 deletions(-) diff --git a/packages/server/src/authentication/verifyAuthenticationResponse.test.ts b/packages/server/src/authentication/verifyAuthenticationResponse.test.ts index 6917f977..76bf3a92 100644 --- a/packages/server/src/authentication/verifyAuthenticationResponse.test.ts +++ b/packages/server/src/authentication/verifyAuthenticationResponse.test.ts @@ -578,35 +578,68 @@ Deno.test('should return user verified flag after successful auth', async () => assertFalse(verification.authenticationInfo?.userVerified); }); -// Deno.test('should throw when crossOrigin is true but topOrigin is missing', async () => { -// const mockDecodeClientData = stub( -// _decodeClientDataJSONInternals, -// 'stubThis', -// returnsNext([ -// { -// type: 'webauthn.get', -// origin: assertionOrigin, -// challenge: assertionChallenge, -// crossOrigin: true, -// }, -// ]), -// ); -// -// await assertRejects( -// () => -// verifyAuthenticationResponse({ -// response: assertionResponse, -// expectedChallenge: assertionChallenge, -// expectedOrigin: assertionOrigin, -// expectedRPID: 'dev.dontneeda.pw', -// credential, -// }), -// Error, -// 'Invalid cross-origin authentication response - missing topOrigin', -// ); -// -// mockDecodeClientData.restore(); -// }); +Deno.test('should throw when crossOrigin is true, topOrigin is missing, and expectedTopOrigin is not specified', async () => { + const mockDecodeClientData = stub( + _decodeClientDataJSONInternals, + 'stubThis', + returnsNext([ + { + type: 'webauthn.get', + origin: assertionOrigin, + challenge: assertionChallenge, + crossOrigin: true, + }, + ]), + ); + + try { + await assertRejects( + () => + verifyAuthenticationResponse({ + response: assertionResponse, + expectedChallenge: assertionChallenge, + expectedOrigin: assertionOrigin, + expectedRPID: 'dev.dontneeda.pw', + credential, + }), + Error, + 'Unexpected cross-origin request', + ); + } finally { + mockDecodeClientData.restore(); + } +}); + +Deno.test('should verify when crossOrigin is true, topOrigin is missing, but expectedTopOrigin is specified (Safari workaround)', async () => { + const mockDecodeClientData = stub( + _decodeClientDataJSONInternals, + 'stubThis', + returnsNext([ + { + type: 'webauthn.get', + origin: assertionOrigin, + challenge: assertionChallenge, + crossOrigin: true, + }, + ]), + ); + + try { + const verification = await verifyAuthenticationResponse({ + response: assertionResponse, + expectedChallenge: assertionChallenge, + expectedOrigin: assertionOrigin, + expectedRPID: 'dev.dontneeda.pw', + expectedTopOrigin: 'https://top.origin.com', + credential, + requireUserVerification: false, + }); + + assertEquals(verification.verified, true); + } finally { + mockDecodeClientData.restore(); + } +}); Deno.test('should verify when crossOrigin is true and topOrigin matches expectedTopOrigin (string)', async () => { const mockDecodeClientData = stub( diff --git a/packages/server/src/authentication/verifyAuthenticationResponse.ts b/packages/server/src/authentication/verifyAuthenticationResponse.ts index 13910fca..d0fd9141 100644 --- a/packages/server/src/authentication/verifyAuthenticationResponse.ts +++ b/packages/server/src/authentication/verifyAuthenticationResponse.ts @@ -140,10 +140,11 @@ export async function verifyAuthenticationResponse( ); } } - // } else { - // throw new Error( - // 'Invalid cross-origin authentication response - missing topOrigin', - // ); + } else if (!expectedTopOrigin) { + // If `expectedTopOrigin` is not set, this is an unexpected cross-origin request. + throw new Error( + 'Unexpected cross-origin request', + ); } } From 9d6e68d07a644dbf27c1e44cd3d996ca4d402b26 Mon Sep 17 00:00:00 2001 From: Eiji Kitamura Date: Sun, 10 May 2026 12:50:30 +0900 Subject: [PATCH 3/8] Update packages/server/src/authentication/verifyAuthenticationResponse.ts Co-authored-by: Matthew Miller --- .../server/src/authentication/verifyAuthenticationResponse.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/authentication/verifyAuthenticationResponse.ts b/packages/server/src/authentication/verifyAuthenticationResponse.ts index d0fd9141..9b461a9b 100644 --- a/packages/server/src/authentication/verifyAuthenticationResponse.ts +++ b/packages/server/src/authentication/verifyAuthenticationResponse.ts @@ -130,7 +130,7 @@ export async function verifyAuthenticationResponse( if (!expectedTopOrigin.includes(topOrigin)) { const joinedExpectedTopOrigin = expectedTopOrigin.join(', '); throw new Error( - `Unexpected cross-origin authentication within "${topOrigin}", expected one of: ${joinedExpectedTopOrigin}`, + `Unexpected cross-origin authentication response origin "${topOrigin}", expected one of: ${joinedExpectedTopOrigin}`, ); } } else { From f9ffc78b45ab32bcc249b1fc1e00921579154b42 Mon Sep 17 00:00:00 2001 From: Eiji Kitamura Date: Sun, 10 May 2026 12:50:38 +0900 Subject: [PATCH 4/8] Update packages/server/src/authentication/verifyAuthenticationResponse.ts Co-authored-by: Matthew Miller --- .../server/src/authentication/verifyAuthenticationResponse.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/authentication/verifyAuthenticationResponse.ts b/packages/server/src/authentication/verifyAuthenticationResponse.ts index 9b461a9b..976e910c 100644 --- a/packages/server/src/authentication/verifyAuthenticationResponse.ts +++ b/packages/server/src/authentication/verifyAuthenticationResponse.ts @@ -143,7 +143,7 @@ export async function verifyAuthenticationResponse( } else if (!expectedTopOrigin) { // If `expectedTopOrigin` is not set, this is an unexpected cross-origin request. throw new Error( - 'Unexpected cross-origin request', + 'Unexpected cross-origin authentication response', ); } } From f505cc5d61c71fbd91da8c2bae86d6c0c6a99155 Mon Sep 17 00:00:00 2001 From: Eiji Kitamura Date: Sun, 10 May 2026 12:53:37 +0900 Subject: [PATCH 5/8] fix: throw error when topOrigin is present without cross-origin request --- .../src/authentication/verifyAuthenticationResponse.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/server/src/authentication/verifyAuthenticationResponse.ts b/packages/server/src/authentication/verifyAuthenticationResponse.ts index 976e910c..9c847e57 100644 --- a/packages/server/src/authentication/verifyAuthenticationResponse.ts +++ b/packages/server/src/authentication/verifyAuthenticationResponse.ts @@ -146,6 +146,11 @@ export async function verifyAuthenticationResponse( 'Unexpected cross-origin authentication response', ); } + } else if (topOrigin) { + // If `topOrigin` is set despite `crossOrigin` being false, this is an unexpected request. + throw new Error( + 'Unexpected top origin without cross origin request', + ); } // Check that the origin is our site From 229d817e918f2ae94684b2ae24542879a9cb51b6 Mon Sep 17 00:00:00 2001 From: Eiji Kitamura Date: Mon, 11 May 2026 13:04:27 +0900 Subject: [PATCH 6/8] test: update cross-origin error messages and validate topOrigin constraints in verifyAuthenticationResponse --- .../verifyAuthenticationResponse.test.ts | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/server/src/authentication/verifyAuthenticationResponse.test.ts b/packages/server/src/authentication/verifyAuthenticationResponse.test.ts index 76bf3a92..592e76bc 100644 --- a/packages/server/src/authentication/verifyAuthenticationResponse.test.ts +++ b/packages/server/src/authentication/verifyAuthenticationResponse.test.ts @@ -603,7 +603,7 @@ Deno.test('should throw when crossOrigin is true, topOrigin is missing, and expe credential, }), Error, - 'Unexpected cross-origin request', + 'Unexpected cross-origin authentication response', ); } finally { mockDecodeClientData.restore(); @@ -766,7 +766,7 @@ Deno.test('should throw when crossOrigin is true and topOrigin does not match an credential, }), Error, - 'Unexpected cross-origin authentication within "https://wrong.top.origin.com", expected one of: https://top.origin.com, https://other.origin.com', + 'Unexpected cross-origin authentication response origin "https://wrong.top.origin.com", expected one of: https://top.origin.com, https://other.origin.com', ); } finally { mockDecodeClientData.restore(); @@ -806,7 +806,7 @@ Deno.test('should throw when crossOrigin is true but expectedTopOrigin is not sp } }); -Deno.test('should NOT check topOrigin when crossOrigin is false', async () => { +Deno.test('should throw when topOrigin is set despite crossOrigin being false', async () => { const mockDecodeClientData = stub( _decodeClientDataJSONInternals, 'stubThis', @@ -822,16 +822,19 @@ Deno.test('should NOT check topOrigin when crossOrigin is false', async () => { ); try { - const verification = await verifyAuthenticationResponse({ - response: assertionResponse, - expectedChallenge: assertionChallenge, - expectedOrigin: assertionOrigin, - expectedRPID: 'dev.dontneeda.pw', - credential, - requireUserVerification: false, - }); - - assertEquals(verification.verified, true); + await assertRejects( + () => + verifyAuthenticationResponse({ + response: assertionResponse, + expectedChallenge: assertionChallenge, + expectedOrigin: assertionOrigin, + expectedRPID: 'dev.dontneeda.pw', + credential, + requireUserVerification: false, + }), + Error, + 'Unexpected top origin without cross origin request', + ); } finally { mockDecodeClientData.restore(); } From 23e16af10f5949235bb7e5e3424203003771415a Mon Sep 17 00:00:00 2001 From: Eiji Kitamura Date: Sun, 31 May 2026 10:29:03 +0900 Subject: [PATCH 7/8] Update packages/server/src/authentication/verifyAuthenticationResponse.ts Co-authored-by: Matthew Miller --- .../verifyAuthenticationResponse.ts | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/packages/server/src/authentication/verifyAuthenticationResponse.ts b/packages/server/src/authentication/verifyAuthenticationResponse.ts index 9c847e57..97e80263 100644 --- a/packages/server/src/authentication/verifyAuthenticationResponse.ts +++ b/packages/server/src/authentication/verifyAuthenticationResponse.ts @@ -124,33 +124,46 @@ export async function verifyAuthenticationResponse( // Check that the authentication response is within an expected iframe if (crossOrigin) { - // TODO: Since Safari doesn't support `topOrigin` as of May 2026, only check this when `topOrigin` is available for now. + /** + * TODO: Since Safari doesn't support `topOrigin` as of May 2026, only check this when + * `topOrigin` is available for now. + */ if (topOrigin) { + if (!expectedTopOrigin) { + /** + * If `expectedTopOrigin` is not set, this should be considered an unexpected cross-origin + * request. Reject the response while helping the RP understand how they need to update this + * method call if they want to support verification of such WebAuthn authentication requests. + */ + throw new Error( + `Detected cross-origin authentication response from top origin of "${topOrigin}", but a value for \`expectedTopOrigin\` was not specified when calling \`verifyAuthenticationResponse()\``, + ); + } + if (Array.isArray(expectedTopOrigin)) { if (!expectedTopOrigin.includes(topOrigin)) { const joinedExpectedTopOrigin = expectedTopOrigin.join(', '); throw new Error( - `Unexpected cross-origin authentication response origin "${topOrigin}", expected one of: ${joinedExpectedTopOrigin}`, + `Unexpected cross-origin authentication response top origin of "${topOrigin}", expected one of: ${joinedExpectedTopOrigin}`, ); } } else { if (topOrigin !== expectedTopOrigin) { throw new Error( - `Unexpected cross-origin authentication within "${topOrigin}", expected: ${expectedTopOrigin}`, + `Unexpected cross-origin authentication response top origin of "${topOrigin}", expected: ${expectedTopOrigin}`, ); } } - } else if (!expectedTopOrigin) { - // If `expectedTopOrigin` is not set, this is an unexpected cross-origin request. - throw new Error( - 'Unexpected cross-origin authentication response', - ); } - } else if (topOrigin) { - // If `topOrigin` is set despite `crossOrigin` being false, this is an unexpected request. - throw new Error( - 'Unexpected top origin without cross origin request', - ); + } else { + if (topOrigin) { + /** + * If `topOrigin` is set despite `crossOrigin` being false, this is an unexpected request. + * + * See https://w3c.github.io/webauthn/#dom-collectedclientdata-toporigin. + */ + throw new Error(`Unexpected top origin of "${topOrigin}" within a non-cross-origin authentication response. This error should be reported to the browser vendor as a WebAuthn specification violation with a link to https://w3c.github.io/webauthn/#dom-collectedclientdata-toporigin`); + } } // Check that the origin is our site From e4fc5eee139553b31b0e20f10e699afc226f95bb Mon Sep 17 00:00:00 2001 From: Eiji Kitamura Date: Sun, 31 May 2026 10:38:51 +0900 Subject: [PATCH 8/8] fix: update verifyAuthenticationResponse tests to allow Safari cross-origin behavior and refine cross-origin error messages --- .../verifyAuthenticationResponse.test.ts | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/packages/server/src/authentication/verifyAuthenticationResponse.test.ts b/packages/server/src/authentication/verifyAuthenticationResponse.test.ts index 592e76bc..b52c2e01 100644 --- a/packages/server/src/authentication/verifyAuthenticationResponse.test.ts +++ b/packages/server/src/authentication/verifyAuthenticationResponse.test.ts @@ -578,7 +578,7 @@ Deno.test('should return user verified flag after successful auth', async () => assertFalse(verification.authenticationInfo?.userVerified); }); -Deno.test('should throw when crossOrigin is true, topOrigin is missing, and expectedTopOrigin is not specified', async () => { +Deno.test('should verify when crossOrigin is true, topOrigin is missing, and expectedTopOrigin is not specified (Safari workaround)', async () => { const mockDecodeClientData = stub( _decodeClientDataJSONInternals, 'stubThis', @@ -593,18 +593,16 @@ Deno.test('should throw when crossOrigin is true, topOrigin is missing, and expe ); try { - await assertRejects( - () => - verifyAuthenticationResponse({ - response: assertionResponse, - expectedChallenge: assertionChallenge, - expectedOrigin: assertionOrigin, - expectedRPID: 'dev.dontneeda.pw', - credential, - }), - Error, - 'Unexpected cross-origin authentication response', - ); + const verification = await verifyAuthenticationResponse({ + response: assertionResponse, + expectedChallenge: assertionChallenge, + expectedOrigin: assertionOrigin, + expectedRPID: 'dev.dontneeda.pw', + credential, + requireUserVerification: false, + }); + + assertEquals(verification.verified, true); } finally { mockDecodeClientData.restore(); } @@ -700,7 +698,7 @@ Deno.test('should throw when crossOrigin is true and topOrigin does not match ex credential, }), Error, - 'Unexpected cross-origin authentication within "https://wrong.top.origin.com", expected: https://top.origin.com', + 'Unexpected cross-origin authentication response top origin of "https://wrong.top.origin.com", expected: https://top.origin.com', ); } finally { mockDecodeClientData.restore(); @@ -766,7 +764,7 @@ Deno.test('should throw when crossOrigin is true and topOrigin does not match an credential, }), Error, - 'Unexpected cross-origin authentication response origin "https://wrong.top.origin.com", expected one of: https://top.origin.com, https://other.origin.com', + 'Unexpected cross-origin authentication response top origin of "https://wrong.top.origin.com", expected one of: https://top.origin.com, https://other.origin.com', ); } finally { mockDecodeClientData.restore(); @@ -799,7 +797,7 @@ Deno.test('should throw when crossOrigin is true but expectedTopOrigin is not sp credential, }), Error, - 'Unexpected cross-origin authentication within "https://top.origin.com", expected: undefined', + 'Detected cross-origin authentication response from top origin of "https://top.origin.com", but a value for `expectedTopOrigin` was not specified when calling `verifyAuthenticationResponse()`', ); } finally { mockDecodeClientData.restore(); @@ -833,7 +831,7 @@ Deno.test('should throw when topOrigin is set despite crossOrigin being false', requireUserVerification: false, }), Error, - 'Unexpected top origin without cross origin request', + 'Unexpected top origin of "https://some.top.origin.com" within a non-cross-origin authentication response. This error should be reported to the browser vendor as a WebAuthn specification violation with a link to https://w3c.github.io/webauthn/#dom-collectedclientdata-toporigin', ); } finally { mockDecodeClientData.restore();