@adonis-agora/authkit-server 0.58.3 → 0.60.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/index.d.ts +4 -2
- package/build/index.js +3 -1
- package/build/src/accounts/account_store.d.ts +20 -1
- package/build/src/accounts/account_store.js +4 -0
- package/build/src/accounts/lucid_account_store.js +21 -2
- package/build/src/accounts/lucid_store/login_methods.d.ts +11 -0
- package/build/src/accounts/lucid_store/login_methods.js +32 -0
- package/build/src/audit/audit_sink.d.ts +10 -0
- package/build/src/doctor/checks.d.ts +13 -0
- package/build/src/doctor/checks.js +79 -0
- package/build/src/events/dispatcher.d.ts +7 -3
- package/build/src/events/dispatcher.js +9 -3
- package/build/src/host/account_api/account_api_controller.d.ts +38 -0
- package/build/src/host/account_api/account_api_controller.js +84 -3
- package/build/src/host/admin_api/admin_orgs_service.js +8 -0
- package/build/src/host/controllers/account_orgs_controller.js +13 -2
- package/build/src/host/controllers/interaction_controller.js +159 -12
- package/build/src/host/controllers/social_controller.js +29 -1
- package/build/src/host/register_auth_host.js +3 -0
- package/build/src/host/ui-dist/assets/{index-6cE5JMyP.js → index-Cb38N16r.js} +1 -1
- package/build/src/host/ui-dist/index.html +1 -1
- package/build/src/host/user_login_methods.d.ts +94 -0
- package/build/src/host/user_login_methods.js +132 -0
- package/package.json +3 -3
|
@@ -72,8 +72,13 @@ export default class AccountOrgsController {
|
|
|
72
72
|
if (!name || !slug)
|
|
73
73
|
return response.redirect(accountPath('orgs'));
|
|
74
74
|
try {
|
|
75
|
-
await store.createOrg({ name, slug, ownerAccountId: accountId });
|
|
76
|
-
await cfg.audit?.record({
|
|
75
|
+
const org = await store.createOrg({ name, slug, ownerAccountId: accountId });
|
|
76
|
+
await cfg.audit?.record({
|
|
77
|
+
type: 'organization.created',
|
|
78
|
+
accountId,
|
|
79
|
+
orgId: org.id,
|
|
80
|
+
metadata: { slug },
|
|
81
|
+
});
|
|
77
82
|
}
|
|
78
83
|
catch {
|
|
79
84
|
// slug duplicado ou outro erro — redireciona sem mensagem de erro específica
|
|
@@ -112,6 +117,7 @@ export default class AccountOrgsController {
|
|
|
112
117
|
await cfg.audit?.record({
|
|
113
118
|
type: 'organization.switched',
|
|
114
119
|
accountId,
|
|
120
|
+
orgId,
|
|
115
121
|
metadata: { orgId, orgSlug: org.slug },
|
|
116
122
|
});
|
|
117
123
|
return response.redirect(accountPath('orgs'));
|
|
@@ -140,6 +146,7 @@ export default class AccountOrgsController {
|
|
|
140
146
|
await cfg.audit?.record({
|
|
141
147
|
type: 'organization.member_removed',
|
|
142
148
|
accountId,
|
|
149
|
+
orgId: params.id,
|
|
143
150
|
metadata: { orgId: params.id, self: true },
|
|
144
151
|
});
|
|
145
152
|
}
|
|
@@ -217,6 +224,7 @@ export default class AccountOrgsController {
|
|
|
217
224
|
await cfg.audit?.record({
|
|
218
225
|
type: 'organization.invitation_sent',
|
|
219
226
|
accountId,
|
|
227
|
+
orgId: params.id,
|
|
220
228
|
metadata: { orgId: params.id, email, role },
|
|
221
229
|
});
|
|
222
230
|
return response.redirect(accountPath('orgs'));
|
|
@@ -270,6 +278,7 @@ export default class AccountOrgsController {
|
|
|
270
278
|
await cfg.audit?.record({
|
|
271
279
|
type: 'organization.invitation_accepted',
|
|
272
280
|
accountId,
|
|
281
|
+
orgId: invitation.organizationId,
|
|
273
282
|
metadata: { orgId: invitation.organizationId, invitationId: invitation.id },
|
|
274
283
|
});
|
|
275
284
|
}
|
|
@@ -294,6 +303,7 @@ export default class AccountOrgsController {
|
|
|
294
303
|
await cfg.audit?.record({
|
|
295
304
|
type: 'organization.member_removed',
|
|
296
305
|
actorId,
|
|
306
|
+
orgId: params.id,
|
|
297
307
|
metadata: { orgId: params.id, targetAccountId: params.accountId },
|
|
298
308
|
});
|
|
299
309
|
}
|
|
@@ -318,6 +328,7 @@ export default class AccountOrgsController {
|
|
|
318
328
|
await cfg.audit?.record({
|
|
319
329
|
type: 'organization.invitation_revoked',
|
|
320
330
|
actorId,
|
|
331
|
+
orgId: params.id,
|
|
321
332
|
metadata: { orgId: params.id, invitationId: params.invId },
|
|
322
333
|
});
|
|
323
334
|
return response.redirect(accountPath('orgs'));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import '../augmentations.js';
|
|
2
|
-
import { supportsMagicLink, supportsOtpLogin, supportsPasskeys, } from '../../accounts/account_store.js';
|
|
2
|
+
import { supportsLoginMethodsPreference, supportsMagicLink, supportsOtpLogin, supportsPasskeys, } from '../../accounts/account_store.js';
|
|
3
3
|
import { AdminSessionsService } from '../admin_sessions_service.js';
|
|
4
4
|
import { guardBotProtection, resolveEffectiveBotProtection } from '../bot_protection.js';
|
|
5
5
|
import { brandFor, isFirstParty } from '../branding.js';
|
|
@@ -14,6 +14,7 @@ import { createOtpLockout, generateOtpUnlockToken, rawToDbOtpUnlockToken, resolv
|
|
|
14
14
|
import { resolveRuntimeSettingsOrNoop } from '../runtime_settings.js';
|
|
15
15
|
import { resolveEffectiveAuthMethods, resolveEffectiveMaintenanceMode, resolveEffectiveRegistration, resolveEffectiveSessionPolicy, } from '../runtime_toggles.js';
|
|
16
16
|
import { TRUSTED_DEVICE_COOKIE, buildTrustedDevicePayload, isTrustedDeviceValid, } from '../trusted_device.js';
|
|
17
|
+
import { normalizeUserLoginMethods, resolveEffectiveUserLoginMethods, } from '../user_login_methods.js';
|
|
17
18
|
/**
|
|
18
19
|
* Chave i18n do erro de status de conta (disabled/expired) compartilhada pelos
|
|
19
20
|
* fluxos passwordless (magic link, OTP, passkey).
|
|
@@ -73,6 +74,44 @@ export default class AuthInteractionController {
|
|
|
73
74
|
otpEnabled,
|
|
74
75
|
};
|
|
75
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Métodos de login com a preferência POR USUÁRIO aplicada (interseção global ×
|
|
79
|
+
* `login_methods` da conta). Usado em TODO render/POST que já conhece o email
|
|
80
|
+
* (passo 2+ do login) — o passo 1 (identifier) não tem usuário, então usa só o
|
|
81
|
+
* global via {@link #loginMethods}.
|
|
82
|
+
*
|
|
83
|
+
* Fail-safe: store sem a capacidade, conta inexistente ou qualquer erro →
|
|
84
|
+
* métodos globais sem alteração (a preferência nunca derruba o login).
|
|
85
|
+
*/
|
|
86
|
+
async #userScopedLoginMethods(ctx, cfg, email, runtimeSettings) {
|
|
87
|
+
const base = await this.#loginMethods(ctx, cfg, runtimeSettings);
|
|
88
|
+
if (!email)
|
|
89
|
+
return base;
|
|
90
|
+
try {
|
|
91
|
+
if (!supportsLoginMethodsPreference(cfg.accountStore))
|
|
92
|
+
return base;
|
|
93
|
+
const acc = await cfg.accountStore.findByEmail(email);
|
|
94
|
+
if (!acc)
|
|
95
|
+
return base;
|
|
96
|
+
const pref = await cfg.accountStore.getLoginMethods(acc.id);
|
|
97
|
+
const normalized = normalizeUserLoginMethods(pref);
|
|
98
|
+
if (!normalized)
|
|
99
|
+
return base;
|
|
100
|
+
const scoped = resolveEffectiveUserLoginMethods(base.authMethods, normalized);
|
|
101
|
+
const magicLinkCapableConfig = cfg.passwordless.magicLink && supportsMagicLink(cfg.accountStore);
|
|
102
|
+
return {
|
|
103
|
+
...base,
|
|
104
|
+
authMethods: scoped,
|
|
105
|
+
magicLinkAvailable: scoped.magicLink && magicLinkCapableConfig,
|
|
106
|
+
// OTP é extensão do magic link — desligar magic link desliga o código.
|
|
107
|
+
otpEnabled: base.otpEnabled && scoped.magicLink,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Preferência nunca derruba o login.
|
|
112
|
+
return base;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
76
115
|
async show(ctx) {
|
|
77
116
|
const service = await ctx.containerResolver.make('authkit.server');
|
|
78
117
|
const cfg = service.config;
|
|
@@ -129,7 +168,11 @@ export default class AuthInteractionController {
|
|
|
129
168
|
// `authMethods`, `magicLinkAvailable` e `otpEnabled` saiam POR CONSTRUÇÃO
|
|
130
169
|
// (o seletor choose-first precisa de `otpEnabled` para oferecer "código").
|
|
131
170
|
// Passa o runtimeSettings já resolvido para maintenance (sem 2ª resolução).
|
|
132
|
-
|
|
171
|
+
// Com email na sessão (passo 2+), aplica a preferência POR USUÁRIO — os
|
|
172
|
+
// métodos que o dono da conta desligou não aparecem nem aqui nem nos POSTs.
|
|
173
|
+
const loginMethods = email
|
|
174
|
+
? await this.#userScopedLoginMethods(ctx, cfg, email, runtimeSettingsForMaintenance)
|
|
175
|
+
: await this.#loginMethods(ctx, cfg, runtimeSettingsForMaintenance);
|
|
133
176
|
const authMethods = loginMethods.authMethods;
|
|
134
177
|
if (!email) {
|
|
135
178
|
// Step 1: identifier (email only).
|
|
@@ -215,7 +258,7 @@ export default class AuthInteractionController {
|
|
|
215
258
|
? { fullName: found.name ?? null, globalRoles: found.globalRoles ?? [] }
|
|
216
259
|
: null;
|
|
217
260
|
return render(ctx, 'login', {
|
|
218
|
-
...(await this.#
|
|
261
|
+
...(await this.#userScopedLoginMethods(ctx, cfg, email, runtimeSettings)),
|
|
219
262
|
uid: ctx.request.param('uid'),
|
|
220
263
|
csrfToken: ctx.request.csrfToken,
|
|
221
264
|
step: 'password',
|
|
@@ -226,6 +269,35 @@ export default class AuthInteractionController {
|
|
|
226
269
|
botProtection: effectiveBotLogin?.widget,
|
|
227
270
|
});
|
|
228
271
|
}
|
|
272
|
+
// Preferência por usuário: senha desligada para ESTA conta → recusa ANTES de
|
|
273
|
+
// verificar credenciais (mesma tela/erro de credencial inválida — não vaza
|
|
274
|
+
// se a conta existe nem qual preferência está gravada).
|
|
275
|
+
const userMethodsForLogin = await this.#userScopedLoginMethods(ctx, cfg, email, runtimeSettings);
|
|
276
|
+
if (!userMethodsForLogin.authMethods.password) {
|
|
277
|
+
const found = await cfg.accountStore.findByEmail(email);
|
|
278
|
+
const account = found
|
|
279
|
+
? { fullName: found.name ?? null, globalRoles: found.globalRoles ?? [] }
|
|
280
|
+
: null;
|
|
281
|
+
await cfg.audit?.record({
|
|
282
|
+
type: 'login.failure',
|
|
283
|
+
ip,
|
|
284
|
+
clientId,
|
|
285
|
+
metadata: { stage: 'password', reason: 'method_disabled_by_user' },
|
|
286
|
+
});
|
|
287
|
+
return render(ctx, 'login', {
|
|
288
|
+
...userMethodsForLogin,
|
|
289
|
+
uid: ctx.request.param('uid'),
|
|
290
|
+
csrfToken: ctx.request.csrfToken,
|
|
291
|
+
step: 'password',
|
|
292
|
+
email,
|
|
293
|
+
account,
|
|
294
|
+
error: translate(cfg.messages, 'errors.invalid_credentials'),
|
|
295
|
+
brand,
|
|
296
|
+
botProtection: effectiveBotLogin?.on.includes('login')
|
|
297
|
+
? effectiveBotLogin.widget
|
|
298
|
+
: undefined,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
229
301
|
// Verificamos as credenciais ANTES de finalizar a interaction, porque com MFA
|
|
230
302
|
// ligado precisamos exigir o 2º fator e NÃO podemos chamar interactionFinished
|
|
231
303
|
// ainda. A sequência verificação + lockout + auditoria de falha é centralizada
|
|
@@ -260,7 +332,7 @@ export default class AuthInteractionController {
|
|
|
260
332
|
? { fullName: found.name ?? null, globalRoles: found.globalRoles ?? [] }
|
|
261
333
|
: null;
|
|
262
334
|
return render(ctx, 'login', {
|
|
263
|
-
...
|
|
335
|
+
...userMethodsForLogin,
|
|
264
336
|
uid: ctx.request.param('uid'),
|
|
265
337
|
csrfToken: ctx.request.csrfToken,
|
|
266
338
|
step: 'password',
|
|
@@ -633,6 +705,24 @@ export default class AuthInteractionController {
|
|
|
633
705
|
// o e-mail e a tela mostrem SÓ aquele método. Ausente/ inválido = both (histórico).
|
|
634
706
|
// NÃO condiciona a emissão de token — os dois continuam saindo co-locados.
|
|
635
707
|
const channel = normalizeLoginChannel(ctx.request.input('channel'));
|
|
708
|
+
// Preferência por usuário: magic link desligado para ESTA conta → não emite
|
|
709
|
+
// token (resposta uniforme "enviado", sem vazar a preferência).
|
|
710
|
+
const userScopedMethods = email && cfg.passwordless.magicLink && supportsMagicLink(cfg.accountStore)
|
|
711
|
+
? await this.#userScopedLoginMethods(ctx, cfg, email)
|
|
712
|
+
: null;
|
|
713
|
+
if (userScopedMethods && !userScopedMethods.authMethods.magicLink) {
|
|
714
|
+
return render(ctx, 'login', {
|
|
715
|
+
...userScopedMethods,
|
|
716
|
+
uid,
|
|
717
|
+
csrfToken: ctx.request.csrfToken,
|
|
718
|
+
step: 'password',
|
|
719
|
+
email,
|
|
720
|
+
account: null,
|
|
721
|
+
brand,
|
|
722
|
+
magicLinkSent: true,
|
|
723
|
+
magicChannel: 'none',
|
|
724
|
+
});
|
|
725
|
+
}
|
|
636
726
|
if (cfg.passwordless.magicLink && supportsMagicLink(cfg.accountStore) && email) {
|
|
637
727
|
const ip = ctx.request.ip?.() ?? null;
|
|
638
728
|
const clientId = details.params.client_id ?? null;
|
|
@@ -676,7 +766,9 @@ export default class AuthInteractionController {
|
|
|
676
766
|
// `otpEnabled` vem do spread de `#loginMethods` (mesmo valor do `otpEnabled`
|
|
677
767
|
// local usado acima para decidir a emissão) — não re-declarado aqui.
|
|
678
768
|
return render(ctx, 'login', {
|
|
679
|
-
...(
|
|
769
|
+
...(email
|
|
770
|
+
? await this.#userScopedLoginMethods(ctx, cfg, email)
|
|
771
|
+
: await this.#loginMethods(ctx, cfg)),
|
|
680
772
|
uid,
|
|
681
773
|
csrfToken: ctx.request.csrfToken,
|
|
682
774
|
step: 'password',
|
|
@@ -715,6 +807,31 @@ export default class AuthInteractionController {
|
|
|
715
807
|
});
|
|
716
808
|
return ctx.response.redirect(`/auth/interaction/${uid}`);
|
|
717
809
|
}
|
|
810
|
+
// Preferência por usuário: magic link desligado para esta conta (p. ex. o
|
|
811
|
+
// token foi emitido antes de o usuário desligar o método) → não materializa
|
|
812
|
+
// a sessão; volta ao login com erro genérico (não vaza a preferência).
|
|
813
|
+
const magicScoped = await this.#userScopedLoginMethods(ctx, cfg, acc.email);
|
|
814
|
+
if (!magicScoped.authMethods.magicLink) {
|
|
815
|
+
await cfg.audit?.record({
|
|
816
|
+
type: 'login.failure',
|
|
817
|
+
accountId: acc.id,
|
|
818
|
+
email: acc.email,
|
|
819
|
+
ip,
|
|
820
|
+
clientId,
|
|
821
|
+
metadata: { stage: 'magic_link', reason: 'method_disabled_by_user' },
|
|
822
|
+
});
|
|
823
|
+
const render = cfg.render;
|
|
824
|
+
return render(ctx, 'login', {
|
|
825
|
+
...magicScoped,
|
|
826
|
+
uid,
|
|
827
|
+
csrfToken: ctx.request.csrfToken,
|
|
828
|
+
step: 'password',
|
|
829
|
+
email: acc.email,
|
|
830
|
+
account: null,
|
|
831
|
+
brand: brandFor(cfg.branding, clientId ?? undefined, undefined),
|
|
832
|
+
error: translate(cfg.messages, 'errors.invalid_credentials'),
|
|
833
|
+
});
|
|
834
|
+
}
|
|
718
835
|
// E-mail não verificado (LGPD/compliance): mesmo com o link válido, não
|
|
719
836
|
// materializa a sessão se a política exige verificação. Volta ao login com erro.
|
|
720
837
|
const magicLinkRuntimeSettings = await resolveRuntimeSettingsOrNoop(ctx);
|
|
@@ -729,7 +846,7 @@ export default class AuthInteractionController {
|
|
|
729
846
|
});
|
|
730
847
|
const render = cfg.render;
|
|
731
848
|
return render(ctx, 'login', {
|
|
732
|
-
...(await this.#
|
|
849
|
+
...(await this.#userScopedLoginMethods(ctx, cfg, acc.email)),
|
|
733
850
|
uid,
|
|
734
851
|
csrfToken: ctx.request.csrfToken,
|
|
735
852
|
step: 'password',
|
|
@@ -762,7 +879,7 @@ export default class AuthInteractionController {
|
|
|
762
879
|
if (!magicLinkStatusGate.allowed) {
|
|
763
880
|
const render = cfg.render;
|
|
764
881
|
return render(ctx, 'login', {
|
|
765
|
-
...(await this.#
|
|
882
|
+
...(await this.#userScopedLoginMethods(ctx, cfg, acc.email)),
|
|
766
883
|
uid,
|
|
767
884
|
csrfToken: ctx.request.csrfToken,
|
|
768
885
|
step: 'password',
|
|
@@ -814,10 +931,10 @@ export default class AuthInteractionController {
|
|
|
814
931
|
maxAttempts: cfg.login.otp.maxAttempts,
|
|
815
932
|
});
|
|
816
933
|
// Re-render da tela "link enviado" com o campo de código + erro localizado.
|
|
817
|
-
// `otpEnabled` vem do spread
|
|
818
|
-
//
|
|
934
|
+
// `otpEnabled` vem do spread (scoped — OTP é extensão do magic link, e a
|
|
935
|
+
// preferência do usuário que desligou magic link já foi barrada acima).
|
|
819
936
|
const renderOtpError = async (messageKey) => render(ctx, 'login', {
|
|
820
|
-
...(await this.#
|
|
937
|
+
...(await this.#userScopedLoginMethods(ctx, cfg, email)),
|
|
821
938
|
uid,
|
|
822
939
|
csrfToken: ctx.request.csrfToken,
|
|
823
940
|
step: 'password',
|
|
@@ -842,7 +959,7 @@ export default class AuthInteractionController {
|
|
|
842
959
|
metadata: { stage: 'otp', reason: 'unverified' },
|
|
843
960
|
});
|
|
844
961
|
return render(ctx, 'login', {
|
|
845
|
-
...(await this.#
|
|
962
|
+
...(await this.#userScopedLoginMethods(ctx, cfg, result.account.email)),
|
|
846
963
|
uid,
|
|
847
964
|
csrfToken: ctx.request.csrfToken,
|
|
848
965
|
step: 'password',
|
|
@@ -868,7 +985,7 @@ export default class AuthInteractionController {
|
|
|
868
985
|
});
|
|
869
986
|
if (!otpStatusGate.allowed) {
|
|
870
987
|
return render(ctx, 'login', {
|
|
871
|
-
...(await this.#
|
|
988
|
+
...(await this.#userScopedLoginMethods(ctx, cfg, result.account.email)),
|
|
872
989
|
uid,
|
|
873
990
|
csrfToken: ctx.request.csrfToken,
|
|
874
991
|
step: 'password',
|
|
@@ -1033,6 +1150,36 @@ export default class AuthInteractionController {
|
|
|
1033
1150
|
false)
|
|
1034
1151
|
: false;
|
|
1035
1152
|
ctx.session.forget(PASSKEY_AUTH_CHALLENGE_KEY);
|
|
1153
|
+
// Preferência por usuário: passkey desligada para esta conta → recusa a
|
|
1154
|
+
// assertion. Checamos DEPOIS de `ok` para não revelar a preferência no
|
|
1155
|
+
// timing de uma assertion inválida. Aqui vale SÓ a preferência explícita do
|
|
1156
|
+
// usuário (`pref.passkey === false`): os switches globais de passkey já são
|
|
1157
|
+
// aplicados na RENDERIZAÇÃO do botão/passkey-first (e o desafio MFA usa
|
|
1158
|
+
// passkey como 2º fator independente do passkey-first).
|
|
1159
|
+
if (ok && accountId && supportsLoginMethodsPreference(cfg.accountStore)) {
|
|
1160
|
+
const prefAccount = await cfg.accountStore.findById(accountId);
|
|
1161
|
+
const pref = prefAccount
|
|
1162
|
+
? normalizeUserLoginMethods(await cfg.accountStore.getLoginMethods(prefAccount.id))
|
|
1163
|
+
: null;
|
|
1164
|
+
if (pref?.passkey === false) {
|
|
1165
|
+
await cfg.audit?.record({
|
|
1166
|
+
type: 'login.failure',
|
|
1167
|
+
accountId,
|
|
1168
|
+
ip,
|
|
1169
|
+
clientId,
|
|
1170
|
+
metadata: { stage: 'mfa', method: 'webauthn', reason: 'method_disabled_by_user' },
|
|
1171
|
+
});
|
|
1172
|
+
return render(ctx, 'mfa-challenge', {
|
|
1173
|
+
uid: ctx.request.param('uid'),
|
|
1174
|
+
csrfToken: ctx.request.csrfToken,
|
|
1175
|
+
error: translate(cfg.messages, 'mfa_challenge.passkey_error'),
|
|
1176
|
+
brand,
|
|
1177
|
+
passkeyAvailable: false,
|
|
1178
|
+
trustedDevicesEnabled: cfg.trustedDevices.enabled,
|
|
1179
|
+
trustedDeviceDays: cfg.trustedDevices.days,
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1036
1183
|
if (!ok) {
|
|
1037
1184
|
await cfg.audit?.record({
|
|
1038
1185
|
type: 'login.failure',
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import '../augmentations.js';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import { supportsProviderIdentity } from '../../accounts/account_store.js';
|
|
3
|
+
import { supportsLoginMethodsPreference, supportsMagicLink, supportsPasskeys, supportsProviderIdentity, } from '../../accounts/account_store.js';
|
|
4
4
|
import { assertLoginAllowed } from '../login_attempt.js';
|
|
5
5
|
import { resolveRuntimeSettingsOrNoop } from '../runtime_settings.js';
|
|
6
|
+
import { resolveEffectiveAuthMethods } from '../runtime_toggles.js';
|
|
7
|
+
import { resolveEffectiveUserLoginMethods, } from '../user_login_methods.js';
|
|
6
8
|
const UID_SESSION_KEY = 'authkit_social_uid';
|
|
7
9
|
/**
|
|
8
10
|
* `AllyService.use()` é tipado contra a interface `SocialProviders`, que só é
|
|
@@ -13,6 +15,19 @@ const UID_SESSION_KEY = 'authkit_social_uid';
|
|
|
13
15
|
function useProvider(ctx, provider) {
|
|
14
16
|
return ctx.ally.use(provider);
|
|
15
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Métodos globais efetivos para o gate por usuário do callback social.
|
|
20
|
+
* Espelha o `#loginMethods` do interaction controller (mesma fonte de
|
|
21
|
+
* capabilities), sem acoplar os dois controllers.
|
|
22
|
+
*/
|
|
23
|
+
async function resolveGlobalAuthMethodsForUser(cfg, settings) {
|
|
24
|
+
return await resolveEffectiveAuthMethods(settings, {
|
|
25
|
+
configuredSocialProviders: cfg.social?.providers ?? [],
|
|
26
|
+
magicLinkCapable: cfg.passwordless?.magicLink && supportsMagicLink(cfg.accountStore),
|
|
27
|
+
passkeyCapable: supportsPasskeys(cfg.accountStore),
|
|
28
|
+
configOverrides: cfg.authMethods,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
16
31
|
export default class AuthSocialController {
|
|
17
32
|
/** GET /auth/:provider/redirect/:uid — guarda o uid e redireciona para o provider OAuth. */
|
|
18
33
|
async redirect(ctx) {
|
|
@@ -118,6 +133,19 @@ export default class AuthSocialController {
|
|
|
118
133
|
ctx.session.forget(UID_SESSION_KEY);
|
|
119
134
|
return ctx.response.redirect(backToLogin);
|
|
120
135
|
}
|
|
136
|
+
// Preferência por usuário: método social desligado para esta conta → recusa.
|
|
137
|
+
// Aplica-se a contas EXISTENTES (identidade ligada ou e-mail conhecido); uma
|
|
138
|
+
// conta recém-criada pelo callback não tem preferência gravada, então herda
|
|
139
|
+
// os globais — o gate é no-op para ela por construção.
|
|
140
|
+
const existingUser = user;
|
|
141
|
+
if (supportsLoginMethodsPreference(store)) {
|
|
142
|
+
const pref = await store.getLoginMethods(existingUser.id);
|
|
143
|
+
const scoped = resolveEffectiveUserLoginMethods(await resolveGlobalAuthMethodsForUser(cfg, settings), pref);
|
|
144
|
+
if (scoped.social.length === 0) {
|
|
145
|
+
ctx.session.forget(UID_SESSION_KEY);
|
|
146
|
+
return ctx.response.redirect(backToLogin);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
121
149
|
ctx.session.forget(UID_SESSION_KEY);
|
|
122
150
|
// Conclui a interaction OIDC para este usuário (escreve o 303 de volta ao authorize).
|
|
123
151
|
// O cookie de interaction (path '/') sobrevive ao round-trip do provider OAuth.
|
|
@@ -607,6 +607,9 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
607
607
|
router.delete(`${apiBase}/apps/:clientId`, [C.accountApi, 'revokeApp']);
|
|
608
608
|
// MFA + passkeys.
|
|
609
609
|
router.get(`${apiBase}/mfa`, [C.accountApi, 'mfaStatus']);
|
|
610
|
+
// Login methods preference (self-service, por usuário).
|
|
611
|
+
router.get(`${apiBase}/login-methods`, [C.accountApi, 'getLoginMethods']);
|
|
612
|
+
router.put(`${apiBase}/login-methods`, [C.accountApi, 'updateLoginMethods']);
|
|
610
613
|
router.get(`${apiBase}/passkeys`, [C.accountApi, 'listPasskeys']);
|
|
611
614
|
router.delete(`${apiBase}/passkeys/:id`, [C.accountApi, 'removePasskey']);
|
|
612
615
|
// PATs.
|