@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/WebAuthnClientBase.js
CHANGED
|
@@ -3,6 +3,7 @@ import GLOBAL from './GlobalScoping.js';
|
|
|
3
3
|
import { uid2iid, iid4uid } from './IdentityIndexing.js';
|
|
4
4
|
import { toObject } from './Utils.js';
|
|
5
5
|
import {zsmPluginManager} from './PluginManager.js';
|
|
6
|
+
import PasskeysPlusClient from './PasskeysPlusClient.js';
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
eventCoordinator.update('WebAuthnClientBase');
|
|
@@ -27,6 +28,8 @@ class WebAuthnClientBase {
|
|
|
27
28
|
// Helper function to standardize URL format (ensuring it's parsable and ends with a '/')
|
|
28
29
|
const cleanURL = (url) => canParseURL(url) ? new URL(url).origin + '/' : false;
|
|
29
30
|
|
|
31
|
+
this.passkeysPlus = new PasskeysPlusClient();
|
|
32
|
+
|
|
30
33
|
if(config?.region){
|
|
31
34
|
config.authenticator_host_url ||= config.authenticator_host = `https://zsm-authenticator-${config.region}.useideem.com/`;
|
|
32
35
|
config.zsm_host_url ||= config.zsm_host = `https://zsm-${config.region}.useideem.com/`;
|
|
@@ -49,10 +52,10 @@ class WebAuthnClientBase {
|
|
|
49
52
|
delete config.use_origin;
|
|
50
53
|
}
|
|
51
54
|
|
|
52
|
-
this.zsmApi = undefined;
|
|
53
|
-
this.relyingParty = undefined;
|
|
54
|
-
this.status = undefined;
|
|
55
|
-
this.config = undefined;
|
|
55
|
+
this.zsmApi = undefined; // The ZSM API instance (read: WASM-driven Crypto Module)
|
|
56
|
+
this.relyingParty = undefined; // The Relying Party instance (read: ZSM Authenticator API)
|
|
57
|
+
this.status = undefined; // The status of the WebAuthnClientBase
|
|
58
|
+
this.config = undefined; // The configuration object passed into the constructor
|
|
56
59
|
|
|
57
60
|
// If the inputted host_url is not a valid URL, null it for future checks' sake.
|
|
58
61
|
config.host_url = cleanURL(config.host_url) || null;
|
|
@@ -72,8 +75,10 @@ class WebAuthnClientBase {
|
|
|
72
75
|
config.host_url = config.zsm_host_url;
|
|
73
76
|
|
|
74
77
|
this.config = GLOBAL.zsmAppConfig = config;
|
|
78
|
+
|
|
75
79
|
iid4uid(this.config.consumer_id)
|
|
76
80
|
.then(v=>{
|
|
81
|
+
// console.log('(v :', v);
|
|
77
82
|
GLOBAL.mpcConfig = Object.assign({}, config, {consumer_id: v});
|
|
78
83
|
if((GLOBAL.mpcConfig?.use_passkeys??undefined)) {
|
|
79
84
|
GLOBAL.mpcConfig.use_origin = true;
|
|
@@ -98,7 +103,6 @@ class WebAuthnClientBase {
|
|
|
98
103
|
get credentialID() { return this.relyingParty.credentialID; }
|
|
99
104
|
set credentialID(v) { return this.relyingParty.credentialID = v; }
|
|
100
105
|
get isEnrolled() { return !!this.credentialID }
|
|
101
|
-
// get version() { return "ZSM WebAuthn client"; }
|
|
102
106
|
|
|
103
107
|
/**
|
|
104
108
|
* @name getZsmApi
|
|
@@ -142,10 +146,10 @@ class WebAuthnClientBase {
|
|
|
142
146
|
*/
|
|
143
147
|
|
|
144
148
|
async checkIdentity (userIdentifier, primeEnroll=false) {
|
|
145
|
-
if(userIdentifier == null
|
|
146
|
-
|| userIdentifier === '')
|
|
149
|
+
if(userIdentifier == null
|
|
150
|
+
|| userIdentifier === '') throw new Error(`[WebAuthnClient] :: checkIdentity :: A userIdentifier String is required! Received: ${userIdentifier}.`);
|
|
147
151
|
if(userIdentifier && typeof userIdentifier === 'string') this.userIdentifier = userIdentifier;
|
|
148
|
-
|
|
152
|
+
|
|
149
153
|
try {
|
|
150
154
|
const resultOfCheck = await this.relyingParty.checkServerSideIdentity(primeEnroll);
|
|
151
155
|
|
|
@@ -162,6 +166,137 @@ class WebAuthnClientBase {
|
|
|
162
166
|
}
|
|
163
167
|
}
|
|
164
168
|
|
|
169
|
+
/**
|
|
170
|
+
* @name webauthnRetrieve
|
|
171
|
+
* @description Looks up the Identity ID for the specified userIdentifier and retrieves its credential ID (if it exists).
|
|
172
|
+
* @ This is used to make a client-only check to see if a given userIdentifier already has an enrollment on the device.
|
|
173
|
+
* @param {string} userIdentifier The user identifier.
|
|
174
|
+
* @param {boolean} returnExpandedDetails Whether to return the full enrollment details object or just the credential ID.
|
|
175
|
+
* @ - false (default): retrieves the ZSM credential ID, or false if not enrolled
|
|
176
|
+
* @ - true: retrieves a cleanFinalData object containing all relevant enrollment data
|
|
177
|
+
* @param {boolean} forceRemoteCheck If true, forces server consultation even when hasRemotePasskey is false (use before passkey enrollment workflows).
|
|
178
|
+
* @returns {Promise<Boolean:false>} Returns `false` if retrieval fails (no enrollment found).
|
|
179
|
+
* @returns {Promise<String>} Returns the credential ID if found.
|
|
180
|
+
* @returns {Promise<Object>} Resolves with an object containing all relevant credential data.
|
|
181
|
+
* @throws {Error} Throws an error if an uncaught/unhandled error occurs while retrieving the credential.
|
|
182
|
+
* @memberof WebAuthnClientBase
|
|
183
|
+
*/
|
|
184
|
+
async webauthnRetrieve (userIdentifier, returnExpandedDetails=false, forceRemoteCheck=false) { // [J] Merged with __webauthnRetrieve; added forceRemoteCheck for enrollment flows
|
|
185
|
+
// console.log('[CORE] :: [WebAuthnClientBase] :: webauthnRetrieve :: userIdentifier, returnExpandedDetails', userIdentifier, returnExpandedDetails);
|
|
186
|
+
|
|
187
|
+
if(userIdentifier == null || typeof userIdentifier !== 'string'
|
|
188
|
+
|| userIdentifier === '') throw new Error(`[WebAuthnClient] :: webauthnRetrieve :: A userIdentifier String is required! Received: ${userIdentifier} (of type ${typeof userIdentifier}).`);
|
|
189
|
+
|
|
190
|
+
if(userIdentifier && typeof userIdentifier === 'string') this.userIdentifier = userIdentifier;
|
|
191
|
+
this.credentialID = null;
|
|
192
|
+
|
|
193
|
+
this.initializeZsm('webauthnRetrieve');
|
|
194
|
+
|
|
195
|
+
let credentialIdentifier = await iid4uid(userIdentifier); // Obtain identity_id for userIdentifier
|
|
196
|
+
GLOBAL.mpcConfig.consumer_id = credentialIdentifier; // Update mpcConfig with identity_id
|
|
197
|
+
|
|
198
|
+
const response = await this.zsmApi.webauthn_retrieve(userIdentifier); // Perform local retrieval of credential details
|
|
199
|
+
let localData = toObject(response);
|
|
200
|
+
|
|
201
|
+
if (!returnExpandedDetails){ // If we're only looking for the credential ID, we can short-circuit so...
|
|
202
|
+
return (localData?.error || !localData?.result) // ... if there's an error (or no result/result's missing)...
|
|
203
|
+
? false // ... return false (indicating no enrollment)...
|
|
204
|
+
: (this.credentialID = localData.result['zsm_credential_id']??null); // ... otherwise, set the credential ID (if it exists) and return it.
|
|
205
|
+
}else{ // Otherwise (we're looking for expanded details)...
|
|
206
|
+
localData = (localData?.error || !localData?.result) // ... if there's an error (or no result/result's missing)... [J] Fixed: was reassigning const `response`
|
|
207
|
+
? {} // ... we're starting with an empty object (indicating no local knowledge)...
|
|
208
|
+
: localData.result; // ... otherwise, we start with any local knowledge we DO have
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
let passkeySupported = false; // [J] Best-effort detection of platform authenticator support
|
|
212
|
+
try {
|
|
213
|
+
passkeySupported = !!(typeof window !== 'undefined' && window.PublicKeyCredential
|
|
214
|
+
&& navigator?.credentials
|
|
215
|
+
&& await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable());
|
|
216
|
+
} catch { /* unsupported environment */ }
|
|
217
|
+
|
|
218
|
+
this.credentialID = localData['zsm_credential_id']??null;
|
|
219
|
+
|
|
220
|
+
// Server consultation: only needed when local passkey knowledge is insufficient // [J] Replaced hasFailingStatus shotgun check with targeted remote passkey logic
|
|
221
|
+
const shouldCheckServer = forceRemoteCheck
|
|
222
|
+
|| (passkeySupported && !localData?.['has_remote_passkey'] && !localData?.['passkey_credential_id'])
|
|
223
|
+
|| !localData?.['zsm_active']
|
|
224
|
+
|| !localData?.['zsm_active']
|
|
225
|
+
|
|
226
|
+
let completeData = localData;
|
|
227
|
+
if(shouldCheckServer) {
|
|
228
|
+
let remoteData = await this.checkRemoteEnrollmentDetails(userIdentifier, completeData); // Grab the remote enrollment details...
|
|
229
|
+
|
|
230
|
+
if(remoteData && !(remoteData instanceof Error)) { // ... and if we got something back (and it wasn't an error)...
|
|
231
|
+
completeData = Object.assign({}, localData, remoteData); // ...merge it with completeData (giving remote precedence)
|
|
232
|
+
// if(remoteData?.['rcr']) // Persist remote passkey discovery locally to prevent future RTTs
|
|
233
|
+
// this.zsmApi.updateEnrollmentStatus(userIdentifier, "passkey"); // [J] Fire-and-forget; next webauthn_retrieve will return hasRemotePasskey locally
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
const cleanFinalData = { // Finally, clean up the net enrollment details results...
|
|
240
|
+
userIdentifier,
|
|
241
|
+
enrollmentActive : completeData?.['zsm_active'] ?? false,
|
|
242
|
+
passkeySupported, // [J] Device/browser supports platform authenticator (best-effort)
|
|
243
|
+
passkeyEligible : passkeySupported && !!(completeData?.['passkey_credential_id'] || completeData?.['has_remote_passkey']), // [J] Passkeys supported AND a passkey credential exists to use
|
|
244
|
+
hasLocalPasskey : passkeySupported && !!(completeData?.['passkey_credential_id']??false), // [J] Now sourced from WASM response directly
|
|
245
|
+
hasRemotePasskey : !!completeData?.['rcr'] ?? null, // [J] null = unknown; false = confirmed absent; true = confirmed present
|
|
246
|
+
pkpCredentialID : completeData?.['passkey_credential_id'] ?? null, // [J] If we have a local passkey credential ID, use it (this is the most reliable source for passkey presence, as it indicates we've actually used the passkey on this device before); if not, null it (instead of false) to indicate uncertainty
|
|
247
|
+
pkpCredID : completeData?.['passkey_credential_id'] ?? null, // [J] If we have a local passkey credential ID, use it (this is the most reliable source for passkey presence, as it indicates we've actually used the passkey on this device before); if not, null it (instead of false) to indicate uncertainty
|
|
248
|
+
hasZSMCred : !!completeData?.['zsm_credential'] || false,
|
|
249
|
+
zsmCredential : completeData?.['zsm_credential'] ?? null,
|
|
250
|
+
zsmCredentialID : completeData?.['zsm_credential_id'] ?? null,
|
|
251
|
+
zsmCredID : completeData?.['zsm_credential_id'] ?? null
|
|
252
|
+
}
|
|
253
|
+
if(cleanFinalData.hasLocalPasskey === true) cleanFinalData.hasRemotePasskey = true; // [J] Sanity check: local passkey presence implies remote passkey presence
|
|
254
|
+
if(completeData?.['rcr']) {
|
|
255
|
+
cleanFinalData.pkpCredential = completeData['rcr']; // [J] If the server provided a remote credential ID, include it in the cleanFinalData for use in authentication flows (e.g., Passkeys+)
|
|
256
|
+
cleanFinalData.hasLocalPasskey = true;
|
|
257
|
+
if (!cleanFinalData.pkpCredentialID) {
|
|
258
|
+
cleanFinalData.pkpCredentialID = completeData['rcr']?.publicKey?.allowCredentials?.flatMap(c=>c.id)??null;
|
|
259
|
+
if(cleanFinalData.pkpCredentialID.length) cleanFinalData.pkpCredentialID = cleanFinalData.pkpCredentialID[0]; // [J] If the server provided multiple remote credential IDs (which shouldn't happen, but just in case), we'll take the first one as the primary indicator of passkey presence/eligibility
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// console.log('[CORE] :: [WebAuthnClientBase] :: webauthnRetrieve :: cleanFinalData :', cleanFinalData);
|
|
264
|
+
return cleanFinalData;
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* @name webauthnRetrieveAll
|
|
269
|
+
* @description Syntactic sugar for webauthnRetrieve with returnExpandedDetails=true to retrieve all enrollment information.
|
|
270
|
+
* @param {string} userIdentifier The user identifier.
|
|
271
|
+
* @param {boolean} forceRemoteCheck If true, forces server consultation even when hasRemotePasskey is false (use before enrollment workflows).
|
|
272
|
+
* @returns {Promise<Object>} Returns the full enrollment details for the user.
|
|
273
|
+
* @memberof WebAuthnClientBase
|
|
274
|
+
*/
|
|
275
|
+
async webauthnRetrieveAll (userIdentifier, forceRemoteCheck=false) {
|
|
276
|
+
const allEnrollmentDetails = await this.webauthnRetrieve(userIdentifier, true, forceRemoteCheck);
|
|
277
|
+
return allEnrollmentDetails;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* @name checkRemoteEnrollmentDetails
|
|
282
|
+
* @description Checks the server for remote enrollment details when local data has failing status flags.
|
|
283
|
+
* @param {string} userIdentifier The user identifier.
|
|
284
|
+
* @param {Object} localPayload The local enrollment data to send to the server for comparison.
|
|
285
|
+
* @returns {Promise<Object>} Returns remote enrollment details, or false if none found.
|
|
286
|
+
* @throws {Error} Throws if the server returns an error.
|
|
287
|
+
* @memberof WebAuthnClientBase
|
|
288
|
+
*/
|
|
289
|
+
async checkRemoteEnrollmentDetails (userIdentifier, localPayload) {
|
|
290
|
+
try {
|
|
291
|
+
const allEnrollmentDetails = await this.relyingParty.getRemoteEnrollmentDetails(userIdentifier, localPayload);
|
|
292
|
+
if(allEnrollmentDetails instanceof Error || !allEnrollmentDetails || allEnrollmentDetails['identity_id'] == null) return false;
|
|
293
|
+
return allEnrollmentDetails; // [J] Fixed: was missing return
|
|
294
|
+
} catch(err) {
|
|
295
|
+
console.error('Error checking remote enrollment details:', err);
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
165
300
|
/**
|
|
166
301
|
* @name webauthnCreate
|
|
167
302
|
* @description Creates a new WebAuthn credential for the specified userIdentifier.
|
|
@@ -174,8 +309,7 @@ class WebAuthnClientBase {
|
|
|
174
309
|
* @throws {Error} Throws a COUNTED error if unable to verify the credential with the Relying Party Server.
|
|
175
310
|
*/
|
|
176
311
|
async webauthnCreate (userIdentifier) {
|
|
177
|
-
if(userIdentifier
|
|
178
|
-
|| userIdentifier === '') return new Error(`[WebAuthnClient] :: webauthnCreate :: A userIdentifier String is required! Received: ${userIdentifier}.`);
|
|
312
|
+
if(!userIdentifier) throw new Error(`[WebAuthnClient] :: webauthnCreate :: A userIdentifier String is required! Received: ${userIdentifier}.`);
|
|
179
313
|
if(userIdentifier && typeof userIdentifier === 'string') this.userIdentifier = userIdentifier;
|
|
180
314
|
|
|
181
315
|
await this.initializeZsm('webauthnCreate');
|
|
@@ -186,62 +320,26 @@ class WebAuthnClientBase {
|
|
|
186
320
|
else if (!publicKey) throw new Error('[WebAuthnClient] :: webauthnCreate :: Unable to retrieve public key from Relying Party Server!');
|
|
187
321
|
|
|
188
322
|
publicKey.user_id = publicKey.user.id;
|
|
189
|
-
|
|
323
|
+
|
|
190
324
|
let createResponse = await this.zsmApi.webauthn_create(publicKey, undefined, userIdentifier, tracePrimer());
|
|
191
325
|
if(!createResponse) throw new Error('[WebAuthnClient - mpc_round] :: webauthnCreate :: Unable to obtain response from Crypto Server!');
|
|
192
326
|
|
|
193
|
-
|
|
327
|
+
|
|
194
328
|
createResponse = toObject(createResponse);
|
|
195
329
|
GLOBAL.dispatchEvent(new CustomEvent("MPCResponse", {bubbles: true, cancelable: false, detail: createResponse }));
|
|
196
|
-
|
|
330
|
+
|
|
197
331
|
if(!createResponse?.result) throw new Error('[WebAuthnClient - mpc_round] :: webauthnCreate :: Unable to obtain credential from Crypto Server!');
|
|
198
332
|
if(!createResponse?.result?.response) throw new Error('[WebAuthnClient - mpc_round] :: webauthnCreate :: Unable to acquire attestation object!');
|
|
199
|
-
|
|
333
|
+
|
|
200
334
|
const credential = createResponse.result;
|
|
201
335
|
|
|
202
|
-
|
|
336
|
+
|
|
203
337
|
const registrationResult = await this.relyingParty.registrationFinish(credential);
|
|
204
338
|
if(registrationResult instanceof Error) throw new Error(registrationResult);
|
|
205
339
|
else if(registrationResult == false) throw new Error('[WebAuthnClient] :: webauthnCreate :: Unable to verify credential Relying Party Server!');
|
|
206
340
|
|
|
207
|
-
|
|
208
|
-
return registrationResult;
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* @name webauthnCreate
|
|
213
|
-
* @description Creates a new WebAuthn credential for the specified userIdentifier.
|
|
214
|
-
* @param {string} userIdentifier The user identifier.
|
|
215
|
-
* @returns {Promise<Object>} Resolves with the created credential.
|
|
216
|
-
* @throws {Error} Throws an error if unable to start enrollment with the Relying Party Server.
|
|
217
|
-
* @throws {Error} Throws an error if unable to retrieve the public key/IdentityID from the Relying Party Server.
|
|
218
|
-
* @throws {Error} Throws an error if unable to obtain the createResponse/credential from the Crypto Server.
|
|
219
|
-
* @throws {Error} Throws an error if unable to verify the credential with the Relying Party Server.
|
|
220
|
-
* @throws {Error} Throws an error if unable to acquire the identity ID from the Relying Party Server.
|
|
221
|
-
* @throws {Error} Throws an error if the user ID or token is missing from the response body.
|
|
222
|
-
*/
|
|
223
|
-
async webauthnCreateMpcThenFinish (userIdentifier, publicKey, pairing_token) {
|
|
224
|
-
if(userIdentifier == null
|
|
225
|
-
|| userIdentifier === '') return new Error(`[WebAuthnClient] :: webauthnCreateMpcThenFinish :: A userIdentifier String is required! Received: ${userIdentifier}.`);
|
|
226
|
-
if(userIdentifier && typeof userIdentifier === 'string') this.userIdentifier = userIdentifier;
|
|
227
|
-
this.status = 'ENROLLMENT_STARTING';
|
|
228
|
-
await this.initializeZsm('webauthnCreateMpcThenFinish');
|
|
229
|
-
|
|
230
|
-
let createResponse = await this.zsmApi.webauthn_create(publicKey, pairing_token, userIdentifier, tracePrimer());
|
|
231
|
-
if(!createResponse) throw new Error('[WebAuthnClient - mpc_round] :: webauthnCreateMpcThenFinish :: Unable to obtain response from Crypto Server!');
|
|
232
|
-
|
|
233
|
-
createResponse = toObject(createResponse);
|
|
234
|
-
GLOBAL.dispatchEvent(new CustomEvent("MPCResponse", {bubbles: true, cancelable: false, detail: createResponse }));
|
|
235
|
-
|
|
236
|
-
if(!createResponse?.result) throw new Error('[WebAuthnClient - mpc_round] :: webauthnCreateMpcThenFinish :: Unable to obtain credential from Crypto Server!');
|
|
237
|
-
if(!createResponse?.result?.response) throw new Error('[WebAuthnClient - mpc_round] :: webauthnCreateMpcThenFinish :: Unable to acquire attestation object!');
|
|
238
|
-
|
|
239
|
-
const credential = createResponse.result;
|
|
240
|
-
|
|
241
|
-
const registrationResult = await this.relyingParty.registrationFinish(credential);
|
|
242
|
-
if (!registrationResult) throw new Error('[WebAuthnClient] :: webauthnCreateMpcThenFinish :: Unable to verify credential Relying Party Server!');
|
|
243
341
|
|
|
244
|
-
return registrationResult
|
|
342
|
+
return registrationResult;
|
|
245
343
|
};
|
|
246
344
|
|
|
247
345
|
/**
|
|
@@ -257,12 +355,11 @@ class WebAuthnClientBase {
|
|
|
257
355
|
* @throws {Error} Throws an error if the user ID or token is missing from the response body.
|
|
258
356
|
*/
|
|
259
357
|
async webauthnGet (userIdentifier) {
|
|
260
|
-
if(userIdentifier == null
|
|
261
|
-
|| userIdentifier === '') return new Error(`[WebAuthnClient] :: webauthnGet :: A userIdentifier String is required! Received: ${userIdentifier}.`);
|
|
358
|
+
if(userIdentifier == null|| userIdentifier === '') throw new Error(`[WebAuthnClient] :: webauthnGet :: A userIdentifier String is required! Received: ${userIdentifier}.`);
|
|
262
359
|
if(userIdentifier && typeof userIdentifier === 'string') this.userIdentifier = userIdentifier;
|
|
263
360
|
|
|
264
361
|
await this.initializeZsm('webauthnGet');
|
|
265
|
-
const credentialId = await this.webauthnRetrieve(userIdentifier, true);
|
|
362
|
+
const credentialId = await this.webauthnRetrieve(userIdentifier, true); // [J] Now passes expanded details to authenticationStart
|
|
266
363
|
|
|
267
364
|
if (!this.isEnrolled) throw new Error(`[WebAuthnClient] :: webauthnGet :: ${userIdentifier} is not enrolled, lacks a credential, or credential is corrupted!`);
|
|
268
365
|
|
|
@@ -270,14 +367,13 @@ class WebAuthnClientBase {
|
|
|
270
367
|
|
|
271
368
|
if (authStartResponse instanceof Error) throw new Error(authStartResponse);
|
|
272
369
|
else if (!authStartResponse) throw new Error('[WebAuthnClient] :: webauthnGet :: Unable to start authentication with Relying Party Server!');
|
|
273
|
-
|
|
370
|
+
|
|
274
371
|
authStartResponse = toObject(authStartResponse);
|
|
275
372
|
|
|
276
373
|
if(!authStartResponse?.rcr?.publicKey?.challenge) throw new Error('[WebAuthnClient] :: webauthnGet :: Unable to locate public key/challenge inside Relying Party Server response!\nResponse Body: ' + JSON.stringify(authStartResponse));
|
|
277
374
|
const publicKey = authStartResponse.rcr.publicKey;
|
|
278
375
|
|
|
279
376
|
let mpcResponse = await this.zsmApi.webauthn_get(publicKey, undefined, userIdentifier, tracePrimer());
|
|
280
|
-
|
|
281
377
|
|
|
282
378
|
if(!mpcResponse) throw new Error('[WebAuthnClient - mpc_round] :: webauthnGet :: Crypto server did not respond!');
|
|
283
379
|
|
|
@@ -285,62 +381,7 @@ class WebAuthnClientBase {
|
|
|
285
381
|
if(!mpcResponse?.result) throw new Error('[WebAuthnClient - mpc_round] :: webauthnGet :: Response from Crypto Server did not contain a credential!\nResponse Body: ' + JSON.stringify(mpcResponse));
|
|
286
382
|
|
|
287
383
|
GLOBAL.dispatchEvent(new CustomEvent("MPCResponse", {bubbles: true, cancelable: false, detail: mpcResponse }));
|
|
288
|
-
|
|
289
|
-
const credential = mpcResponse.result;
|
|
290
|
-
return credential;
|
|
291
|
-
let authFinishResponse = await this.relyingParty.authenticationFinish(credential);
|
|
292
|
-
|
|
293
|
-
if (authFinishResponse instanceof Error) throw new Error(authFinishResponse);
|
|
294
|
-
else if (!authFinishResponse) throw new Error('[WebAuthnClient] :: webauthnGet :: Unable to verify credential from Relying Party Server!');
|
|
295
|
-
|
|
296
|
-
authFinishResponse = toObject(authFinishResponse)
|
|
297
|
-
if (!authFinishResponse.token) throw new Error('[WebAuthnClient] :: webauthnGet :: Token mismatched or missing from Relying Party Server verification attempt!\nResponse Body: ' + JSON.stringify(authFinishResponse));
|
|
298
|
-
|
|
299
|
-
return {
|
|
300
|
-
signature : credential.response.signature,
|
|
301
|
-
signedChallenge : credential.response.clientDataJSON,
|
|
302
|
-
user_id : authFinishResponse.user_id,
|
|
303
|
-
token : authFinishResponse.token,
|
|
304
|
-
challenge : publicKey.challenge,
|
|
305
|
-
credential
|
|
306
|
-
};
|
|
307
|
-
};
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* @name webauthnRetrieve
|
|
313
|
-
* @description Looks up the Identity ID for the specified userIdentifier and retrieves its credential ID (if it exists).
|
|
314
|
-
* @ This is used to make a client-only check to see if a given userIdentifier already has an enrollment on the device.
|
|
315
|
-
* @param {string} userIdentifier The user identifier.
|
|
316
|
-
* @param {string} targetData The specific data to retrieve. Options are:
|
|
317
|
-
* @ - 'id' or omitted: retrieves the ZSM credential ID (default)
|
|
318
|
-
* @ - 'all': retrieves an object containing all relevant credential data
|
|
319
|
-
* @ - any other string: retrieves the specified field from the response
|
|
320
|
-
* @returns {Promise<Boolean:false>} Returns `false` if retrieval fails (no enrollment found).
|
|
321
|
-
* @returns {Promise<String>} Returns the requested data if found (e.g., ").
|
|
322
|
-
* @returns {Promise<Object>} Resolves with an object containing all relevant credential data.
|
|
323
|
-
* @returns {Promise<Null>} Resolves with null if the requested data is not found.
|
|
324
|
-
* @throws {Error} Throws an error if an uncaught/unhandled error occurs while retrieving the credential.
|
|
325
|
-
*/
|
|
326
|
-
async webauthnRetrieve (userIdentifier) {
|
|
327
|
-
if(userIdentifier == null
|
|
328
|
-
|| userIdentifier === '') return new Error(`[WebAuthnClient] :: webauthnRetrieve :: A userIdentifier String is required! Received: ${userIdentifier}.`);
|
|
329
|
-
if(userIdentifier && typeof userIdentifier === 'string') this.userIdentifier = userIdentifier;
|
|
330
|
-
this.credentialID = null;
|
|
331
|
-
|
|
332
|
-
this.initializeZsm('webauthnRetrieve');
|
|
333
|
-
let credentialIdentifier = await iid4uid(userIdentifier),
|
|
334
|
-
requestedData = {};
|
|
335
|
-
GLOBAL.mpcConfig.consumer_id = credentialIdentifier;
|
|
336
|
-
|
|
337
|
-
let response = await this.zsmApi.webauthn_retrieve(userIdentifier);
|
|
338
|
-
response = toObject(response);
|
|
339
|
-
|
|
340
|
-
if (response?.error || !response?.result) return false;
|
|
341
|
-
|
|
342
|
-
this.credentialID = response?.result['zsm_credential_id']??null;
|
|
343
|
-
return this.credentialID || false;
|
|
384
|
+
return mpcResponse.result;
|
|
344
385
|
};
|
|
345
386
|
|
|
346
387
|
/**
|
|
@@ -355,8 +396,8 @@ class WebAuthnClientBase {
|
|
|
355
396
|
* @emits {CustomEvent} Emits a "MPCResponse" event with the mpcResponse data.
|
|
356
397
|
*/
|
|
357
398
|
async webauthnPartialGet (userIdentifier) {
|
|
358
|
-
if(userIdentifier == null
|
|
359
|
-
|| userIdentifier === '')
|
|
399
|
+
if(userIdentifier == null
|
|
400
|
+
|| userIdentifier === '') throw new Error(`[WebAuthnClient] :: webauthnPartialGet :: A userIdentifier String is required! Received: ${userIdentifier}.`);
|
|
360
401
|
if(userIdentifier && typeof userIdentifier === 'string') this.userIdentifier = userIdentifier;
|
|
361
402
|
this.status = 'AUTHENTICATION_STARTING';
|
|
362
403
|
await this.initializeZsm('webauthnPartialGet');
|
|
@@ -379,16 +420,7 @@ class WebAuthnClientBase {
|
|
|
379
420
|
if(!mpcResponse?.result) throw new Error('[WebAuthnClient - mpc_round] :: webauthnPartialGet :: Unable to obtain credential from Crypto Server!');
|
|
380
421
|
|
|
381
422
|
GLOBAL.dispatchEvent(new CustomEvent("MPCResponse", {bubbles: true, cancelable: false, detail: mpcResponse }));
|
|
382
|
-
|
|
383
|
-
const getCredential = mpcResponse.result;
|
|
384
|
-
return getCredential;
|
|
385
|
-
return {
|
|
386
|
-
signature: getCredential.response.signature,
|
|
387
|
-
signedChallenge: getCredential.response.clientDataJSON,
|
|
388
|
-
user_id: userIdentifier,
|
|
389
|
-
challenge: getChallenge.challenge,
|
|
390
|
-
credential: getCredential,
|
|
391
|
-
};
|
|
423
|
+
return mpcResponse.result;
|
|
392
424
|
};
|
|
393
425
|
|
|
394
426
|
/**
|
|
@@ -401,8 +433,8 @@ class WebAuthnClientBase {
|
|
|
401
433
|
* @throws {Error<UnenrollError>} Throws an UnenrollError if a credential matching userIdentifier IS stored on the device, but was unable to be removed.
|
|
402
434
|
*/
|
|
403
435
|
async unbindFromDevice (userIdentifier) {
|
|
404
|
-
if(userIdentifier == null
|
|
405
|
-
|| userIdentifier === '')
|
|
436
|
+
if(userIdentifier == null
|
|
437
|
+
|| userIdentifier === '') throw new Error(`[WebAuthnClient] :: unbindFromDevice :: A userIdentifier String is required! Received: ${userIdentifier}.`);
|
|
406
438
|
if(userIdentifier && typeof userIdentifier === 'string') this.userIdentifier = userIdentifier;
|
|
407
439
|
this.status = 'UNENROLL_STARTING';
|
|
408
440
|
if(userIdentifier !== this.userIdentifier) return false;
|
|
@@ -420,9 +452,9 @@ class WebAuthnClientBase {
|
|
|
420
452
|
if (cursor) {
|
|
421
453
|
const { key, value } = cursor;
|
|
422
454
|
if(
|
|
423
|
-
key.includes(userIdentifier) ||
|
|
455
|
+
key.includes(userIdentifier) ||
|
|
424
456
|
(typeof value === 'string' && value.includes(userIdentifier)) ||
|
|
425
|
-
typeof value === 'object' && value?.
|
|
457
|
+
typeof value === 'object' && value?.zsmCredential?.get('rawId') === rawID
|
|
426
458
|
) (store.delete(key), true);
|
|
427
459
|
cursor.continue();
|
|
428
460
|
}
|
|
@@ -435,6 +467,292 @@ class WebAuthnClientBase {
|
|
|
435
467
|
return true;
|
|
436
468
|
}
|
|
437
469
|
|
|
470
|
+
// PKP Methods ============================================================================================================
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* @name webauthnCreateFromPKP
|
|
474
|
+
* @description Constructs a new ZSM WebAuthn credential from a Passkeys Plus Platform Authenticator registration response.
|
|
475
|
+
* @param {string} userIdentifier The user identifier.
|
|
476
|
+
* @param {string} publicKey The public key for the enrollment process.
|
|
477
|
+
* @param {string} pairing_token The pairing token for the enrollment process.
|
|
478
|
+
* @returns {Promise<Object>} Resolves with the created credential.
|
|
479
|
+
* @throws {TypeError} userIdentifier, publicKey, or pairing_token parameters are missing.
|
|
480
|
+
* @throws {Error} (MPC Round) Crypto Server failed to respond.
|
|
481
|
+
* @throws {Error} (MPC Round) Response did not contain a valid credential/attestation object.
|
|
482
|
+
* @throws {Error} Could not verify credential with Relying Party Server.
|
|
483
|
+
* @memberof WebAuthnClientBase
|
|
484
|
+
* @async
|
|
485
|
+
*/
|
|
486
|
+
async webauthnCreateFromPKP (userIdentifier, publicKey, pairing_token) {
|
|
487
|
+
if(userIdentifier == null
|
|
488
|
+
|| userIdentifier === '') throw new TypeError(`[PK+ WebAuthnClient] :: webauthnCreateFromPKP :: A userIdentifier String is required! Received: ${userIdentifier}.`);
|
|
489
|
+
if(userIdentifier && typeof userIdentifier === 'string') this.userIdentifier = userIdentifier;
|
|
490
|
+
if(!publicKey) throw new TypeError('[WebAuthnClient] :: webauthnCreateFromPKP :: Missing publicKey parameter!');
|
|
491
|
+
if(!pairing_token) throw new TypeError('[WebAuthnClient] :: webauthnCreateFromPKP :: Missing pairing_token parameter!');
|
|
492
|
+
await this.initializeZsm('webauthnCreate');
|
|
493
|
+
|
|
494
|
+
let createResponse = await this.zsmApi.webauthn_create(publicKey, pairing_token, userIdentifier);
|
|
495
|
+
if(!createResponse) throw new Error('[WebAuthnClient - mpc_round] :: webauthnCreateFromPKP :: Unable to obtain response from Crypto Server!');
|
|
496
|
+
|
|
497
|
+
createResponse = toObject(createResponse);
|
|
498
|
+
GLOBAL.dispatchEvent(new CustomEvent("MPCResponse", {bubbles: true, cancelable: false, detail: createResponse }));
|
|
499
|
+
|
|
500
|
+
if(!createResponse?.result) throw new Error('[WebAuthnClient - mpc_round] :: webauthnCreateFromPKP :: Unable to obtain credential from Crypto Server!');
|
|
501
|
+
if(!createResponse?.result?.response) throw new Error('[WebAuthnClient - mpc_round] :: webauthnCreateFromPKP :: Unable to acquire attestation object!');
|
|
502
|
+
|
|
503
|
+
const credential = createResponse.result;
|
|
504
|
+
return credential;
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* @name webauthnGetFromPKP
|
|
509
|
+
* @description Retrieves a WebAuthn credential for the specified userIdentifier.
|
|
510
|
+
* @param {string} userIdentifier The user identifier.
|
|
511
|
+
* @param {string} publicKey The public key for the authentication process.
|
|
512
|
+
* @param {string} pairing_token The pairing token for the authentication process.
|
|
513
|
+
* @returns {Promise<ZSMAPI>} Resolves with the retrieved credential.
|
|
514
|
+
* @throws {TypeError} userIdentifier, publicKey, or pairing_token parameters are missing.
|
|
515
|
+
* @throws {Error} The class-level credential is missing for the specified userIdentifier.
|
|
516
|
+
* @throws {Error} (MPC Round) Crypto Server did not respond.
|
|
517
|
+
* @throws {Error} (MPC Round) Response from Crypto Server did not contain a credential.
|
|
518
|
+
* @throws {Error} Could not verify credential with Relying Party Server.
|
|
519
|
+
* @throws {Error} Token mismatched or missing from Relying Party Server verification attempt.
|
|
520
|
+
* @memberof WebAuthnClientBase
|
|
521
|
+
* @async
|
|
522
|
+
*/
|
|
523
|
+
async webauthnGetFromPKP (userIdentifier, publicKey, pairing_token) {
|
|
524
|
+
if(userIdentifier == null
|
|
525
|
+
|| userIdentifier === '') throw new Error(`[PK+ WebAuthnClient] :: webauthnGetFromPKP :: A userIdentifier String is required! Received: ${userIdentifier}.`);
|
|
526
|
+
if(userIdentifier && typeof userIdentifier === 'string') this.userIdentifier = userIdentifier;
|
|
527
|
+
if(!publicKey) throw new TypeError('[WebAuthnClient] :: webauthnGetFromPKP :: Missing publicKey parameter!');
|
|
528
|
+
if(!pairing_token) throw new TypeError('[WebAuthnClient] :: webauthnGetFromPKP :: Missing pairing_token parameter!');
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
let mpcResponse = await this.zsmApi.webauthn_get(publicKey, pairing_token, userIdentifier);
|
|
532
|
+
if(!mpcResponse) throw new Error('[WebAuthnClient - mpc_round] :: webauthnGetFromPKP :: Crypto server did not respond!');
|
|
533
|
+
|
|
534
|
+
mpcResponse = toObject(mpcResponse);
|
|
535
|
+
if(!mpcResponse?.result) throw new Error('[WebAuthnClient - mpc_round] :: webauthnGetFromPKP :: Response from Crypto Server did not contain a credential!\nResponse Body: ' + JSON.stringify(mpcResponse));
|
|
536
|
+
|
|
537
|
+
GLOBAL.dispatchEvent(new CustomEvent("MPCResponse", {bubbles: true, cancelable: false, detail: mpcResponse }));
|
|
538
|
+
|
|
539
|
+
const credential = mpcResponse.result;
|
|
540
|
+
return credential;
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* @name pkpCreate
|
|
545
|
+
* @description Creates a Passkeys+ credential for the user, if the user is not already enrolled, then creates a ZSM credential.
|
|
546
|
+
* @param {string|object} userIdentifier The identifier for the user (or bootstrapping challenge object).
|
|
547
|
+
* @param {string} userVerified Accepts "required", "preferred", "discouraged", to be passed into the Passkey ceremony.
|
|
548
|
+
* @param {boolean} bootstrapping If true, indicates that this is part of a bootstrapping flow from another device.
|
|
549
|
+
* @returns {Promise<Object>} Call to WebAuthnClientBase's webauthnCreate method
|
|
550
|
+
* @throws {TypeError} The userIdentifier is not provided, not a string, or is empty.
|
|
551
|
+
* @throws {Error} No response is received from the Relying Party Server.
|
|
552
|
+
* @throws {Error} Relying Party Challenge does not contain a publicKey.
|
|
553
|
+
* @throws {Error} If the Platform Authenticator Credential is not created successfully.
|
|
554
|
+
* @throws {Error} If the Platform Authenticator response is invalid.
|
|
555
|
+
* @memberof WebAuthnClientBase
|
|
556
|
+
* @async
|
|
557
|
+
*/
|
|
558
|
+
async pkpCreate (userIdentifier, userVerified="preferred", bootstrapping=false) {
|
|
559
|
+
|
|
560
|
+
let zsmCredentialChainID = null;
|
|
561
|
+
if(bootstrapping && typeof userIdentifier === 'object') {
|
|
562
|
+
const credentialDataObject = Object.assign({}, userIdentifier);
|
|
563
|
+
zsmCredentialChainID = userIdentifier.zsmCredentialID; // [J] Was .zsmCredID; now uses cleanFinalData field name
|
|
564
|
+
this.userIdentifier = userIdentifier = userIdentifier.userIdentifier;
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
if(userIdentifier == null || typeof userIdentifier !== 'string'
|
|
568
|
+
|| userIdentifier === '') throw new TypeError(`[PK+ WebAuthnClient] :: pkpCreate :: A userIdentifier String is required! Received: ${userIdentifier}.`);
|
|
569
|
+
this.userIdentifier = userIdentifier;
|
|
570
|
+
|
|
571
|
+
const relyingPartyChallenge = await this.relyingParty.pkpAuthenticationStart(credentialDataObject);
|
|
572
|
+
if(!relyingPartyChallenge) throw new Error(`[PK+ WebAuthnClient] :: pkpCreate :: No response from ZSM API's attempt to interface with Relying Party Server!`);
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
const paCredential = await this.passkeysPlus.pkpGetPasskeyCredential(relyingPartyChallenge, userVerified);
|
|
576
|
+
if (!paCredential) throw new Error('[PK+ WebAuthnClient] :: pkpCreate :: Unable to call Platform Authenticator to create Passkey!');
|
|
577
|
+
|
|
578
|
+
const rpRegistrationResult = await this.relyingParty.pkpAuthenticationFinish(paCredential);
|
|
579
|
+
if (!rpRegistrationResult) throw new Error('[PK+ WebAuthnClient] :: pkpCreate :: Platform Authenticator response was invalid!');
|
|
580
|
+
|
|
581
|
+
const publicKey = rpRegistrationResult?.response?.ccr?.publicKey??rpRegistrationResult?.response?.rcr?.publicKey;
|
|
582
|
+
|
|
583
|
+
let zsm_create = await this.webauthnGetFromPKP(userIdentifier, publicKey, rpRegistrationResult.response.token);
|
|
584
|
+
this.zsmCredentialId = zsm_create.rawId;
|
|
585
|
+
|
|
586
|
+
return Promise.resolve(zsm_create);
|
|
587
|
+
}else{
|
|
588
|
+
|
|
589
|
+
if(userIdentifier == null || typeof userIdentifier !== 'string'
|
|
590
|
+
|| userIdentifier === '') throw new TypeError(`[PK+ WebAuthnClient] :: pkpCreate :: A userIdentifier String is required! Received: ${userIdentifier}.`);
|
|
591
|
+
this.userIdentifier = userIdentifier;
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
const relyingPartyChallenge = await this.relyingParty.pkpRegistrationStart(userIdentifier, userVerified, zsmCredentialChainID);
|
|
595
|
+
if(!relyingPartyChallenge) throw new Error(`[PK+ WebAuthnClient] :: pkpCreate :: No response from ZSM API's attempt to interface with Relying Party Server!`);
|
|
596
|
+
|
|
597
|
+
const rpChallengePublicKey = relyingPartyChallenge?.publicKey;
|
|
598
|
+
if(!rpChallengePublicKey) throw new Error(`[PK+ WebAuthnClient] :: pkpCreate :: No publicKey found in Relying Party Challenge!`);
|
|
599
|
+
|
|
600
|
+
const paCredential = await this.passkeysPlus.pkpCreatePasskeyCredential(rpChallengePublicKey, userVerified);
|
|
601
|
+
if (!paCredential) throw new Error('[PK+ WebAuthnClient] :: pkpCreate :: Unable to call Platform Authenticator to create Passkey!');
|
|
602
|
+
|
|
603
|
+
const rpRegistrationResult = await this.relyingParty.pkpRegistrationFinish(paCredential);
|
|
604
|
+
if (!rpRegistrationResult) throw new Error('[PK+ WebAuthnClient] :: pkpCreate :: Platform Authenticator response was invalid!');
|
|
605
|
+
|
|
606
|
+
uid2iid('pk:' + userIdentifier, paCredential.id);
|
|
607
|
+
|
|
608
|
+
const publicKey = rpRegistrationResult?.response?.ccr?.publicKey??rpRegistrationResult?.response?.rcr?.publicKey;
|
|
609
|
+
|
|
610
|
+
let zsm_create = await this.webauthnCreateFromPKP(userIdentifier, publicKey, rpRegistrationResult.response.token);
|
|
611
|
+
this.zsmCredentialId = zsm_create.rawId;
|
|
612
|
+
|
|
613
|
+
return Promise.resolve(zsm_create);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* @name pkpAuthenticate
|
|
619
|
+
* @description Authenticates a user using a Passkeys+ credential.
|
|
620
|
+
* @param {string|object} userIdentifier The identifier for the user (or bootstrapping challenge object).
|
|
621
|
+
* @param {string} userVerified Accepts "required", "preferred", "discouraged", to be passed into the Passkey ceremony.
|
|
622
|
+
* @returns {Promise<Object>} Call to WebAuthnClientBase's webauthnGet method
|
|
623
|
+
* @throws {TypeError} The userIdentifier is not provided, not a string, or is empty.
|
|
624
|
+
* @throws {Error} If no Platform Authenticator Credential ID is found for the user.
|
|
625
|
+
* @throws {Error} If the Platform Authority Challenge is not retrieved successfully.
|
|
626
|
+
* @throws {Error} If the Platform Authenticator Credential is not created successfully.
|
|
627
|
+
* @throws {Error} If the Platform Authenticator response is invalid.
|
|
628
|
+
* @memberof WebAuthnClientBase
|
|
629
|
+
* @async
|
|
630
|
+
*/
|
|
631
|
+
async pkpAuthenticate (userIdentifier, userVerified="preferred", bootstrapping=false) {
|
|
632
|
+
let enrollmentDetails = null;
|
|
633
|
+
if(userIdentifier && typeof userIdentifier === 'object') {
|
|
634
|
+
enrollmentDetails = {...userIdentifier};
|
|
635
|
+
this.userIdentifier = userIdentifier = userIdentifier.userIdentifier;
|
|
636
|
+
}
|
|
637
|
+
if(userIdentifier == null || userIdentifier === '') throw new Error(`[PK+ WebAuthnClient] :: pkpAuthenticate :: A userIdentifier String is required! Received: ${userIdentifier}.`);
|
|
638
|
+
|
|
639
|
+
let platformAuthorityChallenge = null;
|
|
640
|
+
|
|
641
|
+
// Normal authentication flow
|
|
642
|
+
this.userIdentifier = userIdentifier;
|
|
643
|
+
// const completeUserIdentity = await this.webauthnRetrieveAll(userIdentifier);
|
|
644
|
+
platformAuthorityChallenge = await this.relyingParty.pkpAuthenticationStart(enrollmentDetails); // [J] Updated to use cleanFinalData field names; will pass null if we have no info on passkey presence, allowing the server to determine eligibility based on userIdentifier alone (and potentially return a remote credential ID if the user is eligible for Passkeys+ but doesn't have a local passkey)
|
|
645
|
+
if(!platformAuthorityChallenge) throw new Error(`[PK+ WebAuthnClient] :: pkpAuthenticate :: Unable to retrieve Platform Authority Challenge from Relying Party Server!`);
|
|
646
|
+
|
|
647
|
+
const paCredential = await this.passkeysPlus.pkpGetPasskeyCredential(platformAuthorityChallenge, userVerified);
|
|
648
|
+
if (!paCredential) throw new Error('[PK+ WebAuthnClient] :: pkpAuthenticate :: Unable to call Platform Authenticator to create Passkey!');
|
|
649
|
+
|
|
650
|
+
const paAuthenticationResult = await this.relyingParty.pkpAuthenticationFinish(paCredential);
|
|
651
|
+
if (!paAuthenticationResult) throw new Error('[PK+ WebAuthnClient] :: pkpAuthenticate :: Platform Authenticator response was invalid!');
|
|
652
|
+
|
|
653
|
+
const publicKey = paAuthenticationResult?.response?.ccr?.publicKey??paAuthenticationResult?.response?.rcr?.publicKey;
|
|
654
|
+
|
|
655
|
+
const getResult = bootstrapping ? await this.webauthnCreateFromPKP(userIdentifier, publicKey, paAuthenticationResult.response.token)
|
|
656
|
+
: await this.webauthnGetFromPKP(userIdentifier, publicKey, paAuthenticationResult.response.token);
|
|
657
|
+
|
|
658
|
+
return getResult;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* @name pkpAddZSMCredential
|
|
663
|
+
* @description Adds a ZSM credential for a user who already has Passkeys+ credentials registered on another device.
|
|
664
|
+
* @param {string} userIdentifier The identifier for the user.
|
|
665
|
+
* @returns {Promise<Object>} Completed ZSM + Passkeys+ authentication result.
|
|
666
|
+
* @throws {Error} If no Passkeys+ credentials are found for the user on another device.
|
|
667
|
+
* @memberof WebAuthnClientBase
|
|
668
|
+
* @async
|
|
669
|
+
*/
|
|
670
|
+
async pkpAddZSMCredential (userIdentifier) {
|
|
671
|
+
if(userIdentifier == null || userIdentifier === '') throw new Error(`[PK+ WebAuthnClient] :: pkpAddZSMCredential :: A userIdentifier String is required! Received: ${userIdentifier}.`);
|
|
672
|
+
if(userIdentifier && typeof userIdentifier === 'string') this.userIdentifier = userIdentifier;
|
|
673
|
+
const bootstrapChallenge = await this.webauthnRetrieveAll(userIdentifier);
|
|
674
|
+
if(bootstrapChallenge === false) throw new Error('[PK+ WebAuthnClient] :: pkpAddZSMCredential :: Cannot add ZSM; no passkey credentials registered for this user!');
|
|
675
|
+
|
|
676
|
+
bootstrapChallenge.userIdentifier = userIdentifier;
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
// userVerified set to 'required' to ensure strong authentication when adding ZSM credential
|
|
680
|
+
return this.pkpAuthenticate(bootstrapChallenge, 'required', true);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* @name pkpAddPasskeyCredential
|
|
685
|
+
* @description Adds a Passkeys+ credential for a user who already has a ZSM credential registered.
|
|
686
|
+
* @param {string} userIdentifier The user identifier.
|
|
687
|
+
* @returns {Promise<Object>} Call to pkpCreate to add Passkeys+ credential.
|
|
688
|
+
* @throws {Error} If the user is not eligible to add a Passkeys+ credential.
|
|
689
|
+
* @memberof WebAuthnClientBase
|
|
690
|
+
* @async
|
|
691
|
+
*/
|
|
692
|
+
async pkpAddPasskeyCredential (userIdentifier, userVerified="preferred") {
|
|
693
|
+
let enrollmentDetails = null;
|
|
694
|
+
if(userIdentifier && typeof userIdentifier === 'object') {
|
|
695
|
+
enrollmentDetails = {...userIdentifier};
|
|
696
|
+
this.userIdentifier = userIdentifier = userIdentifier.userIdentifier;
|
|
697
|
+
}else{
|
|
698
|
+
this.userIdentifier = userIdentifier;
|
|
699
|
+
enrollmentDetails = await this.webauthnRetrieveAll(userIdentifier, true);
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
if (enrollmentDetails instanceof Error) throw new Error(`[WebAuthnClient] :: pkpAddPasskeyCredential :: Unable to retrieve existing credentials for userIdentifier: ${userIdentifier}. Details: ${enrollmentDetails.message || enrollmentDetails}`);
|
|
703
|
+
if (!enrollmentDetails?.zsmCredentialID) throw new Error(`[WebAuthnClient] :: pkpAddPasskeyCredential :: userIdentifier: ${userIdentifier} does not have an existing ZSM credential!`); // [J] Was .hasZSMCred
|
|
704
|
+
|
|
705
|
+
return this.pkpCreate ( // [J] Updated to use cleanFinalData field names
|
|
706
|
+
!enrollmentDetails.hasLocalPasskey ? userIdentifier : Object.assign({}, enrollmentDetails, { userIdentifier }), // [J] Was enrollmentDetails.userIdentifier; now attaches userIdentifier to the object for pkpCreate's bootstrapping path
|
|
707
|
+
userVerified,
|
|
708
|
+
enrollmentDetails.hasLocalPasskey // [J] Was .hasLocalPasskey
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* @name checkForPasskeysOnOtherDevice
|
|
714
|
+
* @description Checks if Passkeys+ credentials are registered for the specified userIdentifier on another device.
|
|
715
|
+
* @ (If found, converts challenge data to ArrayBuffers for WebAuthn API consumption.)
|
|
716
|
+
* @param {string} userIdentifier The user identifier.
|
|
717
|
+
* @returns {object} Returns the bootstrap challenge object if passkey(s) are found, or...
|
|
718
|
+
* @returns {boolean} ...returns false if no passkey credentials are found.
|
|
719
|
+
* @memberof WebAuthnClientBase
|
|
720
|
+
* @async
|
|
721
|
+
*/
|
|
722
|
+
async checkForPasskeysOnOtherDevice (userIdentifier) {
|
|
723
|
+
if(userIdentifier == null
|
|
724
|
+
|| userIdentifier === '') throw new Error(`[PK+ WebAuthnClient] :: checkForPasskeysOnOtherDevice :: A userIdentifier String is required! Received: ${userIdentifier}.`);
|
|
725
|
+
if(userIdentifier && typeof userIdentifier === 'string') this.userIdentifier = userIdentifier;
|
|
726
|
+
try {
|
|
727
|
+
|
|
728
|
+
this.userIdentifier = userIdentifier;
|
|
729
|
+
const bootstrapChallenge = await this.relyingParty.makePostRequest( `api/passkey/bootstrap/start`)
|
|
730
|
+
.catch( error => {
|
|
731
|
+
return false;
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
if(!bootstrapChallenge || bootstrapChallenge instanceof Error) return false;
|
|
735
|
+
|
|
736
|
+
if((bootstrapChallenge?.rcr?.publicKey?.allowCredentials?.length??0) === 0) return false;
|
|
737
|
+
if((bootstrapChallenge?.rcr?.publicKey?.allowCredentials?.length??0) >= 1) {
|
|
738
|
+
uid2iid('pk:' + userIdentifier, bootstrapChallenge.rcr.publicKey.allowCredentials[0].id);
|
|
739
|
+
return bootstrapChallenge.rcr.publicKey.allowCredentials[0].id;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if(bootstrapChallenge?.rcr?.publicKey?.challenge) bootstrapChallenge.rcr.publicKey.challenge = this.passkeysPlus.toArrayBuffer(bootstrapChallenge.rcr.publicKey.challenge);
|
|
743
|
+
if(bootstrapChallenge?.rcr?.publicKey?.allowCredentials) {
|
|
744
|
+
bootstrapChallenge.rcr.publicKey.allowCredentials = bootstrapChallenge.rcr.publicKey.allowCredentials.map(cred => {
|
|
745
|
+
return {
|
|
746
|
+
...cred,
|
|
747
|
+
id: this.passkeysPlus.toArrayBuffer(cred.id)
|
|
748
|
+
};
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
return bootstrapChallenge;
|
|
752
|
+
} catch (error) {
|
|
753
|
+
return false;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
438
756
|
/**
|
|
439
757
|
* Resets the WebAuthn credential for the user.
|
|
440
758
|
* @param {string} userIdentifier - The userIdentifier.
|