@adonis-agora/authkit-server 0.69.0 → 0.71.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/commands.json +18 -0
- package/build/commands/normalize_emails.d.ts +21 -0
- package/build/commands/normalize_emails.js +106 -0
- package/build/index.d.ts +4 -3
- package/build/index.js +6 -2
- package/build/src/accounts/account_store.d.ts +65 -1
- package/build/src/accounts/account_store.js +24 -0
- package/build/src/accounts/lucid_store/core.d.ts +2 -2
- package/build/src/accounts/lucid_store/core.js +16 -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.js +7 -2
- package/build/src/commands/normalize_emails.d.ts +78 -0
- package/build/src/commands/normalize_emails.js +129 -0
- 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_users_service.js +11 -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 +4 -18
- package/build/src/host/controllers/account_security_controller.js +3 -1
- package/build/src/host/controllers/account_session_controller.js +7 -5
- package/build/src/host/controllers/interaction_controller.d.ts +10 -0
- package/build/src/host/controllers/interaction_controller.js +46 -14
- package/build/src/host/controllers/registration_controller.js +15 -5
- package/build/src/host/controllers/social_controller.js +8 -1
- package/build/src/host/email_identifier.d.ts +29 -0
- package/build/src/host/email_identifier.js +31 -0
- 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 +51 -0
- 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/package.json +2 -2
|
@@ -48,7 +48,7 @@ export declare function grantDto(grant: AdminGrant): {
|
|
|
48
48
|
};
|
|
49
49
|
export declare function auditDto(event: StoredAuditEvent): {
|
|
50
50
|
id: string;
|
|
51
|
-
type: "account.deleted" | "account.expiration_warned" | "account.expired_login_blocked" | "account.exported" | "account.locked" | "account.signed_out_all" | "bot_protection.rejected" | "client.created" | "client.deleted" | "client.secret_regenerated" | "client.updated" | "email.change_requested" | "email.changed" | "email_change.cancelled" | "email_change.confirmed" | "email_change.requested" | "email_verification.consumed" | "email_verification.issued" | "grant.revoked_by_user" | "impersonation" | "impersonation.panel_viewed" | "impersonation.stopped" | "keys.rotated" | "login.failure" | "login.magic_link_sent" | "login.new_device" | "login.new_ip_notified" | "login.otp_failed" | "login.otp_invalidated" | "login.otp_sent" | "login.otp_verified" | "login.success" | "maintenance.disabled" | "maintenance.enabled" | "mfa.disabled" | "mfa.enabled" | "organization.created" | "organization.deactivated" | "organization.deleted" | "organization.invitation_accepted" | "organization.invitation_revoked" | "organization.invitation_sent" | "organization.member_added" | "organization.member_removed" | "organization.member_role_changed" | "organization.member_role_updated" | "organization.switched" | "organization.updated" | "otp.locked" | "otp.unlock_failed" | "otp.unlocked" | "passkey.registered" | "passkey.removed" | "password.changed" | "password.expired_change_forced" | "password.rehashed" | "password_reset.consumed" | "password_reset.issued" | "pat.issued" | "pat.revoked" | "pat.used" | "profile.updated" | "roles_catalog.updated" | "security_notice.sent" | "session.revoked" | "session.revoked_all" | "session.single_enforced" | "settings.updated" | "signup" | "sudo.confirmed" | "trusted_device.revoked" | "user.created" | "user.deleted" | "user.disabled" | "user.enabled" | "user.password_reset_sent";
|
|
51
|
+
type: "account.deleted" | "account.expiration_warned" | "account.expired_login_blocked" | "account.exported" | "account.locked" | "account.signed_out_all" | "bot_protection.rejected" | "client.created" | "client.deleted" | "client.secret_regenerated" | "client.updated" | "email.change_requested" | "email.changed" | "email_change.cancelled" | "email_change.confirmed" | "email_change.requested" | "email_verification.consumed" | "email_verification.issued" | "grant.revoked_by_user" | "impersonation" | "impersonation.panel_viewed" | "impersonation.stopped" | "keys.rotated" | "login.failure" | "login.magic_link_sent" | "login.new_device" | "login.new_ip_notified" | "login.otp_failed" | "login.otp_invalidated" | "login.otp_sent" | "login.otp_verified" | "login.success" | "maintenance.disabled" | "maintenance.enabled" | "mfa.disabled" | "mfa.enabled" | "mfa.recovery_codes_regenerated" | "organization.created" | "organization.deactivated" | "organization.deleted" | "organization.invitation_accepted" | "organization.invitation_revoked" | "organization.invitation_sent" | "organization.member_added" | "organization.member_removed" | "organization.member_role_changed" | "organization.member_role_updated" | "organization.switched" | "organization.updated" | "otp.locked" | "otp.unlock_failed" | "otp.unlocked" | "passkey.registered" | "passkey.removed" | "password.changed" | "password.expired_change_forced" | "password.rehashed" | "password_reset.consumed" | "password_reset.issued" | "pat.issued" | "pat.revoked" | "pat.used" | "profile.updated" | "roles_catalog.updated" | "security_notice.sent" | "session.revoked" | "session.revoked_all" | "session.single_enforced" | "settings.updated" | "signup" | "sudo.confirmed" | "trusted_device.revoked" | "user.created" | "user.deleted" | "user.disabled" | "user.enabled" | "user.password_reset_sent";
|
|
52
52
|
accountId: string | null;
|
|
53
53
|
email: string | null;
|
|
54
54
|
clientId: string | null;
|
|
@@ -69,7 +69,7 @@ export declare function clientPartialInput(v: ClientInputFields): Partial<Client
|
|
|
69
69
|
* `minLength` — seria duplicar/contornar a policy configurável do projeto.
|
|
70
70
|
*/
|
|
71
71
|
export declare const adminUserCreateValidator: import("@vinejs/vine").VineValidator<import("@vinejs/vine").VineObject<{
|
|
72
|
-
email: import("@vinejs/vine").VineString
|
|
72
|
+
email: import("@vinejs/vine/schema/base/literal").TransformModifier<import("@vinejs/vine").VineString, string>;
|
|
73
73
|
name: import("@vinejs/vine/schema/base/literal").OptionalModifier<import("@vinejs/vine").VineString>;
|
|
74
74
|
password: import("@vinejs/vine/schema/base/literal").OptionalModifier<import("@vinejs/vine").VineString>;
|
|
75
75
|
invite: import("@vinejs/vine/schema/base/literal").OptionalModifier<import("@vinejs/vine").VineBoolean>;
|
|
@@ -179,7 +179,7 @@ export declare const orgMemberRoleValidator: import("@vinejs/vine").VineValidato
|
|
|
179
179
|
}>, Record<string, any> | undefined>;
|
|
180
180
|
/** Criação de convite: e-mail obrigatório e validado; role opcional (default `member`). */
|
|
181
181
|
export declare const orgInvitationValidator: import("@vinejs/vine").VineValidator<import("@vinejs/vine").VineObject<{
|
|
182
|
-
email: import("@vinejs/vine").VineString
|
|
182
|
+
email: import("@vinejs/vine/schema/base/literal").TransformModifier<import("@vinejs/vine").VineString, string>;
|
|
183
183
|
role: import("@vinejs/vine/schema/base/literal").OptionalModifier<import("@vinejs/vine").VineString>;
|
|
184
184
|
}, {
|
|
185
185
|
email: string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import vine from '@vinejs/vine';
|
|
2
|
+
import { normalizeEmailIdentifier } from './email_identifier.js';
|
|
2
3
|
/**
|
|
3
4
|
* Validators VineJS dos recursos administrativos (Admin REST API + console admin).
|
|
4
5
|
*
|
|
@@ -123,7 +124,7 @@ export function clientPartialInput(v) {
|
|
|
123
124
|
* `minLength` — seria duplicar/contornar a policy configurável do projeto.
|
|
124
125
|
*/
|
|
125
126
|
export const adminUserCreateValidator = vine.compile(vine.object({
|
|
126
|
-
email: vine.string().trim().email(),
|
|
127
|
+
email: vine.string().trim().email().transform(normalizeEmailIdentifier),
|
|
127
128
|
name: vine.string().trim().maxLength(255).optional(),
|
|
128
129
|
password: vine.string().maxLength(255).optional(),
|
|
129
130
|
invite: vine.boolean().optional(),
|
|
@@ -162,7 +163,7 @@ export const orgMemberRoleValidator = vine.compile(vine.object({
|
|
|
162
163
|
}));
|
|
163
164
|
/** Criação de convite: e-mail obrigatório e validado; role opcional (default `member`). */
|
|
164
165
|
export const orgInvitationValidator = vine.compile(vine.object({
|
|
165
|
-
email: vine.string().trim().email(),
|
|
166
|
+
email: vine.string().trim().email().transform(normalizeEmailIdentifier),
|
|
166
167
|
role: vine.string().trim().minLength(1).optional(),
|
|
167
168
|
}));
|
|
168
169
|
// ─── Catálogo de roles ──────────────────────────────────────────────────────
|
|
@@ -4,11 +4,10 @@ import { supportsPasskeys } from '../../accounts/account_store.js';
|
|
|
4
4
|
import { accountPath } from '../account_paths.js';
|
|
5
5
|
import { ACCOUNT_SESSION_KEY } from '../account_session_key.js';
|
|
6
6
|
import { translate } from '../i18n.js';
|
|
7
|
+
import { PASSKEY_REG_CHALLENGE_KEY } from '../passkey_registration_challenge.js';
|
|
7
8
|
import { resolveRuntimeSettings } from '../runtime_settings.js';
|
|
8
9
|
import { dispatchSecurityNotice } from '../security_notice_service.js';
|
|
9
10
|
import { requireSudo } from '../sudo_mode.js';
|
|
10
|
-
/** Desafio WebAuthn pendente (registro) guardado na sessão entre begin/finish. */
|
|
11
|
-
const PASSKEY_REG_CHALLENGE_KEY = 'authkit_passkey_reg_challenge';
|
|
12
11
|
/**
|
|
13
12
|
* `true` quando a requisição é uma NAVEGAÇÃO (form/browser) e não um XHR/fetch:
|
|
14
13
|
* o cliente aceita `text/html` e NÃO pede explicitamente `application/json`.
|
|
@@ -6,26 +6,12 @@ import { ACCOUNT_SESSION_KEY } from '../account_session_key.js';
|
|
|
6
6
|
import { ACTIVE_ORG_COOKIE, ACTIVE_ORG_COOKIE_TTL, encodeActiveOrgCookie, } from '../active_org_cookie.js';
|
|
7
7
|
import { sendOrgInvitationEmail } from '../default_mailer.js';
|
|
8
8
|
import { ensureConsoleSession } from '../idp_session_bridge.js';
|
|
9
|
+
// Política efetiva compartilhada com o espelho JSON (`account_orgs_api_controller`):
|
|
10
|
+
// ponto de verdade único, para as duas superfícies nunca divergirem.
|
|
11
|
+
import { effectiveOrgPolicy, orgPolicyDefaults } from '../org_policy.js';
|
|
9
12
|
import { authkitOrigin } from '../origin.js';
|
|
10
13
|
import { resolveRuntimeSettings } from '../runtime_settings.js';
|
|
11
|
-
import { isRoleInCatalog, resolveEffectiveOrganizationsPolicy
|
|
12
|
-
/** Defaults estáticos da política de org (config do host) — o fallback da setting. */
|
|
13
|
-
function orgPolicyDefaults(cfg) {
|
|
14
|
-
return {
|
|
15
|
-
roles: cfg.organizations.roles,
|
|
16
|
-
allowSelfCreate: cfg.organizations.allowSelfCreate,
|
|
17
|
-
invitationTtlHours: cfg.organizations.invitationTtlHours,
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Política EFETIVA de organizações: setting `organizations_policy` (org → global)
|
|
22
|
-
* → `config.organizations` → default da lib. É o que a doc promete; antes este
|
|
23
|
-
* controller lia só o config estático, e a setting não tinha efeito aqui.
|
|
24
|
-
*/
|
|
25
|
-
async function effectiveOrgPolicy(ctx, cfg, orgId) {
|
|
26
|
-
const settings = await resolveRuntimeSettings(ctx);
|
|
27
|
-
return resolveEffectiveOrganizationsPolicy(settings, orgPolicyDefaults(cfg), orgId);
|
|
28
|
-
}
|
|
14
|
+
import { isRoleInCatalog, resolveEffectiveOrganizationsPolicy } from '../runtime_toggles.js';
|
|
29
15
|
/**
|
|
30
16
|
* Console de conta — Organizations. Server-rendered, padrão dos outros controllers
|
|
31
17
|
* de conta (account_tokens_controller, account_security_controller, etc.).
|
|
@@ -10,6 +10,7 @@ import { AdminSessionsService } from '../admin_sessions_service.js';
|
|
|
10
10
|
import { syncAdonisAuthLogout } from '../adonis_auth_sync.js';
|
|
11
11
|
import { AvatarUploadError, isAvatarUploadSupported, storeAvatar } from '../avatar_storage.js';
|
|
12
12
|
import { sendEmailChangeConfirmationEmail, sendEmailChangedCompletedEmail, sendEmailChangeNoticeEmail, } from '../default_mailer.js';
|
|
13
|
+
import { normalizeEmailIdentifier } from '../email_identifier.js';
|
|
13
14
|
import { translate } from '../i18n.js';
|
|
14
15
|
import { authkitOrigin } from '../origin.js';
|
|
15
16
|
import { resolveRuntimeSettings } from '../runtime_settings.js';
|
|
@@ -164,7 +165,8 @@ export default class AccountSecurityController {
|
|
|
164
165
|
confirmed = !!(await store.verifyCredentials(account.email, currentPassword));
|
|
165
166
|
}
|
|
166
167
|
if (!confirmed && confirmEmail) {
|
|
167
|
-
confirmed =
|
|
168
|
+
confirmed =
|
|
169
|
+
normalizeEmailIdentifier(confirmEmail) === normalizeEmailIdentifier(account.email);
|
|
168
170
|
}
|
|
169
171
|
if (!confirmed) {
|
|
170
172
|
ctx.session.flash('deleteError', translate(cfg.messages, 'account.delete.invalid_confirmation'));
|
|
@@ -3,6 +3,7 @@ import { accountHome } from '../account_home.js';
|
|
|
3
3
|
import { getAccountLoginUrl } from '../account_login_url.js';
|
|
4
4
|
import { ACCOUNT_SESSION_KEY } from '../account_session_key.js';
|
|
5
5
|
import { syncAdonisAuthLogin, syncAdonisAuthLogout } from '../adonis_auth_sync.js';
|
|
6
|
+
import { normalizeEmailIdentifier } from '../email_identifier.js';
|
|
6
7
|
import { translate } from '../i18n.js';
|
|
7
8
|
import { endBridgedIdpSession } from '../idp_session_bridge.js';
|
|
8
9
|
import { attemptPasswordLogin } from '../login_attempt.js';
|
|
@@ -60,10 +61,6 @@ export default class AccountSessionController {
|
|
|
60
61
|
const cfg = service.config;
|
|
61
62
|
const render = cfg.render;
|
|
62
63
|
const { email: rawEmail, password } = ctx.request.only(['email', 'password']);
|
|
63
|
-
// L6: normaliza o e-mail (trim + lowercase) ANTES de usar — garante que o
|
|
64
|
-
// lookup, o lockout (keyed por email) e a auditoria usem a forma canônica,
|
|
65
|
-
// independente do casing/espaços digitados.
|
|
66
|
-
const email = typeof rawEmail === 'string' ? rawEmail.trim().toLowerCase() : rawEmail;
|
|
67
64
|
const ip = ctx.request.ip?.() ?? null;
|
|
68
65
|
// Lê e valida o return_to do corpo do formulário (hidden input) — nunca confiar sem revalidar.
|
|
69
66
|
const rawReturnTo = ctx.request.input?.('return_to');
|
|
@@ -71,6 +68,11 @@ export default class AccountSessionController {
|
|
|
71
68
|
// Verificação + lockout + auditoria de falha centralizados (sem clientId no console).
|
|
72
69
|
// M1: passa `settings` p/ o lockout (e verified-email/expiração) runtime valerem aqui também.
|
|
73
70
|
const settings = await resolveRuntimeSettings(ctx);
|
|
71
|
+
// L6: normaliza (trim + lowercase, a MESMA função do cadastro e do passo de
|
|
72
|
+
// identificador do login OIDC) para que o lookup, o lockout (keyed por
|
|
73
|
+
// e-mail) e a auditoria usem a forma canônica, independente do casing/espaços
|
|
74
|
+
// digitados. A tela de erro é a mesma (credencial inválida), ache conta ou não.
|
|
75
|
+
const email = normalizeEmailIdentifier(rawEmail);
|
|
74
76
|
const result = await attemptPasswordLogin(cfg, {
|
|
75
77
|
email,
|
|
76
78
|
password,
|
|
@@ -106,7 +108,7 @@ export default class AccountSessionController {
|
|
|
106
108
|
// Opt-in: sincroniza ctx.auth.use(cfg.adonisAuth.guard) com a mesma conta —
|
|
107
109
|
// no-op sem `adonisAuth` configurado ou sem @adonisjs/auth inicializado.
|
|
108
110
|
await syncAdonisAuthLogin(ctx, cfg, acc);
|
|
109
|
-
await notifyLoginSuccess(ctx, cfg, { accountId: acc.id, email, ip });
|
|
111
|
+
await notifyLoginSuccess(ctx, cfg, { accountId: acc.id, email: acc.email, ip });
|
|
110
112
|
// Redireciona pro destino original (validado), ou cai no accountHome configurado.
|
|
111
113
|
return ctx.response.redirect(returnTo ?? accountHome(cfg));
|
|
112
114
|
}
|
|
@@ -7,6 +7,16 @@ export default class AuthInteractionController {
|
|
|
7
7
|
* POST /auth/interaction/:uid/identifier
|
|
8
8
|
* Step 1: receive email, store in session, redirect to step 2.
|
|
9
9
|
* ENUMERATION-SAFE: always advances regardless of whether the email exists.
|
|
10
|
+
*
|
|
11
|
+
* Normaliza o e-mail com a MESMA regra do cadastro (`normalizeEmailIdentifier`:
|
|
12
|
+
* `trim` + `toLowerCase`) — antes disto o valor cru ia para a sessão e a busca
|
|
13
|
+
* no store era por igualdade exata, então nem `Davi@x.com` achava `davi@x.com`.
|
|
14
|
+
*
|
|
15
|
+
* À PROVA DE ENUMERAÇÃO: o passo NÃO toca no account store — só normaliza e
|
|
16
|
+
* guarda. A resposta é o MESMO redirect incondicional, exista a conta ou não,
|
|
17
|
+
* e não há busca alguma aqui para diferenciar os dois casos nem no tempo de
|
|
18
|
+
* resposta. Quem decide se a conta existe é o passo 2, que renderiza a mesma
|
|
19
|
+
* tela nos dois casos.
|
|
10
20
|
*/
|
|
11
21
|
identifier(ctx: HttpContext): Promise<void>;
|
|
12
22
|
/**
|
|
@@ -4,6 +4,7 @@ import { AdminSessionsService } from '../admin_sessions_service.js';
|
|
|
4
4
|
import { guardBotProtection, resolveEffectiveBotProtection } from '../bot_protection.js';
|
|
5
5
|
import { brandFor, isFirstParty } from '../branding.js';
|
|
6
6
|
import { sendMagicLinkEmail, sendOtpUnlockEmail } from '../default_mailer.js';
|
|
7
|
+
import { normalizeEmailIdentifier } from '../email_identifier.js';
|
|
7
8
|
import { translate } from '../i18n.js';
|
|
8
9
|
import { assertLoginAllowed, attemptPasswordLogin, isEmailUnverifiedBlock, } from '../login_attempt.js';
|
|
9
10
|
import { magicChannelProp, normalizeLoginChannel } from '../login_channel.js';
|
|
@@ -36,11 +37,26 @@ function accountStatusErrorKey(reason) {
|
|
|
36
37
|
return 'login.password_expired_intro';
|
|
37
38
|
}
|
|
38
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* E-mail DIGITADO no passo de identificador, já normalizado (`trim` +
|
|
42
|
+
* `toLowerCase`). É o que a tela SEMPRE mostra — nunca o endereço gravado —,
|
|
43
|
+
* para que a canonicalização não vire um oráculo de "esta conta existe".
|
|
44
|
+
*/
|
|
39
45
|
const SESSION_KEY = 'authkit_login_email';
|
|
40
46
|
/** accountId aguardando o 2º fator depois da senha verificada. */
|
|
41
47
|
const MFA_PENDING_KEY = 'authkit_mfa_pending';
|
|
42
48
|
/** Desafio WebAuthn pendente (autenticação) guardado entre begin/finish no login. */
|
|
43
49
|
const PASSKEY_AUTH_CHALLENGE_KEY = 'authkit_passkey_auth_challenge';
|
|
50
|
+
/**
|
|
51
|
+
* Esquece o e-mail do login.
|
|
52
|
+
*
|
|
53
|
+
* Helper de UMA linha de propósito: os fluxos que encerram o passo de login
|
|
54
|
+
* (sucesso, "usar outro e-mail", expiração) chamam ISTO em vez de `forget` da
|
|
55
|
+
* chave crua, para que nenhum caminho deixe o e-mail pendurado na sessão.
|
|
56
|
+
*/
|
|
57
|
+
function forgetLoginEmail(ctx) {
|
|
58
|
+
ctx.session.forget(SESSION_KEY);
|
|
59
|
+
}
|
|
44
60
|
export default class AuthInteractionController {
|
|
45
61
|
/**
|
|
46
62
|
* Métodos de login efetivos (com os pins do `cfg.authMethods`) para QUALQUER render do
|
|
@@ -215,10 +231,22 @@ export default class AuthInteractionController {
|
|
|
215
231
|
* POST /auth/interaction/:uid/identifier
|
|
216
232
|
* Step 1: receive email, store in session, redirect to step 2.
|
|
217
233
|
* ENUMERATION-SAFE: always advances regardless of whether the email exists.
|
|
234
|
+
*
|
|
235
|
+
* Normaliza o e-mail com a MESMA regra do cadastro (`normalizeEmailIdentifier`:
|
|
236
|
+
* `trim` + `toLowerCase`) — antes disto o valor cru ia para a sessão e a busca
|
|
237
|
+
* no store era por igualdade exata, então nem `Davi@x.com` achava `davi@x.com`.
|
|
238
|
+
*
|
|
239
|
+
* À PROVA DE ENUMERAÇÃO: o passo NÃO toca no account store — só normaliza e
|
|
240
|
+
* guarda. A resposta é o MESMO redirect incondicional, exista a conta ou não,
|
|
241
|
+
* e não há busca alguma aqui para diferenciar os dois casos nem no tempo de
|
|
242
|
+
* resposta. Quem decide se a conta existe é o passo 2, que renderiza a mesma
|
|
243
|
+
* tela nos dois casos.
|
|
218
244
|
*/
|
|
219
245
|
async identifier(ctx) {
|
|
220
246
|
const { email } = ctx.request.only(['email']);
|
|
221
|
-
|
|
247
|
+
// `request.only` não valida tipo (`email[]=x` chega array): a normalização
|
|
248
|
+
// devolve string vazia para não-string, e o passo segue com o redirect.
|
|
249
|
+
ctx.session.put(SESSION_KEY, normalizeEmailIdentifier(email));
|
|
222
250
|
return ctx.response.redirect(`/auth/interaction/${ctx.request.param('uid')}`);
|
|
223
251
|
}
|
|
224
252
|
/**
|
|
@@ -251,7 +279,10 @@ export default class AuthInteractionController {
|
|
|
251
279
|
// credenciais. Fail-safe: erro/timeout no verify do host PERMITE o fluxo.
|
|
252
280
|
// We use a cfg-compatible object: pass effectiveBotLogin merged into cfg.
|
|
253
281
|
const cfgWithEffectiveBot = effectiveBotLogin !== cfg.botProtection ? { ...cfg, botProtection: effectiveBotLogin } : cfg;
|
|
254
|
-
if (!(await guardBotProtection(ctx, cfgWithEffectiveBot, 'login', {
|
|
282
|
+
if (!(await guardBotProtection(ctx, cfgWithEffectiveBot, 'login', {
|
|
283
|
+
email,
|
|
284
|
+
clientId,
|
|
285
|
+
}))) {
|
|
255
286
|
const found = await cfg.accountStore.findByEmail(email);
|
|
256
287
|
const account = found
|
|
257
288
|
? { fullName: found.name ?? null, globalRoles: found.globalRoles ?? [] }
|
|
@@ -407,7 +438,7 @@ export default class AuthInteractionController {
|
|
|
407
438
|
clientId,
|
|
408
439
|
trustedDevice: true,
|
|
409
440
|
});
|
|
410
|
-
ctx
|
|
441
|
+
forgetLoginEmail(ctx);
|
|
411
442
|
return;
|
|
412
443
|
}
|
|
413
444
|
}
|
|
@@ -447,7 +478,7 @@ export default class AuthInteractionController {
|
|
|
447
478
|
}
|
|
448
479
|
}
|
|
449
480
|
await service.interactions.completeLogin(ctx, acc.id, { remember });
|
|
450
|
-
await notifyLoginSuccess(ctx, cfg, { accountId: acc.id, email, ip, clientId });
|
|
481
|
+
await notifyLoginSuccess(ctx, cfg, { accountId: acc.id, email: acc.email, ip, clientId });
|
|
451
482
|
// single-session: após o completeLogin, revoga sessões que existiam ANTES.
|
|
452
483
|
// A sessão nova foi criada pelo provider no resume — ela NÃO está em prevSessionIds.
|
|
453
484
|
if (sessionPolicy.singleSession && prevSessionIds.size > 0) {
|
|
@@ -467,7 +498,7 @@ export default class AuthInteractionController {
|
|
|
467
498
|
}
|
|
468
499
|
}
|
|
469
500
|
// Clean up the session key after a successful login.
|
|
470
|
-
ctx
|
|
501
|
+
forgetLoginEmail(ctx);
|
|
471
502
|
}
|
|
472
503
|
/**
|
|
473
504
|
* POST /auth/interaction/:uid/mfa
|
|
@@ -554,7 +585,7 @@ export default class AuthInteractionController {
|
|
|
554
585
|
await this.maybeTrustDevice(ctx, cfg, accountId);
|
|
555
586
|
// Finaliza a interaction para o accountId pendente.
|
|
556
587
|
ctx.session.forget(MFA_PENDING_KEY);
|
|
557
|
-
ctx
|
|
588
|
+
forgetLoginEmail(ctx);
|
|
558
589
|
await notifyLoginSuccess(ctx, cfg, {
|
|
559
590
|
accountId,
|
|
560
591
|
ip,
|
|
@@ -738,7 +769,7 @@ export default class AuthInteractionController {
|
|
|
738
769
|
await cfg.audit?.record({
|
|
739
770
|
type: 'login.magic_link_sent',
|
|
740
771
|
accountId: issued.account.id,
|
|
741
|
-
email,
|
|
772
|
+
email: issued.account.email,
|
|
742
773
|
ip,
|
|
743
774
|
clientId,
|
|
744
775
|
});
|
|
@@ -746,18 +777,19 @@ export default class AuthInteractionController {
|
|
|
746
777
|
await cfg.audit?.record({
|
|
747
778
|
type: 'login.otp_sent',
|
|
748
779
|
accountId: issued.account.id,
|
|
749
|
-
email,
|
|
780
|
+
email: issued.account.email,
|
|
750
781
|
ip,
|
|
751
782
|
clientId,
|
|
752
783
|
});
|
|
753
784
|
}
|
|
754
785
|
const origin = authkitOrigin(cfg);
|
|
755
786
|
const magicUrl = `${origin}/auth/interaction/${uid}/magic?token=${encodeURIComponent(issued.token)}`;
|
|
787
|
+
const to = issued.account.email;
|
|
756
788
|
if (cfg.mail?.onMagicLink) {
|
|
757
|
-
await cfg.mail.onMagicLink({ email, magicUrl, token: issued.token, code, channel });
|
|
789
|
+
await cfg.mail.onMagicLink({ email: to, magicUrl, token: issued.token, code, channel });
|
|
758
790
|
}
|
|
759
791
|
else {
|
|
760
|
-
await sendMagicLinkEmail(ctx, { email, magicUrl, code, channel });
|
|
792
|
+
await sendMagicLinkEmail(ctx, { email: to, magicUrl, code, channel });
|
|
761
793
|
}
|
|
762
794
|
}
|
|
763
795
|
}
|
|
@@ -895,7 +927,7 @@ export default class AuthInteractionController {
|
|
|
895
927
|
clientId: clientId ?? null,
|
|
896
928
|
metadata: { method: 'magic_link' },
|
|
897
929
|
});
|
|
898
|
-
ctx
|
|
930
|
+
forgetLoginEmail(ctx);
|
|
899
931
|
await service.interactions.completeLogin(ctx, acc.id, { amr: ['email'] });
|
|
900
932
|
}
|
|
901
933
|
/**
|
|
@@ -1008,7 +1040,7 @@ export default class AuthInteractionController {
|
|
|
1008
1040
|
clientId: clientId ?? null,
|
|
1009
1041
|
metadata: { method: 'otp' },
|
|
1010
1042
|
});
|
|
1011
|
-
ctx
|
|
1043
|
+
forgetLoginEmail(ctx);
|
|
1012
1044
|
return service.interactions.completeLogin(ctx, result.account.id, { amr: ['email'] });
|
|
1013
1045
|
}
|
|
1014
1046
|
if (result.status === 'locked') {
|
|
@@ -1252,7 +1284,7 @@ export default class AuthInteractionController {
|
|
|
1252
1284
|
// Passkey OK: opcionalmente confia neste dispositivo (checkbox no challenge).
|
|
1253
1285
|
await this.maybeTrustDevice(ctx, cfg, accountId);
|
|
1254
1286
|
ctx.session.forget(MFA_PENDING_KEY);
|
|
1255
|
-
ctx
|
|
1287
|
+
forgetLoginEmail(ctx);
|
|
1256
1288
|
await notifyLoginSuccess(ctx, cfg, {
|
|
1257
1289
|
accountId,
|
|
1258
1290
|
ip,
|
|
@@ -1268,7 +1300,7 @@ export default class AuthInteractionController {
|
|
|
1268
1300
|
* Clears the stored email and redirects back to step 1.
|
|
1269
1301
|
*/
|
|
1270
1302
|
async switchIdentifier(ctx) {
|
|
1271
|
-
ctx
|
|
1303
|
+
forgetLoginEmail(ctx);
|
|
1272
1304
|
return ctx.response.redirect(`/auth/interaction/${ctx.request.param('uid')}`);
|
|
1273
1305
|
}
|
|
1274
1306
|
/**
|
|
@@ -118,6 +118,8 @@ export default class AuthRegistrationController {
|
|
|
118
118
|
}
|
|
119
119
|
const data = await ctx.request.validateUsing(signupValidator);
|
|
120
120
|
const accountStore = cfg.accountStore;
|
|
121
|
+
// Duplicado pela MESMA forma que o login busca (`data.email` já saiu
|
|
122
|
+
// normalizado do validator).
|
|
121
123
|
const existing = await accountStore.findByEmail(data.email);
|
|
122
124
|
if (existing) {
|
|
123
125
|
return render(ctx, 'signup', {
|
|
@@ -233,15 +235,18 @@ export default class AuthRegistrationController {
|
|
|
233
235
|
});
|
|
234
236
|
}
|
|
235
237
|
// Emite + envia o magic link (mesma construção do login por magic link).
|
|
238
|
+
// `data.email` já é a forma normalizada — a mesma sob a qual a conta foi
|
|
239
|
+
// encontrada acima ou acabou de ser criada.
|
|
236
240
|
const issued = await accountStore.issueMagicLinkToken(data.email);
|
|
237
241
|
if (issued) {
|
|
238
242
|
const origin = authkitOrigin(cfg);
|
|
239
243
|
const magicUrl = `${origin}/auth/interaction/${uid}/magic?token=${encodeURIComponent(issued.token)}`;
|
|
244
|
+
const to = issued.account.email;
|
|
240
245
|
if (cfg.mail?.onMagicLink) {
|
|
241
|
-
await cfg.mail.onMagicLink({ email:
|
|
246
|
+
await cfg.mail.onMagicLink({ email: to, magicUrl, token: issued.token });
|
|
242
247
|
}
|
|
243
248
|
else {
|
|
244
|
-
await sendMagicLinkEmail(ctx, { email:
|
|
249
|
+
await sendMagicLinkEmail(ctx, { email: to, magicUrl });
|
|
245
250
|
}
|
|
246
251
|
}
|
|
247
252
|
// Resposta uniforme: "enviamos um link" (não vaza existência da conta).
|
|
@@ -322,21 +327,26 @@ export default class AuthRegistrationController {
|
|
|
322
327
|
}
|
|
323
328
|
const { email } = await ctx.request.validateUsing(forgotPasswordValidator);
|
|
324
329
|
const accountStore = cfg.accountStore;
|
|
330
|
+
// `email` já saiu normalizado do validator (a MESMA normalização do
|
|
331
|
+
// cadastro e do login). Resposta uniforme: a tela abaixo é a mesma, ache
|
|
332
|
+
// conta ou não.
|
|
325
333
|
const result = await accountStore.issuePasswordResetToken(email);
|
|
326
334
|
if (result) {
|
|
327
335
|
await cfg.audit?.record({
|
|
328
336
|
type: 'password_reset.issued',
|
|
329
|
-
email,
|
|
337
|
+
email: result.account.email,
|
|
330
338
|
ip: ctx.request.ip?.() ?? null,
|
|
331
339
|
});
|
|
332
340
|
const origin = authkitOrigin(cfg);
|
|
333
341
|
const url = `${origin}/auth/reset-password?token=${result.token}`;
|
|
334
342
|
// Hook do config tem prioridade (override); senão usa o mailer default do host.
|
|
343
|
+
// O link vai para a caixa postal sob a qual a conta está gravada.
|
|
344
|
+
const to = result.account.email;
|
|
335
345
|
if (cfg.mail?.onPasswordReset) {
|
|
336
|
-
await cfg.mail.onPasswordReset({ email, resetUrl: url, token: result.token });
|
|
346
|
+
await cfg.mail.onPasswordReset({ email: to, resetUrl: url, token: result.token });
|
|
337
347
|
}
|
|
338
348
|
else {
|
|
339
|
-
await sendPasswordResetEmail(ctx, { email, resetUrl: url });
|
|
349
|
+
await sendPasswordResetEmail(ctx, { email: to, resetUrl: url });
|
|
340
350
|
}
|
|
341
351
|
}
|
|
342
352
|
return render(ctx, 'forgot', {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import '../augmentations.js';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { supportsLoginMethodsPreference, supportsMagicLink, supportsPasskeys, supportsProviderIdentity, } from '../../accounts/account_store.js';
|
|
4
|
+
import { normalizeEmailIdentifier } from '../email_identifier.js';
|
|
4
5
|
import { assertLoginAllowed } from '../login_attempt.js';
|
|
5
6
|
import { resolveRuntimeSettingsOrNoop } from '../runtime_settings.js';
|
|
6
7
|
import { resolveEffectiveAuthMethods } from '../runtime_toggles.js';
|
|
@@ -54,7 +55,10 @@ export default class AuthSocialController {
|
|
|
54
55
|
const service = await ctx.containerResolver.make('authkit.server');
|
|
55
56
|
const cfg = service.config;
|
|
56
57
|
const store = cfg.accountStore;
|
|
57
|
-
|
|
58
|
+
// Normaliza o e-mail do provider com a MESMA regra do cadastro/login: sem
|
|
59
|
+
// isto, um provider que devolve o endereço com maiúsculas criava uma conta
|
|
60
|
+
// que o login (normalizado) não encontrava mais.
|
|
61
|
+
const email = profile.email ? normalizeEmailIdentifier(profile.email) : undefined;
|
|
58
62
|
// Account linking exige a capacidade de provider-identity (model wired no store).
|
|
59
63
|
// Ausente → não há como ligar a identidade; volta ao login em vez de quebrar.
|
|
60
64
|
if (!supportsProviderIdentity(store)) {
|
|
@@ -67,6 +71,9 @@ export default class AuthSocialController {
|
|
|
67
71
|
// 3. Senão → cria conta nova e liga a identidade.
|
|
68
72
|
let user = await store.findByProviderIdentity(provider, profile.id);
|
|
69
73
|
if (!user && email) {
|
|
74
|
+
// Busca pela MESMA forma normalizada que o login usa. Uma conta gravada
|
|
75
|
+
// com outra grafia não é encontrada aqui e a pessoa ganharia uma SEGUNDA
|
|
76
|
+
// conta — por isso `authkit:users:normalize-emails` é passo de upgrade.
|
|
70
77
|
const byEmail = await store.findByEmail(email);
|
|
71
78
|
if (byEmail) {
|
|
72
79
|
await store.linkProviderIdentity({
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalização ÚNICA e CONSERVADORA do e-mail usado como identidade da conta.
|
|
3
|
+
*
|
|
4
|
+
* Faz `trim()` + `toLowerCase()` e MAIS NADA. O endereço que a pessoa digitou é a
|
|
5
|
+
* identidade dela: a lib NÃO decide que `a.b@gmail.com` e `ab@gmail.com` são a
|
|
6
|
+
* mesma pessoa, nem descarta o sub-endereço (`+tag`) que ela escolheu usar.
|
|
7
|
+
*
|
|
8
|
+
* POR QUE ISTO EXISTE: até a v0.68 o cadastro validava o e-mail com
|
|
9
|
+
* `.normalizeEmail()` do VineJS (os defaults do validator.js), que para o gmail
|
|
10
|
+
* REMOVE os pontos e o `+tag` do local part. A conta nascia com um endereço
|
|
11
|
+
* DIFERENTE do digitado (`davi.carvalho96@gmail.com` → `davicarvalho96@gmail.com`)
|
|
12
|
+
* enquanto o passo de identificador do login não normalizava NADA e buscava por
|
|
13
|
+
* igualdade exata — resultado: quem tinha ponto ou `+tag` no gmail ficava
|
|
14
|
+
* trancado do lado de fora, e, por o login ser à prova de enumeração, sem
|
|
15
|
+
* nenhuma mensagem de erro. Uma normalização só, usada nos DOIS lados, fecha a
|
|
16
|
+
* assimetria.
|
|
17
|
+
*
|
|
18
|
+
* Use em TODO ponto que grava ou busca a identidade: cadastro (com e sem senha),
|
|
19
|
+
* "esqueci a senha", troca de e-mail, criação por admin, convite de organização,
|
|
20
|
+
* import de usuários, cadastro social e o passo de identificador do login.
|
|
21
|
+
*
|
|
22
|
+
* As contas GRAVADAS antes disto (com o endereço mutilado, ou com maiúsculas
|
|
23
|
+
* vindas de import/convite/provider social) ficam inalcançáveis pelo login, que
|
|
24
|
+
* busca a forma normalizada. Elas pedem MIGRAÇÃO do endereço gravado — é o que o
|
|
25
|
+
* comando `authkit:users:normalize-emails` faz (ver `commands/normalize_emails.ts`).
|
|
26
|
+
* A lib não tenta adivinhar grafias no caminho do login: cada tentativa extra é
|
|
27
|
+
* uma query a mais por e-mail desconhecido, que é justamente o caminho de ataque.
|
|
28
|
+
*/
|
|
29
|
+
export declare function normalizeEmailIdentifier(raw: unknown): string;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalização ÚNICA e CONSERVADORA do e-mail usado como identidade da conta.
|
|
3
|
+
*
|
|
4
|
+
* Faz `trim()` + `toLowerCase()` e MAIS NADA. O endereço que a pessoa digitou é a
|
|
5
|
+
* identidade dela: a lib NÃO decide que `a.b@gmail.com` e `ab@gmail.com` são a
|
|
6
|
+
* mesma pessoa, nem descarta o sub-endereço (`+tag`) que ela escolheu usar.
|
|
7
|
+
*
|
|
8
|
+
* POR QUE ISTO EXISTE: até a v0.68 o cadastro validava o e-mail com
|
|
9
|
+
* `.normalizeEmail()` do VineJS (os defaults do validator.js), que para o gmail
|
|
10
|
+
* REMOVE os pontos e o `+tag` do local part. A conta nascia com um endereço
|
|
11
|
+
* DIFERENTE do digitado (`davi.carvalho96@gmail.com` → `davicarvalho96@gmail.com`)
|
|
12
|
+
* enquanto o passo de identificador do login não normalizava NADA e buscava por
|
|
13
|
+
* igualdade exata — resultado: quem tinha ponto ou `+tag` no gmail ficava
|
|
14
|
+
* trancado do lado de fora, e, por o login ser à prova de enumeração, sem
|
|
15
|
+
* nenhuma mensagem de erro. Uma normalização só, usada nos DOIS lados, fecha a
|
|
16
|
+
* assimetria.
|
|
17
|
+
*
|
|
18
|
+
* Use em TODO ponto que grava ou busca a identidade: cadastro (com e sem senha),
|
|
19
|
+
* "esqueci a senha", troca de e-mail, criação por admin, convite de organização,
|
|
20
|
+
* import de usuários, cadastro social e o passo de identificador do login.
|
|
21
|
+
*
|
|
22
|
+
* As contas GRAVADAS antes disto (com o endereço mutilado, ou com maiúsculas
|
|
23
|
+
* vindas de import/convite/provider social) ficam inalcançáveis pelo login, que
|
|
24
|
+
* busca a forma normalizada. Elas pedem MIGRAÇÃO do endereço gravado — é o que o
|
|
25
|
+
* comando `authkit:users:normalize-emails` faz (ver `commands/normalize_emails.ts`).
|
|
26
|
+
* A lib não tenta adivinhar grafias no caminho do login: cada tentativa extra é
|
|
27
|
+
* uma query a mais por e-mail desconhecido, que é justamente o caminho de ataque.
|
|
28
|
+
*/
|
|
29
|
+
export function normalizeEmailIdentifier(raw) {
|
|
30
|
+
return typeof raw === 'string' ? raw.trim().toLowerCase() : '';
|
|
31
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Política EFETIVA de organizações — ponto de verdade ÚNICO das duas
|
|
3
|
+
* superfícies member-facing.
|
|
4
|
+
*
|
|
5
|
+
* O console HTML (`AccountOrgsController`) e o espelho JSON
|
|
6
|
+
* (`AccountOrgsApiController`) precisam responder à MESMA pergunta — "este
|
|
7
|
+
* usuário pode criar org? que papéis existem? quanto tempo o convite vale?" —
|
|
8
|
+
* e a resposta não está no config estático: ela é resolvida na ordem setting da
|
|
9
|
+
* org → setting global → `config.organizations` → default da lib.
|
|
10
|
+
*
|
|
11
|
+
* Mora num módulo próprio, e não dentro de um dos controllers, porque duas
|
|
12
|
+
* cópias desta resolução são exatamente como o caminho JSON acaba mais frouxo
|
|
13
|
+
* (ou mais apertado) que o formulário que ele espelha — o bug que este pacote
|
|
14
|
+
* de mudanças existe para não ter.
|
|
15
|
+
*/
|
|
16
|
+
import './augmentations.js';
|
|
17
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
18
|
+
import { type OrganizationsPolicyConfigDefaults, type ResolvedOrganizationsPolicySetting } from './runtime_toggles.js';
|
|
19
|
+
/** Defaults estáticos da política de org (config do host) — o fallback da setting. */
|
|
20
|
+
export declare function orgPolicyDefaults(cfg: any): OrganizationsPolicyConfigDefaults;
|
|
21
|
+
/**
|
|
22
|
+
* Política efetiva para o `orgId` (ou global, quando ausente). `settings` nulo —
|
|
23
|
+
* DB fora do ar, app sem lucid — cai no config estático, fail-safe herdado de
|
|
24
|
+
* {@link resolveEffectiveOrganizationsPolicy}.
|
|
25
|
+
*/
|
|
26
|
+
export declare function effectiveOrgPolicy(ctx: HttpContext, cfg: any, orgId?: string | null): Promise<ResolvedOrganizationsPolicySetting>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Política EFETIVA de organizações — ponto de verdade ÚNICO das duas
|
|
3
|
+
* superfícies member-facing.
|
|
4
|
+
*
|
|
5
|
+
* O console HTML (`AccountOrgsController`) e o espelho JSON
|
|
6
|
+
* (`AccountOrgsApiController`) precisam responder à MESMA pergunta — "este
|
|
7
|
+
* usuário pode criar org? que papéis existem? quanto tempo o convite vale?" —
|
|
8
|
+
* e a resposta não está no config estático: ela é resolvida na ordem setting da
|
|
9
|
+
* org → setting global → `config.organizations` → default da lib.
|
|
10
|
+
*
|
|
11
|
+
* Mora num módulo próprio, e não dentro de um dos controllers, porque duas
|
|
12
|
+
* cópias desta resolução são exatamente como o caminho JSON acaba mais frouxo
|
|
13
|
+
* (ou mais apertado) que o formulário que ele espelha — o bug que este pacote
|
|
14
|
+
* de mudanças existe para não ter.
|
|
15
|
+
*/
|
|
16
|
+
import './augmentations.js';
|
|
17
|
+
import { resolveRuntimeSettings } from './runtime_settings.js';
|
|
18
|
+
import { resolveEffectiveOrganizationsPolicy, } from './runtime_toggles.js';
|
|
19
|
+
/** Defaults estáticos da política de org (config do host) — o fallback da setting. */
|
|
20
|
+
export function orgPolicyDefaults(cfg) {
|
|
21
|
+
return {
|
|
22
|
+
roles: cfg.organizations.roles,
|
|
23
|
+
allowSelfCreate: cfg.organizations.allowSelfCreate,
|
|
24
|
+
invitationTtlHours: cfg.organizations.invitationTtlHours,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Política efetiva para o `orgId` (ou global, quando ausente). `settings` nulo —
|
|
29
|
+
* DB fora do ar, app sem lucid — cai no config estático, fail-safe herdado de
|
|
30
|
+
* {@link resolveEffectiveOrganizationsPolicy}.
|
|
31
|
+
*/
|
|
32
|
+
export async function effectiveOrgPolicy(ctx, cfg, orgId) {
|
|
33
|
+
const settings = await resolveRuntimeSettings(ctx);
|
|
34
|
+
return resolveEffectiveOrganizationsPolicy(settings, orgPolicyDefaults(cfg), orgId);
|
|
35
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chave de sessão do desafio WebAuthn PENDENTE de REGISTRO de passkey.
|
|
3
|
+
*
|
|
4
|
+
* Mora num módulo próprio porque a cerimônia agora tem DOIS pares de rotas — o
|
|
5
|
+
* clássico (`/account/mfa/passkeys/{options,verify}`, que responde redirect numa
|
|
6
|
+
* navegação) e o JSON (`/account/api/mfa/passkeys/{options,verify}`, que nunca
|
|
7
|
+
* navega). Os dois precisam ler e escrever o MESMO slot: um `begin` feito por um
|
|
8
|
+
* caminho tem de poder ser finalizado pelo outro, e — mais importante — um
|
|
9
|
+
* desafio só pode existir UMA vez por sessão. Duas constantes com o mesmo valor
|
|
10
|
+
* em arquivos diferentes seriam a mesma coisa até alguém mudar uma delas.
|
|
11
|
+
*/
|
|
12
|
+
export declare const PASSKEY_REG_CHALLENGE_KEY = "authkit_passkey_reg_challenge";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chave de sessão do desafio WebAuthn PENDENTE de REGISTRO de passkey.
|
|
3
|
+
*
|
|
4
|
+
* Mora num módulo próprio porque a cerimônia agora tem DOIS pares de rotas — o
|
|
5
|
+
* clássico (`/account/mfa/passkeys/{options,verify}`, que responde redirect numa
|
|
6
|
+
* navegação) e o JSON (`/account/api/mfa/passkeys/{options,verify}`, que nunca
|
|
7
|
+
* navega). Os dois precisam ler e escrever o MESMO slot: um `begin` feito por um
|
|
8
|
+
* caminho tem de poder ser finalizado pelo outro, e — mais importante — um
|
|
9
|
+
* desafio só pode existir UMA vez por sessão. Duas constantes com o mesmo valor
|
|
10
|
+
* em arquivos diferentes seriam a mesma coisa até alguém mudar uma delas.
|
|
11
|
+
*/
|
|
12
|
+
export const PASSKEY_REG_CHALLENGE_KEY = 'authkit_passkey_reg_challenge';
|