@ideem/plugins.passkeys-plus
Version:
Extend your ZSM Client SDK with Passkeys Plus functionality, enabling advanced passkey management and integration, with optional, sync-proof device-binding and attestation, invisible second-factor authentication, and user identity verification!
132 lines (118 loc) • 9.32 kB
JavaScript
import eventCoordinator from '@ideem/zsm-client-sdk/EventCoordinator.js';
import UMFAClientBase from '@ideem/zsm-client-sdk/UMFAClientBase.js';
import { iid4uid } from '@ideem/zsm-client-sdk/IdentityIndexing.js';
class UMFAClient extends UMFAClientBase {
/**
* Constructs a UMFAClient object by extending the UMFAClientBase class.
* @param {Object} config - The configuration for ZSM initialization (ref: ./zsm_app_config.json)
*/
constructor(config) {
super(config);
eventCoordinator.update('UMFAClient', 'PENDING');
this.passkeyEnroll = this.passkeyEnroll.bind(this);
this.passkeyAuthenticate = this.passkeyAuthenticate.bind(this);
if(!(this.config?.use_passkeys??undefined) && window.location.hostname === 'localhost') console.warn('[UMFAClient] :: use_passkeys is not enabled, but you are running on localhost. This may lead to unexpected behavior when directing requests to production environments.');
eventCoordinator.update('UMFAClient', 'READY');
}
/**
* @name checkEnrollment
* @description Checks if a ZSM credential is enrolled on the device for the specified user
* @param {string} userIdentifier The identifier for the user
* @returns {Promise<Object>|object} If the user IS enrolled (possesses a ZSM and/or PKP credential ID), returns an object ({zsmCredID:string, hasZSMCred:boolean, pkpCred:string, hasPKPCred:boolean})
* @returns {Promise<Object>|boolean} If the user is NOT enrolled, returns false
* @returns {Promise<Object>|error} If an error occurs while checking enrollment, returns the Error object
* @throws {Error} If the userIdentifier is not provided or is empty
* @throws {Error} If an uncaught/unhandled error occurs while checking enrollment
* @memberOf UMFAClientBase
*/
async checkEnrollment(userIdentifier) {
try {
if(userIdentifier == null
|| userIdentifier === '') throw new Error(`[UMFAClient] :: checkEnrollment :: A userIdentifier String is required! Received: ${userIdentifier}.`);
const zsmCredID = await this.zsmAPI.webauthnRetrieve(userIdentifier);
if(zsmCredID instanceof Error || zsmCredID == false) return zsmCredID;
const hasZSMCred = true;
const pkpCred = await iid4uid(`pk:${userIdentifier}`);
const hasPKPCred = (pkpCred !== `pk:${userIdentifier}`);
return { zsmCredID, hasZSMCred, pkpCred, hasPKPCred };
} catch(e) {
return new Error(e.message || e || '[UMFAClient] :: checkEnrollment :: An error occurred while attempting to check enrollment.');
}
}
/**
* @name enroll
* @description Enrolls a user by creating a ZSM credential on the device or acts as a proxy for Passkeys+ enrollment when usePasskeys flag is set
* @param {string} userIdentifier The identifier for the user
* @param {boolean} usePasskeys Whether to use Passkeys+ enrollment method instead of base class method
* @param {boolean} pkpOnly Whether to only use Passkeys+ enrollment method
* @returns {Promise<Object>} The results of the call to UMFAClientBase's enroll or the passkeyEnroll methods
* @overrides {function} enroll (ref: UMFAClientBase)
* @memberOf UMFAClient, PasskeysPlus
*/
enroll(userIdentifier, usePasskeys=false, pkpOnly=false) {
return usePasskeys ? this.passkeyEnroll(userIdentifier, pkpOnly) : super.enroll(userIdentifier);
}
/**
* @name passkeyEnroll
* @description Creates a Passkeys+ credential for the user, if the user is not already enrolled, then creates a ZSM credential
* @param {string} userIdentifier The identifier for the user
* @param {boolean} pkpOnly Whether to only use Passkeys+ enrollment method
* @returns {Promise<Object>} The results of the call to WebAuthnClient's pkpCreate method or an Error
* @throws {Error} If the userIdentifier is not provided or is empty
* @throws {Error} If an error occurs during Passkeys+ enrollment
* @extends UMFAClientBase Adds this function to base class
* @memberOf UMFAClient, PasskeysPlus
*/
async passkeyEnroll(userIdentifier, pkpOnly=false) {
try {
if(userIdentifier == null
|| userIdentifier === '') throw new Error(`[PK+ UMFAClient] :: passkeyEnroll :: A userIdentifier String is required! Received: ${userIdentifier}.`);
const pkpCreateResult = await this.zsmAPI.pkpCreate(userIdentifier, pkpOnly);
if (pkpCreateResult instanceof Error) throw new Error(`[PK+ UMFAClient] :: passkeyEnroll :: Error occurred during Passkeys+ enrollment! \nDetails:\n${pkpCreateResult.message}`);
return pkpCreateResult;
} catch (e) {
return new Error(e.message || e || '[PK+ UMFAClient] :: passkeyEnroll :: An error occurred during Passkeys+ enrollment.');
}
}
/**
* @name authenticate
* @description Authenticates a user by retrieving their ZSM credential or acts as a proxy for Passkeys+ authentication when usePasskeys flag is set
* @param {string} userIdentifier The identifier for the user
* @param {boolean} usePasskeys Whether to use Passkeys+ authentication method instead of base class method
* @returns {Promise<Object>} The results of the call to UMFAClientBase's authenticate or the passkeyAuthenticate methods
* @overrides {function} authenticate (ref: UMFAClientBase)
* @memberOf UMFAClient, PasskeysPlus
*/
authenticate(userIdentifier, usePasskeys=false) {
return usePasskeys ? this.passkeyAuthenticate(userIdentifier) : super.authenticate(userIdentifier);
}
/**
* @name passkeyAuthenticate
* @description Authenticates a user using Passkeys+ credentials
* @param {string} userIdentifier The identifier for the user
* @returns {Promise<Object>} The results of the call to WebAuthnClient's pkpAuthenticate method or an Error
* @throws {Error} If the userIdentifier is not provided or is empty
* @throws {Error} If an error occurs during Passkeys+ authentication
* @extends UMFAClientBase Adds this function to base class
* @memberOf UMFAClient, PasskeysPlus
*/
async passkeyAuthenticate(userIdentifier) {
try {
if(userIdentifier == null
|| userIdentifier === '') throw new Error(`[PK+ UMFAClient] :: passkeyAuthenticate :: A userIdentifier String is required! Received: ${userIdentifier}.`);
const storedZSMCred = await iid4uid(userIdentifier);
const storedPKPCred = await iid4uid(`pk:${userIdentifier}`);
const hasZSMCred = (storedZSMCred !== userIdentifier);
const hasPKPCred = (storedPKPCred !== `pk:${userIdentifier}`);
if(!hasPKPCred && !hasZSMCred) throw new Error(`[PK+ UMFAClient] :: passkeyAuthenticate :: No ZSM credentials found for userIdentifier: ${userIdentifier}. Please enroll the user first.`);
if(!hasPKPCred && hasZSMCred) return new Error(`[PK+ UMFAClient] :: passkeyAuthenticate :: ZSM credentials WERE found for userIdentifier: ${userIdentifier}, but no Passkeys+ credential found on this device. Enroll the user using Passkeys+ in addition to their ZSM Enrollment.`, {cause: 'PKP_UNREGISTERED'});
if(hasPKPCred === `pk:${userIdentifier}`) throw new Error(`[PK+ UMFAClient] :: passkeyAuthenticate :: No Passkeys+ credentials found for userIdentifier: ${userIdentifier}.`);
const pkpAuthResult = await this.zsmAPI.pkpAuthenticate(userIdentifier);
if (pkpAuthResult instanceof Error) throw new Error(`[PK+ UMFAClient] :: passkeyAuthenticate :: Error occurred during Passkeys+ authentication! \nDetails:\n${pkpAuthResult.message}`);
return pkpAuthResult;
} catch (e) {
return new Error(e.message || e || '[PK+ UMFAClient] :: passkeyAuthenticate :: An error occurred during Passkeys+ authentication.', e?.cause);
}
}
}
export {UMFAClient};