@aooth/auth-moost 0.1.8 → 0.1.10
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/dist/atscript/index.d.mts +2 -2
- package/dist/atscript/index.mjs +2 -2
- package/dist/{forms-Bkr7ECKu.mjs → forms-11EDZgS1.mjs} +77 -10
- package/dist/index.d.mts +449 -82
- package/dist/index.mjs +875 -132
- package/package.json +19 -19
- package/src/atscript/models/forms.as +164 -26
- package/src/atscript/models/forms.as.d.ts +85 -12
package/dist/index.d.mts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { Mate, TConsoleBase, TInterceptorDef, TMateParamMeta, TMoostMetadata } from "moost";
|
|
2
2
|
import { AuthContext, AuthContext as AuthContext$1, AuthCredential, AuthEmailEvent, AuthEmailKind, AuthEmailKind as AuthEmailKind$1, AuthSmsEvent, AuthSmsKind, BuildMagicLinkUrl, BuildMagicLinkUrl as BuildMagicLinkUrl$1, CredentialMetadata, EmailSender, EmailSender as EmailSender$1, EnrichedSession, EnrichedSession as EnrichedSession$1, IssueResult, IssueResult as IssueResult$1, SessionEnricher, SessionEnricher as SessionEnricher$1, SessionInfo, SessionInfo as SessionInfo$1, SmsSender, generateMagicLinkToken } from "@aooth/auth";
|
|
3
3
|
import { TCookieAttributesInput } from "@wooksjs/event-http";
|
|
4
|
-
import { FederatedIdentityStore, FederatedProfileSnapshot, TransferablePolicy, TrustedDeviceRecord, UserCredentials, UserService } from "@aooth/user";
|
|
4
|
+
import { FederatedIdentityStore, FederatedProfileSnapshot, MfaMethod, TransferablePolicy, TrustedDeviceRecord, UserCredentials, UserService } from "@aooth/user";
|
|
5
5
|
import { FinishWfOpts, WfFinished, useAtscriptWf } from "@atscript/moost-wf";
|
|
6
6
|
import { MoostWf, WfOutlet, WfOutletTokenConfig, WfStateStrategy } from "@moostjs/event-wf";
|
|
7
7
|
import { FederatedLoginService, NormalizedProfile, OAuthProviderRegistry } from "@aooth/idp";
|
|
8
8
|
import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
|
|
9
|
-
import { AuthCodeStore, ClientRedirectPolicy, PendingAuthorizationStore } from "@aooth/auth/authz";
|
|
9
|
+
import { AuthCodeStore, ClientRedirectPolicy, IdTokenSigner, OidcClaimsResolver, PendingAuthorizationStore } from "@aooth/auth/authz";
|
|
10
10
|
|
|
11
11
|
//#region src/auth.config.d.ts
|
|
12
12
|
/** Resolved cookie attributes. Same shape is used for both access + refresh. */
|
|
@@ -837,25 +837,50 @@ declare const FEDERATED_IDENTITY_STORE_TOKEN = "aooth:FederatedIdentityStore";
|
|
|
837
837
|
interface TokenError {
|
|
838
838
|
error: string;
|
|
839
839
|
}
|
|
840
|
+
/** OIDC token-endpoint success (RFC 6749 + OIDC Core). `id_token` for OIDC clients; `access_token` per registration. */
|
|
841
|
+
interface TokenSuccess {
|
|
842
|
+
token_type: "Bearer";
|
|
843
|
+
access_token?: string;
|
|
844
|
+
id_token?: string;
|
|
845
|
+
expires_in?: number;
|
|
846
|
+
/** The authenticated user id (`sub`) — convenience for a CLI; not part of the OIDC token response. */
|
|
847
|
+
userId: string;
|
|
848
|
+
}
|
|
849
|
+
/** A minimal OIDC discovery document (`/.well-known/openid-configuration`). */
|
|
850
|
+
interface OidcDiscoveryDocument {
|
|
851
|
+
issuer: string;
|
|
852
|
+
authorization_endpoint: string;
|
|
853
|
+
token_endpoint: string;
|
|
854
|
+
jwks_uri: string;
|
|
855
|
+
response_types_supported: string[];
|
|
856
|
+
grant_types_supported: string[];
|
|
857
|
+
subject_types_supported: string[];
|
|
858
|
+
id_token_signing_alg_values_supported: string[];
|
|
859
|
+
scopes_supported: string[];
|
|
860
|
+
code_challenge_methods_supported: string[];
|
|
861
|
+
token_endpoint_auth_methods_supported: string[];
|
|
862
|
+
}
|
|
840
863
|
/**
|
|
841
|
-
* The authorization-server endpoints (AUTH-SERVER.md
|
|
842
|
-
*
|
|
843
|
-
*
|
|
844
|
-
* (Tier 2)
|
|
845
|
-
*
|
|
864
|
+
* The authorization-server endpoints (AUTH-SERVER.md). Turns the existing login
|
|
865
|
+
* workflow into an OAuth/OIDC authorization server for the app's OWN clients — a
|
|
866
|
+
* local CLI on a loopback redirect (Tier 1) and registered first-party services
|
|
867
|
+
* (Tier 2, `id_token` / "Sign in with <main app>"). One authorization-code + PKCE
|
|
868
|
+
* flow; the only things that vary are the injected {@link ClientRedirectPolicy}
|
|
869
|
+
* and whether an {@link IdTokenSigner} is wired.
|
|
846
870
|
*
|
|
847
871
|
* - `GET /auth/authorize` — validate the client + `redirect_uri` (the policy),
|
|
848
|
-
* record a {@link PendingAuthorizationStore} entry
|
|
849
|
-
* login page carrying the opaque `handle`. The login
|
|
850
|
-
*
|
|
851
|
-
* — this controller never runs the login itself.
|
|
872
|
+
* record a {@link PendingAuthorizationStore} entry (authority fixed HERE), and
|
|
873
|
+
* 302 the browser to the login page carrying the opaque `handle`. The login
|
|
874
|
+
* workflow's `mint-authz-code` terminal delivers the code to the client.
|
|
852
875
|
* - `POST /auth/token` — the back-channel: consume the single-use code, verify
|
|
853
|
-
* PKCE,
|
|
854
|
-
* HERE, off the browser, so nothing long-lived
|
|
876
|
+
* PKCE, authenticate the client (Tier 2), and mint the access token and/or the
|
|
877
|
+
* `id_token`. Minted HERE, off the browser, so nothing long-lived rides a URL.
|
|
878
|
+
* - `GET /auth/.well-known/openid-configuration` + `GET /auth/jwks` — OIDC
|
|
879
|
+
* discovery + the signer's public JWKS (Tier 2 only; 404 without a signer).
|
|
855
880
|
*
|
|
856
|
-
*
|
|
857
|
-
* `/authorize` time
|
|
858
|
-
* authorization
|
|
881
|
+
* All routes are `@Public()`. The grant's authority (token policy, `id_token`
|
|
882
|
+
* intent, audience, scope) is fixed at `/authorize` time and recorded on the
|
|
883
|
+
* pending authorization + the issued code — never inferred at `/token`.
|
|
859
884
|
*/
|
|
860
885
|
declare class AuthorizeController {
|
|
861
886
|
protected readonly auth: AuthCredential;
|
|
@@ -863,6 +888,20 @@ declare class AuthorizeController {
|
|
|
863
888
|
protected readonly pending: PendingAuthorizationStore;
|
|
864
889
|
protected readonly codes: AuthCodeStore;
|
|
865
890
|
constructor(auth: AuthCredential, policy: ClientRedirectPolicy, pending: PendingAuthorizationStore, codes: AuthCodeStore);
|
|
891
|
+
/**
|
|
892
|
+
* The Tier-2 OIDC `id_token` signer, or `undefined` for a Tier-1-only (CLI)
|
|
893
|
+
* deployment — then discovery / `/auth/jwks` 404 and no `id_token` is minted.
|
|
894
|
+
* **Override** in a subclass to enable OIDC (return one `IdTokenSigner` whose
|
|
895
|
+
* issuer is `{origin}/auth`). A plain getter rather than a DI token because an
|
|
896
|
+
* OPTIONAL `@Inject`/`@Optional` dependency panics in moost's `resolveMoost`
|
|
897
|
+
* route-table pass (`useHandlerPaths`); a method has nothing for it to resolve.
|
|
898
|
+
*/
|
|
899
|
+
protected getIdTokenSigner(): IdTokenSigner | undefined;
|
|
900
|
+
/**
|
|
901
|
+
* The Tier-2 OIDC profile-claims resolver. Defaults to a no-op (`sub`-only
|
|
902
|
+
* tokens); **override** to emit `email` / `name` / … from your user record.
|
|
903
|
+
*/
|
|
904
|
+
protected getOidcClaimsResolver(): OidcClaimsResolver;
|
|
866
905
|
/**
|
|
867
906
|
* The SPA login route the authorize request bounces to. The opaque pending-auth
|
|
868
907
|
* `handle` is appended as `?authz=`; the SPA forwards it into the login
|
|
@@ -870,18 +909,23 @@ declare class AuthorizeController {
|
|
|
870
909
|
* custom login path.
|
|
871
910
|
*/
|
|
872
911
|
protected loginPath(): string;
|
|
873
|
-
authorize(responseType: string | undefined, redirectUri: string | undefined, clientId: string | undefined, state: string | undefined, codeChallenge: string | undefined, codeChallengeMethod: string | undefined, scope: string | undefined): Promise<string>;
|
|
912
|
+
authorize(responseType: string | undefined, redirectUri: string | undefined, clientId: string | undefined, state: string | undefined, codeChallenge: string | undefined, codeChallengeMethod: string | undefined, scope: string | undefined, nonce: string | undefined): Promise<string>;
|
|
874
913
|
token(body: {
|
|
875
914
|
grant_type?: string;
|
|
876
915
|
code?: string;
|
|
877
916
|
code_verifier?: string;
|
|
878
917
|
client_id?: string;
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
918
|
+
client_secret?: string;
|
|
919
|
+
} | undefined): Promise<TokenSuccess | TokenError>;
|
|
920
|
+
/**
|
|
921
|
+
* OIDC discovery (Tier 2). Derives every endpoint from the signer's `issuer`
|
|
922
|
+
* (configured as `{origin}/auth`), so a relying `OidcProvider` configured with
|
|
923
|
+
* the same `issuer` resolves `/authorize`, `/token`, and `/jwks` automatically.
|
|
924
|
+
* 404 when no signer is wired (Tier-1-only deployment).
|
|
925
|
+
*/
|
|
926
|
+
discovery(): OidcDiscoveryDocument | TokenError;
|
|
927
|
+
/** The signer's public JWKS (Tier 2). 404 when no signer is wired. */
|
|
928
|
+
jwks(): Promise<Awaited<ReturnType<IdTokenSigner["jwks"]>>> | TokenError;
|
|
885
929
|
/** Fail soft: 302 the validated client redirect with an `?error=` (+ echoed `state`). */
|
|
886
930
|
protected redirectError(redirectUri: string, error: string, state: string | undefined): string;
|
|
887
931
|
}
|
|
@@ -932,7 +976,8 @@ declare const AUTH_CODE_STORE_TOKEN = "aooth:AuthCodeStore";
|
|
|
932
976
|
/**
|
|
933
977
|
* DI token for the {@link import("@aooth/auth/authz").ClientRedirectPolicy} — an
|
|
934
978
|
* interface, so it has no class reference to inject by. Provide the concrete
|
|
935
|
-
* policy (e.g. `new LoopbackClientPolicy()`
|
|
979
|
+
* policy (e.g. `new LoopbackClientPolicy()`, a `RegisteredClientPolicy`, or a
|
|
980
|
+
* `CompositeClientPolicy` of both) under this string.
|
|
936
981
|
*/
|
|
937
982
|
declare const CLIENT_REDIRECT_POLICY_TOKEN = "aooth:ClientRedirectPolicy";
|
|
938
983
|
//#endregion
|
|
@@ -1008,8 +1053,21 @@ interface AuthWfMfaEnrollState {
|
|
|
1008
1053
|
secret?: string;
|
|
1009
1054
|
uri?: string;
|
|
1010
1055
|
availableTransports?: MfaTransport[];
|
|
1011
|
-
|
|
1056
|
+
/**
|
|
1057
|
+
* Drives skip / cancel visibility on the enrolment forms:
|
|
1058
|
+
* - `'optional'` — login/invite first-time opt-in: "Skip for now" shows.
|
|
1059
|
+
* - `'required'` — forced enrolment: neither skip nor cancel.
|
|
1060
|
+
* - `'manage'` — the standalone manage-MFA flow (user opened it on purpose):
|
|
1061
|
+
* "Skip for now" is hidden, a "Cancel" action shows instead.
|
|
1062
|
+
*/
|
|
1063
|
+
mode?: "required" | "optional" | "manage";
|
|
1012
1064
|
done?: boolean;
|
|
1065
|
+
/**
|
|
1066
|
+
* Gates the standalone `enroll-totp-qr` step (TOTP only). Set once the user
|
|
1067
|
+
* has been shown the QR/secret and clicked Continue, so the QR pause fires
|
|
1068
|
+
* before — not alongside — the code-entry step. Shared by both surfaces.
|
|
1069
|
+
*/
|
|
1070
|
+
qrSeen?: boolean;
|
|
1013
1071
|
/**
|
|
1014
1072
|
* When set, `enroll-confirm` does NOT make the freshly-confirmed method the
|
|
1015
1073
|
* user's default. Set by `init-add-mfa` (the standalone add-MFA flow) when the
|
|
@@ -1311,12 +1369,52 @@ interface AuthWfPendingLinkState {
|
|
|
1311
1369
|
interface AuthWfAddMfaState {
|
|
1312
1370
|
/**
|
|
1313
1371
|
* Transports the user has NOT yet confirmed = resolved
|
|
1314
|
-
* `availableTransports` minus already-enrolled. The
|
|
1315
|
-
*
|
|
1316
|
-
*
|
|
1317
|
-
*
|
|
1372
|
+
* `availableTransports` minus already-enrolled. The manage menu offers these
|
|
1373
|
+
* as "Add" options (a zero-MFA user goes straight to the enrol picker over
|
|
1374
|
+
* the same list). `finish-add-mfa` reads it to distinguish "nothing to add"
|
|
1375
|
+
* from "user cancelled".
|
|
1318
1376
|
*/
|
|
1319
1377
|
candidates?: MfaTransport[];
|
|
1378
|
+
/**
|
|
1379
|
+
* Transports the user may NOT change or remove via this flow — resolved by
|
|
1380
|
+
* `resolveLockedMfaTransports` (default: none). A customer locks a factor
|
|
1381
|
+
* whose value IS a login handle (e.g. the MFA email equals the
|
|
1382
|
+
* `@aooth.user.email` handle) so the user can't silently desync it here.
|
|
1383
|
+
* Locked transports are omitted from the menu's Change/Remove options and
|
|
1384
|
+
* re-checked server-side in `manage-menu` / `confirm-remove-mfa`.
|
|
1385
|
+
*/
|
|
1386
|
+
locked?: MfaTransport[];
|
|
1387
|
+
/**
|
|
1388
|
+
* `true` when the user already has ≥1 confirmed method, so the manage flow
|
|
1389
|
+
* must step-up (re-verify identity) BEFORE offering add/change/remove, and
|
|
1390
|
+
* shows the management menu. The METHOD of step-up is `stepUpMode`. `false`
|
|
1391
|
+
* (zero confirmed methods) skips both — the flow degrades to the first-time
|
|
1392
|
+
* enrol picker (the opt-in path).
|
|
1393
|
+
*/
|
|
1394
|
+
stepUpRequired?: boolean;
|
|
1395
|
+
/**
|
|
1396
|
+
* How the step-up is performed (set by `init-add-mfa` when `stepUpRequired`):
|
|
1397
|
+
* - `'mfa'` — the user has ≥1 confirmed factor whose kind is still in the
|
|
1398
|
+
* policy's `availableTransports`, so `mfaStepUpLoop` challenges it.
|
|
1399
|
+
* - `'password'` — every confirmed factor is of a kind the policy no longer
|
|
1400
|
+
* allows (none is challengeable), so `manage-password-reauth` re-verifies
|
|
1401
|
+
* via the account password instead. Fail-closed fallback that keeps "prove
|
|
1402
|
+
* identity before managing factors" intact even after a policy tightening
|
|
1403
|
+
* orphaned the only enrolled factor.
|
|
1404
|
+
*/
|
|
1405
|
+
stepUpMode?: "mfa" | "password";
|
|
1406
|
+
/**
|
|
1407
|
+
* Set once the step-up factor verifies AND the flow has swapped off the
|
|
1408
|
+
* encapsulated start onto the durable `store` strategy (server-anchored,
|
|
1409
|
+
* replay-resistant). Gates the one-time swap + the management menu.
|
|
1410
|
+
*/
|
|
1411
|
+
stepUpDone?: boolean;
|
|
1412
|
+
/** The management action the user picked on the menu. */
|
|
1413
|
+
action?: "add" | "replace" | "remove";
|
|
1414
|
+
/** The transport the chosen `action` applies to. */
|
|
1415
|
+
target?: MfaTransport;
|
|
1416
|
+
/** Set by `confirm-remove-mfa` so `finish-add-mfa` can report which factor was removed. */
|
|
1417
|
+
removed?: MfaTransport;
|
|
1320
1418
|
}
|
|
1321
1419
|
/**
|
|
1322
1420
|
* Self-signup flow state. Populated by `init-signup` (policy from
|
|
@@ -1412,12 +1510,15 @@ interface AuthWfPublicState {
|
|
|
1412
1510
|
* Mirrors `ctx.mfaEnroll` — only what the enrolment forms display.
|
|
1413
1511
|
* `address` stays internal (user-typed, no need to bounce it back).
|
|
1414
1512
|
*/
|
|
1415
|
-
mfaEnroll?:
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1513
|
+
mfaEnroll?: Pick<AuthWfMfaEnrollState, "method" | "mode" | "availableTransports" | "secret" | "uri">;
|
|
1514
|
+
/**
|
|
1515
|
+
* Mirrors the manage-MFA menu inputs — the un-enrolled transports the user
|
|
1516
|
+
* can Add and the locked transports to omit from Change/Remove. The enrolled
|
|
1517
|
+
* method list the menu cross-references is `public.mfa.enrolledMethods`.
|
|
1518
|
+
*/
|
|
1519
|
+
manage?: {
|
|
1520
|
+
candidates?: MfaTransport[];
|
|
1521
|
+
locked?: MfaTransport[];
|
|
1421
1522
|
};
|
|
1422
1523
|
/** Mirrors `ctx.defaults` — prefill source for the recovery email field. */
|
|
1423
1524
|
defaults?: {
|
|
@@ -1460,6 +1561,13 @@ interface AuthWfCtx {
|
|
|
1460
1561
|
mfaEnroll?: AuthWfMfaEnrollState;
|
|
1461
1562
|
password?: AuthWfPasswordUiState;
|
|
1462
1563
|
completion?: AuthWfCompletionState;
|
|
1564
|
+
/**
|
|
1565
|
+
* Marks that the `promote-to-handle` @Step has already run for this flow, so
|
|
1566
|
+
* it fires once after a channel is confirmed and is skipped on every later
|
|
1567
|
+
* resume (the store write is idempotent, but re-running it each resume would
|
|
1568
|
+
* be wasteful). Server-only — never `@wf.context.pass`'d.
|
|
1569
|
+
*/
|
|
1570
|
+
promoteToHandleDone?: boolean;
|
|
1463
1571
|
alternateCredentials?: AuthWfAltCredsPolicy;
|
|
1464
1572
|
deviceTrust?: AuthWfDeviceTrustPolicy;
|
|
1465
1573
|
enrollment?: AuthWfEnrollmentPolicy;
|
|
@@ -1548,8 +1656,12 @@ interface AuthWorkflowOpts {
|
|
|
1548
1656
|
askEmail?: TAtscriptAnnotatedType;
|
|
1549
1657
|
askPhone?: TAtscriptAnnotatedType;
|
|
1550
1658
|
enrollPickMethod?: TAtscriptAnnotatedType;
|
|
1551
|
-
enrollAddress?: TAtscriptAnnotatedType;
|
|
1552
|
-
|
|
1659
|
+
enrollAddress?: TAtscriptAnnotatedType; /** TOTP QR step — shown before the code-entry pause (manage + opt-in). */
|
|
1660
|
+
enrollTotpQr?: TAtscriptAnnotatedType;
|
|
1661
|
+
enrollConfirm?: TAtscriptAnnotatedType; /** Manage-MFA menu (Add / Change / Remove) shown after step-up. */
|
|
1662
|
+
manageMfa?: TAtscriptAnnotatedType; /** Confirm-removal pause for the manage-MFA "Remove" action. */
|
|
1663
|
+
removeMfaConfirm?: TAtscriptAnnotatedType; /** Password re-auth — step-up fallback when no factor is MFA-challengeable. */
|
|
1664
|
+
passwordReauth?: TAtscriptAnnotatedType;
|
|
1553
1665
|
select2fa?: TAtscriptAnnotatedType;
|
|
1554
1666
|
mfaCode?: TAtscriptAnnotatedType;
|
|
1555
1667
|
pincode?: TAtscriptAnnotatedType;
|
|
@@ -1594,7 +1706,11 @@ interface ResolvedAuthWorkflowOpts {
|
|
|
1594
1706
|
askPhone: TAtscriptAnnotatedType;
|
|
1595
1707
|
enrollPickMethod: TAtscriptAnnotatedType;
|
|
1596
1708
|
enrollAddress: TAtscriptAnnotatedType;
|
|
1709
|
+
enrollTotpQr: TAtscriptAnnotatedType;
|
|
1597
1710
|
enrollConfirm: TAtscriptAnnotatedType;
|
|
1711
|
+
manageMfa: TAtscriptAnnotatedType;
|
|
1712
|
+
removeMfaConfirm: TAtscriptAnnotatedType;
|
|
1713
|
+
passwordReauth: TAtscriptAnnotatedType;
|
|
1598
1714
|
select2fa: TAtscriptAnnotatedType;
|
|
1599
1715
|
mfaCode: TAtscriptAnnotatedType;
|
|
1600
1716
|
pincode: TAtscriptAnnotatedType;
|
|
@@ -1624,7 +1740,7 @@ type AuthDeliveryPayload = {
|
|
|
1624
1740
|
expiresInMs: number;
|
|
1625
1741
|
} | {
|
|
1626
1742
|
kind: "recovery-pincode";
|
|
1627
|
-
channel: "email";
|
|
1743
|
+
channel: "sms" | "email";
|
|
1628
1744
|
recipient: string;
|
|
1629
1745
|
code: string;
|
|
1630
1746
|
expiresInMs: number;
|
|
@@ -1869,6 +1985,28 @@ declare class AuthWorkflow {
|
|
|
1869
1985
|
* Reached from login.flow + invite.start.
|
|
1870
1986
|
*/
|
|
1871
1987
|
protected resolveMfaPolicy(_ctx: AuthWfCtx): NonNullable<AuthWfCtx["mfaPolicy"]> | Promise<NonNullable<AuthWfCtx["mfaPolicy"]>>;
|
|
1988
|
+
/**
|
|
1989
|
+
* Transports the user may NOT change or remove via the manage-MFA flow.
|
|
1990
|
+
* Default: none — every factor is freely manageable. Reached from
|
|
1991
|
+
* `auth/add-mfa/flow` (`prepare-locked-mfa-transports`).
|
|
1992
|
+
*
|
|
1993
|
+
* Override to forbid changing a factor whose value IS a login handle — e.g.
|
|
1994
|
+
* the MFA `email` equals the `@aooth.user.email` handle, so letting the user
|
|
1995
|
+
* swap it here would desync identity. A typical override loads the user and
|
|
1996
|
+
* compares each enrolled channel value against the boot-resolved handle
|
|
1997
|
+
* fields (`getAoothUserHandleSpec(...).emailField` / `.phoneField`):
|
|
1998
|
+
*
|
|
1999
|
+
* ```ts
|
|
2000
|
+
* protected async resolveLockedMfaTransports(ctx: AuthWfCtx): Promise<MfaTransport[]> {
|
|
2001
|
+
* const user = await this.users.getUser(ctx.subject!);
|
|
2002
|
+
* const locked: MfaTransport[] = [];
|
|
2003
|
+
* const email = user.mfa?.methods?.find((m) => m.name === "email" && m.confirmed);
|
|
2004
|
+
* if (email && email.value === (user as { email?: string }).email) locked.push("email");
|
|
2005
|
+
* return locked;
|
|
2006
|
+
* }
|
|
2007
|
+
* ```
|
|
2008
|
+
*/
|
|
2009
|
+
protected resolveLockedMfaTransports(_ctx: AuthWfCtx): MfaTransport[] | Promise<MfaTransport[]>;
|
|
1872
2010
|
/**
|
|
1873
2011
|
* Resolve the channel-OTP disclosure copy rendered beneath the email/phone
|
|
1874
2012
|
* input on `AskEmailForm` / `AskPhoneForm`. Reached from login.flow Phase 3.
|
|
@@ -1939,6 +2077,79 @@ declare class AuthWorkflow {
|
|
|
1939
2077
|
address: string;
|
|
1940
2078
|
channel: "sms" | "email";
|
|
1941
2079
|
}>;
|
|
2080
|
+
/**
|
|
2081
|
+
* Resolve the recovery OTP `{ address, channel }` for the chosen delivery
|
|
2082
|
+
* `source`.
|
|
2083
|
+
*
|
|
2084
|
+
* - `"typed"` (M1): the address is the typed recovery identifier (`ctx.email`)
|
|
2085
|
+
* — identifier == destination, so no cross-account redirect — and the
|
|
2086
|
+
* channel comes from `resolveRecoveryChannel` (identifier-shape inference).
|
|
2087
|
+
* - `"registered"` (M2): the address is read off a confirmed MFA method on the
|
|
2088
|
+
* row (`selectRecoveryRegisteredMethod`) and the channel is that method's own
|
|
2089
|
+
* kind. The user only typed an account identifier; the destination is a
|
|
2090
|
+
* pre-verified channel they already control, so this also can't redirect
|
|
2091
|
+
* cross-account. `request`'s M2 guard normally generic-finishes any row with
|
|
2092
|
+
* no deliverable method up front; if the method is deleted in the narrow
|
|
2093
|
+
* window between that guard and this send (e.g. on a resend), this throws
|
|
2094
|
+
* `RecoveryMethodUnavailableError`, which `pincode-send` degrades to the
|
|
2095
|
+
* same generic finish — never a distinguishable 500.
|
|
2096
|
+
*/
|
|
2097
|
+
private recoveryPincodeTarget;
|
|
2098
|
+
/**
|
|
2099
|
+
* Recovery OTP delivery channel. The address is ALWAYS the typed recovery
|
|
2100
|
+
* identifier (`ctx.email`) — symmetric with how email recovery already works:
|
|
2101
|
+
* the OTP goes to the value the user typed, which is the handle that resolved
|
|
2102
|
+
* the account (`findByHandle`), so identifier == destination and there is no
|
|
2103
|
+
* cross-account redirect. Default `"email"`. A deployment whose recovery form
|
|
2104
|
+
* accepts a phone overrides this to route SMS (e.g. infer from the identifier
|
|
2105
|
+
* shape) — see the demo's `DemoAuthWorkflow`. Recovery picks ONE channel per
|
|
2106
|
+
* run, so the single `resendAllowedAt` cooldown gate still suffices.
|
|
2107
|
+
*/
|
|
2108
|
+
protected resolveRecoveryChannel(_ctx: AuthWfCtx): "email" | "sms" | Promise<"email" | "sms">;
|
|
2109
|
+
/**
|
|
2110
|
+
* Recovery OTP delivery model. Two options:
|
|
2111
|
+
*
|
|
2112
|
+
* - `"typed"` (default — M1): the OTP goes to the recovery identifier the user
|
|
2113
|
+
* types. Identifier == destination, so there is no cross-account redirect;
|
|
2114
|
+
* `resolveRecoveryChannel` picks email vs SMS from the identifier shape.
|
|
2115
|
+
* - `"registered"` (M2): the user enters only an account identifier (e.g. a
|
|
2116
|
+
* username) and the OTP is delivered to a channel **already verified on the
|
|
2117
|
+
* row** — `selectRecoveryRegisteredMethod` picks the confirmed MFA method;
|
|
2118
|
+
* the destination is never taken from user input, so it cannot be redirected
|
|
2119
|
+
* to an attacker-controlled address. A row with no deliverable confirmed
|
|
2120
|
+
* method finishes with the generic anti-enumeration envelope (see `request`).
|
|
2121
|
+
*
|
|
2122
|
+
* Consulted inline by `request` (no-method guard) and `recoveryPincodeTarget`
|
|
2123
|
+
* — no `prepare-*` step, mirroring `resolveRecoveryChannel`. Override to arm
|
|
2124
|
+
* M2 (per-tenant / per-variant); see the demo's `DemoAuthWorkflow`.
|
|
2125
|
+
*/
|
|
2126
|
+
protected resolveRecoveryDeliverySource(_ctx: AuthWfCtx): "typed" | "registered" | Promise<"typed" | "registered">;
|
|
2127
|
+
/**
|
|
2128
|
+
* Pick the confirmed MFA method a registered-channel recovery (M2) delivers
|
|
2129
|
+
* its OTP to. Prefers a confirmed SMS method, then a confirmed email method —
|
|
2130
|
+
* phone-recovery-first. TOTP carries no deliverable address and is skipped.
|
|
2131
|
+
* Returns `null` when the row has no deliverable confirmed method; the caller
|
|
2132
|
+
* turns that into the anti-enumeration generic finish. Stays sync (operates on
|
|
2133
|
+
* an already-loaded row); override to change the selection policy (e.g. honour
|
|
2134
|
+
* the user's `mfa.defaultMethod`).
|
|
2135
|
+
*/
|
|
2136
|
+
protected selectRecoveryRegisteredMethod(user: UserCredentials): MfaMethod | null;
|
|
2137
|
+
/**
|
|
2138
|
+
* Decide which login-handle column a freshly-confirmed channel should be
|
|
2139
|
+
* promoted into — so a verified email/phone becomes a login + recovery
|
|
2140
|
+
* handle (`findByHandle`) automatically. Returns the target field name, or
|
|
2141
|
+
* `undefined` to NOT promote (the default).
|
|
2142
|
+
*
|
|
2143
|
+
* Default is OFF: the handle columns are declared via `@aooth.user.*`
|
|
2144
|
+
* annotations on the consumer's concrete model and resolved ONCE at boot
|
|
2145
|
+
* (`@aooth/arbac-moost`'s `getAoothUserHandleSpec`) — `AuthWorkflow` holds no
|
|
2146
|
+
* handle to that model and stays off the per-request reflection path. A
|
|
2147
|
+
* deployment turns promotion ON by overriding this to return the
|
|
2148
|
+
* boot-resolved `emailField` / `phoneField` for the channel — see the demo's
|
|
2149
|
+
* `DemoAuthWorkflow`. `channel` is the wire protocol (`'email'` | `'sms'`),
|
|
2150
|
+
* matching `resolveOtpDisclosure` / the MFA transport.
|
|
2151
|
+
*/
|
|
2152
|
+
protected resolvePromoteHandleField(_ctx: AuthWfCtx, _channel: "email" | "sms"): string | undefined | Promise<string | undefined>;
|
|
1942
2153
|
/**
|
|
1943
2154
|
* Route a form alt-action click to a canonical outcome. Defaults match the
|
|
1944
2155
|
* action ids the bundled `PincodeForm` declares; customers override per
|
|
@@ -2046,13 +2257,60 @@ declare class AuthWorkflow {
|
|
|
2046
2257
|
*/
|
|
2047
2258
|
protected sendEnrollPincode(ctx: AuthWfCtx, address: string, code: string): Promise<void>;
|
|
2048
2259
|
/**
|
|
2049
|
-
*
|
|
2050
|
-
*
|
|
2051
|
-
*
|
|
2052
|
-
*
|
|
2053
|
-
*
|
|
2054
|
-
|
|
2055
|
-
|
|
2260
|
+
* Drop the per-enrolment scratch fields (the `mfaEnroll` provisioning fields +
|
|
2261
|
+
* the pincode timers/`sentTo`) off ctx — the shared teardown used by the
|
|
2262
|
+
* opt-in `skip` / `useDifferentMethod` arms and {@link cancelManageEnrollment}.
|
|
2263
|
+
* Does NOT touch the user record: the enrol trio stages every candidate value
|
|
2264
|
+
* (sms/email address, totp secret) in wf-state and writes it to the store ONLY
|
|
2265
|
+
* on confirm (write-on-confirm), so a bailed enrolment never persisted a
|
|
2266
|
+
* partial row to undo.
|
|
2267
|
+
*/
|
|
2268
|
+
protected clearEnrollScratch(ctx: AuthWfCtx): void;
|
|
2269
|
+
/**
|
|
2270
|
+
* Validate a user-typed MFA address for its transport. Server-side counterpart
|
|
2271
|
+
* to `EnrollAddressForm`'s client `@ui.form.validate` hint — the authoritative
|
|
2272
|
+
* check (a client can bypass the hint). Returns an error string for the form,
|
|
2273
|
+
* or `undefined` when valid. Email must look like an email; SMS is permissive
|
|
2274
|
+
* E.164-ish (normalized by {@link normalizeMfaAddress}). Override for stricter
|
|
2275
|
+
* (e.g. libphonenumber) validation.
|
|
2276
|
+
*/
|
|
2277
|
+
protected validateMfaAddress(method: MfaTransport, value: string): string | undefined;
|
|
2278
|
+
/**
|
|
2279
|
+
* Light normalization of a validated MFA address before it is stored. Default:
|
|
2280
|
+
* trims, and strips spacing/punctuation from SMS numbers while KEEPING the
|
|
2281
|
+
* leading `+` (E.164 canonical form). Email is just trimmed. Override for full
|
|
2282
|
+
* E.164 canonicalization.
|
|
2283
|
+
*/
|
|
2284
|
+
protected normalizeMfaAddress(method: MfaTransport, value: string): string;
|
|
2285
|
+
/**
|
|
2286
|
+
* Abort an in-progress manage-MFA enrolment cleanly: drop the wf-state scratch
|
|
2287
|
+
* and set `ctx.aborted` so the schema breaks to `finish-add-mfa` (cancelled
|
|
2288
|
+
* terminal). Nothing to undo in the user record — the manage flow stages every
|
|
2289
|
+
* candidate value (sms/email address, totp secret) in wf-state and writes it
|
|
2290
|
+
* to the store ONLY on confirm (write-on-confirm), so an in-progress
|
|
2291
|
+
* add/change/replace never touched the existing factors. This is exactly why
|
|
2292
|
+
* a cancel — or a crafted `useDifferentMethod` routed here — can never strand
|
|
2293
|
+
* or clobber a live factor.
|
|
2294
|
+
*/
|
|
2295
|
+
protected cancelManageEnrollment(ctx: AuthWfCtx): void;
|
|
2296
|
+
/**
|
|
2297
|
+
* Shared skip / cancel / useDifferentMethod triage for the enrol-trio steps
|
|
2298
|
+
* that pause after the candidate value is staged (`enroll-totp-qr` +
|
|
2299
|
+
* `enroll-confirm`): `cancel` / `useDifferentMethod` (manage) abort the flow,
|
|
2300
|
+
* `skip` (opt-in) and `useDifferentMethod` (opt-in) drop the wf-state scratch.
|
|
2301
|
+
* Returns `true` when the action terminated the step so the caller can
|
|
2302
|
+
* `return undefined`. (`enroll-pick-method` / `enroll-address` keep their own
|
|
2303
|
+
* preludes — their skip/useDifferentMethod arms diverge from this one.)
|
|
2304
|
+
*
|
|
2305
|
+
* SECURITY: in `'manage'` mode BOTH `cancel` and `useDifferentMethod` (the
|
|
2306
|
+
* manage forms HIDE the latter but it stays in their declared action
|
|
2307
|
+
* whitelist, so a crafted resume can still send it) route through the abort,
|
|
2308
|
+
* which only clears scratch. Because the enrol trio writes the user record
|
|
2309
|
+
* ONLY on confirm (write-on-confirm), an in-progress add/change has touched
|
|
2310
|
+
* nothing in the store — so a cancel/useDifferentMethod can never strand or
|
|
2311
|
+
* clobber the user's live factor, by construction.
|
|
2312
|
+
*/
|
|
2313
|
+
protected handleEnrollExit(ctx: AuthWfCtx, action: string | undefined): boolean;
|
|
2056
2314
|
initLogin(ctx: AuthWfCtx): void;
|
|
2057
2315
|
initInviteAdmin(ctx: AuthWfCtx): void;
|
|
2058
2316
|
initInviteAccept(ctx: AuthWfCtx): void;
|
|
@@ -2070,23 +2328,25 @@ declare class AuthWorkflow {
|
|
|
2070
2328
|
*/
|
|
2071
2329
|
initChangePassword(ctx: AuthWfCtx): void;
|
|
2072
2330
|
/**
|
|
2073
|
-
* Bind the standalone "
|
|
2074
|
-
*
|
|
2075
|
-
*
|
|
2076
|
-
*
|
|
2077
|
-
* `
|
|
2078
|
-
*
|
|
2079
|
-
*
|
|
2080
|
-
*
|
|
2331
|
+
* Bind the standalone "Manage two-factor authentication" flow (add / change /
|
|
2332
|
+
* remove) to the CURRENT authenticated user. Identity comes from the session
|
|
2333
|
+
* (`useAuth().getUserId()`) — never form input — so it is structurally "manage
|
|
2334
|
+
* MY factors". Mirrors `init-change-password`'s arbac gate (`auth.add-mfa` /
|
|
2335
|
+
* `self`): a customer enables the feature with a single
|
|
2336
|
+
* `allow("auth.add-mfa", "*")` grant and forbids it by omitting it.
|
|
2337
|
+
* `getUserId()` throws 401 if unauthenticated — defence in depth on top of the
|
|
2338
|
+
* guarded trigger route.
|
|
2081
2339
|
*
|
|
2082
|
-
*
|
|
2083
|
-
* `
|
|
2084
|
-
*
|
|
2085
|
-
*
|
|
2086
|
-
*
|
|
2087
|
-
*
|
|
2088
|
-
*
|
|
2089
|
-
*
|
|
2340
|
+
* Sets `ctx.mfaPolicy.availableTransports` to the FULL policy set (so the
|
|
2341
|
+
* step-up's `load-enrolled-mfa-methods` can see the confirmed factors to
|
|
2342
|
+
* challenge) and tracks the un-enrolled `candidates` separately on
|
|
2343
|
+
* `ctx.addMfa` for the menu's Add options. `stepUpRequired` is set when the
|
|
2344
|
+
* user has ANY confirmed factor — gating both the step-up and the management
|
|
2345
|
+
* menu; a zero-MFA user skips both and falls through to the first-time enrol
|
|
2346
|
+
* picker (the opt-in path). `stepUpMode` picks the step-up method: `'mfa'`
|
|
2347
|
+
* when a confirmed factor is still challengeable, else `'password'` (a
|
|
2348
|
+
* password re-auth fallback for an orphaned factor). Puts the enrol forms in
|
|
2349
|
+
* `'manage'` mode (Cancel, not "Skip for now") and keeps the existing default.
|
|
2090
2350
|
*/
|
|
2091
2351
|
initAddMfa(ctx: AuthWfCtx): Promise<undefined>;
|
|
2092
2352
|
credentials(ctx: AuthWfCtx): Promise<unknown>;
|
|
@@ -2267,13 +2527,55 @@ declare class AuthWorkflow {
|
|
|
2267
2527
|
*/
|
|
2268
2528
|
finishChangePassword(ctx: AuthWfCtx): Promise<undefined>;
|
|
2269
2529
|
/**
|
|
2270
|
-
* Terminal for the
|
|
2271
|
-
* re-issue, no cookies) —
|
|
2272
|
-
*
|
|
2273
|
-
*
|
|
2274
|
-
* `addMfa.candidates` distinguishes "nothing left to add" from a user cancel.
|
|
2530
|
+
* Terminal for the manage-MFA flow. The user KEEPS their current session (no
|
|
2531
|
+
* re-issue, no cookies) — a plain data finish. Outcomes, in priority order:
|
|
2532
|
+
* removed → changed (`replace` + done) → added (done) → nothing-available
|
|
2533
|
+
* (zero candidates, never had to step-up) → cancelled.
|
|
2275
2534
|
*/
|
|
2276
2535
|
finishAddMfa(ctx: AuthWfCtx): undefined;
|
|
2536
|
+
/**
|
|
2537
|
+
* Resolve which transports the user may NOT change/remove via the manage flow
|
|
2538
|
+
* (calls {@link resolveLockedMfaTransports}) and write them to
|
|
2539
|
+
* `ctx.addMfa.locked`. Mirrors the `prepare-<group>` convention.
|
|
2540
|
+
*/
|
|
2541
|
+
prepareLockedMfaTransports(ctx: AuthWfCtx): undefined | Promise<undefined>;
|
|
2542
|
+
/**
|
|
2543
|
+
* Fires once the step-up factor verifies — anchor the rest of the flow in the
|
|
2544
|
+
* durable `store` strategy (mirrors login's swap-after-credentials): the
|
|
2545
|
+
* pincode becomes single-use server state and the staged new factor lives
|
|
2546
|
+
* server-side instead of in the SPA-held encapsulated token. Degrades to
|
|
2547
|
+
* encapsulated when no durable store is wired (the registry default).
|
|
2548
|
+
*/
|
|
2549
|
+
manageStepUpDone(ctx: AuthWfCtx): undefined;
|
|
2550
|
+
/**
|
|
2551
|
+
* Manage-MFA password re-auth — the step-up FALLBACK when the user's only
|
|
2552
|
+
* confirmed factor(s) are of kinds the policy no longer allows, so nothing is
|
|
2553
|
+
* MFA-challengeable (`addMfa.stepUpMode === "password"`; see `init-add-mfa`).
|
|
2554
|
+
* Pauses on `PasswordReauthForm`, verifies the account password via
|
|
2555
|
+
* `UserService.verifyPassword`, and on success flips `ctx.otp.verified` — the
|
|
2556
|
+
* SAME step-up success signal `mfaStepUpLoop` sets — so `manage-stepup-done`
|
|
2557
|
+
* (swap-to-store) and `manage-menu` proceed identically. `cancel` aborts to
|
|
2558
|
+
* the cancelled terminal (fail closed: no management write without a fresh
|
|
2559
|
+
* proof of identity). Only ARBAC-gated callers reach it (session-bound
|
|
2560
|
+
* subject), and `verifyPassword` is the same check `changePassword` enforces.
|
|
2561
|
+
*/
|
|
2562
|
+
managePasswordReauth(ctx: AuthWfCtx): Promise<undefined>;
|
|
2563
|
+
/**
|
|
2564
|
+
* Manage-MFA menu — pauses on `ManageMfaForm` and routes the chosen
|
|
2565
|
+
* `operation` (`add:<t>` / `replace:<t>` / `remove:<t>`). Only reached when
|
|
2566
|
+
* the user has ≥1 confirmed factor (a zero-MFA user goes straight to the enrol
|
|
2567
|
+
* picker). Re-checks the locked set + candidate membership server-side, then
|
|
2568
|
+
* sets `ctx.addMfa.action`/`target` (and pre-seeds `mfaEnroll.method` for
|
|
2569
|
+
* add/change). `cancel`, or nothing actionable, aborts to the finish terminal.
|
|
2570
|
+
*/
|
|
2571
|
+
manageMenu(ctx: AuthWfCtx): Promise<undefined>;
|
|
2572
|
+
/**
|
|
2573
|
+
* Manage-MFA remove confirmation. Pauses on `RemoveMfaConfirmForm`; the
|
|
2574
|
+
* 'Remove' submit performs the removal, 'Cancel' aborts. Re-checks the locked
|
|
2575
|
+
* set (defence in depth) and blocks removing the LAST confirmed factor when
|
|
2576
|
+
* the policy mode is `required` (you must keep at least one).
|
|
2577
|
+
*/
|
|
2578
|
+
confirmRemoveMfa(ctx: AuthWfCtx): Promise<undefined>;
|
|
2277
2579
|
askChannel(ctx: AuthWfCtx, channel: "email" | "phone"): Promise<unknown>;
|
|
2278
2580
|
verifyChannel(ctx: AuthWfCtx, channel: "email" | "phone"): Promise<unknown>;
|
|
2279
2581
|
/**
|
|
@@ -2346,22 +2648,72 @@ declare class AuthWorkflow {
|
|
|
2346
2648
|
/**
|
|
2347
2649
|
* Unified MFA-enrol phase 1 (pick method). Auto-picks a single transport,
|
|
2348
2650
|
* otherwise pauses for `EnrollPickMethodForm`. When TOTP is picked, the
|
|
2349
|
-
* secret is idempotently provisioned in the same step body. Handles
|
|
2350
|
-
*
|
|
2651
|
+
* secret is idempotently provisioned in the same step body. Handles `skip`
|
|
2652
|
+
* (optional opt-in) / `cancel` (manage). In the manage flow this only runs
|
|
2653
|
+
* for a zero-MFA user — once the user has factors, the menu pre-seeds
|
|
2654
|
+
* `mfaEnroll.method` (add/change) so the picker is skipped.
|
|
2351
2655
|
*/
|
|
2352
2656
|
enrollPickMethod(ctx: AuthWfCtx): undefined | Promise<undefined>;
|
|
2353
2657
|
/**
|
|
2354
2658
|
* Unified MFA-enrol phase 2 (collect sms/email address + send pincode).
|
|
2355
|
-
* Not invoked for totp. Handles `skip` / `
|
|
2659
|
+
* Not invoked for totp. Handles `skip` (opt-in) / `cancel` (manage) /
|
|
2660
|
+
* `useDifferentMethod`. Validates the address server-side (the client
|
|
2661
|
+
* `@ui.form.validate` hint is advisory), then STAGES the candidate value in
|
|
2662
|
+
* wf-state (`m.address`) — the user record is written only on confirm
|
|
2663
|
+
* (write-on-confirm), so an ADD leaves no partial row and a REPLACE keeps the
|
|
2664
|
+
* old confirmed value live until the new code verifies in `enroll-confirm`.
|
|
2356
2665
|
*/
|
|
2357
2666
|
enrollAddress(ctx: AuthWfCtx): Promise<undefined>;
|
|
2358
2667
|
/**
|
|
2359
|
-
*
|
|
2668
|
+
* MFA-enrol TOTP QR step — shown on its OWN pause between method-pick and
|
|
2669
|
+
* code-entry (so the user scans first, types the code next). Idempotently
|
|
2670
|
+
* provisions the TOTP secret in wf-state ONLY (covers the auto-pick /
|
|
2671
|
+
* menu-pre-seeded paths where `enroll-pick-method` was skipped), then pauses
|
|
2672
|
+
* on `EnrollTotpQrForm`. The user record is written only on confirm
|
|
2673
|
+
* (write-on-confirm), so a manage **replace** never clobbers the live totp
|
|
2674
|
+
* secret and a cancel/crash leaves the existing factor intact — no stash or
|
|
2675
|
+
* restore needed. Handles `skip` (opt-in) / `cancel` (manage) /
|
|
2676
|
+
* `useDifferentMethod`.
|
|
2677
|
+
*/
|
|
2678
|
+
enrollTotpQr(ctx: AuthWfCtx): Promise<undefined>;
|
|
2679
|
+
/**
|
|
2680
|
+
* Unified MFA-enrol phase 3 (verify pincode/TOTP, then write the factor). On
|
|
2360
2681
|
* success sets `ctx.mfaEnroll.done = true` AND `ctx.otp.verified = true`
|
|
2361
2682
|
* (the loop-exit signal — enrol-confirm verifies an OTP, so the unified
|
|
2362
2683
|
* `otp.verified` flag fires alongside the MFA-specific `mfaEnroll.done`).
|
|
2684
|
+
* This is the ONLY place the enrol trio touches the user record
|
|
2685
|
+
* (write-on-confirm): the proven value (sms/email address or totp secret,
|
|
2686
|
+
* staged in wf-state) is upserted as confirmed via `addMfaMethod`, which
|
|
2687
|
+
* atomically swaps in a REPLACE with no pre-confirm clobber window and creates
|
|
2688
|
+
* a fresh row for an ADD.
|
|
2363
2689
|
*/
|
|
2364
2690
|
enrollConfirm(ctx: AuthWfCtx): Promise<undefined>;
|
|
2691
|
+
/**
|
|
2692
|
+
* Promote a freshly-confirmed channel into its login-handle column so future
|
|
2693
|
+
* login + recovery resolve the account by it (`findByHandle`). Runs once,
|
|
2694
|
+
* right after `enroll-confirm` in the shared enrolment trio (so it covers
|
|
2695
|
+
* add-mfa AND login/invite forced first-time enrolment). Default is a no-op
|
|
2696
|
+
* unless `resolvePromoteHandleField` is overridden to name a handle column.
|
|
2697
|
+
*
|
|
2698
|
+
* Overridable extension point: a deployment can replace this with richer
|
|
2699
|
+
* logic — e.g. pause on a carrier form asking whether to use the new number
|
|
2700
|
+
* as a login handle before writing it.
|
|
2701
|
+
*
|
|
2702
|
+
* Fires only for a freshly-confirmed `email` / `sms` factor carrying an
|
|
2703
|
+
* address. TOTP has no address; a skipped / `useDifferentMethod` enrolment
|
|
2704
|
+
* cleared `method` + `address` via `clearEnrollScratch`, so the guard below
|
|
2705
|
+
* excludes both — only an actually-confirmed channel is promoted.
|
|
2706
|
+
*/
|
|
2707
|
+
promoteToHandle(ctx: AuthWfCtx): Promise<undefined>;
|
|
2708
|
+
/**
|
|
2709
|
+
* Best-effort write of a confirmed channel value into its handle column.
|
|
2710
|
+
* Swallows `ALREADY_EXISTS` — the value is already a handle on ANOTHER
|
|
2711
|
+
* account (e.g. two accounts legitimately sharing one phone for MFA): the
|
|
2712
|
+
* second account keeps the factor as MFA-only and is simply not promoted.
|
|
2713
|
+
* Any other store error propagates. (`UserService.update` translates a
|
|
2714
|
+
* unique-index `CONFLICT` to `ALREADY_EXISTS` for both store adapters.)
|
|
2715
|
+
*/
|
|
2716
|
+
protected applyHandlePromotion(subject: string, field: string, value: string): Promise<void>;
|
|
2365
2717
|
/**
|
|
2366
2718
|
* Risk step-up: re-evaluate whether to require another MFA round. Default
|
|
2367
2719
|
* `resolveRiskStepUp` returns `{require: false}`. When `require: true`,
|
|
@@ -2664,20 +3016,35 @@ declare class AuthWorkflow {
|
|
|
2664
3016
|
*/
|
|
2665
3017
|
changePasswordFlow(): void;
|
|
2666
3018
|
/**
|
|
2667
|
-
* add-mfa.flow — authenticated self-service "
|
|
2668
|
-
*
|
|
2669
|
-
*
|
|
2670
|
-
*
|
|
2671
|
-
*
|
|
3019
|
+
* add-mfa.flow — authenticated self-service "Manage two-factor
|
|
3020
|
+
* authentication" (add / change / remove). Same gating model as
|
|
3021
|
+
* change-password: NOT `@Public()` — `init-add-mfa` is arbac-gated
|
|
3022
|
+
* (`auth.add-mfa` / `self`) and binds `ctx.subject` from the session, so an
|
|
3023
|
+
* unauthenticated / unauthorized caller is rejected at the first step. NOT in
|
|
3024
|
+
* `DEFAULT_AUTH_WORKFLOWS` — reached only via the GUARDED trigger route
|
|
2672
3025
|
* (`AuthController.addMfa`), never the public `/auth/trigger`.
|
|
2673
3026
|
*
|
|
2674
|
-
*
|
|
2675
|
-
*
|
|
2676
|
-
*
|
|
2677
|
-
*
|
|
2678
|
-
*
|
|
2679
|
-
*
|
|
2680
|
-
*
|
|
3027
|
+
* Shape:
|
|
3028
|
+
* 1. `init-add-mfa` — bind subject, resolve the FULL transport set + the
|
|
3029
|
+
* un-enrolled `candidates`, mark `stepUpRequired` when the user already has
|
|
3030
|
+
* ≥1 confirmed factor, and put the enrol forms in `'manage'` mode.
|
|
3031
|
+
* 2. `prepare-locked-mfa-transports` — resolve which factors the consumer
|
|
3032
|
+
* forbids changing (handle-bound email/phone).
|
|
3033
|
+
* 3. STEP-UP (only when `stepUpRequired`): re-verify identity before any
|
|
3034
|
+
* change — `mfaStepUpLoop` challenges an EXISTING factor when one is still
|
|
3035
|
+
* challengeable (`stepUpMode==='mfa'`), else `manage-password-reauth` falls
|
|
3036
|
+
* back to the account password (`stepUpMode==='password'`). On success
|
|
3037
|
+
* `manage-stepup-done` swaps off the encapsulated start onto the durable
|
|
3038
|
+
* `store` strategy (server-anchored, replay-resistant; mirrors login's
|
|
3039
|
+
* swap-after-credentials).
|
|
3040
|
+
* 4. `manage-menu` (only when `stepUpRequired`) — pick add / change / remove +
|
|
3041
|
+
* target; pre-seeds `mfaEnroll.method` for add/change.
|
|
3042
|
+
* 5. Route: `confirm-remove-mfa` for remove; otherwise the REUSED enrol trio
|
|
3043
|
+
* (`enroll-pick-method` → `enroll-address` / `enroll-totp-qr` →
|
|
3044
|
+
* `enroll-confirm`). A zero-MFA user skips step-up + menu and lands on the
|
|
3045
|
+
* enrol picker directly (the first-time opt-in path).
|
|
3046
|
+
* 6. `finish-add-mfa` — added / changed / removed / cancelled / nothing terminal.
|
|
3047
|
+
* The user KEEPS their session (no token re-issue).
|
|
2681
3048
|
*/
|
|
2682
3049
|
addMfaFlow(): void;
|
|
2683
3050
|
/**
|