@adonis-agora/authkit-server 0.68.4 → 0.70.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 (54) hide show
  1. package/build/commands/import_users.js +6 -1
  2. package/build/index.d.ts +5 -1
  3. package/build/index.js +7 -1
  4. package/build/src/accounts/account_store.d.ts +40 -0
  5. package/build/src/accounts/account_store.js +20 -0
  6. package/build/src/accounts/lucid_store/mfa.js +22 -0
  7. package/build/src/audit/audit_sink.d.ts +1 -1
  8. package/build/src/audit/audit_sink.js +4 -0
  9. package/build/src/commands/import_users.d.ts +7 -0
  10. package/build/src/commands/import_users.js +15 -3
  11. package/build/src/define_config.d.ts +81 -0
  12. package/build/src/define_config.js +18 -3
  13. package/build/src/host/account_api/account_api_controller.d.ts +2 -0
  14. package/build/src/host/account_api/account_api_controller.js +22 -3
  15. package/build/src/host/account_api/account_mfa_api_controller.d.ts +101 -0
  16. package/build/src/host/account_api/account_mfa_api_controller.js +286 -0
  17. package/build/src/host/account_api/account_orgs_api_controller.d.ts +126 -0
  18. package/build/src/host/account_api/account_orgs_api_controller.js +468 -0
  19. package/build/src/host/account_lockout.js +7 -2
  20. package/build/src/host/admin_api/admin_orgs_service.js +3 -2
  21. package/build/src/host/admin_api/admin_users_service.js +13 -3
  22. package/build/src/host/admin_api/dto.d.ts +1 -1
  23. package/build/src/host/admin_validators.d.ts +2 -2
  24. package/build/src/host/admin_validators.js +3 -2
  25. package/build/src/host/controllers/account_mfa_controller.js +1 -2
  26. package/build/src/host/controllers/account_orgs_controller.js +19 -11
  27. package/build/src/host/controllers/account_security_controller.js +3 -1
  28. package/build/src/host/controllers/account_session_controller.js +17 -6
  29. package/build/src/host/controllers/interaction_controller.d.ts +10 -0
  30. package/build/src/host/controllers/interaction_controller.js +93 -34
  31. package/build/src/host/controllers/registration_controller.js +35 -9
  32. package/build/src/host/controllers/social_controller.js +11 -2
  33. package/build/src/host/email_identifier.d.ts +92 -0
  34. package/build/src/host/email_identifier.js +234 -0
  35. package/build/src/host/idp_session_bridge.d.ts +55 -0
  36. package/build/src/host/idp_session_bridge.js +108 -0
  37. package/build/src/host/middleware/account_auth.js +3 -2
  38. package/build/src/host/org_policy.d.ts +26 -0
  39. package/build/src/host/org_policy.js +35 -0
  40. package/build/src/host/passkey_registration_challenge.d.ts +12 -0
  41. package/build/src/host/passkey_registration_challenge.js +12 -0
  42. package/build/src/host/register_auth_host.js +56 -1
  43. package/build/src/host/runtime_toggles.d.ts +2 -2
  44. package/build/src/host/runtime_toggles.js +3 -1
  45. package/build/src/host/sudo_mode.d.ts +17 -0
  46. package/build/src/host/sudo_mode.js +31 -12
  47. package/build/src/host/ui-dist/assets/{index-D9CYQnZR.js → index-Dct63ai-.js} +2 -2
  48. package/build/src/host/ui-dist/index.html +1 -1
  49. package/build/src/host/validators.d.ts +5 -5
  50. package/build/src/host/validators.js +17 -5
  51. package/build/src/provider/build_provider.js +46 -14
  52. package/build/src/provider/registration_policy.d.ts +99 -0
  53. package/build/src/provider/registration_policy.js +229 -0
  54. package/package.json +2 -2
@@ -8,7 +8,7 @@
8
8
  <link rel="preconnect" href="https://fonts.googleapis.com">
9
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
10
  <link href="https://fonts.googleapis.com/css2?family=Sora:wght@400;500;600;700&family=JetBrains+Mono:ital,wght@0,400;0,500;0,600;1,400&display=swap" rel="stylesheet">
11
- <script type="module" crossorigin src="/__AUTHKIT_BASE__/assets/index-D9CYQnZR.js"></script>
11
+ <script type="module" crossorigin src="/__AUTHKIT_BASE__/assets/index-Dct63ai-.js"></script>
12
12
  <link rel="stylesheet" crossorigin href="/__AUTHKIT_BASE__/assets/index-DTSmD4RU.css">
