@adonis-agora/authkit-server 0.34.1 → 0.36.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.
@@ -36,11 +36,19 @@
36
36
  class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none transition focus:border-gray-900 focus:ring-2 focus:ring-gray-900" />
37
37
  </div>
38
38
 
39
- <div class="mt-4">
40
- <label for="password" class="mb-1 block text-sm font-medium text-gray-700">{{ t('signup.password_label') }}</label>
41
- <input id="password" name="password" type="password" required minlength="8"
42
- class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none transition focus:border-gray-900 focus:ring-2 focus:ring-gray-900" />
43
- </div>
39
+ {{-- Cadastro passwordless: sem campo de senha. O login vem por magic link. --}}
40
+ @if(!passwordlessSignup)
41
+ <div class="mt-4">
42
+ <label for="password" class="mb-1 block text-sm font-medium text-gray-700">{{ t('signup.password_label') }}</label>
43
+ <input id="password" name="password" type="password" required minlength="8"
44
+ class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none transition focus:border-gray-900 focus:ring-2 focus:ring-gray-900" />
45
+ </div>
46
+ @end
47
+
48
+ {{-- Passwordless: confirmação de link enviado (anti-enumeração). --}}
49
+ @if(magicLinkSent)
50
+ <p class="mt-4 rounded-lg bg-green-50 px-3 py-2 text-sm text-green-700">{{ t('signup.magic_link_sent') }}</p>
51
+ @end
44
52
 
45
53
  {{-- Bot protection: container do widget (config-trusted; HTML do host renderizado raw). --}}
46
54
  @if(botProtection && botProtection.html)
package/build/index.d.ts CHANGED
@@ -113,3 +113,4 @@ export { getAccountId, hasAccountSession, consoleLoginUrl, } from './src/host/co
113
113
  export { ACCOUNT_SESSION_KEY } from './src/host/middleware/account_auth.js';
114
114
  export { SUDO_SESSION_KEY, SUDO_MODE_DEFAULTS, requireSudo, isSudoActive, markSudo, resolveEffectiveSudoMode, } from './src/host/sudo_mode.js';
115
115
  export type { SudoModeSetting, ResolvedSudoModeSetting, } from './src/host/sudo_mode.js';
116
+ export { authkitUserProvider } from './src/host/adonis_auth_user_provider.js';
package/build/index.js CHANGED
@@ -74,3 +74,8 @@ export { getAccountId, hasAccountSession, consoleLoginUrl, } from './src/host/co
74
74
  export { ACCOUNT_SESSION_KEY } from './src/host/middleware/account_auth.js';
75
75
  // Sudo mode — helpers for host controllers that require step-up authentication.
76
76
  export { SUDO_SESSION_KEY, SUDO_MODE_DEFAULTS, requireSudo, isSudoActive, markSudo, resolveEffectiveSudoMode, } from './src/host/sudo_mode.js';
77
+ // @adonisjs/auth integration (opt-in) — user provider for config/auth.ts's
78
+ // sessionGuard(), backed by authkit's own accountStore. Pair with
79
+ // `adonisAuth: { guard: '...' }` in config/authkit.ts so the account console's
80
+ // login/logout also sync ctx.auth. See src/host/adonis_auth_user_provider.ts.
81
+ export { authkitUserProvider } from './src/host/adonis_auth_user_provider.js';
@@ -12,12 +12,27 @@ import { defaultEncryptForStore } from '../src/define_config.js';
12
12
  import { resolveKeystoreVault, KeystoreManager } from '../src/keys/keystore_manager.js';
13
13
  import { KeystoreCodec } from '../src/keys/keystore_codec.js';
14
14
  import { loadEncryptionService } from '../src/keys/keystore_crypto.js';
