@adonis-agora/authkit-server 0.82.3 → 0.84.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.
Files changed (34) hide show
  1. package/build/host/views/login.edge +25 -5
  2. package/build/host/views/partials/styles.edge +1 -1
  3. package/build/src/define_config.d.ts +44 -4
  4. package/build/src/define_config.js +1 -0
  5. package/build/src/host/account_roles.d.ts +14 -3
  6. package/build/src/host/account_roles.js +15 -4
  7. package/build/src/host/admin_api/api_users_controller.d.ts +3 -0
  8. package/build/src/host/admin_api/api_users_controller.js +9 -5
  9. package/build/src/host/admin_api/dto.d.ts +24 -0
  10. package/build/src/host/admin_api/dto.js +14 -0
  11. package/build/src/host/admin_api/token_verify_service.js +7 -2
  12. package/build/src/host/admin_console/admin_shell_controller.js +3 -1
  13. package/build/src/host/admin_console/console_users_controller.d.ts +4 -0
  14. package/build/src/host/admin_console/console_users_controller.js +12 -6
  15. package/build/src/host/controllers/interaction_controller.d.ts +8 -0
  16. package/build/src/host/controllers/interaction_controller.js +59 -12
  17. package/build/src/host/controllers/pat_introspection_controller.d.ts +1 -1
  18. package/build/src/host/controllers/pat_introspection_controller.js +2 -1
  19. package/build/src/host/controllers/registration_controller.d.ts +10 -0
  20. package/build/src/host/controllers/registration_controller.js +97 -30
  21. package/build/src/host/controllers/social_controller.js +64 -1
  22. package/build/src/host/i18n.d.ts +2 -0
  23. package/build/src/host/i18n.js +3 -0
  24. package/build/src/host/last_login_method.d.ts +67 -0
  25. package/build/src/host/last_login_method.js +119 -0
  26. package/build/src/host/register_auth_host.js +4 -0
  27. package/build/src/host/signup_token.d.ts +9 -0
  28. package/build/src/host/signup_token.js +31 -0
  29. package/build/src/host/social_pending.d.ts +17 -0
  30. package/build/src/host/social_pending.js +11 -0
  31. package/build/src/host/ui-dist/assets/{index-C1eSfG8B.js → index-DhQzXw2i.js} +1 -1
  32. package/build/src/host/ui-dist/index.html +1 -1
  33. package/build/src/provider/interaction_actions.js +7 -0
  34. package/package.json +2 -2
