mirror of https://github.com/ghostfolio/ghostfolio
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
95 lines
4.9 KiB
95 lines
4.9 KiB
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),
|
|
};
|
|
}
|
|
|