@adonis-agora/authkit-server 0.61.1 → 0.61.3

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.
@@ -0,0 +1,4 @@
1
+ (() => {
2
+ const f = document.forms['op.logoutForm'];
3
+ if (f) f.submit();
4
+ })();
@@ -0,0 +1,27 @@
1
+ import type { HttpContext } from '@adonisjs/core/http';
2
+ /**
3
+ * GET /authkit/assets/logout.js
4
+ *
5
+ * Serve o script de auto-submit do splash de logout a partir do próprio host.
6
+ *
7
+ * O `logoutSource` (provider/logout_sources.ts) injeta este script VIA
8
+ * `<script src>` em vez de inline: hosts com CSP `script-src 'self'`
9
+ * bloqueiam script inline sem nonce/hash, e o auto-confirm do logout
10
+ * silenciosamente parava de rodar — a tela "Saindo / Encerrando sua
11
+ * sessão…" ficava presa para sempre. Um asset same-origin é permitido por
12
+ * `'self'` em qualquer CSP razoável, sem depender de nonce nem hash.
13
+ *
14
+ * SEM AUTENTICAÇÃO, e é intencional: é asset estático necessário na tela de
15
+ * logout, antes de a sessão SSO ser destruída. Também não pode viver sob o
16
+ * prefixo do console admin (que é opt-in).
17
+ */
18
+ export default class LogoutAssetController {
19
+ handle(ctx: HttpContext): Promise<void>;
20
+ }
21
+ /**
22
+ * Limpa o cache do asset. Existe para os testes conseguirem exercitar tanto o
23
+ * caminho feliz quanto o 404 no mesmo processo.
24
+ *
25
+ * @internal
26
+ */
27
+ export declare function resetLogoutAssetCache(): void;
@@ -0,0 +1,71 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ /**
3
+ * URL do script de auto-confirmação do RP-initiated logout, servido pelo
4
+ * próprio host.
5
+ *
6
+ * ⚠️ RESOLVIDO VIA `import.meta.url`, NUNCA relativo ao cwd. Os apps que
7
+ * consomem este pacote rodam `pnpm deploy --legacy`, que remonta a árvore de
8
+ * `node_modules` num diretório novo — um caminho relativo ao cwd do host
9
+ * apontaria para o lugar errado e a rota daria 404 em produção. Ancorado no
10
+ * módulo, o caminho segue o pacote para onde quer que ele seja copiado.
11
+ *
12
+ * Funciona nos dois layouts porque o asset é commitado em `src/host/assets/`
13
+ * e copiado para `build/src/host/assets/` pelo script `build`:
14
+ * dev → src/host/controllers/… → src/host/assets/logout.js
15
+ * build → build/src/host/controllers/… → build/src/host/assets/logout.js
16
+ */
17
+ const ASSET_URL = new URL('../assets/logout.js', import.meta.url);
18
+ /**
19
+ * Cache do conteúdo do asset. É imutável por versão do pacote — ler do disco
20
+ * a cada request de logout não compra nada.
21
+ *
22
+ * `null` = ainda não lido. `false` = lido e ausente (404 memoizado); sem isso
23
+ * um asset faltando viraria um `readFile` que falha por request.
24
+ */
25
+ let cached = null;
26
+ /**
27
+ * GET /authkit/assets/logout.js
28
+ *
29
+ * Serve o script de auto-submit do splash de logout a partir do próprio host.
30
+ *
31
+ * O `logoutSource` (provider/logout_sources.ts) injeta este script VIA
32
+ * `<script src>` em vez de inline: hosts com CSP `script-src 'self'`
33
+ * bloqueiam script inline sem nonce/hash, e o auto-confirm do logout
34
+ * silenciosamente parava de rodar — a tela "Saindo / Encerrando sua
35
+ * sessão…" ficava presa para sempre. Um asset same-origin é permitido por
36
+ * `'self'` em qualquer CSP razoável, sem depender de nonce nem hash.
37
+ *
38
+ * SEM AUTENTICAÇÃO, e é intencional: é asset estático necessário na tela de
39
+ * logout, antes de a sessão SSO ser destruída. Também não pode viver sob o
40
+ * prefixo do console admin (que é opt-in).
41
+ */
42
+ export default class LogoutAssetController {
43
+ async handle(ctx) {
44
+ if (cached === null) {
45
+ try {
46
+ cached = await readFile(ASSET_URL);
47
+ }
48
+ catch {
49
+ cached = false;
50
+ }
51
+ }
52
+ if (cached === false) {
53
+ // 404 limpo: o asset não foi incluído no pacote. O splash degrada para o
54
+ // botão `<noscript>` (que continua funcionando sem JS) em vez de 500.
55
+ return ctx.response.notFound();
56
+ }
57
+ return ctx.response
58
+ .type('text/javascript')
59
+ .header('Cache-Control', 'public, max-age=31536000, immutable')
60
+ .send(cached);
61
+ }
62
+ }
63
+ /**
64
+ * Limpa o cache do asset. Existe para os testes conseguirem exercitar tanto o
65
+ * caminho feliz quanto o 404 no mesmo processo.
66
+ *
67
+ * @internal
68
+ */
69
+ export function resetLogoutAssetCache() {
70
+ cached = null;
71
+ }
@@ -181,6 +181,7 @@ const C = {
181
181
  accountOrgs: () => import('./controllers/account_orgs_controller.js'),
182
182
  accountConfirm: () => import('./controllers/account_confirm_controller.js'),
183
183
  webauthnAsset: () => import('./controllers/webauthn_asset_controller.js'),
184
+ logoutAsset: () => import('./controllers/logout_asset_controller.js'),
184
185
  // Console React JSON API (session-authed, under {prefix}/api/*).
185
186
  consoleShell: () => import('./admin_console/admin_shell_controller.js'),
186
187
  consoleOverview: () => import('./admin_console/console_overview_controller.js'),
@@ -386,6 +387,11 @@ export function registerAuthHost(router, opts = {}) {
386
387
  // • registrado ANTES do wildcard `${mount}/*` para que nenhum mountPath
387
388
  // agressivo (ex.: '/') consiga engolir o asset e quebrar o login.
388
389
  router.get('/authkit/assets/webauthn.js', [C.webauthnAsset]).as('authkit.assets.webauthn');
390
+ // Script de auto-confirmação do splash de logout (RP-initiated logout).
391
+ // Same-origin (permitido por CSP `script-src 'self'`) — o logoutSource NÃO
392
+ // pode usar script inline: o CSP bloqueia sem nonce/hash e o logout fica
393
+ // preso em "Saindo / Encerrando sua sessão…".
394
+ router.get('/authkit/assets/logout.js', [C.logoutAsset]).as('authkit.assets.logout');
389
395
  // Provider OIDC (wildcard + root) — o que registerOidcRoutes fazia.
390
396
  router.any(`${mount}/*`, [C.oidc]).as('authkit.oidc.wildcard');
391
397
  router.any(mount, [C.oidc]).as('authkit.oidc.root');
@@ -45,7 +45,12 @@ export function createLogoutSources(messages) {
45
45
  async logoutSource(ctx, form) {
46
46
  // Injeta o campo de confirmação dentro do form fornecido pela lib.
47
47
  const confirmedForm = form.replace(/<\/form>/, '<input type="hidden" name="logout" value="yes"/></form>');
48
- ctx.body = page(t('logout.title'), `<div class="spinner" aria-hidden="true"></div><h1>${esc(t('logout.title'))}</h1><p>${esc(t('logout.body'))}</p>${confirmedForm}<noscript><button type="submit" form="op.logoutForm">${esc(t('logout.fallback'))}</button></noscript><script>(function(){var f=document.forms['op.logoutForm'];if(f){f.submit();}})();</script>`);
48
+ // Auto-submit via asset SAME-ORIGIN (`/authkit/assets/logout.js`), NÃO
49
+ // script inline: hosts com CSP `script-src 'self'` (+ nonce/hash, sem
50
+ // 'unsafe-inline') bloqueiam inline e o logout ficava preso em "Saindo /
51
+ // Encerrando sua sessão…" para sempre (o POST de confirm nunca rodava).
52
+ // `<script src>` same-origin é permitido por `'self'` sem nonce/hash.
53
+ ctx.body = page(t('logout.title'), `<div class="spinner" aria-hidden="true"></div><h1>${esc(t('logout.title'))}</h1><p>${esc(t('logout.body'))}</p>${confirmedForm}<noscript><button type="submit" form="op.logoutForm">${esc(t('logout.fallback'))}</button></noscript><script src="/authkit/assets/logout.js"></script>`);
49
54
  },
50
55
  /**
51
56
  * Mostrada apenas quando NÃO há `post_logout_redirect_uri` (senão o provider
@@ -260,5 +260,30 @@ export async function ensureAuthkitSchema(db, options = {}) {
260
260
  report.altered[def.name].push(column);
261
261
  }
262
262
  }
263
+ // Coluna `login_methods` em `auth.users` — a tabela é HOST-owned (não está em
264
+ // TABLES porque o nome/shape são decisão do app), mas a LI B consome a coluna
265
+ // (LoginMethodsPreferenceCapability). Alguns deploys de host só migram o schema
266
+ // do domínio, não o schema `auth` — então a migration manual da coluna pode
267
+ // nunca rodar, e isso derrubava TODO login/callback OIDC com "column users.login_methods
268
+ // does not exist" no instante em que o model passou a declará-la.
269
+ // Aqui garantimos: SE `auth.users` existe E a coluna falta, adicionamos. Nunca
270
+ // criamos a tabela (host-owned). Aditivo + idempotente — roda em todo boot.
271
+ try {
272
+ if (await tableExists(conn, 'users')) {
273
+ if (!(await columnExists(conn, 'users', 'login_methods'))) {
274
+ await conn.schema.alterTable('users', (t) => {
275
+ t.jsonb('login_methods').nullable();
276
+ });
277
+ report.altered.users = ['login_methods'];
278
+ }
279
+ }
280
+ }
281
+ catch (error) {
282
+ // Tabela host-owned pode não existir num banco que nunca criou users — não
283
+ // é erro: fail-soft (a migração do host cuida). Nunca criar a tabela aqui.
284
+ if (!(await tableExists(conn, 'users'))) {
285
+ throw error; // alguém criou entre o probe e o ALTER — repropaga se sumiu
286
+ }
287
+ }
263
288
  return report;
264
289
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adonis-agora/authkit-server",
3
- "version": "0.61.1",
3
+ "version": "0.61.3",
4
4
  "description": "AdonisJS OIDC/OAuth2 provider (Identity Provider) toolkit: ejectable auth server with sessions, rate-limiting, MFA/TOTP, audit log, federated logout and OpenTelemetry metrics.",
5
5
  "license": "MIT",
6
6
  "author": "dudousxd",