@adonis-agora/authkit-server 0.72.0 → 0.74.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 (49) hide show
  1. package/README.md +4 -0
  2. package/build/commands/clients_create.d.ts +2 -0
  3. package/build/commands/clients_create.js +41 -13
  4. package/build/commands/commands.json +7 -0
  5. package/build/index.d.ts +6 -0
  6. package/build/index.js +7 -0
  7. package/build/src/commands/import_clients.js +2 -0
  8. package/build/src/host/access_token_verifier.d.ts +100 -0
  9. package/build/src/host/access_token_verifier.js +240 -0
  10. package/build/src/host/account_api/account_orgs_api_controller.js +6 -1
  11. package/build/src/host/active_org_cookie.d.ts +21 -1
  12. package/build/src/host/active_org_cookie.js +38 -0
  13. package/build/src/host/admin_api/admin_orgs_service.d.ts +7 -1
  14. package/build/src/host/admin_api/admin_orgs_service.js +11 -1
  15. package/build/src/host/admin_api/api_clients_controller.d.ts +4 -1
  16. package/build/src/host/admin_api/api_clients_controller.js +25 -2
  17. package/build/src/host/admin_api/api_orgs_controller.js +20 -20
  18. package/build/src/host/admin_api/dto.d.ts +1 -0
  19. package/build/src/host/admin_api/dto.js +1 -0
  20. package/build/src/host/admin_clients_service.d.ts +9 -0
  21. package/build/src/host/admin_clients_service.js +21 -0
  22. package/build/src/host/admin_console/console_clients_controller.d.ts +3 -1
  23. package/build/src/host/admin_console/console_clients_controller.js +25 -2
  24. package/build/src/host/admin_console/console_orgs_controller.js +1 -1
  25. package/build/src/host/admin_sessions_service.d.ts +21 -0
  26. package/build/src/host/admin_sessions_service.js +49 -0
  27. package/build/src/host/admin_validators.d.ts +11 -1
  28. package/build/src/host/admin_validators.js +36 -8
  29. package/build/src/host/bearer_account.d.ts +9 -0
  30. package/build/src/host/bearer_account.js +28 -0
  31. package/build/src/host/client_metadata.d.ts +51 -0
  32. package/build/src/host/client_metadata.js +98 -0
  33. package/build/src/host/console_session.d.ts +14 -3
  34. package/build/src/host/console_session.js +21 -9
  35. package/build/src/host/controllers/account_orgs_controller.js +3 -0
  36. package/build/src/host/controllers/interaction_controller.d.ts +18 -1
  37. package/build/src/host/controllers/interaction_controller.js +55 -14
  38. package/build/src/host/oidc_bearer_guard.d.ts +113 -0
  39. package/build/src/host/oidc_bearer_guard.js +223 -0
  40. package/build/src/host/oidc_rp_guard.d.ts +3 -1
  41. package/build/src/host/oidc_rp_guard.js +6 -2
  42. package/build/src/host/org_access_revocation.d.ts +20 -0
  43. package/build/src/host/org_access_revocation.js +41 -0
  44. package/build/src/host/ui-dist/assets/{index-Dct63ai-.js → index-CIqtoNcO.js} +1 -1
  45. package/build/src/host/ui-dist/index.html +1 -1
  46. package/build/src/provider/build_provider.js +42 -17
  47. package/build/src/provider/oidc_service.d.ts +8 -0
  48. package/build/src/provider/oidc_service.js +23 -4
  49. package/package.json +3 -3
@@ -158,3 +158,41 @@ export function readActiveOrgFromHostCtx(ctx, opts) {
158
158
  }
159
159
  return readActiveOrgFromKoaCtx(ctx, opts);
160
160
  }
161
+ /**
162
+ * Confere, NO STORE, se a org que a sessão diz ser a ativa ainda vale para a conta,
163
+ * e devolve os dados ATUAIS dela — ou `null` quando não vale mais.
164
+ *
165
+ * O `activeOrg` que chega aqui é um RETRATO: foi gravado no Grant no consent (ou lido
166
+ * do cookie) e o refresh token o reemite por até 30 dias. Sem esta conferência, quem
167
+ * é removido da equipe continua recebendo `org_*` até o grant expirar, e um rebaixado
168
+ * continua recebendo o papel antigo. Por isso:
169
+ *
170
+ * - a conta precisa ser membro da org AGORA (`getOrgMembership`);
171
+ * - a org precisa existir AGORA (`findOrgById`) — o slug sai daqui, não do retrato;
172
+ * - o papel é o da membership ATUAL, nunca o `orgRole` do retrato.
173
+ *
174
+ * Store sem `findOrgById`/`getOrgMembership`: não há como conferir, então não há
175
+ * claim (fail-closed). As rotas que gravam o cookie de org só existem com a capacidade.
176
+ *
177
+ * Erro do store PROPAGA: um banco fora do ar não pode virar, em silêncio, um token
178
+ * sem org (que o app leria como "usuário sem tenant") nem um token com a org velha.
179
+ */
180
+ export async function verifyActiveOrgMembership(store, accountId, claimed) {
181
+ if (!claimed)
182
+ return null;
183
+ // Sonda os DOIS métodos que esta função usa — não o `createOrg` do
184
+ // `supportsOrganizations`. Um store com a capacidade PARCIAL (sem um deles)
185
+ // derrubaria todo mint com TypeError; aqui ele só deixa de emitir a claim.
186
+ const orgs = store;
187
+ if (typeof orgs.findOrgById !== 'function' || typeof orgs.getOrgMembership !== 'function') {
188
+ return null;
189
+ }
190
+ // Org primeiro: apagada, nem pergunta pela membership.
191
+ const org = await orgs.findOrgById(claimed.orgId);
192
+ if (!org)
193
+ return null;
194
+ const membership = await orgs.getOrgMembership(claimed.orgId, accountId);
195
+ if (!membership || typeof membership.role !== 'string' || !membership.role)
196
+ return null;
197
+ return { orgId: org.id, orgSlug: org.slug, orgRole: membership.role };
198
+ }
@@ -1,6 +1,7 @@
1
1
  import type { HttpContext } from '@adonisjs/core/http';
