diff options
Diffstat (limited to 'packages/browser/src/helpers')
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(); |