@adonis-agora/authkit-server 0.68.4 → 0.70.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/build/commands/import_users.js +6 -1
- package/build/index.d.ts +5 -1
- package/build/index.js +7 -1
- package/build/src/accounts/account_store.d.ts +40 -0
- package/build/src/accounts/account_store.js +20 -0
- package/build/src/accounts/lucid_store/mfa.js +22 -0
- package/build/src/audit/audit_sink.d.ts +1 -1
- package/build/src/audit/audit_sink.js +4 -0
- package/build/src/commands/import_users.d.ts +7 -0
- package/build/src/commands/import_users.js +15 -3
- package/build/src/define_config.d.ts +81 -0
- package/build/src/define_config.js +18 -3
- package/build/src/host/account_api/account_api_controller.d.ts +2 -0
- package/build/src/host/account_api/account_api_controller.js +22 -3
- package/build/src/host/account_api/account_mfa_api_controller.d.ts +101 -0
- package/build/src/host/account_api/account_mfa_api_controller.js +286 -0
- package/build/src/host/account_api/account_orgs_api_controller.d.ts +126 -0
- package/build/src/host/account_api/account_orgs_api_controller.js +468 -0
- package/build/src/host/account_lockout.js +7 -2
- package/build/src/host/admin_api/admin_orgs_service.js +3 -2
- package/build/src/host/admin_api/admin_users_service.js +13 -3
- package/build/src/host/admin_api/dto.d.ts +1 -1
- package/build/src/host/admin_validators.d.ts +2 -2
- package/build/src/host/admin_validators.js +3 -2
- package/build/src/host/controllers/account_mfa_controller.js +1 -2
- package/build/src/host/controllers/account_orgs_controller.js +19 -11
- package/build/src/host/controllers/account_security_controller.js +3 -1
- package/build/src/host/controllers/account_session_controller.js +17 -6
- package/build/src/host/controllers/interaction_controller.d.ts +10 -0
- package/build/src/host/controllers/interaction_controller.js +93 -34
- package/build/src/host/controllers/registration_controller.js +35 -9
- package/build/src/host/controllers/social_controller.js +11 -2
- package/build/src/host/email_identifier.d.ts +92 -0
- package/build/src/host/email_identifier.js +234 -0
- package/build/src/host/idp_session_bridge.d.ts +55 -0
- package/build/src/host/idp_session_bridge.js +108 -0
- package/build/src/host/middleware/account_auth.js +3 -2
- package/build/src/host/org_policy.d.ts +26 -0
- package/build/src/host/org_policy.js +35 -0
- package/build/src/host/passkey_registration_challenge.d.ts +12 -0
- package/build/src/host/passkey_registration_challenge.js +12 -0
- package/build/src/host/register_auth_host.js +56 -1
- package/build/src/host/runtime_toggles.d.ts +2 -2
- package/build/src/host/runtime_toggles.js +3 -1
- package/build/src/host/sudo_mode.d.ts +17 -0
- package/build/src/host/sudo_mode.js +31 -12
- package/build/src/host/ui-dist/assets/{index-D9CYQnZR.js → index-Dct63ai-.js} +2 -2
- package/build/src/host/ui-dist/index.html +1 -1
- package/build/src/host/validators.d.ts +5 -5
- package/build/src/host/validators.js +17 -5
- package/build/src/provider/build_provider.js +46 -14
- package/build/src/provider/registration_policy.d.ts +99 -0
- package/build/src/provider/registration_policy.js +229 -0
- package/package.json +2 -2
|
@@ -53,7 +53,12 @@ export default class AuthkitImportUsers extends BaseCommand {
|
|
|
53
53
|
return;
|
|
54
54
|
}
|
|
55
55
|
const { records, parseErrors } = parseImportFile(content);
|
|
56
|
-
const report = await importUsers(store, records, {
|
|
56
|
+
const report = await importUsers(store, records, {
|
|
57
|
+
dryRun: this.dryRun,
|
|
58
|
+
// Ponte legada do config (default ligada) — a checagem de duplicado
|
|
59
|
+
// enxerga as contas gravadas com o endereço mutilado pelo cadastro antigo.
|
|
60
|
+
legacyFallback: authkitConfig?.login?.legacyEmailFallback ?? true,
|
|
61
|
+
});
|
|
57
62
|
// Erros de parsing entram no relatório agregado.
|
|
58
63
|
report.errors.unshift(...parseErrors);
|
|
59
64
|
if (this.dryRun) {
|
package/build/index.d.ts
CHANGED
|
@@ -64,10 +64,13 @@ export { deriveLockedSettingKeys, isSettingLocked, lockedSettingKeys, POLICY_ROU
|
|
|
64
64
|
export { consoleLoginUrl, getAccountId, hasAccountSession, realAccountId, } from './src/host/console_session.js';
|
|
65
65
|
export type { AuthkitCsrfOptions } from './src/host/csrf.js';
|
|
66
66
|
export { authkitCsrfExceptions } from './src/host/csrf.js';
|
|
67
|
+
export type { EmailIdentifierLookup, ResolvedEmailIdentifier, } from './src/host/email_identifier.js';
|
|
68
|
+
export { legacyNormalizeEmailIdentifier, normalizeEmailIdentifier, resolveEmailIdentifier, } from './src/host/email_identifier.js';
|
|
67
69
|
export type { ResolveGeo } from './src/host/geo.js';
|
|
68
70
|
export { GEO_RESOLVE_TIMEOUT_MS, resolveGeoSafe } from './src/host/geo.js';
|
|
69
71
|
export type { AuthMessages, I18nConfig } from './src/host/i18n.js';
|
|
70
72
|
export { BUILTIN_MESSAGES, DEFAULT_LOCALE, DEFAULT_MESSAGES, PT_BR_MESSAGES, resolveMessages, translate, } from './src/host/i18n.js';
|
|
73
|
+
export { ensureConsoleSession } from './src/host/idp_session_bridge.js';
|
|
71
74
|
export type { ImpersonationClientLike, ImpersonationPanel } from './src/host/impersonation.js';
|
|
72
75
|
export { buildImpersonationPanel } from './src/host/impersonation.js';
|
|
73
76
|
export type { ImpersonationStartErrorCode, ImpersonationState, StartImpersonationParams, StopImpersonationOptions, TokenExchangeResult, } from './src/host/impersonation_session.js';
|
|
@@ -93,7 +96,7 @@ export { sudoMethods } from './src/host/sudo/index.js';
|
|
|
93
96
|
export { completeSudo, fail as failSudo, LAST_METHOD_SESSION_KEY, sudoContextFrom, } from './src/host/sudo/runtime.js';
|
|
94
97
|
export type { SudoContext, SudoMethod, SudoMethodDescriptor, SudoRouteHelpers, } from './src/host/sudo/types.js';
|
|
95
98
|
export type { ResolvedSudoModeSetting, SudoModeSetting, } from './src/host/sudo_mode.js';
|
|
96
|
-
export { isSudoActive, markSudo, requireSudo, resolveEffectiveSudoMode, SUDO_ACCOUNT_SESSION_KEY, SUDO_MODE_DEFAULTS, SUDO_SESSION_KEY, } from './src/host/sudo_mode.js';
|
|
99
|
+
export { isSudoActive, isSudoSatisfied, markSudo, requireSudo, resolveEffectiveSudoMode, SUDO_ACCOUNT_SESSION_KEY, SUDO_MODE_DEFAULTS, SUDO_SESSION_KEY, } from './src/host/sudo_mode.js';
|
|
97
100
|
export { barChartSvg } from './src/host/svg_chart.js';
|
|
98
101
|
export type { ResolvedTrustedDevicesConfig, TrustedDevicePayload, TrustedDevicesConfigInput, } from './src/host/trusted_device.js';
|
|
99
102
|
export { buildTrustedDevicePayload, isTrustedDeviceValid, resolveTrustedDevices, TRUSTED_DEVICE_COOKIE, } from './src/host/trusted_device.js';
|
|
@@ -122,6 +125,7 @@ export { lucidPatStore } from './src/pat/lucid_pat_store.js';
|
|
|
122
125
|
export type { IssuePatInput, PatRecord, PatStore } from './src/pat/pat_store.js';
|
|
123
126
|
export { generatePatToken, hashPatToken } from './src/pat/pat_tokens.js';
|
|
124
127
|
export { OidcService } from './src/provider/oidc_service.js';
|
|
128
|
+
export { checkClientRegistration, classifyRedirect, type RedirectUriPolicy, type RegistrationOperation, RegistrationPolicyError, type ResolvedRedirectUriPolicy, type ValidateRegistrationHook, } from './src/provider/registration_policy.js';
|
|
125
129
|
export { registerOidcRoutes } from './src/register_routes.js';
|
|
126
130
|
export type { EnsureSchemaOptions, EnsureSchemaReport } from './src/schema/ensure.js';
|
|
127
131
|
export { ensureAuthkitSchema } from './src/schema/ensure.js';
|
package/build/index.js
CHANGED
|
@@ -49,8 +49,10 @@ export { brandFor, isFirstParty, isFirstPartyClient } from './src/host/branding.
|
|
|
49
49
|
export { deriveLockedSettingKeys, isSettingLocked, lockedSettingKeys, POLICY_ROUTE_OPTIONS, resetLockedSettingKeys, SettingLockedError, setLockedSettingKeys, } from './src/host/config_locks.js';
|
|
50
50
|
export { consoleLoginUrl, getAccountId, hasAccountSession, realAccountId, } from './src/host/console_session.js';
|
|
51
51
|
export { authkitCsrfExceptions } from './src/host/csrf.js';
|
|
52
|
+
export { legacyNormalizeEmailIdentifier, normalizeEmailIdentifier, resolveEmailIdentifier, } from './src/host/email_identifier.js';
|
|
52
53
|
export { GEO_RESOLVE_TIMEOUT_MS, resolveGeoSafe } from './src/host/geo.js';
|
|
53
54
|
export { BUILTIN_MESSAGES, DEFAULT_LOCALE, DEFAULT_MESSAGES, PT_BR_MESSAGES, resolveMessages, translate, } from './src/host/i18n.js';
|
|
55
|
+
export { ensureConsoleSession } from './src/host/idp_session_bridge.js';
|
|
54
56
|
export { buildImpersonationPanel } from './src/host/impersonation.js';
|
|
55
57
|
// Session impersonation — RP-side glue that routes through the IdP's RFC 8693
|
|
56
58
|
// token-exchange (the IdP validates the admin role + audits). See
|
|
@@ -89,7 +91,10 @@ export { sudoMethods } from './src/host/sudo/index.js';
|
|
|
89
91
|
// `returnTo` montado na mão é um open redirect esperando acontecer.
|
|
90
92
|
export { completeSudo, fail as failSudo, LAST_METHOD_SESSION_KEY, sudoContextFrom, } from './src/host/sudo/runtime.js';
|
|
91
93
|
// Sudo mode — helpers for host controllers that require step-up authentication.
|
|
92
|
-
export { isSudoActive,
|
|
94
|
+
export { isSudoActive,
|
|
95
|
+
// A DECISÃO de sudo sem efeito na resposta — o que o host chama quando quer
|
|
96
|
+
// recusar em JSON em vez de redirecionar para `/account/confirm`.
|
|
97
|
+
isSudoSatisfied, markSudo, requireSudo, resolveEffectiveSudoMode, SUDO_ACCOUNT_SESSION_KEY, SUDO_MODE_DEFAULTS, SUDO_SESSION_KEY, } from './src/host/sudo_mode.js';
|
|
93
98
|
export { barChartSvg } from './src/host/svg_chart.js';
|
|
94
99
|
export { buildTrustedDevicePayload, isTrustedDeviceValid, resolveTrustedDevices, TRUSTED_DEVICE_COOKIE, } from './src/host/trusted_device.js';
|
|
95
100
|
export { parseUserAgent } from './src/host/user_agent.js';
|
|
@@ -110,6 +115,7 @@ export { __setFetchForTests as __setPwnedFetchForTests, isPasswordPwned, } from
|
|
|
110
115
|
export { lucidPatStore } from './src/pat/lucid_pat_store.js';
|
|
111
116
|
export { generatePatToken, hashPatToken } from './src/pat/pat_tokens.js';
|
|
112
117
|
export { OidcService } from './src/provider/oidc_service.js';
|
|
118
|
+
export { checkClientRegistration, classifyRedirect, RegistrationPolicyError, } from './src/provider/registration_policy.js';
|
|
113
119
|
export { registerOidcRoutes } from './src/register_routes.js';
|
|
114
120
|
export { ensureAuthkitSchema } from './src/schema/ensure.js';
|
|
115
121
|
export { stubsRoot } from './stubs/main.js';
|
|
@@ -278,6 +278,26 @@ export interface MfaCapability {
|
|
|
278
278
|
consumeRecoveryCode(accountId: string, code: string): Promise<boolean>;
|
|
279
279
|
/** Desliga o MFA: limpa segredo + mfaEnabledAt + recovery codes. */
|
|
280
280
|
disableMfa(accountId: string): Promise<void>;
|
|
281
|
+
/**
|
|
282
|
+
* Quantos recovery codes AINDA não foram consumidos. OPCIONAL: existe para a
|
|
283
|
+
* tela de segundo fator dizer "restam N" sem revelar os códigos (só o hash
|
|
284
|
+
* fica guardado). `null` quando o MFA não está ativo.
|
|
285
|
+
*
|
|
286
|
+
* Opcional de propósito: um store de terceiro escrito contra a versão
|
|
287
|
+
* anterior de {@link MfaCapability} continua satisfazendo o tipo, e os
|
|
288
|
+
* callers fazem capability-probe (`typeof store.countRecoveryCodes ===
|
|
289
|
+
* 'function'`) antes de chamar.
|
|
290
|
+
*/
|
|
291
|
+
countRecoveryCodes?(accountId: string): Promise<number | null>;
|
|
292
|
+
/**
|
|
293
|
+
* Regenera os recovery codes de uma conta com MFA ATIVO: descarta os antigos
|
|
294
|
+
* (inclusive os não usados) e devolve os novos em claro — uma única vez, como
|
|
295
|
+
* no {@link MfaCapability.confirmTotpEnrollment}. `null` quando o MFA não
|
|
296
|
+
* está ativo (não há o que regenerar).
|
|
297
|
+
*
|
|
298
|
+
* Opcional pelo mesmo motivo de {@link MfaCapability.countRecoveryCodes}.
|
|
299
|
+
*/
|
|
300
|
+
regenerateRecoveryCodes?(accountId: string): Promise<string[] | null>;
|
|
281
301
|
}
|
|
282
302
|
/**
|
|
283
303
|
* MFA / WebAuthn (passkeys) — 2º fator alternativo ao TOTP. Como o TOTP, é uma
|
|
@@ -654,6 +674,26 @@ export declare function supportsCountByGlobalRole(store: AccountStore): store is
|
|
|
654
674
|
};
|
|
655
675
|
/** Type guard: o store implementa a capacidade de passkeys / WebAuthn. */
|
|
656
676
|
export declare function supportsPasskeys(store: AccountStore): store is AccountStore & WebauthnCapability;
|
|
677
|
+
/**
|
|
678
|
+
* Type guard: o store sabe REGENERAR recovery codes
|
|
679
|
+
* ({@link MfaCapability.regenerateRecoveryCodes}).
|
|
680
|
+
*
|
|
681
|
+
* Guard PRÓPRIO, separado de {@link supportsMfa}, porque o método é opcional
|
|
682
|
+
* dentro da capacidade: um store escrito antes desta versão implementa MFA
|
|
683
|
+
* inteiro e não implementa este método. Quem oferece o botão "gerar novos
|
|
684
|
+
* códigos" pergunta por aqui.
|
|
685
|
+
*/
|
|
686
|
+
export declare function supportsRecoveryCodeRegeneration(store: AccountStore): store is AccountStore & MfaCapability & {
|
|
687
|
+
regenerateRecoveryCodes(accountId: string): Promise<string[] | null>;
|
|
688
|
+
};
|
|
689
|
+
/**
|
|
690
|
+
* Type guard: o store sabe CONTAR recovery codes restantes
|
|
691
|
+
* ({@link MfaCapability.countRecoveryCodes}). Mesmo motivo do
|
|
692
|
+
* {@link supportsRecoveryCodeRegeneration} para ser um guard separado.
|
|
693
|
+
*/
|
|
694
|
+
export declare function supportsRecoveryCodeCount(store: AccountStore): store is AccountStore & MfaCapability & {
|
|
695
|
+
countRecoveryCodes(accountId: string): Promise<number | null>;
|
|
696
|
+
};
|
|
657
697
|
/** Type guard: o store implementa account linking por identidade de provider. */
|
|
658
698
|
export declare function supportsProviderIdentity(store: AccountStore): store is AccountStore & ProviderIdentityCapability;
|
|
659
699
|
/** Type guard: o store implementa o self-service de segurança (senha/e-mail). */
|
|
@@ -18,6 +18,26 @@ export function supportsCountByGlobalRole(store) {
|
|
|
18
18
|
export function supportsPasskeys(store) {
|
|
19
19
|
return typeof store.listPasskeys === 'function';
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Type guard: o store sabe REGENERAR recovery codes
|
|
23
|
+
* ({@link MfaCapability.regenerateRecoveryCodes}).
|
|
24
|
+
*
|
|
25
|
+
* Guard PRÓPRIO, separado de {@link supportsMfa}, porque o método é opcional
|
|
26
|
+
* dentro da capacidade: um store escrito antes desta versão implementa MFA
|
|
27
|
+
* inteiro e não implementa este método. Quem oferece o botão "gerar novos
|
|
28
|
+
* códigos" pergunta por aqui.
|
|
29
|
+
*/
|
|
30
|
+
export function supportsRecoveryCodeRegeneration(store) {
|
|
31
|
+
return typeof store.regenerateRecoveryCodes === 'function';
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Type guard: o store sabe CONTAR recovery codes restantes
|
|
35
|
+
* ({@link MfaCapability.countRecoveryCodes}). Mesmo motivo do
|
|
36
|
+
* {@link supportsRecoveryCodeRegeneration} para ser um guard separado.
|
|
37
|
+
*/
|
|
38
|
+
export function supportsRecoveryCodeCount(store) {
|
|
39
|
+
return typeof store.countRecoveryCodes === 'function';
|
|
40
|
+
}
|
|
21
41
|
/** Type guard: o store implementa account linking por identidade de provider. */
|
|
22
42
|
export function supportsProviderIdentity(store) {
|
|
23
43
|
return typeof store.findByProviderIdentity === 'function';
|
|
@@ -100,6 +100,28 @@ export function buildMfa(ctx) {
|
|
|
100
100
|
await repo.upsert(accountId, { recoveryCodes: remaining });
|
|
101
101
|
return true;
|
|
102
102
|
},
|
|
103
|
+
async countRecoveryCodes(accountId) {
|
|
104
|
+
const state = await repo.read(accountId);
|
|
105
|
+
// Sem MFA ativo não há conjunto de códigos a contar — `null` diz "não se
|
|
106
|
+
// aplica", diferente de `0` ("acabaram, gere novos").
|
|
107
|
+
if (!state || !state.mfaEnabledAt)
|
|
108
|
+
return null;
|
|
109
|
+
return Array.isArray(state.recoveryCodes) ? state.recoveryCodes.length : 0;
|
|
110
|
+
},
|
|
111
|
+
async regenerateRecoveryCodes(accountId) {
|
|
112
|
+
const state = await repo.read(accountId);
|
|
113
|
+
// Só regenera sobre MFA JÁ ATIVO. Num enrollment pendente os códigos saem
|
|
114
|
+
// do `confirmTotpEnrollment`; deixar regenerar antes disso criaria um
|
|
115
|
+
// conjunto válido de credenciais de recuperação para um fator que o
|
|
116
|
+
// usuário ainda não provou possuir.
|
|
117
|
+
if (!state || !state.mfaEnabledAt)
|
|
118
|
+
return null;
|
|
119
|
+
const codes = Array.from({ length: recoveryCodeCount }, () => generateRecoveryCode());
|
|
120
|
+
// Substitui o conjunto inteiro: os antigos (inclusive os não usados)
|
|
121
|
+
// param de valer no mesmo instante — é esse o ponto de "regenerar".
|
|
122
|
+
await repo.upsert(accountId, { recoveryCodes: codes.map(sha256) });
|
|
123
|
+
return codes;
|
|
124
|
+
},
|
|
103
125
|
async disableMfa(accountId) {
|
|
104
126
|
// Limpa todo o estado de MFA. Inclui o anti-replay: um futuro re-enroll começa do zero.
|
|
105
127
|
await repo.clear(accountId);
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* varre os `audit.record({ type: ... })` do `src/` e falha quando um evento
|
|
9
9
|
* emitido não está aqui.
|
|
10
10
|
*/
|
|
11
|
-
export declare const AUDIT_EVENT_TYPES: readonly ['login.success', 'login.failure', 'signup', 'password_reset.issued', 'password_reset.consumed', 'pat.issued', 'pat.revoked', 'pat.used', 'impersonation', 'impersonation.panel_viewed', 'impersonation.stopped', 'mfa.enabled', 'mfa.disabled', 'account.locked', 'passkey.registered', 'passkey.removed', 'email_verification.issued', 'email_verification.consumed', 'client.created', 'client.updated', 'client.deleted', 'session.revoked_all', 'password.changed', 'password.rehashed', 'email.change_requested', 'email.changed', 'login.new_ip_notified', 'login.new_device', 'login.otp_sent', 'login.otp_verified', 'login.otp_failed', 'login.otp_invalidated', 'bot_protection.rejected', 'grant.revoked_by_user', 'user.created', 'user.password_reset_sent', 'user.disabled', 'user.enabled', 'user.deleted', 'profile.updated', 'account.deleted', 'account.exported', 'keys.rotated', 'organization.created', 'organization.updated', 'organization.deleted', 'organization.member_added', 'organization.member_removed', 'organization.member_role_changed', 'organization.member_role_updated', 'organization.switched', 'organization.deactivated', 'organization.invitation_sent', 'organization.invitation_accepted', 'organization.invitation_revoked', 'email_change.requested', 'email_change.confirmed', 'email_change.cancelled', 'security_notice.sent', 'settings.updated', 'maintenance.enabled', 'maintenance.disabled', 'trusted_device.revoked', 'password.expired_change_forced', 'otp.locked', 'otp.unlocked', 'otp.unlock_failed', 'sudo.confirmed', 'session.single_enforced', 'account.expired_login_blocked', 'account.expiration_warned', 'login.magic_link_sent', 'session.revoked', 'account.signed_out_all', 'client.secret_regenerated', 'roles_catalog.updated'];
|
|
11
|
+
export declare const AUDIT_EVENT_TYPES: readonly ['login.success', 'login.failure', 'signup', 'password_reset.issued', 'password_reset.consumed', 'pat.issued', 'pat.revoked', 'pat.used', 'impersonation', 'impersonation.panel_viewed', 'impersonation.stopped', 'mfa.enabled', 'mfa.disabled', 'mfa.recovery_codes_regenerated', 'account.locked', 'passkey.registered', 'passkey.removed', 'email_verification.issued', 'email_verification.consumed', 'client.created', 'client.updated', 'client.deleted', 'session.revoked_all', 'password.changed', 'password.rehashed', 'email.change_requested', 'email.changed', 'login.new_ip_notified', 'login.new_device', 'login.otp_sent', 'login.otp_verified', 'login.otp_failed', 'login.otp_invalidated', 'bot_protection.rejected', 'grant.revoked_by_user', 'user.created', 'user.password_reset_sent', 'user.disabled', 'user.enabled', 'user.deleted', 'profile.updated', 'account.deleted', 'account.exported', 'keys.rotated', 'organization.created', 'organization.updated', 'organization.deleted', 'organization.member_added', 'organization.member_removed', 'organization.member_role_changed', 'organization.member_role_updated', 'organization.switched', 'organization.deactivated', 'organization.invitation_sent', 'organization.invitation_accepted', 'organization.invitation_revoked', 'email_change.requested', 'email_change.confirmed', 'email_change.cancelled', 'security_notice.sent', 'settings.updated', 'maintenance.enabled', 'maintenance.disabled', 'trusted_device.revoked', 'password.expired_change_forced', 'otp.locked', 'otp.unlocked', 'otp.unlock_failed', 'sudo.confirmed', 'session.single_enforced', 'account.expired_login_blocked', 'account.expiration_warned', 'login.magic_link_sent', 'session.revoked', 'account.signed_out_all', 'client.secret_regenerated', 'roles_catalog.updated'];
|
|
12
12
|
/** Tipos de eventos de auditoria relevantes para segurança emitidos pelo IdP. */
|
|
13
13
|
export type AuditEventType = (typeof AUDIT_EVENT_TYPES)[number];
|
|
14
14
|
/**
|
|
@@ -29,6 +29,10 @@ export const AUDIT_EVENT_TYPES = [
|
|
|
29
29
|
'impersonation.stopped',
|
|
30
30
|
'mfa.enabled',
|
|
31
31
|
'mfa.disabled',
|
|
32
|
+
// Os recovery codes foram TROCADOS: os antigos (inclusive os não usados)
|
|
33
|
+
// deixaram de valer no mesmo instante. Evento próprio, e não `mfa.enabled`,
|
|
34
|
+
// porque o fator não mudou — mudou o conjunto de credenciais de contorno.
|
|
35
|
+
'mfa.recovery_codes_regenerated',
|
|
32
36
|
'account.locked',
|
|
33
37
|
'passkey.registered',
|
|
34
38
|
'passkey.removed',
|
|
@@ -42,10 +42,17 @@ export declare function parseImportFile(content: string): {
|
|
|
42
42
|
*
|
|
43
43
|
* Lógica PURA quanto a I/O de arquivo (recebe os registros já parseados) — fácil
|
|
44
44
|
* de testar. Lança se o store não suporta import nem create.
|
|
45
|
+
*
|
|
46
|
+
* `legacyFallback` (default ligado, como no resto da lib) faz a checagem de
|
|
47
|
+
* duplicado enxergar também a conta que o cadastro antigo gravou com o endereço
|
|
48
|
+
* mutilado — importar a grafia REAL do dono dela pula como duplicado em vez de
|
|
49
|
+
* criar uma SEGUNDA conta. O comando não tem acesso ao config, então o flag
|
|
50
|
+
* chega por aqui.
|
|
45
51
|
*/
|
|
46
52
|
export declare function importUsers(store: AccountStore, records: {
|
|
47
53
|
line: number;
|
|
48
54
|
record: ImportUserRecord;
|
|
49
55
|
}[], options?: {
|
|
50
56
|
dryRun?: boolean;
|
|
57
|
+
legacyFallback?: boolean;
|
|
51
58
|
}): Promise<ImportReport>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { supportsAccountImport } from '../accounts/account_store.js';
|
|
2
|
+
import { normalizeEmailIdentifier, resolveEmailIdentifier } from '../host/email_identifier.js';
|
|
2
3
|
/**
|
|
3
4
|
* Faz o parse do conteúdo do arquivo: aceita NDJSON (uma linha JSON por usuário)
|
|
4
5
|
* OU um array JSON. Retorna os registros + os erros de parsing por linha. PURO
|
|
@@ -47,18 +48,29 @@ export function parseImportFile(content) {
|
|
|
47
48
|
*
|
|
48
49
|
* Lógica PURA quanto a I/O de arquivo (recebe os registros já parseados) — fácil
|
|
49
50
|
* de testar. Lança se o store não suporta import nem create.
|
|
51
|
+
*
|
|
52
|
+
* `legacyFallback` (default ligado, como no resto da lib) faz a checagem de
|
|
53
|
+
* duplicado enxergar também a conta que o cadastro antigo gravou com o endereço
|
|
54
|
+
* mutilado — importar a grafia REAL do dono dela pula como duplicado em vez de
|
|
55
|
+
* criar uma SEGUNDA conta. O comando não tem acesso ao config, então o flag
|
|
56
|
+
* chega por aqui.
|
|
50
57
|
*/
|
|
51
58
|
export async function importUsers(store, records, options = {}) {
|
|
52
59
|
const report = { created: 0, skippedDuplicate: 0, errors: [] };
|
|
53
60
|
const canImport = supportsAccountImport(store);
|
|
54
61
|
for (const { line, record } of records) {
|
|
55
|
-
|
|
62
|
+
// MESMA normalização do cadastro/login (`trim` + `toLowerCase`) — sem ela o
|
|
63
|
+
// import gravava `Davi@Acme.com` e o login (normalizado) não achava a conta.
|
|
64
|
+
const email = normalizeEmailIdentifier(record.email);
|
|
56
65
|
if (!email) {
|
|
57
66
|
report.errors.push({ line, reason: 'missing email' });
|
|
58
67
|
continue;
|
|
59
68
|
}
|
|
60
|
-
// Duplicado: e-mail já existe → pula.
|
|
61
|
-
|
|
69
|
+
// Duplicado: e-mail já existe → pula. Enxerga também a conta gravada com o
|
|
70
|
+
// endereço mutilado (ponte legada) — empate segue criando, como no cadastro.
|
|
71
|
+
const existing = (await resolveEmailIdentifier(store, record.email, {
|
|
72
|
+
legacyFallback: options.legacyFallback,
|
|
73
|
+
})).account;
|
|
62
74
|
if (existing) {
|
|
63
75
|
report.skippedDuplicate++;
|
|
64
76
|
continue;
|
|
@@ -14,6 +14,7 @@ import type { AuthHostOptions } from './host/register_auth_host.js';
|
|
|
14
14
|
import type { SudoMethod } from './host/sudo/types.js';
|
|
15
15
|
import { type ResolvedTrustedDevicesConfig, type TrustedDevicesConfigInput } from './host/trusted_device.js';
|
|
16
16
|
import type { PatStore } from './pat/pat_store.js';
|
|
17
|
+
import { type RedirectUriPolicy, type ResolvedRedirectUriPolicy, type ValidateRegistrationHook } from './provider/registration_policy.js';
|
|
17
18
|
export type { AuthAccount };
|
|
18
19
|
export { adapters };
|
|
19
20
|
export type AuthHostRenderer = (ctx: HttpContext, view: string, props: Record<string, unknown>) => unknown;
|
|
@@ -305,11 +306,37 @@ export interface DynamicRegistrationConfigInput {
|
|
|
305
306
|
* registrado via o `registration_access_token` devolvido no registro. Default: false.
|
|
306
307
|
*/
|
|
307
308
|
management?: boolean;
|
|
309
|
+
/**
|
|
310
|
+
* Política de redirect URIs aplicada a TODO registro (`POST /reg`) e update
|
|
311
|
+
* (`PUT /reg/:id`) ANTES do oidc-provider. Com a política ativa, o client
|
|
312
|
+
* também fica restrito ao fluxo de código (`authorization_code` +
|
|
313
|
+
* `refresh_token`, `response_type=code`; PKCE já é obrigatório no IdP), e
|
|
314
|
+
* um client só-loopback/app instalado é registrado como `application_type: native`.
|
|
315
|
+
*
|
|
316
|
+
* Default:
|
|
317
|
+
* - registro ABERTO (sem `initialAccessToken`): `{ loopback: true }` — só
|
|
318
|
+
* `http://localhost|127.0.0.1|[::1]` em qualquer porta. Callbacks web de
|
|
319
|
+
* fornecedores (ex.: `https://claude.ai/api/mcp/auth_callback`) e esquemas
|
|
320
|
+
* de app (`cursor`, `vscode`) precisam ser listados em `exact`/`appSchemes`.
|
|
321
|
+
* - registro com `initialAccessToken`: sem política (quem tem o IAT é confiável).
|
|
322
|
+
*
|
|
323
|
+
* `false` desliga a política explicitamente (comportamento puro do oidc-provider).
|
|
324
|
+
*/
|
|
325
|
+
redirectUriPolicy?: RedirectUriPolicy | false;
|
|
326
|
+
/**
|
|
327
|
+
* Gancho do host rodado depois da política de redirect: valida/ajusta o
|
|
328
|
+
* metadata do registro. Lance {@link RegistrationPolicyError} para recusar com
|
|
329
|
+
* `400`; retorne um objeto para substituir o metadata.
|
|
330
|
+
*/
|
|
331
|
+
validateRegistration?: ValidateRegistrationHook;
|
|
308
332
|
}
|
|
309
333
|
export interface ResolvedDynamicRegistrationConfig {
|
|
310
334
|
enabled: boolean;
|
|
311
335
|
initialAccessToken?: string;
|
|
312
336
|
management: boolean;
|
|
337
|
+
/** `null` = sem política de redirect (oidc-provider puro). */
|
|
338
|
+
redirectUriPolicy: ResolvedRedirectUriPolicy | null;
|
|
339
|
+
validateRegistration?: ValidateRegistrationHook;
|
|
313
340
|
}
|
|
314
341
|
/**
|
|
315
342
|
* Resolve a config de registro dinâmico e VALIDA invariantes em tempo de resolução.
|
|
@@ -510,10 +537,26 @@ export interface LoginConfigInput {
|
|
|
510
537
|
* idêntico ao de antes, e-mail idêntico). Ver `host/otp_login.ts`.
|
|
511
538
|
*/
|
|
512
539
|
otp?: OtpLoginConfigInput;
|
|
540
|
+
/**
|
|
541
|
+
* PONTE TEMPORÁRIA: no passo de identificador, quando o e-mail normalizado
|
|
542
|
+
* (`trim` + `toLowerCase`) não acha conta nenhuma, tenta também o endereço
|
|
543
|
+
* exatamente como foi digitado e a normalização LEGADA (os defaults do
|
|
544
|
+
* validator.js que o cadastro aplicava até a v0.68 e que, no gmail, removiam
|
|
545
|
+
* pontos e sub-endereço `+tag`). Só aceita quando essas formas apontam para
|
|
546
|
+
* EXATAMENTE UMA conta — empate é tratado como "não achei".
|
|
547
|
+
*
|
|
548
|
+
* Existe para que as contas que nasceram com o endereço mutilado
|
|
549
|
+
* (`davi.carvalho96@gmail.com` gravado como `davicarvalho96@gmail.com`)
|
|
550
|
+
* continuem tendo caminho de volta. Default: `true` — desligar tranca essas
|
|
551
|
+
* contas do lado de fora. Depois de migrar os endereços gravados, desligue e,
|
|
552
|
+
* mais adiante, a ponte inteira sai da lib (ver `host/email_identifier.ts`).
|
|
553
|
+
*/
|
|
554
|
+
legacyEmailFallback?: boolean;
|
|
513
555
|
}
|
|
514
556
|
export interface ResolvedLoginConfig {
|
|
515
557
|
requireVerifiedEmail: boolean;
|
|
516
558
|
otp: ResolvedOtpLoginConfig;
|
|
559
|
+
legacyEmailFallback: boolean;
|
|
517
560
|
}
|
|
518
561
|
export declare function resolveLogin(input?: LoginConfigInput): ResolvedLoginConfig;
|
|
519
562
|
/**
|
|
@@ -635,6 +678,9 @@ export declare function resolveAdminApi(input?: AdminApiConfigInput): ResolvedAd
|
|
|
635
678
|
* A role `'owner'` é reservada: uma org SEMPRE precisa de pelo menos um owner.
|
|
636
679
|
* `allowSelfCreate`: se um usuário autenticado pode criar sua própria org (default false).
|
|
637
680
|
* `invitationTtlHours`: TTL dos convites em horas (default 168 = 7 dias).
|
|
681
|
+
* Os três são o default estático da política; a setting `organizations_policy`
|
|
682
|
+
* os sobrescreve em runtime — exceto quando `organizations` está declarado no
|
|
683
|
+
* config, o que trava a setting e faz destes campos a política efetiva.
|
|
638
684
|
* `claimStrategy: 'active'`: emite claims da org ATIVA da sessão (única estratégia implementada).
|
|
639
685
|
*/
|
|
640
686
|
export interface OrganizationsConfigInput {
|
|
@@ -646,6 +692,21 @@ export interface OrganizationsConfigInput {
|
|
|
646
692
|
* Default: 'active'.
|
|
647
693
|
*/
|
|
648
694
|
claimStrategy?: 'active';
|
|
695
|
+
/**
|
|
696
|
+
* Catálogo de roles de org aceitas em convites/membros. `owner` é sempre
|
|
697
|
+
* garantido. Default: `['owner', 'admin', 'member']`.
|
|
698
|
+
*
|
|
699
|
+
* Declarar `organizations` TRAVA a setting `organizations_policy` (ver
|
|
700
|
+
* `config-locks`): com a chave travada, os campos de política daqui SÃO a
|
|
701
|
+
* política efetiva. Sem eles, a política trava no default da lib — era o
|
|
702
|
+
* que acontecia antes destes campos existirem (e.g. `allowSelfCreate` preso
|
|
703
|
+
* em `false`, sem jeito de ligar).
|
|
704
|
+
*/
|
|
705
|
+
roles?: string[];
|
|
706
|
+
/** Usuário autenticado pode criar a própria org em `/account/orgs`. Default: false. */
|
|
707
|
+
allowSelfCreate?: boolean;
|
|
708
|
+
/** TTL dos convites em horas. Default: 168 (7 dias). */
|
|
709
|
+
invitationTtlHours?: number;
|
|
649
710
|
}
|
|
650
711
|
export interface ResolvedOrganizationsConfig {
|
|
651
712
|
/** `undefined` = auto (decide em runtime pelo capability-probing do store). */
|
|
@@ -1061,6 +1122,22 @@ export interface AuthServerConfigInput {
|
|
|
1061
1122
|
adonisAuth?: {
|
|
1062
1123
|
guard: string;
|
|
1063
1124
|
};
|
|
1125
|
+
/**
|
|
1126
|
+
* Sessão do console de conta (`/account/*`, e o `/admin/*`).
|
|
1127
|
+
*
|
|
1128
|
+
* `acceptIdpSession: true` — SSO: o console aceita a sessão ATIVA do IdP (o
|
|
1129
|
+
* login feito na interaction OIDC — senha, magic link, OTP, passkey, social)
|
|
1130
|
+
* em vez de pedir um segundo login. A sessão de console aberta assim fica
|
|
1131
|
+
* amarrada à sessão do IdP: termina quando ela termina (logout OIDC,
|
|
1132
|
+
* expiração), e o "Sair" do console encerra também a sessão do IdP. Contas
|
|
1133
|
+
* desabilitadas não entram. Operações sensíveis continuam pedindo sudo.
|
|
1134
|
+
*
|
|
1135
|
+
* Default `false`: o console só aceita a própria sessão (`POST /account/login`),
|
|
1136
|
+
* como sempre.
|
|
1137
|
+
*/
|
|
1138
|
+
accountSession?: {
|
|
1139
|
+
acceptIdpSession?: boolean;
|
|
1140
|
+
};
|
|
1064
1141
|
}
|
|
1065
1142
|
export interface ResolvedServerConfig {
|
|
1066
1143
|
issuer: string;
|
|
@@ -1204,6 +1281,10 @@ export interface ResolvedServerConfig {
|
|
|
1204
1281
|
adonisAuth?: {
|
|
1205
1282
|
guard: string;
|
|
1206
1283
|
};
|
|
1284
|
+
/** Sessão do console. Ver {@link AuthServerConfigInput.accountSession}. */
|
|
1285
|
+
accountSession: {
|
|
1286
|
+
acceptIdpSession: boolean;
|
|
1287
|
+
};
|
|
1207
1288
|
}
|
|
1208
1289
|
export declare function toSeconds(value: string | number | undefined, fallback: number): number;
|
|
1209
1290
|
/**
|
|
@@ -13,6 +13,7 @@ import { generateJwks } from './keys/jwks_manager.js';
|
|
|
13
13
|
import { KeystoreCodec } from './keys/keystore_codec.js';
|
|
14
14
|
import { loadEncryptionService } from './keys/keystore_crypto.js';
|
|
15
15
|
import { KeystoreManager, resolveKeystoreVault } from './keys/keystore_manager.js';
|
|
16
|
+
import { OPEN_REGISTRATION_REDIRECT_POLICY, resolveRedirectUriPolicy, } from './provider/registration_policy.js';
|
|
16
17
|
export { adapters };
|
|
17
18
|
const RATE_LIMIT_DEFAULTS = {
|
|
18
19
|
login: { points: 10, duration: '1 min' },
|
|
@@ -64,10 +65,20 @@ export function resolveDynamicRegistration(input) {
|
|
|
64
65
|
'dynamicRegistration.enabled: true (RFC 7591). Habilite o registro dinâmico ' +
|
|
65
66
|
'ou desligue o management.');
|
|
66
67
|
}
|
|
68
|
+
const declared = input?.redirectUriPolicy;
|
|
69
|
+
const redirectUriPolicy = declared === false
|
|
70
|
+
? null
|
|
71
|
+
: declared
|
|
72
|
+
? resolveRedirectUriPolicy(declared)
|
|
73
|
+
: input?.initialAccessToken
|
|
74
|
+
? null
|
|
75
|
+
: { ...OPEN_REGISTRATION_REDIRECT_POLICY };
|
|
67
76
|
return {
|
|
68
77
|
enabled,
|
|
69
78
|
initialAccessToken: input?.initialAccessToken,
|
|
70
79
|
management,
|
|
80
|
+
redirectUriPolicy,
|
|
81
|
+
validateRegistration: input?.validateRegistration,
|
|
71
82
|
};
|
|
72
83
|
}
|
|
73
84
|
export function resolveDeviceFlow(input) {
|
|
@@ -126,6 +137,7 @@ export function resolveLogin(input) {
|
|
|
126
137
|
return {
|
|
127
138
|
requireVerifiedEmail: input?.requireVerifiedEmail ?? false,
|
|
128
139
|
otp: resolveOtpLoginConfig(input?.otp),
|
|
140
|
+
legacyEmailFallback: input?.legacyEmailFallback ?? true,
|
|
129
141
|
};
|
|
130
142
|
}
|
|
131
143
|
export function resolveRegistration(input) {
|
|
@@ -183,9 +195,11 @@ export function resolveAdminApi(input) {
|
|
|
183
195
|
export function resolveOrganizations(input) {
|
|
184
196
|
return {
|
|
185
197
|
enabled: input?.enabled,
|
|
186
|
-
roles: ['owner', 'admin', 'member'],
|
|
187
|
-
allowSelfCreate: false,
|
|
188
|
-
invitationTtlHours:
|
|
198
|
+
roles: input?.roles && input.roles.length > 0 ? input.roles : ['owner', 'admin', 'member'],
|
|
199
|
+
allowSelfCreate: input?.allowSelfCreate ?? false,
|
|
200
|
+
invitationTtlHours: typeof input?.invitationTtlHours === 'number' && input.invitationTtlHours >= 1
|
|
201
|
+
? Math.floor(input.invitationTtlHours)
|
|
202
|
+
: 168,
|
|
189
203
|
claimStrategy: input?.claimStrategy ?? 'active',
|
|
190
204
|
};
|
|
191
205
|
}
|
|
@@ -433,6 +447,7 @@ export function defineConfig(config) {
|
|
|
433
447
|
lockedRouteOptions: deriveLockedRouteOptions(config),
|
|
434
448
|
// Opt-in: ausente = authkit nunca toca `ctx.auth` (comportamento de sempre).
|
|
435
449
|
adonisAuth: config.adonisAuth,
|
|
450
|
+
accountSession: { acceptIdpSession: config.accountSession?.acceptIdpSession === true },
|
|
436
451
|
};
|
|
437
452
|
});
|
|
438
453
|
}
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* GET /account/api/orgs/invitations → convites pendentes
|
|
29
29
|
*/
|
|
30
30
|
import '../augmentations.js';
|
|
31
|
-
import { supportsAccountSecurity, supportsLoginMethodsPreference, supportsMagicLink, supportsOrganizations, supportsPasskeys, supportsProfile, } from '../../accounts/account_store.js';
|
|
31
|
+
import { supportsAccountSecurity, supportsLoginMethodsPreference, supportsMagicLink, supportsOrganizations, supportsPasskeys, supportsProfile, supportsRecoveryCodeCount, supportsRecoveryCodeRegeneration, } from '../../accounts/account_store.js';
|
|
32
32
|
import { PasswordPolicyError } from '../../password/password_manager.js';
|
|
33
33
|
import { accountPath } from '../account_paths.js';
|
|
34
34
|
import { ACCOUNT_SESSION_KEY } from '../account_session_key.js';
|
|
@@ -627,6 +627,16 @@ export default class AccountApiController {
|
|
|
627
627
|
/* fail-safe */
|
|
628
628
|
}
|
|
629
629
|
}
|
|
630
|
+
// Quantos recovery codes restam (capability-probed, fail-safe).
|
|
631
|
+
let recoveryRemaining = null;
|
|
632
|
+
if (supportsRecoveryCodeCount(cfg.accountStore)) {
|
|
633
|
+
try {
|
|
634
|
+
recoveryRemaining = await cfg.accountStore.countRecoveryCodes(userId);
|
|
635
|
+
}
|
|
636
|
+
catch {
|
|
637
|
+
/* fail-safe */
|
|
638
|
+
}
|
|
639
|
+
}
|
|
630
640
|
return {
|
|
631
641
|
enabled,
|
|
632
642
|
totp: { enrolled: enabled },
|
|
@@ -639,8 +649,17 @@ export default class AccountApiController {
|
|
|
639
649
|
createdAt: p.createdAt ?? null,
|
|
640
650
|
})),
|
|
641
651
|
},
|
|
642
|
-
|
|
643
|
-
|
|
652
|
+
recovery: {
|
|
653
|
+
// Os códigos em si NUNCA voltam aqui — só no instante em que são
|
|
654
|
+
// criados (`/mfa/totp/confirm` ou `/mfa/recovery-codes`), porque é só
|
|
655
|
+
// o hash deles que fica guardado.
|
|
656
|
+
available: enabled,
|
|
657
|
+
// `remaining` é `null` quando o store não sabe contar (capacidade
|
|
658
|
+
// opcional) — diferente de `0`, que significa "acabaram, gere novos".
|
|
659
|
+
remaining: recoveryRemaining,
|
|
660
|
+
// O host só mostra o botão "gerar novos códigos" se houver como.
|
|
661
|
+
regenerable: supportsRecoveryCodeRegeneration(cfg.accountStore),
|
|
662
|
+
},
|
|
644
663
|
};
|
|
645
664
|
}
|
|
646
665
|
// ─── GET /account/api/login-methods ─────────────────────────────────────
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account Self-Service JSON API — SEGUNDO FATOR (TOTP + passkeys)
|
|
3
|
+
*
|
|
4
|
+
* Espelho JSON do `AccountMfaController`, para o host que desenha a própria
|
|
5
|
+
* tela de "segundo fator" e não pode navegar o browser no meio do fluxo.
|
|
6
|
+
*
|
|
7
|
+
* Mapa de rotas (sob o `accountGuard`; mutantes sob o CSRF do shield do host):
|
|
8
|
+
* POST /account/api/mfa/totp/enroll → inicia o enrolamento (sudo)
|
|
9
|
+
* POST /account/api/mfa/totp/confirm → confirma com o código (throttled)
|
|
10
|
+
* POST /account/api/mfa/totp/disable → desliga o MFA (sudo)
|
|
11
|
+
* POST /account/api/mfa/recovery-codes → regenera os códigos (sudo)
|
|
12
|
+
* POST /account/api/mfa/passkeys/options → options da cerimônia de registro
|
|
13
|
+
* POST /account/api/mfa/passkeys/verify → verifica o attestation (sudo)
|
|
14
|
+
*
|
|
15
|
+
* Paridade de gates com o console HTML, endpoint a endpoint:
|
|
16
|
+
*
|
|
17
|
+
* | ação | console HTML | aqui |
|
|
18
|
+
* | ---------------- | ------------ | ---------------- |
|
|
19
|
+
* | enroll | sudo | sudo → 403 JSON |
|
|
20
|
+
* | confirm | sem sudo | sem sudo |
|
|
21
|
+
* | disable | sudo | sudo → 403 JSON |
|
|
22
|
+
* | passkey options | sem sudo | sem sudo |
|
|
23
|
+
* | passkey verify | sudo | sudo → 403 JSON |
|
|
24
|
+
* | recovery codes | (não existe) | sudo → 403 JSON |
|
|
25
|
+
*
|
|
26
|
+
* A única diferença é a FORMA da recusa: `requireSudo` devolve um redirect para
|
|
27
|
+
* `/account/confirm`, que numa SPA chega como uma página HTML onde ela esperava
|
|
28
|
+
* JSON. Aqui a recusa vira `403 { error: { code: 'sudo_required' } }` e a tela
|
|
29
|
+
* do host manda o usuário confirmar identidade por conta própria. Mesma
|
|
30
|
+
* política, resposta legível por máquina.
|
|
31
|
+
*
|
|
32
|
+
* `confirm` NÃO exige sudo — porque o `enroll` que criou o segredo pendente já
|
|
33
|
+
* exigiu, e pedir de novo no passo seguinte quebraria o enrolamento de quem
|
|
34
|
+
* demorou a digitar o código. É exatamente o que o console faz. O que o console
|
|
35
|
+
* não tem, e aqui existe, é o THROTTLE: o código de 6 dígitos é adivinhável, e
|
|
36
|
+
* a rota carrega o bucket de sudo (por IP) — mais apertado que o form, nunca
|
|
37
|
+
* mais frouxo.
|
|
38
|
+
*
|
|
39
|
+
* Os dois caminhos (clássico e JSON) COMPARTILHAM o slot do desafio WebAuthn
|
|
40
|
+
* (`PASSKEY_REG_CHALLENGE_KEY`), então um `options` pedido num deles pode ser
|
|
41
|
+
* finalizado pelo outro e nunca existem dois desafios vivos na mesma sessão.
|
|
42
|
+
*/
|
|
43
|
+
import '../augmentations.js';
|
|
44
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
45
|
+
export default class AccountMfaApiController {
|
|
46
|
+
#private;
|
|
47
|
+
/**
|
|
48
|
+
* Inicia o enrolamento TOTP: segredo pendente + `otpauth://` URI + o QR já
|
|
49
|
+
* renderizado como data-URL (o console renderiza server-side; aqui a tela do
|
|
50
|
+
* host recebe pronto e decide se mostra o QR, o segredo, ou os dois).
|
|
51
|
+
*/
|
|
52
|
+
enrollTotp(ctx: HttpContext): Promise<void | {
|
|
53
|
+
secret: string;
|
|
54
|
+
otpauthUri: string;
|
|
55
|
+
qrDataUrl: string;
|
|
56
|
+
}>;
|
|
57
|
+
/**
|
|
58
|
+
* Confirma o enrolamento com o código do app autenticador. Sucesso ativa o
|
|
59
|
+
* MFA e devolve os recovery codes — UMA vez, como no console (lá eles vão num
|
|
60
|
+
* flash; aqui, no corpo da resposta, que é o equivalente headless).
|
|
61
|
+
*/
|
|
62
|
+
confirmTotp(ctx: HttpContext): Promise<void | {
|
|
63
|
+
ok: boolean;
|
|
64
|
+
enabled: boolean;
|
|
65
|
+
recoveryCodes: string[];
|
|
66
|
+
}>;
|
|
67
|
+
/** Desliga o MFA (TOTP + recovery codes). Exige sudo, como o console. */
|
|
68
|
+
disableTotp(ctx: HttpContext): Promise<{
|
|
69
|
+
ok: boolean;
|
|
70
|
+
enabled: boolean;
|
|
71
|
+
} | undefined>;
|
|
72
|
+
/**
|
|
73
|
+
* Regenera os recovery codes de uma conta com MFA ATIVO e devolve os novos —
|
|
74
|
+
* uma única vez. Exige sudo: o resultado é um conjunto de credenciais que
|
|
75
|
+
* contorna o segundo fator, então vale o mesmo gate do `enroll`/`disable`.
|
|
76
|
+
*
|
|
77
|
+
* Capability-probed: stores que não implementam
|
|
78
|
+
* `regenerateRecoveryCodes` respondem 422 em vez de 500.
|
|
79
|
+
*/
|
|
80
|
+
regenerateRecoveryCodes(ctx: HttpContext): Promise<void | {
|
|
81
|
+
ok: boolean;
|
|
82
|
+
recoveryCodes: string[];
|
|
83
|
+
}>;
|
|
84
|
+
/**
|
|
85
|
+
* Options da cerimônia de REGISTRO de passkey, guardando o desafio na sessão.
|
|
86
|
+
* Sem sudo, igual ao endpoint clássico: quem paga o gate é o `verify`, que é
|
|
87
|
+
* onde a credencial passa a existir.
|
|
88
|
+
*/
|
|
89
|
+
passkeyRegisterOptions(ctx: HttpContext): Promise<any>;
|
|
90
|
+
/**
|
|
91
|
+
* Verifica o attestation contra o desafio da sessão e persiste a credencial.
|
|
92
|
+
*
|
|
93
|
+
* O endpoint clássico responde 302 numa navegação e `{ok:true}` num fetch;
|
|
94
|
+
* este responde SEMPRE JSON — inclusive na recusa de sudo, que lá é um
|
|
95
|
+
* redirect. É a diferença que faz a cerimônia caber numa SPA: o browser não
|
|
96
|
+
* pode navegar entre `startRegistration()` e a verificação.
|
|
97
|
+
*/
|
|
98
|
+
passkeyRegisterVerify(ctx: HttpContext): Promise<void | {
|
|
99
|
+
ok: boolean;
|
|
100
|
+
}>;
|
|
101
|
+
}
|