2
2
  import type { OrgInvitation, OrgMember, OrgSummary } from '../../accounts/account_store.js';
3
3
  import type { ResolvedServerConfig } from '../../define_config.js';
4
+ import type { OidcService } from '../../provider/oidc_service.js';
4
5
  import type { SettingsCapability } from '../runtime_settings.js';
5
6
  import type { AdminActor } from './admin_users_service.js';
6
7
  export interface OrgWithMemberCount extends OrgSummary {
@@ -52,7 +53,12 @@ export type InvalidRoleResult = {
52
53
  */
53
54
  export declare class AdminOrgsService {
54
55
  private cfg;
55
- constructor(cfg: ResolvedServerConfig);
56
+ private oidc?;
57
+ /**
58
+ * @param oidc Serviço OIDC — onde vivem os grants a revogar quando um membro sai
59
+ * ou a org é apagada ({@link revokeOrgAccess}). Opcional para quem só lê.
60
+ */
61
+ constructor(cfg: ResolvedServerConfig, oidc?: Pick<OidcService, 'config'> | undefined);
56
62
  get supported(): boolean;
57
63
  /**
58
64
  * Resolve o catálogo efetivo de roles de org (runtime settings → config →
@@ -2,6 +2,7 @@ import { supportsOrganizations } from '../../accounts/account_store.js';
2
2
  import { ADMIN_LIST_DEFAULT_SIZE, LIST_FIRST_PAGE } from '../../pagination.js';
3
3
  import { accountPath } from '../account_paths.js';
4
4
  import { sendOrgInvitationEmail } from '../default_mailer.js';
5
+ import { revokeOrgAccess } from '../org_access_revocation.js';
5
6
  import { isRoleInCatalog, resolveEffectiveOrganizationsPolicy, resolveRoleCatalogList, } from '../runtime_toggles.js';
6
7
  /**
7
8
  * Lógica de gestão de organizações compartilhada entre o console admin (HTML)
@@ -10,8 +11,14 @@ import { isRoleInCatalog, resolveEffectiveOrganizationsPolicy, resolveRoleCatalo
10
11
  */
11
12
  export class AdminOrgsService {
12
13
  cfg;
13
- constructor(cfg) {
14
+ oidc;
15
+ /**
16
+ * @param oidc Serviço OIDC — onde vivem os grants a revogar quando um membro sai
17
+ * ou a org é apagada ({@link revokeOrgAccess}). Opcional para quem só lê.
18
+ */
19
+ constructor(cfg, oidc) {
14
20
  this.cfg = cfg;
21
+ this.oidc = oidc;
15
22
  }
16
23
  get supported() {
17
24
  return supportsOrganizations(this.cfg.accountStore);
@@ -157,6 +164,8 @@ export class AdminOrgsService {
157
164
  if (!existing)
158
165
  return { ok: false, reason: 'not_found' };
159
166
  await store.deleteOrg(orgId);
167
+ // Grants que carregam esta org (de qualquer conta) caem já — não no refresh.
168
+ await revokeOrgAccess(this.oidc, orgId);
160
169
  await this.cfg.audit?.record({
161
170
  type: 'organization.deleted',
162
171
  actorId: actor.actorId,
@@ -210,6 +219,7 @@ export class AdminOrgsService {
210
219
  return { ok: false, reason: 'last_owner' };
211
220
  return { ok: false, reason: 'member_not_found' };
212
221
  }
222
+ await revokeOrgAccess(this.oidc, orgId, accountId);
213
223
  await this.cfg.audit?.record({
214
224
  type: 'organization.member_removed',
215
225
  actorId: actor.actorId,
@@ -14,6 +14,7 @@ export default class ApiClientsController {
14
14
  redirectUris: string[];
15
15
  postLogoutRedirectUris: string[];
16
16
  tokenEndpointAuthMethod: string;
17
+ applicationType: import("../client_metadata.js").ApplicationType;
17
18
  backchannelLogoutUri: string | null;
18
19
  backchannelLogoutSessionRequired: boolean;
19
20
  }[];
@@ -26,10 +27,11 @@ export default class ApiClientsController {
26
27
  redirectUris: string[];
27
28
  postLogoutRedirectUris: string[];
28
29
  tokenEndpointAuthMethod: string;
30
+ applicationType: import("../client_metadata.js").ApplicationType;
29
31
  backchannelLogoutUri: string | null;
30
32
  backchannelLogoutSessionRequired: boolean;
31
33
  }>;
32
- store(ctx: HttpContext): Promise<{
34
+ store(ctx: HttpContext): Promise<void | {
33
35
  clientId: string;
34
36
  clientSecret: string | null;
35
37
  }>;
@@ -40,6 +42,7 @@ export default class ApiClientsController {
40
42
  redirectUris: string[];
41
43
  postLogoutRedirectUris: string[];
42
44
  tokenEndpointAuthMethod: string;
45
+ applicationType: import("../client_metadata.js").ApplicationType;
43
46
  backchannelLogoutUri: string | null;
44
47
  backchannelLogoutSessionRequired: boolean;
45
48
  }>;
@@ -1,12 +1,20 @@
1
1
  import '../augmentations.js';
2
2
  import { AdminClientsService } from '../admin_clients_service.js';
3
3
  import { clientCreateInput, clientInputValidator, clientPartialInput, } from '../admin_validators.js';
4
+ import { ClientMetadataError } from '../client_metadata.js';
4
5
  import { apiError, clientDto, createdClientDto } from './dto.js';
5
6
  /** Resolve o serviço (== OidcService) + o AdminClientsService. */
6
7
  async function clientsService(ctx) {
7
8
  const service = await ctx.containerResolver.make('authkit.server');
8
9
  return { service, svc: new AdminClientsService(service) };
9
10
  }
11
+ /**
12
+ * Metadata incoerente com o tipo de client (ex.: nativo com secret, redirect
13
+ * `myapp://` num client web) — 422, igual a um input que falha no Vine.
14
+ */
15
+ function invalidMetadata(ctx, err) {
16
+ return ctx.response.unprocessableEntity(apiError('invalid_client_metadata', err.message));
17
+ }
10
18
  /**
11
19
  * Recurso de clients OIDC da Admin REST API (R6). Reaproveita o
12
20
  * {@link AdminClientsService} (mesmo que o console B6). O secret é retornado UMA vez
@@ -31,7 +39,15 @@ export default class ApiClientsController {
31
39
  async store(ctx) {
32
40
  const { service, svc } = await clientsService(ctx);
33
41
  const input = clientCreateInput(await ctx.request.validateUsing(clientInputValidator));
34
- const created = await svc.create(input);
42
+ let created;
43
+ try {
44
+ created = await svc.create(input);
45
+ }
46
+ catch (err) {
47
+ if (err instanceof ClientMetadataError)
48
+ return invalidMetadata(ctx, err);
49
+ throw err;
50
+ }
35
51
  await service.config.audit?.record({
36
52
  type: 'client.created',
37
53
  clientId: created.clientId,
@@ -47,7 +63,14 @@ export default class ApiClientsController {
47
63
  const existing = await svc.find(id);
48
64
  if (!existing)
49
65
  return ctx.response.notFound(apiError('not_found', 'Client não encontrado.'));
50
- await svc.update(id, clientPartialInput(await ctx.request.validateUsing(clientInputValidator)));
66
+ try {
67
+ await svc.update(id, clientPartialInput(await ctx.request.validateUsing(clientInputValidator)));
68
+ }
69
+ catch (err) {
70
+ if (err instanceof ClientMetadataError)
71
+ return invalidMetadata(ctx, err);
72
+ throw err;
73
+ }
51
74
  await service.config.audit?.record({
52
75
  type: 'client.updated',
53
76
  clientId: id,
@@ -22,8 +22,8 @@ function notSupported(ctx) {
22
22
  export default class ApiOrgsController {
23
23
  /** GET /organizations — lista todas as orgs com contagem de membros. */
24
24
  async index(ctx) {
25
- const { cfg } = await ctxBits(ctx);
26
- const svc = new AdminOrgsService(cfg);
25
+ const { cfg, service } = await ctxBits(ctx);
26
+ const svc = new AdminOrgsService(cfg, service);
27
27
  const result = await svc.listOrgs();
28
28
  if (!Array.isArray(result)) {
29
29
  if (result.reason === 'not_supported')
@@ -36,8 +36,8 @@ export default class ApiOrgsController {
36
36
  }
37
37
  /** POST /organizations — cria uma org. Body: { name, slug, ownerAccountId, logoUrl? } */
38
38
  async store(ctx) {
39
- const { cfg, actor } = await ctxBits(ctx);
40
- const svc = new AdminOrgsService(cfg);
39
+ const { cfg, actor, service } = await ctxBits(ctx);
40
+ const svc = new AdminOrgsService(cfg, service);
41
41
  const { name, slug, ownerAccountId, logoUrl } = await ctx.request.validateUsing(orgCreateValidator);
42
42
  const result = await svc.createOrg({ name, slug, logoUrl: logoUrl ?? null, ownerAccountId }, actor);
43
43
  if ('ok' in result && result.ok === false) {
@@ -50,8 +50,8 @@ export default class ApiOrgsController {
50
50
  }
51
51
  /** GET /organizations/:id — obtém uma org com membros e convites. */
52
52
  async show(ctx) {
53
- const { cfg } = await ctxBits(ctx);
54
- const svc = new AdminOrgsService(cfg);
53
+ const { cfg, service } = await ctxBits(ctx);
54
+ const svc = new AdminOrgsService(cfg, service);
55
55
  const id = ctx.request.param('id');
56
56
  const result = await svc.getOrg(id);
57
57
  if ('ok' in result && result.ok === false) {
@@ -63,8 +63,8 @@ export default class ApiOrgsController {
63
63
  }
64
64
  /** PATCH /organizations/:id — atualiza nome e/ou logo. */
65
65
  async update(ctx) {
66
- const { cfg, actor } = await ctxBits(ctx);
67
- const svc = new AdminOrgsService(cfg);
66
+ const { cfg, actor, service } = await ctxBits(ctx);
67
+ const svc = new AdminOrgsService(cfg, service);
68
68
  const id = ctx.request.param('id');
69
69
  const { name, logoUrl } = await ctx.request.validateUsing(orgUpdateValidator);
70
70
  const patch = {};
@@ -84,8 +84,8 @@ export default class ApiOrgsController {
84
84
  }
85
85
  /** DELETE /organizations/:id */
86
86
  async destroy(ctx) {
87
- const { cfg, actor } = await ctxBits(ctx);
88
- const svc = new AdminOrgsService(cfg);
87
+ const { cfg, actor, service } = await ctxBits(ctx);
88
+ const svc = new AdminOrgsService(cfg, service);
89
89
  const id = ctx.request.param('id');
90
90
  const result = await svc.deleteOrg(id, actor);
91
91
  if (!result.ok) {
@@ -97,8 +97,8 @@ export default class ApiOrgsController {
97
97
  }
98
98
  /** POST /organizations/:id/members — adiciona membro. Body: { accountId, role } */
99
99
  async addMember(ctx) {
100
- const { cfg, actor } = await ctxBits(ctx);
101
- const svc = new AdminOrgsService(cfg);
100
+ const { cfg, actor, service } = await ctxBits(ctx);
101
+ const svc = new AdminOrgsService(cfg, service);
102
102
  const orgId = ctx.request.param('id');
103
103
  const { accountId, role } = await ctx.request.validateUsing(orgAddMemberValidator);
104
104
  const result = await svc.addMember(orgId, { accountId, role: role ?? 'member' }, actor, await resolveRuntimeSettings(ctx));
@@ -116,8 +116,8 @@ export default class ApiOrgsController {
116
116
  }
117
117
  /** DELETE /organizations/:id/members/:accountId */
118
118
  async removeMember(ctx) {
119
- const { cfg, actor } = await ctxBits(ctx);
120
- const svc = new AdminOrgsService(cfg);
119
+ const { cfg, actor, service } = await ctxBits(ctx);
120
+ const svc = new AdminOrgsService(cfg, service);
121
121
  const orgId = ctx.request.param('id');
122
122
  const accountId = ctx.request.param('accountId');
123
123
  const result = await svc.removeMember(orgId, accountId, actor);
@@ -134,8 +134,8 @@ export default class ApiOrgsController {
134
134
  }
135
135
  /** PATCH /organizations/:id/members/:accountId — troca o papel. Body: { role } */
136
136
  async updateMemberRole(ctx) {
137
- const { cfg, actor } = await ctxBits(ctx);
138
- const svc = new AdminOrgsService(cfg);
137
+ const { cfg, actor, service } = await ctxBits(ctx);
138
+ const svc = new AdminOrgsService(cfg, service);
139
139
  const orgId = ctx.request.param('id');
140
140
  const accountId = ctx.request.param('accountId');
141
141
  const { role } = await ctx.request.validateUsing(orgMemberRoleValidator);
@@ -155,8 +155,8 @@ export default class ApiOrgsController {
155
155
  }
156
156
  /** POST /organizations/:id/invitations — cria convite. Body: { email, role } */
157
157
  async createInvitation(ctx) {
158
- const { cfg, actor } = await ctxBits(ctx);
159
- const svc = new AdminOrgsService(cfg);
158
+ const { cfg, actor, service } = await ctxBits(ctx);
159
+ const svc = new AdminOrgsService(cfg, service);
160
160
  const orgId = ctx.request.param('id');
161
161
  const { email, role } = await ctx.request.validateUsing(orgInvitationValidator);
162
162
  const origin = authkitOrigin(cfg);
@@ -173,8 +173,8 @@ export default class ApiOrgsController {
173
173
  }
174
174
  /** DELETE /organizations/:id/invitations/:invitationId */
175
175
  async revokeInvitation(ctx) {
176
- const { cfg, actor } = await ctxBits(ctx);
177
- const svc = new AdminOrgsService(cfg);
176
+ const { cfg, actor, service } = await ctxBits(ctx);
177
+ const svc = new AdminOrgsService(cfg, service);
178
178
  const orgId = ctx.request.param('id');
179
179
  const invitationId = ctx.request.param('invitationId');
180
180
  const result = await svc.revokeInvitation(orgId, invitationId, actor);
@@ -19,6 +19,7 @@ export declare function clientDto(client: AdminClient): {
19
19
  redirectUris: string[];
20
20
  postLogoutRedirectUris: string[];
21
21
  tokenEndpointAuthMethod: string;
22
+ applicationType: import("../client_metadata.ts").ApplicationType;
22
23
  backchannelLogoutUri: string | null;
23
24
  backchannelLogoutSessionRequired: boolean;
24
25
  };
@@ -17,6 +17,7 @@ export function clientDto(client) {
17
17
  redirectUris: client.redirectUris,
18
18
  postLogoutRedirectUris: client.postLogoutRedirectUris,
19
19
  tokenEndpointAuthMethod: client.tokenEndpointAuthMethod,
20
+ applicationType: client.applicationType,
20
21
  backchannelLogoutUri: client.backchannelLogoutUri ?? null,
21
22
  backchannelLogoutSessionRequired: client.backchannelLogoutSessionRequired ?? false,
22
23
  };
@@ -1,4 +1,5 @@
1
1
  import type { OidcService } from '../provider/oidc_service.js';
2
+ import { type ApplicationType } from './client_metadata.js';
2
3
  /** Métodos de autenticação no token endpoint suportados pelo formulário admin. */
3
4
  export type TokenEndpointAuthMethod = 'client_secret_basic' | 'client_secret_post' | 'none';
4
5
  /** Entrada normalizada de um client gerenciável (vinda do formulário admin). */
@@ -8,6 +9,12 @@ export interface ClientInput {
8
9
  postLogoutRedirectUris: string[];
9
10
  grantTypes: string[];
10
11
  tokenEndpointAuthMethod: TokenEndpointAuthMethod;
12
+ /**
13
+ * `application_type` do client. Default `'web'`. `'native'` (RFC 8252) aceita
14
+ * redirect de esquema privado / https claimed / loopback e exige client público
15
+ * (`tokenEndpointAuthMethod: 'none'`). Ver `client_metadata.ts`.
16
+ */
17
+ applicationType?: ApplicationType;
11
18
  /**
12
19
  * Endpoint de OIDC Back-Channel Logout do RP. Quando definido, o IdP envia um
13
20
  * `logout_token` para esta URI ao encerrar a sessão/grant do usuário (RFC 7644).
@@ -27,6 +34,8 @@ export interface AdminClient {
27
34
  redirectUris: string[];
28
35
  postLogoutRedirectUris: string[];
29
36
  tokenEndpointAuthMethod: string;
37
+ /** `application_type` (payload sem a chave = `'web'`, o default do oidc-provider). */
38
+ applicationType: ApplicationType;
30
39
  /** Endpoint de OIDC Back-Channel Logout (opcional). */
31
40
  backchannelLogoutUri?: string;
32
41
  /** Exige sid no logout_token (opcional). */
@@ -1,4 +1,5 @@
1
1
  import { randomBytes } from 'node:crypto';
2
+ import { assertClientMetadata } from './client_metadata.js';
2
3
  /** Métodos públicos (sem segredo) — espelham os "non-secret" do oidc-provider. */
3
4
  const PUBLIC_AUTH_METHODS = new Set(['none']);
4
5
  /** Gera um identificador opaco no estilo do oidc-provider (~43 chars base64url). */
@@ -96,6 +97,7 @@ export class AdminClientsService {
96
97
  tokenEndpointAuthMethod: input.tokenEndpointAuthMethod ??
97
98
  existing.token_endpoint_auth_method ??
98
99
  'client_secret_basic',
100
+ applicationType: input.applicationType ?? existing.application_type ?? 'web',
99
101
  backchannelLogoutUri: input.backchannelLogoutUri ?? existing.backchannel_logout_uri,
100
102
  backchannelLogoutSessionRequired: input.backchannelLogoutSessionRequired ??
101
103
  existing.backchannel_logout_session_required,
@@ -131,6 +133,19 @@ export class AdminClientsService {
131
133
  await this.#adapter.destroy(clientId);
132
134
  await this.oidc.evictDynamicClientCache();
133
135
  }
136
+ /**
137
+ * Valida o input contra as regras do tipo de aplicação ANTES de gravar (ver
138
+ * `client_metadata.ts`). Lança `ClientMetadataError`.
139
+ */
140
+ #assertValid(input) {
141
+ assertClientMetadata({
142
+ applicationType: input.applicationType ?? 'web',
143
+ tokenEndpointAuthMethod: input.tokenEndpointAuthMethod,
144
+ grantTypes: input.grantTypes,
145
+ redirectUris: input.redirectUris,
146
+ postLogoutRedirectUris: input.postLogoutRedirectUris,
147
+ });
148
+ }
134
149
  /**
135
150
  * Monta o payload na forma snake_case que o oidc-provider espera/persiste —
136
151
  * verificada contra o que o registro dinâmico (RFC 7591) grava. As chaves de
@@ -138,6 +153,7 @@ export class AdminClientsService {
138
153
  * preenchidas pelo Schema do provider ao construir o Client em `find`.
139
154
  */
140
155
  #buildPayload(clientId, input, clientSecret) {
156
+ this.#assertValid(input);
141
157
  const grantTypes = input.grantTypes.length
142
158
  ? input.grantTypes
143
159
  : ['authorization_code', 'refresh_token'];
@@ -153,6 +169,10 @@ export class AdminClientsService {
153
169
  };
154
170
  if (clientSecret)
155
171
  payload.client_secret = clientSecret;
172
+ // Só grava a chave quando nativo: um client web continua com o payload de
173
+ // sempre (sem `application_type` o oidc-provider assume `web`).
174
+ if (input.applicationType === 'native')
175
+ payload.application_type = 'native';
156
176
  if (input.backchannelLogoutUri) {
157
177
  payload.backchannel_logout_uri = input.backchannelLogoutUri;
158
178
  if (input.backchannelLogoutSessionRequired !== undefined) {
@@ -172,6 +192,7 @@ export class AdminClientsService {
172
192
  redirectUris: p.redirect_uris ?? [],
173
193
  postLogoutRedirectUris: p.post_logout_redirect_uris ?? [],
174
194
  tokenEndpointAuthMethod: authMethod,
195
+ applicationType: p.application_type === 'native' ? 'native' : 'web',
175
196
  };
176
197
  if (p.backchannel_logout_uri) {
177
198
  result.backchannelLogoutUri = p.backchannel_logout_uri;
@@ -19,13 +19,14 @@ export default class ConsoleClientsController {
19
19
  redirectUris: string[];
20
20
  postLogoutRedirectUris: string[];
21
21
  tokenEndpointAuthMethod: string;
22
+ applicationType: import("../client_metadata.js").ApplicationType;
22
23
  backchannelLogoutUri: string | null;
23
24
  backchannelLogoutSessionRequired: boolean;
24
25
  }[];
25
26
  canList: boolean;
26
27
  }>;
27
28
  /** POST {prefix}/api/clients */
28
- store(ctx: HttpContext): Promise<{
29
+ store(ctx: HttpContext): Promise<void | {
29
30
  clientId: string;
30
31
  clientSecret: string | null;
31
32
  }>;
@@ -37,6 +38,7 @@ export default class ConsoleClientsController {
37
38
  redirectUris: string[];
38
39
  postLogoutRedirectUris: string[];
39
40
  tokenEndpointAuthMethod: string;
41
+ applicationType: import("../client_metadata.js").ApplicationType;
40
42
  backchannelLogoutUri: string | null;
41
43
  backchannelLogoutSessionRequired: boolean;
42
44
  }>;
@@ -2,6 +2,14 @@ import '../augmentations.js';
2
2
  import { apiError, clientDto, createdClientDto } from '../admin_api/dto.js';
3
3
  import { AdminClientsService } from '../admin_clients_service.js';
4
4
  import { clientCreateInput, clientInputValidator, clientPartialInput, } from '../admin_validators.js';
5
+ import { ClientMetadataError } from '../client_metadata.js';
6
+ /**
7
+ * Metadata incoerente com o tipo de client (ex.: nativo com secret, redirect
8
+ * `myapp://` num client web) — 422, igual a um input que falha no Vine.
9
+ */
10
+ function invalidMetadata(ctx, err) {
11
+ return ctx.response.unprocessableEntity(apiError('invalid_client_metadata', err.message));
12
+ }
5
13
  /**
6
14
  * Endpoints JSON de clients OIDC do console admin React.
7
15
  *
@@ -28,7 +36,15 @@ export default class ConsoleClientsController {
28
36
  const cfg = service.config;
29
37
  const svc = new AdminClientsService(service);
30
38
  const input = clientCreateInput(await ctx.request.validateUsing(clientInputValidator));
31
- const created = await svc.create(input);
39
+ let created;
40
+ try {
41
+ created = await svc.create(input);
42
+ }
43
+ catch (err) {
44
+ if (err instanceof ClientMetadataError)
45
+ return invalidMetadata(ctx, err);
46
+ throw err;
47
+ }
32
48
  await cfg.audit?.record({
33
49
  type: 'client.created',
34
50
  clientId: created.clientId,
@@ -47,7 +63,14 @@ export default class ConsoleClientsController {
47
63
  const existing = await svc.find(id);
48
64
  if (!existing)
49
65
  return ctx.response.notFound(apiError('not_found', 'Client não encontrado.'));
50
- await svc.update(id, clientPartialInput(await ctx.request.validateUsing(clientInputValidator)));
66
+ try {
67
+ await svc.update(id, clientPartialInput(await ctx.request.validateUsing(clientInputValidator)));
68
+ }
69
+ catch (err) {
70
+ if (err instanceof ClientMetadataError)
71
+ return invalidMetadata(ctx, err);
72
+ throw err;
73
+ }
51
74
  await cfg.audit?.record({
52
75
  type: 'client.updated',
53
76
  clientId: id,
@@ -23,7 +23,7 @@ import { resolveRuntimeSettings } from '../runtime_settings.js';
23
23
  export default class ConsoleOrgsController {
24
24
  async svc(ctx) {
25
25
  const service = await ctx.containerResolver.make('authkit.server');
26
- return new AdminOrgsService(service.config);
26
+ return new AdminOrgsService(service.config, service);
27
27
  }
28
28
  actor(ctx) {
29
29
  return {
@@ -118,4 +118,25 @@ export declare class AdminSessionsService {
118
118
  * contagens do que foi removido.
119
119
  */
120
120
  revokeClientGrants(accountId: string, clientId: string): Promise<RevokeResult>;
121
+ /**
122
+ * Revoga os grants que carregam a org `orgId` como org ativa (`Grant.activeOrg`,
123
+ * gravado no consent) — de UMA conta (`accountId`) ou de TODAS (omitido, usado
124
+ * quando a org é apagada). Destrói os AT/RT desses grants e os próprios grants,
125
+ * como {@link revokeClientGrants}; depois grava a revogação por `sub` de cada conta
126
+ * afetada, para que clients COOKIE-BASED derrubem a sessão já na próxima request.
127
+ *
128
+ * Por que existe: o `org_*` do token sai do grant. Quem é removido da org (ou
129
+ * perde a org inteira) seguiria com o access token e o refresh token daquele grant
130
+ * até o fim do TTL. A emissão já confere a membership no store a cada mint (ver
131
+ * `verifyActiveOrgMembership`); esta revogação faz a mudança valer AGORA — o
132
+ * access token opaco para de introspectar, o refresh falha e o client volta ao
133
+ * authorize, onde o novo grant sai sem a org.
134
+ *
135
+ * Grants de OUTRAS orgs (e sem org) ficam intactos. Sessões do IdP não carregam
136
+ * org (a org vive no grant e no cookie), então não são tocadas.
137
+ *
138
+ * Sem enumeração no adapter não há como achar os grants: devolve zeros e fica
139
+ * valendo só a conferência da emissão (no próximo refresh).
140
+ */
141
+ revokeOrgGrants(orgId: string, accountId?: string): Promise<RevokeResult>;
121
142
  }
@@ -1,5 +1,6 @@
1
1
  import { getBootedApp } from '../../services/booted_app.js';
2
2
  import { pickModelAdapterClass } from '../adapters/factory.js';
3
+ import { normalizeActiveOrg } from './active_org_cookie.js';
3
4
  /**
4
5
  * Serviço de inspeção/revogação das SESSÕES e GRANTS ativos de uma conta.
5
6
  * Lê/escreve via `pickModelAdapterClass` (a MESMA regra do provider): com
@@ -302,6 +303,54 @@ export class AdminSessionsService {
302
303
  }
303
304
  return { sessions: 0, grants: grants.length, accessTokens, refreshTokens };
304
305
  }
306
+ /**
307
+ * Revoga os grants que carregam a org `orgId` como org ativa (`Grant.activeOrg`,
308
+ * gravado no consent) — de UMA conta (`accountId`) ou de TODAS (omitido, usado
309
+ * quando a org é apagada). Destrói os AT/RT desses grants e os próprios grants,
310
+ * como {@link revokeClientGrants}; depois grava a revogação por `sub` de cada conta
311
+ * afetada, para que clients COOKIE-BASED derrubem a sessão já na próxima request.
312
+ *
313
+ * Por que existe: o `org_*` do token sai do grant. Quem é removido da org (ou
314
+ * perde a org inteira) seguiria com o access token e o refresh token daquele grant
315
+ * até o fim do TTL. A emissão já confere a membership no store a cada mint (ver
316
+ * `verifyActiveOrgMembership`); esta revogação faz a mudança valer AGORA — o
317
+ * access token opaco para de introspectar, o refresh falha e o client volta ao
318
+ * authorize, onde o novo grant sai sem a org.
319
+ *
320
+ * Grants de OUTRAS orgs (e sem org) ficam intactos. Sessões do IdP não carregam
321
+ * org (a org vive no grant e no cookie), então não são tocadas.
322
+ *
323
+ * Sem enumeração no adapter não há como achar os grants: devolve zeros e fica
324
+ * valendo só a conferência da emissão (no próximo refresh).
325
+ */
326
+ async revokeOrgGrants(orgId, accountId) {
327
+ const grantAdapter = this.#adapter('Grant');
328
+ const rows = await this.#listModel('Grant');
329
+ const grants = rows.filter((r) => {
330
+ if (normalizeActiveOrg(r.payload.activeOrg)?.orgId !== orgId)
331
+ return false;
332
+ return accountId === undefined || r.payload.accountId === accountId;
333
+ });
334
+ if (grants.length === 0) {
335
+ return { sessions: 0, grants: 0, accessTokens: 0, refreshTokens: 0 };
336
+ }
337
+ const grantIds = new Set(grants.map((g) => g.id));
338
+ const accessTokens = await this.#destroyTokensOfGrants('AccessToken', grantIds);
339
+ const refreshTokens = await this.#destroyTokensOfGrants('RefreshToken', grantIds);
340
+ for (const g of grants) {
341
+ await grantAdapter.revokeByGrantId(g.id);
342
+ await grantAdapter.destroy(g.id);
343
+ }
344
+ // Clients cookie-based não têm refresh para falhar: sem a linha por `sub`, o
345
+ // id_token com a org velha seguiria valendo na sessão do client até expirar.
346
+ const accounts = new Set(grants
347
+ .map((g) => g.payload.accountId)
348
+ .filter((id) => typeof id === 'string' && id.length > 0));
349
+ for (const id of accounts) {
350
+ await this.recordSubRevocation(id);
351
+ }
352
+ return { sessions: 0, grants: grants.length, accessTokens, refreshTokens };
353
+ }
305
354
  /** Destrói (quando enumerável) os artefatos de um model token cujos grantId estão em `grantIds`. */
306
355
  async #destroyTokensOfGrants(model, grantIds) {
307
356
  const adapter = this.#adapter(model);
@@ -1,4 +1,5 @@
1
1
  import type { ClientInput, TokenEndpointAuthMethod } from './admin_clients_service.js';
2
+ import { type ApplicationType } from './client_metadata.js';
2
3
  /**
3
4
  * Input de client OIDC — serve create E update (PATCH). Tudo opcional: no create
4
5
  * o controller aplica defaults (ver {@link clientCreateInput}); no update os campos
@@ -8,6 +9,7 @@ import type { ClientInput, TokenEndpointAuthMethod } from './admin_clients_servi
8
9
  */
9
10
  export declare const clientInputValidator: import("@vinejs/vine").VineValidator<import("@vinejs/vine").VineObject<{
10
11
  clientId: import("@vinejs/vine/schema/base/literal").OptionalModifier<import("@vinejs/vine").VineString>;
12
+ applicationType: import("@vinejs/vine/schema/base/literal").OptionalModifier<import("@vinejs/vine").VineEnum<readonly ["web", "native"]>>;
11
13
  redirectUris: import("@vinejs/vine").OptionalModifier<import("@vinejs/vine").VineArray<import("@vinejs/vine").VineString>>;
12
14
  postLogoutRedirectUris: import("@vinejs/vine").OptionalModifier<import("@vinejs/vine").VineArray<import("@vinejs/vine").VineString>>;
13
15
  grantTypes: import("@vinejs/vine").OptionalModifier<import("@vinejs/vine").VineArray<import("@vinejs/vine").VineEnum<readonly ["authorization_code", "refresh_token", "client_credentials", "urn:ietf:params:oauth:grant-type:token-exchange"]>>>;
@@ -16,6 +18,7 @@ export declare const clientInputValidator: import("@vinejs/vine").VineValidator<
16
18
  backchannelLogoutUri: import("@vinejs/vine/schema/base/literal").OptionalModifier<import("@vinejs/vine").VineString>;
17
19
  backchannelLogoutSessionRequired: import("@vinejs/vine/schema/base/literal").OptionalModifier<import("@vinejs/vine").VineBoolean>;
18
20
  }, {
21
+ applicationType?: "native" | "web" | null | undefined;
19
22
  backchannelLogoutSessionRequired?: string | number | boolean | null | undefined;
20
23
  backchannelLogoutUri?: string | null | undefined;
21
24
  clientId?: string | null | undefined;
@@ -25,6 +28,7 @@ export declare const clientInputValidator: import("@vinejs/vine").VineValidator<
25
28
  redirectUris?: string[] | null | undefined;
26
29
  tokenEndpointAuthMethod?: "client_secret_basic" | "client_secret_post" | "none" | null | undefined;
27
30
  }, {
31
+ applicationType?: "native" | "web" | undefined;
28
32
  backchannelLogoutSessionRequired?: boolean | undefined;
29
33
  backchannelLogoutUri?: string | undefined;
30
34
  clientId?: string | undefined;
@@ -34,6 +38,7 @@ export declare const clientInputValidator: import("@vinejs/vine").VineValidator<
34
38
  redirectUris?: string[] | undefined;
35
39
  tokenEndpointAuthMethod?: "client_secret_basic" | "client_secret_post" | "none" | undefined;
36
40
  }, {
41
+ applicationType?: "native" | "web" | undefined;
37
42
  backchannelLogoutSessionRequired?: boolean | undefined;
38
43
  backchannelLogoutUri?: string | undefined;
39
44
  clientId?: string | undefined;
@@ -46,6 +51,7 @@ export declare const clientInputValidator: import("@vinejs/vine").VineValidator<
46
51
  /** Forma validada de um client (saída do {@link clientInputValidator}). */
47
52
  export type ClientInputFields = {
48
53
  clientId?: string;
54
+ applicationType?: ApplicationType;
49
55
  redirectUris?: string[];
50
56
  postLogoutRedirectUris?: string[];
51
57
  grantTypes?: string[];
@@ -54,7 +60,11 @@ export type ClientInputFields = {
54
60
  backchannelLogoutUri?: string;
55
61
  backchannelLogoutSessionRequired?: boolean;
56
62
  };
57
- /** Mapeia o input validado para um {@link ClientInput} COMPLETO (create), com defaults. */
63
+ /**
64
+ * Mapeia o input validado para um {@link ClientInput} COMPLETO (create), com defaults.
65
+ * Client nativo sem `tokenEndpointAuthMethod` explícito vira público (`none`); um
66
+ * nativo confidencial explícito é recusado pelo service (422).
67
+ */
58
68
  export declare function clientCreateInput(v: ClientInputFields): ClientInput;
59
69
  /**
60
70
  * Mapeia o input validado para um {@link ClientInput} PARCIAL (PATCH): só inclui