@dudousxd/adonis-authkit-server 0.1.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 (174) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +137 -0
  3. package/build/assets/grafana/authkit-dashboard.json +118 -0
  4. package/build/commands/commands.json +30 -0
  5. package/build/commands/configure.d.ts +2 -0
  6. package/build/commands/configure.js +42 -0
  7. package/build/commands/eject.d.ts +11 -0
  8. package/build/commands/eject.js +96 -0
  9. package/build/commands/main.d.ts +12 -0
  10. package/build/commands/main.js +38 -0
  11. package/build/commands/ui_preset.d.ts +4 -0
  12. package/build/commands/ui_preset.js +32 -0
  13. package/build/database/migrations/make_authkit_oidc_table.d.ts +6 -0
  14. package/build/database/migrations/make_authkit_oidc_table.js +19 -0
  15. package/build/host/views/account/login.edge +29 -0
  16. package/build/host/views/account/mfa.edge +151 -0
  17. package/build/host/views/account/tokens.edge +70 -0
  18. package/build/host/views/admin/audit.edge +72 -0
  19. package/build/host/views/admin/clients.edge +51 -0
  20. package/build/host/views/admin/dashboard.edge +58 -0
  21. package/build/host/views/admin/users.edge +76 -0
  22. package/build/host/views/consent.edge +19 -0
  23. package/build/host/views/forgot.edge +30 -0
  24. package/build/host/views/login.edge +91 -0
  25. package/build/host/views/mfa-challenge.edge +88 -0
  26. package/build/host/views/reset.edge +29 -0
  27. package/build/host/views/signup.edge +44 -0
  28. package/build/host/views/verify-email.edge +16 -0
  29. package/build/index.d.ts +42 -0
  30. package/build/index.js +28 -0
  31. package/build/providers/authkit_server_provider.d.ts +19 -0
  32. package/build/providers/authkit_server_provider.js +81 -0
  33. package/build/src/accounts/account_store.d.ts +136 -0
  34. package/build/src/accounts/account_store.js +1 -0
  35. package/build/src/accounts/lucid_account_store.d.ts +75 -0
  36. package/build/src/accounts/lucid_account_store.js +396 -0
  37. package/build/src/adapters/adapter_contract.d.ts +18 -0
  38. package/build/src/adapters/adapter_contract.js +1 -0
  39. package/build/src/adapters/database_adapter.d.ts +15 -0
  40. package/build/src/adapters/database_adapter.js +63 -0
  41. package/build/src/adapters/factory.d.ts +30 -0
  42. package/build/src/adapters/factory.js +43 -0
  43. package/build/src/adapters/redis_adapter.d.ts +16 -0
  44. package/build/src/adapters/redis_adapter.js +95 -0
  45. package/build/src/audit/audit_sink.d.ts +54 -0
  46. package/build/src/audit/audit_sink.js +1 -0
  47. package/build/src/audit/lucid_audit_sink.d.ts +10 -0
  48. package/build/src/audit/lucid_audit_sink.js +60 -0
  49. package/build/src/controllers/oidc_callback_controller.d.ts +22 -0
  50. package/build/src/controllers/oidc_callback_controller.js +33 -0
  51. package/build/src/define_config.d.ts +261 -0
  52. package/build/src/define_config.js +115 -0
  53. package/build/src/host/account_lockout.d.ts +86 -0
  54. package/build/src/host/account_lockout.js +185 -0
  55. package/build/src/host/augmentations.d.ts +1 -0
  56. package/build/src/host/augmentations.js +1 -0
  57. package/build/src/host/branding.d.ts +17 -0
  58. package/build/src/host/branding.js +8 -0
  59. package/build/src/host/controllers/account_mfa_controller.d.ts +30 -0
  60. package/build/src/host/controllers/account_mfa_controller.js +157 -0
  61. package/build/src/host/controllers/account_session_controller.d.ts +7 -0
  62. package/build/src/host/controllers/account_session_controller.js +50 -0
  63. package/build/src/host/controllers/account_tokens_controller.d.ts +7 -0
  64. package/build/src/host/controllers/account_tokens_controller.js +55 -0
  65. package/build/src/host/controllers/admin/admin_audit_controller.d.ts +10 -0
  66. package/build/src/host/controllers/admin/admin_audit_controller.js +56 -0
  67. package/build/src/host/controllers/admin/admin_clients_controller.d.ts +10 -0
  68. package/build/src/host/controllers/admin/admin_clients_controller.js +24 -0
  69. package/build/src/host/controllers/admin/admin_dashboard_controller.d.ts +10 -0
  70. package/build/src/host/controllers/admin/admin_dashboard_controller.js +27 -0
  71. package/build/src/host/controllers/admin/admin_users_controller.d.ts +11 -0
  72. package/build/src/host/controllers/admin/admin_users_controller.js +53 -0
  73. package/build/src/host/controllers/interaction_controller.d.ts +44 -0
  74. package/build/src/host/controllers/interaction_controller.js +304 -0
  75. package/build/src/host/controllers/pat_introspection_controller.d.ts +22 -0
  76. package/build/src/host/controllers/pat_introspection_controller.js +46 -0
  77. package/build/src/host/controllers/registration_controller.d.ts +18 -0
  78. package/build/src/host/controllers/registration_controller.js +169 -0
  79. package/build/src/host/controllers/social_controller.d.ts +8 -0
  80. package/build/src/host/controllers/social_controller.js +82 -0
  81. package/build/src/host/default_mailer.d.ts +39 -0
  82. package/build/src/host/default_mailer.js +141 -0
  83. package/build/src/host/email_templates.d.ts +35 -0
  84. package/build/src/host/email_templates.js +66 -0
  85. package/build/src/host/i18n.d.ts +178 -0
  86. package/build/src/host/i18n.js +208 -0
  87. package/build/src/host/middleware/account_auth.d.ts +7 -0
  88. package/build/src/host/middleware/account_auth.js +11 -0
  89. package/build/src/host/rate_limit.d.ts +32 -0
  90. package/build/src/host/rate_limit.js +87 -0
  91. package/build/src/host/register_auth_host.d.ts +41 -0
  92. package/build/src/host/register_auth_host.js +133 -0
  93. package/build/src/host/renderers/edge_renderer.d.ts +3 -0
  94. package/build/src/host/renderers/edge_renderer.js +29 -0
  95. package/build/src/host/renderers/inertia_renderer.d.ts +5 -0
  96. package/build/src/host/renderers/inertia_renderer.js +26 -0
  97. package/build/src/host/validators.d.ts +39 -0
  98. package/build/src/host/validators.js +13 -0
  99. package/build/src/keys/jwks_manager.d.ts +6 -0
  100. package/build/src/keys/jwks_manager.js +11 -0
  101. package/build/src/mixins/with_audit_log.d.ts +19 -0
  102. package/build/src/mixins/with_audit_log.js +41 -0
  103. package/build/src/mixins/with_auth_user.d.ts +18 -0
  104. package/build/src/mixins/with_auth_user.js +39 -0
  105. package/build/src/mixins/with_credentials.d.ts +20 -0
  106. package/build/src/mixins/with_credentials.js +29 -0
  107. package/build/src/mixins/with_mfa.d.ts +31 -0
  108. package/build/src/mixins/with_mfa.js +39 -0
  109. package/build/src/mixins/with_personal_access_token.d.ts +19 -0
  110. package/build/src/mixins/with_personal_access_token.js +44 -0
  111. package/build/src/mixins/with_provider_identity.d.ts +20 -0
  112. package/build/src/mixins/with_provider_identity.js +32 -0
  113. package/build/src/mixins/with_webauthn_credential.d.ts +37 -0
  114. package/build/src/mixins/with_webauthn_credential.js +49 -0
  115. package/build/src/observability/metrics_controller.d.ts +5 -0
  116. package/build/src/observability/metrics_controller.js +24 -0
  117. package/build/src/observability/metrics_service.d.ts +2 -0
  118. package/build/src/observability/metrics_service.js +7 -0
  119. package/build/src/observability/otel_recorder.d.ts +10 -0
  120. package/build/src/observability/otel_recorder.js +59 -0
  121. package/build/src/observability/wire_provider_events.d.ts +12 -0
  122. package/build/src/observability/wire_provider_events.js +19 -0
  123. package/build/src/pat/lucid_pat_store.d.ts +6 -0
  124. package/build/src/pat/lucid_pat_store.js +62 -0
  125. package/build/src/pat/pat_store.d.ts +31 -0
  126. package/build/src/pat/pat_store.js +1 -0
  127. package/build/src/pat/pat_tokens.d.ts +4 -0
  128. package/build/src/pat/pat_tokens.js +9 -0
  129. package/build/src/provider/build_provider.d.ts +8 -0
  130. package/build/src/provider/build_provider.js +101 -0
  131. package/build/src/provider/interaction_actions.d.ts +21 -0
  132. package/build/src/provider/interaction_actions.js +32 -0
  133. package/build/src/provider/oidc_service.d.ts +17 -0
  134. package/build/src/provider/oidc_service.js +84 -0
  135. package/build/src/provider/token_exchange.d.ts +15 -0
  136. package/build/src/provider/token_exchange.js +72 -0
  137. package/build/src/register_routes.d.ts +16 -0
  138. package/build/src/register_routes.js +21 -0
  139. package/build/stubs/config/authkit.stub +29 -0
  140. package/build/stubs/main.d.ts +1 -0
  141. package/build/stubs/main.js +2 -0
  142. package/build/stubs/models/auth_user.stub +13 -0
  143. package/build/stubs/ui/edge/views/consent.edge +13 -0
  144. package/build/stubs/ui/edge/views/login.edge +19 -0
  145. package/build/stubs/ui/react/components/auth_shell.tsx +67 -0
  146. package/build/stubs/ui/react/pages/account/login.tsx +56 -0
  147. package/build/stubs/ui/react/pages/account/mfa.tsx +132 -0
  148. package/build/stubs/ui/react/pages/account/tokens.tsx +88 -0
  149. package/build/stubs/ui/react/pages/consent.tsx +39 -0
  150. package/build/stubs/ui/react/pages/forgot.tsx +44 -0
  151. package/build/stubs/ui/react/pages/login.tsx +171 -0
  152. package/build/stubs/ui/react/pages/mfa-challenge.tsx +72 -0
  153. package/build/stubs/ui/react/pages/reset.tsx +58 -0
  154. package/build/stubs/ui/react/pages/signup.tsx +78 -0
  155. package/build/stubs/ui/react/pages/verify-email.tsx +24 -0
  156. package/build/types.d.ts +7 -0
  157. package/build/types.js +1 -0
  158. package/package.json +108 -0
  159. package/stubs/config/authkit.stub +29 -0
  160. package/stubs/main.ts +2 -0
  161. package/stubs/models/auth_user.stub +13 -0
  162. package/stubs/ui/edge/views/consent.edge +13 -0
  163. package/stubs/ui/edge/views/login.edge +19 -0
  164. package/stubs/ui/react/components/auth_shell.tsx +67 -0
  165. package/stubs/ui/react/pages/account/login.tsx +56 -0
  166. package/stubs/ui/react/pages/account/mfa.tsx +132 -0
  167. package/stubs/ui/react/pages/account/tokens.tsx +88 -0
  168. package/stubs/ui/react/pages/consent.tsx +39 -0
  169. package/stubs/ui/react/pages/forgot.tsx +44 -0
  170. package/stubs/ui/react/pages/login.tsx +171 -0
  171. package/stubs/ui/react/pages/mfa-challenge.tsx +72 -0
  172. package/stubs/ui/react/pages/reset.tsx +58 -0
  173. package/stubs/ui/react/pages/signup.tsx +78 -0
  174. package/stubs/ui/react/pages/verify-email.tsx +24 -0
