@simplewebauthn/browser
Version:
SimpleWebAuthn for Browsers
96 lines (95 loc) • 5.03 kB
JavaScript
import { bufferToBase64URLString } from '../helpers/bufferToBase64URLString.js';
import { base64URLStringToBuffer } from '../helpers/base64URLStringToBuffer.js';
import { browserSupportsWebAuthn } from '../helpers/browserSupportsWebAuthn.js';
import { browserSupportsWebAuthnAutofill } from '../helpers/browserSupportsWebAuthnAutofill.js';
import { toPublicKeyCredentialDescriptor } from '../helpers/toPublicKeyCredentialDescriptor.js';
import { identifyAuthenticationError } from '../helpers/identifyAuthenticationError.js';
import { WebAuthnAbortService } from '../helpers/webAuthnAbortService.js';
import { toAuthenticatorAttachment } from '../helpers/toAuthenticatorAttachment.js';
/**
* Begin authenticator "login" via WebAuthn assertion
*
* @param optionsJSON Output from **@simplewebauthn/server**'s `generateAuthenticationOptions()`
* @param useBrowserAutofill (Optional) Initialize conditional UI to enable logging in via browser autofill prompts. Defaults to `false`.
* @param verifyBrowserAutofillInput (Optional) Ensure a suitable `<input>` element is present when `useBrowserAutofill` is `true`. Defaults to `true`.
*/
export async function startAuthentication(options) {
// @ts-ignore: Intentionally check for old call structure to warn about improper API call
if (!options.optionsJSON && options.challenge) {
console.warn('startAuthentication() was not called correctly. It will try to continue with the provided options, but this call should be refactored to use the expected call structure instead. See https://simplewebauthn.dev/docs/packages/browser#typeerror-cannot-read-properties-of-undefined-reading-challenge for more information.');
// @ts-ignore: Reassign the options, passed in as a positional argument, to the expected variable
options = { optionsJSON: options };
}
const { optionsJSON, useBrowserAutofill = false, verifyBrowserAutofillInput = true, } = options;
if (!browserSupportsWebAuthn()) {
throw new Error('WebAuthn is not supported in this browser');
}
// We need to avoid passing empty array to avoid blocking retrieval
// of public key
let allowCredentials;
if (optionsJSON.allowCredentials?.length !== 0) {
allowCredentials = optionsJSON.allowCredentials?.map(toPublicKeyCredentialDescriptor);
}
// We need to convert some values to Uint8Arrays before passing the credentials to the navigator
const publicKey = {
...optionsJSON,
challenge: base64URLStringToBuffer(optionsJSON.challenge),
allowCredentials,
};
// Prepare options for `.get()`
const getOptions = {};
/**
* Set up the page to prompt the user to select a credential for authentication via the browser's
* input autofill mechanism.
*/
if (useBrowserAutofill) {
if (!(await browserSupportsWebAuthnAutofill())) {
throw Error('Browser does not support WebAuthn autofill');
}
// Check for an <input> with "webauthn" in its `autocomplete` attribute
const eligibleInputs = document.querySelectorAll("input[autocomplete$='webauthn']");
// WebAuthn autofill requires at least one valid input
if (eligibleInputs.length < 1 && verifyBrowserAutofillInput) {
throw Error('No <input> with "webauthn" as the only or last value in its `autocomplete` attribute was detected');
}
// `CredentialMediationRequirement` doesn't know about "conditional" yet as of
// typescript@4.6.3
getOptions.mediation = 'conditional';
// Conditional UI requires an empty allow list
publicKey.allowCredentials = [];
}
// Finalize options
getOptions.publicKey = publicKey;
// Set up the ability to cancel this request if the user attempts another
getOptions.signal = WebAuthnAbortService.createNewAbortSignal();
// Wait for the user to complete assertion
let credential;
try {
credential = (await navigator.credentials.get(getOptions));
}
catch (err) {
throw identifyAuthenticationError({ error: err, options: getOptions });
}
if (!credential) {
throw new Error('Authentication was not completed');
}
const { id, rawId, response, type } = credential;
let userHandle = undefined;
if (response.userHandle) {
userHandle = bufferToBase64URLString(response.userHandle);
}
// Convert values to base64 to make it easier to send back to the server
return {
id,
rawId: bufferToBase64URLString(rawId),
response: {
authenticatorData: bufferToBase64URLString(response.authenticatorData),
clientDataJSON: bufferToBase64URLString(response.clientDataJSON),
signature: bufferToBase64URLString(response.signature),
userHandle,
},
type,
clientExtensionResults: credential.getClientExtensionResults(),
authenticatorAttachment: toAuthenticatorAttachment(credential.authenticatorAttachment),
};
}