@structured-id/ui-core 1.1.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 +118 -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
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Profile auth API composable.
|
|
3
3
|
*
|
|
4
|
-
* Wraps AuthServiceClient proto calls with Bearer auth.
|
|
5
4
|
* Covers: OPAQUE, MFA, step-up, WebAuthn, magic link, OAuth2, CIBA, captcha, terms.
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* Two surfaces: sign-in ceremonies go to SID's sign-in surface
|
|
7
|
+
* (`getTransport`), where the IdP session lives; managing the signed-in
|
|
8
|
+
* user's factors from the account pages is the account API
|
|
9
|
+
* (`getAccountTransport`), through the BFF.
|
|
10
|
+
*
|
|
11
|
+
* OPAQUE gRPC calls are here (like all other auth RPCs). The password
|
|
12
|
+
* ceremonies over them (client-side crypto, proofs) live in useAuth.ts,
|
|
13
|
+
* behind the injected ZkppClientApi: no dependency on the client package.
|
|
10
14
|
*/
|
|
11
15
|
import {
|
|
12
16
|
getTransport,
|
|
17
|
+
getAccountTransport,
|
|
13
18
|
AuthServiceClient,
|
|
14
19
|
StepUpMethod,
|
|
15
20
|
type OpaqueLoginStartResponse,
|
|
@@ -48,17 +53,43 @@ import {
|
|
|
48
53
|
type RequestPasswordResetResponse,
|
|
49
54
|
type VerifyPasswordResetResponse,
|
|
50
55
|
type CompletePasswordResetResponse,
|
|
56
|
+
type ExecutePasswordResetResponse,
|
|
57
|
+
type OpaqueZkppRegistrationStartResponse,
|
|
58
|
+
type OpaqueZkppRegistrationFinishResponse,
|
|
59
|
+
type PasswordChangeChallengeResponse,
|
|
60
|
+
type PasswordChangeExecuteResponse,
|
|
61
|
+
type PasswordChangeFinishResponse,
|
|
62
|
+
type PasswordHistoryContext,
|
|
63
|
+
type PasswordHistoryEvaluation,
|
|
64
|
+
type PasswordRegistrationProof,
|
|
65
|
+
PasswordHistoryEvaluatorServiceClient,
|
|
51
66
|
} from "../../index";
|
|
52
67
|
import { authMeta } from "../auth";
|
|
53
68
|
|
|
69
|
+
/** The history evaluator: in CE served beside the credential service. */
|
|
70
|
+
function evaluator(): PasswordHistoryEvaluatorServiceClient {
|
|
71
|
+
return new PasswordHistoryEvaluatorServiceClient(getTransport());
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The wire form of an operation id (16 UUIDv7 bytes). */
|
|
75
|
+
function operation(id: Uint8Array): { value: Uint8Array } {
|
|
76
|
+
return { value: id };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The sign-in surface: ceremonies and the IdP session. */
|
|
54
80
|
function client(): AuthServiceClient {
|
|
55
81
|
return new AuthServiceClient(getTransport());
|
|
56
82
|
}
|
|
57
83
|
|
|
84
|
+
/** The account API: the signed-in user managing their own factors. */
|
|
85
|
+
function account(): AuthServiceClient {
|
|
86
|
+
return new AuthServiceClient(getAccountTransport());
|
|
87
|
+
}
|
|
88
|
+
|
|
58
89
|
// ── MFA TOTP ──
|
|
59
90
|
|
|
60
91
|
export async function startTotpEnrollment(): Promise<TotpEnrollmentChallenge> {
|
|
61
|
-
const { response } = await
|
|
92
|
+
const { response } = await account().startTotpEnrollment(
|
|
62
93
|
{},
|
|
63
94
|
{ meta: authMeta() },
|
|
64
95
|
);
|
|
@@ -68,7 +99,7 @@ export async function startTotpEnrollment(): Promise<TotpEnrollmentChallenge> {
|
|
|
68
99
|
export async function finishTotpEnrollment(
|
|
69
100
|
code: string,
|
|
70
101
|
): Promise<FinishTotpEnrollmentResponse> {
|
|
71
|
-
const { response } = await
|
|
102
|
+
const { response } = await account().finishTotpEnrollment(
|
|
72
103
|
{ code },
|
|
73
104
|
{ meta: authMeta() },
|
|
74
105
|
);
|
|
@@ -101,7 +132,7 @@ export async function resendSmsMfa(): Promise<ResendSmsMfaResponse> {
|
|
|
101
132
|
// ── MFA Recovery ──
|
|
102
133
|
|
|
103
134
|
export async function generateRecoveryCodes(): Promise<GenerateRecoveryCodesResponse> {
|
|
104
|
-
const { response } = await
|
|
135
|
+
const { response } = await account().generateRecoveryCodes(
|
|
105
136
|
{},
|
|
106
137
|
{ meta: authMeta() },
|
|
107
138
|
);
|
|
@@ -199,7 +230,7 @@ export async function webAuthnAuthenticationFinish(
|
|
|
199
230
|
export async function webAuthnRegistrationStart(
|
|
200
231
|
label?: string,
|
|
201
232
|
): Promise<WebAuthnRegistrationStartResponse> {
|
|
202
|
-
const { response } = await
|
|
233
|
+
const { response } = await account().webAuthnRegistrationStart(
|
|
203
234
|
{ label },
|
|
204
235
|
{ meta: authMeta() },
|
|
205
236
|
);
|
|
@@ -210,7 +241,7 @@ export async function webAuthnRegistrationFinish(
|
|
|
210
241
|
credential: Uint8Array,
|
|
211
242
|
label?: string,
|
|
212
243
|
): Promise<WebAuthnRegistrationFinishResponse> {
|
|
213
|
-
const { response } = await
|
|
244
|
+
const { response } = await account().webAuthnRegistrationFinish(
|
|
214
245
|
{ credential, label },
|
|
215
246
|
{ meta: authMeta() },
|
|
216
247
|
);
|
|
@@ -241,6 +272,10 @@ export async function oauth2Authorize(params: {
|
|
|
241
272
|
codeChallenge?: string;
|
|
242
273
|
codeChallengeMethod?: string;
|
|
243
274
|
nonce?: string;
|
|
275
|
+
/** Issuer of the authorization request being completed (`/i/<handle>/oauth2/authorize`). */
|
|
276
|
+
issuerHandle: string;
|
|
277
|
+
/** RFC 8707 resource indicators of the request; empty selects the client's default. */
|
|
278
|
+
resource?: string[];
|
|
244
279
|
}): Promise<{ authorizationCode?: string; error?: string; state?: string }> {
|
|
245
280
|
const { response } = await client().oAuth2Authorize(
|
|
246
281
|
{
|
|
@@ -252,6 +287,8 @@ export async function oauth2Authorize(params: {
|
|
|
252
287
|
codeChallenge: params.codeChallenge,
|
|
253
288
|
codeChallengeMethod: params.codeChallengeMethod,
|
|
254
289
|
nonce: params.nonce,
|
|
290
|
+
issuerHandle: params.issuerHandle,
|
|
291
|
+
resource: params.resource ?? [],
|
|
255
292
|
},
|
|
256
293
|
{ meta: authMeta() },
|
|
257
294
|
);
|
|
@@ -455,7 +492,7 @@ export async function completeStepUp(
|
|
|
455
492
|
export async function deleteMfaCredential(
|
|
456
493
|
credentialId: string,
|
|
457
494
|
): Promise<DeleteMfaCredentialResponse> {
|
|
458
|
-
const { response } = await
|
|
495
|
+
const { response } = await account().deleteMfaCredential(
|
|
459
496
|
{ credentialId },
|
|
460
497
|
{ meta: authMeta() },
|
|
461
498
|
);
|
|
@@ -502,23 +539,37 @@ export async function verifyPasswordReset(
|
|
|
502
539
|
return response;
|
|
503
540
|
}
|
|
504
541
|
|
|
542
|
+
/**
|
|
543
|
+
* The OPAQUE start of the replacement password under the reset's operation
|
|
544
|
+
* (from `VerifyPasswordResetResponse.history`).
|
|
545
|
+
*/
|
|
546
|
+
export async function executePasswordReset(
|
|
547
|
+
operationId: Uint8Array,
|
|
548
|
+
registrationRequest: Uint8Array,
|
|
549
|
+
): Promise<ExecutePasswordResetResponse> {
|
|
550
|
+
const { response } = await client().executePasswordReset({
|
|
551
|
+
operationId: operation(operationId),
|
|
552
|
+
registrationRequest,
|
|
553
|
+
});
|
|
554
|
+
return response;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Complete the reset with the replacement password's record and, when the
|
|
559
|
+
* client proved it, the proof; without a proof the password installs
|
|
560
|
+
* policy-unverified.
|
|
561
|
+
*/
|
|
505
562
|
export async function completePasswordReset(
|
|
506
563
|
resetSessionId: string,
|
|
564
|
+
operationId: Uint8Array,
|
|
507
565
|
registrationRecord: Uint8Array,
|
|
508
|
-
|
|
509
|
-
zkppProof?: Uint8Array;
|
|
510
|
-
zkppInstances?: Uint8Array[];
|
|
511
|
-
historyCommitment?: Uint8Array;
|
|
512
|
-
commitmentSalt?: Uint8Array;
|
|
513
|
-
},
|
|
566
|
+
proof?: PasswordRegistrationProof,
|
|
514
567
|
): Promise<CompletePasswordResetResponse> {
|
|
515
568
|
const { response } = await client().completePasswordReset({
|
|
516
569
|
resetSessionId,
|
|
570
|
+
operationId: operation(operationId),
|
|
517
571
|
registrationRecord,
|
|
518
|
-
|
|
519
|
-
zkppInstances: opts?.zkppInstances ?? [],
|
|
520
|
-
historyCommitment: opts?.historyCommitment ?? new Uint8Array(),
|
|
521
|
-
commitmentSalt: opts?.commitmentSalt ?? new Uint8Array(),
|
|
572
|
+
proof,
|
|
522
573
|
});
|
|
523
574
|
return response;
|
|
524
575
|
}
|
|
@@ -526,6 +577,107 @@ export async function completePasswordReset(
|
|
|
526
577
|
// Recovery shard RPCs (downloadRecoveryShard, uploadRecoveryShard, getRecoveryInfo)
|
|
527
578
|
// are EE/SaaS-only — defined in proto-ee, not in CE proto.
|
|
528
579
|
|
|
580
|
+
// ── Password operations (OPAQUE-ZKPP) ──
|
|
581
|
+
//
|
|
582
|
+
// Registration, password change and reset install a password in one
|
|
583
|
+
// server-side operation: the preparing RPC returns the operation's history
|
|
584
|
+
// context, the evaluator answers the client's blinded history input, the
|
|
585
|
+
// client proves, and the finish carries the record with the proof.
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Start a registration: the operation is prepared for the new account and its
|
|
589
|
+
* OPAQUE request answered. `claimToken` claims an installation that has no
|
|
590
|
+
* administrator yet.
|
|
591
|
+
*/
|
|
592
|
+
export async function opaqueZkppRegistrationStart(
|
|
593
|
+
principal: string,
|
|
594
|
+
registrationRequest: Uint8Array,
|
|
595
|
+
claimToken?: string,
|
|
596
|
+
): Promise<OpaqueZkppRegistrationStartResponse> {
|
|
597
|
+
const { response } = await client().opaqueZkppRegistrationStart({
|
|
598
|
+
principal,
|
|
599
|
+
registrationRequest,
|
|
600
|
+
claimToken,
|
|
601
|
+
});
|
|
602
|
+
return response;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/** Finish a registration with the record and, when the client proved, the proof. */
|
|
606
|
+
export async function opaqueZkppRegistrationFinish(
|
|
607
|
+
operationId: Uint8Array,
|
|
608
|
+
registrationRecord: Uint8Array,
|
|
609
|
+
proof?: PasswordRegistrationProof,
|
|
610
|
+
): Promise<OpaqueZkppRegistrationFinishResponse> {
|
|
611
|
+
const { response } = await client().opaqueZkppRegistrationFinish({
|
|
612
|
+
operationId: operation(operationId),
|
|
613
|
+
registrationRecord,
|
|
614
|
+
proof,
|
|
615
|
+
});
|
|
616
|
+
return response;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* The evaluator's answers to the blinded history input of the operation:
|
|
621
|
+
* one per comparison domain of its context, in that order.
|
|
622
|
+
*/
|
|
623
|
+
export async function evaluatePasswordHistory(
|
|
624
|
+
operationId: Uint8Array,
|
|
625
|
+
blindedInput: Uint8Array,
|
|
626
|
+
): Promise<PasswordHistoryEvaluation[]> {
|
|
627
|
+
const { response } = await evaluator().evaluatePasswordHistory({
|
|
628
|
+
operationId: operation(operationId),
|
|
629
|
+
blindedInput,
|
|
630
|
+
});
|
|
631
|
+
return response.evaluations;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/** Prepare a change of the signed-in user's own password `credentialId`. */
|
|
635
|
+
export async function passwordChangeChallenge(
|
|
636
|
+
credentialId: string,
|
|
637
|
+
): Promise<PasswordChangeChallengeResponse> {
|
|
638
|
+
const { response } = await account().passwordChangeChallenge(
|
|
639
|
+
{ credentialId },
|
|
640
|
+
{ meta: authMeta() },
|
|
641
|
+
);
|
|
642
|
+
return response;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/** The OPAQUE start of the new password under the change's operation. */
|
|
646
|
+
export async function passwordChangeExecute(
|
|
647
|
+
operationId: Uint8Array,
|
|
648
|
+
credentialId: string,
|
|
649
|
+
registrationRequest: Uint8Array,
|
|
650
|
+
): Promise<PasswordChangeExecuteResponse> {
|
|
651
|
+
const { response } = await account().passwordChangeExecute(
|
|
652
|
+
{
|
|
653
|
+
operationId: operation(operationId),
|
|
654
|
+
credentialId,
|
|
655
|
+
registrationRequest,
|
|
656
|
+
},
|
|
657
|
+
{ meta: authMeta() },
|
|
658
|
+
);
|
|
659
|
+
return response;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/** Finish the change with the new password's record and, when proved, the proof. */
|
|
663
|
+
export async function passwordChangeFinish(
|
|
664
|
+
operationId: Uint8Array,
|
|
665
|
+
credentialId: string,
|
|
666
|
+
registrationRecord: Uint8Array,
|
|
667
|
+
proof?: PasswordRegistrationProof,
|
|
668
|
+
): Promise<PasswordChangeFinishResponse> {
|
|
669
|
+
const { response } = await account().passwordChangeFinish(
|
|
670
|
+
{
|
|
671
|
+
operationId: operation(operationId),
|
|
672
|
+
credentialId,
|
|
673
|
+
registrationRecord,
|
|
674
|
+
proof,
|
|
675
|
+
},
|
|
676
|
+
{ meta: authMeta() },
|
|
677
|
+
);
|
|
678
|
+
return response;
|
|
679
|
+
}
|
|
680
|
+
|
|
529
681
|
// ── OPAQUE ──
|
|
530
682
|
|
|
531
683
|
export async function opaqueLoginStart(
|
|
@@ -552,13 +704,20 @@ export async function opaqueLoginFinish(
|
|
|
552
704
|
return response;
|
|
553
705
|
}
|
|
554
706
|
|
|
707
|
+
/**
|
|
708
|
+
* `claimToken`: the instance claim token (`sidclaim_...`) from the service log.
|
|
709
|
+
* Required while the installation has no administrator; the new profile then
|
|
710
|
+
* becomes its first administrator.
|
|
711
|
+
*/
|
|
555
712
|
export async function opaqueRegistrationStart(
|
|
556
713
|
principal: string,
|
|
557
714
|
registrationRequest: Uint8Array,
|
|
715
|
+
claimToken?: string,
|
|
558
716
|
): Promise<OpaqueRegistrationStartResponse> {
|
|
559
717
|
const { response } = await client().opaqueRegistrationStart({
|
|
560
718
|
principal,
|
|
561
719
|
registrationRequest,
|
|
720
|
+
claimToken,
|
|
562
721
|
});
|
|
563
722
|
return response;
|
|
564
723
|
}
|
|
@@ -613,5 +772,14 @@ export type {
|
|
|
613
772
|
VerifyMagicLinkResponse,
|
|
614
773
|
RequestPasswordResetResponse,
|
|
615
774
|
VerifyPasswordResetResponse,
|
|
775
|
+
ExecutePasswordResetResponse,
|
|
616
776
|
CompletePasswordResetResponse,
|
|
777
|
+
OpaqueZkppRegistrationStartResponse,
|
|
778
|
+
OpaqueZkppRegistrationFinishResponse,
|
|
779
|
+
PasswordChangeChallengeResponse,
|
|
780
|
+
PasswordChangeExecuteResponse,
|
|
781
|
+
PasswordChangeFinishResponse,
|
|
782
|
+
PasswordHistoryContext,
|
|
783
|
+
PasswordHistoryEvaluation,
|
|
784
|
+
PasswordRegistrationProof,
|
|
617
785
|
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { getInstanceStatus } from "./useEnrollmentApi";
|
|
3
|
+
|
|
4
|
+
const mockGetInstanceStatus = vi.fn();
|
|
5
|
+
|
|
6
|
+
vi.mock("../../index", () => ({
|
|
7
|
+
getTransport: vi.fn(),
|
|
8
|
+
EnrollmentServiceClient: vi.fn().mockImplementation(() => ({
|
|
9
|
+
getInstanceStatus: mockGetInstanceStatus,
|
|
10
|
+
})),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
describe("getInstanceStatus", () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
vi.clearAllMocks();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// The registration page asks before anyone signs in, so the call must
|
|
19
|
+
// carry no credentials and return the server's answer as is.
|
|
20
|
+
it("asks without credentials and returns the status", async () => {
|
|
21
|
+
mockGetInstanceStatus.mockResolvedValue({ response: { claimed: false } });
|
|
22
|
+
|
|
23
|
+
const status = await getInstanceStatus();
|
|
24
|
+
|
|
25
|
+
expect(mockGetInstanceStatus).toHaveBeenCalledWith({});
|
|
26
|
+
expect(status).toEqual({ claimed: false });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("propagates gRPC errors", async () => {
|
|
30
|
+
mockGetInstanceStatus.mockRejectedValue(new Error("UNAVAILABLE"));
|
|
31
|
+
await expect(getInstanceStatus()).rejects.toThrow("UNAVAILABLE");
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile enrollment API composable.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the public EnrollmentServiceClient calls the registration page makes
|
|
5
|
+
* before anyone signs in.
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
getTransport,
|
|
9
|
+
EnrollmentServiceClient,
|
|
10
|
+
type InstanceStatus,
|
|
11
|
+
} from "../../index";
|
|
12
|
+
|
|
13
|
+
function client(): EnrollmentServiceClient {
|
|
14
|
+
return new EnrollmentServiceClient(getTransport());
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type { InstanceStatus };
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Whether the installation has its first administrator. While it has none,
|
|
21
|
+
* registration accepts only the claim token from the service log.
|
|
22
|
+
*/
|
|
23
|
+
export async function getInstanceStatus(): Promise<InstanceStatus> {
|
|
24
|
+
const { response } = await client().getInstanceStatus({});
|
|
25
|
+
return response;
|
|
26
|
+
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Profile identity API composable.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* Shared between profile-ui-ce, profile-ui-ee, and
|
|
4
|
+
* The account API through the BFF (`getAccountTransport`), which adds the
|
|
5
|
+
* session's Bearer token. Shared between profile-ui-ce, profile-ui-ee, and
|
|
6
|
+
* frontend.
|
|
6
7
|
*/
|
|
8
|
+
import type { RpcTransport } from "@protobuf-ts/runtime-rpc";
|
|
7
9
|
import {
|
|
8
|
-
|
|
10
|
+
getAccountTransport,
|
|
9
11
|
IdentityServiceClient,
|
|
10
12
|
type Profile,
|
|
11
13
|
type Session,
|
|
@@ -19,8 +21,8 @@ import {
|
|
|
19
21
|
} from "../../index";
|
|
20
22
|
import { authMeta } from "../auth";
|
|
21
23
|
|
|
22
|
-
function client(): IdentityServiceClient {
|
|
23
|
-
return new IdentityServiceClient(
|
|
24
|
+
function client(transport?: RpcTransport): IdentityServiceClient {
|
|
25
|
+
return new IdentityServiceClient(transport ?? getAccountTransport());
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
// ── Profile ──
|
|
@@ -28,10 +30,17 @@ function client(): IdentityServiceClient {
|
|
|
28
30
|
export type { Profile, Principal };
|
|
29
31
|
|
|
30
32
|
export async function fetchCurrentProfile(): Promise<Profile> {
|
|
31
|
-
const { response } = await client().getCurrentProfile(
|
|
33
|
+
const { response } = await client().getCurrentProfile(
|
|
34
|
+
{},
|
|
35
|
+
{ meta: authMeta() },
|
|
36
|
+
);
|
|
32
37
|
return response.profile!;
|
|
33
38
|
}
|
|
34
39
|
|
|
40
|
+
/**
|
|
41
|
+
* `transport`: the sign-in surface's transport when a sign-in step (a
|
|
42
|
+
* required profile update) makes the call; the account API otherwise.
|
|
43
|
+
*/
|
|
35
44
|
export async function updateProfile(
|
|
36
45
|
id: string,
|
|
37
46
|
update: {
|
|
@@ -40,8 +49,9 @@ export async function updateProfile(
|
|
|
40
49
|
middleName?: string;
|
|
41
50
|
avatarUrl?: string;
|
|
42
51
|
},
|
|
52
|
+
transport?: RpcTransport,
|
|
43
53
|
): Promise<Profile> {
|
|
44
|
-
const { response } = await client().updateProfile(
|
|
54
|
+
const { response } = await client(transport).updateProfile(
|
|
45
55
|
{
|
|
46
56
|
id,
|
|
47
57
|
givenName: update.givenName,
|
|
@@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
|
2
2
|
import { setActivePinia, createPinia } from "pinia";
|
|
3
3
|
import { createApp, defineComponent } from "vue";
|
|
4
4
|
import { createRouter, createMemoryHistory } from "vue-router";
|
|
5
|
-
import { useLoginSuccess } from "./useLoginSuccess";
|
|
5
|
+
import { authorizationContinuation, useLoginSuccess } from "./useLoginSuccess";
|
|
6
6
|
import { useAuthStore } from "../../stores/auth";
|
|
7
7
|
|
|
8
8
|
// Mock checkBffSession from BFF composable
|
|
@@ -13,19 +13,22 @@ vi.mock("../../composables/useBffAuth", () => ({
|
|
|
13
13
|
import { checkBffSession } from "../../composables/useBffAuth";
|
|
14
14
|
const mockCheckBffSession = vi.mocked(checkBffSession);
|
|
15
15
|
|
|
16
|
-
// Helper: run composable inside a real Vue app with router + pinia
|
|
16
|
+
// Helper: run composable inside a real Vue app with router + pinia, the
|
|
17
|
+
// current route at `at`.
|
|
17
18
|
async function withApp<T>(
|
|
18
19
|
fn: () => T,
|
|
20
|
+
at = "/",
|
|
19
21
|
): Promise<{ result: T; router: ReturnType<typeof createRouter> }> {
|
|
20
22
|
const router = createRouter({
|
|
21
23
|
history: createMemoryHistory(),
|
|
22
24
|
routes: [
|
|
23
25
|
{ path: "/", component: { template: "<div />" } },
|
|
26
|
+
{ path: "/login", component: { template: "<div />" } },
|
|
24
27
|
{ path: "/account", component: { template: "<div />" } },
|
|
25
28
|
{ path: "/admin", component: { template: "<div />" } },
|
|
26
29
|
],
|
|
27
30
|
});
|
|
28
|
-
await router.push(
|
|
31
|
+
await router.push(at);
|
|
29
32
|
await router.isReady();
|
|
30
33
|
|
|
31
34
|
const pinia = createPinia();
|
|
@@ -47,6 +50,50 @@ async function withApp<T>(
|
|
|
47
50
|
return { result: result!, router };
|
|
48
51
|
}
|
|
49
52
|
|
|
53
|
+
const ISSUER = "https://sid.example.com";
|
|
54
|
+
const HANDLE = "0123456789abcdef0123456789abcdef";
|
|
55
|
+
const CONTINUATION = `${ISSUER}/i/${HANDLE}/oauth2/authorize?client_id=app&request_uri=${encodeURIComponent("urn:ietf:params:oauth:request_uri:ref_1")}`;
|
|
56
|
+
|
|
57
|
+
describe("authorizationContinuation", () => {
|
|
58
|
+
it("accepts the continuation of a request at the issuer", () => {
|
|
59
|
+
expect(authorizationContinuation(CONTINUATION, ISSUER)).toBe(CONTINUATION);
|
|
60
|
+
// The origin may be given as any URL of the installation.
|
|
61
|
+
expect(authorizationContinuation(CONTINUATION, `${ISSUER}/account`)).toBe(
|
|
62
|
+
CONTINUATION,
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Each substitution an attacker could make in `rd` is refused: another
|
|
67
|
+
// origin, scheme or port, credentials, a fragment, another path, a full
|
|
68
|
+
// request instead of a reference, extra or missing parameters.
|
|
69
|
+
it("refuses everything else", () => {
|
|
70
|
+
const ref = encodeURIComponent("urn:ietf:params:oauth:request_uri:ref_1");
|
|
71
|
+
const at = (base: string) =>
|
|
72
|
+
`${base}/i/${HANDLE}/oauth2/authorize?client_id=app&request_uri=${ref}`;
|
|
73
|
+
for (const rd of [
|
|
74
|
+
undefined,
|
|
75
|
+
["x"],
|
|
76
|
+
"",
|
|
77
|
+
"not a url",
|
|
78
|
+
"javascript:alert(1)",
|
|
79
|
+
at("https://evil.example.com"),
|
|
80
|
+
at("http://sid.example.com"),
|
|
81
|
+
at("https://sid.example.com:8443"),
|
|
82
|
+
at("https://user:pw@sid.example.com"),
|
|
83
|
+
`${CONTINUATION}#x`,
|
|
84
|
+
`${ISSUER}/i/${HANDLE}/oauth2/token?client_id=app&request_uri=${ref}`,
|
|
85
|
+
`${ISSUER}/i/NOTAHANDLE/oauth2/authorize?client_id=app&request_uri=${ref}`,
|
|
86
|
+
`${ISSUER}/i/${HANDLE}/oauth2/authorize?client_id=app&redirect_uri=https%3A%2F%2Fevil.example.com`,
|
|
87
|
+
`${CONTINUATION}&redirect_uri=https%3A%2F%2Fevil.example.com`,
|
|
88
|
+
`${ISSUER}/i/${HANDLE}/oauth2/authorize?client_id=app&request_uri=https%3A%2F%2Fevil`,
|
|
89
|
+
`${ISSUER}/i/${HANDLE}/oauth2/authorize?client_id=&request_uri=${ref}`,
|
|
90
|
+
]) {
|
|
91
|
+
expect(authorizationContinuation(rd, ISSUER), String(rd)).toBeNull();
|
|
92
|
+
}
|
|
93
|
+
expect(authorizationContinuation(CONTINUATION, "not a url")).toBeNull();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
50
97
|
describe("useLoginSuccess (BFF cookie mode)", () => {
|
|
51
98
|
beforeEach(() => {
|
|
52
99
|
vi.clearAllMocks();
|
|
@@ -97,6 +144,53 @@ describe("useLoginSuccess (BFF cookie mode)", () => {
|
|
|
97
144
|
expect(auth.isAuthenticated).toBe(false);
|
|
98
145
|
});
|
|
99
146
|
|
|
147
|
+
// A sign-in an authorization request sent the user to returns the browser
|
|
148
|
+
// to that request's continuation, and does not touch the app's session.
|
|
149
|
+
it("returns to the authorization request it was sent from", async () => {
|
|
150
|
+
const leaveTo = vi.fn();
|
|
151
|
+
const { result, router } = await withApp(
|
|
152
|
+
() => useLoginSuccess("/account", { issuerOrigin: ISSUER, leaveTo }),
|
|
153
|
+
`/login?rd=${encodeURIComponent(CONTINUATION)}`,
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
await result.onLoginSuccess();
|
|
157
|
+
|
|
158
|
+
expect(leaveTo).toHaveBeenCalledWith(CONTINUATION);
|
|
159
|
+
expect(mockCheckBffSession).not.toHaveBeenCalled();
|
|
160
|
+
expect(router.currentRoute.value.path).toBe("/login");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// An `rd` that is not exactly a continuation at this installation's issuer
|
|
164
|
+
// is ignored: the user lands in the app, never at the given address.
|
|
165
|
+
it("ignores an rd it did not validate", async () => {
|
|
166
|
+
mockCheckBffSession.mockResolvedValue(null);
|
|
167
|
+
const leaveTo = vi.fn();
|
|
168
|
+
const { result, router } = await withApp(
|
|
169
|
+
() => useLoginSuccess("/account", { issuerOrigin: ISSUER, leaveTo }),
|
|
170
|
+
`/login?rd=${encodeURIComponent("https://evil.example.com/")}`,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
await result.onLoginSuccess();
|
|
174
|
+
|
|
175
|
+
expect(leaveTo).not.toHaveBeenCalled();
|
|
176
|
+
expect(router.currentRoute.value.path).toBe("/account");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Without an issuer origin no rd is followed at all.
|
|
180
|
+
it("follows no rd without an issuer origin", async () => {
|
|
181
|
+
mockCheckBffSession.mockResolvedValue(null);
|
|
182
|
+
const leaveTo = vi.fn();
|
|
183
|
+
const { result, router } = await withApp(
|
|
184
|
+
() => useLoginSuccess("/account", { leaveTo }),
|
|
185
|
+
`/login?rd=${encodeURIComponent(CONTINUATION)}`,
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
await result.onLoginSuccess();
|
|
189
|
+
|
|
190
|
+
expect(leaveTo).not.toHaveBeenCalled();
|
|
191
|
+
expect(router.currentRoute.value.path).toBe("/account");
|
|
192
|
+
});
|
|
193
|
+
|
|
100
194
|
it("navigates when BFF session returns null (cookie not yet set)", async () => {
|
|
101
195
|
mockCheckBffSession.mockResolvedValue(null);
|
|
102
196
|
|