@adonis-agora/authkit-server 0.34.0 → 0.35.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.
- package/build/index.d.ts +1 -0
- package/build/index.js +5 -0
- package/build/providers/authkit_server_provider.js +21 -13
- package/build/src/define_config.d.ts +31 -1
- package/build/src/define_config.js +11 -1
- package/build/src/events/dispatcher.d.ts +31 -0
- package/build/src/events/dispatcher.js +41 -1
- package/build/src/host/account_api/account_api_controller.js +2 -0
- package/build/src/host/adonis_auth_sync.d.ts +27 -0
- package/build/src/host/adonis_auth_sync.js +47 -0
- package/build/src/host/adonis_auth_user_provider.d.ts +38 -0
- package/build/src/host/adonis_auth_user_provider.js +74 -0
- package/build/src/host/app_key.d.ts +18 -0
- package/build/src/host/app_key.js +28 -0
- package/build/src/host/controllers/account_security_controller.js +2 -0
- package/build/src/host/controllers/account_session_controller.js +8 -0
- package/build/src/host/ui-dist/assets/{index-CRWcmsex.js → index-CCvSqT4s.js} +1 -1
- package/build/src/host/ui-dist/index.html +1 -1
- package/build/src/observability/telescope/data_providers.js +4 -1
- package/build/src/observability/telescope/extension.js +3 -1
- package/package.json +17 -12
package/build/index.d.ts
CHANGED
|
@@ -113,3 +113,4 @@ export { getAccountId, hasAccountSession, consoleLoginUrl, } from './src/host/co
|
|
|
113
113
|
export { ACCOUNT_SESSION_KEY } from './src/host/middleware/account_auth.js';
|
|
114
114
|
export { SUDO_SESSION_KEY, SUDO_MODE_DEFAULTS, requireSudo, isSudoActive, markSudo, resolveEffectiveSudoMode, } from './src/host/sudo_mode.js';
|
|
115
115
|
export type { SudoModeSetting, ResolvedSudoModeSetting, } from './src/host/sudo_mode.js';
|
|
116
|
+
export { authkitUserProvider } from './src/host/adonis_auth_user_provider.js';
|
package/build/index.js
CHANGED
|
@@ -74,3 +74,8 @@ export { getAccountId, hasAccountSession, consoleLoginUrl, } from './src/host/co
|
|
|
74
74
|
export { ACCOUNT_SESSION_KEY } from './src/host/middleware/account_auth.js';
|
|
75
75
|
// Sudo mode — helpers for host controllers that require step-up authentication.
|
|
76
76
|
export { SUDO_SESSION_KEY, SUDO_MODE_DEFAULTS, requireSudo, isSudoActive, markSudo, resolveEffectiveSudoMode, } from './src/host/sudo_mode.js';
|
|
77
|
+
// @adonisjs/auth integration (opt-in) — user provider for config/auth.ts's
|
|
78
|
+
// sessionGuard(), backed by authkit's own accountStore. Pair with
|
|
79
|
+
// `adonisAuth: { guard: '...' }` in config/authkit.ts so the account console's
|
|
80
|
+
// login/logout also sync ctx.auth. See src/host/adonis_auth_user_provider.ts.
|
|
81
|
+
export { authkitUserProvider } from './src/host/adonis_auth_user_provider.js';
|
|
@@ -12,12 +12,27 @@ import { defaultEncryptForStore } from '../src/define_config.js';
|
|
|
12
12
|
import { resolveKeystoreVault, KeystoreManager } from '../src/keys/keystore_manager.js';
|
|
13
13
|
import { KeystoreCodec } from '../src/keys/keystore_codec.js';
|
|
14
14
|
import { loadEncryptionService } from '../src/keys/keystore_crypto.js';
|
|
15
|
+
import { resolveAppKey } from '../src/host/app_key.js';
|
|
15
16
|
export default class AuthkitServerProvider {
|
|
16
17
|
app;
|
|
17
18
|
constructor(app) {
|
|
18
19
|
this.app = app;
|
|
19
20
|
}
|
|
20
21
|
async boot() {
|
|
22
|
+
// EAGER + LOUD (fail-fast): se o host configurou `config/authkit.ts`, a
|
|
23
|
+
// `appKey` é indispensável — o oidc-provider a usa para assinar TODOS os
|
|
24
|
+
// cookies (keygrip). Antes, essa validação só rodava dentro do factory
|
|
25
|
+
// lazy do `authkit.server` (registrado abaixo): sem nada que resolvesse o
|
|
26
|
+
// binding no boot, o app subia normalmente e o erro só aparecia (a) na
|
|
27
|
+
// 1ª request que tocasse uma rota do authkit, como um 500 sem contexto
|
|
28
|
+
// fora do modo dev, OU (b) nunca, porque `#startKeystoreReloadPoll`/
|
|
29
|
+
// `#startKeyRotationScheduler` engolem qualquer erro do `container.make`
|
|
30
|
+
// via `.catch(() => null)`. Validar aqui, fora do try/catch fail-safe
|
|
31
|
+
// abaixo, propaga o erro para `app.boot()` e derruba o boot do processo
|
|
32
|
+
// com a mensagem certa — em vez de um 500 misterioso depois.
|
|
33
|
+
if (this.app.config.get('authkit')) {
|
|
34
|
+
resolveAppKey(this.app);
|
|
35
|
+
}
|
|
21
36
|
// Config locks: trava as settings definidas explicitamente no defineConfig
|
|
22
37
|
// (config vence em runtime; a UI/Admin API não pode alterá-las). Fail-safe:
|
|
23
38
|
// qualquer erro → sem locks (comportamento legado).
|
|
@@ -76,19 +91,12 @@ export default class AuthkitServerProvider {
|
|
|
76
91
|
if (!config) {
|
|
77
92
|
throw new RuntimeException('Config inválido em "config/authkit.ts". Use o método defineConfig de @adonis-agora/authkit-server.');
|
|
78
93
|
}
|
|
79
|
-
// `app.appKey` pode ser uma string crua ou um `Secret` do AdonisJS
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
const
|
|
85
|
-
const appKey = rawAppKey && typeof rawAppKey.release === 'function'
|
|
86
|
-
? rawAppKey.release()
|
|
87
|
-
: rawAppKey;
|
|
88
|
-
if (!appKey || typeof appKey !== 'string') {
|
|
89
|
-
throw new RuntimeException('APP_KEY ausente: defina `export const appKey = new Secret(env.get(\'APP_KEY\'))` em config/app.ts. ' +
|
|
90
|
-
'O @adonis-agora/authkit-server precisa dele para assinar os cookies do oidc-provider.');
|
|
91
|
-
}
|
|
94
|
+
// `app.appKey` pode ser uma string crua ou um `Secret` do AdonisJS — ver
|
|
95
|
+
// `resolveAppKey` para o porquê e para a mensagem de erro (agora também
|
|
96
|
+
// validada EAGER no `boot()`, acima; mantida aqui para o caso raro de
|
|
97
|
+
// alguém resolver `authkit.server` diretamente sem passar pelo boot do
|
|
98
|
+
// provider, ex.: testes unitários do binding).
|
|
99
|
+
const appKey = resolveAppKey(this.app);
|
|
92
100
|
const metrics = await this.app.container.make('authkit.metrics');
|
|
93
101
|
const jwksInput = config.jwksConfig;
|
|
94
102
|
let jwksLoader;
|
|
@@ -521,7 +521,12 @@ export interface AuthServerConfigInput {
|
|
|
521
521
|
* Hosts que preferem mandar o usuário direto pro app podem apontar pra rota deles.
|
|
522
522
|
*/
|
|
523
523
|
accountHome?: string;
|
|
524
|
-
/**
|
|
524
|
+
/**
|
|
525
|
+
* Renderer de páginas do host (Inertia ou Edge). Default (quando omitido):
|
|
526
|
+
* {@link edgeRenderer}. Sem isso, TODA request a `/account/*` e
|
|
527
|
+
* `/auth/interaction/*` estourava um 500 sem explicação — `render` era
|
|
528
|
+
* `undefined` e o controller chamava `render(ctx, ...)` diretamente.
|
|
529
|
+
*/
|
|
525
530
|
render?: AuthHostRenderer;
|
|
526
531
|
/** Configuração de branding por cliente. */
|
|
527
532
|
branding?: BrandingConfig;
|
|
@@ -662,6 +667,27 @@ export interface AuthServerConfigInput {
|
|
|
662
667
|
accountLifecycle?: {
|
|
663
668
|
durable?: boolean;
|
|
664
669
|
};
|
|
670
|
+
/**
|
|
671
|
+
* Integração OPT-IN com `@adonisjs/auth`. authkit-server autentica a área de
|
|
672
|
+
* conta (`/account/*`) com sua PRÓPRIA sessão (`ACCOUNT_SESSION_KEY`, via
|
|
673
|
+
* `@adonisjs/session` diretamente) — nunca chamou `@adonisjs/auth` em lugar
|
|
674
|
+
* nenhum, então `ctx.auth.user`/`middleware.auth()`/Bouncer's
|
|
675
|
+
* `() => ctx.auth.user` ficavam sempre `undefined` num host que os use.
|
|
676
|
+
*
|
|
677
|
+
* Quando `adonisAuth.guard` é setado, `AccountSessionController#login`/`logout`
|
|
678
|
+
* TAMBÉM chamam `ctx.auth.use(guard).login(account)`/`.logout()` do guard
|
|
679
|
+
* indicado — em paralelo ao cookie bespoke do authkit, que continua sendo a
|
|
680
|
+
* fonte de verdade das rotas do próprio authkit. Requer um user provider
|
|
681
|
+
* plugado nesse guard em `config/auth.ts` — veja {@link authkitUserProvider}.
|
|
682
|
+
*
|
|
683
|
+
* Default (ausente): comportamento de sempre, sem tocar em `ctx.auth`.
|
|
684
|
+
* Degrada limpo mesmo sem `@adonisjs/auth` instalado (peer opcional): o
|
|
685
|
+
* código só acessa `ctx.auth` quando esta opção está setada E a propriedade
|
|
686
|
+
* existe no ctx (host com `@adonisjs/auth` de fato inicializado).
|
|
687
|
+
*/
|
|
688
|
+
adonisAuth?: {
|
|
689
|
+
guard: string;
|
|
690
|
+
};
|
|
665
691
|
}
|
|
666
692
|
export interface ResolvedServerConfig {
|
|
667
693
|
issuer: string;
|
|
@@ -755,6 +781,10 @@ export interface ResolvedServerConfig {
|
|
|
755
781
|
};
|
|
756
782
|
/** Keys de `auth_settings` travadas por terem sido definidas no defineConfig. */
|
|
757
783
|
lockedSettingKeys: string[];
|
|
784
|
+
/** Integração opt-in com `@adonisjs/auth` (ausente = não integrado; comportamento de sempre). */
|
|
785
|
+
adonisAuth?: {
|
|
786
|
+
guard: string;
|
|
787
|
+
};
|
|
758
788
|
}
|
|
759
789
|
export declare function toSeconds(value: string | number | undefined, fallback: number): number;
|
|
760
790
|
/**
|
|
@@ -9,6 +9,7 @@ import { resolveMessages, } from "./host/i18n.js";
|
|
|
9
9
|
import { resolveTrustedDevices, } from "./host/trusted_device.js";
|
|
10
10
|
import { resolveBotProtection, } from "./host/bot_protection.js";
|
|
11
11
|
import { deriveLockedSettingKeys } from "./host/config_locks.js";
|
|
12
|
+
import { edgeRenderer } from "./host/renderers/edge_renderer.js";
|
|
12
13
|
export { adapters };
|
|
13
14
|
const RATE_LIMIT_DEFAULTS = {
|
|
14
15
|
login: { points: 10, duration: "1 min" },
|
|
@@ -296,7 +297,14 @@ export function defineConfig(config) {
|
|
|
296
297
|
accountStore: config.accountStore,
|
|
297
298
|
patStore: config.patStore,
|
|
298
299
|
mountPath: config.mountPath ?? "/oidc",
|
|
299
|
-
|
|
300
|
+
// Default de runtime: sem isso, `render` ficava `undefined` e TODA
|
|
301
|
+
// request a `/account/*`/`/auth/interaction/*` estourava um 500 sem
|
|
302
|
+
// contexto (os controllers fazem `cfg.render!(...)`). `edgeRenderer()`
|
|
303
|
+
// só usa `ctx.view` quando de fato invocado por uma request — não
|
|
304
|
+
// importa `edge.js` eagerly, então é seguro construir mesmo em hosts
|
|
305
|
+
// sem o peer opcional instalado (só quebraria se a request realmente
|
|
306
|
+
// chegasse sem Edge configurado, exatamente como hoje).
|
|
307
|
+
render: config.render ?? edgeRenderer(),
|
|
300
308
|
branding: config.branding,
|
|
301
309
|
social: config.social,
|
|
302
310
|
patIntrospectionSecret: config.patIntrospectionSecret,
|
|
@@ -346,6 +354,8 @@ export function defineConfig(config) {
|
|
|
346
354
|
// Keys de auth_settings travadas porque foram definidas no defineConfig:
|
|
347
355
|
// config vence e a UI/Admin API não pode alterá-las (ver host/config_locks.ts).
|
|
348
356
|
lockedSettingKeys: deriveLockedSettingKeys(config),
|
|
357
|
+
// Opt-in: ausente = authkit nunca toca `ctx.auth` (comportamento de sempre).
|
|
358
|
+
adonisAuth: config.adonisAuth,
|
|
349
359
|
};
|
|
350
360
|
});
|
|
351
361
|
}
|
|
@@ -35,6 +35,37 @@ export declare function resolveEvents(input?: EventsConfigInput): ResolvedEvents
|
|
|
35
35
|
export declare function buildWebhookBody(event: AuditEvent): string;
|
|
36
36
|
/** Calcula o header de assinatura `sha256=<hmac>` para um corpo + segredo. */
|
|
37
37
|
export declare function signWebhookBody(body: string, secret: string): string;
|
|
38
|
+
/**
|
|
39
|
+
* Projeta um {@link AuditEvent} para o barramento `@agora/diagnostics` SEM PII
|
|
40
|
+
* direta (LGPD/GDPR — completude do "direito ao esquecimento").
|
|
41
|
+
*
|
|
42
|
+
* Motivação: o ramo de diagnostics espelha CADA evento de auditoria no barramento,
|
|
43
|
+
* de onde o Telescope o captura como um `diagnostic` INDEPENDENTE (tag
|
|
44
|
+
* `lib:authkit`) na SUA PRÓPRIA store. Esse espelho NÃO é alcançado pelo cascade de
|
|
45
|
+
* deleção de conta — o passo `anonymizeAudit` só anonimiza as linhas do audit-sink,
|
|
46
|
+
* não as cópias que vazaram para o Telescope. Se o evento bruto fosse para o
|
|
47
|
+
* barramento, o `email`/`ip` de uma conta deletada sobreviveria no store do
|
|
48
|
+
* Telescope.
|
|
49
|
+
*
|
|
50
|
+
* Para fechar isso na origem (mais robusto que um purge cross-store), a projeção que
|
|
51
|
+
* vai para o barramento já NASCE sem os identificadores diretos:
|
|
52
|
+
*
|
|
53
|
+
* - REMOVE `email` e `ip` (PII direta);
|
|
54
|
+
* - REMOVE `metadata` — é livre (`Record<string, unknown>`) e pode carregar PII,
|
|
55
|
+
* p.ex. `{ email }` (convites de org) ou `{ oldEmail, newEmail }` (troca de
|
|
56
|
+
* e-mail). Nenhum data provider do dashboard lê `metadata`, então dropá-lo é
|
|
57
|
+
* seguro;
|
|
58
|
+
* - MANTÉM `type` (a família do evento — o que os providers agregam) e os ids
|
|
59
|
+
* internos opacos `accountId`/`actorId`/`clientId` (correlação de subject/actor
|
|
60
|
+
* no dashboard; NÃO são PII direta e, sem `email`/`ip`/`metadata` e com a linha
|
|
61
|
+
* da conta já deletada, não são reidentificáveis).
|
|
62
|
+
*
|
|
63
|
+
* Assim o Telescope nunca armazena PII bruta e a deleção de conta não precisa de uma
|
|
64
|
+
* etapa de purge cross-lib. Os ramos `onEvent`/`webhook` (integrações que o host
|
|
65
|
+
* habilita explicitamente) continuam recebendo o evento COMPLETO — só a ponte de
|
|
66
|
+
* diagnostics é redigida.
|
|
67
|
+
*/
|
|
68
|
+
export declare function redactAuditEventForDiagnostics(event: AuditEvent): AuditEvent;
|
|
38
69
|
/**
|
|
39
70
|
* Decora um AuditSink (ou cria um do zero) num sink fan-out: cada `record`
|
|
40
71
|
* persiste no sink original (se houver) E dispara onEvent + webhook. Falhas em
|
|
@@ -29,6 +29,44 @@ export function buildWebhookBody(event) {
|
|
|
29
29
|
export function signWebhookBody(body, secret) {
|
|
30
30
|
return "sha256=" + createHmac("sha256", secret).update(body).digest("hex");
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Projeta um {@link AuditEvent} para o barramento `@agora/diagnostics` SEM PII
|
|
34
|
+
* direta (LGPD/GDPR — completude do "direito ao esquecimento").
|
|
35
|
+
*
|
|
36
|
+
* Motivação: o ramo de diagnostics espelha CADA evento de auditoria no barramento,
|
|
37
|
+
* de onde o Telescope o captura como um `diagnostic` INDEPENDENTE (tag
|
|
38
|
+
* `lib:authkit`) na SUA PRÓPRIA store. Esse espelho NÃO é alcançado pelo cascade de
|
|
39
|
+
* deleção de conta — o passo `anonymizeAudit` só anonimiza as linhas do audit-sink,
|
|
40
|
+
* não as cópias que vazaram para o Telescope. Se o evento bruto fosse para o
|
|
41
|
+
* barramento, o `email`/`ip` de uma conta deletada sobreviveria no store do
|
|
42
|
+
* Telescope.
|
|
43
|
+
*
|
|
44
|
+
* Para fechar isso na origem (mais robusto que um purge cross-store), a projeção que
|
|
45
|
+
* vai para o barramento já NASCE sem os identificadores diretos:
|
|
46
|
+
*
|
|
47
|
+
* - REMOVE `email` e `ip` (PII direta);
|
|
48
|
+
* - REMOVE `metadata` — é livre (`Record<string, unknown>`) e pode carregar PII,
|
|
49
|
+
* p.ex. `{ email }` (convites de org) ou `{ oldEmail, newEmail }` (troca de
|
|
50
|
+
* e-mail). Nenhum data provider do dashboard lê `metadata`, então dropá-lo é
|
|
51
|
+
* seguro;
|
|
52
|
+
* - MANTÉM `type` (a família do evento — o que os providers agregam) e os ids
|
|
53
|
+
* internos opacos `accountId`/`actorId`/`clientId` (correlação de subject/actor
|
|
54
|
+
* no dashboard; NÃO são PII direta e, sem `email`/`ip`/`metadata` e com a linha
|
|
55
|
+
* da conta já deletada, não são reidentificáveis).
|
|
56
|
+
*
|
|
57
|
+
* Assim o Telescope nunca armazena PII bruta e a deleção de conta não precisa de uma
|
|
58
|
+
* etapa de purge cross-lib. Os ramos `onEvent`/`webhook` (integrações que o host
|
|
59
|
+
* habilita explicitamente) continuam recebendo o evento COMPLETO — só a ponte de
|
|
60
|
+
* diagnostics é redigida.
|
|
61
|
+
*/
|
|
62
|
+
export function redactAuditEventForDiagnostics(event) {
|
|
63
|
+
return {
|
|
64
|
+
type: event.type,
|
|
65
|
+
accountId: event.accountId ?? null,
|
|
66
|
+
actorId: event.actorId ?? null,
|
|
67
|
+
clientId: event.clientId ?? null,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
32
70
|
/**
|
|
33
71
|
* Dispara o webhook de forma fire-and-forget: timeout de 5s via AbortSignal,
|
|
34
72
|
* captura QUALQUER erro (rede, abort, HMAC) sem propagar. Nunca lança.
|
|
@@ -87,7 +125,9 @@ export function composeAuditSink(original, events = {}) {
|
|
|
87
125
|
}
|
|
88
126
|
// Diagnostics Agora (best-effort, sempre ligado, no-op sem o slot).
|
|
89
127
|
// Canal: `agora:authkit:<AuditEventType>` (o `type` É o sufixo).
|
|
90
|
-
|
|
128
|
+
// Emite uma projeção REDIGIDA (sem `email`/`ip`/`metadata`) para que o
|
|
129
|
+
// Telescope nunca armazene PII bruta — ver `redactAuditEventForDiagnostics`.
|
|
130
|
+
emitDiagnostic(event.type, redactAuditEventForDiagnostics(event));
|
|
91
131
|
},
|
|
92
132
|
};
|
|
93
133
|
// Preserva a capacidade de consulta do sink original (console admin).
|
|
@@ -35,6 +35,7 @@ import { AdminSessionsService } from '../admin_sessions_service.js';
|
|
|
35
35
|
import { enrichSessionsWithContext } from '../session_context.js';
|
|
36
36
|
import { PasswordPolicyError } from '../../password/password_manager.js';
|
|
37
37
|
import { resolveRuntimeSettings } from '../runtime_settings.js';
|
|
38
|
+
import { syncAdonisAuthLogout } from '../adonis_auth_sync.js';
|
|
38
39
|
import { resolveEffectiveEmailChange, resolveEffectivePasswordHistory, } from '../runtime_toggles.js';
|
|
39
40
|
import { dispatchSecurityNotice } from '../security_notice_service.js';
|
|
40
41
|
import { requireSudo, isSudoActive, SUDO_MODE_DEFAULTS, resolveEffectiveSudoMode } from '../sudo_mode.js';
|
|
@@ -515,6 +516,7 @@ export default class AccountApiController {
|
|
|
515
516
|
// Encerrar a sessão Adonis do console (logout global do self-service).
|
|
516
517
|
ctx.session.forget(ACCOUNT_SESSION_KEY);
|
|
517
518
|
await ctx.session.regenerate();
|
|
519
|
+
await syncAdonisAuthLogout(ctx, cfg);
|
|
518
520
|
return { ok: true, signedOut: true, ...result };
|
|
519
521
|
}
|
|
520
522
|
// ─── GET /account/api/apps ──────────────────────────────────────────────
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
|
+
import type { ResolvedServerConfig } from '../define_config.js';
|
|
3
|
+
import type { AuthAccount } from '../accounts/account_store.js';
|
|
4
|
+
/**
|
|
5
|
+
* Sincroniza `ctx.auth` (`@adonisjs/auth`) com um login bem-sucedido no console
|
|
6
|
+
* de conta do authkit. Ver `adonisAuth` em `define_config.ts` pro porquê: sem
|
|
7
|
+
* isso, `ctx.auth.user` nunca é populado por nenhum fluxo do authkit-server —
|
|
8
|
+
* mesmo num host que roda `@adonisjs/auth` normalmente.
|
|
9
|
+
*
|
|
10
|
+
* NO-OP quando:
|
|
11
|
+
* - `adonisAuth.guard` não está configurado (opt-in; default é não tocar em
|
|
12
|
+
* `ctx.auth` — comportamento de sempre).
|
|
13
|
+
* - `ctx.auth` não existe nesta request (host sem `InitializeAuthMiddleware`
|
|
14
|
+
* de `@adonisjs/auth` registrado — o peer é opcional).
|
|
15
|
+
*
|
|
16
|
+
* BEST-EFFORT: uma falha aqui (ex.: guard mal configurado em `config/auth.ts`)
|
|
17
|
+
* NUNCA quebra o login do authkit — o cookie bespoke (`ACCOUNT_SESSION_KEY`)
|
|
18
|
+
* já é a fonte de verdade das rotas do próprio authkit e continua funcionando
|
|
19
|
+
* mesmo se a sincronização com `@adonisjs/auth` falhar. Loga um warning para
|
|
20
|
+
* não mascarar um guard mal configurado silenciosamente.
|
|
21
|
+
*/
|
|
22
|
+
export declare function syncAdonisAuthLogin(ctx: HttpContext, cfg: Pick<ResolvedServerConfig, 'adonisAuth'>, account: AuthAccount): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Sincroniza `ctx.auth` com um logout do console de conta. Ver
|
|
25
|
+
* {@link syncAdonisAuthLogin} para as mesmas regras de no-op/best-effort.
|
|
26
|
+
*/
|
|
27
|
+
export declare function syncAdonisAuthLogout(ctx: HttpContext, cfg: Pick<ResolvedServerConfig, 'adonisAuth'>): Promise<void>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sincroniza `ctx.auth` (`@adonisjs/auth`) com um login bem-sucedido no console
|
|
3
|
+
* de conta do authkit. Ver `adonisAuth` em `define_config.ts` pro porquê: sem
|
|
4
|
+
* isso, `ctx.auth.user` nunca é populado por nenhum fluxo do authkit-server —
|
|
5
|
+
* mesmo num host que roda `@adonisjs/auth` normalmente.
|
|
6
|
+
*
|
|
7
|
+
* NO-OP quando:
|
|
8
|
+
* - `adonisAuth.guard` não está configurado (opt-in; default é não tocar em
|
|
9
|
+
* `ctx.auth` — comportamento de sempre).
|
|
10
|
+
* - `ctx.auth` não existe nesta request (host sem `InitializeAuthMiddleware`
|
|
11
|
+
* de `@adonisjs/auth` registrado — o peer é opcional).
|
|
12
|
+
*
|
|
13
|
+
* BEST-EFFORT: uma falha aqui (ex.: guard mal configurado em `config/auth.ts`)
|
|
14
|
+
* NUNCA quebra o login do authkit — o cookie bespoke (`ACCOUNT_SESSION_KEY`)
|
|
15
|
+
* já é a fonte de verdade das rotas do próprio authkit e continua funcionando
|
|
16
|
+
* mesmo se a sincronização com `@adonisjs/auth` falhar. Loga um warning para
|
|
17
|
+
* não mascarar um guard mal configurado silenciosamente.
|
|
18
|
+
*/
|
|
19
|
+
export async function syncAdonisAuthLogin(ctx, cfg, account) {
|
|
20
|
+
const guard = cfg.adonisAuth?.guard;
|
|
21
|
+
const auth = ctx.auth;
|
|
22
|
+
if (!guard || !auth)
|
|
23
|
+
return;
|
|
24
|
+
try {
|
|
25
|
+
await auth.use(guard).login(account);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
ctx.logger?.warn({ err: error, guard }, 'authkit: falha ao sincronizar login com @adonisjs/auth (adonisAuth.guard) — ' +
|
|
29
|
+
'ctx.auth.user pode não refletir a conta logada; o cookie do authkit segue normal');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Sincroniza `ctx.auth` com um logout do console de conta. Ver
|
|
34
|
+
* {@link syncAdonisAuthLogin} para as mesmas regras de no-op/best-effort.
|
|
35
|
+
*/
|
|
36
|
+
export async function syncAdonisAuthLogout(ctx, cfg) {
|
|
37
|
+
const guard = cfg.adonisAuth?.guard;
|
|
38
|
+
const auth = ctx.auth;
|
|
39
|
+
if (!guard || !auth)
|
|
40
|
+
return;
|
|
41
|
+
try {
|
|
42
|
+
await auth.use(guard).logout();
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
ctx.logger?.warn({ err: error, guard }, 'authkit: falha ao sincronizar logout com @adonisjs/auth (adonisAuth.guard)');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ConfigProvider } from '@adonisjs/core/types';
|
|
2
|
+
import type { SessionUserProviderContract } from '@adonisjs/auth/types/session';
|
|
3
|
+
import type { AuthAccount } from '../accounts/account_store.js';
|
|
4
|
+
/**
|
|
5
|
+
* User provider de `@adonisjs/auth` (session guard) apoiado no `accountStore`
|
|
6
|
+
* do próprio authkit — a MESMA fonte de identidade que o login do console de
|
|
7
|
+
* conta autentica. Plugado em `config/auth.ts`:
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { sessionGuard } from '@adonisjs/auth/session'
|
|
11
|
+
* import { authkitUserProvider } from '@adonis-agora/authkit-server'
|
|
12
|
+
*
|
|
13
|
+
* export default defineConfig({
|
|
14
|
+
* default: 'web',
|
|
15
|
+
* guards: {
|
|
16
|
+
* web: sessionGuard({
|
|
17
|
+
* useRememberMeTokens: false,
|
|
18
|
+
* provider: authkitUserProvider(),
|
|
19
|
+
* }),
|
|
20
|
+
* },
|
|
21
|
+
* })
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* Junto com `adonisAuth: { guard: 'web' }` em `config/authkit.ts`, o login do
|
|
25
|
+
* console de conta (`AccountSessionController#login`) passa a chamar
|
|
26
|
+
* `ctx.auth.use('web').login(account)` no sucesso — populando `ctx.auth.user`
|
|
27
|
+
* com um {@link AuthAccount} de verdade para o resto do app (Bouncer,
|
|
28
|
+
* `middleware.auth()`, etc.), sem duplicar a sessão bespoke do authkit
|
|
29
|
+
* (`ACCOUNT_SESSION_KEY`), que continua sendo a fonte de verdade das próprias
|
|
30
|
+
* rotas do authkit.
|
|
31
|
+
*
|
|
32
|
+
* Devolve um `ConfigProvider` (mesmo padrão do `sessionUserProvider()` nativo
|
|
33
|
+
* de `@adonisjs/auth`) — a resolução (incluindo o import dinâmico de
|
|
34
|
+
* `@adonisjs/auth` para o símbolo `PROVIDER_REAL_USER` exigido pelo contrato)
|
|
35
|
+
* só acontece quando o HOST resolve a config do guard no boot, nunca no
|
|
36
|
+
* carregamento deste módulo.
|
|
37
|
+
*/
|
|
38
|
+
export declare function authkitUserProvider(): ConfigProvider<SessionUserProviderContract<AuthAccount>>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { configProvider } from '@adonisjs/core';
|
|
2
|
+
import { RuntimeException } from '@adonisjs/core/exceptions';
|
|
3
|
+
/**
|
|
4
|
+
* User provider de `@adonisjs/auth` (session guard) apoiado no `accountStore`
|
|
5
|
+
* do próprio authkit — a MESMA fonte de identidade que o login do console de
|
|
6
|
+
* conta autentica. Plugado em `config/auth.ts`:
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { sessionGuard } from '@adonisjs/auth/session'
|
|
10
|
+
* import { authkitUserProvider } from '@adonis-agora/authkit-server'
|
|
11
|
+
*
|
|
12
|
+
* export default defineConfig({
|
|
13
|
+
* default: 'web',
|
|
14
|
+
* guards: {
|
|
15
|
+
* web: sessionGuard({
|
|
16
|
+
* useRememberMeTokens: false,
|
|
17
|
+
* provider: authkitUserProvider(),
|
|
18
|
+
* }),
|
|
19
|
+
* },
|
|
20
|
+
* })
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* Junto com `adonisAuth: { guard: 'web' }` em `config/authkit.ts`, o login do
|
|
24
|
+
* console de conta (`AccountSessionController#login`) passa a chamar
|
|
25
|
+
* `ctx.auth.use('web').login(account)` no sucesso — populando `ctx.auth.user`
|
|
26
|
+
* com um {@link AuthAccount} de verdade para o resto do app (Bouncer,
|
|
27
|
+
* `middleware.auth()`, etc.), sem duplicar a sessão bespoke do authkit
|
|
28
|
+
* (`ACCOUNT_SESSION_KEY`), que continua sendo a fonte de verdade das próprias
|
|
29
|
+
* rotas do authkit.
|
|
30
|
+
*
|
|
31
|
+
* Devolve um `ConfigProvider` (mesmo padrão do `sessionUserProvider()` nativo
|
|
32
|
+
* de `@adonisjs/auth`) — a resolução (incluindo o import dinâmico de
|
|
33
|
+
* `@adonisjs/auth` para o símbolo `PROVIDER_REAL_USER` exigido pelo contrato)
|
|
34
|
+
* só acontece quando o HOST resolve a config do guard no boot, nunca no
|
|
35
|
+
* carregamento deste módulo.
|
|
36
|
+
*/
|
|
37
|
+
export function authkitUserProvider() {
|
|
38
|
+
return configProvider.create(async (app) => {
|
|
39
|
+
let PROVIDER_REAL_USER;
|
|
40
|
+
try {
|
|
41
|
+
const auth = (await import('@adonisjs/auth'));
|
|
42
|
+
PROVIDER_REAL_USER = auth.symbols.PROVIDER_REAL_USER;
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
throw new RuntimeException('authkitUserProvider() precisa de "@adonisjs/auth" instalado (é um peer opcional do ' +
|
|
46
|
+
'@adonis-agora/authkit-server, só necessário se você plugar este provider em config/auth.ts). ' +
|
|
47
|
+
'Rode `npm i @adonisjs/auth` (ou pnpm/yarn) e configure um sessionGuard em config/auth.ts.', { cause: error });
|
|
48
|
+
}
|
|
49
|
+
// `PROVIDER_REAL_USER` chega em runtime como um `symbol` (não um "unique
|
|
50
|
+
// symbol" literal) — o cast final é seguro pq o valor É literalmente o
|
|
51
|
+
// símbolo exportado por `@adonisjs/auth`, só que o TS não consegue provar
|
|
52
|
+
// isso estruturalmente a partir de um import dinâmico.
|
|
53
|
+
const provider = {
|
|
54
|
+
[PROVIDER_REAL_USER]: undefined,
|
|
55
|
+
async createUserForGuard(user) {
|
|
56
|
+
return {
|
|
57
|
+
getId: () => user.id,
|
|
58
|
+
getOriginal: () => user,
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
async findById(identifier) {
|
|
62
|
+
const store = await app.container.make('authkit.accountStore');
|
|
63
|
+
const account = await store.findById(String(identifier));
|
|
64
|
+
if (!account)
|
|
65
|
+
return null;
|
|
66
|
+
return {
|
|
67
|
+
getId: () => account.id,
|
|
68
|
+
getOriginal: () => account,
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
return provider;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ApplicationService } from '@adonisjs/core/types';
|
|
2
|
+
/**
|
|
3
|
+
* Mensagem única (compartilhada entre o check EAGER do `boot()` e o resolver
|
|
4
|
+
* LAZY do `register()`) para o erro de `appKey` ausente — um só lugar para
|
|
5
|
+
* editar o texto, e garante que os dois caminhos falem exatamente a mesma coisa.
|
|
6
|
+
*/
|
|
7
|
+
export declare const MISSING_APP_KEY_MESSAGE: string;
|
|
8
|
+
/**
|
|
9
|
+
* Lê e valida `app.appKey` (usado pelo oidc-provider para assinar cookies via
|
|
10
|
+
* keygrip). `app.appKey` pode ser uma string crua ou um `Secret` do AdonisJS
|
|
11
|
+
* (`config/app.ts` expõe `export const appKey = new Secret(env.get('APP_KEY'))`).
|
|
12
|
+
*
|
|
13
|
+
* Lança {@link RuntimeException} com uma mensagem que NOMEIA o arquivo e o campo
|
|
14
|
+
* exatos a corrigir — nunca deixa o app seguir com uma appKey ausente/inválida
|
|
15
|
+
* (o oidc-provider quebraria mais adiante com um erro genérico do keygrip,
|
|
16
|
+
* sem apontar a causa raiz).
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveAppKey(app: ApplicationService): string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { RuntimeException } from '@adonisjs/core/exceptions';
|
|
2
|
+
/**
|
|
3
|
+
* Mensagem única (compartilhada entre o check EAGER do `boot()` e o resolver
|
|
4
|
+
* LAZY do `register()`) para o erro de `appKey` ausente — um só lugar para
|
|
5
|
+
* editar o texto, e garante que os dois caminhos falem exatamente a mesma coisa.
|
|
6
|
+
*/
|
|
7
|
+
export const MISSING_APP_KEY_MESSAGE = 'APP_KEY ausente: defina `export const appKey = new Secret(env.get(\'APP_KEY\'))` em config/app.ts. ' +
|
|
8
|
+
'O @adonis-agora/authkit-server precisa dele para assinar os cookies do oidc-provider (NÃO usa config/encryption.ts).';
|
|
9
|
+
/**
|
|
10
|
+
* Lê e valida `app.appKey` (usado pelo oidc-provider para assinar cookies via
|
|
11
|
+
* keygrip). `app.appKey` pode ser uma string crua ou um `Secret` do AdonisJS
|
|
12
|
+
* (`config/app.ts` expõe `export const appKey = new Secret(env.get('APP_KEY'))`).
|
|
13
|
+
*
|
|
14
|
+
* Lança {@link RuntimeException} com uma mensagem que NOMEIA o arquivo e o campo
|
|
15
|
+
* exatos a corrigir — nunca deixa o app seguir com uma appKey ausente/inválida
|
|
16
|
+
* (o oidc-provider quebraria mais adiante com um erro genérico do keygrip,
|
|
17
|
+
* sem apontar a causa raiz).
|
|
18
|
+
*/
|
|
19
|
+
export function resolveAppKey(app) {
|
|
20
|
+
const rawAppKey = app.config.get('app.appKey');
|
|
21
|
+
const appKey = rawAppKey && typeof rawAppKey.release === 'function'
|
|
22
|
+
? rawAppKey.release()
|
|
23
|
+
: rawAppKey;
|
|
24
|
+
if (!appKey || typeof appKey !== 'string') {
|
|
25
|
+
throw new RuntimeException(MISSING_APP_KEY_MESSAGE);
|
|
26
|
+
}
|
|
27
|
+
return appKey;
|
|
28
|
+
}
|
|
@@ -15,6 +15,7 @@ import { resolveRuntimeSettings } from "../runtime_settings.js";
|
|
|
15
15
|
import { resolveEffectiveEmailChange, resolveEffectivePasswordHistory, } from "../runtime_toggles.js";
|
|
16
16
|
import { dispatchSecurityNotice } from "../security_notice_service.js";
|
|
17
17
|
import { requireSudo } from "../sudo_mode.js";
|
|
18
|
+
import { syncAdonisAuthLogout } from "../adonis_auth_sync.js";
|
|
18
19
|
/** Resolve os password history settings em runtime (fail-safe). */
|
|
19
20
|
async function resolvePasswordHistorySettings(ctx) {
|
|
20
21
|
try {
|
|
@@ -193,6 +194,7 @@ export default class AccountSecurityController {
|
|
|
193
194
|
}
|
|
194
195
|
// Encerra a sessão e leva ao login com a mensagem de sucesso.
|
|
195
196
|
ctx.session.forget(ACCOUNT_SESSION_KEY);
|
|
197
|
+
await syncAdonisAuthLogout(ctx, cfg);
|
|
196
198
|
ctx.session.flash("accountDeleted", translate(cfg.messages, "account.delete.deleted"));
|
|
197
199
|
return ctx.response.redirect("/account/login");
|
|
198
200
|
}
|
|
@@ -6,6 +6,7 @@ import { notifyLoginSuccess } from '../login_notify.js';
|
|
|
6
6
|
import { markSudo } from '../sudo_mode.js';
|
|
7
7
|
import { accountHome } from '../account_home.js';
|
|
8
8
|
import { resolveRuntimeSettings } from '../runtime_settings.js';
|
|
9
|
+
import { syncAdonisAuthLogin, syncAdonisAuthLogout } from '../adonis_auth_sync.js';
|
|
9
10
|
/**
|
|
10
11
|
* Valida um valor de `return_to` recebido da query-string ou de um campo hidden.
|
|
11
12
|
*
|
|
@@ -95,11 +96,16 @@ export default class AccountSessionController {
|
|
|
95
96
|
ctx.session.put(ACCOUNT_SESSION_KEY, acc.id);
|
|
96
97
|
// Login com senha = confirmação de identidade → marca sudo (graça a partir do login).
|
|
97
98
|
markSudo(ctx);
|
|
99
|
+
// Opt-in: sincroniza ctx.auth.use(cfg.adonisAuth.guard) com a mesma conta —
|
|
100
|
+
// no-op sem `adonisAuth` configurado ou sem @adonisjs/auth inicializado.
|
|
101
|
+
await syncAdonisAuthLogin(ctx, cfg, acc);
|
|
98
102
|
await notifyLoginSuccess(ctx, cfg, { accountId: acc.id, email, ip });
|
|
99
103
|
// Redireciona pro destino original (validado), ou cai no accountHome configurado.
|
|
100
104
|
return ctx.response.redirect(returnTo ?? accountHome(cfg));
|
|
101
105
|
}
|
|
102
106
|
async logout(ctx) {
|
|
107
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
108
|
+
const cfg = service.config;
|
|
103
109
|
// M6: não basta `forget(ACCOUNT_SESSION_KEY)` — sobravam na sessão
|
|
104
110
|
// `authkit_sudo_at` (sudo), `authkit_last_seen` e qualquer outro estado
|
|
105
111
|
// sensível, com o session id INALTERADO. Regenerar a sessão troca o id E
|
|
@@ -108,6 +114,8 @@ export default class AccountSessionController {
|
|
|
108
114
|
// (belt-and-braces) caso o store de sessão não suporte regenerate.
|
|
109
115
|
ctx.session.forget(ACCOUNT_SESSION_KEY);
|
|
110
116
|
await ctx.session.regenerate();
|
|
117
|
+
// Opt-in: espelha o logout no guard de @adonisjs/auth (ver adonisAuth em define_config.ts).
|
|
118
|
+
await syncAdonisAuthLogout(ctx, cfg);
|
|
111
119
|
return ctx.response.redirect('/account/login');
|
|
112
120
|
}
|
|
113
121
|
}
|