@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.
- package/build/commands/commands.json +28 -0
- package/build/commands/doctor.d.ts +10 -0
- package/build/commands/doctor.js +66 -0
- package/build/commands/rotate_keys.d.ts +10 -0
- package/build/commands/rotate_keys.js +53 -0
- package/build/host/views/account/email-confirmed.edge +15 -0
- package/build/host/views/account/security.edge +83 -0
- package/build/host/views/account/tokens.edge +7 -4
- package/build/host/views/admin/client_form.edge +83 -0
- package/build/host/views/admin/clients.edge +68 -3
- package/build/host/views/admin/sessions.edge +89 -0
- package/build/host/views/admin/users.edge +1 -0
- package/build/host/views/mfa-challenge.edge +29 -23
- package/build/index.d.ts +4 -3
- package/build/index.js +2 -2
- package/build/src/accounts/account_store.d.ts +46 -1
- package/build/src/accounts/account_store.js +4 -0
- package/build/src/accounts/lucid_store/core.d.ts +5 -4
- package/build/src/accounts/lucid_store/core.js +67 -2
- package/build/src/adapters/adapter_contract.d.ts +29 -0
- package/build/src/adapters/database_adapter.d.ts +12 -1
- package/build/src/adapters/database_adapter.js +24 -0
- package/build/src/adapters/redis_adapter.d.ts +14 -1
- package/build/src/adapters/redis_adapter.js +35 -0
- package/build/src/audit/audit_sink.d.ts +1 -1
- package/build/src/define_config.d.ts +102 -0
- package/build/src/define_config.js +46 -3
- package/build/src/doctor/checks.d.ts +51 -0
- package/build/src/doctor/checks.js +231 -0
- package/build/src/host/admin_clients_service.d.ts +65 -0
- package/build/src/host/admin_clients_service.js +143 -0
- package/build/src/host/admin_sessions_service.d.ts +63 -0
- package/build/src/host/admin_sessions_service.js +127 -0
- package/build/src/host/controllers/account_security_controller.d.ts +16 -0
- package/build/src/host/controllers/account_security_controller.js +119 -0
- package/build/src/host/controllers/account_session_controller.js +2 -1
- package/build/src/host/controllers/admin/admin_clients_controller.d.ts +17 -3
- package/build/src/host/controllers/admin/admin_clients_controller.js +158 -4
- package/build/src/host/controllers/admin/admin_sessions_controller.d.ts +14 -0
- package/build/src/host/controllers/admin/admin_sessions_controller.js +64 -0
- package/build/src/host/controllers/interaction_controller.d.ts +11 -0
- package/build/src/host/controllers/interaction_controller.js +49 -10
- package/build/src/host/default_mailer.d.ts +17 -0
- package/build/src/host/default_mailer.js +51 -0
- package/build/src/host/i18n.d.ts +80 -0
- package/build/src/host/i18n.js +86 -1
- package/build/src/host/login_notify.d.ts +20 -0
- package/build/src/host/login_notify.js +71 -0
- package/build/src/host/register_auth_host.js +20 -0
- package/build/src/host/validators.d.ts +32 -0
- package/build/src/host/validators.js +14 -0
- package/build/src/keys/keystore.d.ts +43 -0
- package/build/src/keys/keystore.js +74 -0
- package/build/src/provider/build_provider.js +23 -0
- package/build/src/provider/device_sources.d.ts +6 -0
- package/build/src/provider/device_sources.js +65 -0
- package/build/src/provider/interaction_actions.d.ts +6 -1
- package/build/src/provider/interaction_actions.js +9 -2
- package/build/src/provider/oidc_service.d.ts +15 -0
- package/build/src/provider/oidc_service.js +27 -0
- package/package.json +2 -2
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { SigningAlg } from './jwks_manager.js';
|
|
2
|
+
/**
|
|
3
|
+
* Keystore JSON em arquivo para o modo `jwks: { source: 'managed', store }`.
|
|
4
|
+
*
|
|
5
|
+
* O modo `managed` "puro" gera UMA chave efêmera por boot (em
|
|
6
|
+
* {@link generateJwks}) — não persiste, então rotacionar não faz sentido: a cada
|
|
7
|
+
* restart o kid muda e tokens antigos param de validar. Para suportar rotação de
|
|
8
|
+
* verdade, este keystore persiste o JWKS PRIVADO em um arquivo. A rotação gera
|
|
9
|
+
* um novo par (novo kid) e mantém as N chaves mais recentes; o JWKS público
|
|
10
|
+
* servido inclui todas (as antigas continuam validando), e a PRIMEIRA chave do
|
|
11
|
+
* array é a de assinatura corrente (o oidc-provider assina com a primeira chave
|
|
12
|
+
* compatível).
|
|
13
|
+
*/
|
|
14
|
+
/** Estrutura persistida: JWKS privado (chaves com `d`). */
|
|
15
|
+
export interface PersistedKeystore {
|
|
16
|
+
keys: Record<string, any>[];
|
|
17
|
+
}
|
|
18
|
+
/** Gera uma chave de assinatura privada como JWK (com use/alg/kid). */
|
|
19
|
+
export declare function generateSigningJwk(alg: SigningAlg): Promise<Record<string, any>>;
|
|
20
|
+
/** Lê o keystore do arquivo, ou null se não existir/for inválido. */
|
|
21
|
+
export declare function readKeystore(path: string): PersistedKeystore | null;
|
|
22
|
+
/** Escreve o keystore no arquivo (cria o diretório se preciso). */
|
|
23
|
+
export declare function writeKeystore(path: string, store: PersistedKeystore): void;
|
|
24
|
+
/**
|
|
25
|
+
* Garante que o keystore exista: se ausente, cria com uma chave nova e persiste.
|
|
26
|
+
* Retorna o keystore (privado).
|
|
27
|
+
*/
|
|
28
|
+
export declare function ensureKeystore(path: string, alg: SigningAlg): Promise<PersistedKeystore>;
|
|
29
|
+
/**
|
|
30
|
+
* Rotaciona o keystore: gera uma chave nova, coloca-a NA FRENTE (vira a chave de
|
|
31
|
+
* assinatura corrente) e mantém apenas as `keep` mais recentes (default 2) para
|
|
32
|
+
* que tokens assinados com a chave anterior continuem validando. Persiste e
|
|
33
|
+
* retorna o keystore atualizado.
|
|
34
|
+
*/
|
|
35
|
+
export declare function rotateKeystore(path: string, alg: SigningAlg, keep?: number): Promise<{
|
|
36
|
+
store: PersistedKeystore;
|
|
37
|
+
newKid: string;
|
|
38
|
+
retiredKids: string[];
|
|
39
|
+
}>;
|
|
40
|
+
/** Deriva o JWKS PÚBLICO (sem `d` e demais campos privados) a partir do privado. */
|
|
41
|
+
export declare function toPublicJwks(store: PersistedKeystore): {
|
|
42
|
+
keys: Record<string, any>[];
|
|
43
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { generateKeyPair, exportJWK } from 'jose';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
4
|
+
import { dirname } from 'node:path';
|
|
5
|
+
/** Gera uma chave de assinatura privada como JWK (com use/alg/kid). */
|
|
6
|
+
export async function generateSigningJwk(alg) {
|
|
7
|
+
const { privateKey } = await generateKeyPair(alg, { extractable: true });
|
|
8
|
+
const jwk = (await exportJWK(privateKey));
|
|
9
|
+
jwk.use = 'sig';
|
|
10
|
+
jwk.alg = alg;
|
|
11
|
+
jwk.kid = randomUUID();
|
|
12
|
+
return jwk;
|
|
13
|
+
}
|
|
14
|
+
/** Lê o keystore do arquivo, ou null se não existir/for inválido. */
|
|
15
|
+
export function readKeystore(path) {
|
|
16
|
+
if (!existsSync(path))
|
|
17
|
+
return null;
|
|
18
|
+
try {
|
|
19
|
+
const parsed = JSON.parse(readFileSync(path, 'utf-8'));
|
|
20
|
+
if (parsed && Array.isArray(parsed.keys))
|
|
21
|
+
return parsed;
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Escreve o keystore no arquivo (cria o diretório se preciso). */
|
|
29
|
+
export function writeKeystore(path, store) {
|
|
30
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
31
|
+
writeFileSync(path, JSON.stringify(store, null, 2) + '\n', { mode: 0o600 });
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Garante que o keystore exista: se ausente, cria com uma chave nova e persiste.
|
|
35
|
+
* Retorna o keystore (privado).
|
|
36
|
+
*/
|
|
37
|
+
export async function ensureKeystore(path, alg) {
|
|
38
|
+
const existing = readKeystore(path);
|
|
39
|
+
if (existing && existing.keys.length > 0)
|
|
40
|
+
return existing;
|
|
41
|
+
const store = { keys: [await generateSigningJwk(alg)] };
|
|
42
|
+
writeKeystore(path, store);
|
|
43
|
+
return store;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Rotaciona o keystore: gera uma chave nova, coloca-a NA FRENTE (vira a chave de
|
|
47
|
+
* assinatura corrente) e mantém apenas as `keep` mais recentes (default 2) para
|
|
48
|
+
* que tokens assinados com a chave anterior continuem validando. Persiste e
|
|
49
|
+
* retorna o keystore atualizado.
|
|
50
|
+
*/
|
|
51
|
+
export async function rotateKeystore(path, alg, keep = 2) {
|
|
52
|
+
const current = readKeystore(path) ?? { keys: [] };
|
|
53
|
+
const fresh = await generateSigningJwk(alg);
|
|
54
|
+
const next = [fresh, ...current.keys];
|
|
55
|
+
const kept = next.slice(0, Math.max(1, keep));
|
|
56
|
+
const retiredKids = next.slice(Math.max(1, keep)).map((k) => k.kid);
|
|
57
|
+
const store = { keys: kept };
|
|
58
|
+
writeKeystore(path, store);
|
|
59
|
+
return { store, newKid: fresh.kid, retiredKids };
|
|
60
|
+
}
|
|
61
|
+
/** Deriva o JWKS PÚBLICO (sem `d` e demais campos privados) a partir do privado. */
|
|
62
|
+
export function toPublicJwks(store) {
|
|
63
|
+
const PRIVATE_FIELDS = ['d', 'p', 'q', 'dp', 'dq', 'qi'];
|
|
64
|
+
return {
|
|
65
|
+
keys: store.keys.map((jwk) => {
|
|
66
|
+
const pub = {};
|
|
67
|
+
for (const [k, v] of Object.entries(jwk)) {
|
|
68
|
+
if (!PRIVATE_FIELDS.includes(k))
|
|
69
|
+
pub[k] = v;
|
|
70
|
+
}
|
|
71
|
+
return pub;
|
|
72
|
+
}),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as oidc from 'oidc-provider';
|
|
2
|
+
import { createDeviceSources } from './device_sources.js';
|
|
2
3
|
export function buildProvider(config, options) {
|
|
3
4
|
const cookieKeys = config.cookieKeys.length ? config.cookieKeys : [options.appKey];
|
|
4
5
|
// OIDC Dynamic Client Registration (RFC 7591/7592). Só montamos as chaves de feature
|
|
@@ -16,6 +17,22 @@ export function buildProvider(config, options) {
|
|
|
16
17
|
...(dynReg.management ? { registrationManagement: { enabled: true } } : {}),
|
|
17
18
|
}
|
|
18
19
|
: {};
|
|
20
|
+
// Device Authorization Grant (RFC 8628). Quando ligado, montamos a feature com
|
|
21
|
+
// as três sources de UI i18n-izadas (entrada/confirmação/sucesso do user-code).
|
|
22
|
+
const deviceFlowFeatures = config.deviceFlow.enabled
|
|
23
|
+
? { deviceFlow: { enabled: true, ...createDeviceSources(config.messages) } }
|
|
24
|
+
: {};
|
|
25
|
+
// DPoP (RFC 9449). A chave EXATA do oidc-provider v9 é `dPoP`.
|
|
26
|
+
const dpopFeatures = config.dpop.enabled ? { dPoP: { enabled: true } } : {};
|
|
27
|
+
// PAR (RFC 9126).
|
|
28
|
+
const parFeatures = config.par.enabled
|
|
29
|
+
? {
|
|
30
|
+
pushedAuthorizationRequests: {
|
|
31
|
+
enabled: true,
|
|
32
|
+
requirePushedAuthorizationRequests: config.par.requirePushedAuthorizationRequests,
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
: {};
|
|
19
36
|
const provider = new oidc.Provider(config.issuer, {
|
|
20
37
|
adapter: config.AdapterClass,
|
|
21
38
|
clients: config.clients.map((c) => ({
|
|
@@ -79,7 +96,13 @@ export function buildProvider(config, options) {
|
|
|
79
96
|
revocation: { enabled: true },
|
|
80
97
|
introspection: { enabled: true },
|
|
81
98
|
...registrationFeatures,
|
|
99
|
+
...deviceFlowFeatures,
|
|
100
|
+
...dpopFeatures,
|
|
101
|
+
...parFeatures,
|
|
82
102
|
},
|
|
103
|
+
// Step-up auth (acr_values): anuncia os acr suportados para que clients possam
|
|
104
|
+
// solicitá-los. A exigência efetiva do 2º fator acontece na interaction de login.
|
|
105
|
+
acrValues: config.stepUp.acrValues,
|
|
83
106
|
ttl: {
|
|
84
107
|
AccessToken: config.ttl.accessToken,
|
|
85
108
|
RefreshToken: config.ttl.refreshToken,
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type AuthMessages } from '../host/i18n.js';
|
|
2
|
+
export declare function createDeviceSources(messages: AuthMessages): {
|
|
3
|
+
userCodeInputSource(_ctx: any, form: string, _out: any, err: any): Promise<void>;
|
|
4
|
+
userCodeConfirmSource(_ctx: any, form: string, _client: any, _deviceInfo: any, userCode: string): Promise<void>;
|
|
5
|
+
successSource(_ctx: any): Promise<void>;
|
|
6
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { translate } from '../host/i18n.js';
|
|
2
|
+
/**
|
|
3
|
+
* Fontes (sources) de renderização do Device Authorization Grant (RFC 8628).
|
|
4
|
+
*
|
|
5
|
+
* O oidc-provider chama estas funções com o KOA ctx (não o HttpContext do Adonis),
|
|
6
|
+
* então elas NÃO têm acesso ao renderer Inertia/Edge do host. Emitimos HTML
|
|
7
|
+
* auto-contido e i18n-izado (mesmo idioma das demais telas), o que também silencia
|
|
8
|
+
* os avisos `shouldChange` dos defaults da lib. As três telas:
|
|
9
|
+
* - userCodeInputSource: entrada do user-code (`/device`)
|
|
10
|
+
* - userCodeConfirmSource: confirmação após o code casar
|
|
11
|
+
* - successSource: tela final pós-aprovação
|
|
12
|
+
*/
|
|
13
|
+
function esc(value) {
|
|
14
|
+
return String(value ?? '')
|
|
15
|
+
.replace(/&/g, '&')
|
|
16
|
+
.replace(/</g, '<')
|
|
17
|
+
.replace(/>/g, '>')
|
|
18
|
+
.replace(/"/g, '"');
|
|
19
|
+
}
|
|
20
|
+
const STYLE = `
|
|
21
|
+
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;background:#f5f5f7;margin:0;padding:48px 16px;color:#1d1d1f}
|
|
22
|
+
.card{max-width:340px;margin:0 auto;background:#fff;border-radius:14px;padding:32px;box-shadow:0 1px 4px rgba(0,0,0,.08)}
|
|
23
|
+
h1{font-size:1.4rem;font-weight:600;margin:0 0 12px;text-align:center}
|
|
24
|
+
p{font-size:.95rem;line-height:1.5;text-align:center;color:#444}
|
|
25
|
+
p.red{color:#c0392b}
|
|
26
|
+
code{display:block;font-size:1.6rem;letter-spacing:.15em;text-align:center;margin:16px 0;font-weight:600}
|
|
27
|
+
input[type=text]{width:100%;box-sizing:border-box;height:46px;font-size:1rem;text-align:center;text-transform:uppercase;border:1px solid #d2d2d7;border-radius:10px;padding:0 12px;margin-bottom:14px}
|
|
28
|
+
button{width:100%;height:44px;font-size:.95rem;font-weight:600;color:#fff;background:#0071e3;border:0;border-radius:10px;cursor:pointer}
|
|
29
|
+
button:hover{background:#0077ed}
|
|
30
|
+
.abort{margin-top:12px;background:none;color:#666;font-weight:400}
|
|
31
|
+
.abort:hover{background:none;text-decoration:underline}
|
|
32
|
+
`;
|
|
33
|
+
function page(title, inner) {
|
|
34
|
+
return `<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${esc(title)}</title><style>${STYLE}</style></head><body><div class="card">${inner}</div></body></html>`;
|
|
35
|
+
}
|
|
36
|
+
export function createDeviceSources(messages) {
|
|
37
|
+
const t = (key, params) => translate(messages, key, params);
|
|
38
|
+
return {
|
|
39
|
+
async userCodeInputSource(_ctx, form, _out, err) {
|
|
40
|
+
const ctx = _ctx;
|
|
41
|
+
let msg;
|
|
42
|
+
if (err && (err.userCode || err.name === 'NoCodeError')) {
|
|
43
|
+
msg = `<p class="red">${esc(t('device.input.error_invalid'))}</p>`;
|
|
44
|
+
}
|
|
45
|
+
else if (err && err.name === 'AbortedError') {
|
|
46
|
+
msg = `<p class="red">${esc(t('device.input.error_aborted'))}</p>`;
|
|
47
|
+
}
|
|
48
|
+
else if (err) {
|
|
49
|
+
msg = `<p class="red">${esc(t('device.input.error_generic'))}</p>`;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
msg = `<p>${esc(t('device.input.intro'))}</p>`;
|
|
53
|
+
}
|
|
54
|
+
ctx.body = page(t('device.input.title'), `<h1>${esc(t('device.input.title'))}</h1>${msg}${form}<button type="submit" form="op.deviceInputForm">${esc(t('device.input.submit'))}</button>`);
|
|
55
|
+
},
|
|
56
|
+
async userCodeConfirmSource(_ctx, form, _client, _deviceInfo, userCode) {
|
|
57
|
+
const ctx = _ctx;
|
|
58
|
+
ctx.body = page(t('device.confirm.title'), `<h1>${esc(t('device.confirm.title'))}</h1><p>${esc(t('device.confirm.body'))}</p><code>${esc(userCode)}</code>${form}<button autofocus type="submit" form="op.deviceConfirmForm">${esc(t('device.confirm.submit'))}</button><button class="abort" type="submit" form="op.deviceConfirmForm" value="yes" name="abort">${esc(t('device.confirm.abort'))}</button>`);
|
|
59
|
+
},
|
|
60
|
+
async successSource(_ctx) {
|
|
61
|
+
const ctx = _ctx;
|
|
62
|
+
ctx.body = page(t('device.success.title'), `<h1>${esc(t('device.success.title'))}</h1><p>${esc(t('device.success.body'))}</p>`);
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -4,6 +4,11 @@ export interface InteractionDeps {
|
|
|
4
4
|
id: string;
|
|
5
5
|
} | null>;
|
|
6
6
|
}
|
|
7
|
+
/** Detalhes opcionais de login (step-up auth): acr alcançado + amr (métodos). */
|
|
8
|
+
export interface CompleteLoginExtra {
|
|
9
|
+
acr?: string;
|
|
10
|
+
amr?: string[];
|
|
11
|
+
}
|
|
7
12
|
export interface InteractionActions {
|
|
8
13
|
details(ctx: HttpContext): Promise<any>;
|
|
9
14
|
login(ctx: HttpContext, input: {
|
|
@@ -12,7 +17,7 @@ export interface InteractionActions {
|
|
|
12
17
|
}): Promise<{
|
|
13
18
|
ok: boolean;
|
|
14
19
|
}>;
|
|
15
|
-
completeLogin(ctx: HttpContext, accountId: string): Promise<{
|
|
20
|
+
completeLogin(ctx: HttpContext, accountId: string, extra?: CompleteLoginExtra): Promise<{
|
|
16
21
|
ok: boolean;
|
|
17
22
|
}>;
|
|
18
23
|
consent(ctx: HttpContext): Promise<unknown>;
|
|
@@ -14,8 +14,15 @@ export function createInteractionActions(provider, deps) {
|
|
|
14
14
|
await provider.interactionFinished(ctx.request.request, ctx.response.response, { login: { accountId: account.id } }, { mergeWithLastSubmission: false });
|
|
15
15
|
return { ok: true };
|
|
16
16
|
},
|
|
17
|
-
async completeLogin(ctx, accountId) {
|
|
18
|
-
|
|
17
|
+
async completeLogin(ctx, accountId, extra) {
|
|
18
|
+
// acr/amr (RFC 8176): quando um step-up de MFA foi efetivamente cumprido,
|
|
19
|
+
// passamos o acr alcançado + os métodos (amr) para que o id_token os carregue.
|
|
20
|
+
const login = { accountId };
|
|
21
|
+
if (extra?.acr)
|
|
22
|
+
login.acr = extra.acr;
|
|
23
|
+
if (extra?.amr && extra.amr.length)
|
|
24
|
+
login.amr = extra.amr;
|
|
25
|
+
await provider.interactionFinished(ctx.request.request, ctx.response.response, { login }, { mergeWithLastSubmission: false });
|
|
19
26
|
return { ok: true };
|
|
20
27
|
},
|
|
21
28
|
async consent(ctx) {
|
|
@@ -12,6 +12,21 @@ export declare class OidcService {
|
|
|
12
12
|
readonly interactions: InteractionActions;
|
|
13
13
|
get config(): ResolvedServerConfig;
|
|
14
14
|
constructor(config: ResolvedServerConfig, appKey: string, recorder?: MetricsRecorder);
|
|
15
|
+
/**
|
|
16
|
+
* Invalida o cache de clients DINÂMICOS do oidc-provider (a `dynamicClients`
|
|
17
|
+
* QuickLRU em `instance(provider)`). DEVE ser chamado após qualquer escrita
|
|
18
|
+
* (create/update/delete) no model `Client` via adapter, pelo console admin.
|
|
19
|
+
*
|
|
20
|
+
* NOTA sobre o porquê: o oidc-provider v9 cacheia clients carregados do adapter
|
|
21
|
+
* numa LRU CUJA CHAVE É O HASH (sha256) DO PAYLOAD persistido — não o client_id.
|
|
22
|
+
* Por isso uma alteração de metadata já é "auto-invalidante": `Client.find` relê o
|
|
23
|
+
* adapter, hasheia o payload NOVO, dá cache-miss e reconstrói o client. Mesmo assim
|
|
24
|
+
* limpamos a LRU explicitamente para (a) tornar o efeito imediato e determinístico
|
|
25
|
+
* (sem depender de pressão de LRU para expulsar a entrada antiga, agora inalcançável)
|
|
26
|
+
* e (b) liberar a entrada órfã na hora. É o caminho de invalidação suportado: a LRU
|
|
27
|
+
* é um detalhe interno acessível via o helper `weak_cache` do próprio provider.
|
|
28
|
+
*/
|
|
29
|
+
evictDynamicClientCache(): Promise<void>;
|
|
15
30
|
/** Verifica client_id + client_secret contra os clients da config (p/ endpoints custom como introspecção de PAT). */
|
|
16
31
|
verifyClientCredentials(clientId: string, clientSecret: string): boolean;
|
|
17
32
|
}
|
|
@@ -72,6 +72,33 @@ export class OidcService {
|
|
|
72
72
|
}
|
|
73
73
|
this.interactions = createInteractionActions(this.provider, { verifyCredentials: config.verifyCredentials });
|
|
74
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Invalida o cache de clients DINÂMICOS do oidc-provider (a `dynamicClients`
|
|
77
|
+
* QuickLRU em `instance(provider)`). DEVE ser chamado após qualquer escrita
|
|
78
|
+
* (create/update/delete) no model `Client` via adapter, pelo console admin.
|
|
79
|
+
*
|
|
80
|
+
* NOTA sobre o porquê: o oidc-provider v9 cacheia clients carregados do adapter
|
|
81
|
+
* numa LRU CUJA CHAVE É O HASH (sha256) DO PAYLOAD persistido — não o client_id.
|
|
82
|
+
* Por isso uma alteração de metadata já é "auto-invalidante": `Client.find` relê o
|
|
83
|
+
* adapter, hasheia o payload NOVO, dá cache-miss e reconstrói o client. Mesmo assim
|
|
84
|
+
* limpamos a LRU explicitamente para (a) tornar o efeito imediato e determinístico
|
|
85
|
+
* (sem depender de pressão de LRU para expulsar a entrada antiga, agora inalcançável)
|
|
86
|
+
* e (b) liberar a entrada órfã na hora. É o caminho de invalidação suportado: a LRU
|
|
87
|
+
* é um detalhe interno acessível via o helper `weak_cache` do próprio provider.
|
|
88
|
+
*/
|
|
89
|
+
async evictDynamicClientCache() {
|
|
90
|
+
try {
|
|
91
|
+
const wc = await import('oidc-provider/lib/helpers/weak_cache.js');
|
|
92
|
+
const get = wc.default ?? wc.get;
|
|
93
|
+
const int = get(this.provider);
|
|
94
|
+
int?.dynamicClients?.clear?.();
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Estrutura interna mudou numa versão futura do oidc-provider: a invalidação por
|
|
98
|
+
// hash-de-conteúdo (acima) continua garantindo correção; só perdemos a expulsão
|
|
99
|
+
// imediata da entrada órfã. Best-effort — não propaga erro pro caminho da request.
|
|
100
|
+
}
|
|
101
|
+
}
|
|
75
102
|
/** Verifica client_id + client_secret contra os clients da config (p/ endpoints custom como introspecção de PAT). */
|
|
76
103
|
verifyClientCredentials(clientId, clientSecret) {
|
|
77
104
|
const client = this.#clients.find((c) => c.clientId === clientId);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dudousxd/adonis-authkit-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "AdonisJS OIDC/OAuth2 provider (Identity Provider) toolkit: ejectable auth server with sessions, rate-limiting, MFA/TOTP, audit log, federated logout and OpenTelemetry metrics.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "dudousxd",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"oidc-provider": "^9.8.4",
|
|
77
77
|
"otplib": "^12.0.1",
|
|
78
78
|
"qrcode": "^1.5.4",
|
|
79
|
-
"@dudousxd/adonis-authkit-core": "0.
|
|
79
|
+
"@dudousxd/adonis-authkit-core": "0.2.0"
|
|
80
80
|
},
|
|
81
81
|
"devDependencies": {
|
|
82
82
|
"@adonisjs/ally": "6.3.0",
|