@structured-id/ui-core 1.2.0 → 1.3.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 +9 -5
- package/dist/browser-vp/index.d.cts +0 -11
- package/dist/browser-vp/index.d.ts +0 -11
- package/dist/index.cjs +13721 -9514
- package/dist/index.d.cts +4362 -2375
- package/dist/index.d.ts +4362 -2375
- package/dist/index.js +13937 -9730
- package/package.json +3 -3
- package/src/browser-vp/crypto.ts +0 -1
- package/src/browser-vp/index.ts +0 -2
- package/src/browser-vp/prf.ts +0 -2
- package/src/browser-vp/storage.ts +0 -2
- package/src/browser-vp/types.ts +0 -2
- package/src/browser-vp/vp.ts +0 -2
- package/src/composables/index.ts +6 -1
- package/src/composables/useAccountTransport.test.ts +121 -0
- package/src/composables/useAccountTransport.ts +69 -0
- package/src/composables/useBffAuth.ts +44 -34
- package/src/generated/google/api/httpbody.ts +159 -0
- package/src/generated/google/protobuf/any.ts +326 -0
- package/src/generated/google/rpc/error_details.ts +1386 -0
- package/src/generated/google/rpc/status.ts +127 -0
- package/src/generated/sid/v1/account/account.ts +1 -1
- package/src/generated/sid/v1/admin/admin.client.ts +15 -12
- package/src/generated/sid/v1/admin/admin.ts +24 -89
- package/src/generated/sid/v1/admin/enrollment.client.ts +24 -12
- package/src/generated/sid/v1/admin/enrollment.ts +65 -3
- package/src/generated/sid/v1/admin/events.client.ts +8 -8
- package/src/generated/sid/v1/admin/flow.ts +1 -1
- package/src/generated/sid/v1/admin/keys.ts +1 -1
- package/src/generated/sid/v1/admin/notifications.client.ts +12 -6
- package/src/generated/sid/v1/admin/notifications.ts +11 -70
- package/src/generated/sid/v1/admin/operations.ts +20 -35
- package/src/generated/sid/v1/admin/organizations.client.ts +2 -2
- package/src/generated/sid/v1/admin/organizations.ts +15 -32
- package/src/generated/sid/v1/admin/provisioning.client.ts +232 -0
- package/src/generated/sid/v1/admin/provisioning.ts +1383 -0
- package/src/generated/sid/v1/admin/scopes.ts +1 -1
- package/src/generated/sid/v1/admin/security.client.ts +22 -24
- package/src/generated/sid/v1/admin/security.ts +13 -22
- package/src/generated/sid/v1/admin/settings.client.ts +29 -18
- package/src/generated/sid/v1/admin/settings.ts +2 -82
- package/src/generated/sid/v1/attestation/attestation.client.ts +6 -6
- package/src/generated/sid/v1/attestation/attestation.ts +8 -8
- package/src/generated/sid/v1/authn/auth.client.ts +101 -58
- package/src/generated/sid/v1/authn/auth.ts +842 -414
- package/src/generated/sid/v1/authn/flows.ts +0 -12
- package/src/generated/sid/v1/authn/issuer.client.ts +489 -0
- package/src/generated/sid/v1/authn/issuer.ts +1600 -0
- package/src/generated/sid/v1/authn/password_history.client.ts +57 -0
- package/src/generated/sid/v1/authn/password_history.ts +578 -0
- package/src/generated/sid/v1/authn/system_integration.client.ts +73 -0
- package/src/generated/sid/v1/authn/system_integration.ts +218 -0
- package/src/generated/sid/v1/authz/authz.ts +1498 -317
- package/src/generated/sid/v1/authz/forward_auth.client.ts +97 -0
- package/src/generated/sid/v1/authz/forward_auth.ts +79 -0
- package/src/generated/sid/v1/authz/governance.client.ts +2 -2
- package/src/generated/sid/v1/authz/governance.ts +61 -4
- package/src/generated/sid/v1/common/errors.ts +532 -6
- package/src/generated/sid/v1/events/event_stream.client.ts +2 -2
- package/src/generated/sid/v1/events/event_stream.ts +2 -1
- package/src/generated/sid/v1/identity/identity.ts +98 -58
- package/src/generated/sid/v1/ids/ids.ts +395 -0
- package/src/generated/sid/v1/machine/machine_user.ts +0 -36
- package/src/generated/sid/v1/machine/pat.client.ts +2 -19
- package/src/generated/sid/v1/machine/pat.ts +1 -186
- package/src/generated/sid/v1/projects/projects.client.ts +137 -19
- package/src/generated/sid/v1/projects/projects.ts +2105 -405
- package/src/generated/sid/v1/scim/scim.client.ts +248 -2
- package/src/generated/sid/v1/scim/scim.ts +153 -14
- package/src/generated/sid/v1/test/test.ts +1 -1
- package/src/i18n/index.ts +0 -11
- package/src/index.ts +36 -2
- package/src/package-exports.test.ts +18 -0
- package/src/profile/components/SidLoginForm.vue +5 -2
- package/src/profile/components/SidRegistrationForm.test.ts +58 -1
- package/src/profile/components/SidRegistrationForm.vue +110 -12
- package/src/profile/components/index.ts +4 -0
- package/src/profile/composables/index.ts +2 -1
- package/src/profile/composables/useAccountApi.ts +20 -7
- package/src/profile/composables/useAuth.test.ts +453 -0
- package/src/profile/composables/useAuth.ts +414 -0
- package/src/profile/composables/useAuthApi.opaque.test.ts +18 -0
- package/src/profile/composables/useAuthApi.ts +188 -20
- package/src/profile/composables/useEnrollmentApi.test.ts +33 -0
- package/src/profile/composables/useEnrollmentApi.ts +26 -0
- package/src/profile/composables/useIdentityApi.ts +17 -7
- package/src/profile/composables/useLoginSuccess.test.ts +97 -3
- package/src/profile/composables/useLoginSuccess.ts +80 -22
- package/src/profile/composables/useSecurityLevel.test.ts +10 -3
- package/src/profile/composables/useSecurityLevel.ts +12 -3
- package/src/quasar/SidPrincipalInput.vue +4 -7
- package/src/utils/normalizePrincipal.test.ts +19 -5
- package/src/utils/normalizePrincipal.ts +7 -9
- package/src/utils/rpcRefusal.test.ts +151 -0
- package/src/utils/rpcRefusal.ts +252 -0
- package/src/utils/validators.test.ts +9 -0
- package/src/utils/validators.ts +14 -2
- package/src/composables/useAuth.ts +0 -40
- package/src/profile/composables/useOpaqueAuth.test.ts +0 -290
- package/src/profile/composables/useOpaqueAuth.ts +0 -191
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The password ceremonies (shared between all profile UIs): registration,
|
|
3
|
+
* sign-in, password change and reset.
|
|
4
|
+
*
|
|
5
|
+
* A password is installed (a first password, a change or a reset) in one
|
|
6
|
+
* server-side operation. The preparing RPC returns the operation's history
|
|
7
|
+
* context; the client blinds its history input for the server's evaluator,
|
|
8
|
+
* proves policy and history over the same password, bound to the operation
|
|
9
|
+
* and to its OPAQUE request, and sends the proof with the final record.
|
|
10
|
+
* Sign-in runs on the same Pallas OPAQUE client, since the server registers
|
|
11
|
+
* on Pallas.
|
|
12
|
+
*
|
|
13
|
+
* The client-side crypto is injected as {@link ZkppClientApi} (implemented by
|
|
14
|
+
* `@structured-id/opaque-zkpp`), keeping the WASM dependency at the app layer.
|
|
15
|
+
* gRPC calls go through useAuthApi wrappers.
|
|
16
|
+
*/
|
|
17
|
+
import {
|
|
18
|
+
completePasswordReset,
|
|
19
|
+
evaluatePasswordHistory,
|
|
20
|
+
executePasswordReset,
|
|
21
|
+
opaqueLoginFinish,
|
|
22
|
+
opaqueLoginStart,
|
|
23
|
+
opaqueZkppRegistrationFinish,
|
|
24
|
+
opaqueZkppRegistrationStart,
|
|
25
|
+
passwordChangeChallenge,
|
|
26
|
+
passwordChangeExecute,
|
|
27
|
+
passwordChangeFinish,
|
|
28
|
+
type CompletePasswordResetResponse,
|
|
29
|
+
type PasswordHistoryContext,
|
|
30
|
+
type PasswordHistoryEvaluation,
|
|
31
|
+
type PasswordRegistrationProof,
|
|
32
|
+
} from "./useAuthApi";
|
|
33
|
+
|
|
34
|
+
// ── Types ──
|
|
35
|
+
|
|
36
|
+
/** A sign-in's session, as the server issued it. */
|
|
37
|
+
export interface LoginResult {
|
|
38
|
+
sessionId: string;
|
|
39
|
+
accessToken: string;
|
|
40
|
+
expiresIn: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A registration's profile and its password credential. */
|
|
44
|
+
export interface RegisterResult {
|
|
45
|
+
profileId: string;
|
|
46
|
+
credentialId: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Progress of a password operation, for a determinate gauge (0..1). */
|
|
50
|
+
export interface PasswordOperationProgress {
|
|
51
|
+
fraction: number;
|
|
52
|
+
label: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The operation's history context as the ZKPP client takes it. */
|
|
56
|
+
export interface ZkppHistoryContext {
|
|
57
|
+
/** The operation, 16 bytes; every later step carries it. */
|
|
58
|
+
operationId: Uint8Array;
|
|
59
|
+
ownerDomain: Uint8Array;
|
|
60
|
+
domains: { comparisonDomain: Uint8Array; evaluatorPublicKey: Uint8Array }[];
|
|
61
|
+
policyVersion: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The client's blinded history input; `blind` stays with the client. */
|
|
65
|
+
export interface ZkppHistoryRequest {
|
|
66
|
+
blind: Uint8Array;
|
|
67
|
+
blinded: Uint8Array;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The evaluator's answer for one comparison domain. */
|
|
71
|
+
export interface ZkppHistoryEvaluation {
|
|
72
|
+
evaluatedElement: Uint8Array;
|
|
73
|
+
proof: { challenge: Uint8Array; response: Uint8Array };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The OPAQUE registration start: the request sent, the state kept local. */
|
|
77
|
+
export interface ZkppRegistrationStart {
|
|
78
|
+
request: Uint8Array;
|
|
79
|
+
state: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** A registration proof: Halo2 bytes and its public instances. */
|
|
83
|
+
export interface ZkppProof {
|
|
84
|
+
proof: Uint8Array;
|
|
85
|
+
instances: Uint8Array[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Interface for the ZKPP client-side crypto.
|
|
90
|
+
* Implemented by @structured-id/opaque ZkppClient.
|
|
91
|
+
*/
|
|
92
|
+
export interface ZkppClientApi {
|
|
93
|
+
/** True once the client answers no more calls (a worker it runs on stopped). */
|
|
94
|
+
readonly stopped: boolean;
|
|
95
|
+
registrationStart(password: string): Promise<ZkppRegistrationStart>;
|
|
96
|
+
/** `null` for a password the circuit cannot hold: it installs unproven. */
|
|
97
|
+
historyRequest(
|
|
98
|
+
password: string,
|
|
99
|
+
ownerDomain: Uint8Array,
|
|
100
|
+
): Promise<ZkppHistoryRequest | null>;
|
|
101
|
+
prove(
|
|
102
|
+
password: string,
|
|
103
|
+
start: ZkppRegistrationStart,
|
|
104
|
+
opts: {
|
|
105
|
+
context: ZkppHistoryContext;
|
|
106
|
+
history: {
|
|
107
|
+
request: ZkppHistoryRequest;
|
|
108
|
+
evaluations: ZkppHistoryEvaluation[];
|
|
109
|
+
} | null;
|
|
110
|
+
onProgress?: (p: PasswordOperationProgress) => void;
|
|
111
|
+
},
|
|
112
|
+
): Promise<ZkppProof | null>;
|
|
113
|
+
registrationFinish(
|
|
114
|
+
password: string,
|
|
115
|
+
state: string,
|
|
116
|
+
response: Uint8Array,
|
|
117
|
+
): Promise<Uint8Array>;
|
|
118
|
+
loginStart(password: string): Promise<{ request: Uint8Array; state: string }>;
|
|
119
|
+
loginFinish(
|
|
120
|
+
password: string,
|
|
121
|
+
state: string,
|
|
122
|
+
response: Uint8Array,
|
|
123
|
+
): Promise<Uint8Array>;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Loads the client on first use. Rejects where no client can run (no
|
|
128
|
+
* threaded WebAssembly: `ZkppUnavailableError` of the package).
|
|
129
|
+
*/
|
|
130
|
+
export type ZkppClientLoader = () => Promise<ZkppClientApi>;
|
|
131
|
+
|
|
132
|
+
/** What the finish of an operation sends. */
|
|
133
|
+
interface Installed {
|
|
134
|
+
operationId: Uint8Array;
|
|
135
|
+
record: Uint8Array;
|
|
136
|
+
proof?: PasswordRegistrationProof;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The preparing RPC's answer to the OPAQUE request: the operation and the response. */
|
|
140
|
+
interface Exchange {
|
|
141
|
+
context: PasswordHistoryContext | undefined;
|
|
142
|
+
registrationResponse: Uint8Array;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── Wire conversions ──
|
|
146
|
+
|
|
147
|
+
/** The operation's context; a server that prepared none cannot take a password. */
|
|
148
|
+
function historyContext(
|
|
149
|
+
wire: PasswordHistoryContext | undefined,
|
|
150
|
+
): ZkppHistoryContext {
|
|
151
|
+
const operationId = wire?.operationId?.value;
|
|
152
|
+
if (!wire || !operationId || operationId.length === 0) {
|
|
153
|
+
throw new Error("The server did not prepare a password operation");
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
operationId,
|
|
157
|
+
ownerDomain: wire.ownerDomain,
|
|
158
|
+
domains: wire.domains.map((d) => ({
|
|
159
|
+
comparisonDomain: d.comparisonDomain,
|
|
160
|
+
evaluatorPublicKey: d.evaluatorPublicKey,
|
|
161
|
+
})),
|
|
162
|
+
policyVersion: wire.policyVersion,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** The evaluator's answers with their proofs; an answer without one proves nothing. */
|
|
167
|
+
function historyEvaluations(
|
|
168
|
+
answers: PasswordHistoryEvaluation[],
|
|
169
|
+
): ZkppHistoryEvaluation[] {
|
|
170
|
+
return answers.map((answer) => {
|
|
171
|
+
if (!answer.proof) {
|
|
172
|
+
throw new Error("The history evaluator answered without a proof");
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
evaluatedElement: answer.evaluatedElement,
|
|
176
|
+
proof: {
|
|
177
|
+
challenge: answer.proof.challenge,
|
|
178
|
+
response: answer.proof.response,
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function wireProof(
|
|
185
|
+
proof: ZkppProof | null,
|
|
186
|
+
): PasswordRegistrationProof | undefined {
|
|
187
|
+
return proof
|
|
188
|
+
? { zkppProof: proof.proof, instances: proof.instances }
|
|
189
|
+
: undefined;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Create the password ceremonies with an injected client loader; an app
|
|
194
|
+
* wraps the result in its `useAuth()`.
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* ```ts
|
|
198
|
+
* import { loadZkppClient } from "@structured-id/opaque";
|
|
199
|
+
* import { createAuth } from "@structured-id/ui-core/profile";
|
|
200
|
+
*
|
|
201
|
+
* const { register, login, prewarm } = createAuth(() => loadZkppClient());
|
|
202
|
+
* void prewarm(); // when the page opens
|
|
203
|
+
* ```
|
|
204
|
+
*/
|
|
205
|
+
export function createAuth(loadClient: ZkppClientLoader) {
|
|
206
|
+
let client: Promise<ZkppClientApi> | undefined;
|
|
207
|
+
let loaded: ZkppClientApi | undefined;
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* The client, loaded once and kept for each next operation; a failed load
|
|
211
|
+
* is retried next time, and a client that stopped answering is replaced.
|
|
212
|
+
*/
|
|
213
|
+
function zkpp(): Promise<ZkppClientApi> {
|
|
214
|
+
if (loaded?.stopped) {
|
|
215
|
+
client = undefined;
|
|
216
|
+
loaded = undefined;
|
|
217
|
+
}
|
|
218
|
+
client ??= loadClient().then(
|
|
219
|
+
(c) => (loaded = c),
|
|
220
|
+
(e: unknown) => {
|
|
221
|
+
client = undefined;
|
|
222
|
+
throw e;
|
|
223
|
+
},
|
|
224
|
+
);
|
|
225
|
+
return client;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* The steps every operation shares: the OPAQUE start, `exchange` (the RPC
|
|
230
|
+
* that answers the request and names the operation), the history
|
|
231
|
+
* evaluation, the proof and the record.
|
|
232
|
+
*/
|
|
233
|
+
async function install(
|
|
234
|
+
password: string,
|
|
235
|
+
exchange: (request: Uint8Array) => Promise<Exchange>,
|
|
236
|
+
onProgress?: (p: PasswordOperationProgress) => void,
|
|
237
|
+
): Promise<Installed> {
|
|
238
|
+
const c = await zkpp();
|
|
239
|
+
onProgress?.({ fraction: 0, label: "Preparing" });
|
|
240
|
+
const start = await c.registrationStart(password);
|
|
241
|
+
const { context: wire, registrationResponse } = await exchange(
|
|
242
|
+
start.request,
|
|
243
|
+
);
|
|
244
|
+
const context = historyContext(wire);
|
|
245
|
+
const request = await c.historyRequest(password, context.ownerDomain);
|
|
246
|
+
const history = request
|
|
247
|
+
? {
|
|
248
|
+
request,
|
|
249
|
+
evaluations: historyEvaluations(
|
|
250
|
+
await evaluatePasswordHistory(context.operationId, request.blinded),
|
|
251
|
+
),
|
|
252
|
+
}
|
|
253
|
+
: null;
|
|
254
|
+
const proof = await c.prove(password, start, {
|
|
255
|
+
context,
|
|
256
|
+
history,
|
|
257
|
+
onProgress,
|
|
258
|
+
});
|
|
259
|
+
const record = await c.registrationFinish(
|
|
260
|
+
password,
|
|
261
|
+
start.state,
|
|
262
|
+
registrationResponse,
|
|
263
|
+
);
|
|
264
|
+
onProgress?.({ fraction: 1, label: "Done" });
|
|
265
|
+
return {
|
|
266
|
+
operationId: context.operationId,
|
|
267
|
+
record,
|
|
268
|
+
proof: wireProof(proof),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Register `identifier` with `password`: the start carries the OPAQUE
|
|
274
|
+
* request and returns the operation, the finish carries the record with
|
|
275
|
+
* the proof. `claimToken` claims an installation that has no administrator.
|
|
276
|
+
*/
|
|
277
|
+
async function register(
|
|
278
|
+
identifier: string,
|
|
279
|
+
password: string,
|
|
280
|
+
claimToken?: string,
|
|
281
|
+
onProgress?: (p: PasswordOperationProgress) => void,
|
|
282
|
+
): Promise<RegisterResult> {
|
|
283
|
+
const done = await install(
|
|
284
|
+
password,
|
|
285
|
+
async (request) => {
|
|
286
|
+
const started = await opaqueZkppRegistrationStart(
|
|
287
|
+
identifier,
|
|
288
|
+
request,
|
|
289
|
+
claimToken,
|
|
290
|
+
);
|
|
291
|
+
return {
|
|
292
|
+
context: started.history,
|
|
293
|
+
registrationResponse: started.registrationResponse,
|
|
294
|
+
};
|
|
295
|
+
},
|
|
296
|
+
onProgress,
|
|
297
|
+
);
|
|
298
|
+
const finished = await opaqueZkppRegistrationFinish(
|
|
299
|
+
done.operationId,
|
|
300
|
+
done.record,
|
|
301
|
+
done.proof,
|
|
302
|
+
);
|
|
303
|
+
return {
|
|
304
|
+
profileId: finished.profileId,
|
|
305
|
+
credentialId: finished.credentialId,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Replace the signed-in user's password `credentialId` with `newPassword`:
|
|
311
|
+
* the challenge names the operation, the execute step answers the OPAQUE
|
|
312
|
+
* request, the finish carries the record with the proof.
|
|
313
|
+
*/
|
|
314
|
+
async function changePassword(
|
|
315
|
+
credentialId: string,
|
|
316
|
+
newPassword: string,
|
|
317
|
+
onProgress?: (p: PasswordOperationProgress) => void,
|
|
318
|
+
): Promise<void> {
|
|
319
|
+
const challenge = await passwordChangeChallenge(credentialId);
|
|
320
|
+
const context = historyContext(challenge.history);
|
|
321
|
+
const done = await install(
|
|
322
|
+
newPassword,
|
|
323
|
+
async (request) => ({
|
|
324
|
+
context: challenge.history,
|
|
325
|
+
registrationResponse: (
|
|
326
|
+
await passwordChangeExecute(
|
|
327
|
+
context.operationId,
|
|
328
|
+
credentialId,
|
|
329
|
+
request,
|
|
330
|
+
)
|
|
331
|
+
).registrationResponse,
|
|
332
|
+
}),
|
|
333
|
+
onProgress,
|
|
334
|
+
);
|
|
335
|
+
await passwordChangeFinish(
|
|
336
|
+
done.operationId,
|
|
337
|
+
credentialId,
|
|
338
|
+
done.record,
|
|
339
|
+
done.proof,
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Replace the password under a verified reset: `context` is the operation
|
|
345
|
+
* the verification prepared (`VerifyPasswordResetResponse.history`).
|
|
346
|
+
*/
|
|
347
|
+
async function resetPassword(
|
|
348
|
+
resetSessionId: string,
|
|
349
|
+
context: PasswordHistoryContext | undefined,
|
|
350
|
+
newPassword: string,
|
|
351
|
+
onProgress?: (p: PasswordOperationProgress) => void,
|
|
352
|
+
): Promise<CompletePasswordResetResponse> {
|
|
353
|
+
const prepared = historyContext(context);
|
|
354
|
+
const done = await install(
|
|
355
|
+
newPassword,
|
|
356
|
+
async (request) => ({
|
|
357
|
+
context,
|
|
358
|
+
registrationResponse: (
|
|
359
|
+
await executePasswordReset(prepared.operationId, request)
|
|
360
|
+
).registrationResponse,
|
|
361
|
+
}),
|
|
362
|
+
onProgress,
|
|
363
|
+
);
|
|
364
|
+
return completePasswordReset(
|
|
365
|
+
resetSessionId,
|
|
366
|
+
done.operationId,
|
|
367
|
+
done.record,
|
|
368
|
+
done.proof,
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Sign in with a Pallas OPAQUE credential (2 round-trips via gRPC-web).
|
|
374
|
+
*/
|
|
375
|
+
async function login(
|
|
376
|
+
identifier: string,
|
|
377
|
+
password: string,
|
|
378
|
+
onStep?: (step: number) => void,
|
|
379
|
+
): Promise<LoginResult> {
|
|
380
|
+
const c = await zkpp();
|
|
381
|
+
onStep?.(1);
|
|
382
|
+
const start = await c.loginStart(password);
|
|
383
|
+
onStep?.(2);
|
|
384
|
+
const started = await opaqueLoginStart(identifier, start.request);
|
|
385
|
+
onStep?.(3);
|
|
386
|
+
const finalization = await c.loginFinish(
|
|
387
|
+
password,
|
|
388
|
+
start.state,
|
|
389
|
+
started.credentialResponse,
|
|
390
|
+
);
|
|
391
|
+
const finished = await opaqueLoginFinish(
|
|
392
|
+
identifier,
|
|
393
|
+
finalization,
|
|
394
|
+
started.serverLoginState,
|
|
395
|
+
);
|
|
396
|
+
return {
|
|
397
|
+
sessionId: finished.sessionId,
|
|
398
|
+
accessToken: finished.accessToken,
|
|
399
|
+
expiresIn: Number(finished.expiresIn),
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Load the client now, when the page opens, so the kernel's download and
|
|
405
|
+
* compilation are done before the user submits a password. Resolves when
|
|
406
|
+
* loading ends either way: a failed load is retried, and reported, by the
|
|
407
|
+
* next operation.
|
|
408
|
+
*/
|
|
409
|
+
async function prewarm(): Promise<void> {
|
|
410
|
+
await zkpp().catch(() => undefined);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return { register, changePassword, resetPassword, login, prewarm };
|
|
414
|
+
}
|
|
@@ -103,6 +103,24 @@ describe("OPAQUE gRPC wrappers", () => {
|
|
|
103
103
|
});
|
|
104
104
|
expect(result).toBe(expected);
|
|
105
105
|
});
|
|
106
|
+
|
|
107
|
+
// An unclaimed installation accepts only a registration carrying the
|
|
108
|
+
// claim token; the wrapper must put it on the wire unchanged.
|
|
109
|
+
it("sends the instance claim token when given", async () => {
|
|
110
|
+
mockOpaqueRegistrationStart.mockResolvedValue({ response: {} });
|
|
111
|
+
|
|
112
|
+
await opaqueRegistrationStart(
|
|
113
|
+
"owner@test.com",
|
|
114
|
+
new Uint8Array([1]),
|
|
115
|
+
"sidclaim_abc",
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
expect(mockOpaqueRegistrationStart).toHaveBeenCalledWith({
|
|
119
|
+
principal: "owner@test.com",
|
|
120
|
+
registrationRequest: new Uint8Array([1]),
|
|
121
|
+
claimToken: "sidclaim_abc",
|
|
122
|
+
});
|
|
123
|
+
});
|
|
106
124
|
});
|
|
107
125
|
|
|
108
126
|
describe("opaqueRegistrationFinish", () => {
|