@simplewebauthn/browser
Version:
SimpleWebAuthn for Browsers
122 lines (121 loc) • 5.8 kB
JavaScript
import { bufferToBase64URLString } from '../helpers/bufferToBase64URLString.js';
import { base64URLStringToBuffer } from '../helpers/base64URLStringToBuffer.js';
import { browserSupportsWebAuthn } from '../helpers/browserSupportsWebAuthn.js';
import { toPublicKeyCredentialDescriptor } from '../helpers/toPublicKeyCredentialDescriptor.js';
import { identifyRegistrationError } from '../helpers/identifyRegistrationError.js';
import { WebAuthnAbortService } from '../helpers/webAuthnAbortService.js';
import { toAuthenticatorAttachment } from '../helpers/toAuthenticatorAttachment.js';
/**
* Begin authenticator "registration" via WebAuthn attestation
*
* @param optionsJSON Output from **@simplewebauthn/server**'s `generateRegistrationOptions()`
* @param useAutoRegister (Optional) Try to silently create a passkey with the password manager that the user just signed in with. Defaults to `false`.
*/
export async function startRegistration(options) {
// @ts-ignore: Intentionally check for old call structure to warn about improper API call
if (!options.optionsJSON && options.challenge) {
console.warn('startRegistration() 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, useAutoRegister = false } = options;
if (!browserSupportsWebAuthn()) {
throw new Error('WebAuthn is not supported in this browser');
}
// We need to convert some values to Uint8Arrays before passing the credentials to the navigator
const publicKey = {
...optionsJSON,
challenge: base64URLStringToBuffer(optionsJSON.challenge),
user: {
...optionsJSON.user,
id: base64URLStringToBuffer(optionsJSON.user.id),
},
excludeCredentials: optionsJSON.excludeCredentials?.map(toPublicKeyCredentialDescriptor),
};
// Prepare options for `.create()`
const createOptions = {};
/**
* Try to use conditional create to register a passkey for the user with the password manager
* the user just used to authenticate with. The user won't be shown any prominent UI by the
* browser.
*/
if (useAutoRegister) {
// @ts-ignore: `mediation` doesn't yet exist on CredentialCreationOptions but it's possible as of Sept 2024
createOptions.mediation = 'conditional';
}
// Finalize options
createOptions.publicKey = publicKey;
// Set up the ability to cancel this request if the user attempts another
createOptions.signal = WebAuthnAbortService.createNewAbortSignal();
// Wait for the user to complete attestation
let credential;
try {
credential = (await navigator.credentials.create(createOptions));
}
catch (err) {
throw identifyRegistrationError({ error: err, options: createOptions });
}
if (!credential) {
throw new Error('Registration was not completed');
}
const { id, rawId, response, type } = credential;
// Continue to play it safe with `getTransports()` for now, even when L3 types say it's required
let transports = undefined;
if (typeof response.getTransports === 'function') {
transports = response.getTransports();
}
// L3 says this is required, but browser and webview support are still not guaranteed.
let responsePublicKeyAlgorithm = undefined;
if (typeof response.getPublicKeyAlgorithm === 'function') {
try {
responsePublicKeyAlgorithm = response.getPublicKeyAlgorithm();
}
catch (error) {
warnOnBrokenImplementation('getPublicKeyAlgorithm()', error);
}
}
let responsePublicKey = undefined;
if (typeof response.getPublicKey === 'function') {
try {
const _publicKey = response.getPublicKey();
if (_publicKey !== null) {
responsePublicKey = bufferToBase64URLString(_publicKey);
}
}
catch (error) {
warnOnBrokenImplementation('getPublicKey()', error);
}
}
// L3 says this is required, but browser and webview support are still not guaranteed.
let responseAuthenticatorData;
if (typeof response.getAuthenticatorData === 'function') {
try {
responseAuthenticatorData = bufferToBase64URLString(response.getAuthenticatorData());
}
catch (error) {
warnOnBrokenImplementation('getAuthenticatorData()', error);
}
}
return {
id,
rawId: bufferToBase64URLString(rawId),
response: {
attestationObject: bufferToBase64URLString(response.attestationObject),
clientDataJSON: bufferToBase64URLString(response.clientDataJSON),
transports,
publicKeyAlgorithm: responsePublicKeyAlgorithm,
publicKey: responsePublicKey,
authenticatorData: responseAuthenticatorData,
},
type,
clientExtensionResults: credential.getClientExtensionResults(),
authenticatorAttachment: toAuthenticatorAttachment(credential.authenticatorAttachment),
};
}
/**
* Visibly warn when we detect an issue related to a passkey provider intercepting WebAuthn API
* calls
*/
function warnOnBrokenImplementation(methodName, cause) {
console.warn(`The browser extension that intercepted this WebAuthn API call incorrectly implemented ${methodName}. You should report this error to them.\n`, cause);
}