@adonis-agora/authkit-server 0.90.2 → 1.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3654 -0
- package/build/commands/clients_create.d.ts +1 -0
- package/build/commands/clients_create.js +15 -2
- package/build/commands/commands.json +7 -0
- package/build/host/ui/admin.html +1 -1
- package/build/host/views/account/apps.edge +8 -3
- package/build/host/views/partials/styles.edge +1 -1
- package/build/index.d.ts +5 -3
- package/build/index.js +4 -5
- package/build/providers/authkit_server_provider.js +8 -0
- package/build/src/accounts/lucid_account_store.d.ts +0 -18
- package/build/src/accounts/lucid_account_store.js +7 -48
- package/build/src/accounts/lucid_store/mfa.js +1 -1
- package/build/src/accounts/lucid_store/organizations.d.ts +1 -1
- package/build/src/accounts/lucid_store/password_hygiene.d.ts +2 -3
- package/build/src/accounts/lucid_store/password_hygiene.js +2 -3
- package/build/src/accounts/lucid_store/shared.d.ts +0 -7
- package/build/src/accounts/lucid_store/shared.js +0 -14
- package/build/src/accounts/lucid_stores.d.ts +1 -1
- package/build/src/adapters/redis_adapter.js +12 -2
- package/build/src/console_authorize.d.ts +56 -0
- package/build/src/console_authorize.js +85 -0
- package/build/src/define_config.d.ts +26 -14
- package/build/src/define_config.js +5 -3
- package/build/src/doctor/checks.d.ts +10 -5
- package/build/src/doctor/checks.js +52 -8
- package/build/src/host/account_api/account_api_controller.d.ts +2 -1
- package/build/src/host/account_api/account_api_controller.js +5 -11
- package/build/src/host/admin_api/api_clients_controller.d.ts +9 -3
- package/build/src/host/admin_api/dto.d.ts +3 -1
- package/build/src/host/admin_api/dto.js +3 -1
- package/build/src/host/admin_clients_service.d.ts +13 -5
- package/build/src/host/admin_clients_service.js +28 -6
- package/build/src/host/admin_console/admin_shell_controller.d.ts +16 -1
- package/build/src/host/admin_console/admin_shell_controller.js +21 -7
- package/build/src/host/admin_console/console_clients_controller.d.ts +21 -2
- package/build/src/host/admin_console/console_clients_controller.js +9 -0
- package/build/src/host/admin_console/console_impersonation_controller.js +3 -8
- package/build/src/host/admin_console/console_overview_controller.d.ts +2 -2
- package/build/src/host/admin_console/console_overview_controller.js +4 -2
- package/build/src/host/admin_console_warning.d.ts +8 -0
- package/build/src/host/admin_console_warning.js +18 -0
- package/build/src/host/admin_validators.d.ts +15 -11
- package/build/src/host/admin_validators.js +16 -9
- package/build/src/host/auth_host_config.d.ts +5 -0
- package/build/src/host/client_names.d.ts +9 -2
- package/build/src/host/client_names.js +11 -3
- package/build/src/host/consent_params.d.ts +7 -0
- package/build/src/host/consent_params.js +27 -0
- package/build/src/host/console_denied_page.d.ts +6 -0
- package/build/src/host/console_denied_page.js +18 -0
- package/build/src/host/console_sso.d.ts +24 -0
- package/build/src/host/console_sso.js +70 -0
- package/build/src/host/controllers/account_apps_controller.js +4 -3
- package/build/src/host/controllers/account_confirm_controller.d.ts +0 -7
- package/build/src/host/controllers/account_confirm_controller.js +0 -6
- package/build/src/host/controllers/account_orgs_controller.d.ts +0 -50
- package/build/src/host/controllers/account_orgs_controller.js +7 -113
- package/build/src/host/controllers/account_security_controller.js +7 -11
- package/build/src/host/controllers/account_session_controller.js +10 -1
- package/build/src/host/controllers/agent_consent_controller.js +23 -5
- package/build/src/host/controllers/console_sso_controller.d.ts +12 -0
- package/build/src/host/controllers/console_sso_controller.js +61 -0
- package/build/src/host/controllers/interaction_controller.js +9 -3
- package/build/src/host/controllers/poppy_consent_controller.js +33 -7
- package/build/src/host/controllers/registration_controller.js +7 -2
- package/build/src/host/i18n.d.ts +0 -4
- package/build/src/host/i18n.js +0 -6
- package/build/src/host/impersonation.d.ts +7 -15
- package/build/src/host/impersonation.js +7 -16
- package/build/src/host/interaction_recovery.js +4 -1
- package/build/src/host/login_attempt.js +5 -1
- package/build/src/host/middleware/account_auth.js +6 -0
- package/build/src/host/rate_limit.js +5 -1
- package/build/src/host/register_auth_host.d.ts +8 -0
- package/build/src/host/register_auth_host.js +116 -11
- package/build/src/host/renderers/inertia_renderer.d.ts +9 -21
- package/build/src/host/renderers/inertia_renderer.js +5 -6
- package/build/src/host/screen_props.d.ts +309 -0
- package/build/src/host/screen_props.js +11 -0
- package/build/src/host/sudo_mode.d.ts +5 -11
- package/build/src/host/sudo_mode.js +5 -11
- package/build/src/host/ui-dist/assets/index-D0EIeyKK.js +67 -0
- package/build/src/host/ui-dist/index.html +1 -1
- package/build/src/password/password_manager.js +5 -0
- package/build/src/provider/build_provider.js +6 -34
- package/build/src/provider/oidc_service.d.ts +0 -2
- package/build/src/provider/oidc_service.js +1 -12
- package/build/src/provider/token_exchange.d.ts +1 -3
- package/build/src/provider/token_exchange.js +1 -1
- package/build/stubs/config/authkit_react.stub +3 -3
- package/package.json +12 -11
- package/skills/authkit-interactions/SKILL.md +32 -5
- package/stubs/config/authkit_react.stub +3 -3
- package/build/src/commands/import_clients.d.ts +0 -40
- package/build/src/commands/import_clients.js +0 -81
- package/build/src/host/ui-dist/assets/index-DUVBDAyh.js +0 -67
- package/build/src/mixins/with_mfa.d.ts +0 -35
- package/build/src/mixins/with_mfa.js +0 -21
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** `client_id` do client interno do console. Reservado: não pode ser cadastrado. */
|
|
2
|
+
export declare const CONSOLE_SSO_CLIENT_ID = "authkit-console";
|
|
3
|
+
/** Rota que inicia o login do console pelo IdP. */
|
|
4
|
+
export declare const CONSOLE_SSO_START_PATH = "/auth/console/sso";
|
|
5
|
+
/** `redirect_uri` do client interno: valida o `state` e abre o console. */
|
|
6
|
+
export declare const CONSOLE_SSO_CALLBACK_PATH = "/auth/console/callback";
|
|
7
|
+
/** Chave da sessão Adonis com o `state` e o destino de um login em andamento. */
|
|
8
|
+
export declare const CONSOLE_SSO_STATE_KEY = "authkit_console_sso";
|
|
9
|
+
/** O que fica na sessão entre o início e o callback. */
|
|
10
|
+
export interface ConsoleSsoState {
|
|
11
|
+
state: string;
|
|
12
|
+
returnTo: string | null;
|
|
13
|
+
}
|
|
14
|
+
/** Metadata do client interno para o `clients` estático do oidc-provider. */
|
|
15
|
+
export declare function consoleSsoClientMetadata(issuer: string): Record<string, unknown>;
|
|
16
|
+
/** Novo `state` opaco para um login do console. */
|
|
17
|
+
export declare function newConsoleSsoState(): string;
|
|
18
|
+
/**
|
|
19
|
+
* Path do authorize do provider. O issuer termina no `mountPath` (garantido por
|
|
20
|
+
* `registerAuthHost`), então o mount é o path do issuer.
|
|
21
|
+
*/
|
|
22
|
+
export declare function authorizationPath(issuer: string, provider?: any): string;
|
|
23
|
+
/** URL do login do console pelo IdP, preservando o destino como `return_to`. */
|
|
24
|
+
export declare function consoleSsoUrl(returnTo?: string | null): string;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Login do console pelo IdP (SSO), o jeito padrão de entrar em `/account/*` e no
|
|
3
|
+
* console admin.
|
|
4
|
+
*
|
|
5
|
+
* O console é tratado como mais um app do IdP, como o Keycloak faz com o
|
|
6
|
+
* `account-console` e o `security-admin-console`: sem sessão, o visitante vai
|
|
7
|
+
* para a tela de login do IdP (a mesma interaction de qualquer client: senha,
|
|
8
|
+
* magic link, OTP, passkey, social), volta, e a ponte de
|
|
9
|
+
* `idp_session_bridge.ts` abre o console para a conta da sessão do IdP.
|
|
10
|
+
*
|
|
11
|
+
* Para isso existe um client interno, `authkit-console`, injetado como client
|
|
12
|
+
* ESTÁTICO do provider (não fica no adapter, então não aparece na lista de
|
|
13
|
+
* clients do console nem pode ser editado):
|
|
14
|
+
*
|
|
15
|
+
* - `response_types: ['none']`: o authorize não emite code nem token. O client
|
|
16
|
+
* só usa o IdP para autenticar; o que abre o console é a sessão do IdP que o
|
|
17
|
+
* login criou, lida pela ponte. Não há segredo, PKCE ou troca de code.
|
|
18
|
+
* - o consent é concedido automaticamente (é a própria lib).
|
|
19
|
+
* - a única `redirect_uri` é `/auth/console/callback`, no MESMO host do issuer.
|
|
20
|
+
*
|
|
21
|
+
* Só existe com `accountSession.acceptIdpSession` ligado (o default).
|
|
22
|
+
*/
|
|
23
|
+
import { randomBytes } from 'node:crypto';
|
|
24
|
+
/** `client_id` do client interno do console. Reservado: não pode ser cadastrado. */
|
|
25
|
+
export const CONSOLE_SSO_CLIENT_ID = 'authkit-console';
|
|
26
|
+
/** Rota que inicia o login do console pelo IdP. */
|
|
27
|
+
export const CONSOLE_SSO_START_PATH = '/auth/console/sso';
|
|
28
|
+
/** `redirect_uri` do client interno: valida o `state` e abre o console. */
|
|
29
|
+
export const CONSOLE_SSO_CALLBACK_PATH = '/auth/console/callback';
|
|
30
|
+
/** Chave da sessão Adonis com o `state` e o destino de um login em andamento. */
|
|
31
|
+
export const CONSOLE_SSO_STATE_KEY = 'authkit_console_sso';
|
|
32
|
+
/** Metadata do client interno para o `clients` estático do oidc-provider. */
|
|
33
|
+
export function consoleSsoClientMetadata(issuer) {
|
|
34
|
+
const origin = new URL(issuer).origin;
|
|
35
|
+
return {
|
|
36
|
+
client_id: CONSOLE_SSO_CLIENT_ID,
|
|
37
|
+
client_name: 'Account console',
|
|
38
|
+
application_type: 'web',
|
|
39
|
+
token_endpoint_auth_method: 'none',
|
|
40
|
+
response_types: ['none'],
|
|
41
|
+
grant_types: [],
|
|
42
|
+
redirect_uris: [`${origin}${CONSOLE_SSO_CALLBACK_PATH}`],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** Novo `state` opaco para um login do console. */
|
|
46
|
+
export function newConsoleSsoState() {
|
|
47
|
+
return randomBytes(24).toString('base64url');
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Path do authorize do provider. O issuer termina no `mountPath` (garantido por
|
|
51
|
+
* `registerAuthHost`), então o mount é o path do issuer.
|
|
52
|
+
*/
|
|
53
|
+
export function authorizationPath(issuer, provider) {
|
|
54
|
+
const mount = new URL(issuer).pathname.replace(/\/+$/, '');
|
|
55
|
+
if (provider && typeof provider.pathFor === 'function') {
|
|
56
|
+
try {
|
|
57
|
+
return provider.pathFor('authorization', { mountPath: mount });
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// cai no default do oidc-provider abaixo
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return `${mount}/auth`;
|
|
64
|
+
}
|
|
65
|
+
/** URL do login do console pelo IdP, preservando o destino como `return_to`. */
|
|
66
|
+
export function consoleSsoUrl(returnTo) {
|
|
67
|
+
if (!returnTo)
|
|
68
|
+
return CONSOLE_SSO_START_PATH;
|
|
69
|
+
return `${CONSOLE_SSO_START_PATH}?return_to=${encodeURIComponent(returnTo)}`;
|
|
70
|
+
}
|
|
@@ -4,7 +4,7 @@ import { personalAgentsFor } from '../../agents/runtime.js';
|
|
|
4
4
|
import { accountPath } from '../account_paths.js';
|
|
5
5
|
import { ACCOUNT_SESSION_KEY } from '../account_session_key.js';
|
|
6
6
|
import { AdminSessionsService } from '../admin_sessions_service.js';
|
|
7
|
-
import {
|
|
7
|
+
import { clientDisplayInfo } from '../client_names.js';
|
|
8
8
|
/**
|
|
9
9
|
* Self-service de consentimento ("apps com acesso") no console de conta. Lista os
|
|
10
10
|
* Grants da própria conta agrupados por client (resolvendo o nome do client da
|
|
@@ -23,8 +23,8 @@ export default class AccountAppsController {
|
|
|
23
23
|
const supported = sessions.canList;
|
|
24
24
|
const grantList = supported ? await sessions.listGrants(accountId) : [];
|
|
25
25
|
// O nome que o client registrou; sem ele, o clientId.
|
|
26
|
-
const
|
|
27
|
-
const nameOf = (clientId) => clientId ? (
|
|
26
|
+
const info = await clientDisplayInfo(service, grantList.map((g) => g.clientId).filter((id) => !!id));
|
|
27
|
+
const nameOf = (clientId) => clientId ? (info.get(clientId)?.name ?? clientId) : '';
|
|
28
28
|
const revoked = ctx.session.flashMessages.get('appRevoked');
|
|
29
29
|
// Personal agents com delegação ativa (só quando a feature está ligada).
|
|
30
30
|
const agentsRuntime = await personalAgentsFor(ctx);
|
|
@@ -58,6 +58,7 @@ export default class AccountAppsController {
|
|
|
58
58
|
.map((g) => ({
|
|
59
59
|
clientId: g.clientId,
|
|
60
60
|
name: nameOf(g.clientId),
|
|
61
|
+
logoUrl: info.get(g.clientId)?.logoUrl ?? null,
|
|
61
62
|
accessTokens: g.accessTokens,
|
|
62
63
|
refreshTokens: g.refreshTokens,
|
|
63
64
|
})),
|
|
@@ -9,13 +9,6 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import '../augmentations.js';
|
|
11
11
|
import type { HttpContext } from '@adonisjs/core/http';
|
|
12
|
-
import { sudoContextFrom } from '../sudo/runtime.js';
|
|
13
|
-
/**
|
|
14
|
-
* Reexport de compatibilidade. O construtor canônico do `SudoContext` vive em
|
|
15
|
-
* `sudo/runtime.ts` (é runtime do SPI, não detalhe da tela); este caminho
|
|
16
|
-
* antigo segue valendo para quem já o importava.
|
|
17
|
-
*/
|
|
18
|
-
export { sudoContextFrom };
|
|
19
12
|
export default class AccountConfirmController {
|
|
20
13
|
show(ctx: HttpContext): Promise<unknown>;
|
|
21
14
|
}
|
|
@@ -9,12 +9,6 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import '../augmentations.js';
|
|
11
11
|
import { configuredSudoMethods, isSudoMethodMounted, LAST_METHOD_SESSION_KEY, resolveAvailableMethods, sudoContextFrom, } from '../sudo/runtime.js';
|
|
12
|
-
/**
|
|
13
|
-
* Reexport de compatibilidade. O construtor canônico do `SudoContext` vive em
|
|
14
|
-
* `sudo/runtime.ts` (é runtime do SPI, não detalhe da tela); este caminho
|
|
15
|
-
* antigo segue valendo para quem já o importava.
|
|
16
|
-
*/
|
|
17
|
-
export { sudoContextFrom };
|
|
18
12
|
export default class AccountConfirmController {
|
|
19
13
|
async show(ctx) {
|
|
20
14
|
const c = await sudoContextFrom(ctx);
|
|
@@ -27,54 +27,4 @@ export default class AccountOrgsController {
|
|
|
27
27
|
removeMember(ctx: HttpContext): Promise<void>;
|
|
28
28
|
/** POST /account/orgs/:id/invitations/:invId/revoke */
|
|
29
29
|
revokeInvitation(ctx: HttpContext): Promise<void>;
|
|
30
|
-
/**
|
|
31
|
-
* GET /account/orgs/json — lista as orgs do usuário logado com papel + se ativa.
|
|
32
|
-
* Usado pelo hook `useOrganizations()`.
|
|
33
|
-
*/
|
|
34
|
-
listJson(ctx: HttpContext): Promise<void | {
|
|
35
|
-
supported: boolean;
|
|
36
|
-
activeOrgId: any;
|
|
37
|
-
orgs: {
|
|
38
|
-
id: string;
|
|
39
|
-
name: string;
|
|
40
|
-
slug: string;
|
|
41
|
-
logoUrl: string | null;
|
|
42
|
-
role: string;
|
|
43
|
-
isActive: boolean;
|
|
44
|
-
}[];
|
|
45
|
-
}>;
|
|
46
|
-
/**
|
|
47
|
-
* GET /account/orgs/:id/json — detalhes da org ativa (para uso pelo hook `useOrganization()`).
|
|
48
|
-
* Só retorna se o usuário é membro.
|
|
49
|
-
*/
|
|
50
|
-
showJson(ctx: HttpContext): Promise<void | {
|
|
51
|
-
id: string;
|
|
52
|
-
name: string;
|
|
53
|
-
slug: string;
|
|
54
|
-
logoUrl: string | null;
|
|
55
|
-
role: string;
|
|
56
|
-
canManage: boolean;
|
|
57
|
-
members: {
|
|
58
|
-
accountId: string;
|
|
59
|
-
email: string | null;
|
|
60
|
-
role: string;
|
|
61
|
-
joinedAt: string;
|
|
62
|
-
}[];
|
|
63
|
-
}>;
|
|
64
|
-
/**
|
|
65
|
-
* GET /account/orgs/invitations/json — lista convites pendentes pro e-mail do usuário logado.
|
|
66
|
-
* Usado pelo hook `useOrgInvitations()`.
|
|
67
|
-
*/
|
|
68
|
-
listInvitationsJson(ctx: HttpContext): Promise<void | {
|
|
69
|
-
invitations: {
|
|
70
|
-
id: string;
|
|
71
|
-
organizationId: string;
|
|
72
|
-
orgName: string;
|
|
73
|
-
orgSlug: string;
|
|
74
|
-
email: string;
|
|
75
|
-
role: string;
|
|
76
|
-
expiresAt: string;
|
|
77
|
-
createdAt: string;
|
|
78
|
-
}[];
|
|
79
|
-
}>;
|
|
80
30
|
}
|
|
@@ -27,12 +27,10 @@ export default class AccountOrgsController {
|
|
|
27
27
|
const service = await ctx.containerResolver.make('authkit.server');
|
|
28
28
|
const cfg = service.config;
|
|
29
29
|
const store = cfg.accountStore;
|
|
30
|
-
const messages = cfg.messages;
|
|
31
30
|
if (!supportsOrganizations(store)) {
|
|
32
31
|
return cfg.render
|
|
33
32
|
? cfg.render(ctx, 'account/orgs', {
|
|
34
33
|
supported: false,
|
|
35
|
-
messages,
|
|
36
34
|
csrfToken: ctx.request.csrfToken,
|
|
37
35
|
})
|
|
38
36
|
: response.notFound();
|
|
@@ -64,10 +62,11 @@ export default class AccountOrgsController {
|
|
|
64
62
|
pendingInvitations: invitationsWithOrg,
|
|
65
63
|
allowSelfCreate: policy.allowSelfCreate,
|
|
66
64
|
availableRoles: policy.roles,
|
|
67
|
-
messages,
|
|
68
65
|
csrfToken: ctx.request.csrfToken,
|
|
69
66
|
};
|
|
70
|
-
return cfg.render
|
|
67
|
+
return cfg.render
|
|
68
|
+
? cfg.render(ctx, 'account/orgs', props)
|
|
69
|
+
: response.notFound();
|
|
71
70
|
}
|
|
72
71
|
/** POST /account/orgs — criar nova org (requer allowSelfCreate). */
|
|
73
72
|
async store(ctx) {
|
|
@@ -265,11 +264,13 @@ export default class AccountOrgsController {
|
|
|
265
264
|
const props = {
|
|
266
265
|
invitation,
|
|
267
266
|
token: params.token,
|
|
268
|
-
messages: cfg.messages,
|
|
269
267
|
csrfToken: ctx.request.csrfToken,
|
|
270
268
|
};
|
|
271
269
|
return cfg.render
|
|
272
|
-
? cfg.render(ctx, 'account/orgs', {
|
|
270
|
+
? cfg.render(ctx, 'account/orgs', {
|
|
271
|
+
...props,
|
|
272
|
+
subview: 'accept-invitation',
|
|
273
|
+
})
|
|
273
274
|
: response.notFound();
|
|
274
275
|
}
|
|
275
276
|
/** POST /account/orgs/invitations/:token/accept — processa aceite. */
|
|
@@ -351,111 +352,4 @@ export default class AccountOrgsController {
|
|
|
351
352
|
});
|
|
352
353
|
return response.redirect(accountPath('orgs'));
|
|
353
354
|
}
|
|
354
|
-
// ──────────────────────────────────────────────────────────────────────────
|
|
355
|
-
// JSON endpoints — consumed by @adonis-agora/authkit-react hooks.
|
|
356
|
-
// ──────────────────────────────────────────────────────────────────────────
|
|
357
|
-
/**
|
|
358
|
-
* GET /account/orgs/json — lista as orgs do usuário logado com papel + se ativa.
|
|
359
|
-
* Usado pelo hook `useOrganizations()`.
|
|
360
|
-
*/
|
|
361
|
-
async listJson(ctx) {
|
|
362
|
-
const { session, response, request } = ctx;
|
|
363
|
-
const service = await ctx.containerResolver.make('authkit.server');
|
|
364
|
-
const cfg = service.config;
|
|
365
|
-
const store = cfg.accountStore;
|
|
366
|
-
if (!supportsOrganizations(store))
|
|
367
|
-
return { supported: false, orgs: [], activeOrgId: null };
|
|
368
|
-
const accountId = session.get(ACCOUNT_SESSION_KEY);
|
|
369
|
-
if (!accountId)
|
|
370
|
-
return response.unauthorized({ message: 'Not authenticated.' });
|
|
371
|
-
const orgs = await store.listOrgsForAccount(accountId);
|
|
372
|
-
const activeOrgRaw = request.cookie(ACTIVE_ORG_COOKIE);
|
|
373
|
-
const activeOrgId = activeOrgRaw ? activeOrgRaw.split('\t')[0] : null;
|
|
374
|
-
return {
|
|
375
|
-
supported: true,
|
|
376
|
-
activeOrgId,
|
|
377
|
-
orgs: orgs.map((org) => ({
|
|
378
|
-
id: org.id,
|
|
379
|
-
name: org.name,
|
|
380
|
-
slug: org.slug,
|
|
381
|
-
logoUrl: org.logoUrl ?? null,
|
|
382
|
-
role: org.role,
|
|
383
|
-
isActive: org.id === activeOrgId,
|
|
384
|
-
})),
|
|
385
|
-
};
|
|
386
|
-
}
|
|
387
|
-
/**
|
|
388
|
-
* GET /account/orgs/:id/json — detalhes da org ativa (para uso pelo hook `useOrganization()`).
|
|
389
|
-
* Só retorna se o usuário é membro.
|
|
390
|
-
*/
|
|
391
|
-
async showJson(ctx) {
|
|
392
|
-
const { session, response, params } = ctx;
|
|
393
|
-
const service = await ctx.containerResolver.make('authkit.server');
|
|
394
|
-
const cfg = service.config;
|
|
395
|
-
const store = cfg.accountStore;
|
|
396
|
-
if (!supportsOrganizations(store))
|
|
397
|
-
return response.notFound({ message: 'Not supported.' });
|
|
398
|
-
const accountId = session.get(ACCOUNT_SESSION_KEY);
|
|
399
|
-
if (!accountId)
|
|
400
|
-
return response.unauthorized({ message: 'Not authenticated.' });
|
|
401
|
-
const membership = await store.getOrgMembership(params.id, accountId);
|
|
402
|
-
if (!membership)
|
|
403
|
-
return response.notFound({ message: 'Organization not found or not a member.' });
|
|
404
|
-
const org = await store.findOrgById(params.id);
|
|
405
|
-
if (!org)
|
|
406
|
-
return response.notFound({ message: 'Organization not found.' });
|
|
407
|
-
const canManage = membership.role === 'owner' || membership.role === 'admin';
|
|
408
|
-
const members = canManage ? await store.listOrgMembers(params.id) : [];
|
|
409
|
-
const enrichedMembers = await Promise.all(members.map(async (m) => {
|
|
410
|
-
const account = await store.findById(m.accountId);
|
|
411
|
-
return {
|
|
412
|
-
accountId: m.accountId,
|
|
413
|
-
email: account?.email ?? null,
|
|
414
|
-
role: m.role,
|
|
415
|
-
joinedAt: m.joinedAt,
|
|
416
|
-
};
|
|
417
|
-
}));
|
|
418
|
-
return {
|
|
419
|
-
id: org.id,
|
|
420
|
-
name: org.name,
|
|
421
|
-
slug: org.slug,
|
|
422
|
-
logoUrl: org.logoUrl ?? null,
|
|
423
|
-
role: membership.role,
|
|
424
|
-
canManage,
|
|
425
|
-
members: enrichedMembers,
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
/**
|
|
429
|
-
* GET /account/orgs/invitations/json — lista convites pendentes pro e-mail do usuário logado.
|
|
430
|
-
* Usado pelo hook `useOrgInvitations()`.
|
|
431
|
-
*/
|
|
432
|
-
async listInvitationsJson(ctx) {
|
|
433
|
-
const { session, response } = ctx;
|
|
434
|
-
const service = await ctx.containerResolver.make('authkit.server');
|
|
435
|
-
const cfg = service.config;
|
|
436
|
-
const store = cfg.accountStore;
|
|
437
|
-
if (!supportsOrganizations(store))
|
|
438
|
-
return { invitations: [] };
|
|
439
|
-
const accountId = session.get(ACCOUNT_SESSION_KEY);
|
|
440
|
-
if (!accountId)
|
|
441
|
-
return response.unauthorized({ message: 'Not authenticated.' });
|
|
442
|
-
const account = await store.findById(accountId);
|
|
443
|
-
if (!account)
|
|
444
|
-
return response.unauthorized({ message: 'Not authenticated.' });
|
|
445
|
-
const invitations = await store.listPendingInvitationsForEmail(account.email);
|
|
446
|
-
const enriched = await Promise.all(invitations.map(async (inv) => {
|
|
447
|
-
const org = await store.findOrgById(inv.organizationId);
|
|
448
|
-
return {
|
|
449
|
-
id: inv.id,
|
|
450
|
-
organizationId: inv.organizationId,
|
|
451
|
-
orgName: org?.name ?? inv.organizationId,
|
|
452
|
-
orgSlug: org?.slug ?? inv.organizationId,
|
|
453
|
-
email: inv.email,
|
|
454
|
-
role: inv.role,
|
|
455
|
-
expiresAt: inv.expiresAt,
|
|
456
|
-
createdAt: inv.createdAt,
|
|
457
|
-
};
|
|
458
|
-
}));
|
|
459
|
-
return { invitations: enriched };
|
|
460
|
-
}
|
|
461
355
|
}
|
|
@@ -426,7 +426,7 @@ export default class AccountSecurityController {
|
|
|
426
426
|
});
|
|
427
427
|
const origin = authkitOrigin(cfg);
|
|
428
428
|
const confirmUrl = `${origin}${accountPath('emailConfirm')}?token=${encodeURIComponent(issued.token)}`;
|
|
429
|
-
// 1. Link de confirmação → NOVO e-mail (hook onEmailChangeConfirm >
|
|
429
|
+
// 1. Link de confirmação → NOVO e-mail (hook onEmailChangeConfirm > default).
|
|
430
430
|
if (cfg.mail?.onEmailChangeConfirm) {
|
|
431
431
|
await cfg.mail.onEmailChangeConfirm({
|
|
432
432
|
email: newEmail,
|
|
@@ -435,14 +435,6 @@ export default class AccountSecurityController {
|
|
|
435
435
|
oldEmail: account?.email ?? '',
|
|
436
436
|
});
|
|
437
437
|
}
|
|
438
|
-
else if (cfg.mail?.onEmailVerification) {
|
|
439
|
-
// Retrocompat: hook legado onEmailVerification (ainda funciona como override).
|
|
440
|
-
await cfg.mail.onEmailVerification({
|
|
441
|
-
email: newEmail,
|
|
442
|
-
verifyUrl: confirmUrl,
|
|
443
|
-
token: issued.token,
|
|
444
|
-
});
|
|
445
|
-
}
|
|
446
438
|
else {
|
|
447
439
|
await sendEmailChangeConfirmationEmail(ctx, {
|
|
448
440
|
email: newEmail,
|
|
@@ -513,7 +505,9 @@ export default class AccountSecurityController {
|
|
|
513
505
|
const store = cfg.accountStore;
|
|
514
506
|
const render = cfg.render;
|
|
515
507
|
if (!supportsAccountSecurity(store)) {
|
|
516
|
-
return render(ctx, 'account/email-confirmed', {
|
|
508
|
+
return render(ctx, 'account/email-confirmed', {
|
|
509
|
+
ok: false,
|
|
510
|
+
});
|
|
517
511
|
}
|
|
518
512
|
const token = ctx.request.qs().token ?? '';
|
|
519
513
|
const result = await store.confirmEmailChange(token);
|
|
@@ -551,6 +545,8 @@ export default class AccountSecurityController {
|
|
|
551
545
|
metadata: { oldEmail: result.oldEmail, newEmail: result.newEmail },
|
|
552
546
|
}, cfg.mail, cfg.audit, cfg);
|
|
553
547
|
}
|
|
554
|
-
return render(ctx, 'account/email-confirmed', {
|
|
548
|
+
return render(ctx, 'account/email-confirmed', {
|
|
549
|
+
ok: result.ok,
|
|
550
|
+
});
|
|
555
551
|
}
|
|
556
552
|
}
|
|
@@ -3,6 +3,7 @@ import { accountHome } from '../account_home.js';
|
|
|
3
3
|
import { getAccountLoginUrl } from '../account_login_url.js';
|
|
4
4
|
import { ACCOUNT_SESSION_KEY } from '../account_session_key.js';
|
|
5
5
|
import { syncAdonisAuthLogin, syncAdonisAuthLogout } from '../adonis_auth_sync.js';
|
|
6
|
+
import { consoleSsoUrl } from '../console_sso.js';
|
|
6
7
|
import { normalizeEmailIdentifier } from '../email_identifier.js';
|
|
7
8
|
import { translate } from '../i18n.js';
|
|
8
9
|
import { endBridgedIdpSession } from '../idp_session_bridge.js';
|
|
@@ -49,8 +50,16 @@ export default class AccountSessionController {
|
|
|
49
50
|
return ctx.response.redirect(accountHome(cfg));
|
|
50
51
|
}
|
|
51
52
|
// Lê e valida o return_to da query-string — descarta valores inválidos (open-redirect).
|
|
52
|
-
const
|
|
53
|
+
const qs = (ctx.request.qs?.() ?? {});
|
|
54
|
+
const rawReturnTo = qs.return_to ?? ctx.request.input?.('return_to');
|
|
53
55
|
const returnTo = validateReturnTo(rawReturnTo);
|
|
56
|
+
// SSO (default): o console entra pelo login do IdP, a mesma tela de qualquer
|
|
57
|
+
// app. O formulário de senha daqui fica como porta alternativa
|
|
58
|
+
// (`?method=password`), e só quando a senha não foi desligada no config.
|
|
59
|
+
const passwordForm = qs.method === 'password' && cfg.authMethods?.password !== false;
|
|
60
|
+
if (cfg.accountSession?.acceptIdpSession === true && !passwordForm) {
|
|
61
|
+
return redirectExact(ctx.response, consoleSsoUrl(returnTo));
|
|
62
|
+
}
|
|
54
63
|
const props = {
|
|
55
64
|
csrfToken: ctx.request.csrfToken,
|
|
56
65
|
returnTo,
|
|
@@ -51,14 +51,20 @@ export default class AgentConsentController {
|
|
|
51
51
|
error: null,
|
|
52
52
|
};
|
|
53
53
|
if (impersonationState(ctx).active) {
|
|
54
|
-
return render(ctx, 'agents/consent', {
|
|
54
|
+
return render(ctx, 'agents/consent', {
|
|
55
|
+
...base,
|
|
56
|
+
error: 'agents.consent.impersonating',
|
|
57
|
+
});
|
|
55
58
|
}
|
|
56
59
|
const input = ctx.request.input('user_code');
|
|
57
60
|
if (!input)
|
|
58
61
|
return render(ctx, 'agents/consent', base);
|
|
59
62
|
const pending = await runtime.delegation.pendingRequest(input);
|
|
60
63
|
if (!pending) {
|
|
61
|
-
return render(ctx, 'agents/consent', {
|
|
64
|
+
return render(ctx, 'agents/consent', {
|
|
65
|
+
...base,
|
|
66
|
+
error: 'agents.consent.invalid_code',
|
|
67
|
+
});
|
|
62
68
|
}
|
|
63
69
|
const accountId = ctx.session.get(ACCOUNT_SESSION_KEY);
|
|
64
70
|
const account = await service.config.accountStore.findById(accountId);
|
|
@@ -96,7 +102,11 @@ export default class AgentConsentController {
|
|
|
96
102
|
const userCode = typeof rawCode === 'string' ? rawCode : '';
|
|
97
103
|
const pending = await runtime.delegation.pendingRequest(userCode);
|
|
98
104
|
if (!pending) {
|
|
99
|
-
return render(ctx, 'agents/done', {
|
|
105
|
+
return render(ctx, 'agents/done', {
|
|
106
|
+
status: 'expired',
|
|
107
|
+
agentName: null,
|
|
108
|
+
scopes: [],
|
|
109
|
+
});
|
|
100
110
|
}
|
|
101
111
|
const name = await agentName(runtime, pending.clientId);
|
|
102
112
|
const raw = ctx.request.input('scope');
|
|
@@ -108,7 +118,11 @@ export default class AgentConsentController {
|
|
|
108
118
|
// Negou ou desmarcou tudo. Se nem negar deu, o pedido mudou de estado no
|
|
109
119
|
// meio do caminho (aprovado em outra aba, expirou).
|
|
110
120
|
if (!(await runtime.delegation.deny({ userCode, accountId }))) {
|
|
111
|
-
return render(ctx, 'agents/done', {
|
|
121
|
+
return render(ctx, 'agents/done', {
|
|
122
|
+
status: 'expired',
|
|
123
|
+
agentName: name,
|
|
124
|
+
scopes: [],
|
|
125
|
+
});
|
|
112
126
|
}
|
|
113
127
|
await cfg.audit?.record({
|
|
114
128
|
type: 'agent.delegation_denied',
|
|
@@ -117,7 +131,11 @@ export default class AgentConsentController {
|
|
|
117
131
|
ip: ctx.request.ip?.() ?? null,
|
|
118
132
|
metadata: { requested: pending.scopes.map((s) => s.id) },
|
|
119
133
|
});
|
|
120
|
-
return render(ctx, 'agents/done', {
|
|
134
|
+
return render(ctx, 'agents/done', {
|
|
135
|
+
status: 'denied',
|
|
136
|
+
agentName: name,
|
|
137
|
+
scopes: [],
|
|
138
|
+
});
|
|
121
139
|
}
|
|
122
140
|
await cfg.audit?.record({
|
|
123
141
|
type: 'agent.delegation_approved',
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import '../augmentations.js';
|
|
2
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
3
|
+
/**
|
|
4
|
+
* Login do console pelo IdP (ver `console_sso.ts`).
|
|
5
|
+
*
|
|
6
|
+
* GET /auth/console/sso?return_to=… → authorize do client interno
|
|
7
|
+
* GET /auth/console/callback?state=… → valida o state, abre o console pela ponte
|
|
8
|
+
*/
|
|
9
|
+
export default class ConsoleSsoController {
|
|
10
|
+
start(ctx: HttpContext): Promise<unknown>;
|
|
11
|
+
callback(ctx: HttpContext): Promise<unknown>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import '../augmentations.js';
|
|
2
|
+
import { accountHome } from '../account_home.js';
|
|
3
|
+
import { getAccountLoginUrl } from '../account_login_url.js';
|
|
4
|
+
import { authorizationPath, CONSOLE_SSO_CALLBACK_PATH, CONSOLE_SSO_CLIENT_ID, CONSOLE_SSO_STATE_KEY, newConsoleSsoState, } from '../console_sso.js';
|
|
5
|
+
import { ensureConsoleSession } from '../idp_session_bridge.js';
|
|
6
|
+
import { redirectExact } from '../redirect_exact.js';
|
|
7
|
+
import { validateReturnTo } from './account_session_controller.js';
|
|
8
|
+
/**
|
|
9
|
+
* Login do console pelo IdP (ver `console_sso.ts`).
|
|
10
|
+
*
|
|
11
|
+
* GET /auth/console/sso?return_to=… → authorize do client interno
|
|
12
|
+
* GET /auth/console/callback?state=… → valida o state, abre o console pela ponte
|
|
13
|
+
*/
|
|
14
|
+
export default class ConsoleSsoController {
|
|
15
|
+
async start(ctx) {
|
|
16
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
17
|
+
const cfg = service.config;
|
|
18
|
+
const returnTo = validateReturnTo(ctx.request.qs?.()?.return_to ?? ctx.request.input?.('return_to'));
|
|
19
|
+
// SSO desligado (`acceptIdpSession: false`): o console só tem o login próprio.
|
|
20
|
+
if (cfg.accountSession?.acceptIdpSession !== true) {
|
|
21
|
+
const login = getAccountLoginUrl();
|
|
22
|
+
const sep = login.includes('?') ? '&' : '?';
|
|
23
|
+
return redirectExact(ctx.response, returnTo ? `${login}${sep}return_to=${encodeURIComponent(returnTo)}` : login);
|
|
24
|
+
}
|
|
25
|
+
// Já tem sessão (de console ou do IdP): não precisa passar pelo IdP.
|
|
26
|
+
if (await ensureConsoleSession(ctx)) {
|
|
27
|
+
return redirectExact(ctx.response, returnTo ?? accountHome(cfg));
|
|
28
|
+
}
|
|
29
|
+
const state = newConsoleSsoState();
|
|
30
|
+
ctx.session.put(CONSOLE_SSO_STATE_KEY, { state, returnTo });
|
|
31
|
+
const origin = new URL(cfg.issuer).origin;
|
|
32
|
+
const query = new URLSearchParams({
|
|
33
|
+
client_id: CONSOLE_SSO_CLIENT_ID,
|
|
34
|
+
response_type: 'none',
|
|
35
|
+
scope: 'openid',
|
|
36
|
+
redirect_uri: `${origin}${CONSOLE_SSO_CALLBACK_PATH}`,
|
|
37
|
+
state,
|
|
38
|
+
});
|
|
39
|
+
return redirectExact(ctx.response, `${authorizationPath(cfg.issuer, service.provider)}?${query.toString()}`);
|
|
40
|
+
}
|
|
41
|
+
async callback(ctx) {
|
|
42
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
43
|
+
const cfg = service.config;
|
|
44
|
+
const pending = ctx.session.get(CONSOLE_SSO_STATE_KEY);
|
|
45
|
+
ctx.session.forget(CONSOLE_SSO_STATE_KEY);
|
|
46
|
+
const qs = (ctx.request.qs?.() ?? {});
|
|
47
|
+
// `state` amarra o callback ao navegador que iniciou o login (anti-CSRF de login).
|
|
48
|
+
if (!pending || typeof qs.state !== 'string' || qs.state !== pending.state) {
|
|
49
|
+
return ctx.response.status(400).send('Invalid or expired sign-in request.');
|
|
50
|
+
}
|
|
51
|
+
// O IdP recusou (o usuário cancelou, conta desabilitada, …): sem console.
|
|
52
|
+
// Vai para a raiz, e não de volta ao console, para não entrar em loop.
|
|
53
|
+
if (typeof qs.error === 'string') {
|
|
54
|
+
return redirectExact(ctx.response, '/');
|
|
55
|
+
}
|
|
56
|
+
if (!(await ensureConsoleSession(ctx))) {
|
|
57
|
+
return redirectExact(ctx.response, '/');
|
|
58
|
+
}
|
|
59
|
+
return redirectExact(ctx.response, pending.returnTo ?? accountHome(cfg));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -4,6 +4,8 @@ import { resolveAccountRoles } from '../account_roles.js';
|
|
|
4
4
|
import { AdminSessionsService } from '../admin_sessions_service.js';
|
|
5
5
|
import { guardBotProtection, resolveEffectiveBotProtection } from '../bot_protection.js';
|
|
6
6
|
import { brandFor, isFirstParty } from '../branding.js';
|
|
7
|
+
import { consentRequestParams } from '../consent_params.js';
|
|
8
|
+
import { CONSOLE_SSO_CLIENT_ID } from '../console_sso.js';
|
|
7
9
|
import { isCustomLoginMethodId } from '../custom_login.js';
|
|
8
10
|
import { completeCustomLoginSession } from '../custom_login_completion.js';
|
|
9
11
|
import { beginCustomMfa, clearMfaFlow, customMfaViewProps, getMfaFlow, initializeMfaFlow, recordVerifiedMfaFactor, resolveCustomMfaMethods, verifyCustomMfa, } from '../custom_mfa.js';
|
|
@@ -253,7 +255,8 @@ export default class AuthInteractionController {
|
|
|
253
255
|
}
|
|
254
256
|
}
|
|
255
257
|
if (details.prompt.name === 'consent' &&
|
|
256
|
-
|
|
258
|
+
(details.params.client_id === CONSOLE_SSO_CLIENT_ID ||
|
|
259
|
+
isFirstParty(cfg.branding, details.params.client_id))) {
|
|
257
260
|
// Clients first-party: auto-concede o consent (pula a tela de consent).
|
|
258
261
|
// interactions.consent monta o Grant + interactionFinished e escreve o
|
|
259
262
|
// redirect de volta para o client — opera via provider.interactionDetails,
|
|
@@ -264,7 +267,7 @@ export default class AuthInteractionController {
|
|
|
264
267
|
// Consent or other prompts — unchanged
|
|
265
268
|
return render(ctx, 'consent', {
|
|
266
269
|
uid: details.uid,
|
|
267
|
-
params: details.params,
|
|
270
|
+
params: consentRequestParams(details.params),
|
|
268
271
|
csrfToken: ctx.request.csrfToken,
|
|
269
272
|
brand,
|
|
270
273
|
...(await consentSubject(service, details, cfg.messages)),
|
|
@@ -1950,7 +1953,10 @@ export default class AuthInteractionController {
|
|
|
1950
1953
|
ip,
|
|
1951
1954
|
metadata: { reason: 'token_expired' },
|
|
1952
1955
|
});
|
|
1953
|
-
return render(ctx, 'otp-unlock', {
|
|
1956
|
+
return render(ctx, 'otp-unlock', {
|
|
1957
|
+
ok: false,
|
|
1958
|
+
expired: true,
|
|
1959
|
+
});
|
|
1954
1960
|
}
|
|
1955
1961
|
// Token válido: zera o lock OTP + limpa o token do DB.
|
|
1956
1962
|
const runtimeForOtp = await resolveRuntimeSettingsOrNoop(ctx);
|