@adonis-agora/authkit-server 0.54.0 → 0.55.1

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 (63) hide show
  1. package/build/index.d.ts +4 -2
  2. package/build/index.js +2 -1
  3. package/build/providers/authkit_server_provider.js +18 -0
  4. package/build/src/accounts/lucid_account_store.d.ts +35 -0
  5. package/build/src/accounts/lucid_account_store.js +9 -0
  6. package/build/src/accounts/lucid_store/core.js +88 -19
  7. package/build/src/accounts/lucid_store/shared.d.ts +14 -0
  8. package/build/src/accounts/lucid_store/token_hash.d.ts +79 -0
  9. package/build/src/accounts/lucid_store/token_hash.js +145 -0
  10. package/build/src/define_config.d.ts +93 -8
  11. package/build/src/define_config.js +28 -2
  12. package/build/src/host/account_api/account_api_controller.js +5 -4
  13. package/build/src/host/admin_api/admin_users_service.js +2 -1
  14. package/build/src/host/admin_api/api_orgs_controller.js +2 -1
  15. package/build/src/host/admin_console/console_impersonation_controller.d.ts +15 -1
  16. package/build/src/host/admin_console/console_impersonation_controller.js +26 -2
  17. package/build/src/host/admin_console/console_orgs_controller.js +3 -1
  18. package/build/src/host/admin_validators.d.ts +3 -3
  19. package/build/src/host/auth_host_config.d.ts +30 -0
  20. package/build/src/host/auth_host_config.js +15 -0
  21. package/build/src/host/config_locks.d.ts +29 -0
  22. package/build/src/host/config_locks.js +46 -0
  23. package/build/src/host/console_session.d.ts +38 -2
  24. package/build/src/host/console_session.js +46 -2
  25. package/build/src/host/controllers/account_mfa_controller.js +5 -5
  26. package/build/src/host/controllers/account_orgs_controller.js +2 -1
  27. package/build/src/host/controllers/account_security_controller.js +6 -5
  28. package/build/src/host/controllers/interaction_controller.js +122 -26
  29. package/build/src/host/controllers/registration_controller.js +5 -4
  30. package/build/src/host/controllers/social_controller.js +37 -0
  31. package/build/src/host/default_mailer.d.ts +36 -5
  32. package/build/src/host/default_mailer.js +63 -10
  33. package/build/src/host/i18n.d.ts +10 -0
  34. package/build/src/host/i18n.js +16 -0
  35. package/build/src/host/login_attempt.d.ts +88 -2
  36. package/build/src/host/login_attempt.js +101 -48
  37. package/build/src/host/login_notify.js +2 -2
  38. package/build/src/host/oidc_rp_guard.d.ts +25 -2
  39. package/build/src/host/oidc_rp_guard.js +55 -9
  40. package/build/src/host/origin.d.ts +21 -0
  41. package/build/src/host/origin.js +22 -0
  42. package/build/src/host/register_auth_host.d.ts +104 -3
  43. package/build/src/host/register_auth_host.js +222 -26
  44. package/build/src/host/runtime_settings.d.ts +16 -0
  45. package/build/src/host/runtime_settings.js +24 -0
  46. package/build/src/host/runtime_toggles.d.ts +10 -0
  47. package/build/src/host/runtime_toggles.js +4 -0
  48. package/build/src/host/security_notice_service.d.ts +4 -2
  49. package/build/src/host/security_notice_service.js +4 -2
  50. package/build/src/host/sudo/index.d.ts +8 -0
  51. package/build/src/host/sudo/index.js +8 -0
  52. package/build/src/host/sudo/methods/magic_link.d.ts +17 -3
  53. package/build/src/host/sudo/methods/magic_link.js +37 -13
  54. package/build/src/host/sudo/runtime.d.ts +44 -4
  55. package/build/src/host/sudo/runtime.js +90 -6
  56. package/build/src/host/sudo/satisfiability.d.ts +62 -0
  57. package/build/src/host/sudo/satisfiability.js +89 -0
  58. package/build/src/password/common_passwords.js +27 -7
  59. package/build/src/provider/oidc_service.js +30 -13
  60. package/build/src/provider/token_exchange.d.ts +25 -1
  61. package/build/src/provider/token_exchange.js +29 -2
  62. package/package.json +6 -3
  63. /package/build/{password → src/password}/common_passwords.txt +0 -0
@@ -11,7 +11,7 @@
11
11
  * Fonte dos dados: compilação de senhas comuns de domínio público.
