@adonis-agora/authkit-server 0.75.0 → 0.77.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/README.md +148 -0
- package/build/host/views/account/apps.edge +30 -0
- package/build/host/views/agents/consent.edge +60 -0
- package/build/host/views/agents/done.edge +24 -0
- package/build/host/views/consent.edge +9 -1
- package/build/host/views/mfa-challenge.edge +42 -3
- package/build/host/views/partials/styles.edge +1 -1
- package/build/index.d.ts +18 -1
- package/build/index.js +9 -1
- package/build/providers/authkit_server_provider.js +75 -50
- package/build/src/accounts/account_store.d.ts +2 -0
- package/build/src/accounts/lucid_account_store.js +2 -0
- package/build/src/adapters/adapter_contract.d.ts +2 -0
- package/build/src/adapters/database_adapter.d.ts +1 -0
- package/build/src/adapters/database_adapter.js +26 -0
- package/build/src/adapters/redis_adapter.d.ts +1 -0
- package/build/src/adapters/redis_adapter.js +30 -0
- package/build/src/agents/agent_identity.d.ts +33 -0
- package/build/src/agents/agent_identity.js +156 -0
- package/build/src/agents/config.d.ts +99 -0
- package/build/src/agents/config.js +101 -0
- package/build/src/agents/delegation_service.d.ts +154 -0
- package/build/src/agents/delegation_service.js +394 -0
- package/build/src/agents/delegation_store.d.ts +93 -0
- package/build/src/agents/delegation_store.js +222 -0
- package/build/src/agents/middleware.d.ts +73 -0
- package/build/src/agents/middleware.js +113 -0
- package/build/src/agents/protocol.d.ts +62 -0
- package/build/src/agents/protocol.js +51 -0
- package/build/src/agents/runtime.d.ts +36 -0
- package/build/src/agents/runtime.js +65 -0
- package/build/src/agents/signer.d.ts +34 -0
- package/build/src/agents/signer.js +70 -0
- package/build/src/audit/audit_sink.d.ts +1 -1
- package/build/src/audit/audit_sink.js +5 -0
- package/build/src/controllers/authorization_server_metadata_controller.d.ts +10 -0
- package/build/src/controllers/authorization_server_metadata_controller.js +20 -0
- package/build/src/define_config.d.ts +50 -3
- package/build/src/define_config.js +42 -1
- package/build/src/host/account_api/account_api_controller.d.ts +12 -0
- package/build/src/host/account_api/account_api_controller.js +40 -0
- package/build/src/host/admin_api/dto.d.ts +1 -1
- package/build/src/host/admin_sessions_service.d.ts +2 -0
- package/build/src/host/admin_sessions_service.js +18 -0
- package/build/src/host/auth_host_config.d.ts +4 -0
- package/build/src/host/client_names.d.ts +11 -0
- package/build/src/host/client_names.js +12 -0
- package/build/src/host/controllers/account_apps_controller.d.ts +2 -0
- package/build/src/host/controllers/account_apps_controller.js +39 -3
- package/build/src/host/controllers/account_orgs_controller.js +2 -1
- package/build/src/host/controllers/account_security_controller.js +8 -4
- package/build/src/host/controllers/account_session_controller.js +2 -1
- package/build/src/host/controllers/agent_consent_controller.d.ts +17 -0
- package/build/src/host/controllers/agent_consent_controller.js +138 -0
- package/build/src/host/controllers/agent_oauth_controller.d.ts +23 -0
- package/build/src/host/controllers/agent_oauth_controller.js +110 -0
- package/build/src/host/controllers/interaction_controller.d.ts +13 -1
- package/build/src/host/controllers/interaction_controller.js +380 -24
- package/build/src/host/csrf.d.ts +8 -18
- package/build/src/host/csrf.js +28 -2
- package/build/src/host/custom_login.d.ts +36 -0
- package/build/src/host/custom_login.js +76 -0
- package/build/src/host/custom_login_completion.d.ts +4 -0
- package/build/src/host/custom_login_completion.js +33 -0
- package/build/src/host/custom_mfa.d.ts +77 -0
- package/build/src/host/custom_mfa.js +268 -0
- package/build/src/host/i18n.d.ts +58 -0
- package/build/src/host/i18n.js +64 -6
- package/build/src/host/idp_session_bridge.d.ts +5 -3
- package/build/src/host/idp_session_bridge.js +5 -3
- package/build/src/host/oidc_rp_guard.d.ts +5 -1
- package/build/src/host/oidc_rp_guard.js +15 -2
- package/build/src/host/persistent_rp_session.d.ts +8 -0
- package/build/src/host/persistent_rp_session.js +74 -0
- package/build/src/host/redirect_exact.d.ts +12 -0
- package/build/src/host/redirect_exact.js +17 -0
- package/build/src/host/register_auth_host.js +60 -7
- package/build/src/host/request_url.d.ts +10 -0
- package/build/src/host/request_url.js +17 -0
- package/build/src/host/sudo/methods/magic_link.js +2 -1
- package/build/src/host/sudo/runtime.js +3 -2
- package/build/src/host/sudo_mode.js +4 -4
- package/build/src/host/whatsapp_code_sender.d.ts +20 -0
- package/build/src/host/whatsapp_code_sender.js +9 -0
- package/build/src/host/whatsapp_senders/http.d.ts +5 -0
- package/build/src/host/whatsapp_senders/http.js +33 -0
- package/build/src/host/whatsapp_senders/meta_whatsapp_code_sender.d.ts +20 -0
- package/build/src/host/whatsapp_senders/meta_whatsapp_code_sender.js +51 -0
- package/build/src/host/whatsapp_senders/whatsmiau_code_sender.d.ts +14 -0
- package/build/src/host/whatsapp_senders/whatsmiau_code_sender.js +36 -0
- package/build/src/mcp/mcp_oauth.d.ts +85 -0
- package/build/src/mcp/mcp_oauth.js +154 -0
- package/build/src/provider/build_provider.js +15 -1
- package/build/src/provider/oidc_service.d.ts +8 -0
- package/build/src/provider/oidc_service.js +16 -2
- package/build/src/schema/ensure.js +92 -0
- package/build/stubs/ui/react/pages/consent.tsx +17 -1
- package/build/stubs/ui/react/pages/mfa-challenge.tsx +207 -37
- package/package.json +2 -1
- package/stubs/ui/react/pages/consent.tsx +17 -1
- package/stubs/ui/react/pages/mfa-challenge.tsx +207 -37
package/build/src/host/i18n.js
CHANGED
|
@@ -80,9 +80,11 @@ export const DEFAULT_MESSAGES = {
|
|
|
80
80
|
'verify_email.invalid_title': 'Invalid link',
|
|
81
81
|
'verify_email.invalid_body': 'The verification link is invalid or has already been used.',
|
|
82
82
|
// Desafio de MFA no fluxo de login (mfa-challenge).
|
|
83
|
-
'mfa_challenge.page_title': '
|
|
84
|
-
'mfa_challenge.title': '
|
|
85
|
-
'mfa_challenge.intro': '
|
|
83
|
+
'mfa_challenge.page_title': 'Multi-factor verification',
|
|
84
|
+
'mfa_challenge.title': 'Multi-factor verification',
|
|
85
|
+
'mfa_challenge.intro': 'Verify your identity using an available method.',
|
|
86
|
+
'mfa_challenge.start': 'Start verification',
|
|
87
|
+
'mfa_challenge.progress': '{completed} of {required} factors verified.',
|
|
86
88
|
'mfa_challenge.code_label': 'Code',
|
|
87
89
|
'mfa_challenge.submit': 'Verify',
|
|
88
90
|
'mfa_challenge.recovery_summary': 'Use a recovery code',
|
|
@@ -97,6 +99,11 @@ export const DEFAULT_MESSAGES = {
|
|
|
97
99
|
// raw na view). O nome vem do branding (config-trusted).
|
|
98
100
|
'consent.body': 'The app <strong>{app}</strong> wants to access your account.',
|
|
99
101
|
'consent.submit': 'Authorize',
|
|
102
|
+
'consent.scopes_title': 'It will be able to:',
|
|
103
|
+
'consent.scope.profile': 'See your name and photo',
|
|
104
|
+
'consent.scope.email': 'See your e-mail address',
|
|
105
|
+
'consent.scope.offline_access': 'Stay connected without asking you to sign in again',
|
|
106
|
+
'consent.scope.roles': 'See your roles and organization',
|
|
100
107
|
// Console de conta — login (account/login).
|
|
101
108
|
'account.login.page_title': 'My account',
|
|
102
109
|
'account.login.title': 'My account',
|
|
@@ -183,6 +190,28 @@ export const DEFAULT_MESSAGES = {
|
|
|
183
190
|
'account.apps.revoke': 'Revoke access',
|
|
184
191
|
'account.apps.revoke_confirm': 'Revoke this app’s access? It will need to be authorized again and its tokens will stop working.',
|
|
185
192
|
'account.apps.revoked': 'Access revoked.',
|
|
193
|
+
'account.apps.agents_title': 'Personal agents',
|
|
194
|
+
'account.apps.agents_intro': 'AI assistants you allowed to act on your account, and what each one can do.',
|
|
195
|
+
'account.apps.agents_empty': 'No personal agent can act on your account.',
|
|
196
|
+
'agents.consent.page_title': 'Authorize assistant',
|
|
197
|
+
'agents.consent.title': '{agent} wants to act on your account',
|
|
198
|
+
'agents.consent.body': 'The request came from {origin}. Choose what it can do — you can revoke access at any time.',
|
|
199
|
+
'agents.consent.signed_in_as': 'Signed in as {email}',
|
|
200
|
+
'agents.consent.scopes_label': 'Allow it to:',
|
|
201
|
+
'agents.consent.code': 'Request code: {code}. Make sure it matches the code your assistant shows.',
|
|
202
|
+
'agents.consent.allow': 'Allow',
|
|
203
|
+
'agents.consent.deny': 'Deny',
|
|
204
|
+
'agents.consent.enter_code_title': 'Connect an assistant',
|
|
205
|
+
'agents.consent.enter_code_body': 'Enter the code your assistant showed you.',
|
|
206
|
+
'agents.consent.continue': 'Continue',
|
|
207
|
+
'agents.consent.invalid_code': 'This code is invalid or expired. Ask your assistant for a new one.',
|
|
208
|
+
'agents.consent.impersonating': 'You cannot authorize an assistant while impersonating another account.',
|
|
209
|
+
'agents.done.approved_title': '{agent} can now:',
|
|
210
|
+
'agents.done.denied_title': 'Access denied',
|
|
211
|
+
'agents.done.return': 'You can close this page and return to your assistant.',
|
|
212
|
+
'agents.done.revoke_hint': 'To revoke this access later, go to the apps page of your account.',
|
|
213
|
+
'agents.done.expired_title': 'Request expired',
|
|
214
|
+
'agents.done.expired_body': 'This request is no longer valid. Ask your assistant to start again.',
|
|
186
215
|
'account.apps.not_supported': 'The configured OIDC adapter does not support enumeration — listing apps is unavailable.',
|
|
187
216
|
// Console de conta — organizations (account/orgs).
|
|
188
217
|
'account.orgs.page_title': 'My Organizations',
|
|
@@ -893,9 +922,11 @@ export const PT_BR_MESSAGES = {
|
|
|
893
922
|
'verify_email.invalid_title': 'Link inválido',
|
|
894
923
|
'verify_email.invalid_body': 'O link de verificação é inválido ou já foi utilizado.',
|
|
895
924
|
// Desafio de MFA no fluxo de login (mfa-challenge).
|
|
896
|
-
'mfa_challenge.page_title': 'Verificação
|
|
897
|
-
'mfa_challenge.title': 'Verificação
|
|
898
|
-
'mfa_challenge.intro': '
|
|
925
|
+
'mfa_challenge.page_title': 'Verificação multifator',
|
|
926
|
+
'mfa_challenge.title': 'Verificação multifator',
|
|
927
|
+
'mfa_challenge.intro': 'Verifique sua identidade usando um método disponível.',
|
|
928
|
+
'mfa_challenge.start': 'Iniciar verificação',
|
|
929
|
+
'mfa_challenge.progress': '{completed} de {required} fatores verificados.',
|
|
899
930
|
'mfa_challenge.code_label': 'Código',
|
|
900
931
|
'mfa_challenge.submit': 'Verificar',
|
|
901
932
|
'mfa_challenge.recovery_summary': 'Usar um código de recuperação',
|
|
@@ -908,6 +939,11 @@ export const PT_BR_MESSAGES = {
|
|
|
908
939
|
'consent.title': 'Autorizar acesso',
|
|
909
940
|
'consent.body': 'O app <strong>{app}</strong> quer acessar sua conta.',
|
|
910
941
|
'consent.submit': 'Autorizar',
|
|
942
|
+
'consent.scopes_title': 'Ele vai poder:',
|
|
943
|
+
'consent.scope.profile': 'Ver seu nome e foto',
|
|
944
|
+
'consent.scope.email': 'Ver seu e-mail',
|
|
945
|
+
'consent.scope.offline_access': 'Continuar conectado sem pedir que você entre de novo',
|
|
946
|
+
'consent.scope.roles': 'Ver seus papéis e sua organização',
|
|
911
947
|
// Console de conta — login (account/login).
|
|
912
948
|
'account.login.page_title': 'Minha conta',
|
|
913
949
|
'account.login.title': 'Minha conta',
|
|
@@ -994,6 +1030,28 @@ export const PT_BR_MESSAGES = {
|
|
|
994
1030
|
'account.apps.revoke': 'Revogar acesso',
|
|
995
1031
|
'account.apps.revoke_confirm': 'Revogar o acesso deste app? Ele precisará ser autorizado novamente e seus tokens deixarão de funcionar.',
|
|
996
1032
|
'account.apps.revoked': 'Acesso revogado.',
|
|
1033
|
+
'account.apps.agents_title': 'Assistentes pessoais',
|
|
1034
|
+
'account.apps.agents_intro': 'Assistentes de IA que você autorizou a agir na sua conta, e o que cada um pode fazer.',
|
|
1035
|
+
'account.apps.agents_empty': 'Nenhum assistente pessoal pode agir na sua conta.',
|
|
1036
|
+
'agents.consent.page_title': 'Autorizar assistente',
|
|
1037
|
+
'agents.consent.title': '{agent} quer agir na sua conta',
|
|
1038
|
+
'agents.consent.body': 'O pedido veio de {origin}. Escolha o que ele pode fazer — você pode revogar o acesso quando quiser.',
|
|
1039
|
+
'agents.consent.signed_in_as': 'Conectado como {email}',
|
|
1040
|
+
'agents.consent.scopes_label': 'Permitir que ele:',
|
|
1041
|
+
'agents.consent.code': 'Código do pedido: {code}. Confira se é o mesmo que o seu assistente mostra.',
|
|
1042
|
+
'agents.consent.allow': 'Permitir',
|
|
1043
|
+
'agents.consent.deny': 'Negar',
|
|
1044
|
+
'agents.consent.enter_code_title': 'Conectar um assistente',
|
|
1045
|
+
'agents.consent.enter_code_body': 'Digite o código que o seu assistente mostrou.',
|
|
1046
|
+
'agents.consent.continue': 'Continuar',
|
|
1047
|
+
'agents.consent.invalid_code': 'Este código é inválido ou expirou. Peça um novo ao seu assistente.',
|
|
1048
|
+
'agents.consent.impersonating': 'Não é possível autorizar um assistente enquanto você personifica outra conta.',
|
|
1049
|
+
'agents.done.approved_title': '{agent} agora pode:',
|
|
1050
|
+
'agents.done.denied_title': 'Acesso negado',
|
|
1051
|
+
'agents.done.return': 'Você pode fechar esta página e voltar ao seu assistente.',
|
|
1052
|
+
'agents.done.revoke_hint': 'Para revogar este acesso depois, vá à página de apps da sua conta.',
|
|
1053
|
+
'agents.done.expired_title': 'Pedido expirado',
|
|
1054
|
+
'agents.done.expired_body': 'Este pedido não vale mais. Peça ao seu assistente para começar de novo.',
|
|
997
1055
|
'account.apps.not_supported': 'O adapter OIDC configurado não suporta enumeração — a listagem de apps fica indisponível.',
|
|
998
1056
|
// Console de conta — organizations (account/orgs).
|
|
999
1057
|
'account.orgs.page_title': 'Minhas Organizações',
|
|
@@ -27,7 +27,10 @@ import type { HttpContext } from '@adonisjs/core/http';
|
|
|
27
27
|
/** Chave da sessão Adonis com o `uid` da sessão do IdP que originou o console. */
|
|
28
28
|
export declare const ACCOUNT_IDP_SESSION_KEY = "authkit_idp_session_uid";
|
|
29
29
|
/** O que interessa de uma sessão do oidc-provider (instância do model `Session`). */
|
|
30
|
-
interface IdpSession {
|
|
30
|
+
export interface IdpSession {
|
|
31
|
+
id: string;
|
|
32
|
+
transient?: boolean;
|
|
33
|
+
exp: number;
|
|
31
34
|
uid: string;
|
|
32
35
|
accountId?: string;
|
|
33
36
|
destroy(): Promise<void>;
|
|
@@ -37,7 +40,7 @@ interface IdpSession {
|
|
|
37
40
|
* provider faz: cookie `_session` assinado com as keys do provider, e o registro
|
|
38
41
|
* no adapter, que já descarta expirados). `null` sem sessão logada.
|
|
39
42
|
*/
|
|
40
|
-
export declare function readIdpSession(ctx: HttpContext, service: any): Promise<IdpSession | null>;
|
|
43
|
+
export declare function readIdpSession(ctx: HttpContext, service: any, strict?: boolean): Promise<IdpSession | null>;
|
|
41
44
|
/**
|
|
42
45
|
* Garante a sessão do console, aceitando a sessão do IdP quando configurado.
|
|
43
46
|
* Devolve `true` quando o request tem (ou passou a ter) sessão de console.
|
|
@@ -52,4 +55,3 @@ export declare function ensureConsoleSession(ctx: HttpContext): Promise<boolean>
|
|
|
52
55
|
* própria ponte e o "Sair" não teria efeito. No-op fora da ponte.
|
|
53
56
|
*/
|
|
54
57
|
export declare function endBridgedIdpSession(ctx: HttpContext): Promise<void>;
|
|
55
|
-
export {};
|
|
@@ -9,7 +9,7 @@ export const ACCOUNT_IDP_SESSION_KEY = 'authkit_idp_session_uid';
|
|
|
9
9
|
* provider faz: cookie `_session` assinado com as keys do provider, e o registro
|
|
10
10
|
* no adapter, que já descarta expirados). `null` sem sessão logada.
|
|
11
11
|
*/
|
|
12
|
-
export async function readIdpSession(ctx, service) {
|
|
12
|
+
export async function readIdpSession(ctx, service, strict = false) {
|
|
13
13
|
const provider = service?.provider;
|
|
14
14
|
if (!provider?.Session || typeof provider.createContext !== 'function')
|
|
15
15
|
return null;
|
|
@@ -21,7 +21,9 @@ export async function readIdpSession(ctx, service) {
|
|
|
21
21
|
const session = (await provider.Session.find(id));
|
|
22
22
|
return session?.accountId ? session : null;
|
|
23
23
|
}
|
|
24
|
-
catch {
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (strict)
|
|
26
|
+
throw error;
|
|
25
27
|
// Fail-safe: sem ponte (o guard segue para o login normal).
|
|
26
28
|
return null;
|
|
27
29
|
}
|
|
@@ -81,8 +83,8 @@ export async function ensureConsoleSession(ctx) {
|
|
|
81
83
|
// o login do console faz.
|
|
82
84
|
await ctx.session.regenerate();
|
|
83
85
|
ctx.session.put(ACCOUNT_SESSION_KEY, account.id);
|
|
84
|
-
ctx.session.put(ACCOUNT_IDP_SESSION_KEY, idp.uid);
|
|
85
86
|
await syncAdonisAuthLogin(ctx, cfg, account);
|
|
87
|
+
ctx.session.put(ACCOUNT_IDP_SESSION_KEY, idp.uid);
|
|
86
88
|
return true;
|
|
87
89
|
}
|
|
88
90
|
/**
|
|
@@ -4,6 +4,7 @@ import type { SessionUserProviderContract } from '@adonisjs/auth/types/session';
|
|
|
4
4
|
import type { HttpContext } from '@adonisjs/core/http';
|
|
5
5
|
import type { ConfigProvider } from '@adonisjs/core/types';
|
|
6
6
|
import type { EmitterLike } from '@adonisjs/core/types/events';
|
|
7
|
+
import { type PersistentRpOptions } from './persistent_rp_session.js';
|
|
7
8
|
type RealUser<UserProvider> = UserProvider extends SessionUserProviderContract<infer U> ? U : never;
|
|
8
9
|
/**
|
|
9
10
|
* Construtor do `E_UNAUTHORIZED_ACCESS` do `@adonisjs/auth` — a MESMA classe que
|
|
@@ -27,6 +28,8 @@ export declare function loadUnauthorizedAccess(guardFactoryName?: string): Promi
|
|
|
27
28
|
export type OidcRpGuardOptions<UserProvider extends SessionUserProviderContract<unknown>> = {
|
|
28
29
|
provider: UserProvider | ConfigProvider<UserProvider>;
|
|
29
30
|
sessionKey?: string;
|
|
31
|
+
/** Same-host IdP: restore and renew remembered logins. Default: false. */
|
|
32
|
+
acceptIdpSession?: PersistentRpOptions['acceptIdpSession'];
|
|
30
33
|
};
|
|
31
34
|
export type OidcRpGuardEvents<User> = {
|
|
32
35
|
'oidc_rp:login_succeeded': {
|
|
@@ -76,13 +79,14 @@ export type OidcRpGuardEvents<User> = {
|
|
|
76
79
|
*/
|
|
77
80
|
export declare class OidcRpGuard<UserProvider extends SessionUserProviderContract<unknown>> implements GuardContract<RealUser<UserProvider>> {
|
|
78
81
|
#private;
|
|
82
|
+
private persistent;
|
|
79
83
|
[symbols.GUARD_KNOWN_EVENTS]: OidcRpGuardEvents<RealUser<UserProvider>>;
|
|
80
84
|
driverName: 'oidc_rp';
|
|
81
85
|
authenticationAttempted: boolean;
|
|
82
86
|
isAuthenticated: boolean;
|
|
83
87
|
isLoggedOut: boolean;
|
|
84
88
|
user?: RealUser<UserProvider>;
|
|
85
|
-
constructor(name: string, ctx: HttpContext, sessionKey: string, emitter: EmitterLike<OidcRpGuardEvents<RealUser<UserProvider>>>, userProvider: UserProvider, unauthorized?: UnauthorizedAccessConstructor);
|
|
89
|
+
constructor(name: string, ctx: HttpContext, sessionKey: string, emitter: EmitterLike<OidcRpGuardEvents<RealUser<UserProvider>>>, userProvider: UserProvider, unauthorized?: UnauthorizedAccessConstructor, persistent?: PersistentRpOptions);
|
|
86
90
|
getUserOrFail(): RealUser<UserProvider>;
|
|
87
91
|
/**
|
|
88
92
|
* Grava a identidade na sessão. Chamado pelo callback OIDC após validar o
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { RuntimeException } from '@adonisjs/core/exceptions';
|
|
2
2
|
import { ACCOUNT_SESSION_KEY } from './account_session_key.js';
|
|
3
|
+
import { ACCOUNT_IDP_SESSION_KEY } from './idp_session_bridge.js';
|
|
4
|
+
import { endRpSession, ensureRpSession, } from './persistent_rp_session.js';
|
|
3
5
|
/** Memo do construtor entre instâncias — o `import()` só paga o custo uma vez. */
|
|
4
6
|
let cachedUnauthorizedAccess;
|
|
5
7
|
/** O construtor já resolvido por {@link loadUnauthorizedAccess}, se houver. */
|
|
@@ -52,6 +54,7 @@ export async function loadUnauthorizedAccess(guardFactoryName = 'oidcRpGuard') {
|
|
|
52
54
|
* tudo funciona nativamente.
|
|
53
55
|
*/
|
|
54
56
|
export class OidcRpGuard {
|
|
57
|
+
persistent;
|
|
55
58
|
#name;
|
|
56
59
|
#ctx;
|
|
57
60
|
#sessionKey;
|
|
@@ -63,7 +66,8 @@ export class OidcRpGuard {
|
|
|
63
66
|
isAuthenticated = false;
|
|
64
67
|
isLoggedOut = false;
|
|
65
68
|
user;
|
|
66
|
-
constructor(name, ctx, sessionKey, emitter, userProvider, unauthorized) {
|
|
69
|
+
constructor(name, ctx, sessionKey, emitter, userProvider, unauthorized, persistent = {}) {
|
|
70
|
+
this.persistent = persistent;
|
|
67
71
|
this.#name = name;
|
|
68
72
|
this.#ctx = ctx;
|
|
69
73
|
this.#sessionKey = sessionKey;
|
|
@@ -96,6 +100,8 @@ export class OidcRpGuard {
|
|
|
96
100
|
*/
|
|
97
101
|
async login(user) {
|
|
98
102
|
const guardUser = await this.#userProvider.createUserForGuard(user);
|
|
103
|
+
if (this.persistent.acceptIdpSession)
|
|
104
|
+
this.#ctx.session.forget(ACCOUNT_IDP_SESSION_KEY);
|
|
99
105
|
this.#ctx.session.put(this.#sessionKey, String(guardUser.getId()));
|
|
100
106
|
this.user = user;
|
|
101
107
|
this.isAuthenticated = true;
|
|
@@ -113,6 +119,8 @@ export class OidcRpGuard {
|
|
|
113
119
|
*/
|
|
114
120
|
async logout() {
|
|
115
121
|
const user = this.user ?? null;
|
|
122
|
+
if (this.persistent.acceptIdpSession)
|
|
123
|
+
await endRpSession(this.#ctx);
|
|
116
124
|
this.#ctx.session.forget(this.#sessionKey);
|
|
117
125
|
this.user = undefined;
|
|
118
126
|
this.isAuthenticated = false;
|
|
@@ -129,6 +137,11 @@ export class OidcRpGuard {
|
|
|
129
137
|
}
|
|
130
138
|
this.authenticationAttempted = true;
|
|
131
139
|
this.#unauthorized ??= await loadUnauthorizedAccess();
|
|
140
|
+
const acceptIdp = typeof this.persistent.acceptIdpSession === 'function'
|
|
141
|
+
? this.persistent.acceptIdpSession(this.#ctx)
|
|
142
|
+
: this.persistent.acceptIdpSession;
|
|
143
|
+
if (acceptIdp)
|
|
144
|
+
await ensureRpSession(this.#ctx, this.#sessionKey);
|
|
132
145
|
const userId = this.#ctx.session.get(this.#sessionKey);
|
|
133
146
|
if (!userId) {
|
|
134
147
|
this.#emitter.emit('oidc_rp:authentication_failed', {
|
|
@@ -197,7 +210,7 @@ export function oidcRpGuard(config) {
|
|
|
197
210
|
userProvider = config.provider;
|
|
198
211
|
}
|
|
199
212
|
return (ctx) => {
|
|
200
|
-
return new OidcRpGuard(name, ctx, sessionKey, emitter, userProvider, unauthorized);
|
|
213
|
+
return new OidcRpGuard(name, ctx, sessionKey, emitter, userProvider, unauthorized, { acceptIdpSession: config.acceptIdpSession });
|
|
201
214
|
};
|
|
202
215
|
},
|
|
203
216
|
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
|
+
export interface PersistentRpOptions {
|
|
3
|
+
acceptIdpSession?: boolean | ((ctx: HttpContext) => boolean);
|
|
4
|
+
}
|
|
5
|
+
/** Same-host RP restoration: the signed IdP cookie is the persistent credential. */
|
|
6
|
+
export declare function ensureRpSession(ctx: HttpContext, sessionKey: string): Promise<void>;
|
|
7
|
+
/** Logout must revoke the credential that could otherwise restore the RP. */
|
|
8
|
+
export declare function endRpSession(ctx: HttpContext): Promise<void>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { ACCOUNT_IDP_SESSION_KEY, readIdpSession } from './idp_session_bridge.js';
|
|
2
|
+
import { impersonationState } from './impersonation_session.js';
|
|
3
|
+
import { assertAccountEnabled } from './login_attempt.js';
|
|
4
|
+
/** Same-host RP restoration: the signed IdP cookie is the persistent credential. */
|
|
5
|
+
export async function ensureRpSession(ctx, sessionKey) {
|
|
6
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
7
|
+
// Provider interactions write to the raw Node response. Regenerating Adonis's
|
|
8
|
+
// session there migrates its data after headers are sent, leaving the browser
|
|
9
|
+
// with the old cookie and losing the pending RP state/PKCE verifier.
|
|
10
|
+
const path = ctx.request.url?.().split('?', 1)[0] ?? '';
|
|
11
|
+
const mount = service.mountPath || service.config.mountPath;
|
|
12
|
+
if (path.startsWith('/auth/interaction/') ||
|
|
13
|
+
(mount && mount !== '/' && (path === mount || path.startsWith(`${mount}/`))) ||
|
|
14
|
+
ctx.route?.name === 'authkit.oidc.root' ||
|
|
15
|
+
ctx.route?.name === 'authkit.oidc.wildcard')
|
|
16
|
+
return;
|
|
17
|
+
const current = ctx.session.get(sessionKey);
|
|
18
|
+
const linkedUid = ctx.session.get(ACCOUNT_IDP_SESSION_KEY);
|
|
19
|
+
const idp = await readIdpSession(ctx, service, true);
|
|
20
|
+
const realId = current ? (impersonationState(ctx).impersonatorId ?? current) : undefined;
|
|
21
|
+
if (current && linkedUid && (!idp || idp.uid !== linkedUid || idp.accountId !== realId)) {
|
|
22
|
+
ctx.session.clear();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
// An existing local login cannot silently become a different IdP account.
|
|
26
|
+
if (current && (!idp || idp.accountId !== realId || idp.transient))
|
|
27
|
+
return;
|
|
28
|
+
if (!idp?.accountId || idp.transient)
|
|
29
|
+
return;
|
|
30
|
+
const account = await service.config.accountStore.findById(idp.accountId);
|
|
31
|
+
if (!account ||
|
|
32
|
+
!(await assertAccountEnabled(service.config, account.id, {
|
|
33
|
+
email: account.email ?? '',
|
|
34
|
+
ip: ctx.request.ip(),
|
|
35
|
+
})).allowed) {
|
|
36
|
+
if (current)
|
|
37
|
+
ctx.session.clear();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const adapter = service.provider.Session.adapter;
|
|
41
|
+
const ttl = service.sessionTtlHolder.rememberSec;
|
|
42
|
+
if (adapter.renewSession) {
|
|
43
|
+
if (!(await adapter.renewSession(idp.id, ttl))) {
|
|
44
|
+
if (linkedUid)
|
|
45
|
+
ctx.session.clear();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const kctx = service.provider.createContext(ctx.request.request, ctx.response.response);
|
|
49
|
+
kctx.cookies.set(service.provider.cookieName('session'), idp.id, {
|
|
50
|
+
signed: true,
|
|
51
|
+
httpOnly: true,
|
|
52
|
+
sameSite: 'lax',
|
|
53
|
+
path: '/',
|
|
54
|
+
secure: kctx.secure,
|
|
55
|
+
maxAge: ttl * 1000,
|
|
56
|
+
overwrite: true,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
// Custom adapters without atomic renewal restore until the existing IdP expiry.
|
|
60
|
+
if (!current) {
|
|
61
|
+
await ctx.session.regenerate();
|
|
62
|
+
ctx.session.put(sessionKey, account.id);
|
|
63
|
+
}
|
|
64
|
+
ctx.session.put(ACCOUNT_IDP_SESSION_KEY, idp.uid);
|
|
65
|
+
}
|
|
66
|
+
/** Logout must revoke the credential that could otherwise restore the RP. */
|
|
67
|
+
export async function endRpSession(ctx) {
|
|
68
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
69
|
+
const idp = await readIdpSession(ctx, service, true);
|
|
70
|
+
// The browser's signed IdP credential must be revoked even after its RP expired.
|
|
71
|
+
if (idp)
|
|
72
|
+
await idp.destroy();
|
|
73
|
+
ctx.session.forget(ACCOUNT_IDP_SESSION_KEY);
|
|
74
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redireciona para `url` EXATAMENTE como a lib a montou.
|
|
3
|
+
*
|
|
4
|
+
* O starter do AdonisJS liga `redirect.forwardQueryString: true` no `config/app.ts`: todo
|
|
5
|
+
* `response.redirect(url)` passa a colar a query da request ATUAL no fim — e numa URL que já tem a
|
|
6
|
+
* dela, sai um destino quebrado (`/login?return_to=%2Fx%3Fa%3D1?a=1`). O argumento
|
|
7
|
+
* `forwardQueryString = false` do `redirect()` não desliga o que veio do config; só o
|
|
8
|
+
* `clearQs()` do builder desliga. Toda URL que a lib monta já carrega a query que precisa.
|
|
9
|
+
*
|
|
10
|
+
* Fora de uma Response do AdonisJS (dublês de teste), cai no `redirect(url)` de sempre.
|
|
11
|
+
*/
|
|
12
|
+
export declare function redirectExact(response: any, url: string): unknown;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redireciona para `url` EXATAMENTE como a lib a montou.
|
|
3
|
+
*
|
|
4
|
+
* O starter do AdonisJS liga `redirect.forwardQueryString: true` no `config/app.ts`: todo
|
|
5
|
+
* `response.redirect(url)` passa a colar a query da request ATUAL no fim — e numa URL que já tem a
|
|
6
|
+
* dela, sai um destino quebrado (`/login?return_to=%2Fx%3Fa%3D1?a=1`). O argumento
|
|
7
|
+
* `forwardQueryString = false` do `redirect()` não desliga o que veio do config; só o
|
|
8
|
+
* `clearQs()` do builder desliga. Toda URL que a lib monta já carrega a query que precisa.
|
|
9
|
+
*
|
|
10
|
+
* Fora de uma Response do AdonisJS (dublês de teste), cai no `redirect(url)` de sempre.
|
|
11
|
+
*/
|
|
12
|
+
export function redirectExact(response, url) {
|
|
13
|
+
if (typeof response?.getStatus === 'function') {
|
|
14
|
+
return response.redirect().clearQs().toPath(url);
|
|
15
|
+
}
|
|
16
|
+
return response.redirect(url);
|
|
17
|
+
}
|
|
@@ -9,6 +9,8 @@ import { normalizeAdminApiPrefix, normalizeAdminPrefix, setAdminApiPrefix, setAd
|
|
|
9
9
|
import { getAuthHostConfig, markAuthHostAutoMounted, wasAuthHostAutoMounted, } from './auth_host_config.js';
|
|
10
10
|
import { ensureConsoleSession } from './idp_session_bridge.js';
|
|
11
11
|
import { createAuthThrottles } from './rate_limit.js';
|
|
12
|
+
import { redirectExact } from './redirect_exact.js';
|
|
13
|
+
import { requestPathWithQuery } from './request_url.js';
|
|
12
14
|
import { resolveRuntimeSettings } from './runtime_settings.js';
|
|
13
15
|
import { resolveEffectiveSessionPolicy } from './runtime_toggles.js';
|
|
14
16
|
import { magicLink as sudoMagicLink } from './sudo/methods/magic_link.js';
|
|
@@ -90,9 +92,7 @@ function buildLoginRedirect(ctx, extra) {
|
|
|
90
92
|
// host que desmontou a tela de login (`account: { login: false }`) aponta para
|
|
91
93
|
// a própria rota de login dele (ex.: `/login`). Ver `account_login_url.ts`.
|
|
92
94
|
const loginUrl = getAccountLoginUrl();
|
|
93
|
-
const
|
|
94
|
-
const qs = ctx.request?.parsedUrl?.search ?? '';
|
|
95
|
-
const dest = qs ? `${url}${qs}` : url;
|
|
95
|
+
const dest = requestPathWithQuery(ctx.request);
|
|
96
96
|
// Só inclui return_to quando há um caminho real (não vazio, não é o próprio login).
|
|
97
97
|
if (dest && dest !== '/' && !dest.startsWith(loginUrl)) {
|
|
98
98
|
const encoded = encodeURIComponent(dest);
|
|
@@ -116,12 +116,12 @@ function buildLoginRedirect(ctx, extra) {
|
|
|
116
116
|
const accountGuard = async (ctx, next) => {
|
|
117
117
|
// Sessão do console — ou, com `accountSession.acceptIdpSession`, a do IdP (SSO).
|
|
118
118
|
if (!(await ensureConsoleSession(ctx))) {
|
|
119
|
-
return ctx.response
|
|
119
|
+
return redirectExact(ctx.response, buildLoginRedirect(ctx));
|
|
120
120
|
}
|
|
121
121
|
// Idle timeout: encerra e redireciona com query param de motivo.
|
|
122
122
|
const idleExpired = await checkAndRefreshIdle(ctx);
|
|
123
123
|
if (idleExpired) {
|
|
124
|
-
return ctx.response
|
|
124
|
+
return redirectExact(ctx.response, buildLoginRedirect(ctx, 'reason=idle'));
|
|
125
125
|
}
|
|
126
126
|
return next();
|
|
127
127
|
};
|
|
@@ -150,12 +150,12 @@ export const adminGuard = async (ctx, next) => {
|
|
|
150
150
|
const accountId = ctx.session?.get(ACCOUNT_SESSION_KEY);
|
|
151
151
|
if (!accountId) {
|
|
152
152
|
// `/account/login` é sempre o login da conta — NÃO muda com o prefixo admin.
|
|
153
|
-
return ctx.response
|
|
153
|
+
return redirectExact(ctx.response, buildLoginRedirect(ctx));
|
|
154
154
|
}
|
|
155
155
|
// Idle timeout: também protege o console admin.
|
|
156
156
|
const idleExpired = await checkAndRefreshIdle(ctx);
|
|
157
157
|
if (idleExpired) {
|
|
158
|
-
return ctx.response
|
|
158
|
+
return redirectExact(ctx.response, buildLoginRedirect(ctx, 'reason=idle'));
|
|
159
159
|
}
|
|
160
160
|
const allowed = cfg.admin.roles;
|
|
161
161
|
const account = await cfg.accountStore.findById(accountId);
|
|
@@ -173,6 +173,7 @@ export const adminGuard = async (ctx, next) => {
|
|
|
173
173
|
};
|
|
174
174
|
const C = {
|
|
175
175
|
oidc: () => import('../controllers/oidc_callback_controller.js'),
|
|
176
|
+
authorizationServerMetadata: () => import('../controllers/authorization_server_metadata_controller.js'),
|
|
176
177
|
interaction: () => import('./controllers/interaction_controller.js'),
|
|
177
178
|
registration: () => import('./controllers/registration_controller.js'),
|
|
178
179
|
social: () => import('./controllers/social_controller.js'),
|
|
@@ -184,6 +185,8 @@ const C = {
|
|
|
184
185
|
accountMfa: () => import('./controllers/account_mfa_controller.js'),
|
|
185
186
|
accountOrgs: () => import('./controllers/account_orgs_controller.js'),
|
|
186
187
|
accountConfirm: () => import('./controllers/account_confirm_controller.js'),
|
|
188
|
+
agentOAuth: () => import('./controllers/agent_oauth_controller.js'),
|
|
189
|
+
agentConsent: () => import('./controllers/agent_consent_controller.js'),
|
|
187
190
|
webauthnAsset: () => import('./controllers/webauthn_asset_controller.js'),
|
|
188
191
|
logoutAsset: () => import('./controllers/logout_asset_controller.js'),
|
|
189
192
|
passkeyAutofillAsset: () => import('./controllers/passkey_autofill_asset_controller.js'),
|
|
@@ -421,6 +424,15 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
421
424
|
.get('/authkit/assets/webauthn_confirm.js', [C.webauthnConfirmAsset])
|
|
422
425
|
.as('authkit.assets.webauthnConfirm');
|
|
423
426
|
router.get('/authkit/assets/submit_lock.js', [C.submitLockAsset]).as('authkit.assets.submitLock');
|
|
427
|
+
// Metadata do servidor de autorização no caminho da RFC 8414 §3.1 (issuer com path: os
|
|
428
|
+
// clientes MCP procuram `/.well-known/oauth-authorization-server/oidc` antes do OIDC Discovery).
|
|
429
|
+
// O issuer termina no mountPath, então o mount É o path do issuer.
|
|
430
|
+
const issuerPath = mount.replace(/\/+$/, '');
|
|
431
|
+
if (issuerPath !== '') {
|
|
432
|
+
router
|
|
433
|
+
.get(`/.well-known/oauth-authorization-server${issuerPath}`, [C.authorizationServerMetadata])
|
|
434
|
+
.as('authkit.oauth_authorization_server');
|
|
435
|
+
}
|
|
424
436
|
// Provider OIDC (wildcard + root) — o que registerOidcRoutes fazia.
|
|
425
437
|
router.any(`${mount}/*`, [C.oidc]).as('authkit.oidc.wildcard');
|
|
426
438
|
router.any(mount, [C.oidc]).as('authkit.oidc.root');
|
|
@@ -429,6 +441,18 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
429
441
|
router.post('/auth/interaction/:uid/identifier', [C.interaction, 'identifier']);
|
|
430
442
|
withLogin(router.post('/auth/interaction/:uid/login', [C.interaction, 'login']));
|
|
431
443
|
withLogin(router.post('/auth/interaction/:uid/mfa', [C.interaction, 'mfaVerify']));
|
|
444
|
+
const customMfaBeginRoute = router.post('/auth/interaction/:uid/mfa/custom/:method/begin', [
|
|
445
|
+
C.interaction,
|
|
446
|
+
'customMfaBegin',
|
|
447
|
+
]);
|
|
448
|
+
withLogin(customMfaBeginRoute);
|
|
449
|
+
customMfaBeginRoute.as('authkit.mfa.custom.begin');
|
|
450
|
+
const customMfaVerifyRoute = router.post('/auth/interaction/:uid/mfa/custom/:method/verify', [
|
|
451
|
+
C.interaction,
|
|
452
|
+
'customMfaVerify',
|
|
453
|
+
]);
|
|
454
|
+
withLogin(customMfaVerifyRoute);
|
|
455
|
+
customMfaVerifyRoute.as('authkit.mfa.custom.verify');
|
|
432
456
|
// Troca de senha obrigatória quando a senha expirou (password expiration gate).
|
|
433
457
|
withLogin(router.post('/auth/interaction/:uid/password-expired', [
|
|
434
458
|
C.interaction,
|
|
@@ -463,6 +487,27 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
463
487
|
}
|
|
464
488
|
// PAT introspection (server-to-server).
|
|
465
489
|
withIntrospection(router.post('/authkit/pat/introspect', [C.patIntrospection, 'handle']));
|
|
490
|
+
// Personal agents (PACT §5): authorization server de delegação. Os endpoints
|
|
491
|
+
// OAuth são server-to-server (o agente se autentica pelo JWT dele, sem
|
|
492
|
+
// sessão); a tela de consentimento exige a sessão de conta — sem ela o
|
|
493
|
+
// `accountGuard` manda para o login com `return_to`, e o usuário volta com o
|
|
494
|
+
// `user_code`. Montado só quando `personalAgents` está no config.
|
|
495
|
+
const agentsPrefix = hostCfg?.personalAgents?.prefix;
|
|
496
|
+
if (agentsPrefix) {
|
|
497
|
+
const oauth = `${agentsPrefix}/oauth`;
|
|
498
|
+
router.get(`${oauth}/.well-known/oauth-authorization-server`, [C.agentOAuth, 'metadata']);
|
|
499
|
+
router.get(`${oauth}/jwks.json`, [C.agentOAuth, 'jwks']);
|
|
500
|
+
router.post(`${oauth}/device_authorization`, [C.agentOAuth, 'deviceAuthorization']);
|
|
501
|
+
router.post(`${oauth}/token`, [C.agentOAuth, 'token']);
|
|
502
|
+
// Throttle de código (bucket do OTP, por IP): o `user_code` tem 8 letras e
|
|
503
|
+
// a RFC 8628 §5.1 conta com rate limit para não ser adivinhável.
|
|
504
|
+
router
|
|
505
|
+
.group(() => {
|
|
506
|
+
withOtpLogin(router.get(`${agentsPrefix}/consent`, [C.agentConsent, 'show']));
|
|
507
|
+
withOtpLogin(router.post(`${agentsPrefix}/consent`, [C.agentConsent, 'decide']));
|
|
508
|
+
})
|
|
509
|
+
.use([accountGuard]);
|
|
510
|
+
}
|
|
466
511
|
// Paths do console de conta (configuráveis/localizáveis via `accountRoutes`).
|
|
467
512
|
// As TELAS vêm de `accountPath(key)` (prefixo + segmento configurável); os
|
|
468
513
|
// action-subpaths concatenados (`/password`, `/enroll`, ...) são FIXOS —
|
|
@@ -531,6 +576,9 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
531
576
|
if (mountApps) {
|
|
532
577
|
router.get(appsPath, [C.accountApps, 'index']);
|
|
533
578
|
router.post(`${appsPath}/:clientId/revoke`, [C.accountApps, 'revoke']);
|
|
579
|
+
if (agentsPrefix) {
|
|
580
|
+
router.post(`${appsPath}/agents/:grantId/revoke`, [C.accountApps, 'revokeAgent']);
|
|
581
|
+
}
|
|
534
582
|
}
|
|
535
583
|
// MFA — TOTP + passkeys (tela `mfa`).
|
|
536
584
|
if (mountMfa) {
|
|
@@ -642,6 +690,11 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
642
690
|
// Apps (grants).
|
|
643
691
|
router.get(`${apiBase}/apps`, [C.accountApi, 'listApps']);
|
|
644
692
|
router.delete(`${apiBase}/apps/:clientId`, [C.accountApi, 'revokeApp']);
|
|
693
|
+
// Personal agents com delegação (PACT).
|
|
694
|
+
if (agentsPrefix) {
|
|
695
|
+
router.get(`${apiBase}/agents`, [C.accountApi, 'listAgents']);
|
|
696
|
+
router.delete(`${apiBase}/agents/:id`, [C.accountApi, 'revokeAgent']);
|
|
697
|
+
}
|
|
645
698
|
// MFA + passkeys.
|
|
646
699
|
router.get(`${apiBase}/mfa`, [C.accountApi, 'mfaStatus']);
|
|
647
700
|
// Login methods preference (self-service, por usuário).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path + query string da request atual, para montar um `return_to`.
|
|
3
|
+
*
|
|
4
|
+
* `request.url(true)` é a API do AdonisJS que inclui a query. O código antigo lia
|
|
5
|
+
* `request.parsedUrl.search`, que o AdonisJS 7 não tem mais (o `parsedUrl` virou
|
|
6
|
+
* `{ pathname, query }`) — todo redirect para o login perdia a query em silêncio,
|
|
7
|
+
* e o usuário voltava para a página sem os parâmetros (um `user_code`, uma
|
|
8
|
+
* paginação). O `search` fica como fallback para hosts/dublês que ainda o expõem.
|
|
9
|
+
*/
|
|
10
|
+
export declare function requestPathWithQuery(request: any): string;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path + query string da request atual, para montar um `return_to`.
|
|
3
|
+
*
|
|
4
|
+
* `request.url(true)` é a API do AdonisJS que inclui a query. O código antigo lia
|
|
5
|
+
* `request.parsedUrl.search`, que o AdonisJS 7 não tem mais (o `parsedUrl` virou
|
|
6
|
+
* `{ pathname, query }`) — todo redirect para o login perdia a query em silêncio,
|
|
7
|
+
* e o usuário voltava para a página sem os parâmetros (um `user_code`, uma
|
|
8
|
+
* paginação). O `search` fica como fallback para hosts/dublês que ainda o expõem.
|
|
9
|
+
*/
|
|
10
|
+
export function requestPathWithQuery(request) {
|
|
11
|
+
const withQuery = request?.url?.(true) ?? '';
|
|
12
|
+
if (withQuery.includes('?'))
|
|
13
|
+
return withQuery;
|
|
14
|
+
const parsed = request?.parsedUrl;
|
|
15
|
+
const legacy = parsed?.search ?? (parsed?.query ? `?${parsed.query}` : '');
|
|
16
|
+
return `${withQuery}${legacy}`;
|
|
17
|
+
}
|
|
@@ -2,6 +2,7 @@ import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
|
2
2
|
import { accountPath } from '../../account_paths.js';
|
|
3
3
|
import { sendSudoLinkEmail } from '../../default_mailer.js';
|
|
4
4
|
import { translate } from '../../i18n.js';
|
|
5
|
+
import { redirectExact } from '../../redirect_exact.js';
|
|
5
6
|
import { isSudoMethodEnabled } from '../runtime.js';
|
|
6
7
|
/** Token de sudo pendente, guardado na sessão que o pediu. */
|
|
7
8
|
export const SUDO_LINK_SESSION_KEY = 'authkit_sudo_link';
|
|
@@ -170,7 +171,7 @@ export function magicLink() {
|
|
|
170
171
|
// `translate(...)` em `confirmError`, e a tela leria dois formatos
|
|
171
172
|
// diferentes se este aqui mandasse a chave.
|
|
172
173
|
ctx.session.flash('confirmNotice', translate(c.cfg.messages, 'account.confirm.magic_link_sent'));
|
|
173
|
-
return ctx.response
|
|
174
|
+
return redirectExact(ctx.response, `${accountPath('confirm')}${qs}`);
|
|
174
175
|
});
|
|
175
176
|
router.get(`${accountPath('confirm')}/magic-link/:token`, async (ctx) => {
|
|
176
177
|
const c = await h.contextFrom(ctx);
|
|
@@ -3,6 +3,7 @@ import { accountPath } from '../account_paths.js';
|
|
|
3
3
|
import { ACCOUNT_SESSION_KEY } from '../account_session_key.js';
|
|
4
4
|
import { validateReturnTo } from '../controllers/account_session_controller.js';
|
|
5
5
|
import { translate } from '../i18n.js';
|
|
6
|
+
import { redirectExact } from '../redirect_exact.js';
|
|
6
7
|
import { markSudo } from '../sudo_mode.js';
|
|
7
8
|
/** Último método usado com sucesso — só ordena a tela, não restringe nada. */
|
|
8
9
|
export const LAST_METHOD_SESSION_KEY = 'authkit_sudo_last_method';
|
|
@@ -362,7 +363,7 @@ export async function completeSudo(c, methodId) {
|
|
|
362
363
|
ip: c.ctx.request.ip?.() ?? null,
|
|
363
364
|
metadata: { method: methodId },
|
|
364
365
|
});
|
|
365
|
-
return c.ctx.response
|
|
366
|
+
return redirectExact(c.ctx.response, c.returnTo ?? accountHome(c.cfg));
|
|
366
367
|
}
|
|
367
368
|
/**
|
|
368
369
|
* Falha de confirmação: flash + volta pro /account/confirm preservando o
|
|
@@ -372,7 +373,7 @@ export async function completeSudo(c, methodId) {
|
|
|
372
373
|
export async function fail(c, messageKey) {
|
|
373
374
|
c.ctx.session.flash('confirmError', translate(c.cfg.messages, messageKey));
|
|
374
375
|
const qs = c.returnTo ? `?return_to=${encodeURIComponent(c.returnTo)}` : '';
|
|
375
|
-
return c.ctx.response
|
|
376
|
+
return redirectExact(c.ctx.response, `${accountPath('confirm')}${qs}`);
|
|
376
377
|
}
|
|
377
378
|
/**
|
|
378
379
|
* Filtra os métodos disponíveis para esta conta e promove o último usado.
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { accountPath } from './account_paths.js';
|
|
17
17
|
import { ACCOUNT_SESSION_KEY } from './account_session_key.js';
|
|
18
|
+
import { redirectExact } from './redirect_exact.js';
|
|
19
|
+
import { requestPathWithQuery } from './request_url.js';
|
|
18
20
|
import { SETTING_KEYS } from './runtime_toggles.js';
|
|
19
21
|
export const SUDO_MODE_DEFAULTS = {
|
|
20
22
|
enabled: true,
|
|
@@ -235,11 +237,9 @@ export async function requireSudo(ctx, settings) {
|
|
|
235
237
|
if (await isSudoSatisfied(ctx, settings))
|
|
236
238
|
return true;
|
|
237
239
|
// Fora da graça: redireciona para confirmação.
|
|
238
|
-
const
|
|
239
|
-
const qs = ctx.request.parsedUrl?.search ?? '';
|
|
240
|
-
const dest = qs ? `${rawUrl}${qs}` : rawUrl;
|
|
240
|
+
const dest = requestPathWithQuery(ctx.request);
|
|
241
241
|
const returnTo = dest && dest !== '/' && !dest.startsWith(accountPath('confirm'))
|
|
242
242
|
? `?return_to=${encodeURIComponent(dest)}`
|
|
243
243
|
: '';
|
|
244
|
-
return ctx.response
|
|
244
|
+
return redirectExact(ctx.response, `${accountPath('confirm')}${returnTo}`);
|
|
245
245
|
}
|