@dudousxd/adonis-authkit-server 0.3.0 → 0.5.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 (58) hide show
  1. package/build/commands/commands.json +28 -0
  2. package/build/commands/doctor.d.ts +10 -0
  3. package/build/commands/doctor.js +66 -0
  4. package/build/commands/rotate_keys.d.ts +10 -0
  5. package/build/commands/rotate_keys.js +53 -0
  6. package/build/host/views/account/email-confirmed.edge +15 -0
  7. package/build/host/views/account/security.edge +83 -0
  8. package/build/host/views/account/tokens.edge +7 -4
  9. package/build/host/views/admin/sessions.edge +89 -0
  10. package/build/host/views/admin/users.edge +1 -0
  11. package/build/host/views/mfa-challenge.edge +29 -23
  12. package/build/index.d.ts +5 -4
  13. package/build/index.js +3 -3
  14. package/build/src/accounts/account_store.d.ts +46 -1
  15. package/build/src/accounts/account_store.js +4 -0
  16. package/build/src/accounts/lucid_store/core.d.ts +5 -4
  17. package/build/src/accounts/lucid_store/core.js +67 -2
  18. package/build/src/adapters/adapter_contract.d.ts +17 -0
  19. package/build/src/adapters/database_adapter.d.ts +9 -5
  20. package/build/src/adapters/database_adapter.js +13 -6
  21. package/build/src/adapters/redis_adapter.d.ts +11 -5
  22. package/build/src/adapters/redis_adapter.js +16 -7
  23. package/build/src/audit/audit_sink.d.ts +1 -1
  24. package/build/src/define_config.d.ts +102 -0
  25. package/build/src/define_config.js +46 -3
  26. package/build/src/doctor/checks.d.ts +51 -0
  27. package/build/src/doctor/checks.js +231 -0
  28. package/build/src/host/admin_clients_service.js +12 -5
  29. package/build/src/host/admin_sessions_service.d.ts +63 -0
  30. package/build/src/host/admin_sessions_service.js +127 -0
  31. package/build/src/host/controllers/account_mfa_controller.js +6 -2
  32. package/build/src/host/controllers/account_security_controller.d.ts +16 -0
  33. package/build/src/host/controllers/account_security_controller.js +119 -0
  34. package/build/src/host/controllers/account_session_controller.js +2 -1
  35. package/build/src/host/controllers/admin/admin_sessions_controller.d.ts +14 -0
  36. package/build/src/host/controllers/admin/admin_sessions_controller.js +64 -0
  37. package/build/src/host/controllers/interaction_controller.d.ts +11 -0
  38. package/build/src/host/controllers/interaction_controller.js +55 -12
  39. package/build/src/host/default_mailer.d.ts +17 -0
  40. package/build/src/host/default_mailer.js +94 -9
  41. package/build/src/host/email_templates.d.ts +4 -0
  42. package/build/src/host/email_templates.js +5 -2
  43. package/build/src/host/i18n.d.ts +358 -11
  44. package/build/src/host/i18n.js +393 -12
  45. package/build/src/host/login_notify.d.ts +20 -0
  46. package/build/src/host/login_notify.js +71 -0
  47. package/build/src/host/register_auth_host.js +12 -0
  48. package/build/src/host/validators.d.ts +32 -0
  49. package/build/src/host/validators.js +14 -0
  50. package/build/src/keys/keystore.d.ts +43 -0
  51. package/build/src/keys/keystore.js +74 -0
  52. package/build/src/observability/metrics_controller.js +4 -4
  53. package/build/src/provider/build_provider.js +23 -0
  54. package/build/src/provider/device_sources.d.ts +6 -0
  55. package/build/src/provider/device_sources.js +65 -0
  56. package/build/src/provider/interaction_actions.d.ts +6 -1
  57. package/build/src/provider/interaction_actions.js +9 -2
  58. package/package.json +2 -2
@@ -1,9 +1,12 @@
1
1
  import { randomBytes } from 'node:crypto';
2
2
  import { DateTime } from 'luxon';
3
+ /** Prefixo do token de troca de e-mail (reaproveita a coluna emailVerificationToken). */
4
+ const EMAIL_CHANGE_PREFIX = 'ec:';
3
5
  /**
4
6
  * Núcleo SEMPRE presente do {@link CoreAccountStore} sobre um model Lucid:
5
- * identidade, cadastro, reset de senha, verificação de e-mail e administração
6
- * (listagem paginada + roles globais).
7
+ * identidade, cadastro, reset de senha, verificação de e-mail, administração
8
+ * (listagem paginada + roles globais) e o self-service de segurança
9
+ * ({@link AccountSecurityCapability}: trocar senha/e-mail).
7
10
  */