12
12
  * Licença do arquivo de dados: CC0 / Public Domain.
13
13
  */
14
- import { readFileSync } from 'node:fs';
14
+ import { existsSync, readFileSync } from 'node:fs';
15
15
  import { dirname, join } from 'node:path';
16
16
  import { fileURLToPath } from 'node:url';
17
17
  /**
@@ -23,16 +23,36 @@ let _commonPasswordsSet = null;
23
23
  /**
24
24
  * Carrega o arquivo de senhas comuns uma única vez (lazy).
25
25
  *
26
- * FAIL-SAFE: se o arquivo não existir ou não puder ser lido (ex.: bundle sem
27
- * assets), retorna um Set vazio a checagem vira no-op sem quebrar o fluxo.
26
+ * Onde o arquivo é esperado: o build script copia `common_passwords.txt`
27
+ * para o mesmo diretório do módulo compilado (`build/src/password/`, que
28
+ * `tsc` com `rootDir: "./"` preserva o subcaminho `src/`). Em dev via
29
+ * ts-exec (sem build), o módulo roda direto de `src/password/`, onde o
30
+ * `.txt` já mora ao lado do `.ts` no repo.
31
+ *
32
+ * Checamos mais de um candidato — não só o caminho-irmão — para sobreviver
33
+ * a uma mudança futura em `rootDir`/`outDir` ou a uma regressão no script de
34
+ * cópia do build: o caminho-irmão cobre o layout atual (build e dev); os
35
+ * demais são fallbacks para o layout antigo (`build/password/`) e para ler
36
+ * direto de `src/` a partir de um build.
37
+ *
38
+ * FAIL-SAFE: se o arquivo não existir em nenhum candidato, ou não puder ser
39
+ * lido (ex.: bundle sem assets), retorna um Set vazio — a checagem vira
40
+ * no-op sem quebrar o fluxo. Uma senha comum indevidamente aceita é sempre
41
+ * preferível a um login/signup quebrado por um asset ausente.
28
42
  */
29
43
  function loadCommonPasswords() {
30
44
  if (_commonPasswordsSet !== null)
31
45
  return _commonPasswordsSet;
32
46
  try {
33
- const __filename = fileURLToPath(import.meta.url);
34
- const __dir = dirname(__filename);
35
- const filePath = join(__dir, 'common_passwords.txt');
47
+ const __dir = dirname(fileURLToPath(import.meta.url));
48
+ const candidates = [
49
+ join(__dir, 'common_passwords.txt'), // caminho-irmão: build/src/password/ (prod) ou src/password/ (dev via ts-exec)
50
+ join(__dir, '..', '..', 'password', 'common_passwords.txt'), // layout antigo: build/password/
51
+ join(__dir, '..', '..', '..', 'src', 'password', 'common_passwords.txt'), // fallback: src/ a partir de um build
52
+ ];
53
+ const filePath = candidates.find((p) => existsSync(p));
54
+ if (!filePath)
55
+ throw new Error('common_passwords.txt not found in any candidate path');
36
56
  const content = readFileSync(filePath, 'utf-8');
37
57
  const entries = content
38
58
  .split('\n')
@@ -41,7 +61,7 @@ function loadCommonPasswords() {
41
61
  _commonPasswordsSet = new Set(entries);
42
62
  }
43
63
  catch {
44
- // Fail-safe: arquivo ausente → Set vazio (no-op check).
64
+ // Fail-safe: arquivo ausente em todos os candidatos → Set vazio (no-op check).
45
65
  _commonPasswordsSet = new Set();
46
66
  }
47
67
  return _commonPasswordsSet;
@@ -118,19 +118,36 @@ export class OidcService {
118
118
  },
119
119
  }, this.sessionTtlHolder, this.tokenTtlHolder);
120
120
  wireProviderEvents(provider, this.recorder);
