nid-webauthn-emulator 0.2.0 → 0.2.2

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/README.md CHANGED
@@ -146,10 +146,24 @@ async function startWebAuthnEmulator(page: Page, origin: string, debug = false)
146
146
 
147
147
  await page.exposeFunction(
148
148
  BrowserInjection.WebAuthnEmulatorSignalUnknownCredential,
149
- async (options: { rpId: string; credentialId: string }) => {
149
+ async (options: UnknownCredentialOptionsJSON) => {
150
150
  emulator.signalUnknownCredential(options);
151
151
  },
152
152
  );
153
+
154
+ await page.exposeFunction(
155
+ BrowserInjection.WebAuthnEmulatorSignalAllAcceptedCredentials,
156
+ async (options: AllAcceptedCredentialsOptionsJSON) => {
157
+ emulator.signalAllAcceptedCredentials(options);
158
+ };
159
+ );
160
+
161
+ await page.exposeFunction(
162
+ BrowserInjection.WebAuthnEmulatorSignalCurrentUserDetails,
163
+ async (options: CurrentUserDetailsOptionsJSON) => {
164
+ emulator.signalCurrentUserDetails(options);
165
+ };
166
+ );
153
167
  }
154
168
 
155
169
  test.describe("Passkeys Tests", { tag: ["@daily"] }, () => {
@@ -1,9 +1,14 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  const globals_1 = require("@jest/globals");
4
- const globals_2 = require("@jest/globals");
5
7
  const src_1 = require("../../src");
6
8
  const ctap_model_1 = require("../../src/authenticator/ctap-model");
9
+ const ctap_model_2 = require("../../src/authenticator/ctap-model");
10
+ const encode_utils_1 = __importDefault(require("../../src/libs/encode-utils"));
11
+ const credentials_memory_repository_1 = require("../../src/repository/credentials-memory-repository");
7
12
  (0, globals_1.describe)("Authenticator Emulator Exceptional Test", () => {
8
13
  // Success case has been tested in the webauthn-emulator.spec.ts
9
14
  (0, globals_1.test)("Unknown command _ CTAP Error", async () => {
@@ -11,8 +16,230 @@ const ctap_model_1 = require("../../src/authenticator/ctap-model");
11
16
  command: ctap_model_1.CTAP_COMMAND.authenticatorReset,
12
17
  };
13
18
  const authenticator = new src_1.AuthenticatorEmulator();
14
- (0, globals_2.expect)(() => {
19
+ (0, globals_1.expect)(() => {
15
20
  authenticator.command(testRequest);
16
21
  }).toThrowError("CTAP error: CTAP1_ERR_INVALID_COMMAND (1)");
17
22
  });
18
23
  });
24
+ (0, globals_1.describe)("Authenticator Credential Management Tests", () => {
25
+ let authenticator;
26
+ let repository;
27
+ // Test data
28
+ const rpId1 = "example.com";
29
+ const rpId2 = "test.com";
30
+ const user1 = {
31
+ id: encode_utils_1.default.strToUint8Array("user1-id"),
32
+ name: "user1",
33
+ displayName: "User One",
34
+ };
35
+ const user2 = {
36
+ id: encode_utils_1.default.strToUint8Array("user2-id"),
37
+ name: "user2",
38
+ displayName: "User Two",
39
+ };
40
+ const user3 = {
41
+ id: encode_utils_1.default.strToUint8Array("user3-id"),
42
+ name: "user3",
43
+ displayName: "User Three",
44
+ };
45
+ (0, globals_1.beforeEach)(() => {
46
+ // Create a new repository for each test
47
+ repository = new credentials_memory_repository_1.PasskeysCredentialsMemoryRepository();
48
+ // Create authenticator with the repository
49
+ authenticator = new src_1.AuthenticatorEmulator({
50
+ credentialsRepository: repository,
51
+ });
52
+ // Create test credentials
53
+ createTestCredential(rpId1, user1);
54
+ createTestCredential(rpId1, user2);
55
+ createTestCredential(rpId2, user3);
56
+ });
57
+ // Helper function to create a test credential
58
+ function createTestCredential(rpId, user) {
59
+ const clientDataHash = new Uint8Array(32).fill(1);
60
+ const makeCredentialRequest = {
61
+ clientDataHash,
62
+ rp: { id: rpId, name: rpId },
63
+ user,
64
+ pubKeyCredParams: [{ type: "public-key", alg: -7 }],
65
+ };
66
+ authenticator.authenticatorMakeCredential(makeCredentialRequest);
67
+ }
68
+ (0, globals_1.describe)("enumerateCredentialsBegin", () => {
69
+ (0, globals_1.test)("should return the total number of credentials for a specific RP", () => {
70
+ // Create request for enumerateCredentialsBegin
71
+ const request = {
72
+ subCommand: ctap_model_2.CREDENTIAL_MANAGEMENT_SUBCOMMAND.enumerateCredentialsBegin,
73
+ subCommandParams: {
74
+ rpId: rpId1,
75
+ },
76
+ };
77
+ // Execute the command
78
+ const response = authenticator.authenticatorCredentialManagement(request);
79
+ // Verify the response
80
+ (0, globals_1.expect)(response.totalCredentials).toBe(2); // We created 2 credentials for rpId1
81
+ });
82
+ (0, globals_1.test)("should throw an error if rpId is not provided", () => {
83
+ // Create request without rpId
84
+ const request = {
85
+ subCommand: ctap_model_2.CREDENTIAL_MANAGEMENT_SUBCOMMAND.enumerateCredentialsBegin,
86
+ };
87
+ // Execute the command and expect an error
88
+ (0, globals_1.expect)(() => {
89
+ authenticator.authenticatorCredentialManagement(request);
90
+ }).toThrow(new ctap_model_2.AuthenticationEmulatorError(ctap_model_2.CTAP_STATUS_CODE.CTAP1_ERR_INVALID_PARAMETER));
91
+ });
92
+ (0, globals_1.test)("should return 0 credentials for an RP with no credentials", () => {
93
+ // Create request for a non-existent RP
94
+ const request = {
95
+ subCommand: ctap_model_2.CREDENTIAL_MANAGEMENT_SUBCOMMAND.enumerateCredentialsBegin,
96
+ subCommandParams: {
97
+ rpId: "nonexistent.com",
98
+ },
99
+ };
100
+ // Execute the command
101
+ const response = authenticator.authenticatorCredentialManagement(request);
102
+ // Verify the response
103
+ (0, globals_1.expect)(response.totalCredentials).toBe(0);
104
+ });
105
+ });
106
+ (0, globals_1.describe)("enumerateCredentialsGetNextCredential", () => {
107
+ (0, globals_1.test)("should return credentials one by one", () => {
108
+ // First, call enumerateCredentialsBegin
109
+ const beginRequest = {
110
+ subCommand: ctap_model_2.CREDENTIAL_MANAGEMENT_SUBCOMMAND.enumerateCredentialsBegin,
111
+ subCommandParams: {
112
+ rpId: rpId1,
113
+ },
114
+ };
115
+ authenticator.authenticatorCredentialManagement(beginRequest);
116
+ // Now call enumerateCredentialsGetNextCredential twice to get both credentials
117
+ const getNextRequest = {
118
+ subCommand: ctap_model_2.CREDENTIAL_MANAGEMENT_SUBCOMMAND.enumerateCredentialsGetNextCredential,
119
+ };
120
+ // Get first credential
121
+ const response1 = authenticator.authenticatorCredentialManagement(getNextRequest);
122
+ (0, globals_1.expect)(response1.user).toBeDefined();
123
+ (0, globals_1.expect)(response1.credentialID).toBeDefined();
124
+ (0, globals_1.expect)(response1.publicKey).toBeDefined();
125
+ // Get second credential
126
+ const response2 = authenticator.authenticatorCredentialManagement(getNextRequest);
127
+ (0, globals_1.expect)(response2.user).toBeDefined();
128
+ (0, globals_1.expect)(response2.credentialID).toBeDefined();
129
+ (0, globals_1.expect)(response2.publicKey).toBeDefined();
130
+ // Verify we got different credentials
131
+ if (response1.credentialID && response2.credentialID) {
132
+ (0, globals_1.expect)(encode_utils_1.default.encodeBase64Url(response1.credentialID)).not.toBe(encode_utils_1.default.encodeBase64Url(response2.credentialID));
133
+ }
134
+ // Verify we can't get more credentials
135
+ (0, globals_1.expect)(() => {
136
+ authenticator.authenticatorCredentialManagement(getNextRequest);
137
+ }).toThrow(new ctap_model_2.AuthenticationEmulatorError(ctap_model_2.CTAP_STATUS_CODE.CTAP2_ERR_NO_CREDENTIALS));
138
+ });
139
+ (0, globals_1.test)("should throw an error if called without calling enumerateCredentialsBegin first", () => {
140
+ const getNextRequest = {
141
+ subCommand: ctap_model_2.CREDENTIAL_MANAGEMENT_SUBCOMMAND.enumerateCredentialsGetNextCredential,
142
+ };
143
+ (0, globals_1.expect)(() => {
144
+ authenticator.authenticatorCredentialManagement(getNextRequest);
145
+ }).toThrow(new ctap_model_2.AuthenticationEmulatorError(ctap_model_2.CTAP_STATUS_CODE.CTAP2_ERR_NO_CREDENTIALS));
146
+ });
147
+ });
148
+ (0, globals_1.describe)("updateUserInformation", () => {
149
+ (0, globals_1.test)("should update user information for all credentials of a user", () => {
150
+ // Create updated user information
151
+ const updatedUser = {
152
+ id: user1.id, // Same ID
153
+ name: "user1-updated",
154
+ displayName: "Updated User One",
155
+ };
156
+ // Create request for updateUserInformation
157
+ const request = {
158
+ subCommand: ctap_model_2.CREDENTIAL_MANAGEMENT_SUBCOMMAND.updateUserInformation,
159
+ subCommandParams: {
160
+ rpId: rpId1,
161
+ user: updatedUser,
162
+ },
163
+ };
164
+ // Execute the command
165
+ authenticator.authenticatorCredentialManagement(request);
166
+ // Verify the user information was updated by enumerating credentials
167
+ const beginRequest = {
168
+ subCommand: ctap_model_2.CREDENTIAL_MANAGEMENT_SUBCOMMAND.enumerateCredentialsBegin,
169
+ subCommandParams: {
170
+ rpId: rpId1,
171
+ },
172
+ };
173
+ authenticator.authenticatorCredentialManagement(beginRequest);
174
+ const getNextRequest = {
175
+ subCommand: ctap_model_2.CREDENTIAL_MANAGEMENT_SUBCOMMAND.enumerateCredentialsGetNextCredential,
176
+ };
177
+ // Get credentials and check if user info was updated
178
+ const nextResponse = authenticator.authenticatorCredentialManagement(getNextRequest);
179
+ // Find the credential for user1
180
+ if (nextResponse.user &&
181
+ encode_utils_1.default.encodeBase64Url(nextResponse.user.id) === encode_utils_1.default.encodeBase64Url(user1.id)) {
182
+ (0, globals_1.expect)(nextResponse.user.name).toBe(updatedUser.name);
183
+ (0, globals_1.expect)(nextResponse.user.displayName).toBe(updatedUser.displayName);
184
+ }
185
+ });
186
+ (0, globals_1.test)("should throw an error if rpId is not provided", () => {
187
+ const request = {
188
+ subCommand: ctap_model_2.CREDENTIAL_MANAGEMENT_SUBCOMMAND.updateUserInformation,
189
+ subCommandParams: {
190
+ user: user1,
191
+ },
192
+ };
193
+ (0, globals_1.expect)(() => {
194
+ authenticator.authenticatorCredentialManagement(request);
195
+ }).toThrow(new ctap_model_2.AuthenticationEmulatorError(ctap_model_2.CTAP_STATUS_CODE.CTAP1_ERR_INVALID_PARAMETER));
196
+ });
197
+ (0, globals_1.test)("should throw an error if user is not provided", () => {
198
+ const request = {
199
+ subCommand: ctap_model_2.CREDENTIAL_MANAGEMENT_SUBCOMMAND.updateUserInformation,
200
+ subCommandParams: {
201
+ rpId: rpId1,
202
+ },
203
+ };
204
+ (0, globals_1.expect)(() => {
205
+ authenticator.authenticatorCredentialManagement(request);
206
+ }).toThrow(new ctap_model_2.AuthenticationEmulatorError(ctap_model_2.CTAP_STATUS_CODE.CTAP1_ERR_INVALID_PARAMETER));
207
+ });
208
+ (0, globals_1.test)("should throw an error if no credentials exist for the user", () => {
209
+ const nonExistentUser = {
210
+ id: encode_utils_1.default.strToUint8Array("nonexistent-id"),
211
+ name: "nonexistent",
212
+ displayName: "Non Existent User",
213
+ };
214
+ const request = {
215
+ subCommand: ctap_model_2.CREDENTIAL_MANAGEMENT_SUBCOMMAND.updateUserInformation,
216
+ subCommandParams: {
217
+ rpId: rpId1,
218
+ user: nonExistentUser,
219
+ },
220
+ };
221
+ (0, globals_1.expect)(() => {
222
+ authenticator.authenticatorCredentialManagement(request);
223
+ }).toThrow(new ctap_model_2.AuthenticationEmulatorError(ctap_model_2.CTAP_STATUS_CODE.CTAP2_ERR_NO_CREDENTIALS));
224
+ });
225
+ });
226
+ (0, globals_1.describe)("authenticatorCredentialManagement with stateless authenticator", () => {
227
+ (0, globals_1.test)("should throw an error if authenticator is stateless", () => {
228
+ // Create a stateless authenticator
229
+ const statelessAuthenticator = new src_1.AuthenticatorEmulator({
230
+ stateless: true,
231
+ });
232
+ // Create request for enumerateCredentialsBegin
233
+ const request = {
234
+ subCommand: ctap_model_2.CREDENTIAL_MANAGEMENT_SUBCOMMAND.enumerateCredentialsBegin,
235
+ subCommandParams: {
236
+ rpId: rpId1,
237
+ },
238
+ };
239
+ // Execute the command and expect an error
240
+ (0, globals_1.expect)(() => {
241
+ statelessAuthenticator.authenticatorCredentialManagement(request);
242
+ }).toThrow(new ctap_model_2.AuthenticationEmulatorError(ctap_model_2.CTAP_STATUS_CODE.CTAP2_ERR_NOT_ALLOWED));
243
+ });
244
+ });
245
+ });
@@ -258,3 +258,141 @@ const webauthn_test_server_1 = require("./webauthn-test-server");
258
258
  (0, globals_1.expect)(credentialsAfter.length).toBe(0);
259
259
  });
260
260
  });