@@ -0,0 +1,119 @@
1
+ /**
2
+ * "Último método usado" no login (o selo "Last used" do Cloudflare, Vercel, Clerk…).
3
+ *
4
+ * O método fica num cookie DESTE DISPOSITIVO, não na conta. É de propósito:
5
+ * - o passo 1 (identifier) ainda não sabe quem é o usuário, e é exatamente ali
6
+ * que o selo mais ajuda ("da última vez você entrou com Google");
7
+ * - guardar por conta e mostrar no passo 2 diria a quem digita um e-mail qualquer
8
+ * como aquela conta entra — vazamento de enumeração. O cookie só conta ao
9
+ * próprio browser o que ele mesmo fez.
10
+ *
11
+ * O cookie não carrega identidade nenhuma (nem e-mail, nem accountId), só o método.
12
+ * É puramente de SUPERFÍCIE: a tela destaca um botão, mas nada no fluxo muda — os
13
+ * métodos oferecidos continuam vindo de `authMethods`, e um valor adulterado no
14
+ * cookie só destaca outro botão (ou nenhum, se não passar na validação).
15
+ *
16
+ * Gravado quando o fator PRIMÁRIO é aceito. Numa conta com 2º fator, isso acontece
17
+ * antes do desafio de MFA: o selo responde "como você se identificou", que é o
18
+ * botão que a pessoa procura na próxima vez.
19
+ */
20
+ /** Nome do cookie do último método de login. */
21
+ export const LAST_LOGIN_METHOD_COOKIE = 'authkit_last_login';
22
+ /** Validade do cookie: 1 ano (renovado a cada login). */
23
+ const LAST_LOGIN_METHOD_MAX_AGE = 365 * 24 * 60 * 60;
24
+ const FIXED_METHODS = new Set(['password', 'magic_link', 'otp', 'passkey']);
25
+ /** Ids de provedor/método: mesmo formato curto que vai em rota/config. */
26
+ const QUALIFIED_METHOD = /^(social|custom):[a-z0-9][a-z0-9_-]{0,63}$/i;
27
+ /**
28
+ * Valida um valor lido do cookie. Só os métodos conhecidos passam; qualquer outra
29
+ * coisa (ausente, lixo, valor adulterado) vira `undefined` — sem selo.
30
+ */
31
+ export function normalizeLastLoginMethod(raw) {
32
+ if (typeof raw !== 'string')
33
+ return undefined;
34
+ if (FIXED_METHODS.has(raw) || QUALIFIED_METHOD.test(raw))
35
+ return raw;
36
+ return undefined;
37
+ }
38
+ /** `false` desliga a feature (`login.lastUsedMethod: false`); ausente = ligada. */
39
+ function isEnabled(cfg) {
40
+ return cfg.login?.lastUsedMethod !== false;
41
+ }
42
+ /**
43
+ * Lê o último método deste dispositivo para a tela de login. Fail-safe: feature
44
+ * desligada, ctx sem cookies ou qualquer erro → `undefined` (sem selo).
45
+ */
46
+ export function readLastLoginMethod(ctx, cfg) {
47
+ if (!isEnabled(cfg))
48
+ return undefined;
49
+ try {
50
+ return normalizeLastLoginMethod(ctx.request.plainCookie?.(LAST_LOGIN_METHOD_COOKIE));
51
+ }
52
+ catch {
53
+ return undefined;
54
+ }
55
+ }
56
+ /**
57
+ * Registra o método primário que acabou de ser aceito. Grava via resposta do
58
+ * Adonis; quando o login termina no `interactionFinished` (que encerra a resposta
59
+ * crua), o `completeLogin` repassa o cookie pendente — ver
60
+ * {@link relayPendingCookies}. Best-effort: nunca derruba o login.
61
+ */
62
+ export function rememberLastLoginMethod(ctx, cfg, method) {
63
+ if (!isEnabled(cfg))
64
+ return;
65
+ if (!normalizeLastLoginMethod(method))
66
+ return;
67
+ try {
68
+ ctx.response.plainCookie?.(LAST_LOGIN_METHOD_COOKIE, method, {
69
+ httpOnly: true,
70
+ sameSite: 'lax',
71
+ path: '/',
72
+ maxAge: LAST_LOGIN_METHOD_MAX_AGE,
73
+ secure: ctx.request.secure?.() ?? false,
74
+ });
75
+ }
76
+ catch {
77
+ // Selo é cosmético: falha ao gravar não interfere no login.
78
+ }
79
+ }
80
+ /**
81
+ * Repassa os `Set-Cookie` que o handler gravou na resposta do Adonis para a
82
+ * resposta crua do Node.
83
+ *
84
+ * O Adonis só escreve os headers no `finish()`, depois do handler. Mas o
85
+ * `interactionFinished` do oidc-provider escreve o 303 e dá `res.end()` direto na
86
+ * resposta crua — quando o Adonis chega no `finish()`, a resposta já saiu e todo
87
+ * cookie gravado via `ctx.response.cookie()` no mesmo request é descartado em
88
+ * silêncio. Chamado pelo `completeLogin` logo antes do `interactionFinished`.
89
+ *
90
+ * Os cookies saem do Adonis ao serem repassados, então não há como irem em dobro.
91
+ * O `Set-Cookie` que o próprio oidc-provider grava depois é ACRESCENTADO aos
92
+ * existentes (a lib `cookies` do Koa preserva o header), então nada se perde.
93
+ */
94
+ export function relayPendingCookies(ctx) {
95
+ try {
96
+ const response = ctx.response;
97
+ const raw = response?.response;
98
+ if (!raw || typeof raw.setHeader !== 'function' || raw.headersSent)
99
+ return;
100
+ const pending = response.getHeader?.('set-cookie');
101
+ if (pending === undefined || pending === null)
102
+ return;
103
+ const toList = (value) => value === undefined || value === null
104
+ ? []
105
+ : Array.isArray(value)
106
+ ? value.map(String)
107
+ : [String(value)];
108
+ // O `getHeader` do Adonis também enxerga o que já está na resposta crua, então
109
+ // a lista pendente pode repetir o que lá está — dedup preservando a ordem.
110
+ const cookies = [...new Set([...toList(raw.getHeader?.('set-cookie')), ...toList(pending)])];
111
+ // O `removeHeader` do Adonis apaga também da resposta crua — por isso vem
112
+ // ANTES do `setHeader`, que regrava a lista completa.
113
+ response.removeHeader?.('set-cookie');
114
+ raw.setHeader('set-cookie', cookies);
115
+ }
116
+ catch {
117
+ // Repasse é best-effort: nunca derruba o login.
118
+ }
119
+ }
@@ -488,6 +488,8 @@ export function registerAuthHost(router, opts = {}) {
488
488
  router.get('/auth/otp-unlock/:token', [C.interaction, 'otpUnlock']);
489
489
  router.get('/auth/interaction/:uid/signup', [C.registration, 'showSignup']);
490
490
  withLogin(router.post('/auth/interaction/:uid/signup', [C.registration, 'signup']));
491
+ // Confirmação do cadastro passwordless: a conta nasce aqui, não no POST.
492
+ router.get('/auth/interaction/:uid/signup/confirm', [C.registration, 'signupConfirm']);
491
493
  // Recuperação de senha (standalone).
492
494
  router.get('/auth/forgot-password', [C.registration, 'showForgot']);
493
495
  withLogin(router.post('/auth/forgot-password', [C.registration, 'forgot']));
@@ -499,6 +501,8 @@ export function registerAuthHost(router, opts = {}) {
499
501
  if (social) {
500
502
  router.get('/auth/:provider/redirect/:uid', [C.social, 'redirect']);
501
503
  router.get('/auth/:provider/callback', [C.social, 'callback']);
504
+ // 2º fator da conta depois do provider (ver AuthSocialConfig.secondFactor).
505
+ router.get('/auth/interaction/:uid/social/continue', [C.interaction, 'socialContinue']);
502
506
  }
503
507
  // PAT introspection (server-to-server).
504
508
  withIntrospection(router.post('/authkit/pat/introspect', [C.patIntrospection, 'handle']));
@@ -0,0 +1,9 @@
1
+ /** Mesma janela do magic link de login. */
2
+ export declare const SIGNUP_TOKEN_TTL = "15 minutes";
3
+ export interface SignupClaim {
4
+ email: string;
5
+ fullName: string | null;
6
+ }
7
+ export declare function issueSignupToken(claim: SignupClaim): Promise<string>;
8
+ /** Claim do token, ou `null` se adulterado, de outro propósito ou vencido. */
9
+ export declare function readSignupToken(token: string): Promise<SignupClaim | null>;
@@ -0,0 +1,31 @@
1
+ import { loadEncryptionService } from '../keys/keystore_crypto.js';
2
+ /**
3
+ * Token do link de confirmação do cadastro passwordless.
4
+ *
5
+ * O cadastro não cria a conta no POST: guarda e-mail + nome num token cifrado
6
+ * (APP_KEY), com prazo e propósito próprios, e a conta só nasce quando o dono do
7
+ * e-mail abre o link. Assim um envio que falha, ou um e-mail digitado por quem
8
+ * não é o dono, não deixa conta para trás nem dispara `account.created`.
9
+ *
10
+ * Sem estado no banco: a unicidade vem da própria conta — se ela já existe
11
+ * quando o link é aberto, o link é recusado.
12
+ */
13
+ const PURPOSE = 'authkit_passwordless_signup';
14
+ /** Mesma janela do magic link de login. */
15
+ export const SIGNUP_TOKEN_TTL = '15 minutes';
16
+ async function encryption() {
17
+ return (await loadEncryptionService());
18
+ }
19
+ export async function issueSignupToken(claim) {
20
+ return (await encryption()).encrypt(claim, SIGNUP_TOKEN_TTL, PURPOSE);
21
+ }
22
+ /** Claim do token, ou `null` se adulterado, de outro propósito ou vencido. */
23
+ export async function readSignupToken(token) {
24
+ const claim = (await encryption()).decrypt(token, PURPOSE);
25
+ if (!claim || typeof claim.email !== 'string' || claim.email === '')
26
+ return null;
27
+ return {
28
+ email: claim.email,
29
+ fullName: typeof claim.fullName === 'string' ? claim.fullName : null,
30
+ };
31
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Passo pendente entre o callback social e o 2º fator, compartilhado pelo
3
+ * `AuthSocialController` (grava) e pelo `AuthInteractionController` (consome).
4
+ */
5
+ /**
6
+ * Login social que passou pelo provider mas ainda deve o 2º fator da conta.
7
+ * Lido (uma vez) por `AuthInteractionController#socialContinue`.
8
+ */
9
+ export declare const SOCIAL_PENDING_KEY = "authkit_social_pending";
10
+ /** Validade do passo pendente: o desafio do 2º fator vem logo em seguida. */
11
+ export declare const SOCIAL_PENDING_TTL_MS: number;
12
+ export interface SocialPendingLogin {
13
+ accountId: string;
14
+ uid: string;
15
+ required: boolean;
16
+ at: number;
17
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Passo pendente entre o callback social e o 2º fator, compartilhado pelo
3
+ * `AuthSocialController` (grava) e pelo `AuthInteractionController` (consome).
4
+ */
5
+ /**
6
+ * Login social que passou pelo provider mas ainda deve o 2º fator da conta.
7
+ * Lido (uma vez) por `AuthInteractionController#socialContinue`.
8
+ */
9
+ export const SOCIAL_PENDING_KEY = 'authkit_social_pending';
10
+ /** Validade do passo pendente: o desafio do 2º fator vem logo em seguida. */
11
+ export const SOCIAL_PENDING_TTL_MS = 5 * 60 * 1000;