UNPKG

@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

372 lines (331 loc) 22.7 kB
import eventCoordinator from './EventCoordinator.js'; import GLOBAL from './GlobalScoping.js'; import {uid2iid} from './IdentityIndexing.js'; import { b64urlToBuf, toObject } from './Utils.js'; eventCoordinator.update('RelyingPartyBase'); function tracePrimer (obj=false) { const traceBase = [[new Date().toISOString(), "TRACEPRIMER"]] return (obj) ? {"trace": traceBase} : traceBase; } //## BEGIN RelyingPartyBase =================================================================================================================================== class RelyingPartyBase { /** * Constructs a RelyingParty object. * @param {string} host - The URL of the relying party server. * @param {string} apiKey - The API key for authentication. * @param {string} applicationID - The ID of the application. */ constructor(host, apiKey, applicationID, appEnvironment, useOrigin) { this.host = host; this.apiKey = apiKey; this.applicationId = applicationID; this.appEnvironment = appEnvironment || null; this.customerDefinedIdentifier = undefined; this.publicKey = undefined; this.identityID = undefined; this.credentialID = undefined; this.traceId = undefined; this.xhrHeaders = { 'Content-Type' : 'application/json' }; this.addlRelyingPartyBodyProps = useOrigin ? {use_origin: "true"} : {}; eventCoordinator.update('RelyingPartyBase', 'READY'); } get userIdentifier() { return this.customerDefinedIdentifier; } set userIdentifier(v) { return userIdentifier = this.customerDefinedIdentifier = v; } /** * @name makePostRequest * @description Performs an HTTP POST request * @param {string} url The URL to send the request to. * @param {Object} body The request body data. * @param {Object} headers Additional headers for the request. * @param {string} method The HTTP method to use (default is 'POST'). * @returns {Promise<Object>} Resolves with the response data. */ makePostRequest (url, body={}, headers={}, method='POST') { url = `${this.host.endsWith('/') ? this.host.slice(0,-1) : this.host}/${url.startsWith('/') ? url.slice(1) : url}`; body = ((typeof body === 'object') ? body : {}); if(this.appEnvironment) body = Object.assign(body, {environment: this.appEnvironment}); body = Object.assign({ customer_defined_identifier : this.customerDefinedIdentifier, application_id : this.applicationId}, body, this.addlRelyingPartyBodyProps, tracePrimer(true)); let fetchObj = { method, // headers : Object.assign(this.xhrHeaders, ((!headers?.Authorization) ? {'Authorization': `Bearer ${this.apiKey}`} : {}), headers, {"X-SDK-Version": JSON.stringify(GLOBAL.ZSMVersions)}), headers : Object.assign(this.xhrHeaders, ((!headers?.Authorization) ? {'Authorization': `Bearer ${this.apiKey}`} : {}), headers), body : JSON.stringify(body) }; return fetch(url, fetchObj) .then(response => response.ok ? response.json() : response.json().then(errData => { throw new Error(`[RelyingParty] :: makePostRequest :: Request failed: ${response.statusText}, ${JSON.stringify(errData)}`) }) ) .then(response => { GLOBAL.dispatchEvent(new CustomEvent("AuthResponse", {bubbles: true, cancelable: false, detail: response })); return response; }); } /** * @name clearEnrollmentCredentials * @description Clears the enrollment credentials. * @returns {Promise<ZSMAPI>} */ clearEnrollmentCredentials () { return (this.publicKey = this.credentialID = null) }; /** * @name clearLoginCredentials * @description Clears the login credentials. * @returns {Promise<Void>} * @todo Verify nothing is calling this and remove it outright. */ clearLoginCredentials () {}; /** * @name resetAll * @description Resets all credentials. * @returns {Promise<Void>} */ resetAll () { return (this.clearLoginCredentials(), this.clearEnrollmentCredentials()); } /** * @name registrationStart * @description Sends a login request to the server. * @returns {Promise<Object>} Resolves with publicKey object used in the subsequent Crypto Server API call. */ registrationStart () { return this.makePostRequest(`api/webauthn/registration/start`) .then(data => { if(data.identity_id){ GLOBAL.mpcConfig.consumer_id = data.identity_id; uid2iid(this.userIdentifier, data.identity_id); } return (this.publicKey = data.ccr.publicKey) }) .then(result => result) .catch(error => Promise.reject(new Error(error))); } /** * @name registrationFinish * @description Completes WebAuthn registration by sending the credential to the server. * @param {Object} credential The credential information to finalize registration. * @returns {Promise<Object>} Resolves with the registered credential. * @throws {Error} Throws an error if the request fails. */ registrationFinish (credential) { return credential; return this.makePostRequest(`api/webauthn/registration/finish`, { credential }) .then(body => { this.credentialID = credential.rawId; return {credential, response: body, ...body}; }) .catch(error => (this.clearEnrollmentCredentials(), Promise.reject(new Error(error)))); } /** * @name authenticationStart * @description Starts the authentication process by sending a request to the server. * @param {String} credential_id The ID of the credential to use for authentication. * @returns {Promise<Object>} Resolves with the authentication challenge. * @throws {Error} Throws an error if the request fails. */ authenticationStart (credential_id=this.credentialID, credential_type = 'passkey', chain_handoff, chain_credential_id, chain_challenge) { const body = { credential_type, credential_id, chain_handoff, chain_credential_id, chain_challenge, } return (credential_id != null) ? this.makePostRequest( `api/webauthn/authentication/start`, { credential_id }) .then(result => result) : (Promise.reject(new Error("[RelyingParty] :: authenticationStart :: No user ID or credential ID provided"))); } /** * @name authenticationFinish * @description Completes the authentication process by sending the credential to the server. * @param {Object} credential The credential information to finalize authentication. * @returns {Promise<Object>} Resolves with the authentication result. * @throws {Error} Throws an error if the request fails. */ authenticationFinish (credential) { return credential; return this.makePostRequest("api/webauthn/authentication/finish", { credential }) .then(result => result) .catch(error => Promise.reject(new Error(error))); } /** * @name createIdentityThenRegistrationStart * @description UMFA-OPTIMIZED FLOW: Creates a server-side Identity and then starts the WebAuthn Registration challenge automatically. * @returns {Promise<Object>} Resolves with an object containing the publicKey and identity_id. * @throws {Error} Throws an error if the request fails. */ createIdentityThenRegistrationStart(credential_type = 'zsm', chain_handoff, chain_credential_id, chain_challenge) { const body = { credential_type, chain_handoff, chain_credential_id, chain_challenge, }; return this.makePostRequest("api/webauthn/registration/create-identity-then-start", body) .then(data => { if(data.identity_id){ GLOBAL.mpcConfig.consumer_id = data.identity_id; uid2iid(this.userIdentifier, data.identity_id); } return {publicKey:(this.publicKey = data.ccr.publicKey), identity_id:data.identity_id} }) .then(result => result) .catch(error => Promise.reject(new Error(error))); } /** * @name registrationFinishAuthenticationStart * @description UMFA-OPTIMIZED FLOW: Completes WebAuthn registration by sending the credential to the server. * @param {Object} credential The credential information to finalize registration. * @returns {Promise<Object>} Resolves with the registered credential. * @throws {Error} Throws an error if the request fails. */ registrationFinishAuthenticationStart (credential) { return credential; return this.makePostRequest(`api/webauthn/registration/finish-then-authentication-start`, { credential }) .then(result => (this.credentialID = credential.rawId, result)) .then(credential => credential) .catch(error => (this.clearEnrollmentCredentials(), Promise.reject(new Error(error)))); } /** * @name checkServerSideIdentity * @description Checks if an identity exists on the server and creates one if it doesn't. * @param {boolean} createNewIdentity Whether to create a new identity if one doesn't exist. * @returns {Promise<Object>} Resolves with the server response. * @throws {Error} Throws an error if the request fails. */ checkServerSideIdentity (createNewIdentity) { createNewIdentity = createNewIdentity ? { create_new_identity: "true" } : {}; return this.makePostRequest("api/umfa/check-identity", createNewIdentity) .then(result => result) .catch(error => Promise.reject(new Error(error))); } //! PASSKEYS PLUS RELYING PARTY METHODS ========================================================================================================================= /** * @name pkpRegistrationStart * @description Starts the registration process for a passkey using the Relying Party. * @param {string} userIdentifier The identifier for the user. * @param {string} userVerified Indicates if the user has been verified. * @param {string|null} zsmCredChainID (Optional) The ZSM Credential Chain ID for linking credentials. * @returns {Promise<Object>} Resolves with the Relying Party Challenge object. * @throws {Error} If the Relying Party Challenge is not retrieved successfully. * @throws {Error} If the public key is not found in the Relying Party Challenge. * @throws {Error} If the user ID is not found in the Relying Party Challenge. * @memberOf RelyingPartyBase */ async pkpRegistrationStart (userIdentifier, userVerified, zsmCredChainID = null) { const requestBody = zsmCredChainID ? {chain_handoff: 'internal', chain_credential_id: zsmCredChainID} : {chain_handoff: 'internal'}; const paChallenge = await this.makePostRequest(`api/passkey/registration/create-identity-then-start`, requestBody) .then(data => { if(data.identity_id){ GLOBAL.mpcConfig.consumer_id = data.identity_id; uid2iid(this.userIdentifier, data.identity_id); } return {publicKey:(this.publicKey = data.ccr.publicKey), identity_id:data.identity_id} }) .then(result => result) .catch(error => Promise.reject(new Error(error))); if(!paChallenge) throw new Error(`[PK+ RelyingParty] :: pkpRegistrationStart :: Unable to retrieve Platform Authority Challenge from Relying Party Server!`); if(!paChallenge?.publicKey) throw new Error(`[PK+ RelyingParty] :: pkpRegistrationStart :: Platform Authority Challenge's Public Key was not found!`); paChallenge.publicKey.challenge = b64urlToBuf(paChallenge.publicKey.challenge); if(!paChallenge?.publicKey?.user) throw new Error(`[PK+ RelyingParty] :: pkpRegistrationStart :: User for the Platform Authority Challenge Public Key's user node was not found!`); paChallenge.publicKey.user.id = b64urlToBuf(paChallenge.publicKey.user.id); return paChallenge; } /** * @name pkpRegistrationFinish * @description Completes the registration process for a passkey using the Relying Party. * @param {Object} credential The Platform Authenticator Credential object. * @returns {Promise<Object>} Resolves with the registration result. * @throws {TypeError} If the Platform Authenticator Credential is not provided. * @throws {Error} If the Platform Authenticator response is invalid. * @memberOf RelyingPartyBase * @async */ async pkpRegistrationFinish (credential) { if (!credential) throw new TypeError('[PK+ RelyingParty] :: pkpRegistrationFinish :: No Platform Authenticator Credential provided!'); return this.makePostRequest(`api/passkey/registration/finish`, { credential }) .then(body => { this.credentialID = credential.rawId; return {credential, credentialID: this.credentialID, response: {credentialID: this.credentialID, ...body}}; }) .catch(err => { this.clearEnrollmentCredentials(); throw new Error('[PK+ RelyingParty] :: pkpRegistrationFinish :: Unable to complete registration!', {cause: err}); }); } /** * @name pkpAuthenticationStart * @description Starts the authentication process for a passkey using the Relying Party. * @param {object} credentialData The credential data object (cleanFinalData shape, possibly with userIdentifier attached). * @returns {Promise<Object>} Resolves with the Relying Party Challenge object. * @throws {TypeError} If the Credential Data object is not provided. * @throws {Error} If the Credential Data object does not contain valid IDs. * @throws {Error} If the Relying Party Challenge is not retrieved successfully. * @throws {Error} If the RCR challenge and/or publicKey is not found in the Relying Party Challenge. * @throws {Error} If the allowCredentials and/or id is not found in the Relying Party Challenge. * @memberOf RelyingPartyBase * @async */ async pkpAuthenticationStart (credentialData) { if(credentialData == null || typeof credentialData !== 'object') throw new TypeError('[PK+ RelyingParty] :: pkpAuthenticationStart :: No Credential Data object provided!'); if(!credentialData.hasRemotePasskey && !credentialData.zsmCredentialID) // [J] Field names updated to match cleanFinalData throw new Error('[PK+ RelyingParty] :: pkpAuthenticationStart :: Credential Data object does not contain a valid Passkeys+ Credential ID or ZSM Credential ID!'); const body = { credential_id : credentialData?.pkpCredentialID, // [J] PKP credential ID not present in cleanFinalData; to be addressed when cleanFinalData is moved to post-RP boundary chain_credential_id : credentialData.zsmCredentialID, // [J] Was .zsmCredID chain_handoff : credentialData?.chainHandoff || 'internal', chain_challenge : credentialData?.pkpCredential ? JSON.stringify(credentialData.pkpCredential) : null, }; let paChallenge = await this.makePostRequest( `api/passkey/authentication/start`, body) if(paChallenge instanceof Error || !paChallenge) throw new Error('[PK+ RelyingParty] :: pkpAuthenticationStart :: Unable to retrieve Platform Authority Challenge from Relying Party Server!', {cause: paChallenge}); paChallenge = toObject(paChallenge); if(!paChallenge?.rcr || !paChallenge?.rcr?.publicKey) throw new Error(`[PK+ RelyingParty] :: pkpAuthenticationStart :: Platform Authority's RCR challenge and/or publicKey was not found!`); const paRCRPublicKey = paChallenge.rcr.publicKey; paRCRPublicKey.challenge = b64urlToBuf(paRCRPublicKey.challenge); const paAllowCredentials = paRCRPublicKey.allowCredentials; if(!paAllowCredentials || !paAllowCredentials[0]?.id) throw new Error(`[PK+ RelyingParty] :: pkpAuthenticationStart :: Platform Authority Challenge's allowCredentials and/or id was not found!`); paAllowCredentials[0].id = b64urlToBuf(paAllowCredentials[0].id); return paRCRPublicKey; } /** * @name pkpAuthenticationFinish * @description Completes the authentication process for a passkey using the Relying Party. * @param {Object} paCredential The Platform Authenticator Credential object. * @returns {Promise<Object>} Resolves with the authentication result. * @throws {Error} If the Platform Authenticator Credential is not provided. * @throws {Error} If the Platform Authenticator response is invalid. * @memberOf RelyingPartyBase * @async */ async pkpAuthenticationFinish (credential) { if (!credential || typeof credential !== 'object') throw new Error('[PK+ RelyingParty] :: pkpAuthenticationFinish :: No Platform Authenticator Credential provided or is not an object!'); return this.makePostRequest(`api/passkey/authentication/finish`, { credential }) .then(body => ({credential, response: body})) .catch(err => Promise.reject(new Error('[PK+ RelyingParty] :: pkpAuthenticationFinish :: Unable to complete authentication!', {cause: err}))); } /** * @name getRemoteEnrollmentDetails * @description Retrieves remote enrollment details for a given user identifier. * @param {string} userIdentifier The user identifier. * @param {Object} localDataPayload The local data payload. * @returns {Promise<Object>} Resolves with the enrollment details. * @throws {Error} Throws an error if the request fails. */ async getRemoteEnrollmentDetails (userIdentifier, localDataPayload, bootstrap=false) { // console.log('[CORE] :: [RelyingPartyBase] :: getRemoteEnrollmentDetails :: userIdentifier, localDataPayload', userIdentifier, localDataPayload); const body = { customer_defined_identifier: userIdentifier, }; if(!bootstrap) { // [J] During the initial bootstrap, we want to minimize the data sent in this request to reduce the payload size and speed up the response; on subsequent calls, we can include the full localDataPayload for more informed server responses // if(localDataPayload['zsm_credential_id']) body.zsmCredentialID = localDataPayload['zsm_credential_id']; // [J] Field names updated to match cleanFinalData // if(localDataPayload['passkey_credential_id']) body.pkpCredentialID = localDataPayload['passkey_credential_id']; // [J] Field names updated to match cleanFinalData } const response = await this.makePostRequest('/api/v1/enrollment-details', body); // console.log('[CORE] :: [RelyingPartyBase] :: getRemoteEnrollmentDetails :: response', response); return response; } } //## END RelyingPartyBase ===================================================================================================================================== export default RelyingPartyBase;