UNPKG

@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
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};