@adonis-agora/authkit-server 0.59.0 → 0.61.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/providers/authkit_server_provider.js +7 -0
- 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 +4 -0
- 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/define_config.d.ts +23 -0
- package/build/src/define_config.js +6 -0
- 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/auth_host_config.d.ts +9 -0
- package/build/src/host/controllers/headless_login_methods_controller.d.ts +31 -0
- package/build/src/host/controllers/headless_login_methods_controller.js +79 -0
- package/build/src/host/controllers/interaction_controller.js +159 -12
- package/build/src/host/controllers/social_controller.js +29 -1
- package/build/src/host/login_methods_state.d.ts +39 -0
- package/build/src/host/login_methods_state.js +52 -0
- package/build/src/host/register_auth_host.js +18 -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 +7 -4
- package/skills/authkit-idp-setup/SKILL.md +222 -0
- package/skills/authkit-interactions/SKILL.md +236 -0
|
@@ -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.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ResolvedServerConfig } from '../define_config.js';
|
|
2
|
+
import type { SettingsCapability } from './runtime_settings.js';
|
|
3
|
+
import { type UserLoginMethods } from './user_login_methods.js';
|
|
4
|
+
/** Resultado compartilhado de "estado de tipos de login" entregue à UI. */
|
|
5
|
+
export interface LoginMethodsState {
|
|
6
|
+
supported: boolean;
|
|
7
|
+
/** Preferência crua do usuário; {} = sem preferência (herda globais). */
|
|
8
|
+
methods: UserLoginMethods | Record<string, never>;
|
|
9
|
+
/** Estado final por método (global ∩ preferência) — o que a tela de login mostra. */
|
|
10
|
+
available: {
|
|
11
|
+
password: boolean;
|
|
12
|
+
magicLink: boolean;
|
|
13
|
+
passkey: boolean;
|
|
14
|
+
social: string[];
|
|
15
|
+
forgotPassword: boolean;
|
|
16
|
+
} | null;
|
|
17
|
+
/** Métodos fora do controle do usuário (globalmente off ou pin de config). */
|
|
18
|
+
locked: {
|
|
19
|
+
password: boolean;
|
|
20
|
+
magicLink: boolean;
|
|
21
|
+
passkey: boolean;
|
|
22
|
+
social: boolean;
|
|
23
|
+
} | null;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Métodos globais efetivos + pins de config — resolução compartilhada entre o
|
|
27
|
+
* console (account API) e a API headless. Fail-safe: erro → defaults.
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveGlobalAuthMethods(settings: SettingsCapability, cfg: ResolvedServerConfig): Promise<{
|
|
30
|
+
resolved: import("./runtime_toggles.js").ResolvedAuthMethods;
|
|
31
|
+
locked: (keyof import("./runtime_toggles.js").AuthMethodsConfigOverride)[];
|
|
32
|
+
}>;
|
|
33
|
+
/**
|
|
34
|
+
* Monta o estado final de tipos de login para uma conta, intersectando a
|
|
35
|
+
* preferência do usuário com os métodos globais efetivos. Reusado pelo console
|
|
36
|
+
* de conta (`/account/api/login-methods`) e pela API headless
|
|
37
|
+
* (`{baseUrl}/login-methods`) — sem duplicar regra.
|
|
38
|
+
*/
|
|
39
|
+
export declare function loginMethodsStateForAccount(accountId: string, store: ResolvedServerConfig['accountStore'], settings: SettingsCapability, cfg: ResolvedServerConfig): Promise<LoginMethodsState>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { supportsMagicLink } from '../accounts/account_store.js';
|
|
2
|
+
import { supportsPasskeys } from '../accounts/account_store.js';
|
|
3
|
+
import { supportsLoginMethodsPreference } from '../accounts/account_store.js';
|
|
4
|
+
import { resolveEffectiveAuthMethods } from './runtime_toggles.js';
|
|
5
|
+
import { configLockedAuthMethods } from './runtime_toggles.js';
|
|
6
|
+
import { normalizeUserLoginMethods, resolveEffectiveUserLoginMethods, } from './user_login_methods.js';
|
|
7
|
+
/**
|
|
8
|
+
* Métodos globais efetivos + pins de config — resolução compartilhada entre o
|
|
9
|
+
* console (account API) e a API headless. Fail-safe: erro → defaults.
|
|
10
|
+
*/
|
|
11
|
+
export async function resolveGlobalAuthMethods(settings, cfg) {
|
|
12
|
+
const resolved = await resolveEffectiveAuthMethods(settings, {
|
|
13
|
+
configuredSocialProviders: cfg.social?.providers ?? [],
|
|
14
|
+
magicLinkCapable: cfg.passwordless?.magicLink && supportsMagicLink(cfg.accountStore),
|
|
15
|
+
passkeyCapable: supportsPasskeys(cfg.accountStore),
|
|
16
|
+
configOverrides: cfg.authMethods,
|
|
17
|
+
});
|
|
18
|
+
return { resolved, locked: configLockedAuthMethods(cfg.authMethods) };
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Monta o estado final de tipos de login para uma conta, intersectando a
|
|
22
|
+
* preferência do usuário com os métodos globais efetivos. Reusado pelo console
|
|
23
|
+
* de conta (`/account/api/login-methods`) e pela API headless
|
|
24
|
+
* (`{baseUrl}/login-methods`) — sem duplicar regra.
|
|
25
|
+
*/
|
|
26
|
+
export async function loginMethodsStateForAccount(accountId, store, settings, cfg) {
|
|
27
|
+
if (!supportsLoginMethodsPreference(store)) {
|
|
28
|
+
return { supported: false, methods: {}, available: null, locked: null };
|
|
29
|
+
}
|
|
30
|
+
const pref = normalizeUserLoginMethods(await store.getLoginMethods(accountId));
|
|
31
|
+
const { resolved: global, locked: cfgLocked } = await resolveGlobalAuthMethods(settings, cfg);
|
|
32
|
+
const effective = resolveEffectiveUserLoginMethods(global, pref);
|
|
33
|
+
return {
|
|
34
|
+
supported: true,
|
|
35
|
+
// Preferência crua do usuário; {} = sem preferência (herda globais).
|
|
36
|
+
methods: pref ?? {},
|
|
37
|
+
available: {
|
|
38
|
+
password: effective.password,
|
|
39
|
+
magicLink: effective.magicLink,
|
|
40
|
+
passkey: effective.passkey,
|
|
41
|
+
social: effective.social,
|
|
42
|
+
forgotPassword: effective.forgotPassword,
|
|
43
|
+
},
|
|
44
|
+
// Métodos que o usuário NÃO pode controlar (globalmente off ou pin de config).
|
|
45
|
+
locked: {
|
|
46
|
+
password: !global.password || cfgLocked.includes('password'),
|
|
47
|
+
magicLink: !global.magicLink || cfgLocked.includes('magicLink'),
|
|
48
|
+
passkey: !global.passkey || cfgLocked.includes('passkey'),
|
|
49
|
+
social: global.social.length === 0,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -201,6 +201,8 @@ const C = {
|
|
|
201
201
|
apiKeys: () => import('./admin_api/api_keys_controller.js'),
|
|
202
202
|
// Account self-service JSON API (session-authed, under /account/api/*).
|
|
203
203
|
accountApi: () => import('./account_api/account_api_controller.js'),
|
|
204
|
+
// API headless (Clerk-style) — host-session-authed via `headless.resolveAccountId`.
|
|
205
|
+
headlessLoginMethods: () => import('./controllers/headless_login_methods_controller.js'),
|
|
204
206
|
};
|
|
205
207
|
/**
|
|
206
208
|
* Monta todas as rotas do host-kit do Authorization Server numa chamada.
|
|
@@ -607,6 +609,9 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
607
609
|
router.delete(`${apiBase}/apps/:clientId`, [C.accountApi, 'revokeApp']);
|
|
608
610
|
// MFA + passkeys.
|
|
609
611
|
router.get(`${apiBase}/mfa`, [C.accountApi, 'mfaStatus']);
|
|
612
|
+
// Login methods preference (self-service, por usuário).
|
|
613
|
+
router.get(`${apiBase}/login-methods`, [C.accountApi, 'getLoginMethods']);
|
|
614
|
+
router.put(`${apiBase}/login-methods`, [C.accountApi, 'updateLoginMethods']);
|
|
610
615
|
router.get(`${apiBase}/passkeys`, [C.accountApi, 'listPasskeys']);
|
|
611
616
|
router.delete(`${apiBase}/passkeys/:id`, [C.accountApi, 'removePasskey']);
|
|
612
617
|
// PATs.
|
|
@@ -619,6 +624,19 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
619
624
|
router.get(`${apiBase}/orgs/:id`, [C.accountApi, 'showOrg']);
|
|
620
625
|
})
|
|
621
626
|
.use([accountGuard]);
|
|
627
|
+
// API headless (Clerk-style) — montada fora do `accountGuard` (não depende da
|
|
628
|
+
// sessão de CONTA), autenticada pelo resolver do host (`headless.resolveAccountId`).
|
|
629
|
+
// `PUT` usa CSRF igual à API do console (middleware de shield do host).
|
|
630
|
+
const headlessCfg = hostCfg?.headless;
|
|
631
|
+
if (headlessCfg) {
|
|
632
|
+
const hb = headlessCfg.baseUrl;
|
|
633
|
+
router
|
|
634
|
+
.get(`${hb}/login-methods`, [C.headlessLoginMethods, 'index'])
|
|
635
|
+
.as('authkit.headless.login_methods.index');
|
|
636
|
+
router
|
|
637
|
+
.put(`${hb}/login-methods`, [C.headlessLoginMethods, 'update'])
|
|
638
|
+
.as('authkit.headless.login_methods.update');
|
|
639
|
+
}
|
|
622
640
|
// Prefixos resolvidos dos consoles — `null` quando o grupo não foi montado.
|
|
623
641
|
// Compõem o `AuthHostRouteMap` devolvido no fim.
|
|
624
642
|
let resolvedAdminPrefix = null;
|