@adonis-agora/authkit-server 1.0.0-rc.0 → 1.0.0-rc.2
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/CHANGELOG.md +38 -0
- package/README.md +4 -5
- package/build/index.d.ts +7 -11
- package/build/index.js +5 -9
- package/build/providers/authkit_server_provider.js +14 -8
- package/build/src/agents/config.d.ts +9 -13
- package/build/src/agents/config.js +17 -11
- package/build/src/agents/delegation_service.js +3 -3
- package/build/src/agents/delegation_store.d.ts +3 -2
- package/build/src/agents/delegation_store.js +6 -5
- package/build/src/agents/extension.d.ts +261 -0
- package/build/src/agents/extension.js +40 -0
- package/build/src/agents/middleware.js +1 -1
- package/build/src/agents/protocol.d.ts +4 -4
- package/build/src/agents/registry_store.d.ts +3 -2
- package/build/src/agents/registry_store.js +1 -1
- package/build/src/agents/runtime.d.ts +26 -7
- package/build/src/agents/runtime.js +110 -75
- package/build/src/audit/audit_sink.js +1 -1
- package/build/src/console_authorize.d.ts +56 -0
- package/build/src/console_authorize.js +85 -0
- package/build/src/define_config.d.ts +20 -14
- package/build/src/define_config.js +11 -8
- package/build/src/doctor/checks.d.ts +7 -1
- package/build/src/doctor/checks.js +47 -1
- package/build/src/host/account_api/account_api_controller.js +8 -6
- package/build/src/host/account_session_sign_in.d.ts +12 -0
- package/build/src/host/account_session_sign_in.js +19 -0
- package/build/src/host/admin_clients_service.js +10 -1
- package/build/src/host/admin_console/console_personal_agents_controller.d.ts +1 -6
- package/build/src/host/admin_console/console_personal_agents_controller.js +50 -30
- package/build/src/host/admin_console_warning.d.ts +8 -0
- package/build/src/host/admin_console_warning.js +18 -0
- package/build/src/host/admin_sessions_service.js +8 -5
- package/build/src/host/auth_host_config.d.ts +11 -2
- package/build/src/host/console_denied_page.d.ts +6 -0
- package/build/src/host/console_denied_page.js +18 -0
- package/build/src/host/console_sso.d.ts +24 -0
- package/build/src/host/console_sso.js +70 -0
- package/build/src/host/controllers/account_apps_controller.js +15 -11
- package/build/src/host/controllers/account_session_controller.js +10 -1
- package/build/src/host/controllers/console_sso_controller.d.ts +12 -0
- package/build/src/host/controllers/console_sso_controller.js +61 -0
- package/build/src/host/controllers/interaction_controller.js +3 -1
- package/build/src/host/csrf.d.ts +3 -3
- package/build/src/host/csrf.js +3 -13
- package/build/src/host/default_mailer.d.ts +11 -6
- package/build/src/host/default_mailer.js +16 -12
- package/build/src/host/i18n.d.ts +3 -14
- package/build/src/host/i18n.js +15 -13
- package/build/src/host/register_auth_host.d.ts +2 -0
- package/build/src/host/register_auth_host.js +110 -64
- package/build/src/host/ui-dist/assets/index-BqyJYRVX.js +67 -0
- package/build/src/host/ui-dist/index.html +1 -1
- package/build/src/provider/build_provider.js +6 -0
- package/build/src/provider/oidc_service.d.ts +1 -1
- package/build/src/provider/oidc_service.js +1 -1
- package/build/src/schema/ensure.d.ts +14 -0
- package/build/src/schema/ensure.js +43 -150
- package/package.json +5 -5
- package/build/src/agents/poppy/client_registry.d.ts +0 -50
- package/build/src/agents/poppy/client_registry.js +0 -260
- package/build/src/agents/poppy/config.d.ts +0 -396
- package/build/src/agents/poppy/config.js +0 -228
- package/build/src/agents/poppy/dpop.d.ts +0 -61
- package/build/src/agents/poppy/dpop.js +0 -158
- package/build/src/agents/poppy/email_code_sign_in.d.ts +0 -27
- package/build/src/agents/poppy/email_code_sign_in.js +0 -140
- package/build/src/agents/poppy/errors.d.ts +0 -25
- package/build/src/agents/poppy/errors.js +0 -25
- package/build/src/agents/poppy/replay.d.ts +0 -25
- package/build/src/agents/poppy/replay.js +0 -106
- package/build/src/agents/poppy/resource.d.ts +0 -100
- package/build/src/agents/poppy/resource.js +0 -129
- package/build/src/agents/poppy/safe_fetch.d.ts +0 -48
- package/build/src/agents/poppy/safe_fetch.js +0 -262
- package/build/src/agents/poppy/service.d.ts +0 -293
- package/build/src/agents/poppy/service.js +0 -1458
- package/build/src/agents/poppy/session_jwt.d.ts +0 -30
- package/build/src/agents/poppy/session_jwt.js +0 -133
- package/build/src/agents/poppy/store.d.ts +0 -106
- package/build/src/agents/poppy/store.js +0 -261
- package/build/src/agents/poppy/web_session.d.ts +0 -39
- package/build/src/agents/poppy/web_session.js +0 -122
- package/build/src/host/controllers/poppy_consent_controller.d.ts +0 -19
- package/build/src/host/controllers/poppy_consent_controller.js +0 -199
- package/build/src/host/controllers/poppy_controller.d.ts +0 -33
- package/build/src/host/controllers/poppy_controller.js +0 -219
- package/build/src/host/poppy_session_key.d.ts +0 -9
- package/build/src/host/poppy_session_key.js +0 -9
- package/build/src/host/ui-dist/assets/index-D0EIeyKK.js +0 -67
|
@@ -5,6 +5,7 @@ import { AdminSessionsService } from '../admin_sessions_service.js';
|
|
|
5
5
|
import { guardBotProtection, resolveEffectiveBotProtection } from '../bot_protection.js';
|
|
6
6
|
import { brandFor, isFirstParty } from '../branding.js';
|
|
7
7
|
import { consentRequestParams } from '../consent_params.js';
|
|
8
|
+
import { CONSOLE_SSO_CLIENT_ID } from '../console_sso.js';
|
|
8
9
|
import { isCustomLoginMethodId } from '../custom_login.js';
|
|
9
10
|
import { completeCustomLoginSession } from '../custom_login_completion.js';
|
|
10
11
|
import { beginCustomMfa, clearMfaFlow, customMfaViewProps, getMfaFlow, initializeMfaFlow, recordVerifiedMfaFactor, resolveCustomMfaMethods, verifyCustomMfa, } from '../custom_mfa.js';
|
|
@@ -254,7 +255,8 @@ export default class AuthInteractionController {
|
|
|
254
255
|
}
|
|
255
256
|
}
|
|
256
257
|
if (details.prompt.name === 'consent' &&
|
|
257
|
-
|
|
258
|
+
(details.params.client_id === CONSOLE_SSO_CLIENT_ID ||
|
|
259
|
+
isFirstParty(cfg.branding, details.params.client_id))) {
|
|
258
260
|
// Clients first-party: auto-concede o consent (pula a tela de consent).
|
|
259
261
|
// interactions.consent monta o Grant + interactionFinished e escreve o
|
|
260
262
|
// redirect de volta para o client — opera via provider.interactionDetails,
|
package/build/src/host/csrf.d.ts
CHANGED
|
@@ -11,10 +11,10 @@ export interface AuthkitCsrfOptions {
|
|
|
11
11
|
*/
|
|
12
12
|
personalAgentsPrefix?: string | false;
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* Isenta os `csrfExemptPaths` das `personalAgents.extensions` do config. Default: true.
|
|
15
|
+
* `false` desliga.
|
|
16
16
|
*/
|
|
17
|
-
|
|
17
|
+
personalAgentExtensions?: boolean;
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
20
|
* Retorna `true` quando `url` é uma rota AuthKit que deve ser ISENTA de CSRF
|
package/build/src/host/csrf.js
CHANGED
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
* ```
|
|
17
17
|
*/
|
|
18
18
|
import { normalizePersonalAgentsPrefix } from '../agents/config.js';
|
|
19
|
-
import { normalizePoppyPrefix } from '../agents/poppy/config.js';
|
|
20
19
|
import { getAuthHostConfig } from './auth_host_config.js';
|
|
21
20
|
/**
|
|
22
21
|
* Retorna `true` quando `url` é uma rota AuthKit que deve ser ISENTA de CSRF
|
|
@@ -34,21 +33,12 @@ export function authkitCsrfExceptions(url, options = {}) {
|
|
|
34
33
|
? normalizePersonalAgentsPrefix(options.personalAgentsPrefix)
|
|
35
34
|
: getAuthHostConfig()?.personalAgents?.prefix;
|
|
36
35
|
const agentsOAuth = agentsPrefix ? `${agentsPrefix}/oauth/` : null;
|
|
37
|
-
const poppyPrefix = options.poppyPrefix === false
|
|
38
|
-
? undefined
|
|
39
|
-
: options.poppyPrefix !== undefined
|
|
40
|
-
? normalizePoppyPrefix(options.poppyPrefix)
|
|
41
|
-
: getAuthHostConfig()?.personalAgents?.poppyPrefix;
|
|
42
36
|
const path = url.split('?')[0];
|
|
43
|
-
const
|
|
44
|
-
(
|
|
45
|
-
path === `${poppyPrefix}/oauth/revoke` ||
|
|
46
|
-
path === `${poppyPrefix}/oauth/device` ||
|
|
47
|
-
path === `${poppyPrefix}/browser-session` ||
|
|
48
|
-
path === `${poppyPrefix}/browser-session/authorize`);
|
|
37
|
+
const extensionExempt = options.personalAgentExtensions !== false &&
|
|
38
|
+
(getAuthHostConfig()?.personalAgents?.extensions ?? []).some((e) => e.csrfExemptPaths?.includes(path));
|
|
49
39
|
return (url.includes(mountPath) ||
|
|
50
40
|
url.includes('/authkit/pat') ||
|
|
51
41
|
(backchannel !== null && url === backchannel) ||
|
|
52
42
|
(agentsOAuth !== null && url.startsWith(agentsOAuth)) ||
|
|
53
|
-
|
|
43
|
+
extensionExempt);
|
|
54
44
|
}
|
|
@@ -67,15 +67,20 @@ export declare function sendMagicLinkEmail(ctx: HttpContext, data: {
|
|
|
67
67
|
channel?: 'code' | 'link';
|
|
68
68
|
}): Promise<void>;
|
|
69
69
|
/**
|
|
70
|
-
* Envia
|
|
71
|
-
*
|
|
72
|
-
*
|
|
70
|
+
* Envia um e-mail só com um código (sem link: quem digita é a pessoa, em outro lugar) pelo mailer
|
|
71
|
+
* default do host, com o branding e o i18n do authkit. Os textos saem do catálogo pelas chaves
|
|
72
|
+
* `{messages}.subject`, `.heading`, `.intro`, `.code_label` e `.fallback`, com `params`
|
|
73
|
+
* interpolados — uma extension registra as dela em `messages`. Best-effort: sem `@adonisjs/mail`
|
|
74
|
+
* loga o código (dev); nunca lança.
|
|
75
|
+
*
|
|
76
|
+
* Estável no 1.x (é o mailer das `personalAgents.extensions`).
|
|
73
77
|
*/
|
|
74
|
-
export declare function
|
|
78
|
+
export declare function sendAuthkitCodeEmail(ctx: HttpContext, data: {
|
|
75
79
|
email: string;
|
|
76
80
|
code: string;
|
|
77
|
-
|
|
78
|
-
|
|
81
|
+
/** Prefixo das chaves no catálogo (`'mail.poppy_sign_in'`). */
|
|
82
|
+
messages: string;
|
|
83
|
+
params?: Record<string, string | number>;
|
|
79
84
|
}): Promise<void>;
|
|
80
85
|
/**
|
|
81
86
|
* Envia o link de CONFIRMAÇÃO DE IDENTIDADE (sudo) pelo mailer default do host.
|
|
@@ -295,32 +295,36 @@ export async function sendMagicLinkEmail(ctx, data) {
|
|
|
295
295
|
}
|
|
296
296
|
}
|
|
297
297
|
/**
|
|
298
|
-
* Envia
|
|
299
|
-
*
|
|
300
|
-
*
|
|
298
|
+
* Envia um e-mail só com um código (sem link: quem digita é a pessoa, em outro lugar) pelo mailer
|
|
299
|
+
* default do host, com o branding e o i18n do authkit. Os textos saem do catálogo pelas chaves
|
|
300
|
+
* `{messages}.subject`, `.heading`, `.intro`, `.code_label` e `.fallback`, com `params`
|
|
301
|
+
* interpolados — uma extension registra as dela em `messages`. Best-effort: sem `@adonisjs/mail`
|
|
302
|
+
* loga o código (dev); nunca lança.
|
|
303
|
+
*
|
|
304
|
+
* Estável no 1.x (é o mailer das `personalAgents.extensions`).
|
|
301
305
|
*/
|
|
302
|
-
export async function
|
|
306
|
+
export async function sendAuthkitCodeEmail(ctx, data) {
|
|
303
307
|
try {
|
|
304
308
|
const brand = resolveBrand(ctx);
|
|
305
309
|
const { messages: t, locale } = resolveMailMessages(ctx);
|
|
306
|
-
const
|
|
310
|
+
const key = (name) => `${data.messages}.${name}`;
|
|
307
311
|
const content = renderTransactionalEmail({
|
|
308
312
|
brand,
|
|
309
313
|
locale,
|
|
310
|
-
subject: translate(t, '
|
|
311
|
-
heading: translate(t, '
|
|
312
|
-
intro: translate(t, '
|
|
313
|
-
footnote: translate(t, '
|
|
314
|
+
subject: translate(t, key('subject'), data.params),
|
|
315
|
+
heading: translate(t, key('heading'), data.params),
|
|
316
|
+
intro: translate(t, key('intro'), data.params),
|
|
317
|
+
footnote: translate(t, key('fallback'), data.params),
|
|
314
318
|
code: data.code,
|
|
315
|
-
codeLabel: translate(t, '
|
|
319
|
+
codeLabel: translate(t, key('code_label'), data.params),
|
|
316
320
|
});
|
|
317
321
|
const sent = await sendEmail(ctx, data.email, content);
|
|
318
322
|
if (!sent) {
|
|
319
|
-
ctx.logger.info({ email: data.email,
|
|
323
|
+
ctx.logger.info({ email: data.email, code: data.code, messages: data.messages, ...(data.params ?? {}) }, 'authkit: código por e-mail (dev — @adonisjs/mail ausente)');
|
|
320
324
|
}
|
|
321
325
|
}
|
|
322
326
|
catch (error) {
|
|
323
|
-
ctx.logger.error({ err: error, email: data.email }, 'authkit: falha ao enviar o
|
|
327
|
+
ctx.logger.error({ err: error, email: data.email, messages: data.messages }, 'authkit: falha ao enviar o e-mail com código');
|
|
324
328
|
}
|
|
325
329
|
}
|
|
326
330
|
/**
|
package/build/src/host/i18n.d.ts
CHANGED
|
@@ -560,11 +560,6 @@ export declare const DEFAULT_MESSAGES: {
|
|
|
560
560
|
'mail.magic_link.code_subject': string;
|
|
561
561
|
'mail.magic_link.code_intro': string;
|
|
562
562
|
'mail.magic_link.code_only_label': string;
|
|
563
|
-
'mail.poppy_sign_in.subject': string;
|
|
564
|
-
'mail.poppy_sign_in.heading': string;
|
|
565
|
-
'mail.poppy_sign_in.intro': string;
|
|
566
|
-
'mail.poppy_sign_in.code_label': string;
|
|
567
|
-
'mail.poppy_sign_in.fallback': string;
|
|
568
563
|
'mail.sudo_link.subject': string;
|
|
569
564
|
'mail.sudo_link.heading': string;
|
|
570
565
|
'mail.sudo_link.intro': string;
|
|
@@ -1318,11 +1313,6 @@ export declare const PT_BR_MESSAGES: {
|
|
|
1318
1313
|
'mail.magic_link.code_subject': string;
|
|
1319
1314
|
'mail.magic_link.code_intro': string;
|
|
1320
1315
|
'mail.magic_link.code_only_label': string;
|
|
1321
|
-
'mail.poppy_sign_in.subject': string;
|
|
1322
|
-
'mail.poppy_sign_in.heading': string;
|
|
1323
|
-
'mail.poppy_sign_in.intro': string;
|
|
1324
|
-
'mail.poppy_sign_in.code_label': string;
|
|
1325
|
-
'mail.poppy_sign_in.fallback': string;
|
|
1326
1316
|
'mail.sudo_link.subject': string;
|
|
1327
1317
|
'mail.sudo_link.heading': string;
|
|
1328
1318
|
'mail.sudo_link.intro': string;
|
|
@@ -1547,11 +1537,10 @@ export declare const PT_BR_MESSAGES: {
|
|
|
1547
1537
|
*/
|
|
1548
1538
|
export declare const BUILTIN_MESSAGES: Record<string, AuthMessages>;
|
|
1549
1539
|
/**
|
|
1550
|
-
*
|
|
1551
|
-
*
|
|
1552
|
-
* overrides do host por cima. Sem config, retorna o default `en` intacto.
|
|
1553
|
-
* Chaves omitidas caem no default `en` (fallback de cobertura).
|
|
1540
|
+
* Soma os catálogos de uma extension (`messages`) aos embutidos. Chamado quando o config resolve;
|
|
1541
|
+
* os overrides do host (`i18n.messages`) continuam vencendo.
|
|
1554
1542
|
*/
|
|
1543
|
+
export declare function registerExtensionMessages(catalogs: Readonly<Record<string, Readonly<Record<string, string>>>>): void;
|
|
1555
1544
|
export declare function resolveMessages(i18n?: I18nConfig): AuthMessages;
|
|
1556
1545
|
/**
|
|
1557
1546
|
* Retorna a string para `key` (cai na própria `key` quando ausente) com
|
package/build/src/host/i18n.js
CHANGED
|
@@ -603,12 +603,6 @@ export const DEFAULT_MESSAGES = {
|
|
|
603
603
|
'mail.magic_link.code_subject': 'Your login code',
|
|
604
604
|
'mail.magic_link.code_intro': 'Use the code below to sign in. It expires shortly and can be used once.',
|
|
605
605
|
'mail.magic_link.code_only_label': 'Enter this code to sign in:',
|
|
606
|
-
// Código do Mediated Sign-In do Poppy: um personal agent pede para entrar na conta.
|
|
607
|
-
'mail.poppy_sign_in.subject': 'Your code to connect {agent}',
|
|
608
|
-
'mail.poppy_sign_in.heading': 'Connect {agent} to your account',
|
|
609
|
-
'mail.poppy_sign_in.intro': '{agent} asked to sign in to your account. If that was you, give it the code below. It expires in {minutes} minutes and works once.',
|
|
610
|
-
'mail.poppy_sign_in.code_label': 'Your code:',
|
|
611
|
-
'mail.poppy_sign_in.fallback': 'If you did not ask for this, ignore this email — nobody gets in without the code. Never share it with anyone else.',
|
|
612
606
|
// Link de CONFIRMAÇÃO DE IDENTIDADE (sudo). Copy DISTINTA da do magic link de
|
|
613
607
|
// login de propósito: este link não autentica ninguém — quem o abre já está
|
|
614
608
|
// logado e só reprova que é quem diz ser. Prometer "entre na sua conta" num
|
|
@@ -1447,12 +1441,6 @@ export const PT_BR_MESSAGES = {
|
|
|
1447
1441
|
'mail.magic_link.code_subject': 'Seu código de login',
|
|
1448
1442
|
'mail.magic_link.code_intro': 'Use o código abaixo para entrar. Ele expira em instantes e serve para um único acesso.',
|
|
1449
1443
|
'mail.magic_link.code_only_label': 'Digite este código para entrar:',
|
|
1450
|
-
// Código do Mediated Sign-In do Poppy — ver o comentário no catálogo `en`.
|
|
1451
|
-
'mail.poppy_sign_in.subject': 'Seu código para conectar {agent}',
|
|
1452
|
-
'mail.poppy_sign_in.heading': 'Conecte {agent} à sua conta',
|
|
1453
|
-
'mail.poppy_sign_in.intro': '{agent} pediu para entrar na sua conta. Se foi você, informe o código abaixo. Ele expira em {minutes} minutos e serve uma única vez.',
|
|
1454
|
-
'mail.poppy_sign_in.code_label': 'Seu código:',
|
|
1455
|
-
'mail.poppy_sign_in.fallback': 'Se não foi você, ignore este e-mail — ninguém entra sem o código. Nunca o compartilhe com outra pessoa.',
|
|
1456
1444
|
// Link de CONFIRMAÇÃO DE IDENTIDADE (sudo) — ver o comentário no catálogo `en`.
|
|
1457
1445
|
'mail.sudo_link.subject': 'Confirme que é você',
|
|
1458
1446
|
'mail.sudo_link.heading': 'Confirme sua identidade',
|
|
@@ -1713,13 +1701,27 @@ export const BUILTIN_MESSAGES = {
|
|
|
1713
1701
|
* overrides do host por cima. Sem config, retorna o default `en` intacto.
|
|
1714
1702
|
* Chaves omitidas caem no default `en` (fallback de cobertura).
|
|
1715
1703
|
*/
|
|
1704
|
+
/** Catálogos das `personalAgents.extensions`, por locale (ver {@link registerExtensionMessages}). */
|
|
1705
|
+
const extensionMessages = {};
|
|
1706
|
+
/**
|
|
1707
|
+
* Soma os catálogos de uma extension (`messages`) aos embutidos. Chamado quando o config resolve;
|
|
1708
|
+
* os overrides do host (`i18n.messages`) continuam vencendo.
|
|
1709
|
+
*/
|
|
1710
|
+
export function registerExtensionMessages(catalogs) {
|
|
1711
|
+
for (const [locale, messages] of Object.entries(catalogs)) {
|
|
1712
|
+
extensionMessages[locale] = { ...(extensionMessages[locale] ?? {}), ...messages };
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1716
1715
|
export function resolveMessages(i18n) {
|
|
1717
1716
|
const locale = i18n?.locale ?? DEFAULT_LOCALE;
|
|
1718
1717
|
// Base: sempre o default `en` para garantir cobertura total das chaves; o
|
|
1719
|
-
// catálogo embutido do locale (ex.: pt-BR) é mesclado por cima.
|
|
1718
|
+
// catálogo embutido do locale (ex.: pt-BR) é mesclado por cima. Os das extensions entram
|
|
1719
|
+
// em cada camada, abaixo dos overrides do host.
|
|
1720
1720
|
const base = {
|
|
1721
1721
|
...DEFAULT_MESSAGES,
|
|
1722
|
+
...(extensionMessages[DEFAULT_LOCALE] ?? {}),
|
|
1722
1723
|
...(BUILTIN_MESSAGES[locale] ?? {}),
|
|
1724
|
+
...(locale === DEFAULT_LOCALE ? {} : (extensionMessages[locale] ?? {})),
|
|
1723
1725
|
};
|
|
1724
1726
|
const overrides = i18n?.messages?.[locale];
|
|
1725
1727
|
if (!overrides)
|
|
@@ -20,6 +20,8 @@ export declare function wantsJson(ctx: any): boolean;
|
|
|
20
20
|
* (senão → `accountHome(cfg)`, default /account/security — NÃO revela a
|
|
21
21
|
* existência do /admin, e cai numa tela que o host controla via `accountHome`;
|
|
22
22
|
* se a tela default estiver desmontada, aponte `config.accountHome` para uma montada).
|
|
23
|
+
* 3. `config.admin.authorize` liberando (403/401/redirect conforme o resultado). Omitido:
|
|
24
|
+
* aberto fora de produção, 403 em produção.
|
|
23
25
|
* As roles permitidas são resolvidas em runtime do `authkit.server` (config lazy).
|
|
24
26
|
*/
|
|
25
27
|
export declare const adminGuard: (ctx: any, next: () => Promise<void>) => Promise<any>;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { getBootedApp } from '../../services/booted_app.js';
|
|
2
|
+
import { resolveConsoleAuthorize, } from '../console_authorize.js';
|
|
1
3
|
import { resolveRateLimit } from '../define_config.js';
|
|
2
4
|
import { accountHome } from './account_home.js';
|
|
3
5
|
import { getAccountLoginUrl, setAccountLoginUrl } from './account_login_url.js';
|
|
@@ -7,8 +9,9 @@ import { ACCOUNT_SESSION_KEY } from './account_session_key.js';
|
|
|
7
9
|
import { adminApiGuard } from './admin_api/admin_api_guard.js';
|
|
8
10
|
import { normalizeAdminApiPrefix, normalizeAdminPrefix, setAdminApiPrefix, setAdminPrefix, } from './admin_prefix.js';
|
|
9
11
|
import { getAuthHostConfig, markAuthHostAutoMounted, wasAuthHostAutoMounted, } from './auth_host_config.js';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
+
import { consoleDeniedPage } from './console_denied_page.js';
|
|
13
|
+
import { CONSOLE_SSO_CALLBACK_PATH, CONSOLE_SSO_START_PATH } from './console_sso.js';
|
|
14
|
+
import { endBridgedIdpSession, ensureConsoleSession } from './idp_session_bridge.js';
|
|
12
15
|
import { createAuthThrottles } from './rate_limit.js';
|
|
13
16
|
import { redirectExact } from './redirect_exact.js';
|
|
14
17
|
import { requestPathWithQuery } from './request_url.js';
|
|
@@ -67,7 +70,10 @@ async function checkAndRefreshIdle(ctx) {
|
|
|
67
70
|
const lastSeen = ctx.session?.get(ACCOUNT_LAST_SEEN_KEY);
|
|
68
71
|
const now = Date.now();
|
|
69
72
|
if (lastSeen !== undefined && now - lastSeen > idleMs) {
|
|
70
|
-
// Idle excedido: encerra a sessão.
|
|
73
|
+
// Idle excedido: encerra a sessão. Console aberto pela sessão do IdP (SSO):
|
|
74
|
+
// encerra também essa sessão, senão a ponte reabriria o console no próximo
|
|
75
|
+
// request e o idle timeout não teria efeito.
|
|
76
|
+
await endBridgedIdpSession(ctx);
|
|
71
77
|
ctx.session?.forget(ACCOUNT_SESSION_KEY);
|
|
72
78
|
ctx.session?.forget(ACCOUNT_LAST_SEEN_KEY);
|
|
73
79
|
return true;
|
|
@@ -114,10 +120,21 @@ function buildLoginRedirect(ctx, extra) {
|
|
|
114
120
|
* classe NÃO era aplicada em runtime num grupo, deixando /account/tokens e
|
|
115
121
|
* /account/mfa acessíveis sem sessão.
|
|
116
122
|
*/
|
|
117
|
-
/**
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
123
|
+
/**
|
|
124
|
+
* A sessão é do navegador de um personal agent (`isAgentBrowser` de uma extension)? Um hook que
|
|
125
|
+
* lança conta como "é": na dúvida, o console fica fechado.
|
|
126
|
+
*/
|
|
127
|
+
function isAgentBrowser(ctx) {
|
|
128
|
+
for (const extension of getAuthHostConfig()?.personalAgents?.extensions ?? []) {
|
|
129
|
+
try {
|
|
130
|
+
if (extension.isAgentBrowser?.(ctx))
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
121
138
|
}
|
|
122
139
|
/**
|
|
123
140
|
* `true` when the caller wants JSON (an `Accept` that prefers `application/json`,
|
|
@@ -142,9 +159,9 @@ function unauthenticated(ctx, redirectTo) {
|
|
|
142
159
|
return redirectExact(ctx.response, redirectTo);
|
|
143
160
|
}
|
|
144
161
|
const accountGuard = async (ctx, next) => {
|
|
145
|
-
// O navegador de um personal agent
|
|
146
|
-
//
|
|
147
|
-
if (
|
|
162
|
+
// O navegador de um personal agent não entra no console de conta nem nas telas de
|
|
163
|
+
// consentimento: a sessão é do agente, não do usuário.
|
|
164
|
+
if (isAgentBrowser(ctx))
|
|
148
165
|
return ctx.response.status(403).send('');
|
|
149
166
|
// Sessão do console — ou, com `accountSession.acceptIdpSession`, a do IdP (SSO).
|
|
150
167
|
if (!(await ensureConsoleSession(ctx))) {
|
|
@@ -166,6 +183,8 @@ const accountGuard = async (ctx, next) => {
|
|
|
166
183
|
* (senão → `accountHome(cfg)`, default /account/security — NÃO revela a
|
|
167
184
|
* existência do /admin, e cai numa tela que o host controla via `accountHome`;
|
|
168
185
|
* se a tela default estiver desmontada, aponte `config.accountHome` para uma montada).
|
|
186
|
+
* 3. `config.admin.authorize` liberando (403/401/redirect conforme o resultado). Omitido:
|
|
187
|
+
* aberto fora de produção, 403 em produção.
|
|
169
188
|
* As roles permitidas são resolvidas em runtime do `authkit.server` (config lazy).
|
|
170
189
|
*/
|
|
171
190
|
export const adminGuard = async (ctx, next) => {
|
|
@@ -177,7 +196,7 @@ export const adminGuard = async (ctx, next) => {
|
|
|
177
196
|
if (!cfg.admin.enabled) {
|
|
178
197
|
return ctx.response.notFound();
|
|
179
198
|
}
|
|
180
|
-
if (
|
|
199
|
+
if (isAgentBrowser(ctx))
|
|
181
200
|
return ctx.response.status(403).send('');
|
|
182
201
|
// Com `accountSession.acceptIdpSession`, a sessão do IdP também abre o console.
|
|
183
202
|
await ensureConsoleSession(ctx);
|
|
@@ -206,10 +225,63 @@ export const adminGuard = async (ctx, next) => {
|
|
|
206
225
|
.status(403)
|
|
207
226
|
.send({ error: { code: 'forbidden', message: 'Admin role required.' } });
|
|
208
227
|
}
|
|
209
|
-
return ctx.response
|
|
228
|
+
return redirectExact(ctx.response, accountHome(cfg));
|
|
210
229
|
}
|
|
230
|
+
// `admin.authorize`, after the roles: both must pass. Omitted, the console is open
|
|
231
|
+
// outside production and closed (403) in production.
|
|
232
|
+
const decision = cfg.admin.authorize
|
|
233
|
+
? await adminAuthorizeChecker(cfg.admin.authorize)(ctx)
|
|
234
|
+
: appInProduction()
|
|
235
|
+
? { allowed: false, status: 403 }
|
|
236
|
+
: { allowed: true };
|
|
237
|
+
if (!decision.allowed)
|
|
238
|
+
return consoleDenied(ctx, decision);
|
|
211
239
|
return next();
|
|
212
240
|
};
|
|
241
|
+
/**
|
|
242
|
+
* Whether the app runs in production. Unknown (the provider did not register the
|
|
243
|
+
* app) counts as production, so an omitted `admin.authorize` fails closed.
|
|
244
|
+
*/
|
|
245
|
+
function appInProduction() {
|
|
246
|
+
try {
|
|
247
|
+
return getBootedApp().inProduction !== false;
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/** The response for an `admin.authorize` denial: same rules in every `@adonis-agora` console. */
|
|
254
|
+
function consoleDenied(ctx, decision) {
|
|
255
|
+
const json = wantsJson(ctx);
|
|
256
|
+
if (decision.redirectTo) {
|
|
257
|
+
if (json) {
|
|
258
|
+
return ctx.response
|
|
259
|
+
.status(401)
|
|
260
|
+
.send({ error: 'unauthenticated', redirectTo: decision.redirectTo });
|
|
261
|
+
}
|
|
262
|
+
return redirectExact(ctx.response, decision.redirectTo);
|
|
263
|
+
}
|
|
264
|
+
if (decision.status === 401) {
|
|
265
|
+
if (json)
|
|
266
|
+
return ctx.response.status(401).send({ error: 'unauthenticated' });
|
|
267
|
+
ctx.response.header('Content-Type', 'text/html; charset=utf-8');
|
|
268
|
+
return ctx.response.status(401).send(consoleDeniedPage(401));
|
|
269
|
+
}
|
|
270
|
+
if (json)
|
|
271
|
+
return ctx.response.status(403).send({ error: 'forbidden' });
|
|
272
|
+
ctx.response.header('Content-Type', 'text/html; charset=utf-8');
|
|
273
|
+
return ctx.response.status(403).send(consoleDeniedPage(403));
|
|
274
|
+
}
|
|
275
|
+
/** One checker per configured `admin.authorize`, so a lazy import runs once. */
|
|
276
|
+
const adminAuthorizeCheckers = new WeakMap();
|
|
277
|
+
function adminAuthorizeChecker(authorize) {
|
|
278
|
+
let checker = adminAuthorizeCheckers.get(authorize);
|
|
279
|
+
if (!checker) {
|
|
280
|
+
checker = resolveConsoleAuthorize(authorize);
|
|
281
|
+
adminAuthorizeCheckers.set(authorize, checker);
|
|
282
|
+
}
|
|
283
|
+
return checker;
|
|
284
|
+
}
|
|
213
285
|
const C = {
|
|
214
286
|
oidc: () => import('../controllers/oidc_callback_controller.js'),
|
|
215
287
|
authorizationServerMetadata: () => import('../controllers/authorization_server_metadata_controller.js'),
|
|
@@ -218,6 +290,7 @@ const C = {
|
|
|
218
290
|
social: () => import('./controllers/social_controller.js'),
|
|
219
291
|
patIntrospection: () => import('./controllers/pat_introspection_controller.js'),
|
|
220
292
|
accountSession: () => import('./controllers/account_session_controller.js'),
|
|
293
|
+
consoleSso: () => import('./controllers/console_sso_controller.js'),
|
|
221
294
|
accountTokens: () => import('./controllers/account_tokens_controller.js'),
|
|
222
295
|
accountSecurity: () => import('./controllers/account_security_controller.js'),
|
|
223
296
|
accountApps: () => import('./controllers/account_apps_controller.js'),
|
|
@@ -226,8 +299,6 @@ const C = {
|
|
|
226
299
|
accountConfirm: () => import('./controllers/account_confirm_controller.js'),
|
|
227
300
|
agentOAuth: () => import('./controllers/agent_oauth_controller.js'),
|
|
228
301
|
agentConsent: () => import('./controllers/agent_consent_controller.js'),
|
|
229
|
-
poppy: () => import('./controllers/poppy_controller.js'),
|
|
230
|
-
poppyConsent: () => import('./controllers/poppy_consent_controller.js'),
|
|
231
302
|
webauthnAsset: () => import('./controllers/webauthn_asset_controller.js'),
|
|
232
303
|
logoutAsset: () => import('./controllers/logout_asset_controller.js'),
|
|
233
304
|
passkeyAutofillAsset: () => import('./controllers/passkey_autofill_asset_controller.js'),
|
|
@@ -380,6 +451,17 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
380
451
|
if (accountLoginUrlOpt !== undefined) {
|
|
381
452
|
setAccountLoginUrl(accountLoginUrlOpt);
|
|
382
453
|
}
|
|
454
|
+
// Console sem porta de entrada: SSO desligado (`acceptIdpSession: false`) deixa só o
|
|
455
|
+
// login próprio do console, que é por senha; com a senha desligada ninguém entra em
|
|
456
|
+
// `/account/*` nem no console admin. Só passa com um `accountLoginUrl` do host (aqui
|
|
457
|
+
// ou em `routes.accountLoginUrl`) — por isso a checagem é no registro, não no config.
|
|
458
|
+
if (hostCfg?.consoleNeedsLoginUrl && !accountLoginUrlOpt) {
|
|
459
|
+
throw new Error('authkit: `accountSession.acceptIdpSession: false` com `authMethods: { password: false }` ' +
|
|
460
|
+
'deixa o console de conta e o console admin sem nenhum jeito de entrar (o login próprio ' +
|
|
461
|
+
'do console é por senha). Remova `acceptIdpSession: false` (o default entra pelo login ' +
|
|
462
|
+
'do IdP) ou passe `accountLoginUrl` (no `registerAuthHost` ou em `routes`) apontando ' +
|
|
463
|
+
'para um login do seu app.');
|
|
464
|
+
}
|
|
383
465
|
// Montagem por tela do console de conta. `undefined` → tudo montado;
|
|
384
466
|
// `false` → nada; objeto → cada flag ausente default `true`.
|
|
385
467
|
const accountOpt = opts.account ?? routesCfg?.account;
|
|
@@ -506,6 +588,13 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
506
588
|
// Provider OIDC (wildcard + root) — o que registerOidcRoutes fazia.
|
|
507
589
|
router.any(`${mount}/*`, [C.oidc]).as('authkit.oidc.wildcard');
|
|
508
590
|
router.any(mount, [C.oidc]).as('authkit.oidc.root');
|
|
591
|
+
// Login do console pelo IdP (SSO; client interno `authkit-console`, ver
|
|
592
|
+
// `console_sso.ts`). Sempre montado: com `acceptIdpSession: false` o início só
|
|
593
|
+
// redireciona para o login próprio do console.
|
|
594
|
+
router.get(CONSOLE_SSO_START_PATH, [C.consoleSso, 'start']).as('authkit.console_sso.start');
|
|
595
|
+
router
|
|
596
|
+
.get(CONSOLE_SSO_CALLBACK_PATH, [C.consoleSso, 'callback'])
|
|
597
|
+
.as('authkit.console_sso.callback');
|
|
509
598
|
// Interaction (login multi-step + consent + signup).
|
|
510
599
|
// Cada rota passa por `keep(key, …)`: entra em `AuthHostRouteMap.routes`/`urls` e
|
|
511
600
|
// recebe o `extraMiddleware` do host (depois do throttle da lib).
|
|
@@ -590,56 +679,13 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
590
679
|
})
|
|
591
680
|
.use([accountGuard]);
|
|
592
681
|
}
|
|
593
|
-
//
|
|
594
|
-
//
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
// Nomes explícitos: o Router nomeia pela dupla controller.método, e as duas URLs da metadata
|
|
601
|
-
// (RFC 8414 na raiz e debaixo do issuer) usam o mesmo — "poppy.metadata already exists" no boot.
|
|
602
|
-
router.get('/.well-known/poppy.json', [C.poppy, 'discovery']).as('authkit.poppy.discovery');
|
|
603
|
-
router
|
|
604
|
-
.get('/.well-known/oauth-protected-resource', [C.poppy, 'protectedResource'])
|
|
605
|
-
.as('authkit.poppy.protected_resource');
|
|
606
|
-
router
|
|
607
|
-
.get(`/.well-known/oauth-authorization-server${poppyPrefix}`, [C.poppy, 'metadata'])
|
|
608
|
-
.as('authkit.poppy.metadata');
|
|
609
|
-
router
|
|
610
|
-
.get(`${poppyPrefix}/.well-known/oauth-authorization-server`, [C.poppy, 'metadata'])
|
|
611
|
-
.as('authkit.poppy.metadata_under_issuer');
|
|
612
|
-
router.post(`${poppyPrefix}/oauth/token`, [C.poppy, 'token']).as('authkit.poppy.token');
|
|
613
|
-
router.post(`${poppyPrefix}/oauth/revoke`, [C.poppy, 'revoke']).as('authkit.poppy.revoke');
|
|
614
|
-
router
|
|
615
|
-
.post(`${poppyPrefix}/oauth/device`, [C.poppy, 'deviceAuthorization'])
|
|
616
|
-
.as('authkit.poppy.device_authorization');
|
|
617
|
-
// A página grava um pedido sem autenticação: throttle por IP.
|
|
618
|
-
withLogin(router
|
|
619
|
-
.get(`${poppyPrefix}/browser-session`, [C.poppy, 'browserSessionPage'])
|
|
620
|
-
.as('authkit.poppy.browser_session_page'));
|
|
621
|
-
router
|
|
622
|
-
.post(`${poppyPrefix}/browser-session`, [C.poppy, 'browserSession'])
|
|
623
|
-
.as('authkit.poppy.browser_session');
|
|
624
|
-
router
|
|
625
|
-
.post(`${poppyPrefix}/browser-session/authorize`, [C.poppy, 'browserSessionAuthorize'])
|
|
626
|
-
.as('authkit.poppy.browser_session_authorize');
|
|
627
|
-
router
|
|
628
|
-
.group(() => {
|
|
629
|
-
router
|
|
630
|
-
.get(`${poppyPrefix}/oauth/authorize`, [C.poppyConsent, 'authorize'])
|
|
631
|
-
.as('authkit.poppy.authorize');
|
|
632
|
-
router
|
|
633
|
-
.post(`${poppyPrefix}/oauth/authorize`, [C.poppyConsent, 'decideAuthorization'])
|
|
634
|
-
.as('authkit.poppy.authorize_decide');
|
|
635
|
-
withOtpLogin(router
|
|
636
|
-
.get(`${poppyPrefix}/device`, [C.poppyConsent, 'device'])
|
|
637
|
-
.as('authkit.poppy.device'));
|
|
638
|
-
withOtpLogin(router
|
|
639
|
-
.post(`${poppyPrefix}/device`, [C.poppyConsent, 'decideDevice'])
|
|
640
|
-
.as('authkit.poppy.device_decide'));
|
|
641
|
-
})
|
|
642
|
-
.use([accountGuard]);
|
|
682
|
+
// `personalAgents.extensions`: cada uma monta as rotas dela, com o guard de conta e os
|
|
683
|
+
// buckets de rate limit das telas embutidas.
|
|
684
|
+
for (const extension of hostCfg?.personalAgents?.extensions ?? []) {
|
|
685
|
+
extension.routes?.(router, {
|
|
686
|
+
accountGuard,
|
|
687
|
+
throttle: { login: (route) => withLogin(route), otp: (route) => withOtpLogin(route) },
|
|
688
|
+
});
|
|
643
689
|
}
|
|
644
690
|
// Paths do console de conta (configuráveis/localizáveis via `accountRoutes`).
|
|
645
691
|
// As TELAS vêm de `accountPath(key)` (prefixo + segmento configurável); os
|