@adonis-agora/authkit-server 0.85.0 → 0.86.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.
|
@@ -74,7 +74,7 @@ export default class ConsoleUsersController {
|
|
|
74
74
|
roles: string[];
|
|
75
75
|
invited: boolean;
|
|
76
76
|
}>;
|
|
77
|
-
/** PATCH {prefix}/api/users/:id/roles */
|
|
77
|
+
/** PATCH {prefix}/api/users/:id/roles — body `{ roles }`. */
|
|
78
78
|
updateRoles(ctx: HttpContext): Promise<void | {
|
|
79
79
|
id: string;
|
|
80
80
|
email: string;
|
|
@@ -84,6 +84,22 @@ export default class ConsoleUsersController {
|
|
|
84
84
|
disabled: boolean;
|
|
85
85
|
roles: string[];
|
|
86
86
|
}>;
|
|
87
|
+
/**
|
|
88
|
+
* PATCH {prefix}/api/users/:id — body `{ globalRoles }`, a mesma forma do
|
|
89
|
+
* `PATCH /users/:id` da Admin API. É o que o `client.admin.users.update` do
|
|
90
|
+
* authkit-react chama, e o que o botão "Salvar papéis" do console usa: sem
|
|
91
|
+
* esta rota ele dava 404. Só papéis por aqui; nome e avatar não se editam no
|
|
92
|
+
* console.
|
|
93
|
+
*/
|
|
94
|
+
update(ctx: HttpContext): Promise<void | {
|
|
95
|
+
id: string;
|
|
96
|
+
email: string;
|
|
97
|
+
name: string | null;
|
|
98
|
+
avatarUrl: string | null;
|
|
99
|
+
globalRoles: string[];
|
|
100
|
+
disabled: boolean;
|
|
101
|
+
roles: string[];
|
|
102
|
+
}>;
|
|
87
103
|
/** POST {prefix}/api/users/:id/disable */
|
|
88
104
|
disable(ctx: HttpContext): Promise<void | {}>;
|
|
89
105
|
/** POST {prefix}/api/users/:id/enable */
|
|
@@ -6,7 +6,7 @@ import { ACCOUNT_SESSION_KEY } from '../account_session_key.js';
|
|
|
6
6
|
import { AdminUsersService } from '../admin_api/admin_users_service.js';
|
|
7
7
|
import { apiError, grantDto, rolesManagedExternallyError, sessionDto, userDtoWithRoles, } from '../admin_api/dto.js';
|
|
8
8
|
import { AdminSessionsService } from '../admin_sessions_service.js';
|
|
9
|
-
import { adminUserCreateValidator, adminUserRolesValidator } from '../admin_validators.js';
|
|
9
|
+
import { adminUserCreateValidator, adminUserRolesValidator, adminUserUpdateValidator, } from '../admin_validators.js';
|
|
10
10
|
import { resolveRuntimeSettings } from '../runtime_settings.js';
|
|
11
11
|
import { resolveEffectiveRolesCatalog } from '../runtime_toggles.js';
|
|
12
12
|
import { enrichSessionsWithContext } from '../session_context.js';
|
|
@@ -125,8 +125,23 @@ export default class ConsoleUsersController {
|
|
|
125
125
|
ctx.response.status(201);
|
|
126
126
|
return { ...(await userDtoWithRoles(cfg, result.account)), invited: result.invited };
|
|
127
127
|
}
|
|
128
|
-
/** PATCH {prefix}/api/users/:id/roles */
|
|
128
|
+
/** PATCH {prefix}/api/users/:id/roles — body `{ roles }`. */
|
|
129
129
|
async updateRoles(ctx) {
|
|
130
|
+
const { roles } = await ctx.request.validateUsing(adminUserRolesValidator);
|
|
131
|
+
return this.#setRoles(ctx, roles);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* PATCH {prefix}/api/users/:id — body `{ globalRoles }`, a mesma forma do
|
|
135
|
+
* `PATCH /users/:id` da Admin API. É o que o `client.admin.users.update` do
|
|
136
|
+
* authkit-react chama, e o que o botão "Salvar papéis" do console usa: sem
|
|
137
|
+
* esta rota ele dava 404. Só papéis por aqui; nome e avatar não se editam no
|
|
138
|
+
* console.
|
|
139
|
+
*/
|
|
140
|
+
async update(ctx) {
|
|
141
|
+
const { globalRoles } = await ctx.request.validateUsing(adminUserUpdateValidator);
|
|
142
|
+
return this.#setRoles(ctx, globalRoles);
|
|
143
|
+
}
|
|
144
|
+
async #setRoles(ctx, rolesInput) {
|
|
130
145
|
const service = await ctx.containerResolver.make('authkit.server');
|
|
131
146
|
const cfg = service.config;
|
|
132
147
|
const id = ctx.request.param('id');
|
|
@@ -139,7 +154,6 @@ export default class ConsoleUsersController {
|
|
|
139
154
|
return ctx.response.status(409).send(rolesManagedExternallyError());
|
|
140
155
|
}
|
|
141
156
|
const actorId = ctx.session?.get(ACCOUNT_SESSION_KEY) ?? null;
|
|
142
|
-
const { roles: rolesInput } = await ctx.request.validateUsing(adminUserRolesValidator);
|
|
143
157
|
const roles = Array.from(new Set((rolesInput ?? []).map((r) => r.trim()).filter(Boolean)));
|
|
144
158
|
const users = new AdminUsersService(cfg);
|
|
145
159
|
// Proteções de escalonamento/lockout: último admin + auto-rebaixamento.
|
|
@@ -29,6 +29,21 @@ export declare const adminGuard: (ctx: any, next: () => Promise<void>) => Promis
|
|
|
29
29
|
* (o `adminGuard` 404a quando `config.admin.enabled` é false). Mantenha-os em
|
|
30
30
|
* sincronia; os guards garantem que a divergência não vire um bypass.
|
|
31
31
|
*/
|
|
32
|
+
/**
|
|
33
|
+
* Rotas do fluxo de login que o host pode querer reforçar por conta própria
|
|
34
|
+
* (throttle mais apertado, WAF, log de auditoria, feature flag…).
|
|
35
|
+
*
|
|
36
|
+
* A lib continua aplicando o throttle padrão (`rateLimit`), mas estas são as
|
|
37
|
+
* rotas em que o host tem contexto para ir além — ex.: limitar `/identifier`
|
|
38
|
+
* por e-mail em vez de só por IP.
|
|
39
|
+
*/
|
|
40
|
+
export type AuthHostRouteKey = 'interaction' | 'identifier' | 'login' | 'mfa' | 'mfaCustomBegin' | 'mfaCustomVerify' | 'passwordExpired' | 'passkeyOptions' | 'passkeyVerify' | 'magic' | 'magicConsume' | 'otpVerify' | 'consent' | 'switchIdentifier';
|
|
41
|
+
/**
|
|
42
|
+
* Middleware do AdonisJS aceito por `route.use` — função, import lazy, o nome de um
|
|
43
|
+
* middleware do router, ou a tupla `[middleware, options]`. Tipado frouxo de propósito:
|
|
44
|
+
* a lib não conhece os middlewares do host e não deve inferir a forma deles.
|
|
45
|
+
*/
|
|
46
|
+
export type AuthHostMiddleware = unknown;
|
|
32
47
|
export interface AuthHostOptions {
|
|
33
48
|
/**
|
|
34
49
|
* Onde o provider OIDC é montado. Deve casar com o final do issuer. OPCIONAL:
|
|
@@ -58,6 +73,26 @@ export interface AuthHostOptions {
|
|
|
58
73
|
* NÃO espelhe o config aqui — omitir a chave já herda dele.
|
|
59
74
|
*/
|
|
60
75
|
rateLimit?: RateLimitConfigInput;
|
|
76
|
+
/**
|
|
77
|
+
* Middleware EXTRA por rota do fluxo de login, aplicado DEPOIS do throttle da lib.
|
|
78
|
+
*
|
|
79
|
+
* É o escape hatch para quem quer política própria sem fork: a rota continua sendo
|
|
80
|
+
* da lib (handler, validação, OIDC), mas o host pendura o que quiser nela —
|
|
81
|
+
* `throttle` do AdonisJS, verificação de origem, auditoria:
|
|
82
|
+
*
|
|
83
|
+
* ```ts
|
|
84
|
+
* registerAuthHost(router, {
|
|
85
|
+
* extraMiddleware: {
|
|
86
|
+
* identifier: [throttle({ limit: 5, window: '1 minute' })],
|
|
87
|
+
* },
|
|
88
|
+
* })
|
|
89
|
+
* ```
|
|
90
|
+
*
|
|
91
|
+
* Não é opção de POLÍTICA (não é travada pelo `defineConfig`): é encanamento.
|
|
92
|
+
* Rodeu antes do throttle da lib? Não: a ordem é lib → host, então o seu middleware
|
|
93
|
+
* vê a requisição depois da barreira padrão.
|
|
94
|
+
*/
|
|
95
|
+
extraMiddleware?: Partial<Record<AuthHostRouteKey, readonly AuthHostMiddleware[]>>;
|
|
61
96
|
/**
|
|
62
97
|
* Console admin opt-in (B6).
|
|
63
98
|
*
|
|
@@ -272,6 +307,24 @@ export interface AuthHostRouteMap {
|
|
|
272
307
|
} | null;
|
|
273
308
|
/** Nomes das rotas nomeadas (as demais herdam o auto-naming do AdonisJS). */
|
|
274
309
|
names: Record<string, string>;
|
|
310
|
+
/**
|
|
311
|
+
* Handles das rotas do fluxo de login (os `Route` do AdonisJS), por chave estável.
|
|
312
|
+
*
|
|
313
|
+
* Serve para o host pendurar middleware DEPOIS do registro (`route.use([...])`) ou
|
|
314
|
+
* inspecionar o que já está aplicado. Para o mesmo efeito de forma declarativa, use
|
|
315
|
+
* a opção `extraMiddleware` ao chamar `registerAuthHost`.
|
|
316
|
+
*/
|
|
317
|
+
routes: Partial<Record<AuthHostRouteKey, ReturnType<Router['post']>>>;
|
|
318
|
+
/** Pattern de cada rota do fluxo (para montar href sem hardcodar). `:uid` incluído. */
|
|
319
|
+
urls: Record<AuthHostRouteKey, string>;
|
|
320
|
+
/**
|
|
321
|
+
* URL concreta de uma rota do fluxo, com o `:uid` já preenchido.
|
|
322
|
+
*
|
|
323
|
+
* ```ts
|
|
324
|
+
* map.interactionUrl('magic', uid) // '/auth/interaction/<uid>/magic'
|
|
325
|
+
* ```
|
|
326
|
+
*/
|
|
327
|
+
interactionUrl: (key: AuthHostRouteKey, uid: string) => string;
|
|
275
328
|
/** Ids dos métodos de sudo cujas rotas foram montadas, na ordem de montagem. */
|
|
276
329
|
sudoMethods: string[];
|
|
277
330
|
/**
|
|
@@ -389,6 +389,7 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
389
389
|
const withLogin = (route) => {
|
|
390
390
|
if (throttles)
|
|
391
391
|
route.use([throttles.login]);
|
|
392
|
+
return route;
|
|
392
393
|
};
|
|
393
394
|
const withIntrospection = (route) => {
|
|
394
395
|
if (throttles)
|
|
@@ -406,6 +407,33 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
406
407
|
const withOtpLogin = (route) => {
|
|
407
408
|
if (throttles)
|
|
408
409
|
route.use([throttles.otpLogin]);
|
|
410
|
+
return route;
|
|
411
|
+
};
|
|
412
|
+
const hostRoutes = {};
|
|
413
|
+
const interactionUrls = {
|
|
414
|
+
interaction: '/auth/interaction/:uid',
|
|
415
|
+
identifier: '/auth/interaction/:uid/identifier',
|
|
416
|
+
login: '/auth/interaction/:uid/login',
|
|
417
|
+
mfa: '/auth/interaction/:uid/mfa',
|
|
418
|
+
mfaCustomBegin: '/auth/interaction/:uid/mfa/custom/:method/begin',
|
|
419
|
+
mfaCustomVerify: '/auth/interaction/:uid/mfa/custom/:method/verify',
|
|
420
|
+
passwordExpired: '/auth/interaction/:uid/password-expired',
|
|
421
|
+
passkeyOptions: '/auth/interaction/:uid/passkey/options',
|
|
422
|
+
passkeyVerify: '/auth/interaction/:uid/passkey/verify',
|
|
423
|
+
magic: '/auth/interaction/:uid/magic',
|
|
424
|
+
magicConsume: '/auth/interaction/:uid/magic',
|
|
425
|
+
otpVerify: '/auth/interaction/:uid/otp-verify',
|
|
426
|
+
consent: '/auth/interaction/:uid/consent',
|
|
427
|
+
switchIdentifier: '/auth/interaction/:uid/switch',
|
|
428
|
+
};
|
|
429
|
+
const keep = (key, route) => {
|
|
430
|
+
hostRoutes[key] = route;
|
|
431
|
+
const extraForRoute = opts.extraMiddleware?.[key];
|
|
432
|
+
if (extraForRoute && extraForRoute.length > 0) {
|
|
433
|
+
// `use` espera o MiddlewareList do AdonisJS; aqui é lista opaca do host.
|
|
434
|
+
route.use([...extraForRoute]);
|
|
435
|
+
}
|
|
436
|
+
return route;
|
|
409
437
|
};
|
|
410
438
|
// ─── Assets estáticos do host-kit (públicos, sem autenticação) ─────────────
|
|
411
439
|
// Bundle do @simplewebauthn/browser servido pelo próprio app, no lugar do
|
|
@@ -452,38 +480,46 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
452
480
|
router.any(`${mount}/*`, [C.oidc]).as('authkit.oidc.wildcard');
|
|
453
481
|
router.any(mount, [C.oidc]).as('authkit.oidc.root');
|
|
454
482
|
// Interaction (login multi-step + consent + signup).
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
483
|
+
// Cada rota passa por `keep(key, …)`: entra em `AuthHostRouteMap.routes`/`urls` e
|
|
484
|
+
// recebe o `extraMiddleware` do host (depois do throttle da lib).
|
|
485
|
+
keep('interaction', router.get('/auth/interaction/:uid', [C.interaction, 'show']));
|
|
486
|
+
// `/identifier` é a ENTRADA do fluxo: recebe só o e-mail e abre/avança a interaction.
|
|
487
|
+
// Sem throttle, um anônimo cria estado de interaction sem limite (Redis) e o passo
|
|
488
|
+
// fica de fora do orçamento que o resto do login respeita. Bucket `login` (por IP),
|
|
489
|
+
// o mesmo do `/magic`: um login normal gasta 2 dos 10 pontos por minuto.
|
|
490
|
+
keep('identifier', withLogin(router.post('/auth/interaction/:uid/identifier', [C.interaction, 'identifier'])));
|
|
491
|
+
withLogin(keep('login', router.post('/auth/interaction/:uid/login', [C.interaction, 'login'])));
|
|
492
|
+
withLogin(keep('mfa', router.post('/auth/interaction/:uid/mfa', [C.interaction, 'mfaVerify'])));
|
|
493
|
+
const customMfaBeginRoute = keep('mfaCustomBegin', router.post('/auth/interaction/:uid/mfa/custom/:method/begin', [
|
|
460
494
|
C.interaction,
|
|
461
495
|
'customMfaBegin',
|
|
462
|
-
]);
|
|
496
|
+
]));
|
|
463
497
|
withLogin(customMfaBeginRoute);
|
|
464
498
|
customMfaBeginRoute.as('authkit.mfa.custom.begin');
|
|
465
|
-
const customMfaVerifyRoute = router.post('/auth/interaction/:uid/mfa/custom/:method/verify', [
|
|
499
|
+
const customMfaVerifyRoute = keep('mfaCustomVerify', router.post('/auth/interaction/:uid/mfa/custom/:method/verify', [
|
|
466
500
|
C.interaction,
|
|
467
501
|
'customMfaVerify',
|
|
468
|
-
]);
|
|
502
|
+
]));
|
|
469
503
|
withLogin(customMfaVerifyRoute);
|
|
470
504
|
customMfaVerifyRoute.as('authkit.mfa.custom.verify');
|
|
471
505
|
// Troca de senha obrigatória quando a senha expirou (password expiration gate).
|
|
472
|
-
withLogin(router.post('/auth/interaction/:uid/password-expired', [
|
|
506
|
+
withLogin(keep('passwordExpired', router.post('/auth/interaction/:uid/password-expired', [
|
|
473
507
|
C.interaction,
|
|
474
508
|
'changeExpiredPassword',
|
|
475
|
-
]));
|
|
509
|
+
])));
|
|
476
510
|
// Passkey como 2º fator alternativo no login (begin/finish; challenge na sessão).
|
|
477
|
-
|
|
478
|
-
|
|
511
|
+
// O `options` também cria estado (challenge) e é anônimo: mesmo bucket `login` do
|
|
512
|
+
// `/identifier` e do `/magic`. Um login normal gasta 1 ponto aqui quando usa passkey.
|
|
513
|
+
keep('passkeyOptions', withLogin(router.post('/auth/interaction/:uid/passkey/options', [C.interaction, 'passkeyOptions'])));
|
|
514
|
+
withLogin(keep('passkeyVerify', router.post('/auth/interaction/:uid/passkey/verify', [C.interaction, 'passkeyVerify'])));
|
|
479
515
|
// Magic link (passwordless): POST emite (throttled), GET consome o token do link.
|
|
480
|
-
withLogin(router.post('/auth/interaction/:uid/magic', [C.interaction, 'magicLinkRequest']));
|
|
481
|
-
router.get('/auth/interaction/:uid/magic', [C.interaction, 'magicLinkConsume']);
|
|
516
|
+
withLogin(keep('magic', router.post('/auth/interaction/:uid/magic', [C.interaction, 'magicLinkRequest'])));
|
|
517
|
+
keep('magicConsume', router.get('/auth/interaction/:uid/magic', [C.interaction, 'magicLinkConsume']));
|
|
482
518
|
// Login por OTP (código digitável): verifica o código no bucket dedicado
|
|
483
519
|
// `authkit_otp_login` (por IP, mais apertado que o login).
|
|
484
|
-
withOtpLogin(router.post('/auth/interaction/:uid/otp-verify', [C.interaction, 'otpVerify']));
|
|
485
|
-
router.post('/auth/interaction/:uid/consent', [C.interaction, 'consent']);
|
|
486
|
-
router.get('/auth/interaction/:uid/switch', [C.interaction, 'switchIdentifier']);
|
|
520
|
+
keep('otpVerify', withOtpLogin(router.post('/auth/interaction/:uid/otp-verify', [C.interaction, 'otpVerify'])));
|
|
521
|
+
keep('consent', router.post('/auth/interaction/:uid/consent', [C.interaction, 'consent']));
|
|
522
|
+
keep('switchIdentifier', router.get('/auth/interaction/:uid/switch', [C.interaction, 'switchIdentifier']));
|
|
487
523
|
// OTP unlock: link enviado por e-mail quando o fator TOTP/recovery é travado.
|
|
488
524
|
router.get('/auth/otp-unlock/:token', [C.interaction, 'otpUnlock']);
|
|
489
525
|
router.get('/auth/interaction/:uid/signup', [C.registration, 'showSignup']);
|
|
@@ -867,6 +903,7 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
867
903
|
router.get(`${ap}/api/users/:id`, [C.consoleUsers, 'show']);
|
|
868
904
|
router.post(`${ap}/api/users`, [C.consoleUsers, 'store']);
|
|
869
905
|
router.patch(`${ap}/api/users/:id/roles`, [C.consoleUsers, 'updateRoles']);
|
|
906
|
+
router.patch(`${ap}/api/users/:id`, [C.consoleUsers, 'update']);
|
|
870
907
|
router.post(`${ap}/api/users/:id/disable`, [C.consoleUsers, 'disable']);
|
|
871
908
|
router.post(`${ap}/api/users/:id/enable`, [C.consoleUsers, 'enable']);
|
|
872
909
|
router.post(`${ap}/api/users/:id/reset-password`, [C.consoleUsers, 'resetPassword']);
|
|
@@ -1025,6 +1062,9 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
1025
1062
|
// inteiro, e não só metade (certo no servidor, errado na UI).
|
|
1026
1063
|
return {
|
|
1027
1064
|
mountPath: mount,
|
|
1065
|
+
routes: hostRoutes,
|
|
1066
|
+
urls: interactionUrls,
|
|
1067
|
+
interactionUrl: (key, uid) => interactionUrls[key].replace(':uid', encodeURIComponent(uid)),
|
|
1028
1068
|
account: {
|
|
1029
1069
|
prefix: accountPrefix(),
|
|
1030
1070
|
api: apiBase,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adonis-agora/authkit-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.86.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",
|