@structured-id/ui-core 1.3.0 → 3.0.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/README.md +22 -10
- package/package.json +25 -43
- package/src/components/SidStatusBadge.vue +1 -5
- package/src/composables/useAccountTransport.test.ts +2 -1
- package/src/composables/useGrpc.test.ts +17 -14
- package/src/composables/useGrpc.ts +4 -2
- package/src/index.ts +2 -120
- package/src/profile/components/ConsentManager.vue +5 -1
- package/src/profile/components/ProfileEditor.vue +1 -1
- package/src/profile/components/SecuritySettings.vue +5 -1
- package/src/profile/components/SessionList.vue +2 -1
- package/src/profile/components/SidRegistrationForm.test.ts +173 -1
- package/src/profile/components/SidRegistrationForm.vue +88 -32
- package/src/profile/components/index.ts +1 -4
- package/src/profile/composables/index.ts +1 -0
- package/src/profile/composables/operationPlan.test.ts +155 -0
- package/src/profile/composables/operationPlan.ts +218 -0
- package/src/profile/composables/useAccountApi.ts +4 -4
- package/src/profile/composables/useAuth.test.ts +312 -31
- package/src/profile/composables/useAuth.ts +176 -30
- package/src/profile/composables/useAuthApi.opaque.test.ts +13 -6
- package/src/profile/composables/useAuthApi.ts +40 -18
- package/src/profile/composables/useCurrentPasswordRequirement.test.ts +196 -0
- package/src/profile/composables/useCurrentPasswordRequirement.ts +95 -0
- package/src/profile/composables/useEnrollmentApi.test.ts +7 -3
- package/src/profile/composables/useEnrollmentApi.ts +3 -5
- package/src/profile/composables/useIdentityApi.ts +4 -4
- package/src/profile/composables/useSecurityLevel.test.ts +1 -1
- package/src/profile/composables/useSecurityLevel.ts +1 -1
- package/src/quasar/SidPasswordCeremony.test.ts +253 -0
- package/src/quasar/SidPasswordCeremony.vue +456 -0
- package/src/quasar/index.ts +2 -0
- package/src/utils/rpcRefusal.test.ts +4 -4
- package/src/utils/rpcRefusal.ts +8 -4
- package/dist/browser-vp/index.cjs +0 -349
- package/dist/browser-vp/index.d.cts +0 -276
- package/dist/browser-vp/index.d.ts +0 -276
- package/dist/browser-vp/index.js +0 -349
- package/dist/chunk-C3Q766CY.cjs +0 -125
- package/dist/chunk-J5YXRXB5.js +0 -125
- package/dist/index.cjs +0 -23581
- package/dist/index.d.cts +0 -11539
- package/dist/index.d.ts +0 -11539
- package/dist/index.js +0 -23581
- package/dist/storage-4CB2JTLW.cjs +0 -22
- package/dist/storage-THRWREZP.js +0 -22
- package/src/browser-vp/crypto.ts +0 -253
- package/src/browser-vp/index.test.ts +0 -10
- package/src/browser-vp/index.ts +0 -116
- package/src/browser-vp/prf.ts +0 -179
- package/src/browser-vp/storage.test.ts +0 -9
- package/src/browser-vp/storage.ts +0 -163
- package/src/browser-vp/types.ts +0 -95
- package/src/browser-vp/vp.test.ts +0 -43
- package/src/browser-vp/vp.ts +0 -111
- package/src/generated/google/api/http.ts +0 -687
- package/src/generated/google/api/httpbody.ts +0 -159
- package/src/generated/google/protobuf/any.ts +0 -326
- package/src/generated/google/protobuf/descriptor.ts +0 -4865
- package/src/generated/google/protobuf/duration.ts +0 -230
- package/src/generated/google/protobuf/empty.ts +0 -94
- package/src/generated/google/protobuf/struct.ts +0 -484
- package/src/generated/google/protobuf/timestamp.ts +0 -287
- package/src/generated/google/rpc/error_details.ts +0 -1386
- package/src/generated/google/rpc/status.ts +0 -127
- package/src/generated/sid/v1/account/account.client.ts +0 -527
- package/src/generated/sid/v1/account/account.ts +0 -3261
- package/src/generated/sid/v1/admin/admin.client.ts +0 -139
- package/src/generated/sid/v1/admin/admin.ts +0 -781
- package/src/generated/sid/v1/admin/admin_roles.client.ts +0 -181
- package/src/generated/sid/v1/admin/admin_roles.ts +0 -1606
- package/src/generated/sid/v1/admin/analytics.client.ts +0 -106
- package/src/generated/sid/v1/admin/analytics.ts +0 -1017
- package/src/generated/sid/v1/admin/branding.client.ts +0 -166
- package/src/generated/sid/v1/admin/branding.ts +0 -1906
- package/src/generated/sid/v1/admin/enrollment.client.ts +0 -177
- package/src/generated/sid/v1/admin/enrollment.ts +0 -1766
- package/src/generated/sid/v1/admin/events.client.ts +0 -195
- package/src/generated/sid/v1/admin/events.ts +0 -1900
- package/src/generated/sid/v1/admin/flow.client.ts +0 -183
- package/src/generated/sid/v1/admin/flow.ts +0 -1552
- package/src/generated/sid/v1/admin/keys.client.ts +0 -107
- package/src/generated/sid/v1/admin/keys.ts +0 -931
- package/src/generated/sid/v1/admin/login_experience.client.ts +0 -160
- package/src/generated/sid/v1/admin/login_experience.ts +0 -1061
- package/src/generated/sid/v1/admin/migration.client.ts +0 -127
- package/src/generated/sid/v1/admin/migration.ts +0 -1948
- package/src/generated/sid/v1/admin/notifications.client.ts +0 -134
- package/src/generated/sid/v1/admin/notifications.ts +0 -1273
- package/src/generated/sid/v1/admin/operations.client.ts +0 -241
- package/src/generated/sid/v1/admin/operations.ts +0 -2633
- package/src/generated/sid/v1/admin/organizations.client.ts +0 -57
- package/src/generated/sid/v1/admin/organizations.ts +0 -332
- package/src/generated/sid/v1/admin/provisioning.client.ts +0 -232
- package/src/generated/sid/v1/admin/provisioning.ts +0 -1383
- package/src/generated/sid/v1/admin/scopes.client.ts +0 -171
- package/src/generated/sid/v1/admin/scopes.ts +0 -1324
- package/src/generated/sid/v1/admin/security.client.ts +0 -410
- package/src/generated/sid/v1/admin/security.ts +0 -4077
- package/src/generated/sid/v1/admin/sessions.client.ts +0 -114
- package/src/generated/sid/v1/admin/sessions.ts +0 -923
- package/src/generated/sid/v1/admin/settings.client.ts +0 -351
- package/src/generated/sid/v1/admin/settings.ts +0 -2458
- package/src/generated/sid/v1/admin/user_profile.client.ts +0 -57
- package/src/generated/sid/v1/admin/user_profile.ts +0 -730
- package/src/generated/sid/v1/admin/webhooks.client.ts +0 -269
- package/src/generated/sid/v1/admin/webhooks.ts +0 -2804
- package/src/generated/sid/v1/attestation/attestation.client.ts +0 -115
- package/src/generated/sid/v1/attestation/attestation.ts +0 -813
- package/src/generated/sid/v1/authn/auth.client.ts +0 -943
- package/src/generated/sid/v1/authn/auth.ts +0 -9719
- package/src/generated/sid/v1/authn/flows.client.ts +0 -413
- package/src/generated/sid/v1/authn/flows.ts +0 -3205
- package/src/generated/sid/v1/authn/issuer.client.ts +0 -489
- package/src/generated/sid/v1/authn/issuer.ts +0 -1600
- package/src/generated/sid/v1/authn/password_history.client.ts +0 -57
- package/src/generated/sid/v1/authn/password_history.ts +0 -578
- package/src/generated/sid/v1/authn/system_integration.client.ts +0 -73
- package/src/generated/sid/v1/authn/system_integration.ts +0 -218
- package/src/generated/sid/v1/authz/authz.client.ts +0 -459
- package/src/generated/sid/v1/authz/authz.ts +0 -6524
- package/src/generated/sid/v1/authz/forward_auth.client.ts +0 -97
- package/src/generated/sid/v1/authz/forward_auth.ts +0 -79
- package/src/generated/sid/v1/authz/governance.client.ts +0 -102
- package/src/generated/sid/v1/authz/governance.ts +0 -875
- package/src/generated/sid/v1/common/errors.ts +0 -710
- package/src/generated/sid/v1/common/pagination.ts +0 -152
- package/src/generated/sid/v1/common/transport.ts +0 -227
- package/src/generated/sid/v1/common/types.ts +0 -151
- package/src/generated/sid/v1/events/event_stream.client.ts +0 -67
- package/src/generated/sid/v1/events/event_stream.ts +0 -375
- package/src/generated/sid/v1/federation/upstream.client.ts +0 -142
- package/src/generated/sid/v1/federation/upstream.ts +0 -1736
- package/src/generated/sid/v1/identity/identity.client.ts +0 -467
- package/src/generated/sid/v1/identity/identity.ts +0 -5676
- package/src/generated/sid/v1/ids/ids.ts +0 -395
- package/src/generated/sid/v1/machine/machine_user.client.ts +0 -219
- package/src/generated/sid/v1/machine/machine_user.ts +0 -2364
- package/src/generated/sid/v1/machine/pat.client.ts +0 -130
- package/src/generated/sid/v1/machine/pat.ts +0 -1083
- package/src/generated/sid/v1/projects/projects.client.ts +0 -404
- package/src/generated/sid/v1/projects/projects.ts +0 -4446
- package/src/generated/sid/v1/scim/scim.client.ts +0 -520
- package/src/generated/sid/v1/scim/scim.ts +0 -3182
- package/src/generated/sid/v1/test/test.client.ts +0 -232
- package/src/generated/sid/v1/test/test.ts +0 -1136
- package/src/transport/adaptive.test.ts +0 -728
- package/src/transport/adaptive.ts +0 -308
- package/src/transport/framing.test.ts +0 -227
- package/src/transport/framing.ts +0 -98
- package/src/transport/webtransport.test.ts +0 -728
- package/src/transport/webtransport.ts +0 -323
|
@@ -2,7 +2,9 @@ import { describe, it, expect, vi, beforeEach, type Mock } from "vitest";
|
|
|
2
2
|
import {
|
|
3
3
|
createAuth,
|
|
4
4
|
type ZkppClientApi,
|
|
5
|
+
DEFAULT_STEP_MS,
|
|
5
6
|
type PasswordOperationProgress,
|
|
7
|
+
WrongCurrentPasswordError,
|
|
6
8
|
} from "./useAuth";
|
|
7
9
|
|
|
8
10
|
// Mock the gRPC wrappers
|
|
@@ -83,6 +85,12 @@ const ANSWER = {
|
|
|
83
85
|
evaluatedElement: new Uint8Array([5]),
|
|
84
86
|
proof: { challenge: new Uint8Array([6]), response: new Uint8Array([7]) },
|
|
85
87
|
};
|
|
88
|
+
/** The proof as the finish sends it: the evaluator's proof relayed. */
|
|
89
|
+
const WIRE_PROOF = {
|
|
90
|
+
zkppProof: PROOF.proof,
|
|
91
|
+
instances: PROOF.instances,
|
|
92
|
+
evaluationProofs: [ANSWER.proof],
|
|
93
|
+
};
|
|
86
94
|
|
|
87
95
|
function fakeClient(): ZkppClientApi {
|
|
88
96
|
return {
|
|
@@ -100,12 +108,12 @@ function fakeClient(): ZkppClientApi {
|
|
|
100
108
|
}
|
|
101
109
|
|
|
102
110
|
let client: ZkppClientApi;
|
|
103
|
-
let loader: Mock<
|
|
111
|
+
let loader: Mock<() => Promise<ZkppClientApi>>;
|
|
104
112
|
|
|
105
113
|
beforeEach(() => {
|
|
106
114
|
vi.clearAllMocks();
|
|
107
115
|
client = fakeClient();
|
|
108
|
-
loader = vi.fn<
|
|
116
|
+
loader = vi.fn<() => Promise<ZkppClientApi>>().mockResolvedValue(client);
|
|
109
117
|
api.evaluate.mockResolvedValue([ANSWER]);
|
|
110
118
|
});
|
|
111
119
|
|
|
@@ -199,10 +207,7 @@ describe("createAuth register", () => {
|
|
|
199
207
|
"reg-state",
|
|
200
208
|
RESPONSE,
|
|
201
209
|
);
|
|
202
|
-
expect(api.regFinish).toHaveBeenCalledWith(OPERATION, RECORD,
|
|
203
|
-
zkppProof: PROOF.proof,
|
|
204
|
-
instances: PROOF.instances,
|
|
205
|
-
});
|
|
210
|
+
expect(api.regFinish).toHaveBeenCalledWith(OPERATION, RECORD, WIRE_PROOF);
|
|
206
211
|
expect(result).toEqual({
|
|
207
212
|
profileId: "profile-uuid",
|
|
208
213
|
credentialId: "cred-uuid",
|
|
@@ -227,16 +232,45 @@ describe("createAuth register", () => {
|
|
|
227
232
|
expect(api.regFinish).toHaveBeenCalledWith(OPERATION, RECORD, undefined);
|
|
228
233
|
});
|
|
229
234
|
|
|
230
|
-
|
|
235
|
+
// The user follows the operation by its steps: on this device, the
|
|
236
|
+
// comparison with previous passwords, the proof (filled by the prover's own
|
|
237
|
+
// progress) and the server's check, in that order and never backwards,
|
|
238
|
+
// ending complete.
|
|
239
|
+
it("reports each step, with the prover filling the proof's share", async () => {
|
|
240
|
+
// Step times learned by earlier tests in this browser would move the shares.
|
|
241
|
+
localStorage.clear();
|
|
231
242
|
vi.mocked(client.prove).mockImplementation(async (_pw, _start, opts) => {
|
|
232
|
-
opts.onProgress?.({ fraction: 0.5, label: "
|
|
243
|
+
opts.onProgress?.({ fraction: 0.5, label: "Computing quotient" });
|
|
233
244
|
return PROOF;
|
|
234
245
|
});
|
|
235
246
|
const seen: PasswordOperationProgress[] = [];
|
|
236
247
|
const { register } = createAuth(loader);
|
|
237
248
|
await register("bob@test.com", "pw", undefined, (p) => seen.push(p));
|
|
238
|
-
|
|
239
|
-
|
|
249
|
+
const ms = DEFAULT_STEP_MS;
|
|
250
|
+
const total = ms.protect + ms.compare + ms.prove + ms.verify;
|
|
251
|
+
const proveStart = (ms.protect + ms.compare) / total;
|
|
252
|
+
|
|
253
|
+
expect(seen.map((p) => p.step)).toEqual([
|
|
254
|
+
"protect",
|
|
255
|
+
"compare",
|
|
256
|
+
"prove",
|
|
257
|
+
"prove",
|
|
258
|
+
"verify",
|
|
259
|
+
"verify",
|
|
260
|
+
]);
|
|
261
|
+
[
|
|
262
|
+
0,
|
|
263
|
+
ms.protect / total,
|
|
264
|
+
proveStart,
|
|
265
|
+
proveStart + ms.prove / total / 2,
|
|
266
|
+
proveStart + ms.prove / total,
|
|
267
|
+
1,
|
|
268
|
+
].forEach((expected, i) =>
|
|
269
|
+
expect(seen[i].fraction).toBeCloseTo(expected, 10),
|
|
270
|
+
);
|
|
271
|
+
expect(seen[3].label, "the prover's stage names stay inside").toBe(
|
|
272
|
+
"Building the proof",
|
|
273
|
+
);
|
|
240
274
|
});
|
|
241
275
|
|
|
242
276
|
it("refuses to go on when the server prepared no operation", async () => {
|
|
@@ -316,21 +350,31 @@ describe("createAuth register", () => {
|
|
|
316
350
|
});
|
|
317
351
|
|
|
318
352
|
describe("createAuth changePassword", () => {
|
|
353
|
+
const KE2 = new Uint8Array([4, 4]);
|
|
319
354
|
beforeEach(() => {
|
|
320
|
-
api.challenge.mockResolvedValue({
|
|
355
|
+
api.challenge.mockResolvedValue({
|
|
356
|
+
history: wireContext(),
|
|
357
|
+
credentialResponse: new Uint8Array(),
|
|
358
|
+
});
|
|
321
359
|
api.execute.mockResolvedValue({ registrationResponse: RESPONSE });
|
|
322
|
-
api.changeFinish.mockResolvedValue(
|
|
360
|
+
api.changeFinish.mockResolvedValue();
|
|
323
361
|
});
|
|
324
362
|
|
|
325
|
-
it("
|
|
363
|
+
it("fixes the OPAQUE start in the challenge, executes, proves and finishes", async () => {
|
|
326
364
|
const { changePassword } = createAuth(loader);
|
|
327
|
-
await changePassword(
|
|
365
|
+
await changePassword({
|
|
366
|
+
credentialId: "cred-1",
|
|
367
|
+
newPassword: "N3wP@ssword!",
|
|
368
|
+
});
|
|
328
369
|
|
|
329
|
-
expect(
|
|
370
|
+
expect(client.registrationStart).toHaveBeenCalledOnce();
|
|
371
|
+
expect(client.registrationStart).toHaveBeenCalledWith("N3wP@ssword!");
|
|
372
|
+
expect(api.challenge).toHaveBeenCalledWith("cred-1", START.request);
|
|
373
|
+
expect(client.loginStart).not.toHaveBeenCalled();
|
|
330
374
|
expect(api.execute).toHaveBeenCalledWith(
|
|
331
375
|
OPERATION,
|
|
332
376
|
"cred-1",
|
|
333
|
-
|
|
377
|
+
new Uint8Array(),
|
|
334
378
|
);
|
|
335
379
|
expect(api.evaluate).toHaveBeenCalledWith(OPERATION, HISTORY.blinded);
|
|
336
380
|
expect(client.prove).toHaveBeenCalledWith(
|
|
@@ -338,28 +382,267 @@ describe("createAuth changePassword", () => {
|
|
|
338
382
|
START,
|
|
339
383
|
expect.objectContaining({ context: clientContext }),
|
|
340
384
|
);
|
|
341
|
-
expect(api.changeFinish).toHaveBeenCalledWith(
|
|
342
|
-
|
|
343
|
-
|
|
385
|
+
expect(api.changeFinish).toHaveBeenCalledWith(
|
|
386
|
+
OPERATION,
|
|
387
|
+
"cred-1",
|
|
388
|
+
RECORD,
|
|
389
|
+
WIRE_PROOF,
|
|
390
|
+
);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// The final report comes after the server committed the change: a progress
|
|
394
|
+
// observer that throws on it cannot turn the committed change into a
|
|
395
|
+
// failure the user would retry with a password that no longer applies.
|
|
396
|
+
it("resolves a committed change when the final progress observer throws", async () => {
|
|
397
|
+
const { changePassword } = createAuth(loader);
|
|
398
|
+
await expect(
|
|
399
|
+
changePassword(
|
|
400
|
+
{ credentialId: "cred-1", newPassword: "N3wP@ssword!" },
|
|
401
|
+
(p) => {
|
|
402
|
+
if (p.fraction === 1) throw new Error("gauge unmounted");
|
|
403
|
+
},
|
|
404
|
+
),
|
|
405
|
+
).resolves.toBeUndefined();
|
|
406
|
+
expect(api.changeFinish).toHaveBeenCalledOnce();
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// A change without the current password reports its first step before
|
|
410
|
+
// the new password's OPAQUE start and the challenge, and enters it once,
|
|
411
|
+
// so the gauge starts at once and the step's measured time covers them.
|
|
412
|
+
it("reports the protection step before preparing an unconfirmed change", async () => {
|
|
413
|
+
const order: string[] = [];
|
|
414
|
+
vi.mocked(client.registrationStart).mockImplementation(async () => {
|
|
415
|
+
order.push("registrationStart");
|
|
416
|
+
return START;
|
|
344
417
|
});
|
|
418
|
+
const { changePassword } = createAuth(loader);
|
|
419
|
+
await changePassword(
|
|
420
|
+
{ credentialId: "cred-1", newPassword: "N3wP@ssword!" },
|
|
421
|
+
(p) => {
|
|
422
|
+
if (p.fraction === 0 || order.at(-1) !== `step:${p.step}`)
|
|
423
|
+
order.push(`step:${p.step}`);
|
|
424
|
+
},
|
|
425
|
+
);
|
|
426
|
+
expect(order.slice(0, 2)).toEqual(["step:protect", "registrationStart"]);
|
|
427
|
+
expect(order.filter((e) => e === "step:protect")).toHaveLength(1);
|
|
345
428
|
});
|
|
346
429
|
|
|
347
|
-
|
|
348
|
-
|
|
430
|
+
// The current password is proved with a sign-in carried by the change's
|
|
431
|
+
// own steps: KE1 in the challenge beside the new password's request, KE3
|
|
432
|
+
// in execute, before any proof work. The sign-in runs under the change's
|
|
433
|
+
// context (label, operation, digest of that request), so it confirms this
|
|
434
|
+
// change only and a relayed ordinary sign-in cannot stand for it.
|
|
435
|
+
it("proves the current password inside the change, under its context", async () => {
|
|
436
|
+
api.challenge.mockResolvedValue({
|
|
437
|
+
history: wireContext(),
|
|
438
|
+
credentialResponse: KE2,
|
|
439
|
+
});
|
|
440
|
+
const seen: PasswordOperationProgress[] = [];
|
|
349
441
|
const { changePassword } = createAuth(loader);
|
|
350
|
-
await
|
|
351
|
-
|
|
442
|
+
await changePassword(
|
|
443
|
+
{
|
|
444
|
+
credentialId: "cred-1",
|
|
445
|
+
newPassword: "N3wP@ssword!",
|
|
446
|
+
currentPassword: "0ldP@ssword!",
|
|
447
|
+
},
|
|
448
|
+
(p) => seen.push(p),
|
|
352
449
|
);
|
|
353
|
-
|
|
450
|
+
|
|
451
|
+
const expectedContext = new Uint8Array([
|
|
452
|
+
...new TextEncoder().encode("SID-PASSWORD-CHANGE-v1"),
|
|
453
|
+
...OPERATION,
|
|
454
|
+
...new Uint8Array(
|
|
455
|
+
await crypto.subtle.digest("SHA-256", new Uint8Array(START.request)),
|
|
456
|
+
),
|
|
457
|
+
]);
|
|
458
|
+
expect(client.registrationStart).toHaveBeenCalledOnce();
|
|
459
|
+
expect(client.loginStart).toHaveBeenCalledWith("0ldP@ssword!");
|
|
460
|
+
expect(api.challenge).toHaveBeenCalledWith(
|
|
461
|
+
"cred-1",
|
|
462
|
+
START.request,
|
|
463
|
+
new Uint8Array([1, 2, 3]),
|
|
464
|
+
);
|
|
465
|
+
expect(client.loginFinish).toHaveBeenCalledWith(
|
|
466
|
+
"0ldP@ssword!",
|
|
467
|
+
"login-state",
|
|
468
|
+
KE2,
|
|
469
|
+
expectedContext,
|
|
470
|
+
);
|
|
471
|
+
expect(api.execute).toHaveBeenCalledWith(
|
|
472
|
+
OPERATION,
|
|
473
|
+
"cred-1",
|
|
474
|
+
new Uint8Array([7, 8, 9]),
|
|
475
|
+
);
|
|
476
|
+
expect(seen[0]).toMatchObject({ step: "protect", fraction: 0 });
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
// The new password's OPAQUE start is protection work and is measured as
|
|
480
|
+
// such; confirmation covers only the current password's sign-in, so its
|
|
481
|
+
// time does not teach the protection estimate of registrations and resets.
|
|
482
|
+
it("measures the new password's start as protection, the sign-in as confirmation", async () => {
|
|
483
|
+
api.challenge.mockResolvedValue({
|
|
484
|
+
history: wireContext(),
|
|
485
|
+
credentialResponse: KE2,
|
|
486
|
+
});
|
|
487
|
+
const order: string[] = [];
|
|
488
|
+
vi.mocked(client.registrationStart).mockImplementation(async () => {
|
|
489
|
+
order.push("registrationStart");
|
|
490
|
+
return START;
|
|
491
|
+
});
|
|
492
|
+
vi.mocked(client.loginStart).mockImplementation(async () => {
|
|
493
|
+
order.push("loginStart");
|
|
494
|
+
return { request: new Uint8Array([1, 2, 3]), state: "login-state" };
|
|
495
|
+
});
|
|
496
|
+
const { changePassword } = createAuth(loader);
|
|
497
|
+
await changePassword(
|
|
498
|
+
{
|
|
499
|
+
credentialId: "cred-1",
|
|
500
|
+
newPassword: "N3wP@ssword!",
|
|
501
|
+
currentPassword: "0ldP@ssword!",
|
|
502
|
+
},
|
|
503
|
+
(p) => {
|
|
504
|
+
if (order.at(-1) !== `step:${p.step}`) order.push(`step:${p.step}`);
|
|
505
|
+
},
|
|
506
|
+
);
|
|
507
|
+
expect(order.slice(0, 4)).toEqual([
|
|
508
|
+
"step:protect",
|
|
509
|
+
"registrationStart",
|
|
510
|
+
"step:confirm",
|
|
511
|
+
"loginStart",
|
|
512
|
+
]);
|
|
513
|
+
expect(order.filter((e) => e === "step:protect")).toHaveLength(1);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
// A client that stops while the challenge is in flight keeps the operation:
|
|
517
|
+
// its start belongs to that client, so the proof and the record are never
|
|
518
|
+
// asked of a newly loaded one.
|
|
519
|
+
it("finishes the change on the client that started it", async () => {
|
|
520
|
+
const fresh = fakeClient();
|
|
521
|
+
loader.mockReset();
|
|
522
|
+
loader.mockResolvedValueOnce(client).mockResolvedValueOnce(fresh);
|
|
523
|
+
api.challenge.mockImplementation(async () => {
|
|
524
|
+
(client as { stopped: boolean }).stopped = true;
|
|
525
|
+
return { history: wireContext(), credentialResponse: new Uint8Array() };
|
|
526
|
+
});
|
|
527
|
+
const { changePassword } = createAuth(loader);
|
|
528
|
+
await changePassword({
|
|
529
|
+
credentialId: "cred-1",
|
|
530
|
+
newPassword: "N3wP@ssword!",
|
|
531
|
+
});
|
|
532
|
+
expect(loader).toHaveBeenCalledOnce();
|
|
533
|
+
expect(fresh.prove).not.toHaveBeenCalled();
|
|
534
|
+
expect(fresh.registrationFinish).not.toHaveBeenCalled();
|
|
535
|
+
expect(client.prove).toHaveBeenCalledOnce();
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
// A wrong current password fails in the client's own sign-in; the server
|
|
539
|
+
// already counted the guess when it issued KE2, so the change stops here
|
|
540
|
+
// with an error the form can name, and nothing more is sent.
|
|
541
|
+
it("stops at a wrong current password", async () => {
|
|
542
|
+
api.challenge.mockResolvedValue({
|
|
543
|
+
history: wireContext(),
|
|
544
|
+
credentialResponse: KE2,
|
|
545
|
+
});
|
|
546
|
+
const refused = new Error("invalid login");
|
|
547
|
+
refused.name = "ZkppInvalidLoginError";
|
|
548
|
+
vi.mocked(client.loginFinish).mockRejectedValue(refused);
|
|
549
|
+
const { changePassword } = createAuth(loader);
|
|
550
|
+
await expect(
|
|
551
|
+
changePassword({
|
|
552
|
+
credentialId: "cred-1",
|
|
553
|
+
newPassword: "N3wP@ssword!",
|
|
554
|
+
currentPassword: "wrong",
|
|
555
|
+
}),
|
|
556
|
+
).rejects.toBeInstanceOf(WrongCurrentPasswordError);
|
|
557
|
+
expect(api.execute).not.toHaveBeenCalled();
|
|
558
|
+
expect(client.prove).not.toHaveBeenCalled();
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
// Only a sign-in the client refused is a wrong password: a malformed
|
|
562
|
+
// response or another fault comes through as itself, so the form does not
|
|
563
|
+
// ask for a password that was never judged.
|
|
564
|
+
it("reports a sign-in failure other than a refusal as itself", async () => {
|
|
565
|
+
api.challenge.mockResolvedValue({
|
|
566
|
+
history: wireContext(),
|
|
567
|
+
credentialResponse: KE2,
|
|
568
|
+
});
|
|
569
|
+
const fault = new Error("response: invalid length");
|
|
570
|
+
vi.mocked(client.loginFinish).mockRejectedValue(fault);
|
|
571
|
+
const { changePassword } = createAuth(loader);
|
|
572
|
+
await expect(
|
|
573
|
+
changePassword({
|
|
574
|
+
credentialId: "cred-1",
|
|
575
|
+
newPassword: "N3wP@ssword!",
|
|
576
|
+
currentPassword: "0ldP@ssword!",
|
|
577
|
+
}),
|
|
578
|
+
).rejects.toBe(fault);
|
|
354
579
|
expect(api.execute).not.toHaveBeenCalled();
|
|
355
580
|
});
|
|
356
581
|
|
|
582
|
+
// An empty current password is a password the user gave, not an omitted
|
|
583
|
+
// one: the change proves it and refuses it, instead of going on unconfirmed.
|
|
584
|
+
it("proves an empty current password instead of skipping the proof", async () => {
|
|
585
|
+
api.challenge.mockResolvedValue({
|
|
586
|
+
history: wireContext(),
|
|
587
|
+
credentialResponse: KE2,
|
|
588
|
+
});
|
|
589
|
+
const refused = new Error("invalid login");
|
|
590
|
+
refused.name = "ZkppInvalidLoginError";
|
|
591
|
+
vi.mocked(client.loginFinish).mockRejectedValue(refused);
|
|
592
|
+
const { changePassword } = createAuth(loader);
|
|
593
|
+
await expect(
|
|
594
|
+
changePassword({
|
|
595
|
+
credentialId: "cred-1",
|
|
596
|
+
newPassword: "N3wP@ssword!",
|
|
597
|
+
currentPassword: "",
|
|
598
|
+
}),
|
|
599
|
+
).rejects.toBeInstanceOf(WrongCurrentPasswordError);
|
|
600
|
+
expect(client.loginStart).toHaveBeenCalledWith("");
|
|
601
|
+
expect(api.execute).not.toHaveBeenCalled();
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
// A client that stopped answering is a fault, not a wrong password: its
|
|
605
|
+
// own error comes through.
|
|
606
|
+
it("reports a stopped client as itself", async () => {
|
|
607
|
+
api.challenge.mockResolvedValue({
|
|
608
|
+
history: wireContext(),
|
|
609
|
+
credentialResponse: KE2,
|
|
610
|
+
});
|
|
611
|
+
const stopped = new Error("the worker stopped");
|
|
612
|
+
vi.mocked(client.loginFinish).mockImplementation(async () => {
|
|
613
|
+
(client as { stopped: boolean }).stopped = true;
|
|
614
|
+
throw stopped;
|
|
615
|
+
});
|
|
616
|
+
const { changePassword } = createAuth(loader);
|
|
617
|
+
await expect(
|
|
618
|
+
changePassword({
|
|
619
|
+
credentialId: "cred-1",
|
|
620
|
+
newPassword: "N3wP@ssword!",
|
|
621
|
+
currentPassword: "0ldP@ssword!",
|
|
622
|
+
}),
|
|
623
|
+
).rejects.toBe(stopped);
|
|
624
|
+
expect(api.execute).not.toHaveBeenCalled();
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
it("stops before executing when the challenge prepared no operation", async () => {
|
|
628
|
+
api.challenge.mockResolvedValue({
|
|
629
|
+
history: undefined,
|
|
630
|
+
credentialResponse: new Uint8Array(),
|
|
631
|
+
});
|
|
632
|
+
const { changePassword } = createAuth(loader);
|
|
633
|
+
await expect(
|
|
634
|
+
changePassword({ credentialId: "cred-1", newPassword: "pw" }),
|
|
635
|
+
).rejects.toThrow(/did not prepare/);
|
|
636
|
+
expect(api.execute).not.toHaveBeenCalled();
|
|
637
|
+
expect(client.prove).not.toHaveBeenCalled();
|
|
638
|
+
});
|
|
639
|
+
|
|
357
640
|
it("propagates a refused history (password reused) from the finish", async () => {
|
|
358
641
|
api.changeFinish.mockRejectedValue(new Error("PASSWORD_REUSED"));
|
|
359
642
|
const { changePassword } = createAuth(loader);
|
|
360
|
-
await expect(
|
|
361
|
-
"
|
|
362
|
-
);
|
|
643
|
+
await expect(
|
|
644
|
+
changePassword({ credentialId: "cred-1", newPassword: "pw" }),
|
|
645
|
+
).rejects.toThrow("PASSWORD_REUSED");
|
|
363
646
|
});
|
|
364
647
|
});
|
|
365
648
|
|
|
@@ -382,10 +665,7 @@ describe("createAuth resetPassword", () => {
|
|
|
382
665
|
"reset-1",
|
|
383
666
|
OPERATION,
|
|
384
667
|
RECORD,
|
|
385
|
-
|
|
386
|
-
zkppProof: PROOF.proof,
|
|
387
|
-
instances: PROOF.instances,
|
|
388
|
-
},
|
|
668
|
+
WIRE_PROOF,
|
|
389
669
|
);
|
|
390
670
|
expect(done.sessionId).toBe("sess-1");
|
|
391
671
|
});
|
|
@@ -426,6 +706,7 @@ describe("createAuth login", () => {
|
|
|
426
706
|
"pw",
|
|
427
707
|
"login-state",
|
|
428
708
|
new Uint8Array([4, 5, 6]),
|
|
709
|
+
new Uint8Array(),
|
|
429
710
|
);
|
|
430
711
|
expect(api.loginFinish).toHaveBeenCalledWith(
|
|
431
712
|
"alice@test.com",
|