13
13
  </head>
14
14
  <body>
@@ -1,6 +1,6 @@
1
1
  /** Cadastro passwordless: só e-mail + nome (sem senha). O login vem por magic link. */
2
2
  export declare const passwordlessSignupValidator: import("@vinejs/vine").VineValidator<import("@vinejs/vine").VineObject<{
3
- email: import("@vinejs/vine").VineString;
3
+ email: import("@vinejs/vine/schema/base/literal").TransformModifier<import("@vinejs/vine").VineString, string>;
4
4
  fullName: import("@vinejs/vine").VineString;
5
5
  }, {
6
6
  email: string;
@@ -13,7 +13,7 @@ export declare const passwordlessSignupValidator: import("@vinejs/vine").VineVal
13
13
  fullName: string;
14
14
  }>, Record<string, any> | undefined>;
15
15
  export declare const signupValidator: import("@vinejs/vine").VineValidator<import("@vinejs/vine").VineObject<{
16
- email: import("@vinejs/vine").VineString;
16
+ email: import("@vinejs/vine/schema/base/literal").TransformModifier<import("@vinejs/vine").VineString, string>;
17
17
  fullName: import("@vinejs/vine").VineString;
18
18
  password: import("@vinejs/vine").VineString;
19
19
  }, {
@@ -30,7 +30,7 @@ export declare const signupValidator: import("@vinejs/vine").VineValidator<impor
30
30
  password: string;
31
31
  }>, Record<string, any> | undefined>;