@@ -0,0 +1,37 @@
1
+ import { BaseModel } from '@adonisjs/lucid/orm';
2
+ import type { NormalizeConstructor } from '@adonisjs/core/types/helpers';
3
+ import { DateTime } from 'luxon';
4
+ /**
5
+ * Instância composta pelo mixin {@link withWebauthnCredential}. Representa uma
6
+ * credencial WebAuthn / passkey ligada a uma conta (`accountId`). Uma conta pode
7
+ * ter várias credenciais (vários dispositivos/authenticators).
8
+ *
9
+ * O `id` é o credential id (base64url) devolvido pelo authenticator — é a chave
10
+ * primária. `publicKey` é a chave pública COSE em base64url (texto). `counter` é
11
+ * o contador do signature counter (anti-replay), atualizado a cada autenticação.
12
+ */
13
+ export interface WebauthnCredentialRow {
14
+ /** Conta dona desta credencial (→ auth.users). */
15
+ accountId: string;
16
+ /** Chave pública COSE em base64url (texto). */
17
+ publicKey: string;
18
+ /** Signature counter (anti-replay); atualizado a cada autenticação. */
19
+ counter: number;
20
+ /** Transports reportados (ex.: ['internal','hybrid']); null = desconhecido. */
21
+ transports: string[] | null;
22
+ /** Rótulo legível opcional (ex.: nome do dispositivo). */
23
+ label: string | null;
24
+ createdAt: DateTime;
25
+ updatedAt: DateTime;
26
+ }
27
+ export type WebauthnCredentialClass<Model extends NormalizeConstructor<typeof BaseModel>> = Model & {
28
+ new (...args: any[]): WebauthnCredentialRow;
29
+ };
30
+ /**
31
+ * Mixin de credenciais WebAuthn / passkey. Adiciona as colunas
32
+ * `account_id`, `public_key`, `counter`, `transports`, `label` + timestamps ao
33
+ * model. Segue o mesmo padrão de {@link withProviderIdentity}: o host compõe um
34
+ * model dedicado (`compose(BaseModel, withWebauthnCredential())`) e passa para o
35
+ * `lucidAccountStore` via a opção `webauthnCredentialModel`.
36
+ */
37
+ export declare function withWebauthnCredential(): <Model extends NormalizeConstructor<typeof BaseModel>>(superclass: Model) => WebauthnCredentialClass<Model>;
@@ -0,0 +1,49 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { column } from '@adonisjs/lucid/orm';
8
+ /**
9
+ * Mixin de credenciais WebAuthn / passkey. Adiciona as colunas
10
+ * `account_id`, `public_key`, `counter`, `transports`, `label` + timestamps ao
11
+ * model. Segue o mesmo padrão de {@link withProviderIdentity}: o host compõe um
12
+ * model dedicado (`compose(BaseModel, withWebauthnCredential())`) e passa para o
13
+ * `lucidAccountStore` via a opção `webauthnCredentialModel`.
14
+ */
15
+ export function withWebauthnCredential() {
16
+ return (superclass) => {
17
+ class WebauthnCredentialMixin extends superclass {
18
+ }
19
+ __decorate([
20
+ column()
21
+ ], WebauthnCredentialMixin.prototype, "accountId", void 0);
22
+ __decorate([
23
+ column()
24
+ ], WebauthnCredentialMixin.prototype, "publicKey", void 0);
25
+ __decorate([
26
+ column()
27
+ ], WebauthnCredentialMixin.prototype, "counter", void 0);
28
+ __decorate([
29
+ column({
30
+ prepare: (value) => (value && value.length ? JSON.stringify(value) : null),
31
+ consume: (value) => {
32
+ if (value === null || value === undefined)
33
+ return null;
34
+ return Array.isArray(value) ? value : JSON.parse(value);
35
+ },
36
+ })
37
+ ], WebauthnCredentialMixin.prototype, "transports", void 0);
38
+ __decorate([
39
+ column()
40
+ ], WebauthnCredentialMixin.prototype, "label", void 0);
41
+ __decorate([
42
+ column.dateTime({ autoCreate: true })
43
+ ], WebauthnCredentialMixin.prototype, "createdAt", void 0);
44
+ __decorate([
45
+ column.dateTime({ autoCreate: true, autoUpdate: true })
46
+ ], WebauthnCredentialMixin.prototype, "updatedAt", void 0);
47
+ return WebauthnCredentialMixin;
48
+ };
49
+ }
@@ -0,0 +1,5 @@
1
+ import type { HttpContext } from '@adonisjs/core/http';
2
+ export default class MetricsController {
3
+ json(ctx: HttpContext): Promise<any>;
4
+ dashboard(ctx: HttpContext): Promise<void>;
5
+ }
@@ -0,0 +1,24 @@
1
+ function renderDashboardHtml(snapshot) {
2
+ const counters = Object.entries(snapshot.counters)
3
+ .map(([k, v]) => `<tr><td>${k}</td><td>${v}</td></tr>`)
4
+ .join('');
5
+ const histograms = Object.entries(snapshot.histograms)
6
+ .map(([k, h]) => `<tr><td>${k}</td><td>${h.count}</td><td>${h.sum}</td><td>${h.min}</td><td>${h.max}</td></tr>`)
7
+ .join('');
8
+ return `<!doctype html><html lang="pt-br"><head><meta charset="utf-8"><meta http-equiv="refresh" content="5"><title>AuthKit — Métricas</title>
9
+ <style>body{font-family:system-ui,sans-serif;margin:2rem;color:#111}h1{font-size:1.2rem}table{border-collapse:collapse;margin:1rem 0;width:100%}th,td{border:1px solid #ddd;padding:.4rem .6rem;text-align:left;font-size:.9rem}th{background:#f5f5f5}</style>
10
+ </head><body><h1>AuthKit — Métricas</h1><p>Atualizado: ${snapshot.updatedAt ? new Date(snapshot.updatedAt).toISOString() : '—'}</p>
11
+ <h2>Counters</h2><table><thead><tr><th>Métrica</th><th>Total</th></tr></thead><tbody>${counters || '<tr><td colspan="2">—</td></tr>'}</tbody></table>
12
+ <h2>Histograms</h2><table><thead><tr><th>Métrica</th><th>Count</th><th>Sum</th><th>Min</th><th>Max</th></tr></thead><tbody>${histograms || '<tr><td colspan="5">—</td></tr>'}</tbody></table>
13
+ </body></html>`;
14
+ }
15
+ export default class MetricsController {
16
+ async json(ctx) {
17
+ const recorder = await ctx.containerResolver.make('authkit.metrics');
18
+ return recorder.snapshot();
19
+ }
20
+ async dashboard(ctx) {
21
+ const recorder = await ctx.containerResolver.make('authkit.metrics');
22
+ return ctx.response.type('html').send(renderDashboardHtml(recorder.snapshot()));
23
+ }
24
+ }
@@ -0,0 +1,2 @@
1
+ import { type MetricsRecorder, type ObservabilityConfig } from '@dudousxd/adonis-authkit-core';
2
+ export declare function createMetricsRecorder(observability: ObservabilityConfig, meterName: string): Promise<MetricsRecorder>;
@@ -0,0 +1,7 @@
1
+ import { NoopRecorder } from '@dudousxd/adonis-authkit-core';
2
+ import { OtelRecorder } from './otel_recorder.js';
3
+ export async function createMetricsRecorder(observability, meterName) {
4
+ if (!observability.metrics)
5
+ return new NoopRecorder();
6
+ return OtelRecorder.create(meterName);
7
+ }
@@ -0,0 +1,10 @@
1
+ import { type AuthkitMetricName, type MetricsRecorder, type MetricsSnapshot } from '@dudousxd/adonis-authkit-core';
2
+ /** Recorder que (best-effort) emite via @opentelemetry/api e SEMPRE agrega em memória. */
3
+ export declare class OtelRecorder implements MetricsRecorder {
4
+ #private;
5
+ private constructor();
6
+ static create(name: string): Promise<OtelRecorder>;
7
+ increment(name: AuthkitMetricName, attributes?: Record<string, string | number>): void;
8
+ record(name: AuthkitMetricName, value: number, attributes?: Record<string, string | number>): void;
9
+ snapshot(): MetricsSnapshot;
10
+ }
@@ -0,0 +1,59 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ import { InMemorySnapshot, } from '@dudousxd/adonis-authkit-core';
10
+ /** Recorder que (best-effort) emite via @opentelemetry/api e SEMPRE agrega em memória. */
11
+ export class OtelRecorder {
12
+ #snapshot = new InMemorySnapshot();
13
+ #counters = new Map();
14
+ #histograms = new Map();
15
+ #meter;
16
+ constructor(meter) {
17
+ this.#meter = meter;
18
+ }
19
+ static async create(name) {
20
+ let meter = null;
21
+ try {
22
+ // Specifier dinâmico (não-estático) para que o TS não tente resolver o
23
+ // peer opcional @opentelemetry/api em tempo de compilação. Em runtime,
24
+ // se o pacote não estiver instalado, caímos no catch e ficamos no-op.
25
+ const moduleName = '@opentelemetry/api';
26
+ const otel = await import(__rewriteRelativeImportExtension(moduleName));
27
+ meter = otel.metrics.getMeter(name);
28
+ }
29
+ catch {
30
+ meter = null;
31
+ }
32
+ return new OtelRecorder(meter);
33
+ }
34
+ increment(name, attributes) {
35
+ this.#snapshot.bump(name);
36
+ if (!this.#meter)
37
+ return;
38
+ let c = this.#counters.get(name);
39
+ if (!c) {
40
+ c = this.#meter.createCounter(name);
41
+ this.#counters.set(name, c);
42
+ }
43
+ c.add(1, attributes);
44
+ }
45
+ record(name, value, attributes) {
46
+ this.#snapshot.observe(name, value);
47
+ if (!this.#meter)
48
+ return;
49
+ let h = this.#histograms.get(name);
50
+ if (!h) {
51
+ h = this.#meter.createHistogram(name);
52
+ this.#histograms.set(name, h);
53
+ }
54
+ h.record(value, attributes);
55
+ }
56
+ snapshot() {
57
+ return this.#snapshot.read();
58
+ }
59
+ }
@@ -0,0 +1,12 @@
1
+ import { type MetricsRecorder } from '@dudousxd/adonis-authkit-core';
2
+ /**
3
+ * Liga eventos do oidc-provider aos counters do recorder. Usa só eventos reais
4
+ * do v9 (verificados em node_modules/oidc-provider/lib):
5
+ * - `grant.success` (actions/token.js) -> loginSuccess
6
+ * - `server_error` (shared/error_handler.js) -> loginFailure
7
+ * - `access_token.saved` (models/base_model.js: <kind>.saved) -> tokenIssued
8
+ * - `access_token.issued` (models/base_model.js: <kind>.issued) -> tokenIssued
9
+ * - `refresh_token.saved` (models/base_model.js: <kind>.saved) -> refreshRotated
10
+ * - `grant.revoked` (helpers/revoke.js) -> grantRevoked
11
+ */
12
+ export declare function wireProviderEvents(provider: any, recorder: MetricsRecorder): void;
@@ -0,0 +1,19 @@
1
+ import { AUTHKIT_METRICS } from '@dudousxd/adonis-authkit-core';
2
+ /**
3
+ * Liga eventos do oidc-provider aos counters do recorder. Usa só eventos reais
4
+ * do v9 (verificados em node_modules/oidc-provider/lib):
5
+ * - `grant.success` (actions/token.js) -> loginSuccess
6
+ * - `server_error` (shared/error_handler.js) -> loginFailure
7
+ * - `access_token.saved` (models/base_model.js: <kind>.saved) -> tokenIssued
8
+ * - `access_token.issued` (models/base_model.js: <kind>.issued) -> tokenIssued
9
+ * - `refresh_token.saved` (models/base_model.js: <kind>.saved) -> refreshRotated
10
+ * - `grant.revoked` (helpers/revoke.js) -> grantRevoked
11
+ */
12
+ export function wireProviderEvents(provider, recorder) {
13
+ provider.on('grant.success', () => recorder.increment(AUTHKIT_METRICS.loginSuccess));
14
+ provider.on('server_error', () => recorder.increment(AUTHKIT_METRICS.loginFailure));
15
+ provider.on('access_token.saved', () => recorder.increment(AUTHKIT_METRICS.tokenIssued));
16
+ provider.on('access_token.issued', () => recorder.increment(AUTHKIT_METRICS.tokenIssued));
17
+ provider.on('refresh_token.saved', () => recorder.increment(AUTHKIT_METRICS.refreshRotated));
18
+ provider.on('grant.revoked', () => recorder.increment(AUTHKIT_METRICS.grantRevoked));
19
+ }
@@ -0,0 +1,6 @@
1
+ import type { PatStore } from './pat_store.js';
2
+ /**
3
+ * Implementação default do {@link PatStore} sobre um model Lucid composto de
4
+ * `withPersonalAccessToken()`. A coluna DB `user_id` é mapeada de `accountId`.
5
+ */
6
+ export declare function lucidPatStore(Model: any): PatStore;
@@ -0,0 +1,62 @@
1
+ import { DateTime } from 'luxon';
2
+ import { generatePatToken, hashPatToken } from './pat_tokens.js';
3
+ /**
4
+ * Implementação default do {@link PatStore} sobre um model Lucid composto de
5
+ * `withPersonalAccessToken()`. A coluna DB `user_id` é mapeada de `accountId`.
6
+ */
7
+ export function lucidPatStore(Model) {
8
+ const toRecord = (row) => ({
9
+ id: row.id,
10
+ name: row.name,
11
+ scopes: row.scopes ?? [],
12
+ audience: row.audience ?? null,
13
+ createdAt: row.createdAt ? row.createdAt.toISO() : '',
14
+ lastUsedAt: row.lastUsedAt ? row.lastUsedAt.toISO() : null,
15
+ expiresAt: row.expiresAt ? row.expiresAt.toISO() : null,
16
+ });
17
+ return {
18
+ async issue(input) {
19
+ const token = generatePatToken();
20
+ const row = await Model.create({
21
+ userId: input.accountId,
22
+ name: input.name,
23
+ tokenHash: hashPatToken(token),
24
+ scopes: input.scopes ?? [],
25
+ audience: input.audience ?? null,
26
+ expiresAt: input.expiresInDays
27
+ ? DateTime.now().plus({ days: input.expiresInDays })
28
+ : null,
29
+ });
30
+ return { token, pat: toRecord(row) };
31
+ },
32
+ async listForAccount(accountId) {
33
+ const rows = await Model.query()
34
+ .where('user_id', accountId)
35
+ .orderBy('created_at', 'desc');
36
+ return rows.map(toRecord);
37
+ },
38
+ async revoke(accountId, id) {
39
+ const row = await Model.query().where('id', id).where('user_id', accountId).first();
40
+ if (!row)
41
+ return false;
42
+ await row.delete();
43
+ return true;
44
+ },
45
+ async findActiveByToken(token) {
46
+ const hash = hashPatToken(token);
47
+ const row = await Model.query().where('token_hash', hash).first();
48
+ if (!row)
49
+ return null;
50
+ if (row.expiresAt && row.expiresAt < DateTime.now())
51
+ return null;
52
+ row.lastUsedAt = DateTime.now();
53
+ await row.save();
54
+ return {
55
+ accountId: row.userId,
56
+ scopes: row.scopes ?? [],
57
+ audience: row.audience ?? null,
58
+ exp: row.expiresAt ? Math.floor(row.expiresAt.toSeconds()) : null,
59
+ };
60
+ },
61
+ };
62
+ }
@@ -0,0 +1,31 @@
1
+ /** DTO seguro do PAT — sem o hash do token. */
2
+ export interface PatRecord {
3
+ id: string;
4
+ name: string;
5
+ scopes: string[];
6
+ audience: string | null;
7
+ createdAt: string;
8
+ lastUsedAt: string | null;
9
+ expiresAt: string | null;
10
+ }
11
+ export interface IssuePatInput {
12
+ accountId: string;
13
+ name: string;
14
+ scopes?: string[];
15
+ audience?: string | null;
16
+ expiresInDays?: number | null;
17
+ }
18
+ export interface PatStore {
19
+ issue(input: IssuePatInput): Promise<{
20
+ token: string;
21
+ pat: PatRecord;
22
+ }>;
23
+ listForAccount(accountId: string): Promise<PatRecord[]>;
24
+ revoke(accountId: string, id: string): Promise<boolean>;
25
+ findActiveByToken(token: string): Promise<{
26
+ accountId: string;
27
+ scopes: string[];
28
+ audience: string | null;
29
+ exp: number | null;
30
+ } | null>;
31
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ /** Gera um Personal Access Token opaco no formato `pat_<base64url>`. */
2
+ export declare function generatePatToken(): string;
3
+ /** SHA-256 (hex) do token — é o que se persiste; o token cru nunca é guardado. */
4
+ export declare function hashPatToken(token: string): string;
@@ -0,0 +1,9 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ /** Gera um Personal Access Token opaco no formato `pat_<base64url>`. */
3
+ export function generatePatToken() {
4
+ return 'pat_' + randomBytes(32).toString('base64url');
5
+ }
6
+ /** SHA-256 (hex) do token — é o que se persiste; o token cru nunca é guardado. */
7
+ export function hashPatToken(token) {
8
+ return createHash('sha256').update(token).digest('hex');
9
+ }
@@ -0,0 +1,8 @@
1
+ import * as oidc from 'oidc-provider';
2
+ import type { ResolvedServerConfig } from '../define_config.js';
3
+ export interface BuildProviderOptions {
4
+ /** APP_KEY do consumidor; usado p/ derivar cookies.keys se não houver. */
5
+ appKey: string;
6
+ findAccount: (ctx: any, sub: string, token?: any) => Promise<any>;
7
+ }
8
+ export declare function buildProvider(config: ResolvedServerConfig, options: BuildProviderOptions): oidc.Provider;
@@ -0,0 +1,101 @@
1
+ import * as oidc from 'oidc-provider';
2
+ export function buildProvider(config, options) {
3
+ const cookieKeys = config.cookieKeys.length ? config.cookieKeys : [options.appKey];
4
+ // OIDC Dynamic Client Registration (RFC 7591/7592). Só montamos as chaves de feature
5
+ // quando habilitado — desligado (default), o oidc-provider não expõe o endpoint /reg.
6
+ // O `initialAccessToken`: string => valida o bearer contra esse valor estático; ausente
7
+ // => `false` (registro ABERTO; raramente desejável em prod). Clients registrados aqui
8
+ // são persistidos automaticamente pelo MESMO AdapterClass, coexistindo com os estáticos.
9
+ const dynReg = config.dynamicRegistration;
10
+ const registrationFeatures = dynReg.enabled
11
+ ? {
12
+ registration: {
13
+ enabled: true,
14
+ initialAccessToken: dynReg.initialAccessToken ?? false,
15
+ },
16
+ ...(dynReg.management ? { registrationManagement: { enabled: true } } : {}),
17
+ }
18
+ : {};
19
+ const provider = new oidc.Provider(config.issuer, {
20
+ adapter: config.AdapterClass,
21
+ clients: config.clients.map((c) => ({
22
+ client_id: c.clientId,
23
+ client_secret: c.clientSecret,
24
+ redirect_uris: c.redirectUris,
25
+ post_logout_redirect_uris: c.postLogoutRedirectUris ?? [],
26
+ grant_types: c.grants ?? ['authorization_code', 'refresh_token'],
27
+ response_types: (c.grants ?? ['authorization_code']).includes('authorization_code')
28
+ ? ['code']
29
+ : [],
30
+ token_endpoint_auth_method: c.tokenEndpointAuthMethod ?? (c.clientSecret ? 'client_secret_basic' : 'none'),
31
+ // OIDC Back-Channel Logout: só envia as chaves quando o client as declara,
32
+ // p/ não forçar metadata vazio em clients que não usam o recurso.
33
+ ...(c.backchannelLogoutUri ? { backchannel_logout_uri: c.backchannelLogoutUri } : {}),
34
+ ...(c.backchannelLogoutSessionRequired !== undefined
35
+ ? { backchannel_logout_session_required: c.backchannelLogoutSessionRequired }
36
+ : {}),
37
+ })),
38
+ findAccount: options.findAccount,
39
+ jwks: config.jwks,
40
+ cookies: {
41
+ keys: cookieKeys,
42
+ // O oidc-provider define o path do cookie _interaction como
43
+ // `/auth/interaction/<uid>` (exato) via `...cookieOptions` que
44
+ // SOBRESCREVE o path explícito. Sem `path: '/'` o cookie ficaria
45
+ // restrito ao path exato da tela de interaction e NÃO seria enviado
46
+ // pelo browser no POST de subpaths como `.../consent` ou `.../login`.
47
+ // Definir path: '/' na família "short" garante que os cookies de
48
+ // interaction e resume sejam enviados em todos os endpoints OIDC/auth.
49
+ short: { path: '/' },
50
+ long: { path: '/' },
51
+ },
52
+ // conformIdTokenClaims=false: caso contrário (default v9), no fluxo Authorization Code
53
+ // — onde também é emitido um Access Token — o oidc-provider mascara o ID token para
54
+ // apenas o escopo `openid` (só `sub`), removendo email/profile/roles do ID token.
55
+ // O client valida o ID TOKEN, então precisamos que as claims configuradas cheguem nele.
56
+ conformIdTokenClaims: false,
57
+ // A claim de roles globais é atrelada ao escopo `profile` (sempre concedido pelos
58
+ // scopes padrão do client: openid profile email offline_access). Assim as roles chegam
59
+ // no ID token sem exigir um escopo `roles` customizado. Mantemos também o mapeamento
60
+ // do escopo `roles` para quem optar por solicitá-lo explicitamente.
61
+ claims: {
62
+ openid: ['sub'],
63
+ profile: ['name', 'picture', config.globalRolesClaim],
64
+ email: ['email', 'email_verified'],
65
+ roles: [config.globalRolesClaim],
66
+ },
67
+ scopes: ['openid', 'profile', 'email', 'offline_access', 'roles'],
68
+ // Permite que o parametro `audience` (hint de intencao do client, ex.: 'advisor')
69
+ // sobreviva ao authorize e fique disponivel em interactionDetails().params.audience.
70
+ extraParams: ['audience'],
71
+ pkce: { methods: ['S256'], required: () => true },
72
+ rotateRefreshToken: true,
73
+ features: {
74
+ devInteractions: { enabled: false },
75
+ rpInitiatedLogout: { enabled: true },
76
+ // OIDC Back-Channel Logout: o oidc-provider POSTa um logout_token para o
77
+ // `backchannel_logout_uri` de cada RP quando a sessão/grant é encerrada.
78
+ backchannelLogout: { enabled: true },
79
+ revocation: { enabled: true },
80
+ introspection: { enabled: true },
81
+ ...registrationFeatures,
82
+ },
83
+ ttl: {
84
+ AccessToken: config.ttl.accessToken,
85
+ RefreshToken: config.ttl.refreshToken,
86
+ IdToken: config.ttl.idToken,
87
+ Session: config.ttl.session,
88
+ Interaction: 3600,
89
+ Grant: config.ttl.refreshToken,
90
+ },
91
+ interactions: {
92
+ // Telas de interaction são servidas pelo CONSUMIDOR na RAIZ (fora do koa-mount do
93
+ // provider sob /oidc), evitando colisão com o catch-all /oidc/*. O provider redireciona
94
+ // o browser para cá; o consumidor chama interactionFinished, que devolve ao resume do
95
+ // provider em /oidc/auth/:uid (já corretamente prefixado pelo koa-mount).
96
+ url: (_ctx, interaction) => `/auth/interaction/${interaction.uid}`,
97
+ },
98
+ });
99
+ provider.proxy = true;
100
+ return provider;
101
+ }
@@ -0,0 +1,21 @@
1
+ import type { HttpContext } from '@adonisjs/core/http';
2
+ export interface InteractionDeps {
3
+ verifyCredentials?: (email: string, password: string) => Promise<{
4
+ id: string;
5
+ } | null>;
6
+ }
7
+ export interface InteractionActions {
8
+ details(ctx: HttpContext): Promise<any>;
9
+ login(ctx: HttpContext, input: {
10
+ email: string;
11
+ password: string;
12
+ }): Promise<{
13
+ ok: boolean;
14
+ }>;
15
+ completeLogin(ctx: HttpContext, accountId: string): Promise<{
16
+ ok: boolean;
17
+ }>;
18
+ consent(ctx: HttpContext): Promise<unknown>;
19
+ }
20
+ /** Lógica de interaction (login/consent) sobre o provider. Testável com um provider fake. */
21
+ export declare function createInteractionActions(provider: any, deps: InteractionDeps): InteractionActions;
@@ -0,0 +1,32 @@
1
+ /** Lógica de interaction (login/consent) sobre o provider. Testável com um provider fake. */
2
+ export function createInteractionActions(provider, deps) {
3
+ return {
4
+ async details(ctx) {
5
+ return provider.interactionDetails(ctx.request.request, ctx.response.response);
6
+ },
7
+ async login(ctx, { email, password }) {
8
+ if (!deps.verifyCredentials) {
9
+ throw new Error('authkit: defina `verifyCredentials` no config/authkit.ts para usar o login das interactions.');
10
+ }
11
+ const account = await deps.verifyCredentials(email, password);
12
+ if (!account)
13
+ return { ok: false };
14
+ await provider.interactionFinished(ctx.request.request, ctx.response.response, { login: { accountId: account.id } }, { mergeWithLastSubmission: false });
15
+ return { ok: true };
16
+ },
17
+ async completeLogin(ctx, accountId) {
18
+ await provider.interactionFinished(ctx.request.request, ctx.response.response, { login: { accountId } }, { mergeWithLastSubmission: false });
19
+ return { ok: true };
20
+ },
21
+ async consent(ctx) {
22
+ const details = await provider.interactionDetails(ctx.request.request, ctx.response.response);
23
+ const grant = new provider.Grant({
24
+ accountId: details.session.accountId,
25
+ clientId: details.params.client_id,
26
+ });
27
+ grant.addOIDCScope(String(details.params.scope ?? 'openid'));
28
+ const grantId = await grant.save();
29
+ return provider.interactionFinished(ctx.request.request, ctx.response.response, { consent: { grantId } }, { mergeWithLastSubmission: true });
30
+ },
31
+ };
32
+ }
@@ -0,0 +1,17 @@
1
+ import { type MetricsRecorder } from '@dudousxd/adonis-authkit-core';
2
+ import type { ResolvedServerConfig } from '../define_config.js';
3
+ import { buildProvider } from './build_provider.js';
4
+ import { type InteractionActions } from './interaction_actions.js';
5
+ export declare class OidcService {
6
+ #private;
7
+ readonly provider: ReturnType<typeof buildProvider>;
8
+ readonly callback: (req: any, res: any) => void;
9
+ /** Pathname do issuer sem barra final (ex.: `/oidc`). Vazio quando montado na raiz. */
10
+ readonly mountPath: string;
11
+ readonly recorder: MetricsRecorder;
12
+ readonly interactions: InteractionActions;
13
+ get config(): ResolvedServerConfig;
14
+ constructor(config: ResolvedServerConfig, appKey: string, recorder?: MetricsRecorder);
15
+ /** Verifica client_id + client_secret contra os clients da config (p/ endpoints custom como introspecção de PAT). */
16
+ verifyClientCredentials(clientId: string, clientSecret: string): boolean;
17
+ }
@@ -0,0 +1,84 @@
1
+ import { timingSafeEqual } from 'node:crypto';
2
+ import { NoopRecorder } from '@dudousxd/adonis-authkit-core';
3
+ import Koa from 'koa';
4
+ import mount from 'koa-mount';
5
+ import { wireProviderEvents } from '../observability/wire_provider_events.js';
6
+ import { buildProvider } from './build_provider.js';
7
+ import { createInteractionActions } from './interaction_actions.js';
8
+ import { registerTokenExchange } from './token_exchange.js';
9
+ export class OidcService {
10
+ provider;
11
+ callback;
12
+ /** Pathname do issuer sem barra final (ex.: `/oidc`). Vazio quando montado na raiz. */
13
+ mountPath;
14
+ recorder;
15
+ interactions;
16
+ #clients;
17
+ #config;
18
+ get config() {
19
+ return this.#config;
20
+ }
21
+ constructor(config, appKey, recorder = new NoopRecorder()) {
22
+ this.#config = config;
23
+ this.#clients = config.clients ?? [];
24
+ this.recorder = recorder;
25
+ this.provider = buildProvider(config, {
26
+ appKey,
27
+ findAccount: async (_ctx, sub) => {
28
+ const user = await config.findAccount(sub);
29
+ if (!user)
30
+ return undefined;
31
+ return {
32
+ accountId: user.id,
33
+ claims: async (_use, _scope) => ({
34
+ sub: user.id,
35
+ email: user.email,
36
+ email_verified: true,
37
+ name: user.name,
38
+ picture: user.avatarUrl,
39
+ [config.globalRolesClaim]: user.globalRoles ?? [],
40
+ }),
41
+ };
42
+ },
43
+ });
44
+ wireProviderEvents(this.provider, recorder);
45
+ registerTokenExchange(this.provider, {
46
+ findAccount: config.findAccount,
47
+ globalRolesClaim: config.globalRolesClaim,
48
+ audit: config.audit,
49
+ });
50
+ // Quando o issuer tem um path (ex.: http://host/oidc), o provider precisa ser
51
+ // MONTADO sob esse path via koa-mount. Isso faz o oidc-provider gerar URLs de
52
+ // discovery e redirects de resume/interaction CORRETAMENTE prefixados (ex.:
53
+ // /oidc/auth, /oidc/jwks). Apenas remover o prefixo de req.url (abordagem antiga)
54
+ // fazia o provider se enxergar na raiz e anunciar URLs sem o /oidc.
55
+ // Para issuer na raiz, o mountPath é vazio e usamos o callback Node direto.
56
+ this.mountPath = new URL(this.provider.issuer).pathname.replace(/\/+$/, '');
57
+ if (this.mountPath && this.mountPath !== '/') {
58
+ const koa = new Koa();
59
+ // O app externo PRECISA herdar as Keygrip keys do provider. Sob koa-mount as
60
+ // requisições do provider rodam no contexto do app EXTERNO, então `ctx.cookies`
61
+ // usa as keys deste app. Sem isso, os cookies (_interaction, _session) seriam
62
+ // assinados/lidos de forma inconsistente entre o fluxo via mount (authorize) e as
63
+ // chamadas diretas `interactionDetails`/`interactionFinished` (que usam o contexto
64
+ // do provider), quebrando o "interaction session id cookie not found".
65
+ koa.keys = this.provider.keys;
66
+ koa.proxy = this.provider.proxy;
67
+ koa.use(mount(this.mountPath, this.provider));
68
+ this.callback = koa.callback();
69
+ }
70
+ else {
71
+ this.callback = this.provider.callback();
72
+ }
73
+ this.interactions = createInteractionActions(this.provider, { verifyCredentials: config.verifyCredentials });
74
+ }
75
+ /** Verifica client_id + client_secret contra os clients da config (p/ endpoints custom como introspecção de PAT). */
76
+ verifyClientCredentials(clientId, clientSecret) {
77
+ const client = this.#clients.find((c) => c.clientId === clientId);
78
+ if (!client || !client.clientSecret)
79
+ return false;
80
+ const a = Buffer.from(client.clientSecret);
81
+ const b = Buffer.from(clientSecret);
82
+ return a.length === b.length && timingSafeEqual(a, b);
83
+ }
84
+ }
@@ -0,0 +1,15 @@
1
+ import type { AuditSink } from '../audit/audit_sink.js';
2
+ export interface TokenExchangeAccount {
3
+ id: string;
4
+ email?: string;
5
+ name?: string;
6
+ globalRoles?: string[];
7
+ }
8
+ export interface TokenExchangeDeps {
9
+ findAccount: (sub: string) => Promise<TokenExchangeAccount | null>;
10
+ globalRolesClaim: string;
11
+ adminRole?: string;
12
+ /** Sink de auditoria (best-effort). Quando presente, registra `impersonation`. */
13
+ audit?: AuditSink;
14
+ }
15
+ export declare function registerTokenExchange(provider: any, deps: TokenExchangeDeps): void;