8
11
  export function buildCore(ctx) {
9
12
  const { Model, toAccount } = ctx;
@@ -66,6 +69,10 @@ export function buildCore(ctx) {
66
69
  async consumeEmailVerificationToken(token) {
67
70
  if (!token)
68
71
  return false;
72
+ // Tokens de troca de e-mail (`ec:`) NÃO são verificações de cadastro — só o
73
+ // fluxo de confirmEmailChange pode consumi-los.
74
+ if (token.startsWith(EMAIL_CHANGE_PREFIX))
75
+ return false;
69
76
  const row = await Model.query().where('emailVerificationToken', token).first();
70
77
  if (!row)
71
78
  return false;
@@ -104,5 +111,63 @@ export function buildCore(ctx) {
104
111
  row.globalRoles = roles;
105
112
  await row.save();
106
113
  },
114
+ // ----- Self-service de segurança (console de conta) -----
115
+ async changePassword(accountId, newPassword) {
116
+ const row = await Model.find(accountId);
117
+ if (!row)
118
+ return false;
119
+ // O hash acontece no @beforeSave do mixin withAuthUser ao detectar $dirty.password.
120
+ row.password = newPassword;
121
+ await row.save();
122
+ return true;
123
+ },
124
+ async requestEmailChange(accountId, newEmail) {
125
+ const row = await Model.find(accountId);
126
+ if (!row)
127
+ return null;
128
+ // Não permite tomar um e-mail já usado por OUTRA conta.
129
+ const taken = await Model.query().where('email', newEmail).first();
130
+ if (taken && taken.id !== row.id)
131
+ return null;
132
+ // Token = `ec:<base64url(newEmail)>:<random>`. Reaproveita a coluna
133
+ // emailVerificationToken (sem migração nova); o prefixo `ec:` distingue do
134
+ // token de verificação de cadastro. O e-mail viaja codificado no próprio
135
+ // token, então não precisamos de coluna extra para o "pending email".
136
+ const encodedEmail = Buffer.from(newEmail, 'utf8').toString('base64url');
137
+ const token = `${EMAIL_CHANGE_PREFIX}${encodedEmail}:${randomBytes(24).toString('hex')}`;
138
+ row.emailVerificationToken = token;
139
+ await row.save();
140
+ return { token, account: toAccount(row), newEmail };
141
+ },
142
+ async confirmEmailChange(token) {
143
+ if (!token || !token.startsWith(EMAIL_CHANGE_PREFIX))
144
+ return { ok: false };
145
+ const parts = token.split(':');
146
+ // Forma esperada: ['ec', '<b64email>', '<random>']
147
+ if (parts.length !== 3)
148
+ return { ok: false };
149
+ let newEmail;
150
+ try {
151
+ newEmail = Buffer.from(parts[1], 'base64url').toString('utf8');
152
+ }
153
+ catch {
154
+ return { ok: false };
155
+ }
156
+ if (!newEmail)
157
+ return { ok: false };
158
+ const row = await Model.query().where('emailVerificationToken', token).first();
159
+ if (!row)
160
+ return { ok: false };
161
+ // Defesa contra corrida: o e-mail pode ter sido tomado entre o pedido e a
162
+ // confirmação por outra conta.
163
+ const taken = await Model.query().where('email', newEmail).first();
164
+ if (taken && taken.id !== row.id)
165
+ return { ok: false };
166
+ row.email = newEmail;
167
+ row.emailVerifiedAt = DateTime.now();
168
+ row.emailVerificationToken = null;
169
+ await row.save();
170
+ return { ok: true, account: toAccount(row), newEmail };
171
+ },
107
172
  };
108
173
  }
@@ -11,6 +11,11 @@ export interface EnumeratedClient {
11
11
  clientId: string;
12
12
  payload: Record<string, unknown>;
13
13
  }
14
+ /** Um artefato OIDC enumerado de um model qualquer (id + payload persistido). */
15
+ export interface EnumeratedArtifact {
16
+ id: string;
17
+ payload: Record<string, unknown>;
18
+ }
14
19
  /** Contrato que o oidc-provider espera de um adapter (um por model). */
15
20
  export interface OidcAdapter {
16
21
  upsert(id: string, payload: OidcPayload, expiresIn: number): Promise<void>;
@@ -25,6 +30,18 @@ export interface OidcAdapter {
25
30
  * pelo console admin, para listar clients persistidos (registro dinâmico/CRUD).
26
31
  * Capacidade OPCIONAL (estilo `AuditSink.list`): adapters que não conseguem
27
32
  * enumerar de forma barata omitem o método e a UI degrada graciosamente.
33
+ *
34
+ * @deprecated Use {@link list} (genérico). Mantido por compat: quando presente,
35
+ * delega para `list()` (o adapter é sempre instanciado com `model = 'Client'`).
28
36
  */
29
37
  listClients?(): Promise<EnumeratedClient[]>;
38
+ /**
39
+ * Enumeração GENÉRICA dos artefatos do model deste adapter (id + payload). Usada
40
+ * pelo console admin para listar `Client` (CRUD) e `Session`/`Grant`/tokens
41
+ * (sessões ativas + revogação). Capacidade OPCIONAL (estilo `AuditSink.list`):
42
+ * adapters que não conseguem enumerar de forma barata omitem o método e a UI
43
+ * degrada graciosamente. O adapter já é escopado a UM model na construção
44
+ * (`new AdapterClass(model)`), então não recebe parâmetro.
45
+ */
46
+ list?(): Promise<EnumeratedArtifact[]>;
30
47
  }
@@ -1,5 +1,5 @@
1
1
  import type { Database } from '@adonisjs/lucid/database';
2
- import type { EnumeratedClient, OidcAdapter, OidcPayload } from './adapter_contract.js';
2
+ import type { EnumeratedArtifact, EnumeratedClient, OidcAdapter, OidcPayload } from './adapter_contract.js';
3
3
  export declare class DatabaseAdapter implements OidcAdapter {
4
4
  #private;
5
5
  private name;
@@ -13,10 +13,14 @@ export declare class DatabaseAdapter implements OidcAdapter {
13
13
  destroy(id: string): Promise<void>;
14
14
  revokeByGrantId(grantId: string): Promise<void>;
15
15
  /**
16
- * Enumera os clients persistidos (registro dinâmico ou CRUD do console admin).
17
- * Filtra por `model_name = this.name` (sempre 'Client' aqui) e descarta linhas
18
- * expiradas clients são persistidos sem TTL (`expires_at` NULL), então isso
19
- * só é uma rede de segurança caso algum dia algo grave o model com expiração.
16
+ * Enumeração genérica dos artefatos do model deste adapter (id + payload).
17
+ * Filtra por `model_name = this.name` e descarta linhas expiradas. Usada pelo
18
+ * console admin para listar `Client` (CRUD) e `Session`/`Grant`/tokens.
19
+ */
20
+ list(): Promise<EnumeratedArtifact[]>;
21
+ /**
22
+ * Compat: enumera os clients persistidos. Delega para {@link list} (o adapter é
23
+ * instanciado com `model_name = 'Client'`), reprojetando `id` → `clientId`.
20
24
  */
21
25
  listClients(): Promise<EnumeratedClient[]>;
22
26
  }
@@ -61,20 +61,27 @@ export class DatabaseAdapter {
61
61
  await this.db.query().from(TABLE).where('grant_id', grantId).delete();
62
62
  }
63
63
  /**
64
- * Enumera os clients persistidos (registro dinâmico ou CRUD do console admin).
65
- * Filtra por `model_name = this.name` (sempre 'Client' aqui) e descarta linhas
66
- * expiradas clients são persistidos sem TTL (`expires_at` NULL), então isso
67
- * só é uma rede de segurança caso algum dia algo grave o model com expiração.
64
+ * Enumeração genérica dos artefatos do model deste adapter (id + payload).
65
+ * Filtra por `model_name = this.name` e descarta linhas expiradas. Usada pelo
66
+ * console admin para listar `Client` (CRUD) e `Session`/`Grant`/tokens.
68
67
  */
69
- async listClients() {
68
+ async list() {
70
69
  const rows = await this.#query().orderBy('id', 'asc');
71
70
  const now = Date.now();
72
71
  const result = [];
73
72
  for (const row of rows) {
74
73
  if (row.expires_at && new Date(row.expires_at).getTime() <= now)
75
74
  continue;
76
- result.push({ clientId: row.id, payload: JSON.parse(row.payload) });
75
+ result.push({ id: row.id, payload: JSON.parse(row.payload) });
77
76
  }
78
77
  return result;
79
78
  }
79
+ /**
80
+ * Compat: enumera os clients persistidos. Delega para {@link list} (o adapter é
81
+ * instanciado com `model_name = 'Client'`), reprojetando `id` → `clientId`.
82
+ */
83
+ async listClients() {
84
+ const rows = await this.list();
85
+ return rows.map((r) => ({ clientId: r.id, payload: r.payload }));
86
+ }
80
87
  }
@@ -1,5 +1,5 @@
1
1
  import type { Redis } from 'ioredis';
2
- import type { EnumeratedClient, OidcAdapter, OidcPayload } from './adapter_contract.js';
2
+ import type { EnumeratedArtifact, EnumeratedClient, OidcAdapter, OidcPayload } from './adapter_contract.js';
3
3
  export declare class RedisAdapter implements OidcAdapter {
4
4
  #private;
5
5
  private name;
@@ -14,10 +14,16 @@ export declare class RedisAdapter implements OidcAdapter {
14
14
  destroy(id: string): Promise<void>;
15
15
  revokeByGrantId(grantId: string): Promise<void>;
16
16
  /**
17
- * Enumera os clients persistidos via SCAN sobre o prefixo de chave do model
18
- * (`<prefix>:Client:*`). É limpo porque cada artefato é uma chave única já
19
- * namespaceada por `prefix` + `name`; SCAN é não-bloqueante (cursor) ao
20
- * contrário de KEYS. Usado apenas pelo console admin (model 'Client').
17
+ * Enumeração genérica dos artefatos do model deste adapter via SCAN sobre o
18
+ * prefixo de chave do model (`<prefix>:<name>:*`). É limpo porque cada artefato
19
+ * é uma chave única já namespaceada por `prefix` + `name`; SCAN é não-bloqueante
20
+ * (cursor) ao contrário de KEYS. Usado pelo console admin (`Client`, `Session`,
21
+ * `Grant`, tokens).
22
+ */
23
+ list(): Promise<EnumeratedArtifact[]>;
24
+ /**
25
+ * Compat: enumera os clients persistidos. Delega para {@link list} (o adapter é
26
+ * instanciado com `name = 'Client'`), reprojetando `id` → `clientId`.
21
27
  */
22
28
  listClients(): Promise<EnumeratedClient[]>;
23
29
  }
@@ -93,12 +93,13 @@ export class RedisAdapter {
93
93
  await multi.exec();
94
94
  }
95
95
  /**
96
- * Enumera os clients persistidos via SCAN sobre o prefixo de chave do model
97
- * (`<prefix>:Client:*`). É limpo porque cada artefato é uma chave única já
98
- * namespaceada por `prefix` + `name`; SCAN é não-bloqueante (cursor) ao
99
- * contrário de KEYS. Usado apenas pelo console admin (model 'Client').
96
+ * Enumeração genérica dos artefatos do model deste adapter via SCAN sobre o
97
+ * prefixo de chave do model (`<prefix>:<name>:*`). É limpo porque cada artefato
98
+ * é uma chave única já namespaceada por `prefix` + `name`; SCAN é não-bloqueante
99
+ * (cursor) ao contrário de KEYS. Usado pelo console admin (`Client`, `Session`,
100
+ * `Grant`, tokens).
100
101
  */
101
- async listClients() {
102
+ async list() {
102
103
  const prefix = `${this.prefix}:${this.name}:`;
103
104
  const result = [];
104
105
  let cursor = '0';
@@ -110,12 +111,20 @@ export class RedisAdapter {
110
111
  if (!data)
111
112
  continue;
112
113
  result.push({
113
- clientId: key.slice(prefix.length),
114
+ id: key.slice(prefix.length),
114
115
  payload: JSON.parse(data),
115
116
  });
116
117
  }
117
118
  } while (cursor !== '0');
118
- result.sort((a, b) => a.clientId.localeCompare(b.clientId));
119
+ result.sort((a, b) => a.id.localeCompare(b.id));
119
120
  return result;
120
121
  }
122
+ /**
123
+ * Compat: enumera os clients persistidos. Delega para {@link list} (o adapter é
124
+ * instanciado com `name = 'Client'`), reprojetando `id` → `clientId`.
125
+ */
126
+ async listClients() {
127
+ const rows = await this.list();
128
+ return rows.map((r) => ({ clientId: r.id, payload: r.payload }));
129
+ }
121
130
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Tipos de eventos de auditoria relevantes para segurança emitidos pelo IdP.
3
3
  */
4
- export type AuditEventType = 'login.success' | 'login.failure' | 'signup' | 'password_reset.issued' | 'password_reset.consumed' | 'pat.issued' | 'pat.revoked' | 'pat.used' | 'impersonation' | 'mfa.enabled' | 'mfa.disabled' | 'account.locked' | 'passkey.registered' | 'passkey.removed' | 'email_verification.issued' | 'email_verification.consumed' | 'client.created' | 'client.updated' | 'client.deleted';
4
+ export type AuditEventType = 'login.success' | 'login.failure' | 'signup' | 'password_reset.issued' | 'password_reset.consumed' | 'pat.issued' | 'pat.revoked' | 'pat.used' | 'impersonation' | 'mfa.enabled' | 'mfa.disabled' | 'account.locked' | 'passkey.registered' | 'passkey.removed' | 'email_verification.issued' | 'email_verification.consumed' | 'client.created' | 'client.updated' | 'client.deleted' | 'session.revoked_all' | 'password.changed' | 'email.change_requested' | 'email.changed' | 'login.new_ip_notified';
5
5
  /**
6
6
  * Evento de auditoria a registrar. O timestamp é definido pelo sink (não aqui).
7
7
  */
@@ -89,6 +89,22 @@ export interface ResolvedLockoutConfig {
89
89
  store?: string;
90
90
  }
91
91
  export declare function resolveLockout(input?: LockoutConfigInput): ResolvedLockoutConfig;
92
+ /**
93
+ * Notificações de segurança por e-mail (best-effort, fire-and-forget). Hoje cobre
94
+ * o alerta de NOVO acesso (login de um IP nunca visto antes para a conta).
95
+ */
96
+ export interface NotificationsConfigInput {
97
+ /**
98
+ * Envia um e-mail "novo acesso à sua conta" quando um login bem-sucedido vem de
99
+ * um IP sem `login.success` anterior para a conta. Default: true. A checagem do
100
+ * histórico usa o `audit.list` (degrada para no-op se o sink não consulta).
101
+ */
102
+ newLoginEmail?: boolean;
103
+ }
104
+ export interface ResolvedNotificationsConfig {
105
+ newLoginEmail: boolean;
106
+ }
107
+ export declare function resolveNotifications(input?: NotificationsConfigInput): ResolvedNotificationsConfig;
92
108
  /**
93
109
  * Registro dinâmico de clients (OIDC Dynamic Client Registration — RFC 7591).
94
110
  *
@@ -123,6 +139,72 @@ export interface ResolvedDynamicRegistrationConfig {
123
139
  * (RFC 7591) — `management: true` com `enabled: false` é um erro de configuração.
124
140
  */
125
141
  export declare function resolveDynamicRegistration(input?: DynamicRegistrationConfigInput): ResolvedDynamicRegistrationConfig;
142
+ /**
143
+ * Device Authorization Grant (RFC 8628). Quando habilitado, o oidc-provider expõe
144
+ * o `device_authorization_endpoint` (`/device/auth`) e a tela de verificação de
145
+ * user-code (`/device`). O grant `urn:ietf:params:oauth:grant-type:device_code`
146
+ * deve ser concedido ao client (lista `grants`) para o fluxo funcionar.
147
+ */
148
+ export interface DeviceFlowConfigInput {
149
+ /** Liga o Device Authorization Grant. Default: false. */
150
+ enabled: boolean;
151
+ }
152
+ export interface ResolvedDeviceFlowConfig {
153
+ enabled: boolean;
154
+ }
155
+ export declare function resolveDeviceFlow(input?: DeviceFlowConfigInput): ResolvedDeviceFlowConfig;
156
+ /**
157
+ * DPoP — Demonstrating Proof of Possession (RFC 9449). Quando habilitado, o
158
+ * oidc-provider aceita DPoP proofs e emite tokens sender-constrained
159
+ * (`token_type: DPoP`, com `cnf.jkt`). A discovery passa a anunciar
160
+ * `dpop_signing_alg_values_supported`. Os resolvers do authkit-client aceitam o
161
+ * token via introspecção (a cnf viaja no resultado) — a geração de provas DPoP no
162
+ * client está fora de escopo (documentado como trabalho futuro).
163
+ */
164
+ export interface DpopConfigInput {
165
+ /** Liga o DPoP. Default: false. */
166
+ enabled: boolean;
167
+ }
168
+ export interface ResolvedDpopConfig {
169
+ enabled: boolean;
170
+ }
171
+ export declare function resolveDpop(input?: DpopConfigInput): ResolvedDpopConfig;
172
+ /**
173
+ * PAR — Pushed Authorization Requests (RFC 9126). Quando habilitado, o
174
+ * oidc-provider expõe o `pushed_authorization_request_endpoint` (`/request`): o
175
+ * client POSTa os parâmetros de authorize e recebe um `request_uri` opaco para
176
+ * usar no `/auth`. Com `requirePushedAuthorizationRequests`, o `/auth` SÓ aceita
177
+ * requests via `request_uri` (parâmetros inline são rejeitados).
178
+ */
179
+ export interface ParConfigInput {
180
+ /** Liga o PAR. Default: false. */
181
+ enabled: boolean;
182
+ /** Exige que TODO authorize venha via request_uri do PAR. Default: false. */
183
+ requirePushedAuthorizationRequests?: boolean;
184
+ }
185
+ export interface ResolvedParConfig {
186
+ enabled: boolean;
187
+ requirePushedAuthorizationRequests: boolean;
188
+ }
189
+ export declare function resolvePar(input?: ParConfigInput): ResolvedParConfig;
190
+ /**
191
+ * Step-up authentication via `acr_values` (MVP pragmático de MFA por requisição).
192
+ * Quando o client solicita `acr_values` contendo `mfaAcr`, o login EXIGE o 2º
193
+ * fator: contas com MFA enrolado passam pelo desafio (o `acr` do id_token vira
194
+ * `mfaAcr` e `amr` recebe `['mfa', método]`); contas SEM MFA enrolado têm o login
195
+ * bloqueado naquela requisição com a instrução de configurar MFA no console.
196
+ */
197
+ export interface StepUpConfigInput {
198
+ /** Lista de acr_values anunciados como suportados (discovery). */
199
+ acrValues?: string[];
200
+ /** O acr que dispara a exigência de MFA. Default: 'urn:authkit:mfa'. */
201
+ mfaAcr?: string;
202
+ }
203
+ export interface ResolvedStepUpConfig {
204
+ acrValues: string[];
205
+ mfaAcr: string;
206
+ }
207
+ export declare function resolveStepUp(input?: StepUpConfigInput): ResolvedStepUpConfig;
126
208
  /**
127
209
  * Console admin opt-in do IdP (B6). Quando habilitado, monta o grupo `/admin/*`
128
210
  * (dashboard, usuários/papéis, clients, audit) atrás de um guard que exige sessão
@@ -193,6 +275,8 @@ export interface AuthServerConfigInput {
193
275
  rateLimit?: RateLimitConfigInput;
194
276
  /** Bloqueio progressivo de conta por email em falhas repetidas. Default: ligado (no-op sem limiter). */
195
277
  lockout?: LockoutConfigInput;
278
+ /** Notificações de segurança por e-mail (alerta de novo acesso). Default: ligado. */
279
+ notifications?: NotificationsConfigInput;
196
280
  /** Hooks de e-mail (reset de senha / verificação). Opcional — fallback de log em dev. */
197
281
  mail?: MailHooks;
198
282
  /** Sink de auditoria (best-effort). Opcional — quando ausente, auditoria é no-op. */
@@ -213,6 +297,14 @@ export interface AuthServerConfigInput {
213
297
  * os clients registrados são persistidos pelo mesmo adapter OIDC.
214
298
  */
215
299
  dynamicRegistration?: DynamicRegistrationConfigInput;
300
+ /** Device Authorization Grant (RFC 8628). Default: desligado. */
301
+ deviceFlow?: DeviceFlowConfigInput;
302
+ /** DPoP — sender-constrained tokens (RFC 9449). Default: desligado. */
303
+ dpop?: DpopConfigInput;
304
+ /** Pushed Authorization Requests (RFC 9126). Default: desligado. */
305
+ par?: ParConfigInput;
306
+ /** Step-up auth via acr_values (MFA por requisição). Default: vazio (só o mfaAcr derivado). */
307
+ stepUp?: StepUpConfigInput;
216
308
  /**
217
309
  * Console admin do IdP (B6). Default: desligado. Quando ligado, o host também
218
310
  * deve passar `admin: true` em {@link AuthHostOptions} no registro de rotas
@@ -250,12 +342,22 @@ export interface ResolvedServerConfig {
250
342
  rateLimit: ResolvedRateLimitConfig;
251
343
  /** Bloqueio progressivo de conta resolvido (sempre presente; default ligado). */
252
344
  lockout: ResolvedLockoutConfig;
345
+ /** Notificações de segurança resolvidas (sempre presente; default ligado). */
346
+ notifications: ResolvedNotificationsConfig;
253
347
  mail?: MailHooks;
254
348
  audit?: AuditSink;
255
349
  mfaIssuer: string;
256
350
  /** RP de WebAuthn resolvido (sempre presente; derivado do issuer por default). */
257
351
  webauthn: ResolvedWebauthnConfig;
258
352
  dynamicRegistration: ResolvedDynamicRegistrationConfig;
353
+ /** Device Authorization Grant resolvido (default desligado). */
354
+ deviceFlow: ResolvedDeviceFlowConfig;
355
+ /** DPoP resolvido (default desligado). */
356
+ dpop: ResolvedDpopConfig;
357
+ /** PAR resolvido (default desligado). */
358
+ par: ResolvedParConfig;
359
+ /** Step-up auth resolvido (mfaAcr sempre presente). */
360
+ stepUp: ResolvedStepUpConfig;
259
361
  /** Console admin resolvido (sempre presente; default desligado). */
260
362
  admin: ResolvedAdminConfig;
261
363
  /** Catálogo de mensagens ativo (locale resolvido), pronto para os renderers. */
@@ -1,5 +1,6 @@
1
1
  import { configProvider } from '@adonisjs/core';
2
2
  import { generateJwks } from './keys/jwks_manager.js';
3
+ import { ensureKeystore } from './keys/keystore.js';
3
4
  import { adapters } from './adapters/factory.js';
4
5
  import { resolveMessages } from './host/i18n.js';
5
6
  export { adapters };
@@ -26,6 +27,11 @@ export function resolveLockout(input) {
26
27
  store: input?.store,
27
28
  };
28
29
  }
30
+ export function resolveNotifications(input) {
31
+ return {
32
+ newLoginEmail: input?.newLoginEmail ?? true,
33
+ };
34
+ }
29
35
  /**
30
36
  * Resolve a config de registro dinâmico e VALIDA invariantes em tempo de resolução.
31
37
  * O Registration Management (RFC 7592) só faz sentido com o registro habilitado
@@ -45,6 +51,24 @@ export function resolveDynamicRegistration(input) {
45
51
  management,
46
52
  };
47
53
  }
54
+ export function resolveDeviceFlow(input) {
55
+ return { enabled: input?.enabled ?? false };
56
+ }
57
+ export function resolveDpop(input) {
58
+ return { enabled: input?.enabled ?? false };
59
+ }
60
+ export function resolvePar(input) {
61
+ return {
62
+ enabled: input?.enabled ?? false,
63
+ requirePushedAuthorizationRequests: input?.requirePushedAuthorizationRequests ?? false,
64
+ };
65
+ }
66
+ export function resolveStepUp(input) {
67
+ const mfaAcr = input?.mfaAcr ?? 'urn:authkit:mfa';
68
+ // Garante que o mfaAcr esteja sempre na lista anunciada como suportada.
69
+ const acrValues = Array.from(new Set([...(input?.acrValues ?? []), mfaAcr]));
70
+ return { acrValues, mfaAcr };
71
+ }
48
72
  export function resolveAdmin(input) {
49
73
  return {
50
74
  enabled: input?.enabled ?? false,
@@ -86,9 +110,23 @@ export function toSeconds(value, fallback) {
86
110
  export function defineConfig(config) {
87
111
  return configProvider.create(async (app) => {
88
112
  const AdapterClass = await config.adapter.resolver(app);
89
- const jwks = config.jwks.source === 'managed'
90
- ? await generateJwks(config.jwks.algorithm ?? 'RS256')
91
- : { keys: config.jwks.keys ?? [] };
113
+ let jwks;
114
+ if (config.jwks.source === 'managed') {
115
+ const alg = config.jwks.algorithm ?? 'RS256';
116
+ if (config.jwks.store) {
117
+ // keystore persistido em arquivo: chaves sobrevivem a restarts + rotacionáveis.
118
+ const storePath = app.makePath(config.jwks.store);
119
+ const store = await ensureKeystore(storePath, alg);
120
+ jwks = { keys: store.keys };
121
+ }
122
+ else {
123
+ // managed efêmero: uma chave nova por boot.
124
+ jwks = await generateJwks(alg);
125
+ }
126
+ }
127
+ else {
128
+ jwks = { keys: config.jwks.keys ?? [] };
129
+ }
92
130
  return {
93
131
  issuer: config.issuer,
94
132
  AdapterClass,
@@ -117,11 +155,16 @@ export function defineConfig(config) {
117
155
  patIntrospectionSecret: config.patIntrospectionSecret,
118
156
  rateLimit: resolveRateLimit(config.rateLimit),
119
157
  lockout: resolveLockout(config.lockout),
158
+ notifications: resolveNotifications(config.notifications),
120
159
  mail: config.mail,
121
160
  audit: config.audit,
122
161
  mfaIssuer: config.mfaIssuer ?? 'AuthKit',
123
162
  webauthn: resolveWebauthn(config.issuer, config.mfaIssuer ?? 'AuthKit', config.webauthn),
124
163
  dynamicRegistration: resolveDynamicRegistration(config.dynamicRegistration),
164
+ deviceFlow: resolveDeviceFlow(config.deviceFlow),
165
+ dpop: resolveDpop(config.dpop),
166
+ par: resolvePar(config.par),
167
+ stepUp: resolveStepUp(config.stepUp),
125
168
  admin: resolveAdmin(config.admin),
126
169
  messages: resolveMessages(config.i18n),
127
170
  locale: config.i18n?.locale ?? 'pt-BR',
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Funções puras de verificação para `node ace authkit:doctor`. Não dependem do
3
+ * Ace nem do container — recebem objetos simples para serem testáveis em
4
+ * isolamento. O comando `authkit:doctor` só coleta o ambiente e imprime os
5
+ * resultados destas funções.
6
+ */
7
+ export type FindingLevel = 'ok' | 'warn' | 'error';
8
+ export interface Finding {
9
+ level: FindingLevel;
10
+ message: string;
11
+ }
12
+ /** Entrada mínima necessária para rodar os checks (subconjunto da config AuthKit). */
13
+ export interface DoctorInput {
14
+ /** A config `authkit` resolvida pelo container, ou null se não resolver. */
15
+ authkitConfig: Record<string, any> | null;
16
+ /** A config `session` do app (config('session')), ou null se ausente. */
17
+ sessionConfig: Record<string, any> | null;
18
+ /** Resultado de tentar resolver cada peer (true = importável). */
19
+ peers: {
20
+ session: boolean;
21
+ shield: boolean;
22
+ ally: boolean;
23
+ limiter: boolean;
24
+ };
25
+ }
26
+ /** config('authkit') resolve? */
27
+ export declare function checkConfigResolves(input: DoctorInput): Finding;
28
+ /** issuer é uma URL válida e seu pathname casa com o mountPath. */
29
+ export declare function checkIssuer(input: DoctorInput): Finding[];
30
+ /** Pelo menos um client com redirectUris. */
31
+ export declare function checkClients(input: DoctorInput): Finding;
32
+ /** accountStore presente + quais capacidades implementa. */
33
+ export declare function checkAccountStore(input: DoctorInput): Finding[];
34
+ /** session provider configurado + warn se cookie store com tokenSets grandes. */
35
+ export declare function checkSession(input: DoctorInput): Finding[];
36
+ /** Hint de exceções de CSRF do shield para o mountPath. */
37
+ export declare function checkShield(input: DoctorInput): Finding;
38
+ /** ally só é necessário quando social está configurado. */
39
+ export declare function checkAlly(input: DoctorInput): Finding;
40
+ /** rateLimit ligado mas @adonisjs/limiter ausente → warn. */
41
+ export declare function checkRateLimit(input: DoctorInput): Finding;
42
+ /** admin.enabled mas sem roles → warn. */
43
+ export declare function checkAdmin(input: DoctorInput): Finding | null;
44
+ /** webauthn rpId deve casar com o host do issuer. */
45
+ export declare function checkWebauthn(input: DoctorInput): Finding | null;
46
+ /** info sobre rotação quando jwks é managed. */
47
+ export declare function checkJwks(input: DoctorInput): Finding | null;
48
+ /** Roda todos os checks e devolve a lista plana de findings. */
49
+ export declare function runAllChecks(input: DoctorInput): Finding[];
50
+ /** Há algum finding de nível 'error'? (define o exit code). */
51
+ export declare function hasErrors(findings: Finding[]): boolean;