@ideem/zsm-client-sdk 2.7.1 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -1
- package/ErrorHandler.js +1 -1
- package/FIDO2ClientBase.js +248 -29
- package/PKPUtils.js +61 -0
- package/PasskeysPlusClient.js +120 -0
- package/PluginManager.js +1 -1
- package/RelyingPartyBase.js +156 -6
- package/UMFAClientBase.js +269 -51
- package/WASMRustInterface.js +262 -218
- package/WebAuthnClientBase.js +447 -129
- package/ZSMClientSDK.js +1 -1
- package/encodeUserIdentifier.js +17 -0
- package/package.json +13 -12
package/RelyingPartyBase.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import eventCoordinator
|
|
2
|
-
import GLOBAL
|
|
3
|
-
import {uid2iid}
|
|
1
|
+
import eventCoordinator from './EventCoordinator.js';
|
|
2
|
+
import GLOBAL from './GlobalScoping.js';
|
|
3
|
+
import {uid2iid} from './IdentityIndexing.js';
|
|
4
|
+
import { b64urlToBuf, toObject } from './Utils.js';
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
eventCoordinator.update('RelyingPartyBase');
|
|
@@ -71,20 +72,20 @@ class RelyingPartyBase {
|
|
|
71
72
|
/**
|
|
72
73
|
* @name clearEnrollmentCredentials
|
|
73
74
|
* @description Clears the enrollment credentials.
|
|
74
|
-
* @returns {Promise<ZSMAPI>}
|
|
75
|
+
* @returns {Promise<ZSMAPI>}
|
|
75
76
|
*/
|
|
76
77
|
clearEnrollmentCredentials () { return (this.publicKey = this.credentialID = null) };
|
|
77
78
|
/**
|
|
78
79
|
* @name clearLoginCredentials
|
|
79
80
|
* @description Clears the login credentials.
|
|
80
81
|
* @returns {Promise<Void>}
|
|
81
|
-
* @todo Verify nothing is calling this and remove it outright.
|
|
82
|
+
* @todo Verify nothing is calling this and remove it outright.
|
|
82
83
|
*/
|
|
83
84
|
clearLoginCredentials () {};
|
|
84
85
|
/**
|
|
85
86
|
* @name resetAll
|
|
86
87
|
* @description Resets all credentials.
|
|
87
|
-
* @returns {Promise<Void>}
|
|
88
|
+
* @returns {Promise<Void>}
|
|
88
89
|
*/
|
|
89
90
|
resetAll () { return (this.clearLoginCredentials(), this.clearEnrollmentCredentials()); }
|
|
90
91
|
|
|
@@ -215,6 +216,155 @@ class RelyingPartyBase {
|
|
|
215
216
|
.then(result => result)
|
|
216
217
|
.catch(error => Promise.reject(new Error(error)));
|
|
217
218
|
}
|
|
219
|
+
|
|
220
|
+
//! PASSKEYS PLUS RELYING PARTY METHODS =========================================================================================================================
|
|
221
|
+
/**
|
|
222
|
+
* @name pkpRegistrationStart
|
|
223
|
+
* @description Starts the registration process for a passkey using the Relying Party.
|
|
224
|
+
* @param {string} userIdentifier The identifier for the user.
|
|
225
|
+
* @param {string} userVerified Indicates if the user has been verified.
|
|
226
|
+
* @param {string|null} zsmCredChainID (Optional) The ZSM Credential Chain ID for linking credentials.
|
|
227
|
+
* @returns {Promise<Object>} Resolves with the Relying Party Challenge object.
|
|
228
|
+
* @throws {Error} If the Relying Party Challenge is not retrieved successfully.
|
|
229
|
+
* @throws {Error} If the public key is not found in the Relying Party Challenge.
|
|
230
|
+
* @throws {Error} If the user ID is not found in the Relying Party Challenge.
|
|
231
|
+
* @memberOf RelyingPartyBase
|
|
232
|
+
*/
|
|
233
|
+
async pkpRegistrationStart (userIdentifier, userVerified, zsmCredChainID = null) {
|
|
234
|
+
const requestBody = zsmCredChainID ? {chain_handoff: 'internal', chain_credential_id: zsmCredChainID} : {chain_handoff: 'internal'};
|
|
235
|
+
const paChallenge = await this.makePostRequest(`api/passkey/registration/create-identity-then-start`, requestBody)
|
|
236
|
+
.then(data => {
|
|
237
|
+
if(data.identity_id){
|
|
238
|
+
GLOBAL.mpcConfig.consumer_id = data.identity_id;
|
|
239
|
+
uid2iid(this.userIdentifier, data.identity_id);
|
|
240
|
+
}
|
|
241
|
+
return {publicKey:(this.publicKey = data.ccr.publicKey), identity_id:data.identity_id}
|
|
242
|
+
})
|
|
243
|
+
.then(result => result)
|
|
244
|
+
.catch(error => Promise.reject(new Error(error)));
|
|
245
|
+
if(!paChallenge) throw new Error(`[PK+ RelyingParty] :: pkpRegistrationStart :: Unable to retrieve Platform Authority Challenge from Relying Party Server!`);
|
|
246
|
+
if(!paChallenge?.publicKey) throw new Error(`[PK+ RelyingParty] :: pkpRegistrationStart :: Platform Authority Challenge's Public Key was not found!`);
|
|
247
|
+
|
|
248
|
+
paChallenge.publicKey.challenge = b64urlToBuf(paChallenge.publicKey.challenge);
|
|
249
|
+
if(!paChallenge?.publicKey?.user) throw new Error(`[PK+ RelyingParty] :: pkpRegistrationStart :: User for the Platform Authority Challenge Public Key's user node was not found!`);
|
|
250
|
+
|
|
251
|
+
paChallenge.publicKey.user.id = b64urlToBuf(paChallenge.publicKey.user.id);
|
|
252
|
+
return paChallenge;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* @name pkpRegistrationFinish
|
|
257
|
+
* @description Completes the registration process for a passkey using the Relying Party.
|
|
258
|
+
* @param {Object} credential The Platform Authenticator Credential object.
|
|
259
|
+
* @returns {Promise<Object>} Resolves with the registration result.
|
|
260
|
+
* @throws {TypeError} If the Platform Authenticator Credential is not provided.
|
|
261
|
+
* @throws {Error} If the Platform Authenticator response is invalid.
|
|
262
|
+
* @memberOf RelyingPartyBase
|
|
263
|
+
* @async
|
|
264
|
+
*/
|
|
265
|
+
async pkpRegistrationFinish (credential) {
|
|
266
|
+
if (!credential) throw new TypeError('[PK+ RelyingParty] :: pkpRegistrationFinish :: No Platform Authenticator Credential provided!');
|
|
267
|
+
|
|
268
|
+
return this.makePostRequest(`api/passkey/registration/finish`, { credential })
|
|
269
|
+
.then(body => {
|
|
270
|
+
this.credentialID = credential.rawId;
|
|
271
|
+
return {credential, credentialID: this.credentialID, response: {credentialID: this.credentialID, ...body}};
|
|
272
|
+
})
|
|
273
|
+
.catch(err => {
|
|
274
|
+
this.clearEnrollmentCredentials();
|
|
275
|
+
throw new Error('[PK+ RelyingParty] :: pkpRegistrationFinish :: Unable to complete registration!', {cause: err});
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* @name pkpAuthenticationStart
|
|
281
|
+
* @description Starts the authentication process for a passkey using the Relying Party.
|
|
282
|
+
* @param {object} credentialData The credential data object (cleanFinalData shape, possibly with userIdentifier attached).
|
|
283
|
+
* @returns {Promise<Object>} Resolves with the Relying Party Challenge object.
|
|
284
|
+
* @throws {TypeError} If the Credential Data object is not provided.
|
|
285
|
+
* @throws {Error} If the Credential Data object does not contain valid IDs.
|
|
286
|
+
* @throws {Error} If the Relying Party Challenge is not retrieved successfully.
|
|
287
|
+
* @throws {Error} If the RCR challenge and/or publicKey is not found in the Relying Party Challenge.
|
|
288
|
+
* @throws {Error} If the allowCredentials and/or id is not found in the Relying Party Challenge.
|
|
289
|
+
* @memberOf RelyingPartyBase
|
|
290
|
+
* @async
|
|
291
|
+
*/
|
|
292
|
+
async pkpAuthenticationStart (credentialData) {
|
|
293
|
+
|
|
294
|
+
if(credentialData == null || typeof credentialData !== 'object')
|
|
295
|
+
throw new TypeError('[PK+ RelyingParty] :: pkpAuthenticationStart :: No Credential Data object provided!');
|
|
296
|
+
if(!credentialData.hasRemotePasskey && !credentialData.zsmCredentialID) // [J] Field names updated to match cleanFinalData
|
|
297
|
+
throw new Error('[PK+ RelyingParty] :: pkpAuthenticationStart :: Credential Data object does not contain a valid Passkeys+ Credential ID or ZSM Credential ID!');
|
|
298
|
+
|
|
299
|
+
const body = {
|
|
300
|
+
credential_id : credentialData?.pkpCredentialID, // [J] PKP credential ID not present in cleanFinalData; to be addressed when cleanFinalData is moved to post-RP boundary
|
|
301
|
+
chain_credential_id : credentialData.zsmCredentialID, // [J] Was .zsmCredID
|
|
302
|
+
chain_handoff : credentialData?.chainHandoff || 'internal',
|
|
303
|
+
chain_challenge : credentialData?.pkpCredential ? JSON.stringify(credentialData.pkpCredential) : null,
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
let paChallenge = await this.makePostRequest( `api/passkey/authentication/start`, body)
|
|
307
|
+
if(paChallenge instanceof Error || !paChallenge)
|
|
308
|
+
throw new Error('[PK+ RelyingParty] :: pkpAuthenticationStart :: Unable to retrieve Platform Authority Challenge from Relying Party Server!', {cause: paChallenge});
|
|
309
|
+
|
|
310
|
+
paChallenge = toObject(paChallenge);
|
|
311
|
+
|
|
312
|
+
if(!paChallenge?.rcr ||
|
|
313
|
+
!paChallenge?.rcr?.publicKey) throw new Error(`[PK+ RelyingParty] :: pkpAuthenticationStart :: Platform Authority's RCR challenge and/or publicKey was not found!`);
|
|
314
|
+
|
|
315
|
+
const paRCRPublicKey = paChallenge.rcr.publicKey;
|
|
316
|
+
paRCRPublicKey.challenge = b64urlToBuf(paRCRPublicKey.challenge);
|
|
317
|
+
const paAllowCredentials = paRCRPublicKey.allowCredentials;
|
|
318
|
+
|
|
319
|
+
if(!paAllowCredentials ||
|
|
320
|
+
!paAllowCredentials[0]?.id) throw new Error(`[PK+ RelyingParty] :: pkpAuthenticationStart :: Platform Authority Challenge's allowCredentials and/or id was not found!`);
|
|
321
|
+
|
|
322
|
+
paAllowCredentials[0].id = b64urlToBuf(paAllowCredentials[0].id);
|
|
323
|
+
|
|
324
|
+
return paRCRPublicKey;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* @name pkpAuthenticationFinish
|
|
329
|
+
* @description Completes the authentication process for a passkey using the Relying Party.
|
|
330
|
+
* @param {Object} paCredential The Platform Authenticator Credential object.
|
|
331
|
+
* @returns {Promise<Object>} Resolves with the authentication result.
|
|
332
|
+
* @throws {Error} If the Platform Authenticator Credential is not provided.
|
|
333
|
+
* @throws {Error} If the Platform Authenticator response is invalid.
|
|
334
|
+
* @memberOf RelyingPartyBase
|
|
335
|
+
* @async
|
|
336
|
+
*/
|
|
337
|
+
async pkpAuthenticationFinish (credential) {
|
|
338
|
+
if (!credential || typeof credential !== 'object') throw new Error('[PK+ RelyingParty] :: pkpAuthenticationFinish :: No Platform Authenticator Credential provided or is not an object!');
|
|
339
|
+
return this.makePostRequest(`api/passkey/authentication/finish`, { credential })
|
|
340
|
+
.then(body => ({credential, response: body}))
|
|
341
|
+
.catch(err => Promise.reject(new Error('[PK+ RelyingParty] :: pkpAuthenticationFinish :: Unable to complete authentication!', {cause: err})));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* @name getRemoteEnrollmentDetails
|
|
347
|
+
* @description Retrieves remote enrollment details for a given user identifier.
|
|
348
|
+
* @param {string} userIdentifier The user identifier.
|
|
349
|
+
* @param {Object} localDataPayload The local data payload.
|
|
350
|
+
* @returns {Promise<Object>} Resolves with the enrollment details.
|
|
351
|
+
* @throws {Error} Throws an error if the request fails.
|
|
352
|
+
*/
|
|
353
|
+
|
|
354
|
+
async getRemoteEnrollmentDetails (userIdentifier, localDataPayload, bootstrap=false) {
|
|
355
|
+
// console.log('[CORE] :: [RelyingPartyBase] :: getRemoteEnrollmentDetails :: userIdentifier, localDataPayload', userIdentifier, localDataPayload);
|
|
356
|
+
const body = {
|
|
357
|
+
customer_defined_identifier: userIdentifier,
|
|
358
|
+
};
|
|
359
|
+
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
|
|
360
|
+
// if(localDataPayload['zsm_credential_id']) body.zsmCredentialID = localDataPayload['zsm_credential_id']; // [J] Field names updated to match cleanFinalData
|
|
361
|
+
// if(localDataPayload['passkey_credential_id']) body.pkpCredentialID = localDataPayload['passkey_credential_id']; // [J] Field names updated to match cleanFinalData
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const response = await this.makePostRequest('/api/v1/enrollment-details', body);
|
|
365
|
+
// console.log('[CORE] :: [RelyingPartyBase] :: getRemoteEnrollmentDetails :: response', response);
|
|
366
|
+
return response;
|
|
367
|
+
}
|
|
218
368
|
}
|
|
219
369
|
//## END RelyingPartyBase =====================================================================================================================================
|
|
220
370
|
|
package/UMFAClientBase.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import eventCoordinator from './EventCoordinator.js';
|
|
2
2
|
import {zsmPluginManager} from './PluginManager.js';
|
|
3
|
+
import { encodeUserIdentifier } from './encodeUserIdentifier.js';
|
|
4
|
+
|
|
5
|
+
// [J] The lines modified during the concatenation of the PKP Plugin into the Core SDK have been marked with a
|
|
6
|
+
// [J] `[J]` comment, to make it easier to identify during code review and for cross-platform porting purposes.
|
|
7
|
+
// [J] They can all be removed en masse by replacing `/ *\/\/ [J].*/gm` with `""` (and SHOULD be pre-publish).
|
|
3
8
|
|
|
4
9
|
class UMFAClientBase {
|
|
5
10
|
/**
|
|
@@ -10,20 +15,62 @@ class UMFAClientBase {
|
|
|
10
15
|
constructor(config) {
|
|
11
16
|
eventCoordinator.reset();
|
|
12
17
|
eventCoordinator.update('UMFAClientBase');
|
|
13
|
-
this.config
|
|
14
|
-
const WebAuthnClient
|
|
15
|
-
this.zsmAPI
|
|
16
|
-
|
|
17
|
-
this.
|
|
18
|
-
this.
|
|
19
|
-
this.
|
|
20
|
-
|
|
18
|
+
this.config = config;
|
|
19
|
+
const WebAuthnClient = zsmPluginManager.classes('WEBAUTHNCLIENT');
|
|
20
|
+
this.zsmAPI = new WebAuthnClient(config);
|
|
21
|
+
|
|
22
|
+
this.checkEnrollment = this.checkEnrollment.bind(this);
|
|
23
|
+
this.enroll = this.enroll.bind(this);
|
|
24
|
+
this.authenticate = this.authenticate.bind(this);
|
|
25
|
+
// [J] Note that only EXPOSED methods are BOUND in the constructor
|
|
26
|
+
this.checkAllEnrollments = this.checkAllEnrollments.bind(this); // [J] Migrated from PKP Plugin
|
|
27
|
+
this.passkeyPlusEnroll = this.passkeyPlusEnroll.bind(this); // [J] Migrated from PKP Plugin
|
|
28
|
+
this.passkeyPlusAuthenticate = this.passkeyPlusAuthenticate.bind(this); // [J] Migrated from PKP Plugin
|
|
29
|
+
|
|
30
|
+
this.unenroll = this.unenroll.bind(this);
|
|
31
|
+
this.resetDevice = this.resetDevice.bind(this);
|
|
32
|
+
|
|
21
33
|
eventCoordinator.update('UMFAClientBase', 'READY');
|
|
22
34
|
}
|
|
23
35
|
|
|
24
36
|
get userIdentifier() { return this.zsmAPI.userIdentifier; }
|
|
25
37
|
get credentialID() { return this.zsmAPI.credentialID; }
|
|
26
38
|
|
|
39
|
+
/**
|
|
40
|
+
* @name typeChecks
|
|
41
|
+
* @description Performs type and value checks on the userIdentifier and userVerification parameters for the enroll and authenticate methods, and normalizes the userVerification value
|
|
42
|
+
* @param {string} methodName The name of the method calling typeChecks, used for error messages
|
|
43
|
+
* @param {string} userIdentifier The identifier for the user, required to be a non-empty string
|
|
44
|
+
* @param {string|boolean|null} userVerification The level of user verification required for authentication, can be a boolean or a string ("preferred", "prevented", "required", "discouraged")
|
|
45
|
+
* @param {string} addlUVStates Additional acceptable user verification states to be added to the check against the userVerification parameter, passed as a comma-separated string (e.g. "zsm,re-enroll,reenroll")
|
|
46
|
+
* @returns {Object} An object containing the validated and normalized userIdentifier and userVerification values
|
|
47
|
+
* @throws {TypeError} If the userIdentifier is not provided, is empty, or is not a string
|
|
48
|
+
* @throws {TypeError} If the userVerification value is not a boolean or an accepted string value
|
|
49
|
+
* @memberOf UMFAClientBase
|
|
50
|
+
*/
|
|
51
|
+
typeChecks(methodName, userIdentifier, userVerification=null, addlUVStates="") {
|
|
52
|
+
if(!userIdentifier) throw new TypeError(`[UMFAClient] :: ${methodName} :: A userIdentifier String or Object is required! Received: ${userIdentifier} (type: ${typeof userIdentifier}).`);
|
|
53
|
+
let enrollmentDetails;
|
|
54
|
+
if(typeof userIdentifier === 'object') {
|
|
55
|
+
enrollmentDetails = {...userIdentifier};
|
|
56
|
+
userIdentifier = userIdentifier.userIdentifier;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if(userVerification != null){
|
|
60
|
+
if(typeof userVerification === 'boolean') userVerification = (userVerification === true) ? "preferred" : "prevented";
|
|
61
|
+
else if(typeof userVerification === 'string') userVerification = userVerification.toLowerCase();
|
|
62
|
+
else throw new TypeError(`[UMFAClient] :: ${methodName} :: userVerification must be a boolean or string. Received type: ${typeof userVerification}.`);
|
|
63
|
+
|
|
64
|
+
const uvList = ['preferred', 'required', 'discouraged', 'prevented', ...addlUVStates.split(/, ?/g).filter(Boolean)];
|
|
65
|
+
const uvRE = new RegExp(`^(${uvList.join('|')})$`);
|
|
66
|
+
if(!uvRE.test(userVerification)) throw new TypeError(`[UMFAClient] :: ${methodName} :: userVerification must be one of the following Strings: "${uvList.join('", "')}"). Received: ${userVerification}.`);
|
|
67
|
+
}
|
|
68
|
+
userIdentifier = encodeUserIdentifier(userIdentifier); // [J] Encode the userIdentifier before it is used in any downstream operations (no-op stub pending hash algorithm selection)
|
|
69
|
+
let retVal = { userIdentifier, userVerification };
|
|
70
|
+
if(enrollmentDetails) retVal = { enrollmentDetails, ...retVal }; // If the userIdentifier was passed in as part of an enrollmentDetails object, we want to preserve the other fields on that object and pass them downstream with the updated userIdentifier value
|
|
71
|
+
return retVal;
|
|
72
|
+
}
|
|
73
|
+
|
|
27
74
|
/**
|
|
28
75
|
* @name checkIdentity
|
|
29
76
|
* @description Pass-through method to call WebAuthnClient's checkIdentity
|
|
@@ -32,79 +79,252 @@ class UMFAClientBase {
|
|
|
32
79
|
* @returns {Promise<Object>} Returns the results of zsmAPI.checkIdentity
|
|
33
80
|
* @memberOf FIDO2ClientBase, WebAuthnClient
|
|
34
81
|
*/
|
|
35
|
-
async checkIdentity (userIdentifier, primeEnroll) { return this.zsmAPI.checkIdentity(userIdentifier, primeEnroll); }
|
|
82
|
+
async checkIdentity (userIdentifier, primeEnroll) { return this.zsmAPI.checkIdentity(encodeUserIdentifier(userIdentifier), primeEnroll); }
|
|
36
83
|
|
|
37
84
|
/**
|
|
38
85
|
* @name checkEnrollment
|
|
39
86
|
* @description Checks if a ZSM credential is enrolled on the device for the specified user
|
|
40
87
|
* @param {string} userIdentifier The identifier for the user
|
|
41
|
-
* @param {string} targetData The type of data to retrieve ('id' for credential ID, 'all' for full details)
|
|
42
88
|
* @returns {Promise<Object>|boolean} Resolves with the enrollment details or false if not enrolled, or an error if checking fails
|
|
43
89
|
* @throws {Error} If the userIdentifier is not provided or is empty
|
|
44
90
|
* @throws {Error} If an error occurs while checking enrollment
|
|
45
91
|
* @memberOf UMFAClientBase
|
|
46
92
|
*/
|
|
47
|
-
async checkEnrollment(userIdentifier,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return enrollmentStatus;
|
|
54
|
-
} catch(e) {
|
|
55
|
-
return new Error(e.message || e || '[UMFAClient] :: checkEnrollment :: An error occurred while attempting to check enrollment.');
|
|
56
|
-
}
|
|
93
|
+
async checkEnrollment(userIdentifier) { // [J] Removing now-useless second parameter, as the checkAllEnrollments method returns all details; checkEnrollment returns ONLY the credential ID
|
|
94
|
+
({ userIdentifier } = this.typeChecks('checkEnrollment', userIdentifier));
|
|
95
|
+
const enrollmentStatus = await this.zsmAPI.webauthnRetrieve(userIdentifier);
|
|
96
|
+
|
|
97
|
+
return enrollmentStatus;
|
|
57
98
|
}
|
|
58
99
|
|
|
100
|
+
/**
|
|
101
|
+
* @name checkAllEnrollments
|
|
102
|
+
* @description Pass-through method to call WebAuthnClient's webauthnRetrieveAll
|
|
103
|
+
* @param {string} userIdentifier The identifier for the user
|
|
104
|
+
* @returns {Promise<Object>} Returns the full enrollment details for the user
|
|
105
|
+
* @memberOf UMFAClientBase
|
|
106
|
+
*/
|
|
107
|
+
async checkAllEnrollments(userIdentifier, forceRemoteCheck=false) { return this.zsmAPI.webauthnRetrieveAll(encodeUserIdentifier(userIdentifier), forceRemoteCheck); } // [J] Migrated from PKP Plugin
|
|
108
|
+
|
|
59
109
|
/**
|
|
60
110
|
* @name enroll
|
|
61
111
|
* @description Enrolls a user by creating a ZSM credential on the device
|
|
62
112
|
* @param {string} userIdentifier The identifier for the user
|
|
113
|
+
* @param {string} userVerification The level of user verification required for authentication (true/"preferred", false/"prevented", "required", "discouraged", "zsm", or "re-enroll"/"reenroll")
|
|
63
114
|
* @returns {Promise<Object>|boolean} Resolves with the created credential, false if user is already enrolled, or an error if enrollment fails
|
|
64
115
|
* @throws {Error} If the userIdentifier is not provided or is empty
|
|
65
116
|
* @throws {Error} If an error occurs during enrollment
|
|
66
117
|
* @throws {Error} If the object returned from the ZSM API is not valid (no credential)
|
|
67
118
|
* @memberOf UMFAClientBase
|
|
68
119
|
*/
|
|
69
|
-
async enroll(userIdentifier) {
|
|
120
|
+
async enroll(userIdentifier, userVerification="prevented") { // [J] This method has been merged with the PKP Plugin's enroll method
|
|
121
|
+
({ userIdentifier, userVerification } = this.typeChecks('enroll', userIdentifier, userVerification, 'zsm,re-enroll,reenroll'));
|
|
122
|
+
const isPasskeyReenrollment = /^re-?enroll$/.test(userVerification); // Is this a forced PKP re-enrollment?
|
|
123
|
+
const attemptPasskeyPlusEnroll = isPasskeyReenrollment || /^(preferred|required)$/.test(userVerification); // [J] Does the userVerification value indicate that Passkeys+ enrollment should be attempted alongside ZSM enrollment?
|
|
124
|
+
const continueOnPasskeyFailure = (userVerification === 'preferred');
|
|
70
125
|
|
|
126
|
+
let pkpEnrollmentResult, enrollmentDetails;
|
|
127
|
+
if(attemptPasskeyPlusEnroll) {
|
|
128
|
+
enrollmentDetails = await this.checkAllEnrollments(userIdentifier);
|
|
129
|
+
try {
|
|
130
|
+
if(!enrollmentDetails.passkeySupported) { // If Passkeys+ is not supported, but required, throw an error immediately
|
|
131
|
+
if(!continueOnPasskeyFailure) // ...and if the userVerification is "required", fail the entire enrollment operation...
|
|
132
|
+
return new Error(`[UMFAClient] :: enroll :: Passkeys+ is required for enrollment (userVerification set to "required"), but this device does not support Passkeys+. User ${userIdentifier} cannot be enrolled. Please use a Passkeys+-compatible device to enroll.`);
|
|
133
|
+
else
|
|
134
|
+
console.warn(`[UMFAClient] :: enroll :: Passkeys+ enrollment has been requested, but this device does not support Passkeys+. User ${userIdentifier} will be enrolled with ZSM-only credentials. Please use a Passkeys+-compatible device to enroll with Passkeys+ in the future.`);
|
|
135
|
+
} else if (isPasskeyReenrollment) { // Current state doesn't matter. User has specified a new passkey is to be added...
|
|
136
|
+
if (!enrollmentDetails.zsmCredentialID && !enrollmentDetails.hasLocalPasskey) // ... so, unless the user doesn't have credentials AT ALL...
|
|
137
|
+
return new Error(`[UMFAClient] :: enroll :: Cannot RE-enroll user's passkey; user ${userIdentifier} is not currently enrolled with either ZSM or Passkeys+ credentials. To enroll the user, call enroll with a userVerification value of "preferred", "required", or "discouraged".`);
|
|
138
|
+
pkpEnrollmentResult = await this.passkeyPlusEnroll(enrollmentDetails, 'required'); // ... we trigger the Passkeys+ enrollment flow. Failure mode behavior is static: either passkey works or boot them entirely.
|
|
71
139
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|| userIdentifier === '') throw new Error(`[UMFAClient] :: enroll :: A userIdentifier String is required! Received: ${userIdentifier}.`);
|
|
75
|
-
const resultOfCheck = await this.zsmAPI.checkIdentity(userIdentifier, true);
|
|
140
|
+
} else if (enrollmentDetails.zsmCredentialID && enrollmentDetails.hasLocalPasskey) { // ZSM: ✅ | PKP: ✅ | Remote PKP: ✅ - User already fully-registered; fail the operation.
|
|
141
|
+
return new Error(`[UMFAClient] :: enroll :: User ${userIdentifier} is already enrolled with both ZSM and Passkeys+ credentials. To authenticate one or both credential, call the authenticate method.`);
|
|
76
142
|
|
|
77
|
-
|
|
143
|
+
} else if ((enrollmentDetails instanceof Error || enrollmentDetails === false) ||
|
|
144
|
+
(!enrollmentDetails.zsmCredentialID && !enrollmentDetails.hasLocalPasskey)) { // ZSM: ❌ | PKP: ❌ | Remote PKP: ❌ - Dual enrollment for both ZSM and PKP...
|
|
145
|
+
pkpEnrollmentResult = await this.passkeyPlusEnroll(enrollmentDetails, userVerification); // ...so we attempt Passkeys+ enrollment. Failure mode behavior is dictated by userVerification
|
|
78
146
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
147
|
+
} else if (enrollmentDetails.zsmCredentialID && !enrollmentDetails.hasLocalPasskey) { // ZSM: ✅ | PKP: ❌ | Remote PKP: ❌ - User has ZSM only...
|
|
148
|
+
pkpEnrollmentResult = await this.addPasskeyToZSMCredential(enrollmentDetails, userVerification); // ...so we attempt to add Passkeys+ to existing ZSM credential. Failure mode behavior is dictated by userVerification
|
|
149
|
+
|
|
150
|
+
} else if (!enrollmentDetails.zsmCredentialID && enrollmentDetails.pkpCredential) { // ZSM: ❌ | PKP: ✅ | Remote PKP: ✅ - User ONLY has a (remote) passkey (which the checkAllEnrollments bootstrapped down to local)...
|
|
151
|
+
pkpEnrollmentResult = await this.addZSMToPasskeyCredential(enrollmentDetails, userVerification); // ...so we attempt to add ZSM credential to existing Passkeys+ credential. Failure mode behavior is dictated by userVerification
|
|
152
|
+
}
|
|
153
|
+
return (pkpEnrollmentResult === undefined)
|
|
154
|
+
? new Error(`[UMFAClient] :: enroll :: Unexpected PKP flow: PKP enrollment attempted but no result was returned for user ${userIdentifier}.`)
|
|
155
|
+
: pkpEnrollmentResult;
|
|
156
|
+
//TODO: Determine the cause of the passkey failure and tailor the below error message accordingly (currently only handles NotAllowedError: user cancellation/timeout/failure).
|
|
157
|
+
} catch (pkpError) { // If the passkey portion of the operation fails (for ANY reason)...
|
|
158
|
+
console.error('ENROLL FLOW :: PASSKEY OPERATION FAILED ::', pkpError);
|
|
159
|
+
if (!continueOnPasskeyFailure) // ...and the userVerification is "required", return an error to fail the entire enrollment operation...
|
|
160
|
+
return new Error(`[UMFAClient] :: enroll :: The passkey portion of the Passkeys+ enrollment failed for user ${userIdentifier} with error: ${pkpError.message || pkpError}. Since userVerification is set to "${userVerification}", the entire enrollment process has failed. You may: retry, downgrade the userVerification mandate to "preferred" or "discouraged" to allow for ZSM-only enrollment.`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// The code below handles ZSM-only enrollments, as well as providing the "fallback-to-ZSM-only" behavior for acceptable Passkeys+ failures (e.g. userVerification === "preferred")
|
|
165
|
+
let localZSMCredentialID;
|
|
166
|
+
if(enrollmentDetails?.zsmCredentialID) localZSMCredentialID = enrollmentDetails?.zsmCredentialID // No sense in hitting the ZSM credential check if we already have the enrollment details from the pkp section, above...
|
|
167
|
+
else localZSMCredentialID = await this.checkEnrollment(userIdentifier); // ...but if we didn't, let's check the local-only operation for the credential ID
|
|
85
168
|
|
|
169
|
+
if(typeof localZSMCredentialID === 'string') { // If the user is ALREADY enrolled with a ZSM credential, fail the operation
|
|
170
|
+
return new Error(`[UMFAClient] :: enroll :: User ${userIdentifier} is already enrolled with a ZSM credential. To authenticate it, call the authenticate method.`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
await this.zsmAPI.checkIdentity(userIdentifier, true); // [J] This checkIdentity call "primes" the enrollment on the server & sets credentialID/userIdentifier on the zsmAPI instance
|
|
174
|
+
const createResult = await this.zsmAPI.webauthnCreate(userIdentifier); // Perform the vanilla ZSM enrollment.
|
|
175
|
+
if(createResult instanceof Error) // Should the operation fail...
|
|
176
|
+
return new Error(`[UMFAClient] :: enroll :: Unable to enroll ${userIdentifier}!\nDetails:\n${createResult?.message || createResult}`, {cause: createResult?.stack});
|
|
177
|
+
return createResult; // ...otherwise, all operations either succeeded or failed acceptably, so we return the result of the ZSM enrollment
|
|
178
|
+
}
|
|
179
|
+
|
|
86
180
|
/**
|
|
87
181
|
* @name authenticate
|
|
88
182
|
* @description Authenticates a user by retrieving their ZSM credential
|
|
89
183
|
* @param {string} userIdentifier The identifier for the user
|
|
184
|
+
* @param {string} userVerification The level of user verification required for authentication (true/"preferred", false/"prevented", "required", "discouraged", "zsm")
|
|
90
185
|
* @returns {Promise<Object>} Resolves with the authentication result or an error if authentication fails
|
|
91
186
|
* @throws {Error} If the userIdentifier is not provided or is empty
|
|
92
187
|
* @throws {Error} If an error occurs during authentication
|
|
93
188
|
* @memberOf UMFAClientBase
|
|
94
189
|
*/
|
|
95
|
-
async authenticate(userIdentifier) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
190
|
+
async authenticate (userIdentifier, userVerification="prevented") { // [J] This method has been merged with the PKP Plugin's authenticate method
|
|
191
|
+
({ userIdentifier, userVerification } = this.typeChecks('authenticate', userIdentifier, userVerification, 'zsm'));
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
const attemptPasskeyPlusAuth = /^(preferred|required)$/.test(userVerification); // [J] Does the userVerification value indicate that Passkeys+ enrollment should be attempted alongside ZSM enrollment?
|
|
195
|
+
const continueOnPasskeyFailure = (userVerification === 'preferred');
|
|
196
|
+
|
|
197
|
+
// Passkeys+ Operations
|
|
198
|
+
let pkpAuthResult, enrollmentDetails;
|
|
199
|
+
if(attemptPasskeyPlusAuth) {
|
|
200
|
+
enrollmentDetails = await this.checkAllEnrollments(userIdentifier);
|
|
201
|
+
|
|
202
|
+
if(!enrollmentDetails.zsmCredentialID) { // [J] Field names updated to match cleanFinalData
|
|
203
|
+
|
|
204
|
+
return new Error(`[UMFAClient] :: authenticate :: User ${userIdentifier} must be enrolled with a ZSM credential before authenticating. Please enroll the user first.`);
|
|
205
|
+
} else if (!enrollmentDetails.passkeySupported) {
|
|
206
|
+
|
|
207
|
+
return new Error(`[UMFAClient] :: authenticate :: User ${userIdentifier} is not eligible for Passkeys+ authentication; their device does not support passkey authentication. To authenticate, call the authenticate method with userVerification set to "preferred", "discouraged", or "prevented"/"zsm". To enroll with Passkeys+, call enroll with userVerification set to "preferred" or "required" on a Passkeys+-compatible device.`);
|
|
208
|
+
} else if(!enrollmentDetails.hasLocalPasskey) {
|
|
209
|
+
|
|
210
|
+
return new Error(`[UMFAClient] :: authenticate :: User ${userIdentifier} must be enrolled with both ZSM and Passkeys+ credentials to authenticate using Passkeys+ (userVerification set to a non-prevented value).`);
|
|
211
|
+
} else if(attemptPasskeyPlusAuth) {
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
pkpAuthResult = await this.passkeyPlusAuthenticate(enrollmentDetails, userVerification);
|
|
215
|
+
|
|
216
|
+
} catch (pkpError) {
|
|
217
|
+
if (!continueOnPasskeyFailure) return new Error(`[UMFAClient] :: authenticate :: The passkey portion of the Passkeys+ authentication failed for user ${userIdentifier} with error: ${pkpError.message || pkpError}. Since userVerification is set to "${userVerification}", the entire authentication process has failed. You may: retry or downgrade the userVerification mandate to "preferred" or "discouraged" to allow for ZSM-only authentication.`);
|
|
218
|
+
}
|
|
219
|
+
return (pkpAuthResult === undefined)
|
|
220
|
+
? new Error(`[UMFAClient] :: authenticate :: Unexpected PKP flow: PKP authentication attempted but no result was returned for user ${userIdentifier}.`)
|
|
221
|
+
: pkpAuthResult;
|
|
222
|
+
}
|
|
105
223
|
}
|
|
224
|
+
|
|
225
|
+
// The code below handles ZSM-only enrollments, as well as providing the "fallback-to-ZSM-only" behavior for acceptable Passkeys+ failures (e.g. userVerification === "preferred")
|
|
226
|
+
let localZSMCredentialID;
|
|
227
|
+
if(enrollmentDetails?.zsmCredentialID) localZSMCredentialID = enrollmentDetails?.zsmCredentialID // No sense in hitting the ZSM credential check if we already have the enrollment details from the pkp section, above...
|
|
228
|
+
else localZSMCredentialID = await this.checkEnrollment(userIdentifier); // ...but if we didn't, let's check the local-only operation for the credential ID
|
|
229
|
+
|
|
230
|
+
const userIsEnrolled = typeof localZSMCredentialID === 'string';
|
|
231
|
+
if(userIsEnrolled === false) return new Error(`[UMFAClient] :: authenticate :: ${userIdentifier} is not enrolled. To authenticate the user, please enroll them first by calling the enroll method.`);
|
|
232
|
+
|
|
233
|
+
const authCredential = await this.zsmAPI.webauthnPartialGet(userIdentifier);
|
|
234
|
+
return authCredential?.credential??authCredential;
|
|
106
235
|
}
|
|
107
236
|
|
|
237
|
+
async addPasskey(userIdentifier, userVerification="required") {
|
|
238
|
+
let enrollmentDetails;
|
|
239
|
+
({ userIdentifier, userVerification, enrollmentDetails } = this.typeChecks('addPasskey', userIdentifier, userVerification, 're-enroll,reenroll'));
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
let passkeyAddResult;
|
|
243
|
+
if(!enrollmentDetails) passkeyAddResult = await this.addPasskeyToZSMCredential((enrollmentDetails || userIdentifier), userVerification);
|
|
244
|
+
else passkeyAddResult = await this.zsmAPI.pkpCreate((enrollmentDetails || userIdentifier), userVerification);
|
|
245
|
+
return passkeyAddResult;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// PKP Methods ============================================================================================================
|
|
249
|
+
// [J] These were migrated from PKP Plugin with minimal adjustments to fit into the UMFAClientBase class structure and flow
|
|
250
|
+
/**
|
|
251
|
+
* @name passkeyPlusEnroll
|
|
252
|
+
* @description Creates a Passkeys+ credential for the user, if the user is not already enrolled, then creates a ZSM credential
|
|
253
|
+
* @param {string} userIdentifier The identifier for the user
|
|
254
|
+
* @param {string} userVerification Accepts "required", "preferred", "discouraged", to be passed into the Passkey ceremony
|
|
255
|
+
* @returns {Promise<Object>} The results of the call to WebAuthnClient's pkpCreate method or an Error
|
|
256
|
+
* @throws {Error} If the userIdentifier is not provided or is empty
|
|
257
|
+
* @throws {Error} If an error occurs during Passkeys+ enrollment
|
|
258
|
+
* @memberOf UMFAClientBase
|
|
259
|
+
*/
|
|
260
|
+
async passkeyPlusEnroll (userIdentifier, userVerification="preferred") {
|
|
261
|
+
|
|
262
|
+
({ userIdentifier, userVerification } = this.typeChecks('passkeyPlusEnroll', userIdentifier, userVerification, 're-enroll,reenroll'));
|
|
263
|
+
|
|
264
|
+
const pkpCreateResult = await this.zsmAPI.pkpCreate(userIdentifier, userVerification);
|
|
265
|
+
|
|
266
|
+
return pkpCreateResult;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* @name passkeyPlusAuthenticate
|
|
271
|
+
* @description Authenticates a user using Passkeys+ credentials
|
|
272
|
+
* @param {string} userIdentifier The identifier for the user
|
|
273
|
+
* @param {string} userVerification Accepts "required", "preferred", "discouraged", to be passed into the Passkey ceremony
|
|
274
|
+
* @returns {Promise<Object>} The results of the call to WebAuthnClient's pkpAuthenticate method or an Error
|
|
275
|
+
* @throws {Error} If the userIdentifier is not provided or is empty
|
|
276
|
+
* @throws {Error} If an error occurs during Passkeys+ authentication
|
|
277
|
+
* @memberOf UMFAClientBase
|
|
278
|
+
*/
|
|
279
|
+
async passkeyPlusAuthenticate (userIdentifier, userVerification="preferred") {
|
|
280
|
+
|
|
281
|
+
let enrollmentDetails = null;
|
|
282
|
+
({ userIdentifier, userVerification, enrollmentDetails } = this.typeChecks('passkeyPlusAuthenticate', userIdentifier, userVerification, 're-enroll,reenroll'));
|
|
283
|
+
if(!enrollmentDetails) enrollmentDetails = await this.checkAllEnrollments(userIdentifier, true);
|
|
284
|
+
|
|
285
|
+
// console.log('userIdentifier, userVerification, enrollmentDetails :', userIdentifier, userVerification, enrollmentDetails);
|
|
286
|
+
|
|
287
|
+
// const enrollmentDetails = await this.checkAllEnrollments(userIdentifier);
|
|
288
|
+
const pkpAuthResult = await this.zsmAPI.pkpAuthenticate(enrollmentDetails, userVerification, !enrollmentDetails.zsmCredentialID); // [J] Field name updated to match cleanFinalData
|
|
289
|
+
return pkpAuthResult;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* @name addZSMToPasskeyCredential
|
|
294
|
+
* @description Adds a ZSM credential for a user who already has Passkeys+ credentials registered on another device.
|
|
295
|
+
* @param {string} userIdentifier The identifier for the user.
|
|
296
|
+
* @param {string} userVerification Accepts "required", "preferred", "discouraged", to be passed into the Passkey ceremony.
|
|
297
|
+
* @returns {Promise<Object>} Completed ZSM + Passkeys+ authentication result.
|
|
298
|
+
* @throws {Error} If no Passkeys+ credentials are found for the user on another device.
|
|
299
|
+
* @memberOf UMFAClientBase
|
|
300
|
+
*/
|
|
301
|
+
async addZSMToPasskeyCredential (userIdentifier, userVerification) {
|
|
302
|
+
({ userIdentifier } = this.typeChecks('addZSMToPasskeyCredential', userIdentifier));
|
|
303
|
+
|
|
304
|
+
const passkeyPreauth = await this.passkeyPlusAuthenticate(userIdentifier);
|
|
305
|
+
return passkeyPreauth;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* @name addPasskeyToZSMCredential
|
|
310
|
+
* @description Adds a Passkeys+ credential for a user who already has a local ZSM credential.
|
|
311
|
+
* @param {string} userIdentifier The identifier for the user.
|
|
312
|
+
* @param {string} userVerification Accepts "required", "preferred", "discouraged", to be passed into the Passkey ceremony.
|
|
313
|
+
* @returns {Promise<Object>} Completed ZSM + Passkeys+ authentication result.
|
|
314
|
+
* @throws {Error} If no ZSM credentials are found for the user on another device.
|
|
315
|
+
* @memberOf UMFAClientBase
|
|
316
|
+
*/
|
|
317
|
+
async addPasskeyToZSMCredential (userIdentifier, userVerification="preferred") {
|
|
318
|
+
|
|
319
|
+
let enrollmentDetails = null;
|
|
320
|
+
({ userIdentifier, userVerification, enrollmentDetails } = this.typeChecks('addPasskeyToZSMCredential', userIdentifier, userVerification, 're-enroll,reenroll'));
|
|
321
|
+
|
|
322
|
+
const pkpAddPasskeyResult = await this.zsmAPI.pkpAddPasskeyCredential((enrollmentDetails || userIdentifier), userVerification);
|
|
323
|
+
|
|
324
|
+
return pkpAddPasskeyResult;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Delete/Decouple Methods ================================================================================================
|
|
108
328
|
/**
|
|
109
329
|
* @name unenroll
|
|
110
330
|
* @description Unenrolls a user by removing their ZSM credential from the device
|
|
@@ -115,15 +335,13 @@ class UMFAClientBase {
|
|
|
115
335
|
* @memberOf UMFAClientBase
|
|
116
336
|
*/
|
|
117
337
|
async unenroll(userIdentifier) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
return new Error(e.message || e || '[UMFAClient] :: unenroll :: An error occurred during unenrollment.');
|
|
126
|
-
}
|
|
338
|
+
({ userIdentifier } = this.typeChecks('unenroll', userIdentifier));
|
|
339
|
+
|
|
340
|
+
let userIsEnrolled = await this.checkEnrollment(userIdentifier);
|
|
341
|
+
if(userIsEnrolled === false) return false;
|
|
342
|
+
let unenrollResult = await this.zsmAPI.unbindFromDevice(userIdentifier);
|
|
343
|
+
if(unenrollResult instanceof Error) throw(`Unable to unenroll ${userIdentifier}'s identity from this device.\nDetails:\n${unenrollResult}`);
|
|
344
|
+
return unenrollResult === true;
|
|
127
345
|
}
|
|
128
346
|
|
|
129
347
|
/**
|