summaryrefslogtreecommitdiffhomepage
path: root/packages/browser/src/helpers
diff options
context:
space:
mode:
Diffstat (limited to 'packages/browser/src/helpers')
-rw-r--r--packages/browser/src/helpers/__mocks__/browserSupportsWebAuthnAutofill.ts2
-rw-r--r--packages/browser/src/helpers/browserSupportsWebAuthnAutofill.ts28
-rw-r--r--packages/browser/src/helpers/webAuthnAbortService.test.ts42
-rw-r--r--packages/browser/src/helpers/webAuthnAbortService.ts27
4 files changed, 99 insertions, 0 deletions
diff --git a/packages/browser/src/helpers/__mocks__/browserSupportsWebAuthnAutofill.ts b/packages/browser/src/helpers/__mocks__/browserSupportsWebAuthnAutofill.ts
new file mode 100644
index 0000000..311b635
--- /dev/null
+++ b/packages/browser/src/helpers/__mocks__/browserSupportsWebAuthnAutofill.ts
@@ -0,0 +1,2 @@
+// We just need a simple mock so we can control whether this returns `true` or `false`
+export const browserSupportsWebAuthnAutofill = jest.fn();
diff --git a/packages/browser/src/helpers/browserSupportsWebAuthnAutofill.ts b/packages/browser/src/helpers/browserSupportsWebAuthnAutofill.ts
new file mode 100644
index 0000000..ffca77d
--- /dev/null
+++ b/packages/browser/src/helpers/browserSupportsWebAuthnAutofill.ts
@@ -0,0 +1,28 @@
+/* eslint-disable @typescript-eslint/ban-ts-comment */
+import { PublicKeyCredentialFuture } from '@simplewebauthn/typescript-types';
+
+/**
+ * Determine if the browser supports conditional UI, so that WebAuthn credentials can
+ * be shown to the user in the browser's typical password autofill popup.
+ */
+export async function browserSupportsWebAuthnAutofill(): Promise<boolean> {
+ // Just for Chrome Canary right now; the PublicKeyCredential logic below is the real API
+ // @ts-ignore
+ if (navigator.credentials.conditionalMediationSupported) {
+ return true;
+ }
+
+ /**
+ * I don't like the `as unknown` here but there's a `declare var PublicKeyCredential` in
+ * TS' DOM lib that's making it difficult for me to just go `as PublicKeyCredentialFuture` as I
+ * want. I think I'm fine with this for now since it's _supposed_ to be temporary, until TS types
+ * have a chance to catch up.
+ */
+ const globalPublicKeyCredential =
+ window.PublicKeyCredential as unknown as PublicKeyCredentialFuture;
+
+ return (
+ globalPublicKeyCredential.isConditionalMediationAvailable !== undefined &&
+ globalPublicKeyCredential.isConditionalMediationAvailable()
+ );
+}
diff --git a/packages/browser/src/helpers/webAuthnAbortService.test.ts b/packages/browser/src/helpers/webAuthnAbortService.test.ts
new file mode 100644
index 0000000..f4e6344
--- /dev/null
+++ b/packages/browser/src/helpers/webAuthnAbortService.test.ts
@@ -0,0 +1,42 @@
+import { webauthnAbortService } from './webAuthnAbortService';
+
+test('should create a new abort signal every time', () => {
+ const signal1 = webauthnAbortService.createNewAbortSignal();
+ const signal2 = webauthnAbortService.createNewAbortSignal();
+
+ expect(signal2).not.toBe(signal1);
+});
+
+test('should call abort() on existing controller when creating a new signal', () => {
+ // Populate `.controller`
+ webauthnAbortService.createNewAbortSignal();
+
+ // Spy on the existing instance of AbortController
+ const abortSpy = jest.fn();
+ // @ts-ignore
+ webauthnAbortService.controller?.abort = abortSpy;
+
+ // Generate a new signal, which should call `abort()` on the existing controller
+ webauthnAbortService.createNewAbortSignal();
+ expect(abortSpy).toHaveBeenCalledTimes(1);
+});
+
+test('should reset controller', () => {
+ // Reset the service
+ webauthnAbortService.reset();
+
+ // Populate `.controller`
+ webauthnAbortService.createNewAbortSignal();
+
+ // Spy on the existing instance of AbortController
+ const abortSpy = jest.fn();
+ // @ts-ignore
+ webauthnAbortService.controller?.abort = abortSpy;
+
+ // Reset the service
+ webauthnAbortService.reset();
+
+ // Generate a new signal, which should NOT call `abort()` because the controller was cleared
+ webauthnAbortService.createNewAbortSignal();
+ expect(abortSpy).toHaveBeenCalledTimes(0);
+});
diff --git a/packages/browser/src/helpers/webAuthnAbortService.ts b/packages/browser/src/helpers/webAuthnAbortService.ts
new file mode 100644
index 0000000..c60e6df
--- /dev/null
+++ b/packages/browser/src/helpers/webAuthnAbortService.ts
@@ -0,0 +1,27 @@
+/**
+ * A way to cancel an existing WebAuthn request, for example to cancel a
+ * WebAuthn autofill authentication request for a manual authentication attempt.
+ */
+class WebAuthnAbortService {
+ private controller: AbortController | undefined;
+
+ /**
+ * Prepare an abort signal that will help support multiple auth attempts without needing to
+ * reload the page
+ */
+ createNewAbortSignal() {
+ // Abort any existing calls to navigator.credentials.create() or navigator.credentials.get()
+ if (this.controller) {
+ this.controller.abort();
+ }
+
+ this.controller = new AbortController();
+ return this.controller.signal;
+ }
+
+ reset() {
+ this.controller = undefined;
+ }
+}
+
+export const webauthnAbortService = new WebAuthnAbortService();