@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.
- package/build/index.d.ts +4 -2
- package/build/index.js +2 -1
- package/build/providers/authkit_server_provider.js +18 -0
- package/build/src/accounts/lucid_account_store.d.ts +35 -0
- package/build/src/accounts/lucid_account_store.js +9 -0
- package/build/src/accounts/lucid_store/core.js +88 -19
- package/build/src/accounts/lucid_store/shared.d.ts +14 -0
- package/build/src/accounts/lucid_store/token_hash.d.ts +79 -0
- package/build/src/accounts/lucid_store/token_hash.js +145 -0
- package/build/src/define_config.d.ts +93 -8
- package/build/src/define_config.js +28 -2
- package/build/src/host/account_api/account_api_controller.js +5 -4
- package/build/src/host/admin_api/admin_users_service.js +2 -1
- package/build/src/host/admin_api/api_orgs_controller.js +2 -1
- package/build/src/host/admin_console/console_impersonation_controller.d.ts +15 -1
- package/build/src/host/admin_console/console_impersonation_controller.js +26 -2
- package/build/src/host/admin_console/console_orgs_controller.js +3 -1
- package/build/src/host/admin_validators.d.ts +3 -3
- package/build/src/host/auth_host_config.d.ts +30 -0
- package/build/src/host/auth_host_config.js +15 -0
- package/build/src/host/config_locks.d.ts +29 -0
- package/build/src/host/config_locks.js +46 -0
- package/build/src/host/console_session.d.ts +38 -2
- package/build/src/host/console_session.js +46 -2
- package/build/src/host/controllers/account_mfa_controller.js +5 -5
- package/build/src/host/controllers/account_orgs_controller.js +2 -1
- package/build/src/host/controllers/account_security_controller.js +6 -5
- package/build/src/host/controllers/interaction_controller.js +122 -26
- package/build/src/host/controllers/registration_controller.js +5 -4
- package/build/src/host/controllers/social_controller.js +37 -0
- package/build/src/host/default_mailer.d.ts +36 -5
- package/build/src/host/default_mailer.js +63 -10
- package/build/src/host/i18n.d.ts +10 -0
- package/build/src/host/i18n.js +16 -0
- package/build/src/host/login_attempt.d.ts +88 -2
- package/build/src/host/login_attempt.js +101 -48
- package/build/src/host/login_notify.js +2 -2
- package/build/src/host/oidc_rp_guard.d.ts +25 -2
- package/build/src/host/oidc_rp_guard.js +55 -9
- package/build/src/host/origin.d.ts +21 -0
- package/build/src/host/origin.js +22 -0
- package/build/src/host/register_auth_host.d.ts +104 -3
- package/build/src/host/register_auth_host.js +222 -26
- package/build/src/host/runtime_settings.d.ts +16 -0
- package/build/src/host/runtime_settings.js +24 -0
- package/build/src/host/runtime_toggles.d.ts +10 -0
- package/build/src/host/runtime_toggles.js +4 -0
- package/build/src/host/security_notice_service.d.ts +4 -2
- package/build/src/host/security_notice_service.js +4 -2
- package/build/src/host/sudo/index.d.ts +8 -0
- package/build/src/host/sudo/index.js +8 -0
- package/build/src/host/sudo/methods/magic_link.d.ts +17 -3
- package/build/src/host/sudo/methods/magic_link.js +37 -13
- package/build/src/host/sudo/runtime.d.ts +44 -4
- package/build/src/host/sudo/runtime.js +90 -6
- package/build/src/host/sudo/satisfiability.d.ts +62 -0
- package/build/src/host/sudo/satisfiability.js +89 -0
- package/build/src/password/common_passwords.js +27 -7
- package/build/src/provider/oidc_service.js +30 -13
- package/build/src/provider/token_exchange.d.ts +25 -1
- package/build/src/provider/token_exchange.js +29 -2
- package/package.json +6 -3
- /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
|
-
*
|
|
27
|
-
*
|
|
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/`, já 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
|
|
34
|
-
const
|
|
35
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
config.
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
}
|
|
File without changes
|