@dudousxd/adonis-authkit-server 0.2.0 → 0.4.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 (61) 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/client_form.edge +83 -0
  10. package/build/host/views/admin/clients.edge +68 -3
  11. package/build/host/views/admin/sessions.edge +89 -0
  12. package/build/host/views/admin/users.edge +1 -0
  13. package/build/host/views/mfa-challenge.edge +29 -23
  14. package/build/index.d.ts +4 -3
  15. package/build/index.js +2 -2
  16. package/build/src/accounts/account_store.d.ts +46 -1
  17. package/build/src/accounts/account_store.js +4 -0
  18. package/build/src/accounts/lucid_store/core.d.ts +5 -4
  19. package/build/src/accounts/lucid_store/core.js +67 -2
  20. package/build/src/adapters/adapter_contract.d.ts +29 -0
  21. package/build/src/adapters/database_adapter.d.ts +12 -1
  22. package/build/src/adapters/database_adapter.js +24 -0
  23. package/build/src/adapters/redis_adapter.d.ts +14 -1
  24. package/build/src/adapters/redis_adapter.js +35 -0
  25. package/build/src/audit/audit_sink.d.ts +1 -1
  26. package/build/src/define_config.d.ts +102 -0
  27. package/build/src/define_config.js +46 -3
  28. package/build/src/doctor/checks.d.ts +51 -0
  29. package/build/src/doctor/checks.js +231 -0
  30. package/build/src/host/admin_clients_service.d.ts +65 -0
  31. package/build/src/host/admin_clients_service.js +143 -0
  32. package/build/src/host/admin_sessions_service.d.ts +63 -0
  33. package/build/src/host/admin_sessions_service.js +127 -0
  34. package/build/src/host/controllers/account_security_controller.d.ts +16 -0
  35. package/build/src/host/controllers/account_security_controller.js +119 -0
  36. package/build/src/host/controllers/account_session_controller.js +2 -1
  37. package/build/src/host/controllers/admin/admin_clients_controller.d.ts +17 -3
  38. package/build/src/host/controllers/admin/admin_clients_controller.js +158 -4
  39. package/build/src/host/controllers/admin/admin_sessions_controller.d.ts +14 -0
  40. package/build/src/host/controllers/admin/admin_sessions_controller.js +64 -0
  41. package/build/src/host/controllers/interaction_controller.d.ts +11 -0
  42. package/build/src/host/controllers/interaction_controller.js +49 -10
  43. package/build/src/host/default_mailer.d.ts +17 -0
  44. package/build/src/host/default_mailer.js +51 -0
  45. package/build/src/host/i18n.d.ts +80 -0
  46. package/build/src/host/i18n.js +86 -1
  47. package/build/src/host/login_notify.d.ts +20 -0
  48. package/build/src/host/login_notify.js +71 -0
  49. package/build/src/host/register_auth_host.js +20 -0
  50. package/build/src/host/validators.d.ts +32 -0
  51. package/build/src/host/validators.js +14 -0
  52. package/build/src/keys/keystore.d.ts +43 -0
  53. package/build/src/keys/keystore.js +74 -0
  54. package/build/src/provider/build_provider.js +23 -0
  55. package/build/src/provider/device_sources.d.ts +6 -0
  56. package/build/src/provider/device_sources.js +65 -0
  57. package/build/src/provider/interaction_actions.d.ts +6 -1
  58. package/build/src/provider/interaction_actions.js +9 -2
  59. package/build/src/provider/oidc_service.d.ts +15 -0
  60. package/build/src/provider/oidc_service.js +27 -0
  61. package/package.json +2 -2
