@ideem/zsm-client-sdk 2.8.1 → 2.9.1

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/UMFAClientBase.js CHANGED
@@ -1,356 +1 @@
1
- import eventCoordinator from './EventCoordinator.js';
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).
8
-
9
- class UMFAClientBase {
10
- /**
11
- * Base class for UMFAClient, providing methods for user enrollment and authentication.
12
- * @constructor
13
- * @param {Object} config - The configuration for ZSM initialization (ref: ./zsm_app_config.json)
14
- */
15
- constructor(config) {
16
- eventCoordinator.reset();
17
- eventCoordinator.update('UMFAClientBase');
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
-
33
- eventCoordinator.update('UMFAClientBase', 'READY');
34
- }
35
-
36
- get userIdentifier() { return this.zsmAPI.userIdentifier; }
37
- get credentialID() { return this.zsmAPI.credentialID; }
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
-
74
- /**
75
- * @name checkIdentity
76
- * @description Pass-through method to call WebAuthnClient's checkIdentity
77
- * @param {string} userIdentifier The identifier for the user
78
- * @param {boolean} primeEnroll Whether to create a new identity if it doesn't exist
79
- * @returns {Promise<Object>} Returns the results of zsmAPI.checkIdentity
80
- * @memberOf FIDO2ClientBase, WebAuthnClient
81
- */
82
- async checkIdentity (userIdentifier, primeEnroll) { return this.zsmAPI.checkIdentity(encodeUserIdentifier(userIdentifier), primeEnroll); }
83
-
84
- /**
85
- * @name checkEnrollment
86
- * @description Checks if a ZSM credential is enrolled on the device for the specified user
87
- * @param {string} userIdentifier The identifier for the user
88
- * @returns {Promise<Object>|boolean} Resolves with the enrollment details or false if not enrolled, or an error if checking fails
89
- * @throws {Error} If the userIdentifier is not provided or is empty
90
- * @throws {Error} If an error occurs while checking enrollment
91
- * @memberOf UMFAClientBase
92
- */
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;
98
- }
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
-
109
- /**
110
- * @name enroll
111
- * @description Enrolls a user by creating a ZSM credential on the device
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")
114
- * @returns {Promise<Object>|boolean} Resolves with the created credential, false if user is already enrolled, or an error if enrollment fails
115
- * @throws {Error} If the userIdentifier is not provided or is empty
116
- * @throws {Error} If an error occurs during enrollment
117
- * @throws {Error} If the object returned from the ZSM API is not valid (no credential)
118
- * @memberOf UMFAClientBase
119
- */
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');
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.
139
-
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.`);
142
-
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
146
-
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
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
-
180
- /**
181
- * @name authenticate
182
- * @description Authenticates a user by retrieving their ZSM credential
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")
185
- * @returns {Promise<Object>} Resolves with the authentication result or an error if authentication fails
186
- * @throws {Error} If the userIdentifier is not provided or is empty
187
- * @throws {Error} If an error occurs during authentication
188
- * @memberOf UMFAClientBase
189
- */
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
- }
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;
235
- }
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 ================================================================================================
328
- /**
329
- * @name unenroll
330
- * @description Unenrolls a user by removing their ZSM credential from the device
331
- * @param {string} userIdentifier The identifier for the user
332
- * @returns {Promise<boolean>} Resolves with true if unenrollment was successful, false if the user was not enrolled, or an error if unenrollment fails
333
- * @throws {Error} If the userIdentifier is not provided or is empty
334
- * @throws {Error} If an error occurs during unenrollment
335
- * @memberOf UMFAClientBase
336
- */
337
- async unenroll(userIdentifier) {
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;
345
- }
346
-
347
- /**
348
- * @name resetDevice
349
- * @description Pass-through method to call WebAuthnClient's webauthnReset
350
- * @returns {Promise<void>} No results are returned
351
- * @memberOf UMFAClientBase, WebAuthnClient
352
- */
353
- async resetDevice() { this.zsmAPI.webauthnReset(); }
354
- }
355
-
356
- export default UMFAClientBase;
1
+ import e from"./EventCoordinator.js";import{zsmPluginManager as t}from"./PluginManager.js";import{encodeUserIdentifier as r}from"./encodeUserIdentifier.js";export default class{constructor(r){e.reset(),e.update("UMFAClientBase"),this.config=r;const s=t.classes("WEBAUTHNCLIENT");this.zsmAPI=new s(r),this.checkEnrollment=this.checkEnrollment.bind(this),this.enroll=this.enroll.bind(this),this.authenticate=this.authenticate.bind(this),this.checkAllEnrollments=this.checkAllEnrollments.bind(this),this.passkeyPlusEnroll=this.passkeyPlusEnroll.bind(this),this.passkeyPlusAuthenticate=this.passkeyPlusAuthenticate.bind(this),this.unenroll=this.unenroll.bind(this),this.resetDevice=this.resetDevice.bind(this),e.update("UMFAClientBase","READY")}get userIdentifier(){return this.zsmAPI.userIdentifier}get credentialID(){return this.zsmAPI.credentialID}typeChecks(e,t,s=null,i=""){if(!t)throw new TypeError(`[UMFAClient] :: ${e} :: A userIdentifier String or Object is required! Received: ${t} (type: ${typeof t}).`);let n;if("object"==typeof t&&(n={...t},t=t.userIdentifier),null!=s){if("boolean"==typeof s)s=!0===s?"preferred":"prevented";else{if("string"!=typeof s)throw new TypeError(`[UMFAClient] :: ${e} :: userVerification must be a boolean or string. Received type: ${typeof s}.`);s=s.toLowerCase()}const t=["preferred","required","discouraged","prevented",...i.split(/, ?/g).filter(Boolean)];if(!new RegExp(`^(${t.join("|")})$`).test(s))throw new TypeError(`[UMFAClient] :: ${e} :: userVerification must be one of the following Strings: "${t.join('", "')}"). Received: ${s}.`)}let a={userIdentifier:t=r(t),userVerification:s};return n&&(a={enrollmentDetails:n,...a}),a}async allowBrowserObserversToSettle(){"undefined"!=typeof window&&("function"==typeof requestAnimationFrame&&await new Promise(e=>requestAnimationFrame(e)),await new Promise(e=>setTimeout(e,0)))}async checkIdentity(e,t){return this.zsmAPI.checkIdentity(r(e),t)}async checkEnrollment(e){({userIdentifier:e}=this.typeChecks("checkEnrollment",e));return await this.zsmAPI.webauthnRetrieve(e)}async checkAllEnrollments(e,t=!1){return this.zsmAPI.webauthnRetrieveAll(r(e),t)}async enroll(e,t="prevented"){await this.allowBrowserObserversToSettle(),({userIdentifier:e,userVerification:t}=this.typeChecks("enroll",e,t,"zsm,re-enroll,reenroll"));const r=/^re-?enroll$/.test(t),s="preferred"===t;let i,n,a;if(r||/^(preferred|required)$/.test(t)){n=await this.checkAllEnrollments(e);try{if(n.passkeySupported)if(r){if(!n.zsmCredentialID&&!n.hasLocalPasskey)return new Error(`[UMFAClient] :: enroll :: Cannot RE-enroll user's passkey; user ${e} 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".`);i=await this.passkeyPlusEnroll(n,"required")}else{if(n.zsmCredentialID&&n.hasLocalPasskey)return new Error(`[UMFAClient] :: enroll :: User ${e} is already enrolled with both ZSM and Passkeys+ credentials. To authenticate one or both credential, call the authenticate method.`);n instanceof Error||!1===n||!n.zsmCredentialID&&!n.hasLocalPasskey?i=await this.passkeyPlusEnroll(n,t):n.zsmCredentialID&&!n.hasLocalPasskey?i=await this.addPasskeyToZSMCredential(n,t):!n.zsmCredentialID&&n.pkpCredential&&(i=await this.addZSMToPasskeyCredential(n,t))}else{if(!s)return new Error(`[UMFAClient] :: enroll :: Passkeys+ is required for enrollment (userVerification set to "required"), but this device does not support Passkeys+. User ${e} cannot be enrolled. Please use a Passkeys+-compatible device to enroll.`);console.warn(`[UMFAClient] :: enroll :: Passkeys+ enrollment has been requested, but this device does not support Passkeys+. User ${e} will be enrolled with ZSM-only credentials. Please use a Passkeys+-compatible device to enroll with Passkeys+ in the future.`)}return void 0===i?new Error(`[UMFAClient] :: enroll :: Unexpected PKP flow: PKP enrollment attempted but no result was returned for user ${e}.`):i}catch(r){if(console.error("ENROLL FLOW :: PASSKEY OPERATION FAILED ::",r),!s)return new Error(`[UMFAClient] :: enroll :: The passkey portion of the Passkeys+ enrollment failed for user ${e} with error: ${r.message||r}. Since userVerification is set to "${t}", the entire enrollment process has failed. You may: retry, downgrade the userVerification mandate to "preferred" or "discouraged" to allow for ZSM-only enrollment.`)}}if(a=n?.zsmCredentialID?n?.zsmCredentialID:await this.checkEnrollment(e),"string"==typeof a)return new Error(`[UMFAClient] :: enroll :: User ${e} is already enrolled with a ZSM credential. To authenticate it, call the authenticate method.`);await this.zsmAPI.checkIdentity(e,!0);const l=await this.zsmAPI.webauthnCreate(e);return l instanceof Error?new Error(`[UMFAClient] :: enroll :: Unable to enroll ${e}!\nDetails:\n${l?.message||l}`,{cause:l?.stack}):l}async authenticate(e,t="prevented"){await this.allowBrowserObserversToSettle(),({userIdentifier:e,userVerification:t}=this.typeChecks("authenticate",e,t,"zsm"));const r=/^(preferred|required)$/.test(t),s="preferred"===t;let i,n,a;if(r){if(n=await this.checkAllEnrollments(e),!n.zsmCredentialID)return new Error(`[UMFAClient] :: authenticate :: User ${e} must be enrolled with a ZSM credential before authenticating. Please enroll the user first.`);if(!n.passkeySupported)return new Error(`[UMFAClient] :: authenticate :: User ${e} 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.`);if(!n.hasLocalPasskey)return new Error(`[UMFAClient] :: authenticate :: User ${e} must be enrolled with both ZSM and Passkeys+ credentials to authenticate using Passkeys+ (userVerification set to a non-prevented value).`);if(r){try{i=await this.passkeyPlusAuthenticate(n,t)}catch(r){if(!s)return new Error(`[UMFAClient] :: authenticate :: The passkey portion of the Passkeys+ authentication failed for user ${e} with error: ${r.message||r}. Since userVerification is set to "${t}", the entire authentication process has failed. You may: retry or downgrade the userVerification mandate to "preferred" or "discouraged" to allow for ZSM-only authentication.`)}return void 0===i?new Error(`[UMFAClient] :: authenticate :: Unexpected PKP flow: PKP authentication attempted but no result was returned for user ${e}.`):i}}a=n?.zsmCredentialID?n?.zsmCredentialID:await this.checkEnrollment(e);if(!1===("string"==typeof a))return new Error(`[UMFAClient] :: authenticate :: ${e} is not enrolled. To authenticate the user, please enroll them first by calling the enroll method.`);const l=await this.zsmAPI.webauthnPartialGet(e);return l?.credential??l}async addPasskey(e,t="required"){let r,s;return await this.allowBrowserObserversToSettle(),({userIdentifier:e,userVerification:t,enrollmentDetails:r}=this.typeChecks("addPasskey",e,t,"re-enroll,reenroll")),s=r?await this.zsmAPI.pkpCreate(r||e,t):await this.addPasskeyToZSMCredential(r||e,t),s}async passkeyPlusEnroll(e,t="preferred"){await this.allowBrowserObserversToSettle(),({userIdentifier:e,userVerification:t}=this.typeChecks("passkeyPlusEnroll",e,t,"re-enroll,reenroll"));return await this.zsmAPI.pkpCreate(e,t)}async passkeyPlusAuthenticate(e,t="preferred"){await this.allowBrowserObserversToSettle();let r=null;({userIdentifier:e,userVerification:t,enrollmentDetails:r}=this.typeChecks("passkeyPlusAuthenticate",e,t,"re-enroll,reenroll")),r||(r=await this.checkAllEnrollments(e,!0));return await this.zsmAPI.pkpAuthenticate(r,t,!r.zsmCredentialID)}async addZSMToPasskeyCredential(e,t){({userIdentifier:e}=this.typeChecks("addZSMToPasskeyCredential",e));return await this.passkeyPlusAuthenticate(e)}async addPasskeyToZSMCredential(e,t="preferred"){let r=null;({userIdentifier:e,userVerification:t,enrollmentDetails:r}=this.typeChecks("addPasskeyToZSMCredential",e,t,"re-enroll,reenroll"));return await this.zsmAPI.pkpAddPasskeyCredential(r||e,t)}async unenroll(e){if(({userIdentifier:e}=this.typeChecks("unenroll",e)),!1===await this.checkEnrollment(e))return!1;let t=await this.zsmAPI.unbindFromDevice(e);if(t instanceof Error)throw`Unable to unenroll ${e}'s identity from this device.\nDetails:\n${t}`;return!0===t}async resetDevice(){this.zsmAPI.webauthnReset()}}
package/Utils.js CHANGED
@@ -1,44 +1 @@
1
- /**
2
- * @name b64urlToBuf
3
- * @description Converts a Base64 URL-encoded string to an ArrayBuffer.
4
- * @param {string} b64url The Base64 URL-encoded string to convert.
5
- * @returns {ArrayBuffer} The converted ArrayBuffer.
6
- * @throws {TypeError} Throws a TypeError if the input is not a valid Base64 URL-encoded string.
7
- * @example b64urlToBuf('c29tZV9iYXNlNjRfdGVzdA') // Converts to ArrayBuffer
8
- * @see https://developer.mozilla.org/en-US/docs/Web/API/Base64/Using_Base64_encoding#base64url, https://developer.mozilla.org/en-US/docs/Web/API/atob, https://developer.mozilla.org/en-US/docs/Web/API/Uint8Array, https://developer.mozilla.org/en-US/docs/Web/API/ArrayBuffer
9
- * @memberof Utils
10
- */
11
- const b64urlToBuf = (b64url) => {
12
- if (typeof b64url !== 'string' || b64url.length === 0)
13
- throw new TypeError(`[Utils] :: b64urlToBuf :: This method expects a base64 encoded string as input. Received: "${b64url}" (of type ${typeof b64url} and length ${b64url.length}).`);
14
-
15
- const padded = (b64url + '====').slice(0, -b64url.length % 4);
16
-
17
- const binary = atob(padded.replace(/-/g, '+').replace(/_/g, '/'));
18
-
19
- const buffer = Uint8Array.from(binary, c => c.charCodeAt(0)).buffer;
20
-
21
- return buffer;
22
- }
23
-
24
- /**
25
- * @name toObject
26
- * @description Recursively converts potentially-nested data structures (Map, Array, Object) to a plain object.
27
- * @ This is in use specifically because of wasm-bindgen's proclivity to OCCASIONALLY return Map objects instead of plain objects.
28
- * @param {any} inp The input data structure to convert.
29
- * @returns {Object} The converted plain object.
30
- * @throws {TypeError} Throws a TypeError if the input is not a valid data structure.
31
- * @example toObject(new Map([['key', 'value']]))
32
- * @memberof Utils
33
- */
34
- const toObject = (inp) => {
35
- if (inp === null || typeof inp !== 'object') return inp; // Primitives & functions pass through
36
-
37
- if (inp instanceof Map) return Object.fromEntries([...inp].map(([k, v]) => [k, toObject(v)])); // Recursively convert Map's nodes
38
-
39
- if (Array.isArray(inp)) return inp.map(toObject); // Recursively convert arrays' nodes
40
-
41
- return Object.fromEntries(Object.entries(inp).map(([k, v]) => [k, toObject(v)])); // Recursively convert plain objects' nodes
42
- };
43
-
44
- export { b64urlToBuf, toObject };
1
+ const e=e=>{if("string"!=typeof e||0===e.length)throw new TypeError(`[Utils] :: b64urlToBuf :: This method expects a base64 encoded string as input. Received: "${e}" (of type ${typeof e} and length ${e.length}).`);const t=(e+"====").slice(0,-e.length%4),r=atob(t.replace(/-/g,"+").replace(/_/g,"/"));return Uint8Array.from(r,e=>e.charCodeAt(0)).buffer},t=e=>null===e||"object"!=typeof e?e:e instanceof Map?Object.fromEntries([...e].map(([e,r])=>[e,t(r)])):Array.isArray(e)?e.map(t):Object.fromEntries(Object.entries(e).map(([e,r])=>[e,t(r)]));export{e as b64urlToBuf,t as toObject};