15
+ import { resolveAppKey } from '../src/host/app_key.js';
15
16
  export default class AuthkitServerProvider {
16
17
  app;
17
18
  constructor(app) {
18
19
  this.app = app;
19
20
  }
20
21
  async boot() {
22
+ // EAGER + LOUD (fail-fast): se o host configurou `config/authkit.ts`, a
23
+ // `appKey` é indispensável — o oidc-provider a usa para assinar TODOS os
24
+ // cookies (keygrip). Antes, essa validação só rodava dentro do factory
25
+ // lazy do `authkit.server` (registrado abaixo): sem nada que resolvesse o
26
+ // binding no boot, o app subia normalmente e o erro só aparecia (a) na
27
+ // 1ª request que tocasse uma rota do authkit, como um 500 sem contexto
28
+ // fora do modo dev, OU (b) nunca, porque `#startKeystoreReloadPoll`/
29
+ // `#startKeyRotationScheduler` engolem qualquer erro do `container.make`
30
+ // via `.catch(() => null)`. Validar aqui, fora do try/catch fail-safe
31
+ // abaixo, propaga o erro para `app.boot()` e derruba o boot do processo
32
+ // com a mensagem certa — em vez de um 500 misterioso depois.
33
+ if (this.app.config.get('authkit')) {
34
+ resolveAppKey(this.app);
35
+ }
21
36
  // Config locks: trava as settings definidas explicitamente no defineConfig
22
37
  // (config vence em runtime; a UI/Admin API não pode alterá-las). Fail-safe:
23
38
  // qualquer erro → sem locks (comportamento legado).
@@ -76,19 +91,12 @@ export default class AuthkitServerProvider {
76
91
  if (!config) {
77
92
  throw new RuntimeException('Config inválido em "config/authkit.ts". Use o método defineConfig de @adonis-agora/authkit-server.');
78
93
  }
79
- // `app.appKey` pode ser uma string crua ou um `Secret` do AdonisJS (config/app.ts
80
- // expõe `export const appKey = new Secret(env.get('APP_KEY'))`). O oidc-provider
81
- // assina cookies via keygrip e exige uma string então liberamos o Secret aqui.
82
- // Sem isso, a chave chegaria como objeto/undefined e o fluxo de authorize quebraria
83
- // (keygrip: "key argument must be of type string ...").
84
- const rawAppKey = this.app.config.get('app.appKey');
85
- const appKey = rawAppKey && typeof rawAppKey.release === 'function'
86
- ? rawAppKey.release()
87
- : rawAppKey;
88
- if (!appKey || typeof appKey !== 'string') {
89
- throw new RuntimeException('APP_KEY ausente: defina `export const appKey = new Secret(env.get(\'APP_KEY\'))` em config/app.ts. ' +
90
- 'O @adonis-agora/authkit-server precisa dele para assinar os cookies do oidc-provider.');
91
- }
94
+ // `app.appKey` pode ser uma string crua ou um `Secret` do AdonisJS — ver
95
+ // `resolveAppKey` para o porquê e para a mensagem de erro (agora também
96
+ // validada EAGER no `boot()`, acima; mantida aqui para o caso raro de
97
+ // alguém resolver `authkit.server` diretamente sem passar pelo boot do
98
+ // provider, ex.: testes unitários do binding).
99
+ const appKey = resolveAppKey(this.app);
92
100
  const metrics = await this.app.container.make('authkit.metrics');
93
101
  const jwksInput = config.jwksConfig;
94
102
  let jwksLoader;
@@ -322,10 +322,19 @@ export interface PasswordlessConfigInput {
322
322
  magicLink?: boolean;
323
323
  /** Liga o "entrar com passkey" antes da senha. Default: false. */
324
324
  passkeyFirst?: boolean;
325
+ /**
326
+ * Liga o cadastro público passwordless: o signup pede só e-mail + nome (sem
327
+ * senha), cria a conta com uma senha aleatória inutilizável e envia um magic
328
+ * link que finaliza o login (o mesmo fluxo do login por magic link). Default:
329
+ * false. Exige que o accountStore implemente {@link MagicLinkCapability}; sem
330
+ * ela o cadastro passwordless fica indisponível e o fluxo de senha segue valendo.
331
+ */
332
+ signup?: boolean;
325
333
  }
326
334
  export interface ResolvedPasswordlessConfig {
327
335
  magicLink: boolean;
328
336
  passkeyFirst: boolean;
337
+ signup: boolean;
329
338
  }
330
339
  export declare function resolvePasswordless(input?: PasswordlessConfigInput): ResolvedPasswordlessConfig;
331
340
  /**
@@ -521,7 +530,12 @@ export interface AuthServerConfigInput {
521
530
  * Hosts que preferem mandar o usuário direto pro app podem apontar pra rota deles.
522
531
  */
523
532
  accountHome?: string;
524
- /** Renderer de páginas do host (Inertia ou Edge). */
533
+ /**
534
+ * Renderer de páginas do host (Inertia ou Edge). Default (quando omitido):
535
+ * {@link edgeRenderer}. Sem isso, TODA request a `/account/*` e
536
+ * `/auth/interaction/*` estourava um 500 sem explicação — `render` era
537
+ * `undefined` e o controller chamava `render(ctx, ...)` diretamente.
538
+ */
525
539
  render?: AuthHostRenderer;
526
540
  /** Configuração de branding por cliente. */
527
541
  branding?: BrandingConfig;
@@ -662,6 +676,27 @@ export interface AuthServerConfigInput {
662
676
  accountLifecycle?: {
663
677
  durable?: boolean;
664
678
  };
679
+ /**
680
+ * Integração OPT-IN com `@adonisjs/auth`. authkit-server autentica a área de
681
+ * conta (`/account/*`) com sua PRÓPRIA sessão (`ACCOUNT_SESSION_KEY`, via
682
+ * `@adonisjs/session` diretamente) — nunca chamou `@adonisjs/auth` em lugar
683
+ * nenhum, então `ctx.auth.user`/`middleware.auth()`/Bouncer's
684
+ * `() => ctx.auth.user` ficavam sempre `undefined` num host que os use.
685
+ *
686
+ * Quando `adonisAuth.guard` é setado, `AccountSessionController#login`/`logout`
687
+ * TAMBÉM chamam `ctx.auth.use(guard).login(account)`/`.logout()` do guard
688
+ * indicado — em paralelo ao cookie bespoke do authkit, que continua sendo a
689
+ * fonte de verdade das rotas do próprio authkit. Requer um user provider
690
+ * plugado nesse guard em `config/auth.ts` — veja {@link authkitUserProvider}.
691
+ *
692
+ * Default (ausente): comportamento de sempre, sem tocar em `ctx.auth`.
693
+ * Degrada limpo mesmo sem `@adonisjs/auth` instalado (peer opcional): o
694
+ * código só acessa `ctx.auth` quando esta opção está setada E a propriedade
695
+ * existe no ctx (host com `@adonisjs/auth` de fato inicializado).
696
+ */
697
+ adonisAuth?: {
698
+ guard: string;
699
+ };
665
700
  }
666
701
  export interface ResolvedServerConfig {
667
702
  issuer: string;
@@ -755,6 +790,10 @@ export interface ResolvedServerConfig {
755
790
  };
756
791
  /** Keys de `auth_settings` travadas por terem sido definidas no defineConfig. */
757
792
  lockedSettingKeys: string[];
793
+ /** Integração opt-in com `@adonisjs/auth` (ausente = não integrado; comportamento de sempre). */
794
+ adonisAuth?: {
795
+ guard: string;
796
+ };
758
797
  }
759
798
  export declare function toSeconds(value: string | number | undefined, fallback: number): number;
760
799
  /**
@@ -9,6 +9,7 @@ import { resolveMessages, } from "./host/i18n.js";
9
9
  import { resolveTrustedDevices, } from "./host/trusted_device.js";
10
10
  import { resolveBotProtection, } from "./host/bot_protection.js";
11
11
  import { deriveLockedSettingKeys } from "./host/config_locks.js";
12
+ import { edgeRenderer } from "./host/renderers/edge_renderer.js";
12
13
  export { adapters };
13
14
  const RATE_LIMIT_DEFAULTS = {
14
15
  login: { points: 10, duration: "1 min" },
@@ -91,6 +92,7 @@ export function resolvePasswordless(input) {
91
92
  return {
92
93
  magicLink: input?.magicLink ?? false,
93
94
  passkeyFirst: input?.passkeyFirst ?? false,
95
+ signup: input?.signup ?? false,
94
96
  };
95
97
  }
96
98
  export function resolveLogin(input) {
@@ -296,7 +298,14 @@ export function defineConfig(config) {
296
298
  accountStore: config.accountStore,
297
299
  patStore: config.patStore,
298
300
  mountPath: config.mountPath ?? "/oidc",
299
- render: config.render,
301
+ // Default de runtime: sem isso, `render` ficava `undefined` e TODA
302
+ // request a `/account/*`/`/auth/interaction/*` estourava um 500 sem
303
+ // contexto (os controllers fazem `cfg.render!(...)`). `edgeRenderer()`
304
+ // só usa `ctx.view` quando de fato invocado por uma request — não
305
+ // importa `edge.js` eagerly, então é seguro construir mesmo em hosts
306
+ // sem o peer opcional instalado (só quebraria se a request realmente
307
+ // chegasse sem Edge configurado, exatamente como hoje).
308
+ render: config.render ?? edgeRenderer(),
300
309
  branding: config.branding,
301
310
  social: config.social,
302
311
  patIntrospectionSecret: config.patIntrospectionSecret,
@@ -346,6 +355,8 @@ export function defineConfig(config) {
346
355
  // Keys de auth_settings travadas porque foram definidas no defineConfig:
347
356
  // config vence e a UI/Admin API não pode alterá-las (ver host/config_locks.ts).
348
357
  lockedSettingKeys: deriveLockedSettingKeys(config),
358
+ // Opt-in: ausente = authkit nunca toca `ctx.auth` (comportamento de sempre).
359
+ adonisAuth: config.adonisAuth,
349
360
  };
350
361
  });
351
362
  }
@@ -35,6 +35,7 @@ import { AdminSessionsService } from '../admin_sessions_service.js';
35
35
  import { enrichSessionsWithContext } from '../session_context.js';
36
36
  import { PasswordPolicyError } from '../../password/password_manager.js';
37
37
  import { resolveRuntimeSettings } from '../runtime_settings.js';
38
+ import { syncAdonisAuthLogout } from '../adonis_auth_sync.js';
38
39
  import { resolveEffectiveEmailChange, resolveEffectivePasswordHistory, } from '../runtime_toggles.js';
39
40
  import { dispatchSecurityNotice } from '../security_notice_service.js';
40
41
  import { requireSudo, isSudoActive, SUDO_MODE_DEFAULTS, resolveEffectiveSudoMode } from '../sudo_mode.js';
@@ -515,6 +516,7 @@ export default class AccountApiController {
515
516
  // Encerrar a sessão Adonis do console (logout global do self-service).
516
517
  ctx.session.forget(ACCOUNT_SESSION_KEY);
517
518
  await ctx.session.regenerate();
519
+ await syncAdonisAuthLogout(ctx, cfg);
518
520
  return { ok: true, signedOut: true, ...result };
519
521
  }
520
522
  // ─── GET /account/api/apps ──────────────────────────────────────────────
@@ -0,0 +1,27 @@
1
+ import type { HttpContext } from '@adonisjs/core/http';
2
+ import type { ResolvedServerConfig } from '../define_config.js';
3
+ import type { AuthAccount } from '../accounts/account_store.js';
4
+ /**
5
+ * Sincroniza `ctx.auth` (`@adonisjs/auth`) com um login bem-sucedido no console
6
+ * de conta do authkit. Ver `adonisAuth` em `define_config.ts` pro porquê: sem
7
+ * isso, `ctx.auth.user` nunca é populado por nenhum fluxo do authkit-server —
8
+ * mesmo num host que roda `@adonisjs/auth` normalmente.
9
+ *
10
+ * NO-OP quando:
11
+ * - `adonisAuth.guard` não está configurado (opt-in; default é não tocar em
12
+ * `ctx.auth` — comportamento de sempre).
13
+ * - `ctx.auth` não existe nesta request (host sem `InitializeAuthMiddleware`
14
+ * de `@adonisjs/auth` registrado — o peer é opcional).
15
+ *
16
+ * BEST-EFFORT: uma falha aqui (ex.: guard mal configurado em `config/auth.ts`)
17
+ * NUNCA quebra o login do authkit — o cookie bespoke (`ACCOUNT_SESSION_KEY`)
18
+ * já é a fonte de verdade das rotas do próprio authkit e continua funcionando
19
+ * mesmo se a sincronização com `@adonisjs/auth` falhar. Loga um warning para
20
+ * não mascarar um guard mal configurado silenciosamente.
21
+ */
22
+ export declare function syncAdonisAuthLogin(ctx: HttpContext, cfg: Pick<ResolvedServerConfig, 'adonisAuth'>, account: AuthAccount): Promise<void>;
23
+ /**
24
+ * Sincroniza `ctx.auth` com um logout do console de conta. Ver
25
+ * {@link syncAdonisAuthLogin} para as mesmas regras de no-op/best-effort.
26
+ */
27
+ export declare function syncAdonisAuthLogout(ctx: HttpContext, cfg: Pick<ResolvedServerConfig, 'adonisAuth'>): Promise<void>;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Sincroniza `ctx.auth` (`@adonisjs/auth`) com um login bem-sucedido no console
3
+ * de conta do authkit. Ver `adonisAuth` em `define_config.ts` pro porquê: sem
4
+ * isso, `ctx.auth.user` nunca é populado por nenhum fluxo do authkit-server —
5
+ * mesmo num host que roda `@adonisjs/auth` normalmente.
6
+ *
7
+ * NO-OP quando:
8
+ * - `adonisAuth.guard` não está configurado (opt-in; default é não tocar em
9
+ * `ctx.auth` — comportamento de sempre).
10
+ * - `ctx.auth` não existe nesta request (host sem `InitializeAuthMiddleware`
11
+ * de `@adonisjs/auth` registrado — o peer é opcional).
12
+ *
13
+ * BEST-EFFORT: uma falha aqui (ex.: guard mal configurado em `config/auth.ts`)
14
+ * NUNCA quebra o login do authkit — o cookie bespoke (`ACCOUNT_SESSION_KEY`)
15
+ * já é a fonte de verdade das rotas do próprio authkit e continua funcionando
16
+ * mesmo se a sincronização com `@adonisjs/auth` falhar. Loga um warning para
17
+ * não mascarar um guard mal configurado silenciosamente.
18
+ */
19
+ export async function syncAdonisAuthLogin(ctx, cfg, account) {
20
+ const guard = cfg.adonisAuth?.guard;
21
+ const auth = ctx.auth;
22
+ if (!guard || !auth)
23
+ return;
24
+ try {
25
+ await auth.use(guard).login(account);
26
+ }
27
+ catch (error) {
28
+ ctx.logger?.warn({ err: error, guard }, 'authkit: falha ao sincronizar login com @adonisjs/auth (adonisAuth.guard) — ' +
29
+ 'ctx.auth.user pode não refletir a conta logada; o cookie do authkit segue normal');
30
+ }
31
+ }
32
+ /**
33
+ * Sincroniza `ctx.auth` com um logout do console de conta. Ver
34
+ * {@link syncAdonisAuthLogin} para as mesmas regras de no-op/best-effort.
35
+ */
36
+ export async function syncAdonisAuthLogout(ctx, cfg) {
37
+ const guard = cfg.adonisAuth?.guard;
38
+ const auth = ctx.auth;
39
+ if (!guard || !auth)
40
+ return;
41
+ try {
42
+ await auth.use(guard).logout();
43
+ }
44
+ catch (error) {
45
+ ctx.logger?.warn({ err: error, guard }, 'authkit: falha ao sincronizar logout com @adonisjs/auth (adonisAuth.guard)');
46
+ }
47
+ }
@@ -0,0 +1,38 @@
1
+ import type { ConfigProvider } from '@adonisjs/core/types';
2
+ import type { SessionUserProviderContract } from '@adonisjs/auth/types/session';
3
+ import type { AuthAccount } from '../accounts/account_store.js';
4
+ /**
5
+ * User provider de `@adonisjs/auth` (session guard) apoiado no `accountStore`
6
+ * do próprio authkit — a MESMA fonte de identidade que o login do console de
7
+ * conta autentica. Plugado em `config/auth.ts`:
8
+ *
9
+ * ```ts
10
+ * import { sessionGuard } from '@adonisjs/auth/session'
11
+ * import { authkitUserProvider } from '@adonis-agora/authkit-server'
12
+ *
13
+ * export default defineConfig({
14
+ * default: 'web',
15
+ * guards: {
16
+ * web: sessionGuard({
17
+ * useRememberMeTokens: false,
18
+ * provider: authkitUserProvider(),
19
+ * }),
20
+ * },
21
+ * })
22
+ * ```
23
+ *
24
+ * Junto com `adonisAuth: { guard: 'web' }` em `config/authkit.ts`, o login do
25
+ * console de conta (`AccountSessionController#login`) passa a chamar
26
+ * `ctx.auth.use('web').login(account)` no sucesso — populando `ctx.auth.user`
27
+ * com um {@link AuthAccount} de verdade para o resto do app (Bouncer,
28
+ * `middleware.auth()`, etc.), sem duplicar a sessão bespoke do authkit
29
+ * (`ACCOUNT_SESSION_KEY`), que continua sendo a fonte de verdade das próprias
30
+ * rotas do authkit.
31
+ *
32
+ * Devolve um `ConfigProvider` (mesmo padrão do `sessionUserProvider()` nativo
33
+ * de `@adonisjs/auth`) — a resolução (incluindo o import dinâmico de
34
+ * `@adonisjs/auth` para o símbolo `PROVIDER_REAL_USER` exigido pelo contrato)
35
+ * só acontece quando o HOST resolve a config do guard no boot, nunca no
36
+ * carregamento deste módulo.
37
+ */
38
+ export declare function authkitUserProvider(): ConfigProvider<SessionUserProviderContract<AuthAccount>>;
@@ -0,0 +1,74 @@
1
+ import { configProvider } from '@adonisjs/core';
2
+ import { RuntimeException } from '@adonisjs/core/exceptions';
3
+ /**
4
+ * User provider de `@adonisjs/auth` (session guard) apoiado no `accountStore`
5
+ * do próprio authkit — a MESMA fonte de identidade que o login do console de
6
+ * conta autentica. Plugado em `config/auth.ts`:
7
+ *
8
+ * ```ts
9
+ * import { sessionGuard } from '@adonisjs/auth/session'
10
+ * import { authkitUserProvider } from '@adonis-agora/authkit-server'
11
+ *
12
+ * export default defineConfig({
13
+ * default: 'web',
14
+ * guards: {
15
+ * web: sessionGuard({
16
+ * useRememberMeTokens: false,
17
+ * provider: authkitUserProvider(),
18
+ * }),
19
+ * },
20
+ * })
21
+ * ```
22
+ *
23
+ * Junto com `adonisAuth: { guard: 'web' }` em `config/authkit.ts`, o login do
24
+ * console de conta (`AccountSessionController#login`) passa a chamar
25
+ * `ctx.auth.use('web').login(account)` no sucesso — populando `ctx.auth.user`
26
+ * com um {@link AuthAccount} de verdade para o resto do app (Bouncer,
27
+ * `middleware.auth()`, etc.), sem duplicar a sessão bespoke do authkit
28
+ * (`ACCOUNT_SESSION_KEY`), que continua sendo a fonte de verdade das próprias
29
+ * rotas do authkit.
30
+ *
31
+ * Devolve um `ConfigProvider` (mesmo padrão do `sessionUserProvider()` nativo
32
+ * de `@adonisjs/auth`) — a resolução (incluindo o import dinâmico de
33
+ * `@adonisjs/auth` para o símbolo `PROVIDER_REAL_USER` exigido pelo contrato)
34
+ * só acontece quando o HOST resolve a config do guard no boot, nunca no
35
+ * carregamento deste módulo.
36
+ */
37
+ export function authkitUserProvider() {
38
+ return configProvider.create(async (app) => {
39
+ let PROVIDER_REAL_USER;
40
+ try {
41
+ const auth = (await import('@adonisjs/auth'));
42
+ PROVIDER_REAL_USER = auth.symbols.PROVIDER_REAL_USER;
43
+ }
44
+ catch (error) {
45
+ throw new RuntimeException('authkitUserProvider() precisa de "@adonisjs/auth" instalado (é um peer opcional do ' +
46
+ '@adonis-agora/authkit-server, só necessário se você plugar este provider em config/auth.ts). ' +
47
+ 'Rode `npm i @adonisjs/auth` (ou pnpm/yarn) e configure um sessionGuard em config/auth.ts.', { cause: error });
48
+ }
49
+ // `PROVIDER_REAL_USER` chega em runtime como um `symbol` (não um "unique
50
+ // symbol" literal) — o cast final é seguro pq o valor É literalmente o
51
+ // símbolo exportado por `@adonisjs/auth`, só que o TS não consegue provar
52
+ // isso estruturalmente a partir de um import dinâmico.
53
+ const provider = {
54
+ [PROVIDER_REAL_USER]: undefined,
55
+ async createUserForGuard(user) {
56
+ return {
57
+ getId: () => user.id,
58
+ getOriginal: () => user,
59
+ };
60
+ },
61
+ async findById(identifier) {
62
+ const store = await app.container.make('authkit.accountStore');
63
+ const account = await store.findById(String(identifier));
64
+ if (!account)
65
+ return null;
66
+ return {
67
+ getId: () => account.id,
68
+ getOriginal: () => account,
69
+ };
70
+ },
71
+ };
72
+ return provider;
73
+ });
74
+ }
@@ -0,0 +1,18 @@
1
+ import type { ApplicationService } from '@adonisjs/core/types';
2
+ /**
3
+ * Mensagem única (compartilhada entre o check EAGER do `boot()` e o resolver
4
+ * LAZY do `register()`) para o erro de `appKey` ausente — um só lugar para
5
+ * editar o texto, e garante que os dois caminhos falem exatamente a mesma coisa.
6
+ */
7
+ export declare const MISSING_APP_KEY_MESSAGE: string;
8
+ /**
9
+ * Lê e valida `app.appKey` (usado pelo oidc-provider para assinar cookies via
10
+ * keygrip). `app.appKey` pode ser uma string crua ou um `Secret` do AdonisJS
11
+ * (`config/app.ts` expõe `export const appKey = new Secret(env.get('APP_KEY'))`).
12
+ *
13
+ * Lança {@link RuntimeException} com uma mensagem que NOMEIA o arquivo e o campo
14
+ * exatos a corrigir — nunca deixa o app seguir com uma appKey ausente/inválida
15
+ * (o oidc-provider quebraria mais adiante com um erro genérico do keygrip,
16
+ * sem apontar a causa raiz).
17
+ */
18
+ export declare function resolveAppKey(app: ApplicationService): string;
@@ -0,0 +1,28 @@
1
+ import { RuntimeException } from '@adonisjs/core/exceptions';
2
+ /**
3
+ * Mensagem única (compartilhada entre o check EAGER do `boot()` e o resolver
4
+ * LAZY do `register()`) para o erro de `appKey` ausente — um só lugar para
5
+ * editar o texto, e garante que os dois caminhos falem exatamente a mesma coisa.
6
+ */
7
+ export const MISSING_APP_KEY_MESSAGE = 'APP_KEY ausente: defina `export const appKey = new Secret(env.get(\'APP_KEY\'))` em config/app.ts. ' +
8
+ 'O @adonis-agora/authkit-server precisa dele para assinar os cookies do oidc-provider (NÃO usa config/encryption.ts).';
9
+ /**
10
+ * Lê e valida `app.appKey` (usado pelo oidc-provider para assinar cookies via
11
+ * keygrip). `app.appKey` pode ser uma string crua ou um `Secret` do AdonisJS
12
+ * (`config/app.ts` expõe `export const appKey = new Secret(env.get('APP_KEY'))`).
13
+ *
14
+ * Lança {@link RuntimeException} com uma mensagem que NOMEIA o arquivo e o campo
15
+ * exatos a corrigir — nunca deixa o app seguir com uma appKey ausente/inválida
16
+ * (o oidc-provider quebraria mais adiante com um erro genérico do keygrip,
17
+ * sem apontar a causa raiz).
18
+ */
19
+ export function resolveAppKey(app) {
20
+ const rawAppKey = app.config.get('app.appKey');
21
+ const appKey = rawAppKey && typeof rawAppKey.release === 'function'
22
+ ? rawAppKey.release()
23
+ : rawAppKey;
24
+ if (!appKey || typeof appKey !== 'string') {
25
+ throw new RuntimeException(MISSING_APP_KEY_MESSAGE);
26
+ }
27
+ return appKey;
28
+ }
@@ -15,6 +15,7 @@ import { resolveRuntimeSettings } from "../runtime_settings.js";
15
15
  import { resolveEffectiveEmailChange, resolveEffectivePasswordHistory, } from "../runtime_toggles.js";
16
16
  import { dispatchSecurityNotice } from "../security_notice_service.js";
17
17
  import { requireSudo } from "../sudo_mode.js";
18
+ import { syncAdonisAuthLogout } from "../adonis_auth_sync.js";
18
19
  /** Resolve os password history settings em runtime (fail-safe). */
19
20
  async function resolvePasswordHistorySettings(ctx) {
20
21
  try {
@@ -193,6 +194,7 @@ export default class AccountSecurityController {
193
194
  }
194
195
  // Encerra a sessão e leva ao login com a mensagem de sucesso.
195
196
  ctx.session.forget(ACCOUNT_SESSION_KEY);
197
+ await syncAdonisAuthLogout(ctx, cfg);
196
198
  ctx.session.flash("accountDeleted", translate(cfg.messages, "account.delete.deleted"));
197
199
  return ctx.response.redirect("/account/login");
198
200
  }
@@ -6,6 +6,7 @@ import { notifyLoginSuccess } from '../login_notify.js';
6
6
  import { markSudo } from '../sudo_mode.js';
7
7
  import { accountHome } from '../account_home.js';
8
8
  import { resolveRuntimeSettings } from '../runtime_settings.js';
9
+ import { syncAdonisAuthLogin, syncAdonisAuthLogout } from '../adonis_auth_sync.js';
9
10
  /**
10
11
  * Valida um valor de `return_to` recebido da query-string ou de um campo hidden.
11
12
  *
@@ -95,11 +96,16 @@ export default class AccountSessionController {
95
96
  ctx.session.put(ACCOUNT_SESSION_KEY, acc.id);
96
97
  // Login com senha = confirmação de identidade → marca sudo (graça a partir do login).
97
98
  markSudo(ctx);
99
+ // Opt-in: sincroniza ctx.auth.use(cfg.adonisAuth.guard) com a mesma conta —
100
+ // no-op sem `adonisAuth` configurado ou sem @adonisjs/auth inicializado.
101
+ await syncAdonisAuthLogin(ctx, cfg, acc);
98
102
  await notifyLoginSuccess(ctx, cfg, { accountId: acc.id, email, ip });
99
103
  // Redireciona pro destino original (validado), ou cai no accountHome configurado.
100
104
  return ctx.response.redirect(returnTo ?? accountHome(cfg));
101
105
  }
102
106
  async logout(ctx) {
107
+ const service = await ctx.containerResolver.make('authkit.server');
108
+ const cfg = service.config;
103
109
  // M6: não basta `forget(ACCOUNT_SESSION_KEY)` — sobravam na sessão
104
110
  // `authkit_sudo_at` (sudo), `authkit_last_seen` e qualquer outro estado
105
111
  // sensível, com o session id INALTERADO. Regenerar a sessão troca o id E
@@ -108,6 +114,8 @@ export default class AccountSessionController {
108
114
  // (belt-and-braces) caso o store de sessão não suporte regenerate.
109
115
  ctx.session.forget(ACCOUNT_SESSION_KEY);
110
116
  await ctx.session.regenerate();
117
+ // Opt-in: espelha o logout no guard de @adonisjs/auth (ver adonisAuth em define_config.ts).
118
+ await syncAdonisAuthLogout(ctx, cfg);
111
119
  return ctx.response.redirect('/account/login');
112
120
  }
113
121
  }
@@ -1,6 +1,7 @@
1
1
  import '../augmentations.js';
2
2
  import type { HttpContext } from '@adonisjs/core/http';
3
3
  export default class AuthRegistrationController {
4
+ #private;
4
5
  /** GET /auth/interaction/:uid/signup — tela de cadastro (dentro do fluxo OIDC). */
5
6
  showSignup(ctx: HttpContext): Promise<any>;
6
7
  /** POST /auth/interaction/:uid/signup — cria o usuário e finaliza o login. */
@@ -1,7 +1,8 @@
1
1
  import '../augmentations.js';
2
2
  import { brandFor } from '../branding.js';
3
- import { signupValidator, forgotPasswordValidator, resetPasswordValidator } from '../validators.js';
4
- import { sendPasswordResetEmail, sendEmailVerificationEmail } from '../default_mailer.js';
3
+ import { randomBytes } from 'node:crypto';
4
+ import { signupValidator, passwordlessSignupValidator, forgotPasswordValidator, resetPasswordValidator, } from '../validators.js';
5
+ import { sendPasswordResetEmail, sendEmailVerificationEmail, sendMagicLinkEmail, } from '../default_mailer.js';
5
6
  import { translate } from '../i18n.js';
6
7
  import { PasswordPolicyError } from '../../password/password_manager.js';
7
8
  import { guardBotProtection, resolveEffectiveBotProtection } from '../bot_protection.js';
@@ -54,6 +55,7 @@ export default class AuthRegistrationController {
54
55
  uid: details.uid,
55
56
  csrfToken: ctx.request.csrfToken,
56
57
  brand,
58
+ passwordlessSignup: cfg.passwordless?.signup ?? false,
57
59
  botProtection: effectiveBot?.on.includes('signup') ? effectiveBot.widget : undefined,
58
60
  });
59
61
  }
@@ -102,6 +104,12 @@ export default class AuthRegistrationController {
102
104
  botProtection: effectiveBotSignup?.widget,
103
105
  });
104
106
  }
