@adonis-agora/authkit-server 0.53.0 → 0.55.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/index.d.ts +6 -2
- package/build/index.js +7 -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 +112 -0
- package/build/src/host/oidc_rp_guard.js +200 -0
- 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 +25 -13
- package/build/src/provider/token_exchange.d.ts +12 -1
- package/build/src/provider/token_exchange.js +12 -0
- package/package.json +6 -3
- /package/build/{password → src/password}/common_passwords.txt +0 -0
|
@@ -18,8 +18,17 @@ export declare function sudoContextFrom(ctx: HttpContext): Promise<SudoContext>;
|
|
|
18
18
|
* Registra a lista montada. Chamado UMA vez por `registerAuthHost`, e
|
|
19
19
|
* SUBSTITUI (não acumula): registrar o host de novo é redefinir o que existe,
|
|
20
20
|
* não somar ao que existia.
|
|
21
|
+
*
|
|
22
|
+
* `fromDefaults` distingue "esta é a lista de defaults da LIB" de "esta é a
|
|
23
|
+
* lista que o HOST escreveu" (argumento de `registerAuthHost` ou
|
|
24
|
+
* `config.sudo.methods`). A distinção existe porque a derivação do deadlock
|
|
25
|
+
* (`derivedSudoMethods`) só pode mexer na primeira: mexer na segunda seria
|
|
26
|
+
* mudar, sem pedir, o que um host declarou explicitamente — e a promessa de
|
|
27
|
+
* `sudoMethods` é que ele SUBSTITUI os defaults.
|
|
21
28
|
*/
|
|
22
|
-
export declare function setMountedSudoMethods(methods: SudoMethod[]
|
|
29
|
+
export declare function setMountedSudoMethods(methods: SudoMethod[], opts?: {
|
|
30
|
+
fromDefaults?: boolean;
|
|
31
|
+
}): void;
|
|
23
32
|
/** Um método com este id teve rotas montadas? Usado só para avisar de drift. */
|
|
24
33
|
export declare function isSudoMethodMounted(methodId: string): boolean;
|
|
25
34
|
/**
|
|
@@ -33,6 +42,29 @@ export declare function isSudoMethodMounted(methodId: string): boolean;
|
|
|
33
42
|
* circular se os métodos importassem de volta o controller.
|
|
34
43
|
*/
|
|
35
44
|
export declare function explicitSudoMethods(cfg: ResolvedServerConfig): SudoMethod[] | null;
|
|
45
|
+
/**
|
|
46
|
+
* O DEFAULT da lib, resolvido contra o config — o conserto do deadlock.
|
|
47
|
+
*
|
|
48
|
+
* `registerAuthHost` monta `[password, passkey, magicLink]` sem saber nada do
|
|
49
|
+
* config (a montagem acontece antes de o config lazy resolver). Aqui o config
|
|
50
|
+
* está resolvido, e é aqui que se decide qual metade vale:
|
|
51
|
+
*
|
|
52
|
+
* - host COM senha → `[password, passkey]`, o histórico byte a byte. O endpoint
|
|
53
|
+
* do magic link fica montado e inerte: quem quiser oferecê-lo declara
|
|
54
|
+
* `config.sudo.methods` — e agora isso FUNCIONA, porque a rota existe (era
|
|
55
|
+
* metade da promessa de 0.46 que ficava faltando).
|
|
56
|
+
* - host que declarou `authMethods: { password: false }` → `[passkey, magicLink]`.
|
|
57
|
+
* Sai o campo de senha, que nesse deployment é uma opção que não pode dar
|
|
58
|
+
* certo, e entra o único método que uma conta sem credencial prévia consegue
|
|
59
|
+
* satisfazer (ver `CREDENTIAL_FREE_SUDO_METHOD_IDS` em `sudo/satisfiability.ts`).
|
|
60
|
+
*
|
|
61
|
+
* SÓ MEXE NA LISTA DE DEFAULTS (`mountedFromDefaults`). Lista escrita pelo host
|
|
62
|
+
* passa intacta — inclusive quando isso o deixa em deadlock, caso em que o aviso
|
|
63
|
+
* de boot (`define_config.ts`) é quem fala. Adivinhar a intenção de uma lista
|
|
64
|
+
* explícita seria pior: é a lista dele, e a promessa documentada é que ela
|
|
65
|
+
* SUBSTITUI os defaults.
|
|
66
|
+
*/
|
|
67
|
+
export declare function derivedSudoMethods(cfg: ResolvedServerConfig): SudoMethod[];
|
|
36
68
|
/**
|
|
37
69
|
* O método `methodId` está habilitado para ESTE host?
|
|
38
70
|
*
|
|
@@ -41,9 +73,11 @@ export declare function explicitSudoMethods(cfg: ResolvedServerConfig): SudoMeth
|
|
|
41
73
|
* continuaria vivo e concedendo sudo — uma config que aparenta restringir e não
|
|
42
74
|
* restringe é pior que nenhuma config.
|
|
43
75
|
*
|
|
44
|
-
* Sem configuração explícita nada foi restringido: vale o que tem rota montada
|
|
45
|
-
*
|
|
46
|
-
*
|
|
76
|
+
* Sem configuração explícita nada foi restringido: vale o que tem rota montada,
|
|
77
|
+
* MENOS o que a derivação do default tirou (ver `isDefaultSudoMethodDerivedOut`).
|
|
78
|
+
* Continua não sendo a lista de defaults a fonte de verdade do que está montado —
|
|
79
|
+
* tratá-la como tal derrubaria um método customizado do host —, e a derivação só
|
|
80
|
+
* remove ids que a própria lib pôs na lista.
|
|
47
81
|
*/
|
|
48
82
|
export declare function isSudoMethodEnabled(cfg: ResolvedServerConfig, methodId: string): boolean;
|
|
49
83
|
/**
|
|
@@ -64,6 +98,12 @@ export declare function isSudoMethodEnabled(cfg: ResolvedServerConfig, methodId:
|
|
|
64
98
|
* fica estruturalmente impossível no caso sem config: é literalmente a mesma
|
|
65
99
|
* lista. O aviso de flag-drift do controller passa a valer só para o caso que
|
|
66
100
|
* sobra — config explícita divergindo do que foi montado.
|
|
101
|
+
*
|
|
102
|
+
* "A lista montada" é a lista montada DERIVADA (`derivedSudoMethods`), e os
|
|
103
|
+
* handlers aplicam a MESMA derivação pela mesma função — o drift continua
|
|
104
|
+
* impossível. A derivação existe porque a montagem não conhece o config: ela é
|
|
105
|
+
* quem tira o campo de senha de um host que declarou não ter senha, e quem
|
|
106
|
+
* mantém o default histórico intacto para quem tem.
|
|
67
107
|
*/
|
|
68
108
|
export declare function configuredSudoMethods(cfg: ResolvedServerConfig): SudoMethod[];
|
|
69
109
|
/**
|
|
@@ -40,13 +40,28 @@ export async function sudoContextFrom(ctx) {
|
|
|
40
40
|
* ver `configuredSudoMethods`.
|
|
41
41
|
*/
|
|
42
42
|
const mountedSudoMethods = [];
|
|
43
|
+
/**
|
|
44
|
+
* A lista montada é a de DEFAULTS da lib (e não uma escrita pelo host)?
|
|
45
|
+
*
|
|
46
|
+
* Só a lista de defaults é derivada do config (`derivedSudoMethods`). Uma lista
|
|
47
|
+
* que o host escreveu vale ao pé da letra — ver o docblock de `setMountedSudoMethods`.
|
|
48
|
+
*/
|
|
49
|
+
let mountedFromDefaults = false;
|
|
43
50
|
/**
|
|
44
51
|
* Registra a lista montada. Chamado UMA vez por `registerAuthHost`, e
|
|
45
52
|
* SUBSTITUI (não acumula): registrar o host de novo é redefinir o que existe,
|
|
46
53
|
* não somar ao que existia.
|
|
54
|
+
*
|
|
55
|
+
* `fromDefaults` distingue "esta é a lista de defaults da LIB" de "esta é a
|
|
56
|
+
* lista que o HOST escreveu" (argumento de `registerAuthHost` ou
|
|
57
|
+
* `config.sudo.methods`). A distinção existe porque a derivação do deadlock
|
|
58
|
+
* (`derivedSudoMethods`) só pode mexer na primeira: mexer na segunda seria
|
|
59
|
+
* mudar, sem pedir, o que um host declarou explicitamente — e a promessa de
|
|
60
|
+
* `sudoMethods` é que ele SUBSTITUI os defaults.
|
|
47
61
|
*/
|
|
48
|
-
export function setMountedSudoMethods(methods) {
|
|
62
|
+
export function setMountedSudoMethods(methods, opts = {}) {
|
|
49
63
|
mountedSudoMethods.splice(0, mountedSudoMethods.length, ...methods);
|
|
64
|
+
mountedFromDefaults = opts.fromDefaults === true;
|
|
50
65
|
}
|
|
51
66
|
/** Um método com este id teve rotas montadas? Usado só para avisar de drift. */
|
|
52
67
|
export function isSudoMethodMounted(methodId) {
|
|
@@ -66,6 +81,67 @@ export function explicitSudoMethods(cfg) {
|
|
|
66
81
|
const configured = cfg?.sudo?.methods;
|
|
67
82
|
return Array.isArray(configured) && configured.length ? configured : null;
|
|
68
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* O host declarou, PELO CONFIG, que este deployment não tem senha usável?
|
|
86
|
+
*
|
|
87
|
+
* `authMethods: { password: false }` é pin de config e é autoritativo — tem
|
|
88
|
+
* prioridade sobre o runtime setting e o console admin mostra o toggle travado.
|
|
89
|
+
* Quem declara isso está dizendo que ninguém entra por senha; contas criadas
|
|
90
|
+
* pelo signup passwordless nem têm senha (a coluna leva um hash aleatório
|
|
91
|
+
* inutilizável, indistinguível de um hash real de dentro do pacote).
|
|
92
|
+
*
|
|
93
|
+
* NÃO usa `passwordless.*`: aquelas flags LIGAM vias alternativas de login sem
|
|
94
|
+
* desligar a senha, então um host com `passwordless.magicLink: true` pode
|
|
95
|
+
* perfeitamente ter usuários que conhecem a própria senha. Só o pin em `false`
|
|
96
|
+
* é uma afirmação sobre o deployment inteiro.
|
|
97
|
+
*/
|
|
98
|
+
function isPasswordlessHost(cfg) {
|
|
99
|
+
return cfg?.authMethods?.password === false;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* O DEFAULT da lib, resolvido contra o config — o conserto do deadlock.
|
|
103
|
+
*
|
|
104
|
+
* `registerAuthHost` monta `[password, passkey, magicLink]` sem saber nada do
|
|
105
|
+
* config (a montagem acontece antes de o config lazy resolver). Aqui o config
|
|
106
|
+
* está resolvido, e é aqui que se decide qual metade vale:
|
|
107
|
+
*
|
|
108
|
+
* - host COM senha → `[password, passkey]`, o histórico byte a byte. O endpoint
|
|
109
|
+
* do magic link fica montado e inerte: quem quiser oferecê-lo declara
|
|
110
|
+
* `config.sudo.methods` — e agora isso FUNCIONA, porque a rota existe (era
|
|
111
|
+
* metade da promessa de 0.46 que ficava faltando).
|
|
112
|
+
* - host que declarou `authMethods: { password: false }` → `[passkey, magicLink]`.
|
|
113
|
+
* Sai o campo de senha, que nesse deployment é uma opção que não pode dar
|
|
114
|
+
* certo, e entra o único método que uma conta sem credencial prévia consegue
|
|
115
|
+
* satisfazer (ver `CREDENTIAL_FREE_SUDO_METHOD_IDS` em `sudo/satisfiability.ts`).
|
|
116
|
+
*
|
|
117
|
+
* SÓ MEXE NA LISTA DE DEFAULTS (`mountedFromDefaults`). Lista escrita pelo host
|
|
118
|
+
* passa intacta — inclusive quando isso o deixa em deadlock, caso em que o aviso
|
|
119
|
+
* de boot (`define_config.ts`) é quem fala. Adivinhar a intenção de uma lista
|
|
120
|
+
* explícita seria pior: é a lista dele, e a promessa documentada é que ela
|
|
121
|
+
* SUBSTITUI os defaults.
|
|
122
|
+
*/
|
|
123
|
+
export function derivedSudoMethods(cfg) {
|
|
124
|
+
return mountedSudoMethods.filter((m) => !isDefaultSudoMethodDerivedOut(cfg, m?.id));
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Este id foi DERIVADO PARA FORA da lista de defaults neste host?
|
|
128
|
+
*
|
|
129
|
+
* Ponto único da regra, consultado pelos DOIS lados (a tela, via
|
|
130
|
+
* `configuredSudoMethods`, e os handlers, via `isSudoMethodEnabled`) — é o que
|
|
131
|
+
* torna o drift entre eles estruturalmente impossível também no caso derivado.
|
|
132
|
+
*
|
|
133
|
+
* Nunca responde `true` para uma lista de host, nem para um id que não seja um
|
|
134
|
+
* dos dois built-in do default. Ou seja: só REMOVE, e só remove o que a própria
|
|
135
|
+
* lib pôs lá.
|
|
136
|
+
*/
|
|
137
|
+
function isDefaultSudoMethodDerivedOut(cfg, methodId) {
|
|
138
|
+
if (!mountedFromDefaults)
|
|
139
|
+
return false;
|
|
140
|
+
// Host passwordless: cai a senha (não há senha usável neste deployment).
|
|
141
|
+
// Host com senha: cai o magic link (posture histórica preservada — o default
|
|
142
|
+
// não passa a oferecer um step-up por e-mail a quem nunca pediu).
|
|
143
|
+
return isPasswordlessHost(cfg) ? methodId === 'password' : methodId === 'magic-link';
|
|
144
|
+
}
|
|
69
145
|
/**
|
|
70
146
|
* O método `methodId` está habilitado para ESTE host?
|
|
71
147
|
*
|
|
@@ -74,14 +150,16 @@ export function explicitSudoMethods(cfg) {
|
|
|
74
150
|
* continuaria vivo e concedendo sudo — uma config que aparenta restringir e não
|
|
75
151
|
* restringe é pior que nenhuma config.
|
|
76
152
|
*
|
|
77
|
-
* Sem configuração explícita nada foi restringido: vale o que tem rota montada
|
|
78
|
-
*
|
|
79
|
-
*
|
|
153
|
+
* Sem configuração explícita nada foi restringido: vale o que tem rota montada,
|
|
154
|
+
* MENOS o que a derivação do default tirou (ver `isDefaultSudoMethodDerivedOut`).
|
|
155
|
+
* Continua não sendo a lista de defaults a fonte de verdade do que está montado —
|
|
156
|
+
* tratá-la como tal derrubaria um método customizado do host —, e a derivação só
|
|
157
|
+
* remove ids que a própria lib pôs na lista.
|
|
80
158
|
*/
|
|
81
159
|
export function isSudoMethodEnabled(cfg, methodId) {
|
|
82
160
|
const explicit = explicitSudoMethods(cfg);
|
|
83
161
|
if (explicit === null)
|
|
84
|
-
return
|
|
162
|
+
return !isDefaultSudoMethodDerivedOut(cfg, methodId);
|
|
85
163
|
return explicit.some((m) => m?.id === methodId);
|
|
86
164
|
}
|
|
87
165
|
/**
|
|
@@ -102,9 +180,15 @@ export function isSudoMethodEnabled(cfg, methodId) {
|
|
|
102
180
|
* fica estruturalmente impossível no caso sem config: é literalmente a mesma
|
|
103
181
|
* lista. O aviso de flag-drift do controller passa a valer só para o caso que
|
|
104
182
|
* sobra — config explícita divergindo do que foi montado.
|
|
183
|
+
*
|
|
184
|
+
* "A lista montada" é a lista montada DERIVADA (`derivedSudoMethods`), e os
|
|
185
|
+
* handlers aplicam a MESMA derivação pela mesma função — o drift continua
|
|
186
|
+
* impossível. A derivação existe porque a montagem não conhece o config: ela é
|
|
187
|
+
* quem tira o campo de senha de um host que declarou não ter senha, e quem
|
|
188
|
+
* mantém o default histórico intacto para quem tem.
|
|
105
189
|
*/
|
|
106
190
|
export function configuredSudoMethods(cfg) {
|
|
107
|
-
return explicitSudoMethods(cfg) ??
|
|
191
|
+
return explicitSudoMethods(cfg) ?? derivedSudoMethods(cfg);
|
|
108
192
|
}
|
|
109
193
|
/**
|
|
110
194
|
* Verbos HTTP com a forma `(pattern, handler, ...)` — o handler é o SEGUNDO
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Este host consegue satisfazer o próprio sudo?" — a pergunta que ninguém
|
|
3
|
+
* fazia, e cuja resposta "não" trancava o usuário fora de exportar/excluir os
|
|
4
|
+
* próprios dados.
|
|
5
|
+
*
|
|
6
|
+
* MÓDULO FOLHA DE PROPÓSITO: importa só TIPOS. `define_config.ts` precisa
|
|
7
|
+
* chamar o aviso de boot daqui, e `sudo/runtime.ts` precisa da mesma lista de
|
|
8
|
+
* ids — importar `runtime.ts` de dentro do `define_config` fecharia um ciclo
|
|
9
|
+
* (`runtime` → controllers → ... → `define_config`) cuja ordem de avaliação é
|
|
10
|
+
* exatamente o tipo de coisa que quebra em produção e não em teste.
|
|
11
|
+
*/
|
|
12
|
+
import type { SudoMethod } from './types.js';
|
|
13
|
+
/**
|
|
14
|
+
* Métodos built-in que uma conta SEM senha e SEM passkey consegue satisfazer —
|
|
15
|
+
* os únicos que quebram o deadlock do host passwordless.
|
|
16
|
+
*
|
|
17
|
+
* `oidc-step-up` não exige nada previamente cadastrado (é o `prompt=login` do
|
|
18
|
+
* próprio protocolo); `magic-link` exige apenas que a conta tenha e-mail, e a
|
|
19
|
+
* ENTREGA tem fallback para o mailer default do host-kit, então nem hook é
|
|
20
|
+
* obrigatório. Os outros dois built-in — `password` e `passkey` — exigem, por
|
|
21
|
+
* definição, uma credencial que o usuário teria de ter cadastrado ANTES, e é
|
|
22
|
+
* essa a pré-condição que um host passwordless não satisfaz.
|
|
23
|
+
*/
|
|
24
|
+
export declare const CREDENTIAL_FREE_SUDO_METHOD_IDS: readonly string[];
|
|
25
|
+
/**
|
|
26
|
+
* Avisa, NO BOOT, quando a configuração de sudo deste host não tem um único
|
|
27
|
+
* método que uma conta sem senha possa satisfazer.
|
|
28
|
+
*
|
|
29
|
+
* QUANDO DISPARA (as quatro condições, todas necessárias):
|
|
30
|
+
*
|
|
31
|
+
* 1. O host declarou `sudo.methods` EXPLICITAMENTE. Sem declaração vale o
|
|
32
|
+
* default derivado, que já resolve o caso passwordless — não há o que avisar.
|
|
33
|
+
* 2. O deployment tem contas sem senha usável: `authMethods.password === false`
|
|
34
|
+
* (ninguém entra por senha) ou `passwordless.signup === true` (o cadastro
|
|
35
|
+
* público cria contas com um hash aleatório inutilizável).
|
|
36
|
+
* 3. Nenhum dos métodos declarados é credential-free.
|
|
37
|
+
* 4. TODOS os métodos declarados são built-in deste pacote — ou seja, o pacote
|
|
38
|
+
* consegue de fato PROVAR que a lista exige credencial prévia.
|
|
39
|
+
*
|
|
40
|
+
* WARN, NÃO THROW. Recusar o boot seria transformar um upgrade de patch/minor
|
|
41
|
+
* numa aplicação que não sobe, em produção, por uma condição que é
|
|
42
|
+
* DEGRADAÇÃO — a tela de confirmação fica sem opções, o que já falha fechado — e
|
|
43
|
+
* não uma brecha. Pior: a remediação nem sempre é uma linha de config
|
|
44
|
+
* (`oidcStepUp` exige uma rota e um callback no host), então o app ficaria fora
|
|
45
|
+
* do ar até alguém escrever código. E a detecção é, por construção, incompleta
|
|
46
|
+
* (item 4): um throw baseado numa heurística derruba host correto. O irmão
|
|
47
|
+
* `adonis-agent` recusa montar quando a superfície PROVADAMENTE não funciona;
|
|
48
|
+
* aqui não há prova — há forte suspeita, e o lugar disso é um aviso alto.
|
|
49
|
+
*
|
|
50
|
+
* @returns a mensagem emitida, ou `null` quando não havia nada a avisar (para
|
|
51
|
+
* teste; o efeito de verdade é o `console.warn`).
|
|
52
|
+
*/
|
|
53
|
+
export declare function warnUnsatisfiableSudoConfig(input: {
|
|
54
|
+
/** `config.sudo.methods` — a lista que o host declarou, se declarou. */
|
|
55
|
+
methods?: SudoMethod[];
|
|
56
|
+
/** `authMethods.password` pinado em `false`? */
|
|
57
|
+
passwordPinnedOff: boolean;
|
|
58
|
+
/** `passwordless.signup` ligado? */
|
|
59
|
+
passwordlessSignup: boolean;
|
|
60
|
+
/** Injeção para teste. Default: `console.warn`. */
|
|
61
|
+
warn?: (message: string) => void;
|
|
62
|
+
}): string | null;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Este host consegue satisfazer o próprio sudo?" — a pergunta que ninguém
|
|
3
|
+
* fazia, e cuja resposta "não" trancava o usuário fora de exportar/excluir os
|
|
4
|
+
* próprios dados.
|
|
5
|
+
*
|
|
6
|
+
* MÓDULO FOLHA DE PROPÓSITO: importa só TIPOS. `define_config.ts` precisa
|
|
7
|
+
* chamar o aviso de boot daqui, e `sudo/runtime.ts` precisa da mesma lista de
|
|
8
|
+
* ids — importar `runtime.ts` de dentro do `define_config` fecharia um ciclo
|
|
9
|
+
* (`runtime` → controllers → ... → `define_config`) cuja ordem de avaliação é
|
|
10
|
+
* exatamente o tipo de coisa que quebra em produção e não em teste.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Métodos built-in que uma conta SEM senha e SEM passkey consegue satisfazer —
|
|
14
|
+
* os únicos que quebram o deadlock do host passwordless.
|
|
15
|
+
*
|
|
16
|
+
* `oidc-step-up` não exige nada previamente cadastrado (é o `prompt=login` do
|
|
17
|
+
* próprio protocolo); `magic-link` exige apenas que a conta tenha e-mail, e a
|
|
18
|
+
* ENTREGA tem fallback para o mailer default do host-kit, então nem hook é
|
|
19
|
+
* obrigatório. Os outros dois built-in — `password` e `passkey` — exigem, por
|
|
20
|
+
* definição, uma credencial que o usuário teria de ter cadastrado ANTES, e é
|
|
21
|
+
* essa a pré-condição que um host passwordless não satisfaz.
|
|
22
|
+
*/
|
|
23
|
+
export const CREDENTIAL_FREE_SUDO_METHOD_IDS = ['oidc-step-up', 'magic-link'];
|
|
24
|
+
/**
|
|
25
|
+
* Ids dos métodos que ESTE pacote implementa.
|
|
26
|
+
*
|
|
27
|
+
* Serve para o aviso abaixo se CALAR diante de um método customizado: de um
|
|
28
|
+
* método do SPI o pacote não tem como saber se ele exige credencial prévia, e um
|
|
29
|
+
* aviso de boot que grita para uma configuração correta é um aviso que o host
|
|
30
|
+
* aprende a ignorar — justamente o que não pode acontecer com este.
|
|
31
|
+
*/
|
|
32
|
+
const BUILTIN_SUDO_METHOD_IDS = [
|
|
33
|
+
'password',
|
|
34
|
+
'passkey',
|
|
35
|
+
'oidc-step-up',
|
|
36
|
+
'magic-link',
|
|
37
|
+
];
|
|
38
|
+
/**
|
|
39
|
+
* Avisa, NO BOOT, quando a configuração de sudo deste host não tem um único
|
|
40
|
+
* método que uma conta sem senha possa satisfazer.
|
|
41
|
+
*
|
|
42
|
+
* QUANDO DISPARA (as quatro condições, todas necessárias):
|
|
43
|
+
*
|
|
44
|
+
* 1. O host declarou `sudo.methods` EXPLICITAMENTE. Sem declaração vale o
|
|
45
|
+
* default derivado, que já resolve o caso passwordless — não há o que avisar.
|
|
46
|
+
* 2. O deployment tem contas sem senha usável: `authMethods.password === false`
|
|
47
|
+
* (ninguém entra por senha) ou `passwordless.signup === true` (o cadastro
|
|
48
|
+
* público cria contas com um hash aleatório inutilizável).
|
|
49
|
+
* 3. Nenhum dos métodos declarados é credential-free.
|
|
50
|
+
* 4. TODOS os métodos declarados são built-in deste pacote — ou seja, o pacote
|
|
51
|
+
* consegue de fato PROVAR que a lista exige credencial prévia.
|
|
52
|
+
*
|
|
53
|
+
* WARN, NÃO THROW. Recusar o boot seria transformar um upgrade de patch/minor
|
|
54
|
+
* numa aplicação que não sobe, em produção, por uma condição que é
|
|
55
|
+
* DEGRADAÇÃO — a tela de confirmação fica sem opções, o que já falha fechado — e
|
|
56
|
+
* não uma brecha. Pior: a remediação nem sempre é uma linha de config
|
|
57
|
+
* (`oidcStepUp` exige uma rota e um callback no host), então o app ficaria fora
|
|
58
|
+
* do ar até alguém escrever código. E a detecção é, por construção, incompleta
|
|
59
|
+
* (item 4): um throw baseado numa heurística derruba host correto. O irmão
|
|
60
|
+
* `adonis-agent` recusa montar quando a superfície PROVADAMENTE não funciona;
|
|
61
|
+
* aqui não há prova — há forte suspeita, e o lugar disso é um aviso alto.
|
|
62
|
+
*
|
|
63
|
+
* @returns a mensagem emitida, ou `null` quando não havia nada a avisar (para
|
|
64
|
+
* teste; o efeito de verdade é o `console.warn`).
|
|
65
|
+
*/
|
|
66
|
+
export function warnUnsatisfiableSudoConfig(input) {
|
|
67
|
+
const declared = Array.isArray(input.methods) ? input.methods : [];
|
|
68
|
+
if (!declared.length)
|
|
69
|
+
return null;
|
|
70
|
+
if (!input.passwordPinnedOff && !input.passwordlessSignup)
|
|
71
|
+
return null;
|
|
72
|
+
const ids = declared.map((m) => m?.id).filter((id) => typeof id === 'string');
|
|
73
|
+
if (ids.length !== declared.length)
|
|
74
|
+
return null; // lista malformada: não é este o aviso
|
|
75
|
+
if (ids.some((id) => CREDENTIAL_FREE_SUDO_METHOD_IDS.includes(id)))
|
|
76
|
+
return null;
|
|
77
|
+
if (!ids.every((id) => BUILTIN_SUDO_METHOD_IDS.includes(id)))
|
|
78
|
+
return null;
|
|
79
|
+
const trigger = input.passwordPinnedOff
|
|
80
|
+
? 'authMethods: { password: false }'
|
|
81
|
+
: 'passwordless: { signup: true }';
|
|
82
|
+
const message = [
|
|
83
|
+
`authkit: sudo mode SEM SAÍDA para contas sem senha. Este host declarou \`${trigger}\`, então existem contas sem senha usável — mas \`sudo.methods\` só lista [${ids.join(', ')}], e todos exigem uma credencial cadastrada ANTES.`,
|
|
84
|
+
'Consequência: essas contas ficam trancadas fora de TODA operação sob `requireSudo` — exportar/excluir dados (LGPD), MFA, Personal Access Tokens, troca de e-mail — inclusive fora do cadastro de passkey, que é o que destravaria o resto.',
|
|
85
|
+
'Saída: acrescente `sudoMethods.oidcStepUp({ url: "/auth/step-up" })` (reautenticação no seu IdP) ou `sudoMethods.magicLink()` (link de confirmação por e-mail, enviado pelo mailer do app quando não há hook `mail.onSudoLink`) à lista de `sudo.methods`.',
|
|
86
|
+
].join(' ');
|
|
87
|
+
(input.warn ?? console.warn)(message);
|
|
88
|
+
return message;
|
|
89
|
+
}
|
|
@@ -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,31 @@ 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
|
+
// Resource indicators (RFC 8707) suportados: o `audience` default + cada
|
|
132
|
+
// resource declarado. Usado para validar `audience`/`resource` no pedido de
|
|
133
|
+
// token-exchange — alvos fora desta lista são rejeitados (invalid_target).
|
|
134
|
+
supportedResources: [
|
|
135
|
+
config.accessTokens.audience,
|
|
136
|
+
...Object.keys(config.accessTokens.resources),
|
|
137
|
+
],
|
|
138
|
+
// LOAD-BEARING: sem isto o gate de status do alvo dentro de
|
|
139
|
+
// `token_exchange.ts` degrada para "allowed" (o campo é opcional, para não
|
|
140
|
+
// quebrar hosts com stores mínimos) e um admin consegue impersonar uma
|
|
141
|
+
// conta que acabou de desabilitar, recebendo tokens plenamente funcionais.
|
|
142
|
+
accountStore: config.accountStore,
|
|
143
|
+
audit: config.audit,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
134
146
|
// Quando o issuer tem um path (ex.: http://host/oidc), o provider precisa ser
|
|
135
147
|
// MONTADO sob esse path via koa-mount. Isso faz o oidc-provider gerar URLs de
|
|
136
148
|
// 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;
|
|
@@ -32,5 +32,16 @@ export interface TokenExchangeDeps {
|
|
|
32
32
|
supportedResources?: string[];
|
|
33
33
|
/** Sink de auditoria (best-effort). Quando presente, registra `impersonation`. */
|
|
34
34
|
audit?: AuditSink;
|
|
35
|
+
/**
|
|
36
|
+
* AccountStore do host, usado para checar se o alvo da impersonação está
|
|
37
|
+
* desabilitado/expirado (mesmo gate de `attemptPasswordLogin`, via
|
|
38
|
+
* `assertAccountEnabled`). Impersonar uma conta desabilitada mintaria tokens
|
|
39
|
+
* funcionais para uma identidade que deveria estar sem acesso algum.
|
|
40
|
+
*
|
|
41
|
+
* OPCIONAL e capability-probed (via `supportsAccountStatus`, dentro do
|
|
42
|
+
* helper): ausente ou sem a capacidade → degrada para "permitido" (mesma
|
|
43
|
+
* regra de todo o resto da lib — nunca quebra hosts com um store mínimo).
|
|
44
|
+
*/
|
|
45
|
+
accountStore?: AccountStore;
|
|
35
46
|
}
|
|
36
47
|
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
|
/**
|
|
@@ -46,6 +47,17 @@ export function registerTokenExchange(provider, deps) {
|
|
|
46
47
|
if (!target) {
|
|
47
48
|
throw new errors.InvalidGrant('requested_subject not found');
|
|
48
49
|
}
|
|
50
|
+
// Status do alvo (disabled/expirado): mesmo gate de `attemptPasswordLogin`.
|
|
51
|
+
// Sem isso, impersonar um alvo desabilitado mintava tokens funcionais para
|
|
52
|
+
// uma identidade que o admin acreditava ter revogado. `accountStore` é
|
|
53
|
+
// opcional/capability-probed — sem ele (ou sem a capacidade), permanece
|
|
54
|
+
// "permitido" como antes.
|
|
55
|
+
if (deps.accountStore) {
|
|
56
|
+
const statusGate = await assertAccountEnabled({ accountStore: deps.accountStore, audit: deps.audit }, target.id, { email: target.email ?? '', ip: ctx.req?.socket?.remoteAddress ?? null });
|
|
57
|
+
if (!statusGate.allowed) {
|
|
58
|
+
throw new errors.InvalidGrant('requested_subject is disabled');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
49
61
|
// audience/resource: se o pedido vier com um alvo, ele PRECISA estar entre os
|
|
50
62
|
// resource indicators suportados. Caso contrário rejeitamos (conservador) —
|
|
51
63
|
// 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.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",
|
|
@@ -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
|