261
+ (0, globals_1.describe)("WebAuthnEmulator signalAllAcceptedCredentials Test", () => {
262
+ (0, globals_1.test)("Signal All Accepted Credentials _ OK", async () => {
263
+ // Create users
264
+ const user = { username: "user1", id: "user1-id" };
265
+ // Create a new emulator with a clean repository
266
+ const repository = new credentials_memory_repository_1.PasskeysCredentialsMemoryRepository();
267
+ const authenticator = new src_1.AuthenticatorEmulator({
268
+ credentialsRepository: repository,
269
+ });
270
+ const emulator = new src_1.default(authenticator);
271
+ const testServer = new webauthn_test_server_1.WebAuthnTestServer();
272
+ const options = await testServer.getRegistrationOptions(user);
273
+ // Create credential1 for user
274
+ const credential1 = emulator.createJSON(webauthn_test_server_1.TEST_RP_ORIGIN, options);
275
+ const backupCredential1 = repository.loadCredentials()[0];
276
+ // Create credential2 for user (credential1 is overwritten)
277
+ const credential2 = emulator.createJSON(webauthn_test_server_1.TEST_RP_ORIGIN, options);
278
+ // restore credential1
279
+ repository.saveCredential(backupCredential1);
280
+ // Verify all credentials exist
281
+ const credentialsBefore = repository.loadCredentials();
282
+ (0, globals_1.expect)(credentialsBefore.length).toBe(2);
283
+ // Get the user1 credentials
284
+ const user1Credentials = credentialsBefore.filter((cred) => cred.user.name === user.username);
285
+ // Get the actual user ID from the credential
286
+ const actualUserId = encode_utils_1.default.encodeBase64Url(user1Credentials[0].user.id);
287
+ // Get the IDs of all credentials
288
+ const allCredentialIds = credentialsBefore.map((cred) => encode_utils_1.default.encodeBase64Url(cred.publicKeyCredentialSource.id));
289
+ // Signal that only the first credential is accepted for user1
290
+ emulator.signalAllAcceptedCredentials({
291
+ rpId: webauthn_test_server_1.TEST_RP_ORIGIN.replace("https://", ""),
292
+ userId: actualUserId,
293
+ allAcceptedCredentialIds: [credential1.id],
294
+ });
295
+ // Verify that one credential was deleted
296
+ const credentialsAfter = repository.loadCredentials();
297
+ (0, globals_1.expect)(credentialsAfter.length).toBe(1);
298
+ // Check that credential1 and credential3 still exist
299
+ const remainingIds = credentialsAfter.map((cred) => encode_utils_1.default.encodeBase64Url(cred.publicKeyCredentialSource.id));
300
+ (0, globals_1.expect)(remainingIds).toContain(credential1.id);
301
+ (0, globals_1.expect)(remainingIds).not.toContain(credential2.id);
302
+ // Verify one credential was deleted
303
+ (0, globals_1.expect)(remainingIds.length).toBe(1);
304
+ (0, globals_1.expect)(allCredentialIds.length).toBe(2);
305
+ const deletedIds = allCredentialIds.filter((id) => !remainingIds.includes(id));
306
+ (0, globals_1.expect)(deletedIds.length).toBe(1);
307
+ });
308
+ (0, globals_1.test)("Signal All Accepted Credentials with empty list _ Delete all user credentials", async () => {
309
+ // Create user
310
+ const user = { username: "user-test", id: "user-test-id" };
311
+ // Create a new emulator with a clean repository
312
+ const repository = new credentials_memory_repository_1.PasskeysCredentialsMemoryRepository();
313
+ const authenticator = new src_1.AuthenticatorEmulator({
314
+ credentialsRepository: repository,
315
+ });
316
+ const emulator = new src_1.default(authenticator);
317
+ const testServer = new webauthn_test_server_1.WebAuthnTestServer();
318
+ // Create a credential for the user
319
+ const options = await testServer.getRegistrationOptions(user);
320
+ const credential1 = emulator.createJSON(webauthn_test_server_1.TEST_RP_ORIGIN, options);
321
+ await testServer.getRegistrationVerification(user, credential1);
322
+ // Verify credentials exist
323
+ const credentialsBefore = repository.loadCredentials();
324
+ (0, globals_1.expect)(credentialsBefore.length).toBe(1);
325
+ // Get the actual user ID from the credential
326
+ const actualUserId = encode_utils_1.default.encodeBase64Url(credentialsBefore[0].user.id);
327
+ // Signal empty accepted credentials list
328
+ emulator.signalAllAcceptedCredentials({
329
+ rpId: webauthn_test_server_1.TEST_RP_ORIGIN.replace("https://", ""),
330
+ userId: actualUserId,
331
+ allAcceptedCredentialIds: [],
332
+ });
333
+ // Verify all user credentials were deleted
334
+ const userCredentialsAfter = repository
335
+ .loadCredentials()
336
+ .filter((cred) => encode_utils_1.default.encodeBase64Url(cred.user.id) === actualUserId);
337
+ (0, globals_1.expect)(userCredentialsAfter.length).toBe(0);
338
+ });
339
+ });
340
+ (0, globals_1.describe)("WebAuthnEmulator signalCurrentUserDetails Test", () => {
341
+ (0, globals_1.test)("Signal Current User Details _ OK", async () => {
342
+ // Create user
343
+ const user = { username: "user-details", id: "user-details-id" };
344
+ // Create a new emulator with a clean repository
345
+ const repository = new credentials_memory_repository_1.PasskeysCredentialsMemoryRepository();
346
+ const authenticator = new src_1.AuthenticatorEmulator({
347
+ credentialsRepository: repository,
348
+ });
349
+ const emulator = new src_1.default(authenticator);
350
+ const testServer = new webauthn_test_server_1.WebAuthnTestServer();
351
+ // Create a credential
352
+ const options = await testServer.getRegistrationOptions(user);
353
+ const credential = emulator.createJSON(webauthn_test_server_1.TEST_RP_ORIGIN, options);
354
+ await testServer.getRegistrationVerification(user, credential);
355
+ // Verify the credential exists with original user details
356
+ const credentialsBefore = repository.loadCredentials();
357
+ (0, globals_1.expect)(credentialsBefore.length).toBe(1);
358
+ (0, globals_1.expect)(credentialsBefore[0].user.name).toBe(user.username);
359
+ // Note: We don't check displayName as it's not consistently set by the test server
360
+ // Get the actual user ID from the credential
361
+ const actualUserId = encode_utils_1.default.encodeBase64Url(credentialsBefore[0].user.id);
362
+ // Update user details using the actual user ID from the credential
363
+ const updatedName = "Updated User Name";
364
+ const updatedDisplayName = "Updated Display Name";
365
+ emulator.signalCurrentUserDetails({
366
+ rpId: webauthn_test_server_1.TEST_RP_ORIGIN.replace("https://", ""),
367
+ userId: actualUserId,
368
+ name: updatedName,
369
+ displayName: updatedDisplayName,
370
+ });
371
+ // Verify the user details were updated
372
+ const credentialsAfter = repository.loadCredentials();
373
+ (0, globals_1.expect)(credentialsAfter.length).toBe(1);
374
+ (0, globals_1.expect)(credentialsAfter[0].user.name).toBe(updatedName);
375
+ (0, globals_1.expect)(credentialsAfter[0].user.displayName).toBe(updatedDisplayName);
376
+ // Verify the credential ID and user ID remain unchanged
377
+ (0, globals_1.expect)(encode_utils_1.default.encodeBase64Url(credentialsAfter[0].publicKeyCredentialSource.id)).toBe(credential.id);
378
+ (0, globals_1.expect)(encode_utils_1.default.encodeBase64Url(credentialsAfter[0].user.id)).toBe(actualUserId);
379
+ });
380
+ (0, globals_1.test)("Signal Current User Details with non-existent user _ Error", async () => {
381
+ // Create a new emulator with a clean repository
382
+ const repository = new credentials_memory_repository_1.PasskeysCredentialsMemoryRepository();
383
+ const authenticator = new src_1.AuthenticatorEmulator({
384
+ credentialsRepository: repository,
385
+ });
386
+ const emulator = new src_1.default(authenticator);
387
+ // Try to update non-existent user
388
+ const nonExistentUserId = "non-existent-user-id";
389
+ (0, globals_1.expect)(() => {
390
+ emulator.signalCurrentUserDetails({
391
+ rpId: webauthn_test_server_1.TEST_RP_ORIGIN.replace("https://", ""),
392
+ userId: encode_utils_1.default.encodeBase64Url(encode_utils_1.default.strToUint8Array(nonExistentUserId)),
393
+ name: "New Name",
394
+ displayName: "New Display Name",
395
+ });
396
+ }).toThrow(); // Should throw CTAP2_ERR_NO_CREDENTIALS
397
+ });
398
+ });
@@ -56,6 +56,10 @@ export declare class AuthenticatorEmulator {
56
56
  * @see https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#authenticatorCredentialManagement
57
57
  */
58
58
  authenticatorCredentialManagement(request: AuthenticatorCredentialManagementRequest): AuthenticatorCredentialManagementResponse;
59
+ private enumerationState;
60
+ private authenticatorEnumerateCredentialsBegin;
61
+ private authenticatorEnumerateCredentialsGetNextCredential;
62
+ private authenticatorUpdateUserInformation;
59
63
  private authenticatorDeleteCredential;
60
64
  /** @see https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticatorGetInfo */
61
65
  authenticatorGetInfo(): AuthenticatorGetInfoResponse;
@@ -88,10 +88,82 @@ class AuthenticatorEmulator {
88
88
  switch (request.subCommand) {
89
89
  case ctap_model_1.CREDENTIAL_MANAGEMENT_SUBCOMMAND.deleteCredential:
90
90
  return this.authenticatorDeleteCredential(request);
91
+ case ctap_model_1.CREDENTIAL_MANAGEMENT_SUBCOMMAND.enumerateCredentialsBegin:
92
+ return this.authenticatorEnumerateCredentialsBegin(request);
93
+ case ctap_model_1.CREDENTIAL_MANAGEMENT_SUBCOMMAND.enumerateCredentialsGetNextCredential:
94
+ return this.authenticatorEnumerateCredentialsGetNextCredential();
95
+ case ctap_model_1.CREDENTIAL_MANAGEMENT_SUBCOMMAND.updateUserInformation:
96
+ return this.authenticatorUpdateUserInformation(request);
91
97
  default:
92
98
  throw new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP1_ERR_INVALID_COMMAND);
93
99
  }
94
100
  }