121
- registerTokenExchange(provider, {
122
- findAccount: config.findAccount,
123
- globalRolesClaim: config.globalRolesClaim,
124
- resolveTokenRoles: config.resolveTokenRoles,
125
- // Resource indicators (RFC 8707) suportados: o `audience` default + cada
126
- // resource declarado. Usado para validar `audience`/`resource` no pedido de
127
- // token-exchange — alvos fora desta lista são rejeitados (invalid_target).
128
- supportedResources: [
129
- config.accessTokens.audience,
130
- ...Object.keys(config.accessTokens.resources),
131
- ],
132
- audit: config.audit,
133
- });
121
+ // KILL SWITCH de impersonation (`admin.impersonation`, default `true`).
122
+ // Com a flag em `false` o grant simplesmente NÃO EXISTE no provider — o
123
+ // token endpoint responde `unsupported_grant_type` — em vez de existir e
124
+ // recusar caso a caso. É a mesma flag que fecha o painel do console
125
+ // (`console_impersonation_controller.ts`): uma declaração, duas superfícies.
126
+ if (config.admin.impersonation) {
127
+ registerTokenExchange(provider, {
128
+ findAccount: config.findAccount,
129
+ globalRolesClaim: config.globalRolesClaim,
130
+ resolveTokenRoles: config.resolveTokenRoles,
131
+ // Allowlist de roles que autorizam impersonation — a MESMA de
132
+ // `config.admin.roles` usada pelos guards do console (default
133
+ // `['ADMIN']`). Permite que um host use papel próprio (ex.: `admin`
134
+ // minúsculo num store de roles próprio) sem alterar `globalRoles`.
135
+ adminRoles: config.admin.roles,
136
+ // Resource indicators (RFC 8707) suportados: o `audience` default + cada
137
+ // resource declarado. Usado para validar `audience`/`resource` no pedido de
138
+ // token-exchange — alvos fora desta lista são rejeitados (invalid_target).
139
+ supportedResources: [
140
+ config.accessTokens.audience,
141
+ ...Object.keys(config.accessTokens.resources),
142
+ ],
143
+ // LOAD-BEARING: sem isto o gate de status do alvo dentro de
144
+ // `token_exchange.ts` degrada para "allowed" (o campo é opcional, para não
145
+ // quebrar hosts com stores mínimos) e um admin consegue impersonar uma
146
+ // conta que acabou de desabilitar, recebendo tokens plenamente funcionais.
147
+ accountStore: config.accountStore,
148
+ audit: config.audit,
149
+ });
150
+ }
134
151
  // Quando o issuer tem um path (ex.: http://host/oidc), o provider precisa ser
135
152
  // MONTADO sob esse path via koa-mount. Isso faz o oidc-provider gerar URLs de
136
153
  // discovery e redirects de resume/interaction CORRETAMENTE prefixados (ex.:
@@ -1,4 +1,4 @@
1
- import type { AuthAccount } from '../accounts/account_store.js';
1
+ import type { AccountStore, AuthAccount } from '../accounts/account_store.js';
2
2
  import type { AuditSink } from '../audit/audit_sink.js';