32
32
  export declare const forgotPasswordValidator: import("@vinejs/vine").VineValidator<import("@vinejs/vine").VineObject<{
33
- email: import("@vinejs/vine").VineString;
33
+ email: import("@vinejs/vine/schema/base/literal").TransformModifier<import("@vinejs/vine").VineString, string>;
34
34
  }, {
35
35
  email: string;
36
36
  }, {
@@ -75,7 +75,7 @@ export declare const changePasswordValidator: import("@vinejs/vine").VineValidat
75
75
  */
76
76
  export declare const changeEmailValidator: import("@vinejs/vine").VineValidator<import("@vinejs/vine").VineObject<{
77
77
  currentPassword: import("@vinejs/vine/schema/base/literal").OptionalModifier<import("@vinejs/vine").VineString>;
78
- newEmail: import("@vinejs/vine").VineString;
78
+ newEmail: import("@vinejs/vine/schema/base/literal").TransformModifier<import("@vinejs/vine").VineString, string>;
79
79
  }, {
80
80
  currentPassword?: string | null | undefined;
81
81
  newEmail: string;
@@ -123,7 +123,7 @@ export declare const deleteAccountValidator: import("@vinejs/vine").VineValidato
123
123
  }>, Record<string, any> | undefined>;
124
124
  /** Criação de usuário no console admin (email obrigatório; nome/senha opcionais). */
125
125
  export declare const adminCreateUserValidator: import("@vinejs/vine").VineValidator<import("@vinejs/vine").VineObject<{
126
- email: import("@vinejs/vine").VineString;
126
+ email: import("@vinejs/vine/schema/base/literal").TransformModifier<import("@vinejs/vine").VineString, string>;
127
127
  name: import("@vinejs/vine/schema/base/literal").OptionalModifier<import("@vinejs/vine").VineString>;
128
128
  password: import("@vinejs/vine/schema/base/literal").OptionalModifier<import("@vinejs/vine").VineString>;
129
129
  }, {
@@ -1,16 +1,28 @@
1
1
  import vine from '@vinejs/vine';
2
+ import { normalizeEmailIdentifier } from './email_identifier.js';
3
+ /**
4
+ * E-mail usado como IDENTIDADE da conta. Normalização ÚNICA e conservadora
5
+ * ({@link normalizeEmailIdentifier}: `trim` + `toLowerCase`) — a MESMA aplicada
6
+ * no passo de identificador do login, para que os dois lados concordem.
7
+ *
8
+ * NÃO use `.normalizeEmail()` do VineJS aqui: ele aplica os defaults do
9
+ * validator.js, que no gmail REMOVEM os pontos e o sub-endereço `+tag` — a
10
+ * conta nascia com um endereço diferente do digitado e a pessoa ficava trancada
11
+ * do lado de fora (ver `host/email_identifier.ts`).
12
+ */
13
+ const emailIdentifier = () => vine.string().trim().email().transform(normalizeEmailIdentifier);
2
14
  /** Cadastro passwordless: só e-mail + nome (sem senha). O login vem por magic link. */
3
15
  export const passwordlessSignupValidator = vine.compile(vine.object({
4
- email: vine.string().trim().email().normalizeEmail(),
16
+ email: emailIdentifier(),
5
17
  fullName: vine.string().trim().minLength(2).maxLength(255),
6
18
  }));
7
19
  export const signupValidator = vine.compile(vine.object({
8
- email: vine.string().trim().email().normalizeEmail(),
20
+ email: emailIdentifier(),
9
21
  fullName: vine.string().trim().minLength(2).maxLength(255),
10
22
  password: vine.string().minLength(8).maxLength(255),
11
23
  }));
12
24
  export const forgotPasswordValidator = vine.compile(vine.object({
13
- email: vine.string().trim().email().normalizeEmail(),
25
+ email: emailIdentifier(),
14
26
  }));
15
27
  export const resetPasswordValidator = vine.compile(vine.object({
16
28
  token: vine.string().trim().minLength(1),
@@ -31,7 +43,7 @@ export const changePasswordValidator = vine.compile(vine.object({
31
43
  */
32
44
  export const changeEmailValidator = vine.compile(vine.object({
33
45
  currentPassword: vine.string().minLength(1).optional(),
34
- newEmail: vine.string().trim().email().normalizeEmail(),
46
+ newEmail: emailIdentifier(),
35
47
  }));
36
48
  /**
37
49
  * Edição de perfil no console de conta: nome e avatarUrl, ambos opcionais.
@@ -52,7 +64,7 @@ export const deleteAccountValidator = vine.compile(vine.object({
52
64
  }));
53
65
  /** Criação de usuário no console admin (email obrigatório; nome/senha opcionais). */
54
66
  export const adminCreateUserValidator = vine.compile(vine.object({
55
- email: vine.string().trim().email().normalizeEmail(),
67
+ email: emailIdentifier(),
56
68
  name: vine.string().trim().maxLength(255).optional(),
57
69
  password: vine.string().minLength(8).maxLength(255).optional(),
58
70
  }));
@@ -3,6 +3,7 @@ import { pickModelAdapterClass } from '../adapters/factory.js';
3
3
  import { normalizeActiveOrg, readActiveOrgFromKoaCtx } from '../host/active_org_cookie.js';
4
4
  import { createDeviceSources } from './device_sources.js';
5
5
  import { createLogoutSources } from './logout_sources.js';
6
+ import { registrationPolicyMiddleware } from './registration_policy.js';
6
7
  /** Atualiza o holder mutável do TTL de sessão com os valores da setting. */
7
8
  export function updateSessionTtlHolder(holder, policy) {
8
9
  holder.rememberSec = Math.max(1, Math.floor(policy.rememberDays * 86400));
@@ -47,18 +48,34 @@ export function buildProvider(config, options, sessionTtlHolder, tokenTtlHolder)
47
48
  },
48
49
  }
49
50
  : {};
50
- // Access Tokens RFC 9068 (JWT) via Resource Indicators (RFC 8707). Só montamos a
51
- // feature quando ALGUM AT deve ser JWT — caso contrário (default opaque) o
52
- // oidc-provider mantém o comportamento atual (AT opaco introspecionável) intocado.
51
+ // Resource Indicators (RFC 8707). A feature do oidc-provider é montada quando:
53
52
  //
54
- // Um JWT AT no oidc-provider SEMPRE exige um resource indicator com `aud`: o
55
- // `defaultResource` injeta a resource default (o `audience`, default issuer) quando
56
- // o client não pede `resource` explicitamente, e o `getResourceServerInfo` descreve
57
- // a API (scope/audience/formato/TTL) — onde `accessTokenFormat: 'jwt'` faz o token
58
- // sair como JWS `typ: at+jwt` assinado com a chave corrente do JWKS.
53
+ // - ALGUM AT deve ser JWT (RFC 9068): um JWT AT SEMPRE exige um resource com
54
+ // `aud`, então o `defaultResource` injeta o `audience` (default issuer)
55
+ // quando o client não pede `resource` — comportamento histórico; ou
56
+ // - há `accessTokens.resources` declarados, mesmo todos OPACOS: clientes que
57
+ // mandam `resource` (ex.: clientes MCP, cuja spec exige o parâmetro) passam
58
+ // a ser aceitos. Sem `resource` no pedido, NADA muda: o `defaultResource`
59
+ // devolve `undefined` e o AT continua o opaco de sempre (userinfo, sessão
60
+ // web), sem `aud`.
61
+ //
62
+ // Nos dois casos o `resource` pedido é validado contra a lista declarada
63
+ // (chaves de `resources` + o `audience` no modo JWT): fora dela → `invalid_target`
64
+ // (RFC 8707 §2). O resource concedido fica registrado no Grant (consent) e no
65
+ // AT (`aud` = `audience` da resource), inclusive no token opaco, que continua
66
+ // encontrável por `AccessToken.find` e introspecionável.
59
67
  const at = config.accessTokens;
60
68
  const allScopes = ['openid', 'profile', 'email', 'offline_access', 'roles'];
61
- const resourceIndicatorFeatures = at.anyJwt
69
+ const declaredResources = Object.keys(at.resources);
70
+ const findResource = (indicator) => {
71
+ if (at.resources[indicator])
72
+ return { key: indicator, rc: at.resources[indicator] };
73
+ // Tolerância à barra final (`https://app/mcp` ≡ `https://app/mcp/`).
74
+ const trimmed = indicator.replace(/\/+$/, '');
75
+ const key = declaredResources.find((k) => k.replace(/\/+$/, '') === trimmed);
76
+ return key ? { key, rc: at.resources[key] } : null;
77
+ };
78
+ const resourceIndicatorFeatures = at.anyJwt || declaredResources.length > 0
62
79
  ? {
63
80
  resourceIndicators: {
64
81
  enabled: true,
@@ -67,14 +84,20 @@ export function buildProvider(config, options, sessionTtlHolder, tokenTtlHolder)
67
84
  // resources já concedidas — devolvemos para não falhar a request.
68
85
  if (oneOf)
69
86
  return oneOf;
70
- // Authorize/sem resource explícito: liga ao resource default (modo simples).
71
- return at.audience;
87
+ // Authorize/sem resource explícito: no modo JWT, liga ao resource
88
+ // default (modo simples). Só opaco: sem resource (AT de sempre).
89
+ return at.anyJwt ? at.audience : undefined;
72
90
  },
73
91
  useGrantedResource: async () => true,
74
92
  getResourceServerInfo: (_ctx, resourceIndicator, _client) => {
75
- const rc = at.resources[resourceIndicator];
76
- const format = rc?.format ?? (resourceIndicator === at.audience ? at.format : 'opaque');
77
- const audience = rc?.audience ?? resourceIndicator;
93
+ const found = findResource(resourceIndicator);
94
+ const isDefault = at.anyJwt && resourceIndicator === at.audience;
95
+ if (!found && !isDefault) {
96
+ throw new oidc.errors.InvalidTarget(`resource indicator not allowed: ${resourceIndicator}`);
97
+ }
98
+ const rc = found?.rc;
99
+ const format = rc?.format ?? at.format;
100
+ const audience = rc?.audience ?? found?.key ?? resourceIndicator;
78
101
  const scope = (rc?.scopes ?? allScopes).join(' ');
79
102
  const info = {
80
103
  scope,
@@ -279,6 +302,15 @@ export function buildProvider(config, options, sessionTtlHolder, tokenTtlHolder)
279
302
  writable: true,
280
303
  configurable: true,
281
304
  });
305
+ // Política do registro dinâmico (redirect URIs + só fluxo de código + gancho do
306
+ // host). Middleware PRÉ-rota: roda antes do `/reg` do provider, que sozinho
307
+ // aceitaria qualquer redirect sintaticamente válido. Ver registration_policy.ts.
308
+ if (dynReg.enabled && (dynReg.redirectUriPolicy || dynReg.validateRegistration)) {
309
+ provider.use(registrationPolicyMiddleware({
310
+ policy: dynReg.redirectUriPolicy,
311
+ validate: dynReg.validateRegistration,
312
+ }));
313
+ }
282
314
  provider.proxy = true;
283
315
  return provider;
284
316
  }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Política do registro dinâmico de clients (RFC 7591 / RFC 7592).
3
+ *
4
+ * O oidc-provider, sozinho, aceita qualquer `redirect_uri` que passe na
5
+ * validação sintática de metadata (qualquer `https://…`, por exemplo). Com o
6
+ * registro ABERTO (sem Initial Access Token — o caso dos clientes MCP, que não
7
+ * têm como obter um IAT), isso deixa qualquer um registrar um client cujo
8
+ * callback é um domínio do atacante e usar a tela de consent do IdP como isca.
9
+ * O que protege o usuário é PARA ONDE o código de autorização pode ir.
10
+ *
11
+ * Esta política roda ANTES do provider (middleware Koa em `provider.use`), no
12
+ * `POST /reg` (criação) e no `PUT /reg/:clientId` (update do RFC 7592), e:
13
+ *
14
+ * 1. confere cada `redirect_uris` / `post_logout_redirect_uris` contra a
15
+ * política (loopback, URLs exatas, esquemas de app instalado, https);
16
+ * 2. restringe o client ao fluxo de código (`authorization_code` +
17
+ * `refresh_token`, `response_type=code`) — PKCE já é obrigatório no IdP;
18
+ * 3. normaliza `application_type: 'native'` quando todos os redirects são
19
+ * loopback/app instalado (é o que o client é, e sem isso o oidc-provider
20
+ * recusa esquema próprio);
21
+ * 4. chama o gancho `validateRegistration` do host, se houver.
22
+ *
23
+ * Funções puras + um middleware fino, para serem testadas isoladamente.
24
+ */
25
+ /** Política de redirect URIs aceita no registro dinâmico. */
26
+ export interface RedirectUriPolicy {
27
+ /**
28
+ * Aceita `http://localhost`, `http://127.0.0.1` e `http://[::1]` em QUALQUER
29
+ * porta e path (RFC 8252 §7.3 — apps nativos/CLIs escutam numa porta efêmera).
30
+ * Default: `true`.
31
+ */
32
+ loopback?: boolean;
33
+ /** URLs de callback aceitas por igualdade EXATA (ex.: callbacks de fornecedores). Default: `[]`. */
34
+ exact?: string[];
35
+ /**
36
+ * Esquemas privados de app instalado (RFC 8252 §7.1), sem o `:`, ex.:
37
+ * `['cursor', 'vscode']`. Default: `[]`.
38
+ */
39
+ appSchemes?: string[];
40
+ /**
41
+ * Aceita QUALQUER redirect `https://`. É o comportamento do oidc-provider sem
42
+ * política — só faz sentido com registro protegido por Initial Access Token.
43
+ * Default: `false`.
44
+ */
45
+ anyHttps?: boolean;
46
+ }
47
+ export interface ResolvedRedirectUriPolicy {
48
+ loopback: boolean;
49
+ exact: string[];
50
+ appSchemes: string[];
51
+ anyHttps: boolean;
52
+ }
53
+ /** Operação de registro que está sendo validada. */
54
+ export type RegistrationOperation = 'create' | 'update';
55
+ /**
56
+ * Gancho do host para validar/ajustar o metadata de um registro dinâmico,
57
+ * depois da política de redirect. Pode:
58
+ * - retornar `void` → segue com o metadata como está;
59
+ * - retornar um objeto → ele SUBSTITUI o metadata enviado ao provider;
60
+ * - lançar {@link RegistrationPolicyError} → o registro é recusado com
61
+ * `400 { error, error_description }`.
62
+ * Qualquer outro erro sobe (500).
63
+ */
64
+ export type ValidateRegistrationHook = (metadata: Record<string, unknown>, info: {
65
+ operation: RegistrationOperation;
66
+ ctx: unknown;
67
+ }) => void | Record<string, unknown> | Promise<void | Record<string, unknown>>;
68
+ /** Erro de política: vira `400 { error: code, error_description: message }`. */
69
+ export declare class RegistrationPolicyError extends Error {
70
+ readonly code: 'invalid_redirect_uri' | 'invalid_client_metadata';
71
+ constructor(code: 'invalid_redirect_uri' | 'invalid_client_metadata', description: string);
72
+ }
73
+ /** Default seguro do registro ABERTO: só loopback, nada de web arbitrário. */
74
+ export declare const OPEN_REGISTRATION_REDIRECT_POLICY: ResolvedRedirectUriPolicy;
75
+ export declare function resolveRedirectUriPolicy(input: RedirectUriPolicy): ResolvedRedirectUriPolicy;
76
+ export type RedirectKind = 'loopback' | 'app' | 'web';
77
+ /** Classifica um redirect pela política; `null` = fora dela. */
78
+ export declare function classifyRedirect(uri: string, policy: ResolvedRedirectUriPolicy): RedirectKind | null;
79
+ /**
80
+ * Confere o metadata de um registro contra a política. Devolve o metadata
81
+ * normalizado ou lança {@link RegistrationPolicyError}.
82
+ */
83
+ export declare function checkClientRegistration(metadata: Record<string, unknown>, policy: ResolvedRedirectUriPolicy): Record<string, unknown>;
84
+ /**
85
+ * Qual operação de registro o request atinge, com a MESMA regra de casamento do
86
+ * router do provider: case-insensitive e, se o path exato não casar, uma nova
87
+ * tentativa sem UMA barra final (`/reg/` ≡ `/reg`). `null` = não é registro.
88
+ */
89
+ export declare function registrationOperation(method: string, path: string, foldedBase: string): RegistrationOperation | null;
90
+ /**
91
+ * Middleware Koa (para `provider.use`) que aplica a política no registro
92
+ * dinâmico. `registrationPath` é o path da rota DENTRO do provider (default
93
+ * `/reg`; sob koa-mount o prefixo do issuer já foi removido).
94
+ */
95
+ export declare function registrationPolicyMiddleware(options: {
96
+ policy: ResolvedRedirectUriPolicy | null;
97
+ validate?: ValidateRegistrationHook;
98
+ registrationPath?: string;
99
+ }): (ctx: any, next: () => Promise<void>) => Promise<void>;
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Política do registro dinâmico de clients (RFC 7591 / RFC 7592).
3
+ *
4
+ * O oidc-provider, sozinho, aceita qualquer `redirect_uri` que passe na
5
+ * validação sintática de metadata (qualquer `https://…`, por exemplo). Com o
6
+ * registro ABERTO (sem Initial Access Token — o caso dos clientes MCP, que não
7
+ * têm como obter um IAT), isso deixa qualquer um registrar um client cujo
8
+ * callback é um domínio do atacante e usar a tela de consent do IdP como isca.
9
+ * O que protege o usuário é PARA ONDE o código de autorização pode ir.
10
+ *
11
+ * Esta política roda ANTES do provider (middleware Koa em `provider.use`), no
12
+ * `POST /reg` (criação) e no `PUT /reg/:clientId` (update do RFC 7592), e:
13
+ *
14
+ * 1. confere cada `redirect_uris` / `post_logout_redirect_uris` contra a
15
+ * política (loopback, URLs exatas, esquemas de app instalado, https);
16
+ * 2. restringe o client ao fluxo de código (`authorization_code` +
17
+ * `refresh_token`, `response_type=code`) — PKCE já é obrigatório no IdP;
18
+ * 3. normaliza `application_type: 'native'` quando todos os redirects são
19
+ * loopback/app instalado (é o que o client é, e sem isso o oidc-provider
20
+ * recusa esquema próprio);
21
+ * 4. chama o gancho `validateRegistration` do host, se houver.
22
+ *
23
+ * Funções puras + um middleware fino, para serem testadas isoladamente.
24
+ */
25
+ /** Erro de política: vira `400 { error: code, error_description: message }`. */
26
+ export class RegistrationPolicyError extends Error {
27
+ code;
28
+ constructor(code, description) {
29
+ super(description);
30
+ this.name = 'RegistrationPolicyError';
31
+ this.code = code;
32
+ }
33
+ }
34
+ /** Default seguro do registro ABERTO: só loopback, nada de web arbitrário. */
35
+ export const OPEN_REGISTRATION_REDIRECT_POLICY = Object.freeze({
36
+ loopback: true,
37
+ exact: [],
38
+ appSchemes: [],
39
+ anyHttps: false,
40
+ });
41
+ export function resolveRedirectUriPolicy(input) {
42
+ return {
43
+ loopback: input.loopback ?? true,
44
+ exact: [...(input.exact ?? [])],
45
+ appSchemes: (input.appSchemes ?? []).map((s) => s.replace(/:$/, '').toLowerCase()),
46
+ anyHttps: input.anyHttps ?? false,
47
+ };
48
+ }
49
+ const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]']);
50
+ /** Classifica um redirect pela política; `null` = fora dela. */
51
+ export function classifyRedirect(uri, policy) {
52
+ let url;
53
+ try {
54
+ url = new URL(uri);
55
+ }
56
+ catch {
57
+ return null;
58
+ }
59
+ // Credencial embutida e fragmento nunca são redirect legítimo (RFC 6749 §3.1.2).
60
+ if (url.username || url.password || url.hash)
61
+ return null;
62
+ if (policy.loopback && url.protocol === 'http:' && LOOPBACK_HOSTS.has(url.hostname)) {
63
+ return 'loopback';
64
+ }
65
+ if (policy.exact.includes(uri))
66
+ return 'web';
67
+ const scheme = url.protocol.slice(0, -1).toLowerCase();
68
+ if (policy.appSchemes.includes(scheme))
69
+ return 'app';
70
+ if (policy.anyHttps && url.protocol === 'https:')
71
+ return 'web';
72
+ return null;
73
+ }
74
+ function stringList(value) {
75
+ if (value === undefined)
76
+ return null;
77
+ if (!Array.isArray(value) || value.some((item) => typeof item !== 'string'))
78
+ return null;
79
+ return value;
80
+ }
81
+ const ALLOWED_GRANTS = new Set(['authorization_code', 'refresh_token']);
82
+ /**
83
+ * Confere o metadata de um registro contra a política. Devolve o metadata
84
+ * normalizado ou lança {@link RegistrationPolicyError}.
85
+ */
86
+ export function checkClientRegistration(metadata, policy) {
87
+ const redirects = stringList(metadata.redirect_uris);
88
+ if (!redirects || redirects.length === 0) {
89
+ throw new RegistrationPolicyError('invalid_redirect_uri', 'redirect_uris is required');
90
+ }
91
+ const kinds = [];
92
+ for (const uri of redirects) {
93
+ const kind = classifyRedirect(uri, policy);
94
+ if (!kind) {
95
+ throw new RegistrationPolicyError('invalid_redirect_uri', `redirect_uri not allowed by the registration policy: ${uri}`);
96
+ }
97
+ kinds.push(kind);
98
+ }
99
+ if (metadata.post_logout_redirect_uris !== undefined) {
100
+ const logout = stringList(metadata.post_logout_redirect_uris);
101
+ if (!logout || logout.some((uri) => !classifyRedirect(uri, policy))) {
102
+ throw new RegistrationPolicyError('invalid_client_metadata', 'post_logout_redirect_uris not allowed by the registration policy');
103
+ }
104
+ }
105
+ const grants = metadata.grant_types === undefined ? ['authorization_code'] : stringList(metadata.grant_types);
106
+ if (!grants?.includes('authorization_code') || grants.some((g) => !ALLOWED_GRANTS.has(g))) {
107
+ throw new RegistrationPolicyError('invalid_client_metadata', 'only the authorization_code and refresh_token grant types are allowed');
108
+ }
109
+ const responses = metadata.response_types === undefined ? ['code'] : stringList(metadata.response_types);
110
+ if (responses?.length !== 1 || responses[0] !== 'code') {
111
+ throw new RegistrationPolicyError('invalid_client_metadata', 'only response_type=code is allowed');
112
+ }
113
+ const native = kinds.every((kind) => kind !== 'web');
114
+ return native && metadata.application_type === undefined
115
+ ? { ...metadata, application_type: 'native' }
116
+ : metadata;
117
+ }
118
+ /** Mesmo teto do `selective_body` do oidc-provider. */
119
+ const BODY_LIMIT = 56 * 1024;
120
+ /**
121
+ * Lê o corpo JSON do request. Quando o Adonis já parseou (a ponte do
122
+ * `OidcCallbackController` põe o objeto em `req.body`), usa-o; senão consome o
123
+ * stream. Devolve `undefined` quando não dá para interpretar como objeto — o
124
+ * provider segue e responde o erro de parse ele mesmo.
125
+ */
126
+ async function readJsonBody(ctx) {
127
+ const req = ctx.req;
128
+ let raw;
129
+ if (req.readable && !req.readableEnded) {
130
+ const chunks = [];
131
+ let size = 0;
132
+ for await (const chunk of req) {
133
+ size += chunk.length;
134
+ if (size > BODY_LIMIT)
135
+ return undefined;
136
+ chunks.push(chunk);
137
+ }
138
+ raw = Buffer.concat(chunks).toString('utf8');
139
+ // O stream foi consumido: o provider cai no fallback `req.body`.
140
+ req.body = raw;
141
+ }
142
+ else {
143
+ raw = req.body ?? ctx.request?.body;
144
+ }
145
+ if (typeof raw === 'string' || Buffer.isBuffer(raw)) {
146
+ try {
147
+ raw = JSON.parse(raw.toString());
148
+ }
149
+ catch {
150
+ return undefined;
151
+ }
152
+ }
153
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
154
+ return undefined;
155
+ return raw;
156
+ }
157
+ /**
158
+ * Case-folding IDÊNTICO ao do router do oidc-provider (`lib/helpers/router.js`,
159
+ * `fold`): o router casa rotas sem diferenciar maiúsculas. Comparar o path cru
160
+ * deixaria `POST /REG` chegar ao handler de registro sem passar pela política.
161
+ */
162
+ function foldPath(value) {
163
+ for (let i = 0; i < value.length; i += 1) {
164
+ if (value.charCodeAt(i) > 127) {
165
+ let out = '';
166
+ for (const char of value) {
167
+ const upper = char.toUpperCase();
168
+ out +=
169
+ upper.length === 1 &&
170
+ !((char.codePointAt(0) ?? 0) > 127 && (upper.codePointAt(0) ?? 0) < 128)
171
+ ? upper
172
+ : char;
173
+ }
174
+ return out;
175
+ }
176
+ }
177
+ return value.toUpperCase();
178
+ }
179
+ /**
180
+ * Qual operação de registro o request atinge, com a MESMA regra de casamento do
181
+ * router do provider: case-insensitive e, se o path exato não casar, uma nova
182
+ * tentativa sem UMA barra final (`/reg/` ≡ `/reg`). `null` = não é registro.
183
+ */
184
+ export function registrationOperation(method, path, foldedBase) {
185
+ const folded = foldPath(path);
186
+ if (method === 'POST') {
187
+ const trimmed = folded.length > 1 && folded.endsWith('/') ? folded.slice(0, -1) : folded;
188
+ return folded === foldedBase || trimmed === foldedBase ? 'create' : null;
189
+ }
190
+ // `PUT <base>/:clientId` (RFC 7592). Mais largo que o router de propósito:
191
+ // tudo sob `<base>/` passa pela política.
192
+ if (method === 'PUT' && folded.startsWith(`${foldedBase}/`))
193
+ return 'update';
194
+ return null;
195
+ }
196
+ /**
197
+ * Middleware Koa (para `provider.use`) que aplica a política no registro
198
+ * dinâmico. `registrationPath` é o path da rota DENTRO do provider (default
199
+ * `/reg`; sob koa-mount o prefixo do issuer já foi removido).
200
+ */
201
+ export function registrationPolicyMiddleware(options) {
202
+ const base = foldPath(options.registrationPath ?? '/reg');
203
+ return async (ctx, next) => {
204
+ const operation = registrationOperation(ctx.method, ctx.path, base);
205
+ if (!operation || !ctx.is('application/json'))
206
+ return next();
207
+ const metadata = await readJsonBody(ctx);
208
+ if (!metadata)
209
+ return next();
210
+ try {
211
+ let checked = options.policy ? checkClientRegistration(metadata, options.policy) : metadata;
212
+ if (options.validate) {
213
+ const replaced = await options.validate(checked, { operation, ctx });
214
+ if (replaced && typeof replaced === 'object')
215
+ checked = replaced;
216
+ }
217
+ ctx.req.body = checked;
218
+ }
219
+ catch (error) {
220
+ if (!(error instanceof RegistrationPolicyError))
221
+ throw error;
222
+ ctx.status = 400;
223
+ ctx.set('cache-control', 'no-store');
224
+ ctx.body = { error: error.code, error_description: error.message };
225
+ return;
226
+ }
227
+ return next();
228
+ };
229
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adonis-agora/authkit-server",
3
- "version": "0.68.4",
3
+ "version": "0.70.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",
@@ -113,7 +113,7 @@
113
113
  "qrcode": "1.5.4"
114
114
  },
115
115
  "devDependencies": {
116
- "@adonis-agora/authkit-react": "0.21.0",
116
+ "@adonis-agora/authkit-react": "0.22.0",
117
117
  "@adonis-agora/durable": "0.40.0",
118
118
  "@adonis-agora/telescope": "0.21.1",
119
119
  "@adonisjs/ally": "6.3.0",