import {
Fido2AttestationResult,
Fido2AssertionResult
} from "./response.js";
import {
coerceToArrayBuffer,
abToBuf
} from "./utils.js";
let globalAttestationMap = new Map();
let globalExtensionMap = new Map();
class Webauthn {
/**
* Creates a FIDO2 server class
* @param {Object} opts Options for the server
* @param {Number} [opts.timeout=60000] The amount of time to wait, in milliseconds, before a call has timed out
* @param {String} [opts.rpId="localhost"] The name of the server
* @param {String} [opts.rpName="Anonymous Service"] The name of the server
* @param {String} [opts.rpIcon] A URL for the service's icon. Can be a [RFC 2397]{@link https://tools.ietf.org/html/rfc2397} data URL.
* @param {Number} [opts.challengeSize=64] The number of bytes to use for the challenge
* @param {Object} [opts.authenticatorSelectionCriteria] An object describing what types of authenticators are allowed to register with the service.
* See [AuthenticatorSelectionCriteria]{@link https://w3.org/TR/webauthn/#authenticatorSelection} in the WebAuthn spec for details.
* @param {String} [opts.authenticatorAttachment] Indicates whether authenticators should be part of the OS ("platform"), or can be roaming authenticators ("cross-platform")
* @param {Boolean} [opts.authenticatorRequireResidentKey] Indicates whether authenticators must store the key internally (true) or if they can use a KDF to generate keys
* @param {String} [opts.authenticatorUserVerification] Indicates whether user verification should be performed. Options are "required", "preferred", or "discouraged".
* @param {String} [opts.attestation="direct"] The preferred attestation type to be used.
* See [AttestationConveyancePreference]{https://w3.org/TR/webauthn/#enumdef-attestationconveyancepreference} in the WebAuthn spec
* @param {Array<Number>} [opts.cryptoParams] A list of COSE algorithm identifiers (e.g. -7)
* ordered by the preference in which the authenticator should use them.
*/
constructor(opts, ToolBox) {
/* eslint complexity: ["off"] */
opts = opts || {};
// set defaults
this.config = {};
// Store reference to toolbox in .tools
this.tools = ToolBox;
// timeout
this.config.timeout = (opts.timeout === undefined) ? 60000 : opts.timeout; // 1 minute
checkOptType(this.config, "timeout", "number");
if (!(this.config.timeout >>> 0 === parseFloat(this.config.timeout))) {
throw new RangeError("timeout should be zero or positive integer");
}
// challengeSize
this.config.challengeSize = opts.challengeSize || 64;
checkOptType(this.config, "challengeSize", "number");
if (this.config.challengeSize < 32) {
throw new RangeError("challenge size too small, must be 32 or greater");
}
// rpId
this.config.rpId = opts.rpId;
checkOptType(this.config, "rpId", "string");
// rpName
this.config.rpName = opts.rpName || "Anonymous Service";
checkOptType(this.config, "rpName", "string");
// rpIcon
this.config.rpIcon = opts.rpIcon;
checkOptType(this.config, "rpIcon", "string");
// authenticatorRequireResidentKey
this.config.authenticatorRequireResidentKey = opts.authenticatorRequireResidentKey;
checkOptType(this.config, "authenticatorRequireResidentKey", "boolean");
// authenticatorAttachment
this.config.authenticatorAttachment = opts.authenticatorAttachment;
if (this.config.authenticatorAttachment !== undefined &&
(this.config.authenticatorAttachment !== "platform" &&
this.config.authenticatorAttachment !== "cross-platform")) {
throw new TypeError("expected authenticatorAttachment to be 'platform', or 'cross-platform', got: " + this.config.authenticatorAttachment);
}
// authenticatorUserVerification
this.config.authenticatorUserVerification = opts.authenticatorUserVerification;
if (this.config.authenticatorUserVerification !== undefined &&
(this.config.authenticatorUserVerification !== "required" &&
this.config.authenticatorUserVerification !== "preferred" &&
this.config.authenticatorUserVerification !== "discouraged")) {
throw new TypeError("expected authenticatorUserVerification to be 'required', 'preferred', or 'discouraged', got: " + this.config.authenticatorUserVerification);
}
// attestation
this.config.attestation = opts.attestation || "direct";
if (this.config.attestation !== "direct" &&
this.config.attestation !== "indirect" &&
this.config.attestation !== "none") {
throw new TypeError("expected attestation to be 'direct', 'indirect', or 'none', got: " + this.config.attestation);
}
// cryptoParams
this.config.cryptoParams = opts.cryptoParams || [-7, -257];
checkOptType(this.config, "cryptoParams", Array);
if (this.config.cryptoParams.length < 1) {
throw new TypeError("cryptoParams must have at least one element");
}
this.config.cryptoParams.forEach((param) => {
checkOptType({ cryptoParam: param }, "cryptoParam", "number");
});
this.attestationMap = globalAttestationMap;
this.extSet = new Set(); // enabled extensions (all disabled by default)
this.extOptMap = new Map(); // default options for extensions
// TODO: convert icon file to data-URL icon
// TODO: userVerification
}
/**
* Adds a new global extension that will be available to all instantiations of
* {@link Webauthn}. Note that the extension must still be enabled by calling
* {@link enableExtension} for each instantiation of a Fido2Lib.
* @param {String} extName The name of the extension to add. (e.g. - "appid")
* @param {Function} optionGeneratorFn Extensions are included in
* @param {Function} resultParserFn [description]
* @param {Function} resultValidatorFn [description]
*/
static addExtension(extName, optionGeneratorFn, resultParserFn, resultValidatorFn) {
if (typeof extName !== "string") {
throw new Error("expected 'extName' to be String, got: " + extName);
}
if (globalExtensionMap.has(extName)) {
throw new Error(`the extension '${extName}' has already been added`);
}
if (typeof optionGeneratorFn !== "function") {
throw new Error("expected 'optionGeneratorFn' to be a Function, got: " + optionGeneratorFn);
}
if (typeof resultParserFn !== "function") {
throw new Error("expected 'resultParserFn' to be a Function, got: " + resultParserFn);
}
if (typeof resultValidatorFn !== "function") {
throw new Error("expected 'resultValidatorFn' to be a Function, got: " + resultValidatorFn);
}
globalExtensionMap.set(extName, {
optionGeneratorFn,
resultParserFn,
resultValidatorFn,
});
}
/**
* Removes all extensions from the global extension registry. Mostly used for testing.
*/
static deleteAllExtensions() {
globalExtensionMap.clear();
}
/**
* Generates the options to send to the client for the specified extension
* @private
* @param {String} extName The name of the extension to generate options for. Must be a valid extension that has been registered through {@link Fido2Lib#addExtension}
* @param {String} type The type of options that are being generated. Valid options are "attestation" or "assertion".
* @param {Any} [options] Optional parameters to pass to the generator function
* @return {Any} The extension value that will be sent to the client. If `undefined`, this extension won't be included in the
* options sent to the client.
*/
generateExtensionOptions(extName, type, options) {
if (typeof extName !== "string") {
throw new Error("expected 'extName' to be String, got: " + extName);
}
if (type !== "attestation" && type !== "assertion") {
throw new Error("expected 'type' to be 'attestation' or 'assertion', got: " + type);
}
let ext = globalExtensionMap.get(extName);
if (typeof ext !== "object" ||
typeof ext.optionGeneratorFn !== "function") {
throw new Error(`valid extension for '${extName}' not found`);
}
let ret = ext.optionGeneratorFn(extName, type, options);
return ret;
}
static parseExtensionResult(extName, clientThing, authnrThing) {
if (typeof extName !== "string") {
throw new Error("expected 'extName' to be String, got: " + extName);
}
let ext = globalExtensionMap.get(extName);
if (typeof ext !== "object" ||
typeof ext.parseFn !== "function") {
throw new Error(`valid extension for '${extName}' not found`);
}
let ret = ext.parseFn(extName, clientThing, authnrThing);
return ret;
}
static validateExtensionResult(extName) {
let ext = globalExtensionMap.get(extName);
if (typeof ext !== "object" ||
typeof ext.validateFn !== "function") {
throw new Error(`valid extension for '${extName}' not found`);
}
let ret = ext.validateFn.call(this);
return ret;
}
/**
* Enables the specified extension.
* @param {String} extName The name of the extension to enable. Must be a valid extension that has been registered through {@link Fido2Lib#addExtension}
*/
enableExtension(extName) {
if (typeof extName !== "string") {
throw new Error("expected 'extName' to be String, got: " + extName);
}
if (!globalExtensionMap.has(extName)) {
throw new Error(`valid extension for '${extName}' not found`);
}
this.extSet.add(extName);
}
/**
* Disables the specified extension.
* @param {String} extName The name of the extension to enable. Must be a valid extension that has been registered through {@link Fido2Lib#addExtension}
*/
disableExtension(extName) {
if (typeof extName !== "string") {
throw new Error("expected 'extName' to be String, got: " + extName);
}
if (!globalExtensionMap.has(extName)) {
throw new Error(`valid extension for '${extName}' not found`);
}
this.extSet.delete(extName);
}
/**
* Specifies the options to be used for the extension
* @param {String} extName The name of the extension to set the options for (e.g. - "appid". Must be a valid extension that has been registered through {@link Fido2Lib#addExtension}
* @param {Any} options The parameter that will be passed to the option generator function (e.g. - "https://webauthn.org")
*/
setExtensionOptions(extName, options) {
if (typeof extName !== "string") {
throw new Error("expected 'extName' to be String, got: " + extName);
}
if (!globalExtensionMap.has(extName)) {
throw new Error(`valid extension for '${extName}' not found`);
}
this.extOptMap.set(extName, options);
}
/**
* Validates an attestation response. Will be called within the context (`this`) of a {@link Fido2AttestationResult}
* @private
*/
static async validateAttestation() {
let fmt = this.authnrData.get("fmt");
// validate input
if (typeof fmt !== "string") {
throw new TypeError("expected 'fmt' to be string, got: " + typeof fmt);
}
// get from attestationMap
let fmtObj = globalAttestationMap.get(fmt);
if (typeof fmtObj !== "object" ||
typeof fmtObj.parseFn !== "function" ||
typeof fmtObj.validateFn !== "function") {
throw new Error(`no support for attestation format: ${fmt}`);
}
// call fn
let ret = await fmtObj.validateFn.call(this);
// validate return
if (ret !== true) {
throw new Error(`${fmt} validateFn did not return 'true'`);
}
// return result
return ret;
}
/**
* Adds a new attestation format that will automatically be recognized and parsed
* for any future {@link Fido2CreateRequest} messages
* @param {String} fmt The name of the attestation format, as it appears in the
* ARIN registry and / or as it will appear in the {@link Fido2CreateRequest}
* message that is received
* @param {Function} parseFn The function that will be called to parse the
* attestation format. It will receive the `attStmt` as a parameter and will be
* called from the context (`this`) of the `Fido2CreateRequest`
* @param {Function} validateFn The function that will be called to validate the
* attestation format. It will receive no arguments, as all the necessary
* information for validating the attestation statement will be contained in the
* calling context (`this`).
*/
static addAttestationFormat(fmt, parseFn, validateFn) {
// validate input
if (typeof fmt !== "string") {
throw new TypeError("expected 'fmt' to be string, got: " + typeof fmt);
}
if (typeof parseFn !== "function") {
throw new TypeError("expected 'parseFn' to be string, got: " + typeof parseFn);
}
if (typeof validateFn !== "function") {
throw new TypeError("expected 'validateFn' to be string, got: " + typeof validateFn);
}
if (globalAttestationMap.has(fmt)) {
throw new Error(`can't add format: '${fmt}' already exists`);
}
// add to attestationMap
globalAttestationMap.set(fmt, {
parseFn,
validateFn,
});
return true;
}
/**
* Deletes all currently registered attestation formats.
*/
static deleteAllAttestationFormats() {
globalAttestationMap.clear();
}
/**
* Parses an attestation statememnt of the format specified
* @private
* @param {String} fmt The name of the format to be parsed, as specified in the
* ARIN registry of attestation formats.
* @param {Object} attStmt The attestation object to be parsed.
* @return {Map} A Map of all the attestation fields that were parsed.
* At this point the fields have not yet been verified.
* @throws {Error} when a field cannot be parsed or verified.
* @throws {TypeError} when supplied parameters `fmt` or `attStmt` are of the
* wrong type
*/
static parseAttestation(fmt, attStmt) {
// validate input
if (typeof fmt !== "string") {
throw new TypeError("expected 'fmt' to be string, got: " + typeof fmt);
}
if (typeof attStmt !== "object") {
throw new TypeError("expected 'attStmt' to be object, got: " + typeof attStmt);
}
// get from attestationMap
let fmtObj = globalAttestationMap.get(fmt);
if (typeof fmtObj !== "object" ||
typeof fmtObj.parseFn !== "function" ||
typeof fmtObj.validateFn !== "function") {
throw new Error(`no support for attestation format: ${fmt}`);
}
// call fn
let ret = fmtObj.parseFn.call(this, attStmt);
// validate return
if (!(ret instanceof Map)) {
throw new Error(`${fmt} parseFn did not return a Map`);
}
// return result
return new Map([
["fmt", fmt],
...ret,
]);
}
/**
* Parses and validates an attestation response from the client
* @param {Object} res The assertion result that was generated by the client.
* See {@link https://w3.org/TR/webauthn/#authenticatorattestationresponse AuthenticatorAttestationResponse} in the WebAuthn spec.
* @param {String} [res.id] The base64url encoded id returned by the client
* @param {String} [res.rawId] The base64url encoded rawId returned by the client. If `res.rawId` is missing, `res.id` will be used instead. If both are missing an error will be thrown.
* @param {String} res.response.clientDataJSON The base64url encoded clientDataJSON returned by the client
* @param {String} res.response.authenticatorData The base64url encoded authenticatorData returned by the client
* @param {Object} expected The expected parameters for the assertion response.
* If these parameters don't match the recieved values, validation will fail and an error will be thrown.
* @param {String} expected.challenge The base64url encoded challenge that was sent to the client, as generated by [assertionOptions]{@link Fido2Lib#assertionOptions}
* @param {String} expected.origin The expected origin that the authenticator has signed over. For example, "https://localhost:8443" or "https://webauthn.org"
* @param {String} expected.factor Which factor is expected for the assertion. Valid values are "first", "second", or "either".
* If "first", this requires that the authenticator performed user verification (e.g. - biometric authentication, PIN authentication, etc.).
* If "second", this requires that the authenticator performed user presence (e.g. - user pressed a button).
* If "either", then either "first" or "second" is acceptable
* @return {Promise<Fido2AttestationResult>} Returns a Promise that resolves to a {@link Fido2AttestationResult}
* @throws {Error} If parsing or validation fails
*/
async attestationResult(res, expected) {
expected.flags = factorToFlags(expected.factor, ["AT"]);
delete expected.factor;
return Fido2AttestationResult.create(res, expected, this.tools);
}
/**
* Parses and validates an assertion response from the client
* @param {Object} res The assertion result that was generated by the client.
* See {@link https://w3.org/TR/webauthn/#authenticatorassertionresponse AuthenticatorAssertionResponse} in the WebAuthn spec.
* @param {String} [res.id] The base64url encoded id returned by the client
* @param {String} [res.rawId] The base64url encoded rawId returned by the client. If `res.rawId` is missing, `res.id` will be used instead. If both are missing an error will be thrown.
* @param {String} res.response.clientDataJSON The base64url encoded clientDataJSON returned by the client
* @param {String} res.response.attestationObject The base64url encoded authenticatorData returned by the client
* @param {String} res.response.signature The base64url encoded signature returned by the client
* @param {String|null} [res.response.userHandle] The base64url encoded userHandle returned by the client. May be null or an empty string.
* @param {Object} expected The expected parameters for the assertion response.
* If these parameters don't match the recieved values, validation will fail and an error will be thrown.
* @param {String} expected.challenge The base64url encoded challenge that was sent to the client, as generated by [assertionOptions]{@link Fido2Lib#assertionOptions}
* @param {String} expected.origin The expected origin that the authenticator has signed over. For example, "https://localhost:8443" or "https://webauthn.org"
* @param {String} expected.factor Which factor is expected for the assertion. Valid values are "first", "second", or "either".
* If "first", this requires that the authenticator performed user verification (e.g. - biometric authentication, PIN authentication, etc.).
* If "second", this requires that the authenticator performed user presence (e.g. - user pressed a button).
* If "either", then either "first" or "second" is acceptable
* @param {String} expected.publicKey A PEM encoded public key that will be used to validate the assertion response signature.
* This is the public key that was returned for this user during [attestationResult]{@link Fido2Lib#attestationResult}
* @param {Number} expected.prevCounter The previous value of the signature counter for this authenticator.
* @param {String|null} expected.userHandle The expected userHandle, which was the user.id during registration
* @return {Promise<Fido2AssertionResult>} Returns a Promise that resolves to a {@link Fido2AssertionResult}
* @throws {Error} If parsing or validation fails
*/
async assertionResult(res, expected) {
expected.flags = factorToFlags(expected.factor, []);
delete expected.factor;
return Fido2AssertionResult.create(res, expected, this.tools);
}
/**
* Gets a challenge and any other parameters for the `navigator.credentials.create()` call
* The `challenge` property is an `ArrayBuffer` and will need to be encoded to be transmitted to the client.
* @param {Object} [opts] An object containing various options for the option creation
* @param {Object} [opts.extensionOptions] An object that contains the extensions to enable, and the options to use for each of them.
* The keys of this object are the names of the extensions (e.g. - "appid"), and the value of each key is the option that will
* be passed to that extension when it is generating the value to send to the client. This object overrides the extensions that
* have been set with {@link enableExtension} and the options that have been set with {@link setExtensionOptions}. If an extension
* was enabled with {@link enableExtension} but it isn't included in this object, the extension won't be sent to the client. Likewise,
* if an extension was disabled with {@link disableExtension} but it is included in this object, it will be sent to the client.
* @param {String} [extraData] Extra data to be signed by the authenticator during attestation. The challenge will be a hash:
* SHA256(rawChallenge + extraData) and the `rawChallenge` will be returned as part of PublicKeyCredentialCreationOptions.
* @returns {Promise<PublicKeyCredentialCreationOptions>} The options for creating calling `navigator.credentials.create()`
*/
async attestationOptions(opts) {
opts = opts || {};
// The object being returned is described here:
// https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptions
let challenge = this.tools.randomValues(this.config.challengeSize);
challenge = coerceToArrayBuffer(challenge, "challenge");
let pubKeyCredParams = [];
this.config.cryptoParams.forEach((coseId) => {
pubKeyCredParams.push({
type: "public-key",
alg: coseId });
});
// mix extraData into challenge
let rawChallenge;
if (opts.extraData) {
rawChallenge = challenge;
let extraData = coerceToArrayBuffer(opts.extraData, "extraData");
let hash = await this.tools.hashDigest([...abToBuf(challenge),...abToBuf(extraData)]);
challenge = new Uint8Array(hash).buffer;
}
let options = {
rp: {},
user: {},
};
let extensions = createExtensions.call(this, "attestation", opts.extensionOptions);
/**
* @typedef {Object} PublicKeyCredentialCreationOptions
* @description This object is returned by {@link attestationOptions} and is basially the same as
* the [PublicKeyCredentialCreationOptions]{@link https://w3.org/TR/webauthn/#dictdef-publickeycredentialcreationoptions}
* object that is required to be passed to `navigator.credentials.create()`. With the exception of the `challenge` property,
* all other properties are optional and only set if they were specified in the configuration paramater
* that was passed to the constructor.
* @property {Object} rp Relying party information (a.k.a. - server / service information)
* @property {String} [rp.name] Relying party name (e.g. - "ACME"). This is only set if `rpName` was specified during the `new` call.
* @property {String} [rp.id] Relying party ID, a domain name (e.g. - "example.com"). This is only set if `rpId` was specified during the `new` call.
* @property {Object} user User information. This will be an empty object
* @property {ArrayBuffer} challenge An ArrayBuffer filled with random bytes. This will be verified in {@link attestationResult}
* @property {Array} [pubKeyCredParams] A list of PublicKeyCredentialParameters objects, based on the `cryptoParams` that was passed to the constructor.
* @property {Number} [timeout] The amount of time that the call should take before returning an error
* @property {String} [attestation] Whether the client should request attestation from the authenticator or not
* @property {Object} [authenticatorSelectionCriteria] A object describing which authenticators are preferred for registration
* @property {String} [authenticatorSelectionCriteria.attachment] What type of attachement is acceptable for new authenticators.
* Allowed values are "platform", meaning that the authenticator is embedded in the operating system, or
* "cross-platform", meaning that the authenticator is removeable (e.g. USB, NFC, or BLE).
* @property {Boolean} [authenticatorSelectionCriteria.requireResidentKey] Indicates whether authenticators must store the keys internally, or if they can
* store them externally (using a KDF or key wrapping)
* @property {String} [authenticatorSelectionCriteria.userVerification] Indicates whether user verification is required for authenticators. User verification
* means that an authenticator will validate a use through their biometrics (e.g. fingerprint) or knowledge (e.g. PIN). Allowed
* values for `userVerification` are "required", meaning that registration will fail if no authenticator provides user verification;
* "preferred", meaning that if multiple authenticators are available, the one(s) that provide user verification should be used; or
* "discouraged", which means that authenticators that don't provide user verification are preferred.
* @property {ArrayBuffer} [rawChallenge] If `extraData` was passed to {@link attestationOptions}, this
* will be the original challenge used, and `challenge` will be a hash:
* SHA256(rawChallenge + extraData)
* @property {Object} [extensions] The values of any enabled extensions.
*/
setOpt(options.rp, "name", this.config.rpName);
setOpt(options.rp, "id", this.config.rpId);
setOpt(options.rp, "icon", this.config.rpIcon);
setOpt(options, "challenge", challenge);
setOpt(options, "pubKeyCredParams", pubKeyCredParams);
setOpt(options, "timeout", this.config.timeout);
setOpt(options, "attestation", this.config.attestation);
if (this.config.authenticatorAttachment !== undefined ||
this.config.authenticatorRequireResidentKey !== undefined ||
this.config.authenticatorUserVerification !== undefined) {
options.authenticatorSelection = {};
setOpt(options.authenticatorSelection, "authenticatorAttachment", this.config.authenticatorAttachment);
setOpt(options.authenticatorSelection, "requireResidentKey", this.config.authenticatorRequireResidentKey);
setOpt(options.authenticatorSelection, "userVerification", this.config.authenticatorUserVerification);
}
setOpt(options, "rawChallenge", rawChallenge);
if (Object.keys(extensions).length > 0) {
options.extensions = extensions;
}
return options;
}
/**
* Creates an assertion challenge and any other parameters for the `navigator.credentials.get()` call.
* The `challenge` property is an `ArrayBuffer` and will need to be encoded to be transmitted to the client.
* @param {Object} [opts] An object containing various options for the option creation
* @param {Object} [opts.extensionOptions] An object that contains the extensions to enable, and the options to use for each of them.
* The keys of this object are the names of the extensions (e.g. - "appid"), and the value of each key is the option that will
* be passed to that extension when it is generating the value to send to the client. This object overrides the extensions that
* have been set with {@link enableExtension} and the options that have been set with {@link setExtensionOptions}. If an extension
* was enabled with {@link enableExtension} but it isn't included in this object, the extension won't be sent to the client. Likewise,
* if an extension was disabled with {@link disableExtension} but it is included in this object, it will be sent to the client.
* @param {String} [extraData] Extra data to be signed by the authenticator during attestation. The challenge will be a hash:
* SHA256(rawChallenge + extraData) and the `rawChallenge` will be returned as part of PublicKeyCredentialCreationOptions.
* @returns {Promise<PublicKeyCredentialRequestOptions>} The options to be passed to `navigator.credentials.get()`
*/
async assertionOptions(opts) {
opts = opts || {};
// https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptions
let challenge = this.tools.randomValues(this.config.challengeSize);
challenge = coerceToArrayBuffer(challenge, "challenge");
let options = {};
// mix extraData into challenge
let rawChallenge;
if (opts.extraData) {
rawChallenge = challenge;
let extraData = coerceToArrayBuffer(opts.extraData, "extraData");
let hash = await this.tools.hashDigest([...abToBuf(challenge),...abToBuf(extraData)]);
challenge = new Uint8Array(hash).buffer;
}
let extensions = createExtensions.call(this, "assertion", opts.extensionOptions);
/**
* @typedef {Object} PublicKeyCredentialRequestOptions
* @description This object is returned by {@link assertionOptions} and is basially the same as
* the [PublicKeyCredentialRequestOptions]{@link https://w3.org/TR/webauthn/#dictdef-publickeycredentialrequestoptions}
* object that is required to be passed to `navigator.credentials.get()`. With the exception of the `challenge` property,
* all other properties are optional and only set if they were specified in the configuration paramater
* that was passed to the constructor.
* @property {ArrayBuffer} challenge An ArrayBuffer filled with random bytes. This will be verified in {@link attestationResult}
* @property {Number} [timeout] The amount of time that the call should take before returning an error
* @property {String} [rpId] Relying party ID, a domain name (e.g. - "example.com"). This is only set if `rpId` was specified during the `new` call.
* @property {String} [attestation] Whether the client should request attestation from the authenticator or not
* @property {String} [userVerification] Indicates whether user verification is required for authenticators. User verification
* means that an authenticator will validate a use through their biometrics (e.g. fingerprint) or knowledge (e.g. PIN). Allowed
* values for `userVerification` are "required", meaning that authentication will fail if no authenticator provides user verification;
* "preferred", meaning that if multiple authenticators are available, the one(s) that provide user verification should be used; or
* "discouraged", which means that authenticators that don't provide user verification are preferred.
* @property {ArrayBuffer} [rawChallenge] If `extraData` was passed to {@link attestationOptions}, this
* will be the original challenge used, and `challenge` will be a hash:
* SHA256(rawChallenge + extraData)
* @property {Object} [extensions] The values of any enabled extensions.
*/
setOpt(options, "challenge", challenge);
setOpt(options, "timeout", this.config.timeout);
setOpt(options, "rpId", this.config.rpId);
setOpt(options, "userVerification", this.config.authenticatorUserVerification);
setOpt(options, "rawChallenge", rawChallenge);
if (Object.keys(extensions).length > 0) {
options.extensions = extensions;
}
return options;
}
}
function checkOptType(opts, prop, type) {
if (typeof opts !== "object") return;
// undefined
if (opts[prop] === undefined) return;
// native type
if (typeof type === "string") {
if (typeof opts[prop] !== type) {
throw new TypeError(`expected ${prop} to be ${type}, got: ${opts[prop]}`);
}
}
// class type
if (typeof type === "function") {
if (!(opts[prop] instanceof type)) {
throw new TypeError(`expected ${prop} to be ${type.name}, got: ${opts[prop]}`);
}
}
}
function setOpt(obj, prop, val) {
if (val !== undefined) {
obj[prop] = val;
}
}
function factorToFlags(expectedFactor, flags) {
// var flags = ["AT"];
flags = flags || [];
switch (expectedFactor) {
case "first":
flags.push("UP");
flags.push("UV");
break;
case "second":
flags.push("UP");
break;
case "either":
flags.push("UP-or-UV");
break;
default:
throw new TypeError("expectedFactor should be 'first', 'second' or 'either'");
}
return flags;
}
function createExtensions(type, extObj) {
/* eslint-disable no-invalid-this */
let extensions = {};
// default extensions
let enabledExtensions = this.extSet;
let extensionsOptions = this.extOptMap;
// passed in extensions
if (typeof extObj === "object") {
enabledExtensions = new Set(Object.keys(extObj));
extensionsOptions = new Map();
for (let key of Object.keys(extObj)) {
extensionsOptions.set(key, extObj[key]);
}
}
// generate extension values
for (let extension of enabledExtensions) {
let extVal = this.generateExtensionOptions(extension, type, extensionsOptions.get(extension));
if (extVal !== undefined) extensions[extension] = extVal;
}
return extensions;
}
// add 'none' attestation format
import { noneAttestation } from "./attestations/none.js";
Webauthn.addAttestationFormat(
noneAttestation.name,
noneAttestation.parseFn,
noneAttestation.validateFn
);
export { Webauthn };