@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.
- package/README.md +4 -0
- package/build/commands/clients_create.d.ts +2 -0
- package/build/commands/clients_create.js +41 -13
- package/build/commands/commands.json +7 -0
- package/build/index.d.ts +6 -0
- package/build/index.js +7 -0
- package/build/src/commands/import_clients.js +2 -0
- package/build/src/host/access_token_verifier.d.ts +100 -0
- package/build/src/host/access_token_verifier.js +240 -0
- package/build/src/host/account_api/account_orgs_api_controller.js +6 -1
- package/build/src/host/active_org_cookie.d.ts +21 -1
- package/build/src/host/active_org_cookie.js +38 -0
- package/build/src/host/admin_api/admin_orgs_service.d.ts +7 -1
- package/build/src/host/admin_api/admin_orgs_service.js +11 -1
- package/build/src/host/admin_api/api_clients_controller.d.ts +4 -1
- package/build/src/host/admin_api/api_clients_controller.js +25 -2
- package/build/src/host/admin_api/api_orgs_controller.js +20 -20
- package/build/src/host/admin_api/dto.d.ts +1 -0
- package/build/src/host/admin_api/dto.js +1 -0
- package/build/src/host/admin_clients_service.d.ts +9 -0
- package/build/src/host/admin_clients_service.js +21 -0
- package/build/src/host/admin_console/console_clients_controller.d.ts +3 -1
- package/build/src/host/admin_console/console_clients_controller.js +25 -2
- package/build/src/host/admin_console/console_orgs_controller.js +1 -1
- package/build/src/host/admin_sessions_service.d.ts +21 -0
- package/build/src/host/admin_sessions_service.js +49 -0
- package/build/src/host/admin_validators.d.ts +11 -1
- package/build/src/host/admin_validators.js +36 -8
- package/build/src/host/bearer_account.d.ts +9 -0
- package/build/src/host/bearer_account.js +28 -0
- package/build/src/host/client_metadata.d.ts +51 -0
- package/build/src/host/client_metadata.js +98 -0
- package/build/src/host/console_session.d.ts +14 -3
- package/build/src/host/console_session.js +21 -9
- package/build/src/host/controllers/account_orgs_controller.js +3 -0
- package/build/src/host/controllers/interaction_controller.d.ts +18 -1
- package/build/src/host/controllers/interaction_controller.js +55 -14
- package/build/src/host/oidc_bearer_guard.d.ts +113 -0
- package/build/src/host/oidc_bearer_guard.js +223 -0
- package/build/src/host/oidc_rp_guard.d.ts +3 -1
- package/build/src/host/oidc_rp_guard.js +6 -2
- package/build/src/host/org_access_revocation.d.ts +20 -0
- package/build/src/host/org_access_revocation.js +41 -0
- package/build/src/host/ui-dist/assets/{index-Dct63ai-.js → index-CIqtoNcO.js} +1 -1
- package/build/src/host/ui-dist/index.html +1 -1
- package/build/src/provider/build_provider.js +42 -17
- package/build/src/provider/oidc_service.d.ts +8 -0
- package/build/src/provider/oidc_service.js +23 -4
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|