101
+ // Store the current enumeration state
102
+ enumerationState = null;
103
+ authenticatorEnumerateCredentialsBegin(request) {
104
+ const repository = this.params.credentialsRepository;
105
+ if (!repository) {
106
+ throw new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP2_ERR_NOT_ALLOWED);
107
+ }
108
+ const rpId = request.subCommandParams?.rpId;
109
+ if (!rpId) {
110
+ throw new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP1_ERR_INVALID_PARAMETER);
111
+ }
112
+ // Get all credentials for the RP
113
+ const allCredentials = repository.loadCredentials();
114
+ const rpCredentials = allCredentials.filter((cred) => cred.publicKeyCredentialSource.rpId.value === rpId);
115
+ // Store the enumeration state
116
+ this.enumerationState = {
117
+ rpId,
118
+ credentials: rpCredentials,
119
+ currentIndex: 0,
120
+ };
121
+ return {
122
+ totalCredentials: rpCredentials.length,
123
+ };
124
+ }
125
+ authenticatorEnumerateCredentialsGetNextCredential() {
126
+ if (!this.enumerationState || this.enumerationState.currentIndex >= this.enumerationState.credentials.length) {
127
+ throw new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP2_ERR_NO_CREDENTIALS);
128
+ }
129
+ const credential = this.enumerationState.credentials[this.enumerationState.currentIndex++];
130
+ return {
131
+ user: credential.user,
132
+ credentialID: encode_utils_1.default.bufferSourceToUint8Array(credential.publicKeyCredentialDescriptor.id),
133
+ publicKey: credential.authenticatorData.attestedCredentialData?.credentialPublicKey.toDer(),
134
+ };
135
+ }
136
+ authenticatorUpdateUserInformation(request) {
137
+ const repository = this.params.credentialsRepository;
138
+ if (!repository) {
139
+ throw new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP2_ERR_NOT_ALLOWED);
140
+ }
141
+ const rpId = request.subCommandParams?.rpId;
142
+ const user = request.subCommandParams?.user;
143
+ if (!rpId || !user) {
144
+ throw new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP1_ERR_INVALID_PARAMETER);
145
+ }
146
+ // Find all credentials for this user and RP
147
+ const allCredentials = repository.loadCredentials();
148
+ const userCredentials = allCredentials.filter((cred) => cred.publicKeyCredentialSource.rpId.value === rpId &&
149
+ encode_utils_1.default.encodeBase64Url(cred.user.id) === encode_utils_1.default.encodeBase64Url(user.id));
150
+ if (userCredentials.length === 0) {
151
+ throw new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP2_ERR_NO_CREDENTIALS);
152
+ }
153
+ // Update user information for all matching credentials
154
+ for (const credential of userCredentials) {
155
+ const updatedCredential = {
156
+ ...credential,
157
+ user: {
158
+ ...user,
159
+ id: user.id, // Keep the same ID
160
+ },
161
+ };
162
+ repository.deleteCredential(credential);
163
+ repository.saveCredential(updatedCredential);
164
+ }
165
+ return {};
166
+ }
95
167
  authenticatorDeleteCredential(request) {
96
168
  const repository = this.params.credentialsRepository;
97
169
  if (!repository) {
@@ -1,4 +1,6 @@
1
1
  export declare const WebAuthnEmulatorGet = "webAuthnEmulatorGet";
2
2
  export declare const WebAuthnEmulatorCreate = "webAuthnEmulatorCreate";
3
3
  export declare const WebAuthnEmulatorSignalUnknownCredential = "webAuthnEmulatorSignalUnknownCredential";
4
+ export declare const WebAuthnEmulatorSignalAllAcceptedCredentials = "webAuthnEmulatorSignalAllAcceptedCredentials";
5
+ export declare const WebAuthnEmulatorSignalCurrentUserDetails = "webAuthnEmulatorSignalCurrentUserDetails";
4
6
  export declare const HookWebAuthnApis: string;
@@ -33,11 +33,13 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.HookWebAuthnApis = exports.WebAuthnEmulatorSignalUnknownCredential = exports.WebAuthnEmulatorCreate = exports.WebAuthnEmulatorGet = void 0;
36
+ exports.HookWebAuthnApis = exports.WebAuthnEmulatorSignalCurrentUserDetails = exports.WebAuthnEmulatorSignalAllAcceptedCredentials = exports.WebAuthnEmulatorSignalUnknownCredential = exports.WebAuthnEmulatorCreate = exports.WebAuthnEmulatorGet = void 0;
37
37
  const Model = __importStar(require("../webauthn/webauthn-model-json"));
38
38
  exports.WebAuthnEmulatorGet = "webAuthnEmulatorGet";
39
39
  exports.WebAuthnEmulatorCreate = "webAuthnEmulatorCreate";
40
40
  exports.WebAuthnEmulatorSignalUnknownCredential = "webAuthnEmulatorSignalUnknownCredential";
41
+ exports.WebAuthnEmulatorSignalAllAcceptedCredentials = "webAuthnEmulatorSignalAllAcceptedCredentials";
42
+ exports.WebAuthnEmulatorSignalCurrentUserDetails = "webAuthnEmulatorSignalCurrentUserDetails";
41
43
  const webAuthnModelExports = Object.values(Model)
42
44
  .map((m) => m.toString())
43
45
  .join("\n ");
@@ -65,5 +67,15 @@ exports.HookWebAuthnApis = `
65
67
  if (!options || !options.rpId || !options.credentialId) return;
66
68
  await window.${exports.WebAuthnEmulatorSignalUnknownCredential}(options);
67
69
  }
70
+
71
+ PublicKeyCredential.signalAllAcceptedCredentials = async (options) => {
72
+ if (!options || !options.rpId || !options.userId || !options.allAcceptedCredentialIds) return;
73
+ await window.${exports.WebAuthnEmulatorSignalAllAcceptedCredentials}(options);
74
+ }
75
+
76
+ PublicKeyCredential.signalCurrentUserDetails = async (options) => {
77
+ if (!options || !options.rpId || !options.userId) return;
78
+ await window.${exports.WebAuthnEmulatorSignalCurrentUserDetails}(options);
79
+ }
68
80
  })();
69
81
  `;
@@ -1,5 +1,5 @@
1
1
  import { AuthenticatorEmulator } from "../authenticator/authenticator-emulator";
2
- import { type AuthenticationResponseJSON, type CreatePublicKeyCredential, type PublicKeyCredentialCreationOptionsJSON, type PublicKeyCredentialRequestOptionsJSON, type RegistrationResponseJSON, type RequestPublicKeyCredential, type UnknownCredentialOptionsJSON } from "./webauthn-model-json";
2
+ import { type AllAcceptedCredentialsOptionsJSON, type AuthenticationResponseJSON, type CreatePublicKeyCredential, type CurrentUserDetailsOptionsJSON, type PublicKeyCredentialCreationOptionsJSON, type PublicKeyCredentialRequestOptionsJSON, type RegistrationResponseJSON, type RequestPublicKeyCredential, type UnknownCredentialOptionsJSON } from "./webauthn-model-json";
3
3
  export type AuthenticatorInfo = {
4
4
  version: string;
5
5
  aaguid: string;
@@ -24,6 +24,20 @@ export declare class WebAuthnEmulator {
24
24
  createJSON(origin: string, optionsJSON: PublicKeyCredentialCreationOptionsJSON): RegistrationResponseJSON;
25
25
  getAuthenticatorInfo(): AuthenticatorInfo;
26
26
  signalUnknownCredential(options: UnknownCredentialOptionsJSON): void;
27
+ /**
28
+ * Signal all accepted credentials for a user
29
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredential/signalAllAcceptedCredentials_static
30
+ */
31
+ signalAllAcceptedCredentials(options: AllAcceptedCredentialsOptionsJSON): void;
32
+ /**
33
+ * Process credentials for an RP and delete those not in the accepted list
34
+ */
35
+ private processCredentials;
36
+ /**
37
+ * Signal current user details
38
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredential/signalCurrentUserDetails_static
39
+ */
40
+ signalCurrentUserDetails(options: CurrentUserDetailsOptionsJSON): void;
27
41
  /** @see https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/get */
28
42
  get(origin: string, options: CredentialRequestOptions): RequestPublicKeyCredential;
29
43
  /** @see https://developer.mozilla.org/ja/docs/Web/API/CredentialsContainer/create */
@@ -50,14 +50,83 @@ class WebAuthnEmulator {
50
50
  }
51
51
  signalUnknownCredential(options) {
52
52
  const credentialId = (0, webauthn_model_json_1.decodeBase64Url)(options.credentialId);
53
- const request = (0, ctap_model_1.packCredentialManagementRequest)({
53
+ this.authenticator.command((0, ctap_model_1.packCredentialManagementRequest)({
54
54
  subCommand: ctap_model_1.CREDENTIAL_MANAGEMENT_SUBCOMMAND.deleteCredential,
55
55
  subCommandParams: {
56
56
  credentialId: encode_utils_1.default.bufferSourceToUint8Array(credentialId),
57
57
  rpId: options.rpId,
58
58
  },
59
- });
60
- this.authenticator.command(request);
59
+ }));
60
+ }
61
+ /**
62
+ * Signal all accepted credentials for a user
63
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredential/signalAllAcceptedCredentials_static
64
+ */
65
+ signalAllAcceptedCredentials(options) {
66
+ // Create a set of accepted credential IDs for quick lookup
67
+ const acceptedCredentialIds = new Set(options.allAcceptedCredentialIds);
68
+ // Start enumerating credentials for the specified RP
69
+ const beginResponse = (0, ctap_model_1.unpackCredentialManagementResponse)(this.authenticator.command((0, ctap_model_1.packCredentialManagementRequest)({
70
+ subCommand: ctap_model_1.CREDENTIAL_MANAGEMENT_SUBCOMMAND.enumerateCredentialsBegin,
71
+ subCommandParams: {
72
+ rpId: options.rpId,
73
+ },
74
+ })));
75
+ // Get the total number of credentials
76
+ const totalCredentials = beginResponse.totalCredentials ?? 0;
77
+ // Get all credentials for the RP and delete those not in the accepted list
78
+ this.processCredentials(options.rpId, options.userId, acceptedCredentialIds, totalCredentials);
79
+ }
80
+ /**
81
+ * Process credentials for an RP and delete those not in the accepted list
82
+ */
83
+ processCredentials(rpId, userId, acceptedCredentialIds, totalCredentials) {
84
+ // Process each credential
85
+ for (let i = 0; i < totalCredentials; i++) {
86
+ // Get next credential and unpack the response
87
+ const credResponse = (0, ctap_model_1.unpackCredentialManagementResponse)(this.authenticator.command((0, ctap_model_1.packCredentialManagementRequest)({
88
+ subCommand: ctap_model_1.CREDENTIAL_MANAGEMENT_SUBCOMMAND.enumerateCredentialsGetNextCredential,
89
+ })));
90
+ // Skip if this is not for the user we're looking for
91
+ if (credResponse.user && encode_utils_1.default.encodeBase64Url(credResponse.user.id) !== userId) {
92
+ continue;
93
+ }
94
+ // Delete credential if it's not in the accepted list
95
+ if (credResponse.credentialID) {
96
+ const credentialId = encode_utils_1.default.encodeBase64Url(credResponse.credentialID);
97
+ if (!acceptedCredentialIds.has(credentialId)) {
98
+ // Delete the credential directly
99
+ this.authenticator.command((0, ctap_model_1.packCredentialManagementRequest)({
100
+ subCommand: ctap_model_1.CREDENTIAL_MANAGEMENT_SUBCOMMAND.deleteCredential,
101
+ subCommandParams: {
102
+ credentialId: credResponse.credentialID,
103
+ rpId: rpId,
104
+ },
105
+ }));
106
+ }
107
+ }
108
+ }
109
+ }
110
+ /**
111
+ * Signal current user details
112
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredential/signalCurrentUserDetails_static
113
+ */
114
+ signalCurrentUserDetails(options) {
115
+ const userId = (0, webauthn_model_json_1.decodeBase64Url)(options.userId);
116
+ // Create user object with updated information
117
+ const user = {
118
+ id: userId,
119
+ name: options.name,
120
+ displayName: options.displayName,
121
+ };
122
+ // Update user information using the credential management API
123
+ this.authenticator.command((0, ctap_model_1.packCredentialManagementRequest)({
124
+ subCommand: ctap_model_1.CREDENTIAL_MANAGEMENT_SUBCOMMAND.updateUserInformation,
125
+ subCommandParams: {
126
+ rpId: options.rpId,
127
+ user: user,
128
+ },
129
+ }));
61
130
  }
62
131
  /** @see https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/get */
63
132
  get(origin, options) {
@@ -95,6 +95,17 @@ export interface UnknownCredentialOptionsJSON {
95
95
  rpId: string;
96
96
  credentialId: Base64urlString;
97
97
  }
98
+ export interface AllAcceptedCredentialsOptionsJSON {
99
+ rpId: string;
100
+ userId: Base64urlString;
101
+ allAcceptedCredentialIds: Base64urlString[];
102
+ }
103
+ export interface CurrentUserDetailsOptionsJSON {
104
+ rpId: string;
105
+ userId: Base64urlString;
106
+ name: string;
107
+ displayName: string;
108
+ }
98
109
  export declare function parseAuthenticationResponseFromJSON(options: AuthenticationResponseJSON): RequestPublicKeyCredential;
99
110
  export declare function toPublicKeyCredentialDescriptorJSON(credential: PublicKeyCredentialDescriptor): PublicKeyCredentialDescriptorJSON;
100
111
  export declare function parsePublicKeyCredentialDescriptorFromJSON(credential: PublicKeyCredentialDescriptorJSON): PublicKeyCredentialDescriptor;
@@ -26,8 +26,8 @@ class RpId {
26
26
  }
27
27
  /** @see https://www.w3.org/TR/webauthn-3/#sctn-validating-origin */
28
28
  validate(origin) {
29
- const parsedOrigin = (0, tldts_1.parse)(origin);
30
- const parsedRpId = (0, tldts_1.parse)(this.value);
29
+ const parsedOrigin = (0, tldts_1.parse)(origin, { validHosts: ["localhost"] });
30
+ const parsedRpId = (0, tldts_1.parse)(this.value, { validHosts: ["localhost"] });
31
31
  return Boolean(parsedOrigin.domain &&
32
32
  parsedOrigin.subdomain !== null &&
33
33
  parsedRpId.subdomain !== null &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nid-webauthn-emulator",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "keywords": [
5
5
  "test",
6
6
  "e2e",
@@ -19,15 +19,15 @@
19
19
  "@jest/globals": "^29.7.0",
20
20
  "@simplewebauthn/server": "^13.1.1",
21
21
  "@tsconfig/recommended": "^1.0.8",
22
- "@types/node": "^22.13.4",
22
+ "@types/node": "^22.14.1",
23
23
  "jest": "^29.7.0",
24
- "ts-jest": "^29.2.5",
24
+ "ts-jest": "^29.3.2",
25
25
  "ts-node": "^10.9.2",
26
- "typescript": "^5.7.3"
26
+ "typescript": "^5.8.3"
27
27
  },
28
28
  "dependencies": {
29
29
  "cbor": "^10.0.3",
30
- "tldts": "^6.1.77"
30
+ "tldts": "^7.0.0"
31
31
  },
32
32
  "scripts": {
33
33
  "check": "biome check .",