summaryrefslogtreecommitdiffhomepage
path: root/packages/server/src
diff options
context:
space:
mode:
authorJordan Tucker <jordanbtucker@gmail.com>2023-08-28 14:28:52 -0500
committerJordan Tucker <jordanbtucker@gmail.com>2023-08-28 14:28:52 -0500
commitab1a3e42dfd14301b278d86a677f73b0cb7cf37a (patch)
tree3fab7c8134e779e9677c4534e5958dc1dfaea570 /packages/server/src
parent0d9eda359379d8704eeda1995607bbd27de4ebe2 (diff)
Allow expectedChallenge to return a Promise
Diffstat (limited to 'packages/server/src')
-rw-r--r--packages/server/src/authentication/verifyAuthenticationResponse.test.ts76
-rw-r--r--packages/server/src/authentication/verifyAuthenticationResponse.ts4
-rw-r--r--packages/server/src/registration/verifyRegistrationResponse.test.ts65
-rw-r--r--packages/server/src/registration/verifyRegistrationResponse.ts4
4 files changed, 145 insertions, 4 deletions
diff --git a/packages/server/src/authentication/verifyAuthenticationResponse.test.ts b/packages/server/src/authentication/verifyAuthenticationResponse.test.ts
index bf2a79a..822bdd9 100644
--- a/packages/server/src/authentication/verifyAuthenticationResponse.test.ts
+++ b/packages/server/src/authentication/verifyAuthenticationResponse.test.ts
@@ -394,6 +394,82 @@ Deno.test('should fail verification if custom challenge verifier returns false',
);
});
+Deno.test('should pass verification if custom challenge verifier returns a Promise that resolves with true', async () => {
+ const verification = await verifyAuthenticationResponse({
+ response: {
+ id:
+ 'AaIBxnYfL2pDWJmIii6CYgHBruhVvFGHheWamphVioG_TnEXxKA9MW4FWnJh21zsbmRpRJso9i2JmAtWOtXfVd4oXTgYVusXwhWWsA',
+ rawId:
+ 'AaIBxnYfL2pDWJmIii6CYgHBruhVvFGHheWamphVioG_TnEXxKA9MW4FWnJh21zsbmRpRJso9i2JmAtWOtXfVd4oXTgYVusXwhWWsA',
+ response: {
+ authenticatorData: 'SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFYftypQ',
+ clientDataJSON:
+ 'eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiZXlKaFkzUjFZV3hEYUdGc2JHVnVaMlVpT2lKTE0xRjRUMnB1VmtwTWFVZHNibFpGY0RWMllUVlJTbVZOVmxkT1psODNVRmxuZFhSbllrRjBRVlZCSWl3aVlYSmlhWFJ5WVhKNVJHRjBZU0k2SW5OcFoyNU5aVkJzWldGelpTSjkiLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjgwMDAiLCJjcm9zc09yaWdpbiI6ZmFsc2V9',
+ signature:
+ 'MEUCIByFAVGfkoKPEzynp-37BX_HOXSaC6-58-ELjB7BG9opAiEAyD_1mN9YAPrphcwpzK3ym2Xx8EjAapgQ326mKgQ1pW0',
+ userHandle: 'internalUserId',
+ },
+ type: 'public-key',
+ clientExtensionResults: {},
+ },
+ expectedChallenge: (challenge: string) => {
+ const parsedChallenge: {
+ actualChallenge: string;
+ arbitraryData: string;
+ } = JSON.parse(
+ isoBase64URL.toString(challenge),
+ );
+ return Promise.resolve(
+ parsedChallenge.actualChallenge ===
+ 'K3QxOjnVJLiGlnVEp5va5QJeMVWNf_7PYgutgbAtAUA',
+ );
+ },
+ expectedOrigin: 'http://localhost:8000',
+ expectedRPID: 'localhost',
+ authenticator: {
+ credentialID: isoBase64URL.toBuffer(
+ 'AaIBxnYfL2pDWJmIii6CYgHBruhVvFGHheWamphVioG_TnEXxKA9MW4FWnJh21zsbmRpRJso9i2JmAtWOtXfVd4oXTgYVusXwhWWsA',
+ ),
+ credentialPublicKey: isoBase64URL.toBuffer(
+ 'pQECAyYgASFYILTrxTUQv3X4DRM6L_pk65FSMebenhCx3RMsTKoBm-AxIlggEf3qk5552QLNSh1T1oQs7_2C2qysDwN4r4fCp52Hsqs',
+ ),
+ counter: 0,
+ },
+ });
+
+ assert(verification.verified);
+});
+
+Deno.test('should fail verification if custom challenge verifier returns a Promise that resolves with false', async () => {
+ await assertRejects(
+ () =>
+ verifyAuthenticationResponse({
+ response: assertionResponse,
+ expectedChallenge: (challenge) => Promise.resolve(challenge === 'willNeverMatch'),
+ expectedOrigin: assertionOrigin,
+ expectedRPID: 'dev.dontneeda.pw',
+ authenticator: authenticator,
+ }),
+ Error,
+ 'Custom challenge verifier returned false',
+ );
+});
+
+Deno.test('should fail verification if custom challenge verifier returns a Promise that rejects', async () => {
+ await assertRejects(
+ () =>
+ verifyAuthenticationResponse({
+ response: assertionResponse,
+ expectedChallenge: () => Promise.reject(new Error('rejected')),
+ expectedOrigin: assertionOrigin,
+ expectedRPID: 'dev.dontneeda.pw',
+ authenticator: authenticator,
+ }),
+ Error,
+ 'rejected',
+ );
+});
+
Deno.test('should return authenticator extension output', async () => {
const verification = await verifyAuthenticationResponse({
response: {
diff --git a/packages/server/src/authentication/verifyAuthenticationResponse.ts b/packages/server/src/authentication/verifyAuthenticationResponse.ts
index d3c2484..41370a0 100644
--- a/packages/server/src/authentication/verifyAuthenticationResponse.ts
+++ b/packages/server/src/authentication/verifyAuthenticationResponse.ts
@@ -15,7 +15,7 @@ import { isoBase64URL, isoUint8Array } from '../helpers/iso/index.ts';
export type VerifyAuthenticationResponseOpts = {
response: AuthenticationResponseJSON;
- expectedChallenge: string | ((challenge: string) => boolean);
+ expectedChallenge: string | ((challenge: string) => boolean | Promise<boolean>);
expectedOrigin: string | string[];
expectedRPID: string | string[];
authenticator: AuthenticatorDevice;
@@ -94,7 +94,7 @@ export async function verifyAuthenticationResponse(
// Ensure the device provided the challenge we gave it
if (typeof expectedChallenge === 'function') {
- if (!expectedChallenge(challenge)) {
+ if (!(await expectedChallenge(challenge))) {
throw new Error(
`Custom challenge verifier returned false for registration response challenge "${challenge}"`,
);
diff --git a/packages/server/src/registration/verifyRegistrationResponse.test.ts b/packages/server/src/registration/verifyRegistrationResponse.test.ts
index 26a1d77..59dbd13 100644
--- a/packages/server/src/registration/verifyRegistrationResponse.test.ts
+++ b/packages/server/src/registration/verifyRegistrationResponse.test.ts
@@ -750,6 +750,71 @@ Deno.test('should fail verification if custom challenge verifier returns false',
);
});
+Deno.test('should pass verification if custom challenge verifier returns a Promise that resolves with true', async () => {
+ const verification = await verifyRegistrationResponse({
+ response: {
+ id:
+ 'AUywDsPYEOoucI3-o-jB1J6Kt6QAxLMa1WwFKj1bNi4pAakWAsZX-pJ4gAeDmocL7SXnl8vzUfLkfrOGIVmds1RhjU1DYIWlxcGhAA',
+ rawId:
+ 'AUywDsPYEOoucI3-o-jB1J6Kt6QAxLMa1WwFKj1bNi4pAakWAsZX-pJ4gAeDmocL7SXnl8vzUfLkfrOGIVmds1RhjU1DYIWlxcGhAA',
+ response: {
+ attestationObject:
+ 'o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIhAPgoy3sxIeUvN9Mo8twyIQb9hXDHxQ2urIaEq14u6vNHAiB8ltlCippsMIIsh6AqMoZlUH_BH0bXT1xsN2zKoCEy72hhdXRoRGF0YVjQSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFYfWYqK3OAAI1vMYKZIsLJfHwVQMATAFMsA7D2BDqLnCN_qPowdSeirekAMSzGtVsBSo9WzYuKQGpFgLGV_qSeIAHg5qHC-0l55fL81Hy5H6zhiFZnbNUYY1NQ2CFpcXBoQClAQIDJiABIVggPzMMB0nPKu9zvu6tvvyaP7MlGKJi4zazYQw5kyCjGykiWCCyHxcnMCwcj4llYwRY-MedgOCQzcz_TgKeabY4yFQyrA',
+ clientDataJSON:
+ 'eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiZXlKaFkzUjFZV3hEYUdGc2JHVnVaMlVpT2lKNFVuTlpaRU5SZGpWWFdrOXhiWGhTWldsYWJEWkRPWEUxVTJaeVdtNWxOR3hPVTNJNVVWWjBVR2xuSWl3aVlYSmlhWFJ5WVhKNVJHRjBZU0k2SW1GeVltbDBjbUZ5ZVVSaGRHRkdiM0pUYVdkdWFXNW5JbjAiLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjgwMDAiLCJjcm9zc09yaWdpbiI6ZmFsc2V9',
+ transports: ['internal'],
+ },
+ type: 'public-key',
+ clientExtensionResults: {},
+ },
+ expectedChallenge: (challenge: string) => {
+ const parsedChallenge: {
+ actualChallenge: string;
+ arbitraryData: string;
+ } = JSON.parse(
+ isoBase64URL.toString(challenge),
+ );
+ return Promise.resolve(
+ parsedChallenge.actualChallenge ===
+ 'xRsYdCQv5WZOqmxReiZl6C9q5SfrZne4lNSr9QVtPig',
+ );
+ },
+ expectedOrigin: 'http://localhost:8000',
+ expectedRPID: 'localhost',
+ });
+
+ assert(verification.verified);
+});
+
+Deno.test('should fail verification if custom challenge verifier returns a Promise that resolves with false', async () => {
+ await assertRejects(
+ () =>
+ verifyRegistrationResponse({
+ response: attestationNone,
+ expectedChallenge: (challenge: string) =>
+ Promise.resolve(challenge === 'thisWillneverMatch'),
+ expectedOrigin: 'https://dev.dontneeda.pw',
+ expectedRPID: 'dev.dontneeda.pw',
+ }),
+ Error,
+ 'Custom challenge verifier returned false',
+ );
+});
+
+Deno.test('should fail verification if custom challenge verifier returns a Promise that rejects', async () => {
+ await assertRejects(
+ () =>
+ verifyRegistrationResponse({
+ response: attestationNone,
+ expectedChallenge: (challenge: string) => Promise.reject(new Error('rejected')),
+ expectedOrigin: 'https://dev.dontneeda.pw',
+ expectedRPID: 'dev.dontneeda.pw',
+ }),
+ Error,
+ 'rejected',
+ );
+});
+
Deno.test('should return credential backup info', async () => {
const verification = await verifyRegistrationResponse({
response: attestationNone,
diff --git a/packages/server/src/registration/verifyRegistrationResponse.ts b/packages/server/src/registration/verifyRegistrationResponse.ts
index 081d31d..154d31a 100644
--- a/packages/server/src/registration/verifyRegistrationResponse.ts
+++ b/packages/server/src/registration/verifyRegistrationResponse.ts
@@ -30,7 +30,7 @@ import { verifyAttestationApple } from './verifications/verifyAttestationApple.t
export type VerifyRegistrationResponseOpts = {
response: RegistrationResponseJSON;
- expectedChallenge: string | ((challenge: string) => boolean);
+ expectedChallenge: string | ((challenge: string) => boolean | Promise<boolean>);
expectedOrigin: string | string[];
expectedRPID?: string | string[];
requireUserVerification?: boolean;
@@ -95,7 +95,7 @@ export async function verifyRegistrationResponse(
// Ensure the device provided the challenge we gave it
if (typeof expectedChallenge === 'function') {
- if (!expectedChallenge(challenge)) {
+ if (!(await expectedChallenge(challenge))) {
throw new Error(
`Custom challenge verifier returned false for registration response challenge "${challenge}"`,
);