107
+ // Cadastro passwordless (config): só e-mail + nome. Cria conta com senha random
108
+ // inutilizável e envia um magic link — mesmo fluxo do login por magic link.
109
+ if (cfg.passwordless?.signup &&
110
+ typeof cfg.accountStore.issueMagicLinkToken === 'function') {
111
+ return this.#passwordlessSignup(ctx, { cfg, brand, details });
112
+ }
105
113
  const data = await ctx.request.validateUsing(signupValidator);
106
114
  const accountStore = cfg.accountStore;
107
115
  const existing = await accountStore.findByEmail(data.email);
@@ -183,6 +191,55 @@ export default class AuthRegistrationController {
183
191
  ctx.logger.error({ err: error, email: data.email }, 'authkit: falha ao enviar verificação de e-mail');
184
192
  }
185
193
  }
194
+ /**
195
+ * Cadastro passwordless: valida e-mail + nome, cria a conta (senha random
196
+ * inutilizável) se ainda não existe, emite um magic link e o envia. Sempre
197
+ * responde "link enviado" (anti-enumeração), exista a conta ou não. Consumir o
198
+ * link finaliza o login pelo fluxo de magic link já existente (GET /magic).
199
+ */
200
+ async #passwordlessSignup(ctx, deps) {
201
+ const { cfg, brand, details } = deps;
202
+ const render = cfg.render;
203
+ const accountStore = cfg.accountStore;
204
+ const uid = ctx.request.param('uid');
205
+ const data = await ctx.request.validateUsing(passwordlessSignupValidator);
206
+ // Cria a conta se ainda não existe. Senha random inutilizável: o login é 100%
207
+ // passwordless (mesmo precedente das contas criadas por identidade social).
208
+ const existing = await accountStore.findByEmail(data.email);
209
+ if (!existing) {
210
+ const created = await accountStore.create({
211
+ email: data.email,
212
+ fullName: data.fullName,
213
+ password: randomBytes(24).toString('hex'),
214
+ });
215
+ await cfg.audit?.record({
216
+ type: 'signup',
217
+ accountId: created.id,
218
+ email: data.email,
219
+ ip: ctx.request.ip?.() ?? null,
220
+ clientId: details.params.client_id ?? null,
221
+ });
222
+ }
223
+ // Emite + envia o magic link (mesma construção do login por magic link).
224
+ const issued = await accountStore.issueMagicLinkToken(data.email);
225
+ if (issued) {
226
+ const origin = `${ctx.request.protocol()}://${ctx.request.host()}`;
227
+ const magicUrl = `${origin}/auth/interaction/${uid}/magic?token=${encodeURIComponent(issued.token)}`;
228
+ if (cfg.mail?.onMagicLink) {
229
+ await cfg.mail.onMagicLink({ email: data.email, magicUrl, token: issued.token });
230
+ }
231
+ else {
232
+ await sendMagicLinkEmail(ctx, { email: data.email, magicUrl });
233
+ }
234
+ }
235
+ // Resposta uniforme: "enviamos um link" (não vaza existência da conta).
236
+ return render(ctx, 'signup', {
237
+ uid,
238
+ csrfToken: ctx.request.csrfToken,
239
+ brand,
240
+ magicLinkSent: true,
241
+ });
242
+ }
186
243
  /** GET /auth/forgot-password — tela standalone. */
