Odoo18-Base/addons/auth_passkey/static/lib/simplewebauthn.js
2025-01-06 10:57:38 +07:00

397 lines
14 KiB
JavaScript

/** @odoo-module **/
/*!
* simplewebauthn/browser@9.0.1 (https://github.com/MasterKale/SimpleWebAuthn)
* Copyright 2020 Matthew Miller
* Licensed under MIT (https://github.com/MasterKale/SimpleWebAuthn/blob/master/LICENSE.md)
*/
function utf8StringToBuffer(value) {
return new TextEncoder().encode(value);
}
function bufferToBase64URLString(buffer) {
const bytes = new Uint8Array(buffer);
let str = '';
for (const charCode of bytes) {
str += String.fromCharCode(charCode);
}
const base64String = btoa(str);
return base64String.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
function base64URLStringToBuffer(base64URLString) {
const base64 = base64URLString.replace(/-/g, '+').replace(/_/g, '/');
const padLength = (4 - (base64.length % 4)) % 4;
const padded = base64.padEnd(base64.length + padLength, '=');
const binary = atob(padded);
const buffer = new ArrayBuffer(binary.length);
const bytes = new Uint8Array(buffer);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return buffer;
}
function browserSupportsWebAuthn() {
return (window?.PublicKeyCredential !== undefined &&
typeof window.PublicKeyCredential === 'function');
}
function toPublicKeyCredentialDescriptor(descriptor) {
const { id } = descriptor;
return {
...descriptor,
id: base64URLStringToBuffer(id),
transports: descriptor.transports,
};
}
function isValidDomain(hostname) {
return (hostname === 'localhost' ||
/^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/i.test(hostname));
}
class WebAuthnError extends Error {
constructor({ message, code, cause, name, }) {
super(message, { cause });
this.name = name ?? cause.name;
this.code = code;
}
}
function identifyRegistrationError({ error, options, }) {
const { publicKey } = options;
if (!publicKey) {
throw Error('options was missing required publicKey property');
}
if (error.name === 'AbortError') {
if (options.signal instanceof AbortSignal) {
return new WebAuthnError({
message: 'Registration ceremony was sent an abort signal',
code: 'ERROR_CEREMONY_ABORTED',
cause: error,
});
}
}
else if (error.name === 'ConstraintError') {
if (publicKey.authenticatorSelection?.requireResidentKey === true) {
return new WebAuthnError({
message: 'Discoverable credentials were required but no available authenticator supported it',
code: 'ERROR_AUTHENTICATOR_MISSING_DISCOVERABLE_CREDENTIAL_SUPPORT',
cause: error,
});
}
else if (publicKey.authenticatorSelection?.userVerification === 'required') {
return new WebAuthnError({
message: 'User verification was required but no available authenticator supported it',
code: 'ERROR_AUTHENTICATOR_MISSING_USER_VERIFICATION_SUPPORT',
cause: error,
});
}
}
else if (error.name === 'InvalidStateError') {
return new WebAuthnError({
message: 'The authenticator was previously registered',
code: 'ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED',
cause: error,
});
}
else if (error.name === 'NotAllowedError') {
return new WebAuthnError({
message: error.message,
code: 'ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY',
cause: error,
});
}
else if (error.name === 'NotSupportedError') {
const validPubKeyCredParams = publicKey.pubKeyCredParams.filter((param) => param.type === 'public-key');
if (validPubKeyCredParams.length === 0) {
return new WebAuthnError({
message: 'No entry in pubKeyCredParams was of type "public-key"',
code: 'ERROR_MALFORMED_PUBKEYCREDPARAMS',
cause: error,
});
}
return new WebAuthnError({
message: 'No available authenticator supported any of the specified pubKeyCredParams algorithms',
code: 'ERROR_AUTHENTICATOR_NO_SUPPORTED_PUBKEYCREDPARAMS_ALG',
cause: error,
});
}
else if (error.name === 'SecurityError') {
const effectiveDomain = window.location.hostname;
if (!isValidDomain(effectiveDomain)) {
return new WebAuthnError({
message: `${window.location.hostname} is an invalid domain`,
code: 'ERROR_INVALID_DOMAIN',
cause: error,
});
}
else if (publicKey.rp.id !== effectiveDomain) {
return new WebAuthnError({
message: `The RP ID "${publicKey.rp.id}" is invalid for this domain`,
code: 'ERROR_INVALID_RP_ID',
cause: error,
});
}
}
else if (error.name === 'TypeError') {
if (publicKey.user.id.byteLength < 1 || publicKey.user.id.byteLength > 64) {
return new WebAuthnError({
message: 'User ID was not between 1 and 64 characters',
code: 'ERROR_INVALID_USER_ID_LENGTH',
cause: error,
});
}
}
else if (error.name === 'UnknownError') {
return new WebAuthnError({
message: 'The authenticator was unable to process the specified options, or could not create a new credential',
code: 'ERROR_AUTHENTICATOR_GENERAL_ERROR',
cause: error,
});
}
return error;
}
class BaseWebAuthnAbortService {
createNewAbortSignal() {
if (this.controller) {
const abortError = new Error('Cancelling existing WebAuthn API call for new one');
abortError.name = 'AbortError';
this.controller.abort(abortError);
}
const newController = new AbortController();
this.controller = newController;
return newController.signal;
}
cancelCeremony() {
if (this.controller) {
const abortError = new Error('Manually cancelling existing WebAuthn API call');
abortError.name = 'AbortError';
this.controller.abort(abortError);
this.controller = undefined;
}
}
}
const WebAuthnAbortService = new BaseWebAuthnAbortService();
const attachments = ['cross-platform', 'platform'];
function toAuthenticatorAttachment(attachment) {
if (!attachment) {
return;
}
if (attachments.indexOf(attachment) < 0) {
return;
}
return attachment;
}
async function startRegistration(creationOptionsJSON) {
if (!browserSupportsWebAuthn()) {
throw new Error('WebAuthn is not supported in this browser');
}
const publicKey = {
...creationOptionsJSON,
challenge: base64URLStringToBuffer(creationOptionsJSON.challenge),
user: {
...creationOptionsJSON.user,
id: utf8StringToBuffer(creationOptionsJSON.user.id),
},
excludeCredentials: creationOptionsJSON.excludeCredentials?.map(toPublicKeyCredentialDescriptor),
};
const options = { publicKey };
options.signal = WebAuthnAbortService.createNewAbortSignal();
let credential;
try {
credential = (await navigator.credentials.create(options));
}
catch (err) {
throw identifyRegistrationError({ error: err, options });
}
if (!credential) {
throw new Error('Registration was not completed');
}
const { id, rawId, response, type } = credential;
let transports = undefined;
if (typeof response.getTransports === 'function') {
transports = response.getTransports();
}
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);
}
}
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),
};
}
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);
}
function bufferToUTF8String(value) {
return new TextDecoder('utf-8').decode(value);
}
function browserSupportsWebAuthnAutofill() {
const globalPublicKeyCredential = window
.PublicKeyCredential;
if (globalPublicKeyCredential.isConditionalMediationAvailable === undefined) {
return new Promise((resolve) => resolve(false));
}
return globalPublicKeyCredential.isConditionalMediationAvailable();
}
function identifyAuthenticationError({ error, options, }) {
const { publicKey } = options;
if (!publicKey) {
throw Error('options was missing required publicKey property');
}
if (error.name === 'AbortError') {
if (options.signal instanceof AbortSignal) {
return new WebAuthnError({
message: 'Authentication ceremony was sent an abort signal',
code: 'ERROR_CEREMONY_ABORTED',
cause: error,
});
}
}
else if (error.name === 'NotAllowedError') {
return new WebAuthnError({
message: error.message,
code: 'ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY',
cause: error,
});
}
else if (error.name === 'SecurityError') {
const effectiveDomain = window.location.hostname;
if (!isValidDomain(effectiveDomain)) {
return new WebAuthnError({
message: `${window.location.hostname} is an invalid domain`,
code: 'ERROR_INVALID_DOMAIN',
cause: error,
});
}
else if (publicKey.rpId !== effectiveDomain) {
return new WebAuthnError({
message: `The RP ID "${publicKey.rpId}" is invalid for this domain`,
code: 'ERROR_INVALID_RP_ID',
cause: error,
});
}
}
else if (error.name === 'UnknownError') {
return new WebAuthnError({
message: 'The authenticator was unable to process the specified options, or could not create a new assertion signature',
code: 'ERROR_AUTHENTICATOR_GENERAL_ERROR',
cause: error,
});
}
return error;
}
async function startAuthentication(requestOptionsJSON, useBrowserAutofill = false) {
if (!browserSupportsWebAuthn()) {
throw new Error('WebAuthn is not supported in this browser');
}
let allowCredentials;
if (requestOptionsJSON.allowCredentials?.length !== 0) {
allowCredentials = requestOptionsJSON.allowCredentials?.map(toPublicKeyCredentialDescriptor);
}
const publicKey = {
...requestOptionsJSON,
challenge: base64URLStringToBuffer(requestOptionsJSON.challenge),
allowCredentials,
};
const options = {};
if (useBrowserAutofill) {
if (!(await browserSupportsWebAuthnAutofill())) {
throw Error('Browser does not support WebAuthn autofill');
}
const eligibleInputs = document.querySelectorAll('input[autocomplete$=\'webauthn\']');
if (eligibleInputs.length < 1) {
throw Error('No <input> with "webauthn" as the only or last value in its `autocomplete` attribute was detected');
}
options.mediation = 'conditional';
publicKey.allowCredentials = [];
}
options.publicKey = publicKey;
options.signal = WebAuthnAbortService.createNewAbortSignal();
let credential;
try {
credential = (await navigator.credentials.get(options));
}
catch (err) {
throw identifyAuthenticationError({ error: err, options });
}
if (!credential) {
throw new Error('Authentication was not completed');
}
const { id, rawId, response, type } = credential;
let userHandle = undefined;
if (response.userHandle) {
userHandle = bufferToUTF8String(response.userHandle);
}
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),
};
}
function platformAuthenticatorIsAvailable() {
if (!browserSupportsWebAuthn()) {
return new Promise((resolve) => resolve(false));
}
return PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
}
export { WebAuthnAbortService, WebAuthnError, base64URLStringToBuffer, browserSupportsWebAuthn, browserSupportsWebAuthnAutofill, bufferToBase64URLString, platformAuthenticatorIsAvailable, startAuthentication, startRegistration };