3
3
  export interface TokenExchangeAccount {
4
4
  id: string;
@@ -14,6 +14,11 @@ export interface TokenExchangeDeps {
14
14
  * `target.globalRoles ?? []` (unchanged behavior). Mirrors the mint-time hook used
15
15
  * by the authorization-code flow so impersonated tokens source roles from the same
16
16
  * authority (e.g. @adonis-agora/authz) or custom store.
17
+ *
18
+ * The SAME hook also gates the ACTOR (the admin trying to impersonate): its roles
19
+ * are resolved through it too, so an app that keeps roles in its own table
20
+ * (e.g. via `@adonis-agora/authz`) can impersonate without storing roles in the
21
+ * authkit `globalRoles` column. When omitted, falls back to `actor.globalRoles`.
17
22
  */
18
23
  resolveTokenRoles?: (account: AuthAccount, context: {
19
24
  clientId?: string;
@@ -23,6 +28,14 @@ export interface TokenExchangeDeps {
23
28
  orgRole: string;
24
29
  } | null;
25
30
  }) => string[] | Promise<string[]>;
31
+ /**
32
+ * Allowlist de roles que autorizam a impersonation. Default: `['ADMIN']`.
33
+ * Aceita o papel EXATO (ex.: `'admin'` minúsculo) do app — resolve contra a
34
+ * MESMA fonte de roles do `resolveTokenRoles`/`globalRoles`, não a um papel
35
+ * hardcoded. Mantém `adminRole` como alias singular para back-compat.
36
+ */
37
+ adminRoles?: string[];
38
+ /** @deprecated Use `adminRoles` (allowlist). Mantido como alias singular. */
26
39
  adminRole?: string;
27
40
  /**
28
41
  * Resource indicators (RFC 8707) suportados pelo provider. Quando o pedido traz
@@ -32,5 +45,16 @@ export interface TokenExchangeDeps {
32
45
  supportedResources?: string[];
33
46
  /** Sink de auditoria (best-effort). Quando presente, registra `impersonation`. */
34
47
  audit?: AuditSink;
48
+ /**
49
+ * AccountStore do host, usado para checar se o alvo da impersonação está
50
+ * desabilitado/expirado (mesmo gate de `attemptPasswordLogin`, via
51
+ * `assertAccountEnabled`). Impersonar uma conta desabilitada mintaria tokens
52
+ * funcionais para uma identidade que deveria estar sem acesso algum.
53
+ *
54
+ * OPCIONAL e capability-probed (via `supportsAccountStatus`, dentro do
55
+ * helper): ausente ou sem a capacidade → degrada para "permitido" (mesma
56
+ * regra de todo o resto da lib — nunca quebra hosts com um store mínimo).
57
+ */
58
+ accountStore?: AccountStore;
35
59
  }
36
60
  export declare function registerTokenExchange(provider: any, deps: TokenExchangeDeps): void;
@@ -1,4 +1,5 @@
1
1
  import { errors } from 'oidc-provider';
2
+ import { assertAccountEnabled } from '../host/login_attempt.js';
2
3
  const TOKEN_EXCHANGE = 'urn:ietf:params:oauth:grant-type:token-exchange';
3
4
  const ACCESS_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token';
4
5
  /**
@@ -15,7 +16,7 @@ function intersectScopes(requested, allowed) {
15
16
  return out.join(' ');
16
17
  }
17
18
  export function registerTokenExchange(provider, deps) {
18
- const adminRole = deps.adminRole ?? 'ADMIN';
19
+ const adminRoles = deps.adminRoles ?? (deps.adminRole ? [deps.adminRole] : ['ADMIN']);
19
20
  const supportedResources = new Set(deps.supportedResources ?? []);
20
21
  const handler = async (ctx) => {
21
22
  const { params, client } = ctx.oidc;
@@ -35,7 +36,22 @@ export function registerTokenExchange(provider, deps) {
35
36
  throw new errors.InvalidGrant('subject_token was not issued to this client');
36
37
  }
37
38
  const actor = await deps.findAccount(subjectAt.accountId);
38
- if (!actor || !(actor.globalRoles ?? []).includes(adminRole)) {
39
+ if (!actor) {
40
+ throw new errors.InvalidGrant('actor not permitted to impersonate');
41
+ }
42
+ // Resolve as roles do ATOR pela MESMA fonte do claim (resolveTokenRoles
43
+ // quando o host configura — ex.: roles em tabela própria via
44
+ // `@adonis-agora/authz`), com fallback pro `globalRoles` do store. Sem isso
45
+ // um host que não grava roles na coluna `globalRoles` do authkit nunca
46
+ // conseguiria impersonar (o gate bateria sempre em `[]`). Mesmo padrão do
47
+ // console (`resolveAccountRoles` em `host/account_roles.ts`).
48
+ const actorRoles = deps.resolveTokenRoles
49
+ ? await deps.resolveTokenRoles(actor, {
50
+ clientId: client?.clientId,
51
+ activeOrg: null,
52
+ })
53
+ : (actor.globalRoles ?? []);
54
+ if (!actorRoles.some((r) => adminRoles.includes(r))) {
39
55
  throw new errors.InvalidGrant('actor not permitted to impersonate');
40
56
  }
41
57
  const targetId = params.requested_subject;
@@ -46,6 +62,17 @@ export function registerTokenExchange(provider, deps) {
46
62
  if (!target) {
47
63
  throw new errors.InvalidGrant('requested_subject not found');
48
64
  }
65
+ // Status do alvo (disabled/expirado): mesmo gate de `attemptPasswordLogin`.
66
+ // Sem isso, impersonar um alvo desabilitado mintava tokens funcionais para
67
+ // uma identidade que o admin acreditava ter revogado. `accountStore` é
68
+ // opcional/capability-probed — sem ele (ou sem a capacidade), permanece
69
+ // "permitido" como antes.
70
+ if (deps.accountStore) {
71
+ const statusGate = await assertAccountEnabled({ accountStore: deps.accountStore, audit: deps.audit }, target.id, { email: target.email ?? '', ip: ctx.req?.socket?.remoteAddress ?? null });
72
+ if (!statusGate.allowed) {
73
+ throw new errors.InvalidGrant('requested_subject is disabled');
74
+ }
75
+ }
49
76
  // audience/resource: se o pedido vier com um alvo, ele PRECISA estar entre os
50
77
  // resource indicators suportados. Caso contrário rejeitamos (conservador) —
51
78
  // nunca embutimos audiência arbitrária no token emitido.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adonis-agora/authkit-server",
3
- "version": "0.54.0",
3
+ "version": "0.55.1",
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",
@@ -124,6 +124,8 @@
124
124
  "@japa/assert": "4.2.0",
125
125
  "@japa/runner": "5.3.0",
126
126
  "@poppinss/ts-exec": "1.4.4",
127
+ "@types/better-sqlite3": "^7.6.13",
128
+ "@types/ioredis-mock": "^8.2.7",
127
129
  "@types/koa": "^2.15.2",
128
130
  "@types/koa-mount": "^4.0.5",
129
131
  "@types/luxon": "3.7.1",
@@ -151,11 +153,12 @@
151
153
  "@adonis-agora/authkit-react": "0.18.0"
152
154
  },
153
155
  "scripts": {
154
- "build": "node scripts/build_host_css.mjs && node scripts/build_webauthn.mjs && node scripts/build_ui.mjs && node -e \"const fs=require('node:fs');for(const d of ['build/stubs','build/host/views'])fs.rmSync(d,{recursive:true,force:true})\" && tsc && node -e \"require('node:fs').cpSync('src/host/assets','build/src/host/assets',{recursive:true})\" && node -e \"require('node:fs').cpSync('stubs','build/stubs',{recursive:true,filter:(s)=>!s.endsWith('.ts')})\" && node -e \"const fs=require('node:fs');if(fs.existsSync('assets'))fs.cpSync('assets','build/assets',{recursive:true})\" && node -e \"require('node:fs').cpSync('src/host/views','build/host/views',{recursive:true})\" && node -e \"const fs=require('node:fs');fs.mkdirSync('build/host/ui',{recursive:true});fs.readdirSync('src/host/ui').filter(f=>f.endsWith('.html')).forEach(f=>fs.copyFileSync('src/host/ui/'+f,'build/host/ui/'+f))\" && node -e \"require('node:fs').copyFileSync('commands/commands.json','build/commands/commands.json')\" && node -e \"const fs=require('node:fs');fs.mkdirSync('build/password',{recursive:true});fs.copyFileSync('src/password/common_passwords.txt','build/password/common_passwords.txt')\"",
156
+ "build": "node scripts/build_host_css.mjs && node scripts/build_webauthn.mjs && node scripts/build_ui.mjs && node -e \"const fs=require('node:fs');for(const d of ['build/stubs','build/host/views'])fs.rmSync(d,{recursive:true,force:true})\" && tsc && node -e \"require('node:fs').cpSync('src/host/assets','build/src/host/assets',{recursive:true})\" && node -e \"require('node:fs').cpSync('stubs','build/stubs',{recursive:true,filter:(s)=>!s.endsWith('.ts')})\" && node -e \"const fs=require('node:fs');if(fs.existsSync('assets'))fs.cpSync('assets','build/assets',{recursive:true})\" && node -e \"require('node:fs').cpSync('src/host/views','build/host/views',{recursive:true})\" && node -e \"const fs=require('node:fs');fs.mkdirSync('build/host/ui',{recursive:true});fs.readdirSync('src/host/ui').filter(f=>f.endsWith('.html')).forEach(f=>fs.copyFileSync('src/host/ui/'+f,'build/host/ui/'+f))\" && node -e \"require('node:fs').copyFileSync('commands/commands.json','build/commands/commands.json')\" && node -e \"const fs=require('node:fs');fs.mkdirSync('build/src/password',{recursive:true});fs.copyFileSync('src/password/common_passwords.txt','build/src/password/common_passwords.txt')\"",
155
157
  "build:ui": "node scripts/build_ui.mjs",
156
158
  "build:webauthn": "node scripts/build_webauthn.mjs",
157
159
  "check:webauthn-bundle": "node scripts/check_webauthn_bundle.mjs",
158
- "typecheck": "tsc --noEmit && node scripts/typecheck_ui.mjs",
160
+ "typecheck": "tsc --noEmit && node scripts/typecheck_ui.mjs && node scripts/typecheck_tests.mjs",
161
+ "typecheck:tests": "node scripts/typecheck_tests.mjs",
159
162
  "test": "node --import=@poppinss/ts-exec bin/test.ts",
160
163
  "build:host-css": "node scripts/build_host_css.mjs"
161
164
  }