187
244
  async showForgot(ctx) {
188
245
  const service = await ctx.containerResolver.make('authkit.server');
@@ -48,6 +48,7 @@ export declare const DEFAULT_MESSAGES: {
48
48
  "account.login.idle_timeout": string;
49
49
  "login.magic_link_button": string;
50
50
  "login.magic_link_sent": string;
51
+ "signup.magic_link_sent": string;
51
52
  "login.passkey_button": string;
52
53
  "signup.page_title": string;
53
54
  "signup.title": string;
@@ -734,6 +735,7 @@ export declare const PT_BR_MESSAGES: {
734
735
  "account.login.idle_timeout": string;
735
736
  "login.magic_link_button": string;
736
737
  "login.magic_link_sent": string;
738
+ "signup.magic_link_sent": string;
737
739
  "login.passkey_button": string;
738
740
  "signup.page_title": string;
739
741
  "signup.title": string;
@@ -39,6 +39,7 @@ export const DEFAULT_MESSAGES = {
39
39
  // Passwordless (login).
40
40
  "login.magic_link_button": "Email me a login link",
41
41
  "login.magic_link_sent": "If the account exists, we sent you a login link.",
42
+ "signup.magic_link_sent": "Check your email — we sent you a link to finish creating your account.",
42
43
  "login.passkey_button": "Sign in with a passkey",
43
44
  // Tela de cadastro (signup).
44
45
  "signup.page_title": "Create account",
@@ -803,6 +804,7 @@ export const PT_BR_MESSAGES = {
803
804
  // Passwordless (login).
804
805
  "login.magic_link_button": "Me envie um link de login",
805
806
  "login.magic_link_sent": "Se a conta existir, enviamos um link de login.",
807
+ "signup.magic_link_sent": "Enviamos um link para o seu e-mail. Abra-o para concluir o cadastro.",
806
808
  "login.passkey_button": "Entrar com passkey",
807
809
  // Tela de cadastro (signup).
808
810
  "signup.page_title": "Criar conta",
@@ -1,3 +1,17 @@
1
+ /** Cadastro passwordless: só e-mail + nome (sem senha). O login vem por magic link. */
2
+ export declare const passwordlessSignupValidator: import("@vinejs/vine").VineValidator<import("@vinejs/vine").VineObject<{
3
+ email: import("@vinejs/vine").VineString;
4
+ fullName: import("@vinejs/vine").VineString;
5
+ }, {
6
+ email: string;
7
+ fullName: string;
8
+ }, {
9
+ email: string;
10
+ fullName: string;
11
+ }, {
12
+ email: string;
13
+ fullName: string;
14
+ }>, Record<string, any> | undefined>;
1
15
  export declare const signupValidator: import("@vinejs/vine").VineValidator<import("@vinejs/vine").VineObject<{
2
16
  email: import("@vinejs/vine").VineString;
3
17
  fullName: import("@vinejs/vine").VineString;
@@ -1,4 +1,9 @@
1
1
  import vine from '@vinejs/vine';
2
+ /** Cadastro passwordless: só e-mail + nome (sem senha). O login vem por magic link. */
3
+ export const passwordlessSignupValidator = vine.compile(vine.object({
4
+ email: vine.string().trim().email().normalizeEmail(),
5
+ fullName: vine.string().trim().minLength(2).maxLength(255),
6
+ }));
2
7
  export const signupValidator = vine.compile(vine.object({
3
8
  email: vine.string().trim().email().normalizeEmail(),
4
9
  fullName: vine.string().trim().minLength(2).maxLength(255),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adonis-agora/authkit-server",
3
- "version": "0.34.1",
3
+ "version": "0.36.0",
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",
@@ -56,15 +56,16 @@
56
56
  "peerDependencies": {
57
57
  "@adonis-agora/durable": "^0.2.0",
58
58
  "@adonis-agora/telescope": "^0.1.0",
59
- "@adonisjs/ally": "6.3.0",
60
- "@adonisjs/core": "7.3.3",
61
- "@adonisjs/drive": "4.0.0",
62
- "@adonisjs/lock": "2.1.0",
63
- "@adonisjs/lucid": "22.4.2",
64
- "@adonisjs/redis": "9.2.0",
65
- "@adonisjs/session": "8.1.0",
66
- "@adonisjs/shield": "9.0.0",
67
- "edge.js": "6.5.1"
59
+ "@adonisjs/ally": "^6.3.0",
60
+ "@adonisjs/auth": "^10.1.0",
61
+ "@adonisjs/core": "^7.3.3",
62
+ "@adonisjs/drive": "^4.0.0",
63
+ "@adonisjs/lock": "^2.1.0",
64
+ "@adonisjs/lucid": "^22.4.2",
65
+ "@adonisjs/redis": "^9.2.0",
66
+ "@adonisjs/session": "^8.1.0",
67
+ "@adonisjs/shield": "^9.0.0",
68
+ "edge.js": "^6.5.1"
68
69
  },
69
70
  "peerDependenciesMeta": {
70
71
  "@adonis-agora/durable": {
@@ -79,6 +80,9 @@
79
80
  "@adonisjs/ally": {
80
81
  "optional": true
81
82
  },
83
+ "@adonisjs/auth": {
84
+ "optional": true
85
+ },
82
86
  "@adonisjs/drive": {
83
87
  "optional": true
84
88
  },
@@ -104,7 +108,8 @@
104
108
  "@adonis-agora/durable": "^0.2.0",
105
109
  "@adonis-agora/telescope": "^0.1.0",
106
110
  "@adonisjs/ally": "6.3.0",
107
- "@adonisjs/core": "7.3.3",
111
+ "@adonisjs/auth": "10.1.0",
112
+ "@adonisjs/core": "7.3.5",
108
113
  "@adonisjs/drive": "4.0.0",
109
114
  "@adonisjs/lucid": "22.4.2",
110
115
  "@adonisjs/session": "8.1.0",
@@ -135,7 +140,7 @@
135
140
  "react-error-boundary": "6.1.2",
136
141
  "nuqs": "2.8.9",
137
142
  "recharts": "3.8.1",
138
- "@adonis-agora/authkit-react": "0.13.0"
143
+ "@adonis-agora/authkit-react": "0.14.0"
139
144
  },
140
145
  "scripts": {
141
146
  "build": "node scripts/build_host_css.mjs && node scripts/build_ui.mjs && tsc && node -e \"require('node:fs').cpSync('stubs','build/stubs',{recursive:true,filter:(s)=>!s.endsWith('.ts')})\" && node -e \"const fs=require('node:fs');if(fs.existsSync('assets'))fs.cpSync('assets','build/assets',{recursive:true})\" && node -e \"require('node:fs').cpSync('src/host/views','build/host/views',{recursive:true})\" && node -e \"const fs=require('node:fs');fs.mkdirSync('build/host/ui',{recursive:true});fs.readdirSync('src/host/ui').filter(f=>f.endsWith('.html')).forEach(f=>fs.copyFileSync('src/host/ui/'+f,'build/host/ui/'+f))\" && node -e \"require('node:fs').copyFileSync('commands/commands.json','build/commands/commands.json')\" && node -e \"const fs=require('node:fs');fs.mkdirSync('build/password',{recursive:true});fs.copyFileSync('src/password/common_passwords.txt','build/password/common_passwords.txt')\"",