@@ -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;
@@ -0,0 +1,231 @@
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
+ /** Type guard estrutural: o store expõe um método (capacidade presente). */
8
+ function has(store, method) {
9
+ return !!store && typeof store[method] === 'function';
10
+ }
11
+ /** config('authkit') resolve? */
12
+ export function checkConfigResolves(input) {
13
+ if (!input.authkitConfig) {
14
+ return {
15
+ level: 'error',
16
+ message: "config('authkit') não resolveu — config/authkit.ts está ausente ou inválido.",
17
+ };
18
+ }
19
+ return { level: 'ok', message: "config('authkit') resolvido." };
20
+ }
21
+ /** issuer é uma URL válida e seu pathname casa com o mountPath. */
22
+ export function checkIssuer(input) {
23
+ const cfg = input.authkitConfig;
24
+ if (!cfg)
25
+ return [];
26
+ const issuer = cfg.issuer;
27
+ const mountPath = cfg.mountPath ?? '/oidc';
28
+ if (typeof issuer !== 'string' || issuer.length === 0) {
29
+ return [{ level: 'error', message: 'issuer ausente na config.' }];
30
+ }
31
+ let url;
32
+ try {
33
+ url = new URL(issuer);
34
+ }
35
+ catch {
36
+ return [{ level: 'error', message: `issuer não é uma URL válida: "${issuer}".` }];
37
+ }
38
+ const findings = [{ level: 'ok', message: `issuer válido: ${url.origin}${url.pathname}` }];
39
+ const normalize = (p) => (p.endsWith('/') ? p.slice(0, -1) : p) || '/';
40
+ if (normalize(url.pathname) !== normalize(mountPath)) {
41
+ findings.push({
42
+ level: 'warn',
43
+ message: `O pathname do issuer ("${url.pathname}") difere do mountPath ("${mountPath}"). As rotas OIDC podem não casar com as URLs anunciadas no discovery.`,
44
+ });
45
+ }
46
+ return findings;
47
+ }
48
+ /** Pelo menos um client com redirectUris. */
49
+ export function checkClients(input) {
50
+ const cfg = input.authkitConfig;
51
+ if (!cfg)
52
+ return { level: 'error', message: 'sem config para validar clients.' };
53
+ const clients = Array.isArray(cfg.clients) ? cfg.clients : [];
54
+ if (clients.length === 0) {
55
+ return { level: 'error', message: 'nenhum client configurado em `clients`.' };
56
+ }
57
+ const withRedirects = clients.filter((c) => Array.isArray(c?.redirectUris) && c.redirectUris.length > 0);
58
+ if (withRedirects.length === 0) {
59
+ return {
60
+ level: 'error',
61
+ message: `${clients.length} client(s) configurado(s), mas nenhum tem redirectUris.`,
62
+ };
63
+ }
64
+ return { level: 'ok', message: `${withRedirects.length}/${clients.length} client(s) com redirectUris.` };
65
+ }
66
+ /** accountStore presente + quais capacidades implementa. */
67
+ export function checkAccountStore(input) {
68
+ const cfg = input.authkitConfig;
69
+ if (!cfg)
70
+ return [];
71
+ const store = cfg.accountStore;
72
+ if (!store) {
73
+ return [{ level: 'error', message: 'accountStore ausente — obrigatório.' }];
74
+ }
75
+ const findings = [{ level: 'ok', message: 'accountStore presente.' }];
76
+ const caps = [];
77
+ if (has(store, 'getMfaState'))
78
+ caps.push('MFA');
79
+ if (has(store, 'listPasskeys'))
80
+ caps.push('passkeys/WebAuthn');
81
+ if (has(store, 'findByProviderIdentity'))
82
+ caps.push('account-linking');
83
+ if (has(store, 'changePassword'))
84
+ caps.push('account-security');
85
+ findings.push({
86
+ level: 'ok',
87
+ message: caps.length
88
+ ? `Capacidades opcionais: ${caps.join(', ')}.`
89
+ : 'Apenas o núcleo do accountStore (sem MFA/passkeys/linking/security).',
90
+ });
91
+ return findings;
92
+ }
93
+ /** session provider configurado + warn se cookie store com tokenSets grandes. */
94
+ export function checkSession(input) {
95
+ if (!input.peers.session) {
96
+ return [
97
+ {
98
+ level: 'error',
99
+ message: '@adonisjs/session não é importável — instale-o (peer obrigatório).',
100
+ },
101
+ ];
102
+ }
103
+ if (!input.sessionConfig) {
104
+ return [{ level: 'warn', message: "config('session') ausente — o provider de sessão pode não estar configurado." }];
105
+ }
106
+ const findings = [{ level: 'ok', message: 'provider de sessão configurado.' }];
107
+ const driver = input.sessionConfig.store ?? input.sessionConfig.driver;
108
+ if (driver === 'cookie') {
109
+ findings.push({
110
+ level: 'warn',
111
+ message: 'session store = cookie: token sets grandes podem estourar o limite de 4KB do cookie. Prefira `redis`/`file` em produção.',
112
+ });
113
+ }
114
+ return findings;
115
+ }
116
+ /** Hint de exceções de CSRF do shield para o mountPath. */
117
+ export function checkShield(input) {
118
+ if (!input.peers.shield) {
119
+ return { level: 'error', message: '@adonisjs/shield não é importável — instale-o (peer obrigatório).' };
120
+ }
121
+ const mountPath = input.authkitConfig?.mountPath ?? '/oidc';
122
+ return {
123
+ level: 'warn',
124
+ message: `Garanta que as rotas POST do IdP sob "${mountPath}" estejam nas exceções de CSRF do shield (ex.: endpoint /token), senão chamadas server-to-server falham.`,
125
+ };
126
+ }
127
+ /** ally só é necessário quando social está configurado. */
128
+ export function checkAlly(input) {
129
+ const social = input.authkitConfig?.social;
130
+ const usesSocial = !!social && (Array.isArray(social.providers) ? social.providers.length > 0 : Object.keys(social).length > 0);
131
+ if (!usesSocial) {
132
+ return { level: 'ok', message: 'login social não configurado — @adonisjs/ally é opcional.' };
133
+ }
134
+ if (!input.peers.ally) {
135
+ return { level: 'error', message: 'login social configurado, mas @adonisjs/ally não é importável.' };
136
+ }
137
+ return { level: 'ok', message: 'login social configurado e @adonisjs/ally disponível.' };
138
+ }
139
+ /** rateLimit ligado mas @adonisjs/limiter ausente → warn. */
140
+ export function checkRateLimit(input) {
141
+ const cfg = input.authkitConfig;
142
+ const rateLimit = cfg?.rateLimit;
143
+ const enabled = rateLimit === undefined ? true : rateLimit?.enabled !== false;
144
+ if (!enabled) {
145
+ return { level: 'ok', message: 'rate-limiting desligado por config.' };
146
+ }
147
+ if (!input.peers.limiter) {
148
+ return {
149
+ level: 'warn',
150
+ message: 'rate-limiting está ligado (default), mas @adonisjs/limiter não é importável — vira no-op (sem proteção anti-brute-force).',
151
+ };
152
+ }
153
+ return { level: 'ok', message: 'rate-limiting ligado e @adonisjs/limiter disponível.' };
154
+ }
155
+ /** admin.enabled mas sem roles → warn. */
156
+ export function checkAdmin(input) {
157
+ const admin = input.authkitConfig?.admin;
158
+ if (!admin || admin.enabled !== true)
159
+ return null;
160
+ const roles = Array.isArray(admin.roles) ? admin.roles : [];
161
+ if (roles.length === 0) {
162
+ return {
163
+ level: 'warn',
164
+ message: 'console admin ligado, mas sem `admin.roles` — ninguém terá acesso (default ["ADMIN"] não foi resolvido aqui).',
165
+ };
166
+ }
167
+ return { level: 'ok', message: `console admin ligado para roles: ${roles.join(', ')}.` };
168
+ }
169
+ /** webauthn rpId deve casar com o host do issuer. */
170
+ export function checkWebauthn(input) {
171
+ const cfg = input.authkitConfig;
172
+ const webauthn = cfg?.webauthn;
173
+ if (!webauthn || !webauthn.rpId)
174
+ return null;
175
+ const issuer = cfg.issuer;
176
+ if (typeof issuer !== 'string')
177
+ return null;
178
+ let host;
179
+ try {
180
+ host = new URL(issuer).hostname;
181
+ }
182
+ catch {
183
+ return null;
184
+ }
185
+ if (webauthn.rpId !== host) {
186
+ return {
187
+ level: 'warn',
188
+ message: `webauthn.rpId ("${webauthn.rpId}") difere do host do issuer ("${host}") — as passkeys não validarão no browser.`,
189
+ };
190
+ }
191
+ return { level: 'ok', message: `webauthn.rpId casa com o host do issuer (${host}).` };
192
+ }
193
+ /** info sobre rotação quando jwks é managed. */
194
+ export function checkJwks(input) {
195
+ const jwks = input.authkitConfig?.jwks;
196
+ if (!jwks)
197
+ return null;
198
+ if (jwks.source === 'managed') {
199
+ return {
200
+ level: 'ok',
201
+ message: 'jwks managed — rotacione as chaves de assinatura com `node ace authkit:rotate-keys` (use --store para persistir entre boots).',
202
+ };
203
+ }
204
+ return { level: 'ok', message: 'jwks fornecido inline (source=jwks).' };
205
+ }
206
+ /** Roda todos os checks e devolve a lista plana de findings. */
207
+ export function runAllChecks(input) {
208
+ const findings = [];
209
+ findings.push(checkConfigResolves(input));
210
+ findings.push(...checkIssuer(input));
211
+ findings.push(checkClients(input));
212
+ findings.push(...checkAccountStore(input));
213
+ findings.push(...checkSession(input));
214
+ findings.push(checkShield(input));
215
+ findings.push(checkAlly(input));
216
+ findings.push(checkRateLimit(input));
217
+ const admin = checkAdmin(input);
218
+ if (admin)
219
+ findings.push(admin);
220
+ const webauthn = checkWebauthn(input);
221
+ if (webauthn)
222
+ findings.push(webauthn);
223
+ const jwks = checkJwks(input);
224
+ if (jwks)
225
+ findings.push(jwks);
226
+ return findings;
227
+ }
228
+ /** Há algum finding de nível 'error'? (define o exit code). */
229
+ export function hasErrors(findings) {
230
+ return findings.some((f) => f.level === 'error');
231
+ }
@@ -0,0 +1,65 @@
1
+ import type { OidcService } from '../provider/oidc_service.js';
2
+ /** Métodos de autenticação no token endpoint suportados pelo formulário admin. */
3
+ export type TokenEndpointAuthMethod = 'client_secret_basic' | 'client_secret_post' | 'none';
4
+ /** Entrada normalizada de um client gerenciável (vinda do formulário admin). */
5
+ export interface ClientInput {
6
+ clientId?: string;
7
+ redirectUris: string[];
8
+ postLogoutRedirectUris: string[];
9
+ grantTypes: string[];
10
+ tokenEndpointAuthMethod: TokenEndpointAuthMethod;
11
+ }
12
+ /** Client persistido, apresentado ao console admin. */
13
+ export interface AdminClient {
14
+ clientId: string;
15
+ confidential: boolean;
16
+ grants: string[];
17
+ redirectUris: string[];
18
+ postLogoutRedirectUris: string[];
19
+ tokenEndpointAuthMethod: string;
20
+ }
21
+ /** Resultado de uma criação: o client + o secret em claro (mostrado UMA vez). */
22
+ export interface CreatedClient {
23
+ clientId: string;
24
+ /** undefined para public clients (sem secret). */
25
+ clientSecret?: string;
26
+ }
27
+ /**
28
+ * Serviço de CRUD de clients OIDC persistidos no adapter (model `Client`),
29
+ * usado pelo console admin. Encapsula:
30
+ * - a montagem do payload NA FORMA EXATA que o oidc-provider espera (snake_case,
31
+ * igual ao que o registro dinâmico — RFC 7591 — grava);
32
+ * - a invalidação do cache de clients dinâmicos do provider após cada escrita
33
+ * (ver {@link OidcService.evictDynamicClientCache});
34
+ * - a enumeração via a capacidade opcional `listClients` do adapter.
35
+ */
36
+ export declare class AdminClientsService {
37
+ #private;
38
+ private oidc;
39
+ constructor(oidc: OidcService);
40
+ /** Indica se o adapter suporta enumeração (capacidade opcional). */
41
+ get canList(): boolean;
42
+ /** Lista os clients persistidos (vazio quando o adapter não enumera; cheque canList). */
43
+ list(): Promise<AdminClient[]>;
44
+ /** Lê um client persistido pelo client_id (undefined quando não existe). */
45
+ find(clientId: string): Promise<AdminClient | undefined>;
46
+ /**
47
+ * Cria um client. Gera client_id quando não informado; gera client_secret
48
+ * para clients confidenciais (auth method != 'none'). Retorna o secret em
49
+ * claro UMA vez (não é recuperável depois — o payload guarda o mesmo valor).
50
+ */
51
+ create(input: ClientInput): Promise<CreatedClient>;
52
+ /**
53
+ * Atualiza metadata editável (redirect/post-logout URIs, grants, auth method)
54
+ * PRESERVANDO o client_secret existente. Lança se o client não existe.
55
+ */
56
+ update(clientId: string, input: ClientInput): Promise<void>;
57
+ /**
58
+ * Regenera o client_secret de um client confidencial, preservando o resto da
59
+ * metadata. Retorna o novo secret em claro (mostrado UMA vez). Lança se o
60
+ * client não existe ou é public (auth method 'none').
61
+ */
62
+ regenerateSecret(clientId: string): Promise<string>;
63
+ /** Remove um client persistido e invalida o cache. */
64
+ delete(clientId: string): Promise<void>;
65
+ }
@@ -0,0 +1,143 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ /** Métodos públicos (sem segredo) — espelham os "non-secret" do oidc-provider. */
3
+ const PUBLIC_AUTH_METHODS = new Set(['none']);
4
+ /** Gera um identificador opaco no estilo do oidc-provider (~43 chars base64url). */
5
+ function randomId() {
6
+ return randomBytes(32).toString('base64url');
7
+ }
8
+ /**
9
+ * Serviço de CRUD de clients OIDC persistidos no adapter (model `Client`),
10
+ * usado pelo console admin. Encapsula:
11
+ * - a montagem do payload NA FORMA EXATA que o oidc-provider espera (snake_case,
12
+ * igual ao que o registro dinâmico — RFC 7591 — grava);
13
+ * - a invalidação do cache de clients dinâmicos do provider após cada escrita
14
+ * (ver {@link OidcService.evictDynamicClientCache});
15
+ * - a enumeração via a capacidade opcional `listClients` do adapter.
16
+ */
17
+ export class AdminClientsService {
18
+ oidc;
19
+ #adapter;
20
+ constructor(oidc) {
21
+ this.oidc = oidc;
22
+ // O AdapterClass é o MESMO que o provider usa; instanciamos o model 'Client'
23
+ // para ler/gravar os mesmos artefatos que o oidc-provider persiste.
24
+ this.#adapter = new oidc.config.AdapterClass('Client');
25
+ }
26
+ /** Indica se o adapter suporta enumeração (capacidade opcional). */
27
+ get canList() {
28
+ return typeof this.#adapter.list === 'function' || typeof this.#adapter.listClients === 'function';
29
+ }
30
+ /** Lista os clients persistidos (vazio quando o adapter não enumera; cheque canList). */
31
+ async list() {
32
+ // Prefere a enumeração genérica `list`; cai no `listClients` legado se for o
33
+ // único disponível (adapters customizados antigos).
34
+ if (this.#adapter.list) {
35
+ const rows = await this.#adapter.list();
36
+ return rows.map((r) => this.#present({ clientId: r.id, payload: r.payload }));
37
+ }
38
+ if (this.#adapter.listClients) {
39
+ const rows = await this.#adapter.listClients();
40
+ return rows.map((r) => this.#present(r));
41
+ }
42
+ return [];
43
+ }
44
+ /** Lê um client persistido pelo client_id (undefined quando não existe). */
45
+ async find(clientId) {
46
+ const payload = await this.#adapter.find(clientId);
47
+ if (!payload)
48
+ return undefined;
49
+ return this.#present({ clientId, payload: payload });
50
+ }
51
+ /**
52
+ * Cria um client. Gera client_id quando não informado; gera client_secret
53
+ * para clients confidenciais (auth method != 'none'). Retorna o secret em
54
+ * claro UMA vez (não é recuperável depois — o payload guarda o mesmo valor).
55
+ */
56
+ async create(input) {
57
+ const clientId = (input.clientId ?? '').trim() || randomId();
58
+ const confidential = !PUBLIC_AUTH_METHODS.has(input.tokenEndpointAuthMethod);
59
+ const clientSecret = confidential ? randomId() : undefined;
60
+ const payload = this.#buildPayload(clientId, input, clientSecret);
61
+ // expiresIn 0 => sem TTL (clients são permanentes, como no registro dinâmico).
62
+ await this.#adapter.upsert(clientId, payload, 0);
63
+ await this.oidc.evictDynamicClientCache();
64
+ return { clientId, clientSecret };
65
+ }
66
+ /**
67
+ * Atualiza metadata editável (redirect/post-logout URIs, grants, auth method)
68
+ * PRESERVANDO o client_secret existente. Lança se o client não existe.
69
+ */
70
+ async update(clientId, input) {
71
+ const existing = await this.#adapter.find(clientId);
72
+ if (!existing)
73
+ throw new Error(`client ${clientId} não encontrado`);
74
+ const previousSecret = existing.client_secret;
75
+ const confidential = !PUBLIC_AUTH_METHODS.has(input.tokenEndpointAuthMethod);
76
+ // Mantém o secret atual se continua confidencial; se virou public, remove-o.
77
+ const clientSecret = confidential ? (previousSecret ?? randomId()) : undefined;
78
+ const payload = this.#buildPayload(clientId, input, clientSecret);
79
+ await this.#adapter.upsert(clientId, payload, 0);
80
+ await this.oidc.evictDynamicClientCache();
81
+ }
82
+ /**
83
+ * Regenera o client_secret de um client confidencial, preservando o resto da
84
+ * metadata. Retorna o novo secret em claro (mostrado UMA vez). Lança se o
85
+ * client não existe ou é public (auth method 'none').
86
+ */
87
+ async regenerateSecret(clientId) {
88
+ const existing = await this.#adapter.find(clientId);
89
+ if (!existing)
90
+ throw new Error(`client ${clientId} não encontrado`);
91
+ const authMethod = existing.token_endpoint_auth_method ?? 'client_secret_basic';
92
+ if (PUBLIC_AUTH_METHODS.has(authMethod)) {
93
+ throw new Error(`client ${clientId} é public — não possui secret`);
94
+ }
95
+ const clientSecret = randomId();
96
+ const payload = { ...existing, client_secret: clientSecret };
97
+ await this.#adapter.upsert(clientId, payload, 0);
98
+ await this.oidc.evictDynamicClientCache();
99
+ return clientSecret;
100
+ }
101
+ /** Remove um client persistido e invalida o cache. */
102
+ async delete(clientId) {
103
+ await this.#adapter.destroy(clientId);
104
+ await this.oidc.evictDynamicClientCache();
105
+ }
106
+ /**
107
+ * Monta o payload na forma snake_case que o oidc-provider espera/persiste —
108
+ * verificada contra o que o registro dinâmico (RFC 7591) grava. As chaves de
109
+ * metadata não enviadas (subject_type, id_token_signed_response_alg, etc.) são
110
+ * preenchidas pelo Schema do provider ao construir o Client em `find`.
111
+ */
112
+ #buildPayload(clientId, input, clientSecret) {
113
+ const grantTypes = input.grantTypes.length
114
+ ? input.grantTypes
115
+ : ['authorization_code', 'refresh_token'];
116
+ // response_types: 'code' quando o fluxo de authorization_code está presente.
117
+ const responseTypes = grantTypes.includes('authorization_code') ? ['code'] : [];
118
+ const payload = {
119
+ client_id: clientId,
120
+ redirect_uris: input.redirectUris,
121
+ post_logout_redirect_uris: input.postLogoutRedirectUris,
122
+ grant_types: grantTypes,
123
+ response_types: responseTypes,
124
+ token_endpoint_auth_method: input.tokenEndpointAuthMethod,
125
+ };
126
+ if (clientSecret)
127
+ payload.client_secret = clientSecret;
128
+ return payload;
129
+ }
130
+ /** Projeta um payload persistido para a forma exibida no console admin. */
131
+ #present(row) {
132
+ const p = row.payload;
133
+ const authMethod = p.token_endpoint_auth_method ?? 'client_secret_basic';
134
+ return {
135
+ clientId: row.clientId,
136
+ confidential: !!p.client_secret,
137
+ grants: p.grant_types ?? ['authorization_code', 'refresh_token'],
138
+ redirectUris: p.redirect_uris ?? [],
139
+ postLogoutRedirectUris: p.post_logout_redirect_uris ?? [],
140
+ tokenEndpointAuthMethod: authMethod,
141
+ };
142
+ }
143
+ }
@@ -0,0 +1,63 @@
1
+ import type { OidcService } from '../provider/oidc_service.js';
2
+ /** Uma sessão ativa do IdP (login do usuário no provider), apresentada ao admin. */
3
+ export interface AdminSession {
4
+ /** Id do artefato `Session` no adapter. */
5
+ id: string;
6
+ accountId: string;
7
+ /** Epoch (segundos) do login, quando presente no payload. */
8
+ loginTs?: number;
9
+ /** Métodos de autenticação registrados na sessão (amr), quando presentes. */
10
+ amr?: string[];
11
+ }
12
+ /** Um grant (autorização concedida a um client), com a contagem de tokens vivos. */
13
+ export interface AdminGrant {
14
+ /** Id do artefato `Grant` no adapter (== grantId dos tokens). */
15
+ id: string;
16
+ accountId: string;
17
+ clientId?: string;
18
+ /** Tokens de acesso vivos que referenciam este grant. */
19
+ accessTokens: number;
20
+ /** Refresh tokens vivos que referenciam este grant. */
21
+ refreshTokens: number;
22
+ }
23
+ /** Resultado de uma revogação em massa das sessões/grants de uma conta. */
24
+ export interface RevokeResult {
25
+ sessions: number;
26
+ grants: number;
27
+ accessTokens: number;
28
+ refreshTokens: number;
29
+ }
30
+ /**
31
+ * Serviço de inspeção/revogação das SESSÕES e GRANTS ativos de uma conta,
32
+ * persistidos pelo oidc-provider via o MESMO `AdapterClass` (mesmo padrão do
33
+ * {@link AdminClientsService}). Encapsula:
34
+ * - a enumeração via a capacidade opcional `list` do adapter (degrada quando
35
+ * ausente, igual ao CRUD de clients);
36
+ * - a destruição das sessões + grants da conta. Destruir um grant CASCATEIA a
37
+ * invalidação dos tokens no oidc-provider: os consumidores de access/refresh
38
+ * token carregam `Grant.find(token.grantId)` e lançam `InvalidToken('grant not
39
+ * found')` quando o grant some (verificado em oidc-provider v9). Mesmo assim,
40
+ * por garantia (belt-and-braces), também destruímos as linhas de AT/RT que
41
+ * referenciam os grants revogados quando o adapter enumera.
42
+ */
43
+ export declare class AdminSessionsService {
44
+ #private;
45
+ constructor(oidc: OidcService);
46
+ /** Indica se o adapter suporta enumeração (capacidade opcional). */
47
+ get canList(): boolean;
48
+ /** Lista as sessões ativas da conta (vazio quando o adapter não enumera). */
49
+ listSessions(accountId: string): Promise<AdminSession[]>;
50
+ /**
51
+ * Lista os grants da conta, com a contagem de access/refresh tokens vivos que
52
+ * referenciam cada grant (`payload.grantId`). As contagens são baratas (uma
53
+ * enumeração de cada model token), feitas só quando o adapter enumera.
54
+ */
55
+ listGrants(accountId: string): Promise<AdminGrant[]>;
56
+ /**
57
+ * Revoga TODAS as sessões e grants da conta. Destruir os grants já invalida os
58
+ * tokens (cascata via `grant not found`); ainda assim, quando o adapter enumera,
59
+ * destruímos explicitamente as linhas de AT/RT desses grants (belt-and-braces),
60
+ * deixando o store limpo. Retorna as contagens do que foi removido.
61
+ */
62
+ revokeAll(accountId: string): Promise<RevokeResult>;
63
+ }