@adonis-agora/authkit-server 0.45.0 → 0.46.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/commands/ui_preset.js +15 -1
- package/build/host/views/account/confirm.edge +118 -52
- package/build/host/views/account/mfa.edge +1 -1
- package/build/host/views/login.edge +2 -4
- package/build/host/views/mfa-challenge.edge +1 -1
- package/build/host/views/partials/styles.edge +1 -1
- package/build/index.d.ts +5 -1
- package/build/index.js +15 -1
- package/build/src/define_config.d.ts +51 -0
- package/build/src/define_config.js +8 -0
- package/build/src/host/assets/webauthn.js +2 -0
- package/build/src/host/controllers/account_confirm_controller.d.ts +11 -14
- package/build/src/host/controllers/account_confirm_controller.js +42 -130
- package/build/src/host/controllers/account_session_controller.js +20 -4
- package/build/src/host/controllers/webauthn_asset_controller.d.ts +22 -0
- package/build/src/host/controllers/webauthn_asset_controller.js +66 -0
- package/build/src/host/i18n.d.ts +14 -0
- package/build/src/host/i18n.js +16 -0
- package/build/src/host/impersonation_session.js +15 -0
- package/build/src/host/rate_limit.d.ts +10 -0
- package/build/src/host/rate_limit.js +6 -0
- package/build/src/host/register_auth_host.d.ts +19 -0
- package/build/src/host/register_auth_host.js +72 -4
- package/build/src/host/sudo/index.d.ts +47 -0
- package/build/src/host/sudo/index.js +41 -0
- package/build/src/host/sudo/methods/magic_link.d.ts +43 -0
- package/build/src/host/sudo/methods/magic_link.js +174 -0
- package/build/src/host/sudo/methods/oidc_step_up.d.ts +68 -0
- package/build/src/host/sudo/methods/oidc_step_up.js +78 -0
- package/build/src/host/sudo/methods/passkey.d.ts +28 -0
- package/build/src/host/sudo/methods/passkey.js +139 -0
- package/build/src/host/sudo/methods/password.d.ts +19 -0
- package/build/src/host/sudo/methods/password.js +93 -0
- package/build/src/host/sudo/runtime.d.ts +141 -0
- package/build/src/host/sudo/runtime.js +327 -0
- package/build/src/host/sudo/types.d.ts +93 -0
- package/build/src/host/sudo/types.js +1 -0
- package/build/src/host/sudo_mode.d.ts +116 -6
- package/build/src/host/sudo_mode.js +133 -7
- package/package.json +6 -2
- package/build/stubs/ui/edge/views/consent.edge +0 -13
- package/build/stubs/ui/edge/views/login.edge +0 -19
- package/stubs/ui/edge/views/consent.edge +0 -13
- package/stubs/ui/edge/views/login.edge +0 -19
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
* - `enabled`: habilita/desabilita o sudo mode. Default: true.
|
|
10
10
|
* - `graceMinutes`: janela de graça em minutos. Default: 15.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
12
|
+
* Chaves de sessão: `authkit_sudo_at` (timestamp ms) + `authkit_sudo_account`
|
|
13
|
+
* (a conta que confirmou). As duas juntas são a marca — ver
|
|
14
|
+
* `SUDO_ACCOUNT_SESSION_KEY`.
|
|
13
15
|
*/
|
|
14
16
|
import type { HttpContext } from '@adonisjs/core/http';
|
|
15
17
|
import type { SettingsCapability } from './runtime_settings.js';
|
|
@@ -24,17 +26,84 @@ export interface ResolvedSudoModeSetting {
|
|
|
24
26
|
export declare const SUDO_MODE_DEFAULTS: ResolvedSudoModeSetting;
|
|
25
27
|
/**
|
|
26
28
|
* Resolve a setting `sudo_mode` em runtime (fail-safe).
|
|
29
|
+
*
|
|
30
|
+
* FAIL-SAFE, e de propósito: setting ausente, malformada ou store indisponível
|
|
31
|
+
* caem em `SUDO_MODE_DEFAULTS` em vez de lançar. A pergunta que esta função
|
|
32
|
+
* responde é de DISPONIBILIDADE — "o toggle de sudo mode está ligado, e com que
|
|
33
|
+
* janela de graça?" —, não de identidade. Um erro aqui não é sinal de credencial
|
|
34
|
+
* suspeita; é o store de settings fora do ar.
|
|
35
|
+
*
|
|
36
|
+
* Note que o default é `enabled: true`: o fail-safe não desliga o sudo mode, ele
|
|
37
|
+
* mantém a política default LIGADA. Quem decide o que fazer com uma sessão sem
|
|
38
|
+
* sudo continua sendo `isSudoActive` (fail-CLOSED). Ver o docblock de
|
|
39
|
+
* `requireSudo` para a assimetria completa entre as duas posturas — ela é
|
|
40
|
+
* escolha, não descuido.
|
|
27
41
|
*/
|
|
28
42
|
export declare function resolveEffectiveSudoMode(settings: SettingsCapability): Promise<ResolvedSudoModeSetting>;
|
|
29
43
|
/** Chave da sessão Adonis que registra quando o sudo foi confirmado. */
|
|
30
44
|
export declare const SUDO_SESSION_KEY = "authkit_sudo_at";
|
|
31
45
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
46
|
+
* Conta que CONFIRMOU o sudo registrado em `SUDO_SESSION_KEY`.
|
|
47
|
+
*
|
|
48
|
+
* Por que existe: o timestamp sozinho é uma marca de "alguém confirmou a
|
|
49
|
+
* identidade nesta sessão há pouco" — sem dizer QUEM. E a sessão sobrevive à
|
|
50
|
+
* troca de conta: o `regenerate()` só troca o id do cookie e MIGRA os dados
|
|
51
|
+
* (é o invariante descrito no M6 de `account_session_controller.ts`). Sem esta
|
|
52
|
+
* vinculação, todo caminho que troca a conta da sessão TRANSFERE o sudo junto:
|
|
53
|
+
*
|
|
54
|
+
* - impersonation: um admin que confirmou sudo sobre a PRÓPRIA conta passava
|
|
55
|
+
* a ter sudo sobre a conta personificada (`startImpersonation`); e o sudo
|
|
56
|
+
* obtido ENQUANTO personificava voltava a valer sobre a conta do admin
|
|
57
|
+
* (`stopImpersonation`);
|
|
58
|
+
* - navegador compartilhado: A confirma sudo, faz logout, B loga, e B herda
|
|
59
|
+
* a graça de A (hoje mascarado porque `login()` chama `markSudo` logo em
|
|
60
|
+
* seguida — acidente, não desenho).
|
|
61
|
+
*
|
|
62
|
+
* A garantia vira ESTRUTURAL em vez de depender de alguém lembrar de limpar a
|
|
63
|
+
* marca em toda transição futura: qualquer troca de conta, inclusive as que
|
|
64
|
+
* ainda não existem, invalida o sudo sem tocar em código novo.
|
|
65
|
+
*
|
|
66
|
+
* Chave SEPARADA de propósito (mesma decisão de
|
|
67
|
+
* `CONFIRM_PASSKEY_CHALLENGE_ACCOUNT_KEY`): o valor de `SUDO_SESSION_KEY` é um
|
|
68
|
+
* número e isso é contratual — está pinado em
|
|
69
|
+
* `tests/host/account_confirm_controller.spec.ts` com `assert.isNumber`. Então
|
|
70
|
+
* a vinculação é ADITIVA, em vez de mudar a forma do valor pinado para
|
|
71
|
+
* `{ at, accountId }`. Bônus: as assinaturas públicas de `markSudo` /
|
|
72
|
+
* `isSudoActive` (exportadas em `index.ts`) ficam intactas.
|
|
73
|
+
*/
|
|
74
|
+
export declare const SUDO_ACCOUNT_SESSION_KEY = "authkit_sudo_account";
|
|
75
|
+
/**
|
|
76
|
+
* Registra o timestamp de confirmação de sudo na sessão (NOW), VINCULADO à
|
|
77
|
+
* conta atual da sessão. Chamar após o usuário confirmar sua identidade.
|
|
78
|
+
*
|
|
79
|
+
* Sem conta na sessão não há a quem vincular: grava o timestamp (contrato
|
|
80
|
+
* pinado) e APAGA qualquer vinculação anterior, para que a marca não fique
|
|
81
|
+
* herdando o dono antigo. O resultado é uma marca órfã, que `isSudoActive`
|
|
82
|
+
* recusa (fail-closed).
|
|
83
|
+
*
|
|
84
|
+
* @deprecated Para conceder sudo, use `completeSudo(sudoContextFrom(ctx), id)`.
|
|
85
|
+
* `markSudo` grava a marca e nada mais: não registra o audit `sudo.confirmed`,
|
|
86
|
+
* não lembra o método usado e não redireciona para o `return_to`. Um host que a
|
|
87
|
+
* chame direto — o caminho natural no callback de `oidcStepUp`, antes de
|
|
88
|
+
* `completeSudo` ser público — concede privilégio sem deixar rastro nenhum.
|
|
89
|
+
* Continua exportada porque o login primário do próprio pacote a usa (ver o
|
|
90
|
+
* CHANGELOG desta versão) e porque removê-la seria breaking; o uso legítimo
|
|
91
|
+
* restante é ler/limpar a marca em fluxos que não são confirmação de
|
|
92
|
+
* identidade.
|
|
34
93
|
*/
|
|
35
94
|
export declare function markSudo(ctx: HttpContext): void;
|
|
36
95
|
/**
|
|
37
|
-
* Verifica se o sudo está ativo
|
|
96
|
+
* Verifica se o sudo está ativo: dentro da janela de graça E confirmado PELA
|
|
97
|
+
* CONTA que está logada agora.
|
|
98
|
+
*
|
|
99
|
+
* FAIL-CLOSED na vinculação (mesma postura do challenge de passkey): marca sem
|
|
100
|
+
* `accountId`, ou com um `accountId` que não bate com a conta atual da sessão,
|
|
101
|
+
* é recusada. Isso inclui sessões que já estavam vivas antes deste deploy — elas
|
|
102
|
+
* perdem o sudo e reconfirmam, que é o lado seguro do trade-off.
|
|
103
|
+
*
|
|
104
|
+
* Esta postura é o OPOSTO do fail-safe de `requireSudo`, e as duas estão certas:
|
|
105
|
+
* aqui a pergunta é "esta marca é minha?" (identidade), lá é "o toggle está
|
|
106
|
+
* ligado?" (disponibilidade). O docblock de `requireSudo` desenvolve a distinção.
|
|
38
107
|
*
|
|
39
108
|
* @returns `true` se o sudo está ativo (dentro da graça); `false` caso contrário.
|
|
40
109
|
*/
|
|
@@ -50,7 +119,48 @@ export declare function isSudoActive(ctx: HttpContext, graceMinutes: number): bo
|
|
|
50
119
|
* if (result !== true) return result
|
|
51
120
|
* ```
|
|
52
121
|
*
|
|
53
|
-
* FAIL-SAFE: qualquer erro
|
|
54
|
-
* Quando `sudo_mode.enabled = false`, sempre retorna `true`.
|
|
122
|
+
* FAIL-SAFE: qualquer erro ao resolver a setting → retorna `true` (deixa
|
|
123
|
+
* passar). Quando `sudo_mode.enabled = false`, sempre retorna `true`.
|
|
124
|
+
*
|
|
125
|
+
* ---
|
|
126
|
+
*
|
|
127
|
+
* POR QUE ESTE FAIL-SAFE CONVIVE COM O FAIL-CLOSED DAS VINCULAÇÕES.
|
|
128
|
+
*
|
|
129
|
+
* Lendo este arquivo (e os vizinhos) aparecem duas posturas OPOSTAS diante de
|
|
130
|
+
* uma situação anômala, e isso é deliberado — não é um dos dois lados por
|
|
131
|
+
* consertar. Quem "harmonizar" as duas quebra alguma coisa:
|
|
132
|
+
*
|
|
133
|
+
* - FAIL-CLOSED nas vinculações à conta: a marca de sudo
|
|
134
|
+
* (`SUDO_ACCOUNT_SESSION_KEY` em `isSudoActive`), o challenge de passkey do
|
|
135
|
+
* confirm e o token pendente do magic link de sudo. Todas recusam quando a
|
|
136
|
+
* vinculação falta ou não bate.
|
|
137
|
+
* - FAIL-SAFE aqui, ao resolver a setting `sudo_mode`.
|
|
138
|
+
*
|
|
139
|
+
* A diferença não é de rigor, é de PERGUNTA:
|
|
140
|
+
*
|
|
141
|
+
* - As vinculações perguntam **"esta credencial é minha?"**. É IDENTIDADE. Uma
|
|
142
|
+
* resposta duvidosa é indistinguível de uma resposta negativa — uma marca
|
|
143
|
+
* sem dono pode ser a marca de outra conta que sobreviveu à troca de sessão.
|
|
144
|
+
* Deixar passar concede privilégio a quem talvez não o tenha, e o custo do
|
|
145
|
+
* erro é escalação. Identidade duvidosa é recusada; o usuário reconfirma,
|
|
146
|
+
* que é barato.
|
|
147
|
+
* - Este fail-safe pergunta **"o toggle de sudo mode está ligado?"**. É
|
|
148
|
+
* DISPONIBILIDADE, uma questão de configuração, e a resposta não diz nada
|
|
149
|
+
* sobre quem é o usuário. Quem chega aqui já passou pelo `accountGuard`:
|
|
150
|
+
* tem sessão de conta viva e autenticada.
|
|
151
|
+
*
|
|
152
|
+
* Inverter ESTE lado para fail-closed transformaria uma indisponibilidade do
|
|
153
|
+
* store de settings (BD fora do ar, timeout, migração em curso) num lockout
|
|
154
|
+
* TOTAL: todo mundo, simultaneamente, trancado fora da própria área de conta —
|
|
155
|
+
* inclusive fora dos caminhos de recuperação. E o ataque que isso evitaria
|
|
156
|
+
* exige que o atacante já tenha uma sessão autenticada da vítima E consiga
|
|
157
|
+
* derrubar o store de settings no mesmo instante. Trocar uma falha rara e
|
|
158
|
+
* condicionada por uma indisponibilidade certa e generalizada é o pior dos dois
|
|
159
|
+
* negócios.
|
|
160
|
+
*
|
|
161
|
+
* O fail-safe também é ESTREITO: ele só decide se a BARREIRA roda. Ele não
|
|
162
|
+
* concede sudo, não fabrica marca nenhuma e não mexe em vinculação. Se a setting
|
|
163
|
+
* resolve normalmente, a decisão volta inteira para `isSudoActive` — e lá a
|
|
164
|
+
* postura é fail-closed de novo.
|
|
55
165
|
*/
|
|
56
166
|
export declare function requireSudo(ctx: HttpContext, settings: SettingsCapability | null): Promise<true | unknown>;
|
|
@@ -9,8 +9,11 @@
|
|
|
9
9
|
* - `enabled`: habilita/desabilita o sudo mode. Default: true.
|
|
10
10
|
* - `graceMinutes`: janela de graça em minutos. Default: 15.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
12
|
+
* Chaves de sessão: `authkit_sudo_at` (timestamp ms) + `authkit_sudo_account`
|
|
13
|
+
* (a conta que confirmou). As duas juntas são a marca — ver
|
|
14
|
+
* `SUDO_ACCOUNT_SESSION_KEY`.
|
|
13
15
|
*/
|
|
16
|
+
import { ACCOUNT_SESSION_KEY } from './middleware/account_auth.js';
|
|
14
17
|
import { SETTING_KEYS } from './runtime_toggles.js';
|
|
15
18
|
export const SUDO_MODE_DEFAULTS = {
|
|
16
19
|
enabled: true,
|
|
@@ -18,6 +21,18 @@ export const SUDO_MODE_DEFAULTS = {
|
|
|
18
21
|
};
|
|
19
22
|
/**
|
|
20
23
|
* Resolve a setting `sudo_mode` em runtime (fail-safe).
|
|
24
|
+
*
|
|
25
|
+
* FAIL-SAFE, e de propósito: setting ausente, malformada ou store indisponível
|
|
26
|
+
* caem em `SUDO_MODE_DEFAULTS` em vez de lançar. A pergunta que esta função
|
|
27
|
+
* responde é de DISPONIBILIDADE — "o toggle de sudo mode está ligado, e com que
|
|
28
|
+
* janela de graça?" —, não de identidade. Um erro aqui não é sinal de credencial
|
|
29
|
+
* suspeita; é o store de settings fora do ar.
|
|
30
|
+
*
|
|
31
|
+
* Note que o default é `enabled: true`: o fail-safe não desliga o sudo mode, ele
|
|
32
|
+
* mantém a política default LIGADA. Quem decide o que fazer com uma sessão sem
|
|
33
|
+
* sudo continua sendo `isSudoActive` (fail-CLOSED). Ver o docblock de
|
|
34
|
+
* `requireSudo` para a assimetria completa entre as duas posturas — ela é
|
|
35
|
+
* escolha, não descuido.
|
|
21
36
|
*/
|
|
22
37
|
export async function resolveEffectiveSudoMode(settings) {
|
|
23
38
|
try {
|
|
@@ -44,14 +59,74 @@ export async function resolveEffectiveSudoMode(settings) {
|
|
|
44
59
|
/** Chave da sessão Adonis que registra quando o sudo foi confirmado. */
|
|
45
60
|
export const SUDO_SESSION_KEY = 'authkit_sudo_at';
|
|
46
61
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
62
|
+
* Conta que CONFIRMOU o sudo registrado em `SUDO_SESSION_KEY`.
|
|
63
|
+
*
|
|
64
|
+
* Por que existe: o timestamp sozinho é uma marca de "alguém confirmou a
|
|
65
|
+
* identidade nesta sessão há pouco" — sem dizer QUEM. E a sessão sobrevive à
|
|
66
|
+
* troca de conta: o `regenerate()` só troca o id do cookie e MIGRA os dados
|
|
67
|
+
* (é o invariante descrito no M6 de `account_session_controller.ts`). Sem esta
|
|
68
|
+
* vinculação, todo caminho que troca a conta da sessão TRANSFERE o sudo junto:
|
|
69
|
+
*
|
|
70
|
+
* - impersonation: um admin que confirmou sudo sobre a PRÓPRIA conta passava
|
|
71
|
+
* a ter sudo sobre a conta personificada (`startImpersonation`); e o sudo
|
|
72
|
+
* obtido ENQUANTO personificava voltava a valer sobre a conta do admin
|
|
73
|
+
* (`stopImpersonation`);
|
|
74
|
+
* - navegador compartilhado: A confirma sudo, faz logout, B loga, e B herda
|
|
75
|
+
* a graça de A (hoje mascarado porque `login()` chama `markSudo` logo em
|
|
76
|
+
* seguida — acidente, não desenho).
|
|
77
|
+
*
|
|
78
|
+
* A garantia vira ESTRUTURAL em vez de depender de alguém lembrar de limpar a
|
|
79
|
+
* marca em toda transição futura: qualquer troca de conta, inclusive as que
|
|
80
|
+
* ainda não existem, invalida o sudo sem tocar em código novo.
|
|
81
|
+
*
|
|
82
|
+
* Chave SEPARADA de propósito (mesma decisão de
|
|
83
|
+
* `CONFIRM_PASSKEY_CHALLENGE_ACCOUNT_KEY`): o valor de `SUDO_SESSION_KEY` é um
|
|
84
|
+
* número e isso é contratual — está pinado em
|
|
85
|
+
* `tests/host/account_confirm_controller.spec.ts` com `assert.isNumber`. Então
|
|
86
|
+
* a vinculação é ADITIVA, em vez de mudar a forma do valor pinado para
|
|
87
|
+
* `{ at, accountId }`. Bônus: as assinaturas públicas de `markSudo` /
|
|
88
|
+
* `isSudoActive` (exportadas em `index.ts`) ficam intactas.
|
|
89
|
+
*/
|
|
90
|
+
export const SUDO_ACCOUNT_SESSION_KEY = 'authkit_sudo_account';
|
|
91
|
+
/**
|
|
92
|
+
* Registra o timestamp de confirmação de sudo na sessão (NOW), VINCULADO à
|
|
93
|
+
* conta atual da sessão. Chamar após o usuário confirmar sua identidade.
|
|
94
|
+
*
|
|
95
|
+
* Sem conta na sessão não há a quem vincular: grava o timestamp (contrato
|
|
96
|
+
* pinado) e APAGA qualquer vinculação anterior, para que a marca não fique
|
|
97
|
+
* herdando o dono antigo. O resultado é uma marca órfã, que `isSudoActive`
|
|
98
|
+
* recusa (fail-closed).
|
|
99
|
+
*
|
|
100
|
+
* @deprecated Para conceder sudo, use `completeSudo(sudoContextFrom(ctx), id)`.
|
|
101
|
+
* `markSudo` grava a marca e nada mais: não registra o audit `sudo.confirmed`,
|
|
102
|
+
* não lembra o método usado e não redireciona para o `return_to`. Um host que a
|
|
103
|
+
* chame direto — o caminho natural no callback de `oidcStepUp`, antes de
|
|
104
|
+
* `completeSudo` ser público — concede privilégio sem deixar rastro nenhum.
|
|
105
|
+
* Continua exportada porque o login primário do próprio pacote a usa (ver o
|
|
106
|
+
* CHANGELOG desta versão) e porque removê-la seria breaking; o uso legítimo
|
|
107
|
+
* restante é ler/limpar a marca em fluxos que não são confirmação de
|
|
108
|
+
* identidade.
|
|
49
109
|
*/
|
|
50
110
|
export function markSudo(ctx) {
|
|
111
|
+
const accountId = ctx.session.get(ACCOUNT_SESSION_KEY);
|
|
51
112
|
ctx.session.put(SUDO_SESSION_KEY, Date.now());
|
|
113
|
+
if (accountId)
|
|
114
|
+
ctx.session.put(SUDO_ACCOUNT_SESSION_KEY, accountId);
|
|
115
|
+
else
|
|
116
|
+
ctx.session.forget(SUDO_ACCOUNT_SESSION_KEY);
|
|
52
117
|
}
|
|
53
118
|
/**
|
|
54
|
-
* Verifica se o sudo está ativo
|
|
119
|
+
* Verifica se o sudo está ativo: dentro da janela de graça E confirmado PELA
|
|
120
|
+
* CONTA que está logada agora.
|
|
121
|
+
*
|
|
122
|
+
* FAIL-CLOSED na vinculação (mesma postura do challenge de passkey): marca sem
|
|
123
|
+
* `accountId`, ou com um `accountId` que não bate com a conta atual da sessão,
|
|
124
|
+
* é recusada. Isso inclui sessões que já estavam vivas antes deste deploy — elas
|
|
125
|
+
* perdem o sudo e reconfirmam, que é o lado seguro do trade-off.
|
|
126
|
+
*
|
|
127
|
+
* Esta postura é o OPOSTO do fail-safe de `requireSudo`, e as duas estão certas:
|
|
128
|
+
* aqui a pergunta é "esta marca é minha?" (identidade), lá é "o toggle está
|
|
129
|
+
* ligado?" (disponibilidade). O docblock de `requireSudo` desenvolve a distinção.
|
|
55
130
|
*
|
|
56
131
|
* @returns `true` se o sudo está ativo (dentro da graça); `false` caso contrário.
|
|
57
132
|
*/
|
|
@@ -59,6 +134,14 @@ export function isSudoActive(ctx, graceMinutes) {
|
|
|
59
134
|
const sudoAt = ctx.session.get(SUDO_SESSION_KEY);
|
|
60
135
|
if (!sudoAt)
|
|
61
136
|
return false;
|
|
137
|
+
// A vinculação é checada ANTES da graça: uma marca de outra conta não é
|
|
138
|
+
// "sudo expirado", é sudo que nunca valeu aqui.
|
|
139
|
+
const sudoAccountId = ctx.session.get(SUDO_ACCOUNT_SESSION_KEY);
|
|
140
|
+
if (!sudoAccountId)
|
|
141
|
+
return false;
|
|
142
|
+
const currentAccountId = ctx.session.get(ACCOUNT_SESSION_KEY);
|
|
143
|
+
if (!currentAccountId || sudoAccountId !== currentAccountId)
|
|
144
|
+
return false;
|
|
62
145
|
const graceMs = graceMinutes * 60 * 1000;
|
|
63
146
|
return Date.now() - sudoAt <= graceMs;
|
|
64
147
|
}
|
|
@@ -73,8 +156,49 @@ export function isSudoActive(ctx, graceMinutes) {
|
|
|
73
156
|
* if (result !== true) return result
|
|
74
157
|
* ```
|
|
75
158
|
*
|
|
76
|
-
* FAIL-SAFE: qualquer erro
|
|
77
|
-
* Quando `sudo_mode.enabled = false`, sempre retorna `true`.
|
|
159
|
+
* FAIL-SAFE: qualquer erro ao resolver a setting → retorna `true` (deixa
|
|
160
|
+
* passar). Quando `sudo_mode.enabled = false`, sempre retorna `true`.
|
|
161
|
+
*
|
|
162
|
+
* ---
|
|
163
|
+
*
|
|
164
|
+
* POR QUE ESTE FAIL-SAFE CONVIVE COM O FAIL-CLOSED DAS VINCULAÇÕES.
|
|
165
|
+
*
|
|
166
|
+
* Lendo este arquivo (e os vizinhos) aparecem duas posturas OPOSTAS diante de
|
|
167
|
+
* uma situação anômala, e isso é deliberado — não é um dos dois lados por
|
|
168
|
+
* consertar. Quem "harmonizar" as duas quebra alguma coisa:
|
|
169
|
+
*
|
|
170
|
+
* - FAIL-CLOSED nas vinculações à conta: a marca de sudo
|
|
171
|
+
* (`SUDO_ACCOUNT_SESSION_KEY` em `isSudoActive`), o challenge de passkey do
|
|
172
|
+
* confirm e o token pendente do magic link de sudo. Todas recusam quando a
|
|
173
|
+
* vinculação falta ou não bate.
|
|
174
|
+
* - FAIL-SAFE aqui, ao resolver a setting `sudo_mode`.
|
|
175
|
+
*
|
|
176
|
+
* A diferença não é de rigor, é de PERGUNTA:
|
|
177
|
+
*
|
|
178
|
+
* - As vinculações perguntam **"esta credencial é minha?"**. É IDENTIDADE. Uma
|
|
179
|
+
* resposta duvidosa é indistinguível de uma resposta negativa — uma marca
|
|
180
|
+
* sem dono pode ser a marca de outra conta que sobreviveu à troca de sessão.
|
|
181
|
+
* Deixar passar concede privilégio a quem talvez não o tenha, e o custo do
|
|
182
|
+
* erro é escalação. Identidade duvidosa é recusada; o usuário reconfirma,
|
|
183
|
+
* que é barato.
|
|
184
|
+
* - Este fail-safe pergunta **"o toggle de sudo mode está ligado?"**. É
|
|
185
|
+
* DISPONIBILIDADE, uma questão de configuração, e a resposta não diz nada
|
|
186
|
+
* sobre quem é o usuário. Quem chega aqui já passou pelo `accountGuard`:
|
|
187
|
+
* tem sessão de conta viva e autenticada.
|
|
188
|
+
*
|
|
189
|
+
* Inverter ESTE lado para fail-closed transformaria uma indisponibilidade do
|
|
190
|
+
* store de settings (BD fora do ar, timeout, migração em curso) num lockout
|
|
191
|
+
* TOTAL: todo mundo, simultaneamente, trancado fora da própria área de conta —
|
|
192
|
+
* inclusive fora dos caminhos de recuperação. E o ataque que isso evitaria
|
|
193
|
+
* exige que o atacante já tenha uma sessão autenticada da vítima E consiga
|
|
194
|
+
* derrubar o store de settings no mesmo instante. Trocar uma falha rara e
|
|
195
|
+
* condicionada por uma indisponibilidade certa e generalizada é o pior dos dois
|
|
196
|
+
* negócios.
|
|
197
|
+
*
|
|
198
|
+
* O fail-safe também é ESTREITO: ele só decide se a BARREIRA roda. Ele não
|
|
199
|
+
* concede sudo, não fabrica marca nenhuma e não mexe em vinculação. Se a setting
|
|
200
|
+
* resolve normalmente, a decisão volta inteira para `isSudoActive` — e lá a
|
|
201
|
+
* postura é fail-closed de novo.
|
|
78
202
|
*/
|
|
79
203
|
export async function requireSudo(ctx, settings) {
|
|
80
204
|
try {
|
|
@@ -85,7 +209,9 @@ export async function requireSudo(ctx, settings) {
|
|
|
85
209
|
return true;
|
|
86
210
|
}
|
|
87
211
|
catch {
|
|
88
|
-
// FAIL-SAFE: erro ao resolver a setting → deixa passar.
|
|
212
|
+
// FAIL-SAFE: erro ao resolver a setting → deixa passar. Disponibilidade, não
|
|
213
|
+
// identidade — ver o docblock acima para o porquê de isto NÃO contradizer o
|
|
214
|
+
// fail-closed de `isSudoActive`.
|
|
89
215
|
return true;
|
|
90
216
|
}
|
|
91
217
|
// Fora da graça: redireciona para confirmação.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adonis-agora/authkit-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.46.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",
|
|
@@ -100,6 +100,7 @@
|
|
|
100
100
|
}
|
|
101
101
|
},
|
|
102
102
|
"dependencies": {
|
|
103
|
+
"@simplewebauthn/browser": "^13.3.0",
|
|
103
104
|
"@simplewebauthn/server": "^13.3.1",
|
|
104
105
|
"jose": "^5.9.0",
|
|
105
106
|
"koa": "^3.2.1",
|
|
@@ -130,6 +131,7 @@
|
|
|
130
131
|
"@types/qrcode": "^1.5.5",
|
|
131
132
|
"better-sqlite3": "^11.0.0",
|
|
132
133
|
"edge.js": "6.5.1",
|
|
134
|
+
"esbuild": "0.25.12",
|
|
133
135
|
"ioredis": "5.4.2",
|
|
134
136
|
"ioredis-mock": "^8.9.0",
|
|
135
137
|
"luxon": "3.7.2",
|
|
@@ -149,8 +151,10 @@
|
|
|
149
151
|
"@adonis-agora/authkit-react": "0.16.0"
|
|
150
152
|
},
|
|
151
153
|
"scripts": {
|
|
152
|
-
"build": "node scripts/build_host_css.mjs && node scripts/build_ui.mjs && tsc && 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')\"",
|
|
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')\"",
|
|
153
155
|
"build:ui": "node scripts/build_ui.mjs",
|
|
156
|
+
"build:webauthn": "node scripts/build_webauthn.mjs",
|
|
157
|
+
"check:webauthn-bundle": "node scripts/check_webauthn_bundle.mjs",
|
|
154
158
|
"typecheck": "tsc --noEmit && node scripts/typecheck_ui.mjs",
|
|
155
159
|
"test": "node --import=@poppinss/ts-exec bin/test.ts",
|
|
156
160
|
"build:host-css": "node scripts/build_host_css.mjs"
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
{{{
|
|
2
|
-
exports({ to: app.makePath('resources/views/authkit/consent.edge') })
|
|
3
|
-
}}}
|
|
4
|
-
<!doctype html>
|
|
5
|
-
<html lang="pt-br"><head><meta charset="utf-8"><title>Autorizar</title>
|
|
6
|
-
<script src="https://cdn.tailwindcss.com"></script></head>
|
|
7
|
-
<body class="min-h-screen flex items-center justify-center bg-gray-50">
|
|
8
|
-
<form method="POST" action="/auth/interaction/{{ uid }}/consent" class="w-full max-w-sm bg-white p-6 rounded-lg shadow">
|
|
9
|
-
<h1 class="text-lg font-semibold mb-2">Autorizar acesso</h1>
|
|
10
|
-
<p class="text-sm text-gray-600 mb-4">O app <strong>{{ params.client_id }}</strong> quer acessar sua conta.</p>
|
|
11
|
-
<button class="w-full bg-black text-white rounded py-2">Autorizar</button>
|
|
12
|
-
</form>
|
|
13
|
-
</body></html>
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
{{{
|
|
2
|
-
exports({ to: app.makePath('resources/views/authkit/login.edge') })
|
|
3
|
-
}}}
|
|
4
|
-
<!doctype html>
|
|
5
|
-
<html lang="pt-br"><head><meta charset="utf-8"><title>Entrar</title>
|
|
6
|
-
<script src="https://cdn.tailwindcss.com"></script></head>
|
|
7
|
-
<body class="min-h-screen flex items-center justify-center bg-gray-50">
|
|
8
|
-
<form method="POST" action="/auth/interaction/{{ uid }}/login" class="w-full max-w-sm bg-white p-6 rounded-lg shadow">
|
|
9
|
-
<h1 class="text-lg font-semibold mb-4">Entrar</h1>
|
|
10
|
-
@if(error)
|
|
11
|
-
<p class="text-red-600 text-sm mb-3">{{ error }}</p>
|
|
12
|
-
@end
|
|
13
|
-
<label class="block text-sm mb-1">E-mail</label>
|
|
14
|
-
<input name="email" type="email" required class="w-full border rounded px-3 py-2 mb-3" />
|
|
15
|
-
<label class="block text-sm mb-1">Senha</label>
|
|
16
|
-
<input name="password" type="password" required class="w-full border rounded px-3 py-2 mb-4" />
|
|
17
|
-
<button class="w-full bg-black text-white rounded py-2">Entrar</button>
|
|
18
|
-
</form>
|
|
19
|
-
</body></html>
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
{{{
|
|
2
|
-
exports({ to: app.makePath('resources/views/authkit/consent.edge') })
|
|
3
|
-
}}}
|
|
4
|
-
<!doctype html>
|
|
5
|
-
<html lang="pt-br"><head><meta charset="utf-8"><title>Autorizar</title>
|
|
6
|
-
<script src="https://cdn.tailwindcss.com"></script></head>
|
|
7
|
-
<body class="min-h-screen flex items-center justify-center bg-gray-50">
|
|
8
|
-
<form method="POST" action="/auth/interaction/{{ uid }}/consent" class="w-full max-w-sm bg-white p-6 rounded-lg shadow">
|
|
9
|
-
<h1 class="text-lg font-semibold mb-2">Autorizar acesso</h1>
|
|
10
|
-
<p class="text-sm text-gray-600 mb-4">O app <strong>{{ params.client_id }}</strong> quer acessar sua conta.</p>
|
|
11
|
-
<button class="w-full bg-black text-white rounded py-2">Autorizar</button>
|
|
12
|
-
</form>
|
|
13
|
-
</body></html>
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
{{{
|
|
2
|
-
exports({ to: app.makePath('resources/views/authkit/login.edge') })
|
|
3
|
-
}}}
|
|
4
|
-
<!doctype html>
|
|
5
|
-
<html lang="pt-br"><head><meta charset="utf-8"><title>Entrar</title>
|
|
6
|
-
<script src="https://cdn.tailwindcss.com"></script></head>
|
|
7
|
-
<body class="min-h-screen flex items-center justify-center bg-gray-50">
|
|
8
|
-
<form method="POST" action="/auth/interaction/{{ uid }}/login" class="w-full max-w-sm bg-white p-6 rounded-lg shadow">
|
|
9
|
-
<h1 class="text-lg font-semibold mb-4">Entrar</h1>
|
|
10
|
-
@if(error)
|
|
11
|
-
<p class="text-red-600 text-sm mb-3">{{ error }}</p>
|
|
12
|
-
@end
|
|
13
|
-
<label class="block text-sm mb-1">E-mail</label>
|
|
14
|
-
<input name="email" type="email" required class="w-full border rounded px-3 py-2 mb-3" />
|
|
15
|
-
<label class="block text-sm mb-1">Senha</label>
|
|
16
|
-
<input name="password" type="password" required class="w-full border rounded px-3 py-2 mb-4" />
|
|
17
|
-
<button class="w-full bg-black text-white rounded py-2">Entrar</button>
|
|
18
|
-
</form>
|
|
19
|
-
</body></html>
|