@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.
Files changed (101) hide show
  1. package/README.md +148 -0
  2. package/build/host/views/account/apps.edge +30 -0
  3. package/build/host/views/agents/consent.edge +60 -0
  4. package/build/host/views/agents/done.edge +24 -0
  5. package/build/host/views/consent.edge +9 -1
  6. package/build/host/views/mfa-challenge.edge +42 -3
  7. package/build/host/views/partials/styles.edge +1 -1
  8. package/build/index.d.ts +18 -1
  9. package/build/index.js +9 -1
  10. package/build/providers/authkit_server_provider.js +75 -50
  11. package/build/src/accounts/account_store.d.ts +2 -0
  12. package/build/src/accounts/lucid_account_store.js +2 -0
  13. package/build/src/adapters/adapter_contract.d.ts +2 -0
  14. package/build/src/adapters/database_adapter.d.ts +1 -0
  15. package/build/src/adapters/database_adapter.js +26 -0
  16. package/build/src/adapters/redis_adapter.d.ts +1 -0
  17. package/build/src/adapters/redis_adapter.js +30 -0
  18. package/build/src/agents/agent_identity.d.ts +33 -0
  19. package/build/src/agents/agent_identity.js +156 -0
  20. package/build/src/agents/config.d.ts +99 -0
  21. package/build/src/agents/config.js +101 -0
  22. package/build/src/agents/delegation_service.d.ts +154 -0
  23. package/build/src/agents/delegation_service.js +394 -0
  24. package/build/src/agents/delegation_store.d.ts +93 -0
  25. package/build/src/agents/delegation_store.js +222 -0
  26. package/build/src/agents/middleware.d.ts +73 -0
  27. package/build/src/agents/middleware.js +113 -0
  28. package/build/src/agents/protocol.d.ts +62 -0
  29. package/build/src/agents/protocol.js +51 -0
  30. package/build/src/agents/runtime.d.ts +36 -0
  31. package/build/src/agents/runtime.js +65 -0
  32. package/build/src/agents/signer.d.ts +34 -0
  33. package/build/src/agents/signer.js +70 -0
  34. package/build/src/audit/audit_sink.d.ts +1 -1
  35. package/build/src/audit/audit_sink.js +5 -0
  36. package/build/src/controllers/authorization_server_metadata_controller.d.ts +10 -0
  37. package/build/src/controllers/authorization_server_metadata_controller.js +20 -0
  38. package/build/src/define_config.d.ts +50 -3
  39. package/build/src/define_config.js +42 -1
  40. package/build/src/host/account_api/account_api_controller.d.ts +12 -0
  41. package/build/src/host/account_api/account_api_controller.js +40 -0
  42. package/build/src/host/admin_api/dto.d.ts +1 -1
  43. package/build/src/host/admin_sessions_service.d.ts +2 -0
  44. package/build/src/host/admin_sessions_service.js +18 -0
  45. package/build/src/host/auth_host_config.d.ts +4 -0
  46. package/build/src/host/client_names.d.ts +11 -0
  47. package/build/src/host/client_names.js +12 -0
  48. package/build/src/host/controllers/account_apps_controller.d.ts +2 -0
  49. package/build/src/host/controllers/account_apps_controller.js +39 -3
  50. package/build/src/host/controllers/account_orgs_controller.js +2 -1
  51. package/build/src/host/controllers/account_security_controller.js +8 -4
  52. package/build/src/host/controllers/account_session_controller.js +2 -1
  53. package/build/src/host/controllers/agent_consent_controller.d.ts +17 -0
  54. package/build/src/host/controllers/agent_consent_controller.js +138 -0
  55. package/build/src/host/controllers/agent_oauth_controller.d.ts +23 -0
  56. package/build/src/host/controllers/agent_oauth_controller.js +110 -0
  57. package/build/src/host/controllers/interaction_controller.d.ts +13 -1
  58. package/build/src/host/controllers/interaction_controller.js +380 -24
  59. package/build/src/host/csrf.d.ts +8 -18
  60. package/build/src/host/csrf.js +28 -2
  61. package/build/src/host/custom_login.d.ts +36 -0
  62. package/build/src/host/custom_login.js +76 -0
  63. package/build/src/host/custom_login_completion.d.ts +4 -0
  64. package/build/src/host/custom_login_completion.js +33 -0
  65. package/build/src/host/custom_mfa.d.ts +77 -0
  66. package/build/src/host/custom_mfa.js +268 -0
  67. package/build/src/host/i18n.d.ts +58 -0
  68. package/build/src/host/i18n.js +64 -6
  69. package/build/src/host/idp_session_bridge.d.ts +5 -3
  70. package/build/src/host/idp_session_bridge.js +5 -3
  71. package/build/src/host/oidc_rp_guard.d.ts +5 -1
  72. package/build/src/host/oidc_rp_guard.js +15 -2
  73. package/build/src/host/persistent_rp_session.d.ts +8 -0
  74. package/build/src/host/persistent_rp_session.js +74 -0
  75. package/build/src/host/redirect_exact.d.ts +12 -0
  76. package/build/src/host/redirect_exact.js +17 -0
  77. package/build/src/host/register_auth_host.js +60 -7
  78. package/build/src/host/request_url.d.ts +10 -0
  79. package/build/src/host/request_url.js +17 -0
  80. package/build/src/host/sudo/methods/magic_link.js +2 -1
  81. package/build/src/host/sudo/runtime.js +3 -2
  82. package/build/src/host/sudo_mode.js +4 -4
  83. package/build/src/host/whatsapp_code_sender.d.ts +20 -0
  84. package/build/src/host/whatsapp_code_sender.js +9 -0
  85. package/build/src/host/whatsapp_senders/http.d.ts +5 -0
  86. package/build/src/host/whatsapp_senders/http.js +33 -0
  87. package/build/src/host/whatsapp_senders/meta_whatsapp_code_sender.d.ts +20 -0
  88. package/build/src/host/whatsapp_senders/meta_whatsapp_code_sender.js +51 -0
  89. package/build/src/host/whatsapp_senders/whatsmiau_code_sender.d.ts +14 -0
  90. package/build/src/host/whatsapp_senders/whatsmiau_code_sender.js +36 -0
  91. package/build/src/mcp/mcp_oauth.d.ts +85 -0
  92. package/build/src/mcp/mcp_oauth.js +154 -0
  93. package/build/src/provider/build_provider.js +15 -1
  94. package/build/src/provider/oidc_service.d.ts +8 -0
  95. package/build/src/provider/oidc_service.js +16 -2
  96. package/build/src/schema/ensure.js +92 -0
  97. package/build/stubs/ui/react/pages/consent.tsx +17 -1
  98. package/build/stubs/ui/react/pages/mfa-challenge.tsx +207 -37
  99. package/package.json +2 -1
  100. package/stubs/ui/react/pages/consent.tsx +17 -1
  101. package/stubs/ui/react/pages/mfa-challenge.tsx +207 -37
@@ -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': 'Two-factor verification',
84
- 'mfa_challenge.title': 'Two-factor verification',
85
- 'mfa_challenge.intro': 'Open your authenticator app and enter the 6-digit code.',
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 em duas etapas',
897
- 'mfa_challenge.title': 'Verificação em duas etapas',
898
- 'mfa_challenge.intro': 'Abra seu app autenticador e informe o código de 6 dígitos.',
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 url = ctx.request?.url?.() ?? '';
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.redirect(buildLoginRedirect(ctx));
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.redirect(buildLoginRedirect(ctx, 'reason=idle'));
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.redirect(buildLoginRedirect(ctx));
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.redirect(buildLoginRedirect(ctx, 'reason=idle'));
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.redirect(`${accountPath('confirm')}${qs}`);
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.redirect(c.returnTo ?? accountHome(c.cfg));
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.redirect(`${accountPath('confirm')}${qs}`);
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 rawUrl = ctx.request.url?.() ?? '';
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.redirect(`${accountPath('confirm')}${returnTo}`);
244
+ return redirectExact(ctx.response, `${accountPath('confirm')}${returnTo}`);
245
245
  }