summaryrefslogtreecommitdiffhomepage
path: root/packages/browser/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/browser/src')
-rw-r--r--packages/browser/src/helpers/webAuthnAbortService.test.ts35
-rw-r--r--packages/browser/src/helpers/webAuthnAbortService.ts33
-rw-r--r--packages/browser/src/index.test.ts4
-rw-r--r--packages/browser/src/index.ts2
-rw-r--r--packages/browser/src/methods/startAuthentication.test.ts5
-rw-r--r--packages/browser/src/methods/startAuthentication.ts4
-rw-r--r--packages/browser/src/methods/startRegistration.test.ts5
-rw-r--r--packages/browser/src/methods/startRegistration.ts4
8 files changed, 69 insertions, 23 deletions
diff --git a/packages/browser/src/helpers/webAuthnAbortService.test.ts b/packages/browser/src/helpers/webAuthnAbortService.test.ts
index 506bb2a..87d2535 100644
--- a/packages/browser/src/helpers/webAuthnAbortService.test.ts
+++ b/packages/browser/src/helpers/webAuthnAbortService.test.ts
@@ -1,23 +1,23 @@
-import { webauthnAbortService } from './webAuthnAbortService';
+import { WebAuthnAbortService } from './webAuthnAbortService';
test('should create a new abort signal every time', () => {
- const signal1 = webauthnAbortService.createNewAbortSignal();
- const signal2 = webauthnAbortService.createNewAbortSignal();
+ const signal1 = WebAuthnAbortService.createNewAbortSignal();
+ const signal2 = WebAuthnAbortService.createNewAbortSignal();
expect(signal2).not.toBe(signal1);
});
test('should call abort() with AbortError on existing controller when creating a new signal', () => {
// Populate `.controller`
- webauthnAbortService.createNewAbortSignal();
+ WebAuthnAbortService.createNewAbortSignal();
// Spy on the existing instance of AbortController
const abortSpy = jest.fn();
// @ts-ignore: Ignore the fact that `controller` is private
- webauthnAbortService.controller.abort = abortSpy;
+ WebAuthnAbortService.controller.abort = abortSpy;
// Generate a new signal, which should call `abort()` on the existing controller
- webauthnAbortService.createNewAbortSignal();
+ WebAuthnAbortService.createNewAbortSignal();
expect(abortSpy).toHaveBeenCalledTimes(1);
// Make sure we raise an AbortError so it can be detected correctly
@@ -25,3 +25,26 @@ test('should call abort() with AbortError on existing controller when creating a
expect(abortReason).toBeInstanceOf(Error);
expect(abortReason.name).toEqual('AbortError');
});
+
+test('should cancel active WebAuthn ceremony when manually cancelled', () => {
+ // Populate `.controller`
+ WebAuthnAbortService.createNewAbortSignal();
+
+ // Spy on the existing instance of AbortController
+ const abortSpy = jest.fn();
+ // @ts-ignore: Ignore the fact that `controller` is private
+ WebAuthnAbortService.controller.abort = abortSpy;
+
+ // Cancel the in-flight ceremony, which should call `abort()` on the existing controller
+ WebAuthnAbortService.cancelCeremony();
+ expect(abortSpy).toHaveBeenCalledTimes(1);
+
+ // Make sure we raise an AbortError so it can be detected correctly
+ const abortReason = abortSpy.mock.calls[0][0];
+ expect(abortReason).toBeInstanceOf(Error);
+ expect(abortReason.name).toEqual('AbortError');
+
+ // Ensure that we don't set up a new AbortController because it's unnecessary to do so
+ // @ts-ignore: Ignore the fact that `controller` is private
+ expect(WebAuthnAbortService.controller).toBeUndefined();
+});
diff --git a/packages/browser/src/helpers/webAuthnAbortService.ts b/packages/browser/src/helpers/webAuthnAbortService.ts
index 50e00ba..395ceea 100644
--- a/packages/browser/src/helpers/webAuthnAbortService.ts
+++ b/packages/browser/src/helpers/webAuthnAbortService.ts
@@ -1,13 +1,10 @@
-/**
- * A way to cancel an existing WebAuthn request, for example to cancel a
- * WebAuthn autofill authentication request for a manual authentication attempt.
- */
-class WebAuthnAbortService {
+class BaseWebAuthnAbortService {
private controller: AbortController | undefined;
/**
* Prepare an abort signal that will help support multiple auth attempts without needing to
- * reload the page
+ * reload the page. This is automatically called whenever `startRegistration()` and
+ * `startAuthentication()` are called.
*/
createNewAbortSignal() {
// Abort any existing calls to navigator.credentials.create() or navigator.credentials.get()
@@ -24,6 +21,28 @@ class WebAuthnAbortService {
this.controller = newController;
return newController.signal;
}
+
+ /**
+ * Manually cancel any active WebAuthn registration or authentication attempt.
+ */
+ cancelCeremony() {
+ if (this.controller) {
+ const abortError = new Error(
+ 'Manually cancelling existing WebAuthn API call',
+ );
+ abortError.name = 'AbortError';
+ this.controller.abort(abortError);
+
+ this.controller = undefined;
+ }
+ }
}
-export const webauthnAbortService = new WebAuthnAbortService();
+/**
+ * A service singleton to help ensure that only a single WebAuthn ceremony is active at a time.
+ *
+ * Users of **@simplewebauthn/browser** shouldn't typically need to use this, but it can help e.g.
+ * developers building projects that use client-side routing to better control the behavior of
+ * their UX in response to router navigation events.
+ */
+export const WebAuthnAbortService = new BaseWebAuthnAbortService();
diff --git a/packages/browser/src/index.test.ts b/packages/browser/src/index.test.ts
index 4c726aa..b1a1a5d 100644
--- a/packages/browser/src/index.test.ts
+++ b/packages/browser/src/index.test.ts
@@ -27,3 +27,7 @@ test('should export method `base64URLStringToBuffer`', () => {
test('should export method `bufferToBase64URLString`', () => {
expect(index.bufferToBase64URLString).toBeDefined();
});
+
+test('should export singleton `WebAuthnAbortService`', () => {
+ expect(index.WebAuthnAbortService).toBeDefined();
+});
diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts
index 9e75d75..d45fafb 100644
--- a/packages/browser/src/index.ts
+++ b/packages/browser/src/index.ts
@@ -9,6 +9,7 @@ import { platformAuthenticatorIsAvailable } from './helpers/platformAuthenticato
import { browserSupportsWebAuthnAutofill } from './helpers/browserSupportsWebAuthnAutofill';
import { base64URLStringToBuffer } from './helpers/base64URLStringToBuffer';
import { bufferToBase64URLString } from './helpers/bufferToBase64URLString';
+import { WebAuthnAbortService } from './helpers/webAuthnAbortService';
export {
base64URLStringToBuffer,
@@ -18,6 +19,7 @@ export {
platformAuthenticatorIsAvailable,
startAuthentication,
startRegistration,
+ WebAuthnAbortService,
};
export type { WebAuthnErrorCode } from './helpers/webAuthnError';
diff --git a/packages/browser/src/methods/startAuthentication.test.ts b/packages/browser/src/methods/startAuthentication.test.ts
index 11f078e..fb31dcc 100644
--- a/packages/browser/src/methods/startAuthentication.test.ts
+++ b/packages/browser/src/methods/startAuthentication.test.ts
@@ -11,7 +11,7 @@ import { utf8StringToBuffer } from '../helpers/utf8StringToBuffer';
import { bufferToBase64URLString } from '../helpers/bufferToBase64URLString';
import { WebAuthnError } from '../helpers/webAuthnError';
import { generateCustomError } from '../helpers/__jest__/generateCustomError';
-import { webauthnAbortService } from '../helpers/webAuthnAbortService';
+import { WebAuthnAbortService } from '../helpers/webAuthnAbortService';
import { startAuthentication } from './startAuthentication';
@@ -62,8 +62,7 @@ beforeEach(() => {
mockSupportsAutofill.mockResolvedValue(true);
// Reset the abort service so we get an accurate call count
- // @ts-ignore: Ignore the fact that `controller` is private
- webauthnAbortService.controller = undefined;
+ WebAuthnAbortService.cancelCeremony();
});
afterEach(() => {
diff --git a/packages/browser/src/methods/startAuthentication.ts b/packages/browser/src/methods/startAuthentication.ts
index f6782ab..6e3940d 100644
--- a/packages/browser/src/methods/startAuthentication.ts
+++ b/packages/browser/src/methods/startAuthentication.ts
@@ -11,7 +11,7 @@ import { browserSupportsWebAuthn } from '../helpers/browserSupportsWebAuthn';
import { browserSupportsWebAuthnAutofill } from '../helpers/browserSupportsWebAuthnAutofill';
import { toPublicKeyCredentialDescriptor } from '../helpers/toPublicKeyCredentialDescriptor';
import { identifyAuthenticationError } from '../helpers/identifyAuthenticationError';
-import { webauthnAbortService } from '../helpers/webAuthnAbortService';
+import { WebAuthnAbortService } from '../helpers/webAuthnAbortService';
import { toAuthenticatorAttachment } from '../helpers/toAuthenticatorAttachment';
/**
@@ -79,7 +79,7 @@ export async function startAuthentication(
// Finalize options
options.publicKey = publicKey;
// Set up the ability to cancel this request if the user attempts another
- options.signal = webauthnAbortService.createNewAbortSignal();
+ options.signal = WebAuthnAbortService.createNewAbortSignal();
// Wait for the user to complete assertion
let credential;
diff --git a/packages/browser/src/methods/startRegistration.test.ts b/packages/browser/src/methods/startRegistration.test.ts
index 97878c6..ee0b604 100644
--- a/packages/browser/src/methods/startRegistration.test.ts
+++ b/packages/browser/src/methods/startRegistration.test.ts
@@ -8,7 +8,7 @@ import { generateCustomError } from '../helpers/__jest__/generateCustomError';
import { browserSupportsWebAuthn } from '../helpers/browserSupportsWebAuthn';
import { bufferToBase64URLString } from '../helpers/bufferToBase64URLString';
import { WebAuthnError } from '../helpers/webAuthnError';
-import { webauthnAbortService } from '../helpers/webAuthnAbortService';
+import { WebAuthnAbortService } from '../helpers/webAuthnAbortService';
import { utf8StringToBuffer } from '../helpers/utf8StringToBuffer';
@@ -61,8 +61,7 @@ beforeEach(() => {
mockSupportsWebauthn.mockReturnValue(true);
// Reset the abort service so we get an accurate call count
- // @ts-ignore: Ignore the fact that `controller` is private
- webauthnAbortService.controller = undefined;
+ WebAuthnAbortService.cancelCeremony();
});
afterEach(() => {
diff --git a/packages/browser/src/methods/startRegistration.ts b/packages/browser/src/methods/startRegistration.ts
index 2ad3089..3ffa7d1 100644
--- a/packages/browser/src/methods/startRegistration.ts
+++ b/packages/browser/src/methods/startRegistration.ts
@@ -11,7 +11,7 @@ import { base64URLStringToBuffer } from '../helpers/base64URLStringToBuffer';
import { browserSupportsWebAuthn } from '../helpers/browserSupportsWebAuthn';
import { toPublicKeyCredentialDescriptor } from '../helpers/toPublicKeyCredentialDescriptor';
import { identifyRegistrationError } from '../helpers/identifyRegistrationError';
-import { webauthnAbortService } from '../helpers/webAuthnAbortService';
+import { WebAuthnAbortService } from '../helpers/webAuthnAbortService';
import { toAuthenticatorAttachment } from '../helpers/toAuthenticatorAttachment';
/**
@@ -42,7 +42,7 @@ export async function startRegistration(
// Finalize options
const options: CredentialCreationOptions = { publicKey };
// Set up the ability to cancel this request if the user attempts another
- options.signal = webauthnAbortService.createNewAbortSignal();
+ options.signal = WebAuthnAbortService.createNewAbortSignal();
// Wait for the user to complete attestation
let credential;