@ideem/zsm-client-sdk
Version:
ZSM makes 2FA easy and invisible for everyone, all the time, using advanced cryptography like MPC to establish cryptographic proof of the origin of any transaction or login attempt, while eliminating opportunities for social engineering. ZSM has no relian
645 lines (525 loc) • 57.6 kB
JavaScript
import eventCoordinator from './EventCoordinator.js';
import GLOBAL from './GlobalScoping.js';
import { uid2iid, iid4uid } from './IdentityIndexing.js';
import { toObject } from './Utils.js';
import {zsmPluginManager} from './PluginManager.js';
import PasskeysPlusClient from './PasskeysPlusClient.js';
eventCoordinator.update('WebAuthnClientBase');
const tracePrimer = (obj=false) => {
const traceBase = [[new Date().toISOString(), "TRACEPRIMER"]]
return (obj) ? {"trace": traceBase} : traceBase;
}
const canParseURL = (url) => {
try { new URL(url); return true; } catch { return false; }
}
class WebAuthnClientBase {
/**
* Constructs a WebAuthnClientBase object.
* @param {Object} config - The configuration for ZSM initialization.
*/
constructor(config) {
// console.log('config :', Object.assign({}, config));
// Helper function to standardize URL format (ensuring it's parsable and ends with a '/')
const cleanURL = (url) => canParseURL(url) ? new URL(url).origin + '/' : false;
this.passkeysPlus = new PasskeysPlusClient();
if(config?.region){
config.authenticator_host_url ||= config.authenticator_host = `https://zsm-authenticator-${config.region}.useideem.com/`;
config.zsm_host_url ||= config.zsm_host = `https://zsm-${config.region}.useideem.com/`;
}
const baseConfig = {
"application_id" : "",
"api_key" : "",
"host_url" : "https://zsm-authenticator-demo.useideem.com/",
"installation_description" : (typeof navigator !== 'undefined' ? navigator?.userAgent : 'No Agent Found'),
"log_level" : 'Debug',
"request_timeout_ms" : 300000,
"retry_count" : 0,
"consumer_id" : "",
"use_passkeys" : true
};
if(config?.use_passkeys===false || config?.use_origin===false){
delete config.use_passkeys;
delete config.use_origin;
}
this.zsmApi = undefined; // The ZSM API instance (read: WASM-driven Crypto Module)
this.relyingParty = undefined; // The Relying Party instance (read: ZSM Authenticator API)
this.status = undefined; // The status of the WebAuthnClientBase
this.config = undefined; // The configuration object passed into the constructor
// If the inputted host_url is not a valid URL, null it for future checks' sake.
config.host_url = cleanURL(config.host_url) || null;
// If the inputted host_url is not a valid URL, but a 'host' is provided instead, construct the host_url.
if(config.host_url == null && config.host != null){
config.host_url = cleanURL(config.host) || `https://zsm-authenticator-${config.host}.useideem.com/`;
}
// If, after all that, the host_url is STILL not a valid URL, destroy it, and let the baseConfig address it.
if(!cleanURL(config.host_url)) delete config.host_url;
config = Object.assign(baseConfig, config);
if(!config?.authenticator_host_url) config.authenticator_host = config.authenticator_host_url = config.host_url;
if(!config?.zsm_host_url) config.zsm_host_url = config.zsm_host = config.host_url.replace("zsm-authenticator", "zsm");
config.host_url = config.zsm_host_url;
this.config = GLOBAL.zsmAppConfig = config;
iid4uid(this.config.consumer_id)
.then(v=>{
// console.log('(v :', v);
GLOBAL.mpcConfig = Object.assign({}, config, {consumer_id: v});
if((GLOBAL.mpcConfig?.use_passkeys??undefined)) {
GLOBAL.mpcConfig.use_origin = true;
delete GLOBAL.mpcConfig.use_passkeys;
}
this.MPCConfig = Object.assign({}, GLOBAL.mpcConfig);
this.zsmApi = this.primeZsmApi();
const relyingParty = zsmPluginManager.classes('RELYINGPARTY');
this.relyingParty = new relyingParty(this.config.authenticator_host_url, this.config.api_key, this.config.application_id, this.config.application_environment, (this.config?.use_passkeys??undefined));
this.userIdentifier = config.consumer_id;
})
.then(()=> GLOBAL.dispatchEvent(new CustomEvent("WebAuthnClientReady", {bubbles: true, cancelable: false, detail: true })))
.catch(error => console.error('Error during initialization:', error))
.finally(() => eventCoordinator.update('WebAuthnClientBase', 'READY'));
}
get userIdentifier() { return this.relyingParty.customerDefinedIdentifier; }
set userIdentifier(v) { return this.relyingParty.customerDefinedIdentifier = v; }
get customerDefinedIdentifier() { return this.relyingParty.customerDefinedIdentifier; }
set customerDefinedIdentifier(v) { return this.relyingParty.customerDefinedIdentifier = v; }
get token() { return this.relyingParty.token; }
get credentialID() { return this.relyingParty.credentialID; }
set credentialID(v) { return this.relyingParty.credentialID = v; }
get isEnrolled() { return !!this.credentialID }
/**
* @name getZsmApi
* @description Retrieves the ZSM API instance. Note that this function is overridden in the WebAuthnClient class as it differs from mobile to react-native to browser.
* @param {Object} config The configuration object for ZSM initialization.
* @returns {Promise<ZSMAPI>} Resolves with the initialized ZSM API.
*/
async getZsmApi (config) {}
/**
* @name primeZsmApi
* @description Initializes the ZSM API with the provided configuration. Used specifically on the load of the worker
* @ to ensure the ZSM API is already primed and initialized (and thus only needs to be reconfigured) when the
* @ by the initializeZsm thereafter. Fixes several potential race conditions and improves performance.
* @returns {Promise<ZSMAPI>} Resolves with the initialized ZSM API.
*/
async primeZsmApi () { return (!this.zsmApi) ? (this.zsmApi = await this.getZsmApi(Object.assign({}, GLOBAL.mpcConfig, {consumer_id: await iid4uid(GLOBAL.mpcConfig.consumer_id)}))) : this.zsmApi; }
/**
* @name initializeZsm
* @description Reconfigures the ZSM API, should the consumer_id change (as after an identity_id is obtained)
* @returns {Promise<ZSMAPI>} Resolves when the ZSM API is reconfigured.
*/
async initializeZsm () {
if(GLOBAL.mpcConfig.consumer_id !== this.MPCConfig.consumer_id) {
await this.zsmApi.configure(GLOBAL.mpcConfig);
this.MPCConfig = await Object.assign({}, GLOBAL.mpcConfig);
await uid2iid(this.config.consumer_id, this.MPCConfig.consumer_id);
}
return await this.zsmApi;
}
/**
* @name checkIdentity
* @description Checks if a user has an identity_id and creates a new one if specified.
* @param {string} userIdentifier The userIdentifier.
* @param {boolean} primeEnroll Indicates the operation is a precursor to enrollment.
* @returns {Promise<String>} Resolves with the retrieved identity_id, if it exists.
* @throws {Error} Throws an error if unable to communicate with the identity server.
* @throws {Error} Throws an error if unable to acquire identity_id.
*/
async checkIdentity (userIdentifier, primeEnroll=false) {
if(userIdentifier == null
|| userIdentifier === '') throw new Error(`[WebAuthnClient] :: checkIdentity :: A userIdentifier String is required! Received: ${userIdentifier}.`);
if(userIdentifier && typeof userIdentifier === 'string') this.userIdentifier = userIdentifier;
try {
const resultOfCheck = await this.relyingParty.checkServerSideIdentity(primeEnroll);
if(!resultOfCheck) throw new Error('Unable to communicate with identity server!');
const identityID = Array.isArray(resultOfCheck) ? resultOfCheck[0].identity_id : resultOfCheck.identity_id;
if(!identityID) throw new Error('Unable to acquire identity_id!');
uid2iid(userIdentifier, identityID);
GLOBAL.mpcConfig.consumer_id = identityID;
return identityID;
} catch(err) {
return Promise.reject(err);
}
}
/**
* @name webauthnRetrieve
* @description Looks up the Identity ID for the specified userIdentifier and retrieves its credential ID (if it exists).
* @ This is used to make a client-only check to see if a given userIdentifier already has an enrollment on the device.
* @param {string} userIdentifier The user identifier.
* @param {boolean} returnExpandedDetails Whether to return the full enrollment details object or just the credential ID.
* @ - false (default): retrieves the ZSM credential ID, or false if not enrolled
* @ - true: retrieves a cleanFinalData object containing all relevant enrollment data
* @param {boolean} forceRemoteCheck If true, forces server consultation even when hasRemotePasskey is false (use before passkey enrollment workflows).
* @returns {Promise<Boolean:false>} Returns `false` if retrieval fails (no enrollment found).
* @returns {Promise<String>} Returns the credential ID if found.
* @returns {Promise<Object>} Resolves with an object containing all relevant credential data.
* @throws {Error} Throws an error if an uncaught/unhandled error occurs while retrieving the credential.
* @memberof WebAuthnClientBase
*/
async webauthnRetrieve (userIdentifier, returnExpandedDetails=false, forceRemoteCheck=false) { // [J] Merged with __webauthnRetrieve; added forceRemoteCheck for enrollment flows
// console.log('[CORE] :: [WebAuthnClientBase] :: webauthnRetrieve :: userIdentifier, returnExpandedDetails', userIdentifier, returnExpandedDetails);
if(userIdentifier == null || typeof userIdentifier !== 'string'
|| userIdentifier === '') throw new Error(`[WebAuthnClient] :: webauthnRetrieve :: A userIdentifier String is required! Received: ${userIdentifier} (of type ${typeof userIdentifier}).`);
if(userIdentifier && typeof userIdentifier === 'string') this.userIdentifier = userIdentifier;
this.credentialID = null;
this.initializeZsm('webauthnRetrieve');
let credentialIdentifier = await iid4uid(userIdentifier); // Obtain identity_id for userIdentifier
GLOBAL.mpcConfig.consumer_id = credentialIdentifier; // Update mpcConfig with identity_id
const response = await this.zsmApi.webauthn_retrieve(userIdentifier); // Perform local retrieval of credential details
let localData = toObject(response);
if (!returnExpandedDetails){ // If we're only looking for the credential ID, we can short-circuit so...
return (localData?.error || !localData?.result) // ... if there's an error (or no result/result's missing)...
? false // ... return false (indicating no enrollment)...
: (this.credentialID = localData.result['zsm_credential_id']??null); // ... otherwise, set the credential ID (if it exists) and return it.
}else{ // Otherwise (we're looking for expanded details)...
localData = (localData?.error || !localData?.result) // ... if there's an error (or no result/result's missing)... [J] Fixed: was reassigning const `response`
? {} // ... we're starting with an empty object (indicating no local knowledge)...
: localData.result; // ... otherwise, we start with any local knowledge we DO have
}
let passkeySupported = false; // [J] Best-effort detection of platform authenticator support
// try {
passkeySupported = !!navigator?.credentials;
// } catch { /* unsupported environment */ }
this.credentialID = localData['zsm_credential_id']??null;
// Server consultation: only needed when local passkey knowledge is insufficient // [J] Replaced hasFailingStatus shotgun check with targeted remote passkey logic
const shouldCheckServer = forceRemoteCheck
|| (passkeySupported && !localData?.['has_remote_passkey'] && !localData?.['passkey_credential_id'])
|| !localData?.['zsm_active']
|| !localData?.['zsm_active']
let completeData = localData;
if(shouldCheckServer) {
let remoteData = await this.checkRemoteEnrollmentDetails(userIdentifier, completeData); // Grab the remote enrollment details...
if(remoteData && !(remoteData instanceof Error)) { // ... and if we got something back (and it wasn't an error)...
completeData = Object.assign({}, localData, remoteData); // ...merge it with completeData (giving remote precedence)
// if(remoteData?.['rcr']) // Persist remote passkey discovery locally to prevent future RTTs
// this.zsmApi.updateEnrollmentStatus(userIdentifier, "passkey"); // [J] Fire-and-forget; next webauthn_retrieve will return hasRemotePasskey locally
}
}
const cleanFinalData = { // Finally, clean up the net enrollment details results...
userIdentifier,
enrollmentActive : completeData?.['zsm_active'] ?? false,
passkeySupported, // [J] Device/browser supports platform authenticator (best-effort)
passkeyEligible : passkeySupported && !!(completeData?.['passkey_credential_id'] || completeData?.['has_remote_passkey']), // [J] Passkeys supported AND a passkey credential exists to use
hasLocalPasskey : passkeySupported && !!(completeData?.['passkey_credential_id']??false), // [J] Now sourced from WASM response directly
hasRemotePasskey : !!completeData?.['rcr'] ?? null, // [J] null = unknown; false = confirmed absent; true = confirmed present
pkpCredentialID : completeData?.['passkey_credential_id'] ?? null, // [J] If we have a local passkey credential ID, use it (this is the most reliable source for passkey presence, as it indicates we've actually used the passkey on this device before); if not, null it (instead of false) to indicate uncertainty
pkpCredID : completeData?.['passkey_credential_id'] ?? null, // [J] If we have a local passkey credential ID, use it (this is the most reliable source for passkey presence, as it indicates we've actually used the passkey on this device before); if not, null it (instead of false) to indicate uncertainty
hasZSMCred : !!completeData?.['zsm_credential'] || false,
zsmCredential : completeData?.['zsm_credential'] ?? null,
zsmCredentialID : completeData?.['zsm_credential_id'] ?? null,
zsmCredID : completeData?.['zsm_credential_id'] ?? null
}
if(cleanFinalData.hasLocalPasskey === true) cleanFinalData.hasRemotePasskey = true; // [J] Sanity check: local passkey presence implies remote passkey presence
if(completeData?.['rcr']) {
cleanFinalData.pkpCredential = completeData['rcr']; // [J] If the server provided a remote credential ID, include it in the cleanFinalData for use in authentication flows (e.g., Passkeys+)
cleanFinalData.hasLocalPasskey = true;
if (!cleanFinalData.pkpCredentialID) {
cleanFinalData.pkpCredentialID = completeData['rcr']?.publicKey?.allowCredentials?.flatMap(c=>c.id)??null;
if(cleanFinalData.pkpCredentialID.length) cleanFinalData.pkpCredentialID = cleanFinalData.pkpCredentialID[0]; // [J] If the server provided multiple remote credential IDs (which shouldn't happen, but just in case), we'll take the first one as the primary indicator of passkey presence/eligibility
}
}
// console.log('[CORE] :: [WebAuthnClientBase] :: webauthnRetrieve :: cleanFinalData :', cleanFinalData);
return cleanFinalData;
};
/**
* @name webauthnRetrieveAll
* @description Syntactic sugar for webauthnRetrieve with returnExpandedDetails=true to retrieve all enrollment information.
* @param {string} userIdentifier The user identifier.
* @param {boolean} forceRemoteCheck If true, forces server consultation even when hasRemotePasskey is false (use before enrollment workflows).
* @returns {Promise<Object>} Returns the full enrollment details for the user.
* @memberof WebAuthnClientBase
*/
async webauthnRetrieveAll (userIdentifier, forceRemoteCheck=false) {
const allEnrollmentDetails = await this.webauthnRetrieve(userIdentifier, true, forceRemoteCheck);
return allEnrollmentDetails;
};
/**
* @name checkRemoteEnrollmentDetails
* @description Checks the server for remote enrollment details when local data has failing status flags.
* @param {string} userIdentifier The user identifier.
* @param {Object} localPayload The local enrollment data to send to the server for comparison.
* @returns {Promise<Object>} Returns remote enrollment details, or false if none found.
* @throws {Error} Throws if the server returns an error.
* @memberof WebAuthnClientBase
*/
async checkRemoteEnrollmentDetails (userIdentifier, localPayload) {
try {
const allEnrollmentDetails = await this.relyingParty.getRemoteEnrollmentDetails(userIdentifier, localPayload);
if(allEnrollmentDetails instanceof Error || !allEnrollmentDetails || allEnrollmentDetails['identity_id'] == null) return false;
return allEnrollmentDetails; // [J] Fixed: was missing return
} catch(err) {
console.error('Error checking remote enrollment details:', err);
return false;
}
}
/**
* @name webauthnCreate
* @description Creates a new WebAuthn credential for the specified userIdentifier.
* @param {string} userIdentifier The user identifier.
* @returns {Promise<Object>} Resolves with the created credential.
* @throws {Error} Throws a COUNTED error if unable to retrieve the public key from the Relying Party Server.
* @throws {Error} [MPC] Throws an error if unable to obtain the createResponse from the Crypto Server.
* @throws {Error} [MPC] Throws an error if the createResponse is missing the credential.
* @throws {Error} [MPC] Throws an error if the credential is missing the attestation object.
* @throws {Error} Throws a COUNTED error if unable to verify the credential with the Relying Party Server.
*/
async webauthnCreate (userIdentifier) {
if(!userIdentifier) throw new Error(`[WebAuthnClient] :: webauthnCreate :: A userIdentifier String is required! Received: ${userIdentifier}.`);
if(userIdentifier && typeof userIdentifier === 'string') this.userIdentifier = userIdentifier;
await this.initializeZsm('webauthnCreate');
const publicKey = await this.relyingParty.registrationStart();
if(publicKey instanceof Error) throw new Error(publicKey);
else if (!publicKey) throw new Error('[WebAuthnClient] :: webauthnCreate :: Unable to retrieve public key from Relying Party Server!');
publicKey.user_id = publicKey.user.id;
let createResponse = await this.zsmApi.webauthn_create(publicKey, undefined, userIdentifier, tracePrimer());
if(!createResponse) throw new Error('[WebAuthnClient - mpc_round] :: webauthnCreate :: Unable to obtain response from Crypto Server!');
createResponse = toObject(createResponse);
GLOBAL.dispatchEvent(new CustomEvent("MPCResponse", {bubbles: true, cancelable: false, detail: createResponse }));
if(!createResponse?.result) throw new Error('[WebAuthnClient - mpc_round] :: webauthnCreate :: Unable to obtain credential from Crypto Server!');
if(!createResponse?.result?.response) throw new Error('[WebAuthnClient - mpc_round] :: webauthnCreate :: Unable to acquire attestation object!');
const credential = createResponse.result;
const registrationResult = await this.relyingParty.registrationFinish(credential);
if(registrationResult instanceof Error) throw new Error(registrationResult);
else if(registrationResult == false) throw new Error('[WebAuthnClient] :: webauthnCreate :: Unable to verify credential Relying Party Server!');
return registrationResult;
};
/**
* @name webauthnGet
* @description Retrieves a WebAuthn credential for the specified userIdentifier.
* @param {string} userIdentifier The user identifier.
* @returns {Promise<ZSMAPI>} Resolves with the retrieved credential.
* @throws {Error} Throws an error if no credential ID is found.
* @throws {Error} Throws a COUNTED error if unable to start authentication with the Relying Party Server.
* @throws {Error} Throws an error if unable to retrieve the public key/challenge from the Relying Party Server.
* @throws {Error} Throws an error if unable to obtain the mpcResponse/credential from the Crypto Server.
* @throws {Error} Throws an error if unable to verify the credential with the Relying Party Server.
* @throws {Error} Throws an error if the user ID or token is missing from the response body.
*/
async webauthnGet (userIdentifier) {
if(userIdentifier == null|| userIdentifier === '') throw new Error(`[WebAuthnClient] :: webauthnGet :: A userIdentifier String is required! Received: ${userIdentifier}.`);
if(userIdentifier && typeof userIdentifier === 'string') this.userIdentifier = userIdentifier;
await this.initializeZsm('webauthnGet');
const credentialId = await this.webauthnRetrieve(userIdentifier, true); // [J] Now passes expanded details to authenticationStart
if (!this.isEnrolled) throw new Error(`[WebAuthnClient] :: webauthnGet :: ${userIdentifier} is not enrolled, lacks a credential, or credential is corrupted!`);
let authStartResponse = await this.relyingParty.authenticationStart(credentialId);
if (authStartResponse instanceof Error) throw new Error(authStartResponse);
else if (!authStartResponse) throw new Error('[WebAuthnClient] :: webauthnGet :: Unable to start authentication with Relying Party Server!');
authStartResponse = toObject(authStartResponse);
if(!authStartResponse?.rcr?.publicKey?.challenge) throw new Error('[WebAuthnClient] :: webauthnGet :: Unable to locate public key/challenge inside Relying Party Server response!\nResponse Body: ' + JSON.stringify(authStartResponse));
const publicKey = authStartResponse.rcr.publicKey;
let mpcResponse = await this.zsmApi.webauthn_get(publicKey, undefined, userIdentifier, tracePrimer());
if(!mpcResponse) throw new Error('[WebAuthnClient - mpc_round] :: webauthnGet :: Crypto server did not respond!');
mpcResponse = toObject(mpcResponse);
if(!mpcResponse?.result) throw new Error('[WebAuthnClient - mpc_round] :: webauthnGet :: Response from Crypto Server did not contain a credential!\nResponse Body: ' + JSON.stringify(mpcResponse));
GLOBAL.dispatchEvent(new CustomEvent("MPCResponse", {bubbles: true, cancelable: false, detail: mpcResponse }));
return mpcResponse.result;
};
/**
* @name webauthnPartialGet
* @description UMFA-OPTIMIZED FLOW: Performs a partial get operation for WebAuthn authentication.
* @param {string} userIdentifier The user identifier.
* @returns {Promise<Object>} Resolves with the created credential.
* @throws {Error} Throws an error if the credential is missing for the userIdentifier provided.
* @throws {Error} Throws an error if unable to start authentication with the Relying Party Server.
* @throws {Error} Throws an error if unable to retrieve the public key from the Relying Party Server.
* @throws {Error} Throws an error if unable to obtain the mpcResponse/credential from the Crypto Server.
* @emits {CustomEvent} Emits a "MPCResponse" event with the mpcResponse data.
*/
async webauthnPartialGet (userIdentifier) {
if(userIdentifier == null
|| userIdentifier === '') throw new Error(`[WebAuthnClient] :: webauthnPartialGet :: A userIdentifier String is required! Received: ${userIdentifier}.`);
if(userIdentifier && typeof userIdentifier === 'string') this.userIdentifier = userIdentifier;
this.status = 'AUTHENTICATION_STARTING';
await this.initializeZsm('webauthnPartialGet');
if (!this.isEnrolled) throw new Error(`[WebAuthnClient] :: webauthnPartialGet :: Credential is missing for ${userIdentifier}`);
let getChallenge = await this.relyingParty.authenticationStart(this.credentialID);
if (!getChallenge) throw new Error('[WebAuthnClient] :: webauthnPartialGet :: Unable to start authentication with Relying Party Server!');
getChallenge = toObject(getChallenge);
if(!getChallenge?.rcr || !getChallenge.rcr.publicKey) throw new Error('[WebAuthnClient] :: webauthnPartialGet :: Unable to retrieve public key from Relying Party Server!');
const pubKey = getChallenge.rcr.publicKey;
let mpcResponse = await this.zsmApi.webauthn_get(pubKey, null, userIdentifier, tracePrimer());
if(!mpcResponse) throw new Error('[WebAuthnClient - mpc_round] :: webauthnPartialGet :: Unable to obtain mpcResponse from Crypto Server!');
mpcResponse = toObject(mpcResponse);
if(!mpcResponse?.result) throw new Error('[WebAuthnClient - mpc_round] :: webauthnPartialGet :: Unable to obtain credential from Crypto Server!');
GLOBAL.dispatchEvent(new CustomEvent("MPCResponse", {bubbles: true, cancelable: false, detail: mpcResponse }));
return mpcResponse.result;
};
/**
* @name unbindFromDevice
* @description Removes the device binding and stored account data for userIdentifier
* @param {String} userIdentifier The Customer-Defined Identifier of the user whose account is to be removed
* @returns {Promise<Boolean>} Returns true if the user's account was present (and subsequently removed from the device)
* @returns {Promise<Boolean>} Returns false if there is no credential matching userIdentifier stored on the device
* @throws {Error<CredentialError>} Throws an CredentialError if the credential is missing for the userIdentifier provided
* @throws {Error<UnenrollError>} Throws an UnenrollError if a credential matching userIdentifier IS stored on the device, but was unable to be removed.
*/
async unbindFromDevice (userIdentifier) {
if(userIdentifier == null
|| userIdentifier === '') throw new Error(`[WebAuthnClient] :: unbindFromDevice :: A userIdentifier String is required! Received: ${userIdentifier}.`);
if(userIdentifier && typeof userIdentifier === 'string') this.userIdentifier = userIdentifier;
this.status = 'UNENROLL_STARTING';
if(userIdentifier !== this.userIdentifier) return false;
await this.initializeZsm('unbindFromDevice');
const rawID = await this.webauthnRetrieve(userIdentifier);
if (!rawID) return false;
const request = indexedDB.open("ideem", 1);
request.onsuccess = function(event) {
const store = event.target.result.transaction("zsm", "readwrite").objectStore("zsm");
const cursorRequest = store.openCursor();
cursorRequest.onsuccess = function(event) {
const cursor = event.target.result;
if (cursor) {
const { key, value } = cursor;
if(
key.includes(userIdentifier) ||
(typeof value === 'string' && value.includes(userIdentifier)) ||
typeof value === 'object' && value?.zsmCredential?.get('rawId') === rawID
) (store.delete(key), true);
cursor.continue();
}
};
cursorRequest.onerror = (err) => { throw new Error(`[WebAuthnClient] :: unbindFromDevice :: Unable to unbind the device for ${userIdentifier}\n${err}`); };
};
request.onerror = (err) => { throw new Error(`[WebAuthnClient] :: unbindFromDevice :: Unable to open indexedDB for ${userIdentifier}\n${err}`); };
this.webauthnRetrieve(userIdentifier);
this.status = 'UNENROLL_COMPLETE';
return true;
}
// PKP Methods ============================================================================================================
/**
* @name webauthnCreateFromPKP
* @description Constructs a new ZSM WebAuthn credential from a Passkeys Plus Platform Authenticator registration response.
* @param {string} userIdentifier The user identifier.
* @param {string} publicKey The public key for the enrollment process.
* @param {string} pairing_token The pairing token for the enrollment process.
* @returns {Promise<Object>} Resolves with the created credential.
* @throws {TypeError} userIdentifier, publicKey, or pairing_token parameters are missing.
* @throws {Error} (MPC Round) Crypto Server failed to respond.
* @throws {Error} (MPC Round) Response did not contain a valid credential/attestation object.
* @throws {Error} Could not verify credential with Relying Party Server.
* @memberof WebAuthnClientBase
* @async
*/
async webauthnCreateFromPKP (userIdentifier, publicKey, pairing_token) {
if(userIdentifier == null
|| userIdentifier === '') throw new TypeError(`[PK+ WebAuthnClient] :: webauthnCreateFromPKP :: A userIdentifier String is required! Received: ${userIdentifier}.`);
if(userIdentifier && typeof userIdentifier === 'string') this.userIdentifier = userIdentifier;
if(!publicKey) throw new TypeError('[WebAuthnClient] :: webauthnCreateFromPKP :: Missing publicKey parameter!');
if(!pairing_token) throw new TypeError('[WebAuthnClient] :: webauthnCreateFromPKP :: Missing pairing_token parameter!');
await this.initializeZsm('webauthnCreate');
let createResponse = await this.zsmApi.webauthn_create(publicKey, pairing_token, userIdentifier);
if(!createResponse) throw new Error('[WebAuthnClient - mpc_round] :: webauthnCreateFromPKP :: Unable to obtain response from Crypto Server!');
createResponse = toObject(createResponse);
GLOBAL.dispatchEvent(new CustomEvent("MPCResponse", {bubbles: true, cancelable: false, detail: createResponse }));
if(!createResponse?.result) throw new Error('[WebAuthnClient - mpc_round] :: webauthnCreateFromPKP :: Unable to obtain credential from Crypto Server!');
if(!createResponse?.result?.response) throw new Error('[WebAuthnClient - mpc_round] :: webauthnCreateFromPKP :: Unable to acquire attestation object!');
const credential = createResponse.result;
return credential;
};
/**
* @name webauthnGetFromPKP
* @description Retrieves a WebAuthn credential for the specified userIdentifier.
* @param {string} userIdentifier The user identifier.
* @param {string} publicKey The public key for the authentication process.
* @param {string} pairing_token The pairing token for the authentication process.
* @returns {Promise<ZSMAPI>} Resolves with the retrieved credential.
* @throws {TypeError} userIdentifier, publicKey, or pairing_token parameters are missing.
* @throws {Error} The class-level credential is missing for the specified userIdentifier.
* @throws {Error} (MPC Round) Crypto Server did not respond.
* @throws {Error} (MPC Round) Response from Crypto Server did not contain a credential.
* @throws {Error} Could not verify credential with Relying Party Server.
* @throws {Error} Token mismatched or missing from Relying Party Server verification attempt.
* @memberof WebAuthnClientBase
* @async
*/
async webauthnGetFromPKP (userIdentifier, publicKey, pairing_token) {
if(userIdentifier == null
|| userIdentifier === '') throw new Error(`[PK+ WebAuthnClient] :: webauthnGetFromPKP :: A userIdentifier String is required! Received: ${userIdentifier}.`);
if(userIdentifier && typeof userIdentifier === 'string') this.userIdentifier = userIdentifier;
if(!publicKey) throw new TypeError('[WebAuthnClient] :: webauthnGetFromPKP :: Missing publicKey parameter!');
if(!pairing_token) throw new TypeError('[WebAuthnClient] :: webauthnGetFromPKP :: Missing pairing_token parameter!');
let mpcResponse = await this.zsmApi.webauthn_get(publicKey, pairing_token, userIdentifier);
if(!mpcResponse) throw new Error('[WebAuthnClient - mpc_round] :: webauthnGetFromPKP :: Crypto server did not respond!');
mpcResponse = toObject(mpcResponse);
if(!mpcResponse?.result) throw new Error('[WebAuthnClient - mpc_round] :: webauthnGetFromPKP :: Response from Crypto Server did not contain a credential!\nResponse Body: ' + JSON.stringify(mpcResponse));
GLOBAL.dispatchEvent(new CustomEvent("MPCResponse", {bubbles: true, cancelable: false, detail: mpcResponse }));
const credential = mpcResponse.result;
return credential;
};
/**
* @name pkpCreate
* @description Creates a Passkeys+ credential for the user, if the user is not already enrolled, then creates a ZSM credential.
* @param {string|object} userIdentifier The identifier for the user (or bootstrapping challenge object).
* @param {string} userVerified Accepts "required", "preferred", "discouraged", to be passed into the Passkey ceremony.
* @param {boolean} bootstrapping If true, indicates that this is part of a bootstrapping flow from another device.
* @returns {Promise<Object>} Call to WebAuthnClientBase's webauthnCreate method
* @throws {TypeError} The userIdentifier is not provided, not a string, or is empty.
* @throws {Error} No response is received from the Relying Party Server.
* @throws {Error} Relying Party Challenge does not contain a publicKey.
* @throws {Error} If the Platform Authenticator Credential is not created successfully.
* @throws {Error} If the Platform Authenticator response is invalid.
* @memberof WebAuthnClientBase
* @async
*/
async pkpCreate (userIdentifier, userVerified="preferred", bootstrapping=false) {
let zsmCredentialChainID = null;
if(bootstrapping && typeof userIdentifier === 'object') {
const credentialDataObject = Object.assign({}, userIdentifier);
zsmCredentialChainID = userIdentifier.zsmCredentialID; // [J] Was .zsmCredID; now uses cleanFinalData field name
this.userIdentifier = userIdentifier = userIdentifier.userIdentifier;
if(userIdentifier == null || typeof userIdentifier !== 'string'
|| userIdentifier === '') throw new TypeError(`[PK+ WebAuthnClient] :: pkpCreate :: A userIdentifier String is required! Received: ${userIdentifier}.`);
this.userIdentifier = userIdentifier;
const relyingPartyChallenge = await this.relyingParty.pkpAuthenticationStart(credentialDataObject);
if(!relyingPartyChallenge) throw new Error(`[PK+ WebAuthnClient] :: pkpCreate :: No response from ZSM API's attempt to interface with Relying Party Server!`);
const paCredential = await this.passkeysPlus.pkpGetPasskeyCredential(relyingPartyChallenge, userVerified);
if (!paCredential) throw new Error('[PK+ WebAuthnClient] :: pkpCreate :: Unable to call Platform Authenticator to create Passkey!');
const rpRegistrationResult = await this.relyingParty.pkpAuthenticationFinish(paCredential);
if (!rpRegistrationResult) throw new Error('[PK+ WebAuthnClient] :: pkpCreate :: Platform Authenticator response was invalid!');
const publicKey = rpRegistrationResult?.response?.ccr?.publicKey??rpRegistrationResult?.response?.rcr?.publicKey;
let zsm_create = await this.webauthnGetFromPKP(userIdentifier, publicKey, rpRegistrationResult.response.token);
this.zsmCredentialId = zsm_create.rawId;
return Promise.resolve(zsm_create);
}else{
if(userIdentifier == null || typeof userIdentifier !== 'string'
|| userIdentifier === '') throw new TypeError(`[PK+ WebAuthnClient] :: pkpCreate :: A userIdentifier String is required! Received: ${userIdentifier}.`);
this.userIdentifier = userIdentifier;
const relyingPartyChallenge = await this.relyingParty.pkpRegistrationStart(userIdentifier, userVerified, zsmCredentialChainID);
if(!relyingPartyChallenge) throw new Error(`[PK+ WebAuthnClient] :: pkpCreate :: No response from ZSM API's attempt to interface with Relying Party Server!`);
const rpChallengePublicKey = relyingPartyChallenge?.publicKey;
if(!rpChallengePublicKey) throw new Error(`[PK+ WebAuthnClient] :: pkpCreate :: No publicKey found in Relying Party Challenge!`);
const paCredential = await this.passkeysPlus.pkpCreatePasskeyCredential(rpChallengePublicKey, userVerified);
if (!paCredential) throw new Error('[PK+ WebAuthnClient] :: pkpCreate :: Unable to call Platform Authenticator to create Passkey!');
const rpRegistrationResult = await this.relyingParty.pkpRegistrationFinish(paCredential);
if (!rpRegistrationResult) throw new Error('[PK+ WebAuthnClient] :: pkpCreate :: Platform Authenticator response was invalid!');
uid2iid('pk:' + userIdentifier, paCredential.id);
const publicKey = rpRegistrationResult?.response?.ccr?.publicKey??rpRegistrationResult?.response?.rcr?.publicKey;
let zsm_create = await this.webauthnCreateFromPKP(userIdentifier, publicKey, rpRegistrationResult.response.token);
this.zsmCredentialId = zsm_create.rawId;
return Promise.resolve(zsm_create);
}
}
/**
* @name pkpAuthenticate
* @description Authenticates a user using a Passkeys+ credential.
* @param {string|object} userIdentifier The identifier for the user (or bootstrapping challenge object).
* @param {string} userVerified Accepts "required", "preferred", "discouraged", to be passed into the Passkey ceremony.
* @returns {Promise<Object>} Call to WebAuthnClientBase's webauthnGet method
* @throws {TypeError} The userIdentifier is not provided, not a string, or is empty.
* @throws {Error} If no Platform Authenticator Credential ID is found for the user.
* @throws {Error} If the Platform Authority Challenge is not retrieved successfully.
* @throws {Error} If the Platform Authenticator Credential is not created successfully.
* @throws {Error} If the Platform Authenticator response is invalid.
* @memberof WebAuthnClientBase
* @async
*/
async pkpAuthenticate (userIdentifier, userVerified="preferred", bootstrapping=false) {
let enrollmentDetails = null;
if(userIdentifier && typeof userIdentifier === 'object') {
enrollmentDetails = {...userIdentifier};
this.userIdentifier = userIdentifier = userIdentifier.userIdentifier;
}
if(userIdentifier == null || userIdentifier === '') throw new Error(`[PK+ WebAuthnClient] :: pkpAuthenticate :: A userIdentifier String is required! Received: ${userIdentifier}.`);
let platformAuthorityChallenge = null;
// Normal authentication flow
this.userIdentifier = userIdentifier;
// const completeUserIdentity = await this.webauthnRetrieveAll(userIdentifier);
platformAuthorityChallenge = await this.relyingParty.pkpAuthenticationStart(enrollmentDetails); // [J] Updated to use cleanFinalData field names; will pass null if we have no info on passkey presence, allowing the server to determine eligibility based on userIdentifier alone (and potentially return a remote credential ID if the user is eligible for Passkeys+ but doesn't have a local passkey)
if(!platformAuthorityChallenge) throw new Error(`[PK+ WebAuthnClient] :: pkpAuthenticate :: Unable to retrieve Platform Authority Challenge from Relying Party Server!`);
const paCredential = await this.passkeysPlus.pkpGetPasskeyCredential(platformAuthorit