@adonis-agora/authkit-server 1.0.0-rc.2 → 1.0.0-rc.4
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 +19 -0
- package/build/commands/clients_create.d.ts +4 -0
- package/build/commands/clients_create.js +97 -0
- package/build/commands/commands.json +14 -0
- package/build/index.d.ts +3 -0
- package/build/index.js +1 -0
- package/build/src/audit/audit_sink.d.ts +1 -1
- package/build/src/audit/audit_sink.js +5 -0
- package/build/src/define_config.d.ts +16 -0
- package/build/src/define_config.js +2 -0
- package/build/src/host/access_token_verifier.js +7 -0
- package/build/src/host/admin_api/api_clients_controller.d.ts +37 -0
- package/build/src/host/admin_api/api_clients_controller.js +31 -1
- package/build/src/host/admin_api/dto.d.ts +3 -1
- package/build/src/host/admin_api/dto.js +2 -0
- package/build/src/host/admin_clients_service.d.ts +20 -0
- package/build/src/host/admin_clients_service.js +55 -0
- package/build/src/host/admin_console/console_clients_controller.d.ts +41 -0
- package/build/src/host/admin_console/console_clients_controller.js +34 -1
- package/build/src/host/admin_validators.d.ts +29 -0
- package/build/src/host/admin_validators.js +30 -0
- package/build/src/host/register_auth_host.js +4 -0
- package/build/src/host/ui-dist/assets/index-u0bMxkLg.js +67 -0
- package/build/src/host/ui-dist/index.html +1 -1
- package/build/src/provider/build_provider.js +37 -6
- package/build/src/service_tenants/client_metadata.d.ts +39 -0
- package/build/src/service_tenants/client_metadata.js +80 -0
- package/build/src/service_tenants/config.d.ts +44 -0
- package/build/src/service_tenants/config.js +44 -0
- package/build/src/service_tenants/provider_hooks.d.ts +52 -0
- package/build/src/service_tenants/provider_hooks.js +124 -0
- package/build/src/service_tenants/tenant_verifier.d.ts +73 -0
- package/build/src/service_tenants/tenant_verifier.js +104 -0
- package/package.json +3 -3
- package/build/src/host/ui-dist/assets/index-BqyJYRVX.js +0 -67
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @adonis-agora/authkit-server
|
|
2
2
|
|
|
3
|
+
## 1.0.0-rc.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- fix(deps): update dependency jose to v6.2.16 ([#454](https://github.com/DavideCarvalho/adonis-agora-authkit/issues/454))
|
|
8
|
+
|
|
9
|
+
## 1.0.0-rc.3
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- [#460](https://github.com/DavideCarvalho/adonis-agora-authkit/pull/460) [`1858ee8`](https://github.com/DavideCarvalho/adonis-agora-authkit/commit/1858ee837bb4edcf0578f86d224693ac9a86a495) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Service tenants: a client can act as one partition of another service, such as a durable control plane.
|
|
14
|
+
|
|
15
|
+
- `serviceTenants: { resources: { 'urn:agora:durable': { expiresIn: 600 } } }` in `config/authkit.ts` declares the resources (RFC 8707) that issue service-tenant tokens: JWT access tokens (RFC 9068) with `aud` = the resource and the client's `partition` and `capabilities` as claims.
|
|
16
|
+
- A service-tenant client is confidential, uses only `client_credentials` and has no redirect URIs. Create and edit it from the console (**Service tenant** kind), the Admin API and the SDKs (`serviceTenant: { partition, capabilities? }`), or `node ace authkit:clients:create --partition=acme --capability=reports`.
|
|
17
|
+
- Any client can now be disabled and enabled again (console, `POST /clients/:id/disable|enable`, SDK `clients.disable/enable`): the token endpoint answers `invalid_client` from the next request on.
|
|
18
|
+
- `authkitTenantVerifier()` (authkit-server) verifies those tokens against the local issuer and keys and returns `{ tenant, capabilities }`; `authkitTenantToken({ issuer, clientId, clientSecret, resource })` (authkit-client) fetches, caches and refreshes them on the tenant side. Both fit durable's `verifyTenant` and `tenant.token` as a function or a class, without depending on durable.
|
|
19
|
+
- New audit events: `client.disabled`, `client.enabled`, `service_tenant.token_issued`.
|
|
20
|
+
- Access-token verification (`oidcBearerGuard`, in-process and remote) now refuses a token whose `sub` is a client (a `client_credentials` token), so a service-tenant token never authenticates a user.
|
|
21
|
+
|
|
3
22
|
## 1.0.0-rc.2
|
|
4
23
|
|
|
5
24
|
### Major Changes
|
|
@@ -11,8 +11,10 @@ import type { CommandOptions } from '@adonisjs/core/types/ace';
|
|
|
11
11
|
* node ace authkit:clients:create --client-id=my-api --redirect-uri=https://api/cb --redirect-uri=https://api/cb2 --grant=client_credentials
|
|
12
12
|
* node ace authkit:clients:create --redirect-uri=https://app/cb --json
|
|
13
13
|
* node ace authkit:clients:create --client-id=my-mobile --native --redirect-uri=com.example.app:/oauth
|
|
14
|
+
* node ace authkit:clients:create --client-id=worker-acme --partition=acme --capability=reports
|
|
14
15
|
*/
|
|
15
16
|
export default class AuthkitClientsCreate extends BaseCommand {
|
|
17
|
+
#private;
|
|
16
18
|
static commandName: string;
|
|
17
19
|
static description: string;
|
|
18
20
|
static help: string[];
|
|
@@ -25,6 +27,8 @@ export default class AuthkitClientsCreate extends BaseCommand {
|
|
|
25
27
|
native?: boolean;
|
|
26
28
|
backchannelLogoutUri?: string;
|
|
27
29
|
clientSecret?: string;
|
|
30
|
+
partition?: string;
|
|
31
|
+
capability?: string[];
|
|
28
32
|
json?: boolean;
|
|
29
33
|
run(): Promise<void>;
|
|
30
34
|
}
|
|
@@ -16,6 +16,7 @@ import { BaseCommand, flags } from '@adonisjs/core/ace';
|
|
|
16
16
|
* node ace authkit:clients:create --client-id=my-api --redirect-uri=https://api/cb --redirect-uri=https://api/cb2 --grant=client_credentials
|
|
17
17
|
* node ace authkit:clients:create --redirect-uri=https://app/cb --json
|
|
18
18
|
* node ace authkit:clients:create --client-id=my-mobile --native --redirect-uri=com.example.app:/oauth
|
|
19
|
+
* node ace authkit:clients:create --client-id=worker-acme --partition=acme --capability=reports
|
|
19
20
|
*/
|
|
20
21
|
export default class AuthkitClientsCreate extends BaseCommand {
|
|
21
22
|
static commandName = 'authkit:clients:create';
|
|
@@ -41,9 +42,18 @@ export default class AuthkitClientsCreate extends BaseCommand {
|
|
|
41
42
|
' node ace authkit:clients:create --redirect-uri=https://app/cb --backchannel-logout-uri=https://app/bc',
|
|
42
43
|
' node ace authkit:clients:create --client-id=my-app --redirect-uri=https://a/cb --redirect-uri=https://b/cb --json',
|
|
43
44
|
' node ace authkit:clients:create --client-id=my-mobile --native --redirect-uri=com.example.app:/oauth',
|
|
45
|
+
'',
|
|
46
|
+
'Service tenant (a process that acts as one partition, e.g. a durable tenant pod):',
|
|
47
|
+
'use --partition (and --capability, repeatable). Implies a confidential client with',
|
|
48
|
+
'only the client_credentials grant and no redirect URIs. Needs serviceTenants.resources',
|
|
49
|
+
'in config/authkit.ts.',
|
|
50
|
+
' node ace authkit:clients:create --client-id=worker-acme --partition=acme --capability=reports',
|
|
44
51
|
];
|
|
45
52
|
static options = { startApp: true };
|
|
46
53
|
async run() {
|
|
54
|
+
if (this.partition !== undefined || this.capability?.length) {
|
|
55
|
+
return this.#createServiceTenant();
|
|
56
|
+
}
|
|
47
57
|
const redirectUris = this.redirectUri ?? [];
|
|
48
58
|
if (redirectUris.length === 0) {
|
|
49
59
|
this.logger.logError('❌ --redirect-uri é obrigatório. Passe ao menos uma URI de callback.');
|
|
@@ -120,6 +130,83 @@ export default class AuthkitClientsCreate extends BaseCommand {
|
|
|
120
130
|
this.logger.info('');
|
|
121
131
|
}
|
|
122
132
|
}
|
|
133
|
+
/** `--partition`: a service tenant (client_credentials only, `partition` claim). */
|
|
134
|
+
async #createServiceTenant() {
|
|
135
|
+
if (!this.partition) {
|
|
136
|
+
this.logger.logError('❌ --capability needs --partition.');
|
|
137
|
+
this.exitCode = 1;
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const conflicting = [
|
|
141
|
+
this.redirectUri?.length && '--redirect-uri',
|
|
142
|
+
this.postLogoutUri?.length && '--post-logout-uri',
|
|
143
|
+
this.public && '--public',
|
|
144
|
+
this.native && '--native',
|
|
145
|
+
this.backchannelLogoutUri && '--backchannel-logout-uri',
|
|
146
|
+
this.grant?.some((g) => g !== 'client_credentials') && '--grant',
|
|
147
|
+
].filter(Boolean);
|
|
148
|
+
if (conflicting.length) {
|
|
149
|
+
this.logger.logError(`❌ ${conflicting.join(', ')} cannot be used with --partition (a service tenant only uses client_credentials).`);
|
|
150
|
+
this.exitCode = 1;
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const service = await this.app.container.make('authkit.server');
|
|
154
|
+
const { AdminClientsService } = await import('../src/host/admin_clients_service.js');
|
|
155
|
+
const { ClientMetadataError } = await import('../src/host/client_metadata.js');
|
|
156
|
+
const svc = new AdminClientsService(service);
|
|
157
|
+
const input = {
|
|
158
|
+
clientId: this.clientId,
|
|
159
|
+
applicationType: 'web',
|
|
160
|
+
redirectUris: [],
|
|
161
|
+
postLogoutRedirectUris: [],
|
|
162
|
+
grantTypes: ['client_credentials'],
|
|
163
|
+
tokenEndpointAuthMethod: 'client_secret_basic',
|
|
164
|
+
serviceTenant: { partition: this.partition, capabilities: this.capability ?? [] },
|
|
165
|
+
};
|
|
166
|
+
let created;
|
|
167
|
+
try {
|
|
168
|
+
created = this.clientSecret
|
|
169
|
+
? await svc.createWithSecret(input, this.clientSecret)
|
|
170
|
+
: await svc.create(input);
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
if (err instanceof ClientMetadataError) {
|
|
174
|
+
this.logger.logError(`❌ ${err.message}`);
|
|
175
|
+
this.exitCode = 1;
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
throw err;
|
|
179
|
+
}
|
|
180
|
+
const tenant = (await svc.find(created.clientId))?.serviceTenant;
|
|
181
|
+
await service.config.audit
|
|
182
|
+
?.record({
|
|
183
|
+
type: 'client.created',
|
|
184
|
+
clientId: created.clientId,
|
|
185
|
+
ip: null,
|
|
186
|
+
metadata: { actor: 'cli', partition: tenant?.partition },
|
|
187
|
+
})
|
|
188
|
+
.catch(() => { });
|
|
189
|
+
if (this.json) {
|
|
190
|
+
this.logger.info(JSON.stringify({
|
|
191
|
+
clientId: created.clientId,
|
|
192
|
+
clientSecret: created.clientSecret,
|
|
193
|
+
grantTypes: ['client_credentials'],
|
|
194
|
+
serviceTenant: tenant,
|
|
195
|
+
resources: Object.keys(service.config.serviceTenants.resources),
|
|
196
|
+
}, null, 2));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
this.logger.success(`Service tenant created: ${created.clientId}`);
|
|
200
|
+
this.logger.info(` partition: ${tenant?.partition}`);
|
|
201
|
+
if (tenant?.capabilities.length) {
|
|
202
|
+
this.logger.info(` capabilities: ${tenant.capabilities.join(', ')}`);
|
|
203
|
+
}
|
|
204
|
+
this.logger.info(` resources: ${Object.keys(service.config.serviceTenants.resources).join(', ')}`);
|
|
205
|
+
this.logger.info('');
|
|
206
|
+
this.logger.success('CLIENT SECRET (shown ONCE - store it now):');
|
|
207
|
+
this.logger.success(` ${created.clientSecret}`);
|
|
208
|
+
this.logger.info('');
|
|
209
|
+
}
|
|
123
210
|
}
|
|
124
211
|
__decorate([
|
|
125
212
|
flags.string({ description: 'client_id desejado. Omitir gera um UUID aleatório.' })
|
|
@@ -155,6 +242,16 @@ __decorate([
|
|
|
155
242
|
description: 'Usa este client_secret em vez de gerar um (client confidencial). Útil para manter o secret que os RPs já usam.',
|
|
156
243
|
})
|
|
157
244
|
], AuthkitClientsCreate.prototype, "clientSecret", void 0);
|
|
245
|
+
__decorate([
|
|
246
|
+
flags.string({
|
|
247
|
+
description: 'Service tenant: the partition its tokens act as (`partition` claim). Implies client_credentials only, no redirect URIs.',
|
|
248
|
+
})
|
|
249
|
+
], AuthkitClientsCreate.prototype, "partition", void 0);
|
|
250
|
+
__decorate([
|
|
251
|
+
flags.array({
|
|
252
|
+
description: 'Service tenant: capability granted in its tokens (repeatable). Needs --partition.',
|
|
253
|
+
})
|
|
254
|
+
], AuthkitClientsCreate.prototype, "capability", void 0);
|
|
158
255
|
__decorate([
|
|
159
256
|
flags.boolean({
|
|
160
257
|
description: 'Output em JSON machine-readable (inclui clientId e clientSecret).',
|
|
@@ -130,6 +130,20 @@
|
|
|
130
130
|
"type": "string",
|
|
131
131
|
"description": "Usa este client_secret em vez de gerar um (client confidencial)."
|
|
132
132
|
},
|
|
133
|
+
{
|
|
134
|
+
"name": "partition",
|
|
135
|
+
"flagName": "partition",
|
|
136
|
+
"required": false,
|
|
137
|
+
"type": "string",
|
|
138
|
+
"description": "Service tenant: the partition its tokens act as (`partition` claim). Implies client_credentials only, no redirect URIs."
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"name": "capability",
|
|
142
|
+
"flagName": "capability",
|
|
143
|
+
"required": false,
|
|
144
|
+
"type": "array",
|
|
145
|
+
"description": "Service tenant: capability granted in its tokens (repeatable). Needs --partition."
|
|
146
|
+
},
|
|
133
147
|
{
|
|
134
148
|
"name": "json",
|
|
135
149
|
"flagName": "json",
|
package/build/index.d.ts
CHANGED
|
@@ -158,4 +158,7 @@ export { checkClientRegistration, classifyRedirect, type RedirectUriPolicy, type
|
|
|
158
158
|
export { registerOidcRoutes } from './src/register_routes.js';
|
|
159
159
|
export type { EnsureSchemaOptions, EnsureSchemaReport } from './src/schema/ensure.js';
|
|
160
160
|
export { ensureAuthkitSchema, ensureAuthkitTables } from './src/schema/ensure.js';
|
|
161
|
+
export type { ServiceTenantInfo } from './src/service_tenants/client_metadata.js';
|
|
162
|
+
export type { ResolvedServiceTenantResource, ResolvedServiceTenantsConfig, ServiceTenantResourceInput, ServiceTenantsConfigInput, } from './src/service_tenants/config.js';
|
|
163
|
+
export { AuthkitTenantVerifier, type AuthkitTenantVerifierFn, type AuthkitTenantVerifierOptions, authkitTenantVerifier, type TenantTokenIssuer, type TenantVerifierInput, type VerifiedTenant, } from './src/service_tenants/tenant_verifier.js';
|
|
161
164
|
export { stubsRoot } from './stubs/main.js';
|
package/build/index.js
CHANGED
|
@@ -136,4 +136,5 @@ export { OidcService } from './src/provider/oidc_service.js';
|
|
|
136
136
|
export { checkClientRegistration, classifyRedirect, RegistrationPolicyError, } from './src/provider/registration_policy.js';
|
|
137
137
|
export { registerOidcRoutes } from './src/register_routes.js';
|
|
138
138
|
export { ensureAuthkitSchema, ensureAuthkitTables } from './src/schema/ensure.js';
|
|
139
|
+
export { AuthkitTenantVerifier, authkitTenantVerifier, } from './src/service_tenants/tenant_verifier.js';
|
|
139
140
|
export { stubsRoot } from './stubs/main.js';
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* varre os `audit.record({ type: ... })` do `src/` e falha quando um evento
|
|
9
9
|
* emitido não está aqui.
|
|
10
10
|
*/
|
|
11
|
-
export declare const AUDIT_EVENT_TYPES: readonly ['login.success', 'login.failure', 'signup', 'password_reset.issued', 'password_reset.consumed', 'pat.issued', 'pat.revoked', 'pat.used', 'impersonation', 'impersonation.panel_viewed', 'impersonation.stopped', 'mfa.enabled', 'mfa.disabled', 'mfa.recovery_codes_regenerated', 'account.locked', 'passkey.registered', 'passkey.removed', 'email_verification.issued', 'email_verification.consumed', 'client.created', 'client.updated', 'client.deleted', 'session.revoked_all', 'password.changed', 'password.rehashed', 'email.change_requested', 'email.changed', 'login.new_ip_notified', 'login.new_device', 'login.otp_sent', 'login.otp_verified', 'login.otp_failed', 'login.otp_invalidated', 'bot_protection.rejected', 'grant.revoked_by_user', 'user.created', 'user.password_reset_sent', 'user.disabled', 'user.enabled', 'user.deleted', 'profile.updated', 'account.deleted', 'account.exported', 'keys.rotated', 'organization.created', 'organization.updated', 'organization.deleted', 'organization.member_added', 'organization.member_removed', 'organization.member_role_changed', 'organization.member_role_updated', 'organization.switched', 'organization.deactivated', 'organization.invitation_sent', 'organization.invitation_accepted', 'organization.invitation_revoked', 'email_change.requested', 'email_change.confirmed', 'email_change.cancelled', 'security_notice.sent', 'settings.updated', 'maintenance.enabled', 'maintenance.disabled', 'trusted_device.revoked', 'password.expired_change_forced', 'otp.locked', 'otp.unlocked', 'otp.unlock_failed', 'sudo.confirmed', 'session.single_enforced', 'account.expired_login_blocked', 'account.expiration_warned', 'login.magic_link_sent', 'session.revoked', 'account.signed_out_all', 'client.secret_regenerated', 'roles_catalog.updated', 'agent.delegation_approved', 'agent.delegation_denied', 'agent.grant_revoked', 'agent.grant_revoked_by_admin', 'agent.allowed', 'agent.blocked', 'agent.registry_removed'];
|
|
11
|
+
export declare const AUDIT_EVENT_TYPES: readonly ['login.success', 'login.failure', 'signup', 'password_reset.issued', 'password_reset.consumed', 'pat.issued', 'pat.revoked', 'pat.used', 'impersonation', 'impersonation.panel_viewed', 'impersonation.stopped', 'mfa.enabled', 'mfa.disabled', 'mfa.recovery_codes_regenerated', 'account.locked', 'passkey.registered', 'passkey.removed', 'email_verification.issued', 'email_verification.consumed', 'client.created', 'client.updated', 'client.deleted', 'session.revoked_all', 'password.changed', 'password.rehashed', 'email.change_requested', 'email.changed', 'login.new_ip_notified', 'login.new_device', 'login.otp_sent', 'login.otp_verified', 'login.otp_failed', 'login.otp_invalidated', 'bot_protection.rejected', 'grant.revoked_by_user', 'user.created', 'user.password_reset_sent', 'user.disabled', 'user.enabled', 'user.deleted', 'profile.updated', 'account.deleted', 'account.exported', 'keys.rotated', 'organization.created', 'organization.updated', 'organization.deleted', 'organization.member_added', 'organization.member_removed', 'organization.member_role_changed', 'organization.member_role_updated', 'organization.switched', 'organization.deactivated', 'organization.invitation_sent', 'organization.invitation_accepted', 'organization.invitation_revoked', 'email_change.requested', 'email_change.confirmed', 'email_change.cancelled', 'security_notice.sent', 'settings.updated', 'maintenance.enabled', 'maintenance.disabled', 'trusted_device.revoked', 'password.expired_change_forced', 'otp.locked', 'otp.unlocked', 'otp.unlock_failed', 'sudo.confirmed', 'session.single_enforced', 'account.expired_login_blocked', 'account.expiration_warned', 'login.magic_link_sent', 'session.revoked', 'account.signed_out_all', 'client.secret_regenerated', 'client.disabled', 'client.enabled', 'service_tenant.token_issued', 'roles_catalog.updated', 'agent.delegation_approved', 'agent.delegation_denied', 'agent.grant_revoked', 'agent.grant_revoked_by_admin', 'agent.allowed', 'agent.blocked', 'agent.registry_removed'];
|
|
12
12
|
/** Tipos de eventos de auditoria relevantes para segurança emitidos pelo IdP. */
|
|
13
13
|
export type AuditEventType = (typeof AUDIT_EVENT_TYPES)[number];
|
|
14
14
|
/**
|
|
@@ -107,6 +107,11 @@ export const AUDIT_EVENT_TYPES = [
|
|
|
107
107
|
'session.revoked',
|
|
108
108
|
'account.signed_out_all',
|
|
109
109
|
'client.secret_regenerated',
|
|
110
|
+
// A client was disabled (no new tokens) or enabled again.
|
|
111
|
+
'client.disabled',
|
|
112
|
+
'client.enabled',
|
|
113
|
+
// A service tenant got a token (`partition`, `resource`, `jti` in the metadata).
|
|
114
|
+
'service_tenant.token_issued',
|
|
110
115
|
'roles_catalog.updated',
|
|
111
116
|
// Personal agents (PACT §5): o usuário aprovou/negou a delegação pedida por um
|
|
112
117
|
// agente, ou revogou um grant no console. `clientId` = issuer do agente.
|
|
@@ -22,6 +22,7 @@ import type { WhatsappCodeSenderBinding } from './host/whatsapp_code_sender.js';
|
|
|
22
22
|
import { type McpOAuthConfigInput, type ResolvedMcpOAuthConfig } from './mcp/mcp_oauth.js';
|
|
23
23
|
import type { PatStore } from './pat/pat_store.js';
|
|
24
24
|
import { type RedirectUriPolicy, type ResolvedRedirectUriPolicy, type ValidateRegistrationHook } from './provider/registration_policy.js';
|
|
25
|
+
import { type ResolvedServiceTenantsConfig, type ServiceTenantsConfigInput } from './service_tenants/config.js';
|
|
25
26
|
export type { AuthAccount };
|
|
26
27
|
export { adapters };
|
|
27
28
|
export type AuthHostRenderer = (ctx: HttpContext, view: string, props: Record<string, unknown>) => unknown;
|
|
@@ -1113,6 +1114,19 @@ export interface AuthServerConfigInput {
|
|
|
1113
1114
|
* `resources` mapeia resource indicators (RFC 8707) para audiences/scopes/formato/TTL por API.
|
|
1114
1115
|
*/
|
|
1115
1116
|
accessTokens?: AccessTokensConfig;
|
|
1117
|
+
/**
|
|
1118
|
+
* Service tenants: confidential `client_credentials` clients that act as a partition of
|
|
1119
|
+
* a resource server (e.g. a durable control plane). `resources` lists the resource
|
|
1120
|
+
* indicators (RFC 8707) that issue their JWT access tokens (RFC 9068), each with an
|
|
1121
|
+
* optional `expiresIn` (seconds, default 600), `audience` and `scopes`:
|
|
1122
|
+
*
|
|
1123
|
+
* ```ts
|
|
1124
|
+
* serviceTenants: { resources: { 'urn:agora:durable': { expiresIn: 600 } } }
|
|
1125
|
+
* ```
|
|
1126
|
+
*
|
|
1127
|
+
* Turns on the `client_credentials` grant. Default: off.
|
|
1128
|
+
*/
|
|
1129
|
+
serviceTenants?: ServiceTenantsConfigInput;
|
|
1116
1130
|
/**
|
|
1117
1131
|
* Login OAuth de clientes MCP (Claude Code, Claude, ChatGPT, VS Code, Cursor): `true` liga o
|
|
1118
1132
|
* registro dinâmico restrito aos redirects deles, o refresh token desses clientes e os
|
|
@@ -1355,6 +1369,8 @@ export interface ResolvedServerConfig {
|
|
|
1355
1369
|
registration: ResolvedRegistrationConfig;
|
|
1356
1370
|
/** Access Tokens resolvido (RFC 9068; default opaque). */
|
|
1357
1371
|
accessTokens: ResolvedAccessTokensConfig;
|
|
1372
|
+
/** Service tenants (always present; `enabled: false` when no resource is declared). */
|
|
1373
|
+
serviceTenants: ResolvedServiceTenantsConfig;
|
|
1358
1374
|
mcp: ResolvedMcpOAuthConfig;
|
|
1359
1375
|
/** Console admin resolvido (sempre presente; default desligado). */
|
|
1360
1376
|
admin: ResolvedAdminConfig;
|
|
@@ -17,6 +17,7 @@ import { loadEncryptionService } from './keys/keystore_crypto.js';
|
|
|
17
17
|
import { KeystoreManager, resolveKeystoreVault } from './keys/keystore_manager.js';
|
|
18
18
|
import { mcpClientRegistration, resolveMcpOAuth, } from './mcp/mcp_oauth.js';
|
|
19
19
|
import { OPEN_REGISTRATION_REDIRECT_POLICY, resolveRedirectUriPolicy, } from './provider/registration_policy.js';
|
|
20
|
+
import { resolveServiceTenants, } from './service_tenants/config.js';
|
|
20
21
|
export { adapters };
|
|
21
22
|
const RATE_LIMIT_DEFAULTS = {
|
|
22
23
|
login: { points: 10, duration: '1 min' },
|
|
@@ -475,6 +476,7 @@ export function defineConfig(config) {
|
|
|
475
476
|
: undefined,
|
|
476
477
|
registration: resolveRegistration(config.registration),
|
|
477
478
|
accessTokens: resolveAccessTokens(config.issuer, config.accessTokens),
|
|
479
|
+
serviceTenants: resolveServiceTenants(config.serviceTenants, config.accessTokens?.resources),
|
|
478
480
|
mcp: resolveMcpOAuth(config.mcp),
|
|
479
481
|
admin: resolveAdmin(config.admin),
|
|
480
482
|
adminApi: resolveAdminApi(config.adminApi),
|
|
@@ -41,6 +41,10 @@ function fromJwtPayload(payload) {
|
|
|
41
41
|
if (typeof payload.sub !== 'string' || !payload.sub)
|
|
42
42
|
return null;
|
|
43
43
|
const clientId = payload.client_id;
|
|
44
|
+
// No user behind it: a `client_credentials` token (a service tenant's, for one) has
|
|
45
|
+
// `sub` = the client id (RFC 9068 §2.2) and never authenticates an account.
|
|
46
|
+
if (payload.sub === clientId)
|
|
47
|
+
return null;
|
|
44
48
|
return {
|
|
45
49
|
format: 'jwt',
|
|
46
50
|
sub: payload.sub,
|
|
@@ -209,6 +213,9 @@ export function remoteAccessTokenVerifier(options) {
|
|
|
209
213
|
return null;
|
|
210
214
|
if (typeof body.sub !== 'string' || !body.sub)
|
|
211
215
|
return null;
|
|
216
|
+
// `client_credentials` token: no account behind it.
|
|
217
|
+
if (body.sub === body.client_id)
|
|
218
|
+
return null;
|
|
212
219
|
if (typeof body.exp === 'number' && body.exp <= Math.floor(Date.now() / 1000))
|
|
213
220
|
return null;
|
|
214
221
|
return {
|
|
@@ -6,6 +6,7 @@ import type { HttpContext } from '@adonisjs/core/http';
|
|
|
6
6
|
* em create/regenerate. Audita create/update/delete.
|
|
7
7
|
*/
|
|
8
8
|
export default class ApiClientsController {
|
|
9
|
+
#private;
|
|
9
10
|
index(ctx: HttpContext): Promise<{
|
|
10
11
|
data: {
|
|
11
12
|
clientId: string;
|
|
@@ -19,6 +20,8 @@ export default class ApiClientsController {
|
|
|
19
20
|
backchannelLogoutSessionRequired: boolean;
|
|
20
21
|
clientName: string | null;
|
|
21
22
|
logoUri: string | null;
|
|
23
|
+
disabled: boolean;
|
|
24
|
+
serviceTenant: import("../../service_tenants/client_metadata.ts").ServiceTenantInfo | null;
|
|
22
25
|
}[];
|
|
23
26
|
canList: boolean;
|
|
24
27
|
}>;
|
|
@@ -34,6 +37,8 @@ export default class ApiClientsController {
|
|
|
34
37
|
backchannelLogoutSessionRequired: boolean;
|
|
35
38
|
clientName: string | null;
|
|
36
39
|
logoUri: string | null;
|
|
40
|
+
disabled: boolean;
|
|
41
|
+
serviceTenant: import("../../service_tenants/client_metadata.ts").ServiceTenantInfo | null;
|
|
37
42
|
}>;
|
|
38
43
|
store(ctx: HttpContext): Promise<void | {
|
|
39
44
|
clientId: string;
|
|
@@ -51,11 +56,43 @@ export default class ApiClientsController {
|
|
|
51
56
|
backchannelLogoutSessionRequired: boolean;
|
|
52
57
|
clientName: string | null;
|
|
53
58
|
logoUri: string | null;
|
|
59
|
+
disabled: boolean;
|
|
60
|
+
serviceTenant: import("../../service_tenants/client_metadata.ts").ServiceTenantInfo | null;
|
|
54
61
|
}>;
|
|
55
62
|
regenerateSecret(ctx: HttpContext): Promise<void | {
|
|
56
63
|
clientId: any;
|
|
57
64
|
clientSecret: string;
|
|
58
65
|
}>;
|
|
66
|
+
disable(ctx: HttpContext): Promise<void | {
|
|
67
|
+
clientId: string;
|
|
68
|
+
confidential: boolean;
|
|
69
|
+
grantTypes: string[];
|
|
70
|
+
redirectUris: string[];
|
|
71
|
+
postLogoutRedirectUris: string[];
|
|
72
|
+
tokenEndpointAuthMethod: string;
|
|
73
|
+
applicationType: import("../client_metadata.js").ApplicationType;
|
|
74
|
+
backchannelLogoutUri: string | null;
|
|
75
|
+
backchannelLogoutSessionRequired: boolean;
|
|
76
|
+
clientName: string | null;
|
|
77
|
+
logoUri: string | null;
|
|
78
|
+
disabled: boolean;
|
|
79
|
+
serviceTenant: import("../../service_tenants/client_metadata.ts").ServiceTenantInfo | null;
|
|
80
|
+
}>;
|
|
81
|
+
enable(ctx: HttpContext): Promise<void | {
|
|
82
|
+
clientId: string;
|
|
83
|
+
confidential: boolean;
|
|
84
|
+
grantTypes: string[];
|
|
85
|
+
redirectUris: string[];
|
|
86
|
+
postLogoutRedirectUris: string[];
|
|
87
|
+
tokenEndpointAuthMethod: string;
|
|
88
|
+
applicationType: import("../client_metadata.js").ApplicationType;
|
|
89
|
+
backchannelLogoutUri: string | null;
|
|
90
|
+
backchannelLogoutSessionRequired: boolean;
|
|
91
|
+
clientName: string | null;
|
|
92
|
+
logoUri: string | null;
|
|
93
|
+
disabled: boolean;
|
|
94
|
+
serviceTenant: import("../../service_tenants/client_metadata.ts").ServiceTenantInfo | null;
|
|
95
|
+
}>;
|
|
59
96
|
destroy(ctx: HttpContext): Promise<{
|
|
60
97
|
clientId: any;
|
|
61
98
|
deleted: boolean;
|
|
@@ -85,11 +85,16 @@ export default class ApiClientsController {
|
|
|
85
85
|
const id = ctx.request.param('id');
|
|
86
86
|
try {
|
|
87
87
|
const secret = await svc.regenerateSecret(id);
|
|
88
|
+
const tenant = (await svc.find(id))?.serviceTenant;
|
|
88
89
|
await service.config.audit?.record({
|
|
89
90
|
type: 'client.updated',
|
|
90
91
|
clientId: id,
|
|
91
92
|
ip: ctx.request.ip?.() ?? null,
|
|
92
|
-
metadata: {
|
|
93
|
+
metadata: {
|
|
94
|
+
actor: 'admin-api',
|
|
95
|
+
action: 'regenerate_secret',
|
|
96
|
+
...(tenant ? { partition: tenant.partition } : {}),
|
|
97
|
+
},
|
|
93
98
|
});
|
|
94
99
|
return { clientId: id, clientSecret: secret };
|
|
95
100
|
}
|
|
@@ -97,6 +102,31 @@ export default class ApiClientsController {
|
|
|
97
102
|
return ctx.response.conflict(apiError('cannot_regenerate', e.message));
|
|
98
103
|
}
|
|
99
104
|
}
|
|
105
|
+
async disable(ctx) {
|
|
106
|
+
return this.#setDisabled(ctx, true);
|
|
107
|
+
}
|
|
108
|
+
async enable(ctx) {
|
|
109
|
+
return this.#setDisabled(ctx, false);
|
|
110
|
+
}
|
|
111
|
+
/** Disabled clients get `invalid_client` from the next request on. */
|
|
112
|
+
async #setDisabled(ctx, disabled) {
|
|
113
|
+
const { service, svc } = await clientsService(ctx);
|
|
114
|
+
const id = ctx.request.param('id');
|
|
115
|
+
if (!(await svc.setDisabled(id, disabled))) {
|
|
116
|
+
return ctx.response.notFound(apiError('not_found', 'Client não encontrado.'));
|
|
117
|
+
}
|
|
118
|
+
const client = await svc.find(id);
|
|
119
|
+
await service.config.audit?.record({
|
|
120
|
+
type: disabled ? 'client.disabled' : 'client.enabled',
|
|
121
|
+
clientId: id,
|
|
122
|
+
ip: ctx.request.ip?.() ?? null,
|
|
123
|
+
metadata: {
|
|
124
|
+
actor: 'admin-api',
|
|
125
|
+
...(client?.serviceTenant ? { partition: client.serviceTenant.partition } : {}),
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
return clientDto(client);
|
|
129
|
+
}
|
|
100
130
|
async destroy(ctx) {
|
|
101
131
|
const { service, svc } = await clientsService(ctx);
|
|
102
132
|
const id = ctx.request.param('id');
|
|
@@ -40,6 +40,8 @@ export declare function clientDto(client: AdminClient): {
|
|
|
40
40
|
backchannelLogoutSessionRequired: boolean;
|
|
41
41
|
clientName: string | null;
|
|
42
42
|
logoUri: string | null;
|
|
43
|
+
disabled: boolean;
|
|
44
|
+
serviceTenant: import("../../service_tenants/client_metadata.ts").ServiceTenantInfo | null;
|
|
43
45
|
};
|
|
44
46
|
/** Inclui o secret (mostrado UMA vez) em create/regenerate. */
|
|
45
47
|
export declare function createdClientDto(created: CreatedClient): {
|
|
@@ -67,7 +69,7 @@ export declare function grantDto(grant: AdminGrant): {
|
|
|
67
69
|
};
|
|
68
70
|
export declare function auditDto(event: StoredAuditEvent): {
|
|
69
71
|
id: string;
|
|
70
|
-
type: "account.deleted" | "account.expiration_warned" | "account.expired_login_blocked" | "account.exported" | "account.locked" | "account.signed_out_all" | "agent.allowed" | "agent.blocked" | "agent.delegation_approved" | "agent.delegation_denied" | "agent.grant_revoked" | "agent.grant_revoked_by_admin" | "agent.registry_removed" | "bot_protection.rejected" | "client.created" | "client.deleted" | "client.secret_regenerated" | "client.updated" | "email.change_requested" | "email.changed" | "email_change.cancelled" | "email_change.confirmed" | "email_change.requested" | "email_verification.consumed" | "email_verification.issued" | "grant.revoked_by_user" | "impersonation" | "impersonation.panel_viewed" | "impersonation.stopped" | "keys.rotated" | "login.failure" | "login.magic_link_sent" | "login.new_device" | "login.new_ip_notified" | "login.otp_failed" | "login.otp_invalidated" | "login.otp_sent" | "login.otp_verified" | "login.success" | "maintenance.disabled" | "maintenance.enabled" | "mfa.disabled" | "mfa.enabled" | "mfa.recovery_codes_regenerated" | "organization.created" | "organization.deactivated" | "organization.deleted" | "organization.invitation_accepted" | "organization.invitation_revoked" | "organization.invitation_sent" | "organization.member_added" | "organization.member_removed" | "organization.member_role_changed" | "organization.member_role_updated" | "organization.switched" | "organization.updated" | "otp.locked" | "otp.unlock_failed" | "otp.unlocked" | "passkey.registered" | "passkey.removed" | "password.changed" | "password.expired_change_forced" | "password.rehashed" | "password_reset.consumed" | "password_reset.issued" | "pat.issued" | "pat.revoked" | "pat.used" | "profile.updated" | "roles_catalog.updated" | "security_notice.sent" | "session.revoked" | "session.revoked_all" | "session.single_enforced" | "settings.updated" | "signup" | "sudo.confirmed" | "trusted_device.revoked" | "user.created" | "user.deleted" | "user.disabled" | "user.enabled" | "user.password_reset_sent";
|
|
72
|
+
type: "account.deleted" | "account.expiration_warned" | "account.expired_login_blocked" | "account.exported" | "account.locked" | "account.signed_out_all" | "agent.allowed" | "agent.blocked" | "agent.delegation_approved" | "agent.delegation_denied" | "agent.grant_revoked" | "agent.grant_revoked_by_admin" | "agent.registry_removed" | "bot_protection.rejected" | "client.created" | "client.deleted" | "client.disabled" | "client.enabled" | "client.secret_regenerated" | "client.updated" | "email.change_requested" | "email.changed" | "email_change.cancelled" | "email_change.confirmed" | "email_change.requested" | "email_verification.consumed" | "email_verification.issued" | "grant.revoked_by_user" | "impersonation" | "impersonation.panel_viewed" | "impersonation.stopped" | "keys.rotated" | "login.failure" | "login.magic_link_sent" | "login.new_device" | "login.new_ip_notified" | "login.otp_failed" | "login.otp_invalidated" | "login.otp_sent" | "login.otp_verified" | "login.success" | "maintenance.disabled" | "maintenance.enabled" | "mfa.disabled" | "mfa.enabled" | "mfa.recovery_codes_regenerated" | "organization.created" | "organization.deactivated" | "organization.deleted" | "organization.invitation_accepted" | "organization.invitation_revoked" | "organization.invitation_sent" | "organization.member_added" | "organization.member_removed" | "organization.member_role_changed" | "organization.member_role_updated" | "organization.switched" | "organization.updated" | "otp.locked" | "otp.unlock_failed" | "otp.unlocked" | "passkey.registered" | "passkey.removed" | "password.changed" | "password.expired_change_forced" | "password.rehashed" | "password_reset.consumed" | "password_reset.issued" | "pat.issued" | "pat.revoked" | "pat.used" | "profile.updated" | "roles_catalog.updated" | "security_notice.sent" | "service_tenant.token_issued" | "session.revoked" | "session.revoked_all" | "session.single_enforced" | "settings.updated" | "signup" | "sudo.confirmed" | "trusted_device.revoked" | "user.created" | "user.deleted" | "user.disabled" | "user.enabled" | "user.password_reset_sent";
|
|
71
73
|
accountId: string | null;
|
|
72
74
|
email: string | null;
|
|
73
75
|
clientId: string | null;
|
|
@@ -32,6 +32,8 @@ export function clientDto(client) {
|
|
|
32
32
|
backchannelLogoutSessionRequired: client.backchannelLogoutSessionRequired ?? false,
|
|
33
33
|
clientName: client.clientName,
|
|
34
34
|
logoUri: client.logoUri,
|
|
35
|
+
disabled: client.disabled,
|
|
36
|
+
serviceTenant: client.serviceTenant,
|
|
35
37
|
};
|
|
36
38
|
}
|
|
37
39
|
/** Inclui o secret (mostrado UMA vez) em create/regenerate. */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { OidcService } from '../provider/oidc_service.js';
|
|
2
|
+
import { type ServiceTenantInfo } from '../service_tenants/client_metadata.js';
|
|
2
3
|
import { type ApplicationType } from './client_metadata.js';
|
|
3
4
|
/** Métodos de autenticação no token endpoint suportados pelo formulário admin. */
|
|
4
5
|
export type TokenEndpointAuthMethod = 'client_secret_basic' | 'client_secret_post' | 'none';
|
|
@@ -30,6 +31,15 @@ export interface ClientInput {
|
|
|
30
31
|
* `backchannelLogoutUri` não está definido.
|
|
31
32
|
*/
|
|
32
33
|
backchannelLogoutSessionRequired?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Makes the client a service tenant: confidential, `client_credentials` only, no
|
|
36
|
+
* redirect URIs; its tokens carry `partition` and `capabilities`. On update, `null`
|
|
37
|
+
* turns it back into a regular client; absent keeps the current value.
|
|
38
|
+
*/
|
|
39
|
+
serviceTenant?: {
|
|
40
|
+
partition: string;
|
|
41
|
+
capabilities?: string[];
|
|
42
|
+
} | null;
|
|
33
43
|
}
|
|
34
44
|
/** Client persistido, apresentado ao console admin. */
|
|
35
45
|
export interface AdminClient {
|
|
@@ -49,6 +59,10 @@ export interface AdminClient {
|
|
|
49
59
|
clientName: string | null;
|
|
50
60
|
/** `logo_uri` registrado, ou `null`. */
|
|
51
61
|
logoUri: string | null;
|
|
62
|
+
/** Disabled clients get no tokens (`invalid_client`) until enabled again. */
|
|
63
|
+
disabled: boolean;
|
|
64
|
+
/** Service-tenant settings, or `null` for a regular client. */
|
|
65
|
+
serviceTenant: ServiceTenantInfo | null;
|
|
52
66
|
}
|
|
53
67
|
/** Resultado de uma criação: o client + o secret em claro (mostrado UMA vez). */
|
|
54
68
|
export interface CreatedClient {
|
|
@@ -94,6 +108,12 @@ export declare class AdminClientsService {
|
|
|
94
108
|
* PRESERVANDO o client_secret existente. Lança se o client não existe.
|
|
95
109
|
*/
|
|
96
110
|
update(clientId: string, input: Partial<ClientInput>): Promise<void>;
|
|
111
|
+
/**
|
|
112
|
+
* Disables or re-enables a client. A disabled client gets `invalid_client` on every
|
|
113
|
+
* endpoint from the next request on (tokens already issued stay valid until they
|
|
114
|
+
* expire). Returns `false` when the client does not exist.
|
|
115
|
+
*/
|
|
116
|
+
setDisabled(clientId: string, disabled: boolean): Promise<boolean>;
|
|
97
117
|
/**
|
|
98
118
|
* Regenera o client_secret de um client confidencial, preservando o resto da
|
|
99
119
|
* metadata. Retorna o novo secret em claro (mostrado UMA vez). Lança se o
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { CLIENT_DISABLED_KEY, isClientDisabled, normalizeServiceTenant, SERVICE_TENANT_KEY, serviceTenantOf, } from '../service_tenants/client_metadata.js';
|
|
2
3
|
import { assertClientMetadata, ClientMetadataError, } from './client_metadata.js';
|
|
3
4
|
import { CONSOLE_SSO_CLIENT_ID } from './console_sso.js';
|
|
4
5
|
/** Métodos públicos (sem segredo) — espelham os "non-secret" do oidc-provider. */
|
|
@@ -111,14 +112,37 @@ export class AdminClientsService {
|
|
|
111
112
|
logoUri: input.logoUri === null
|
|
112
113
|
? undefined
|
|
113
114
|
: (input.logoUri ?? existing.logo_uri),
|
|
115
|
+
serviceTenant: input.serviceTenant === undefined
|
|
116
|
+
? serviceTenantOf(existing)
|
|
117
|
+
: input.serviceTenant,
|
|
114
118
|
};
|
|
115
119
|
const confidential = !PUBLIC_AUTH_METHODS.has(merged.tokenEndpointAuthMethod);
|
|
116
120
|
// Mantém o secret atual se continua confidencial; se virou public, remove-o.
|
|
117
121
|
const clientSecret = confidential ? (previousSecret ?? randomId()) : undefined;
|
|
118
122
|
const payload = this.#buildPayload(clientId, merged, clientSecret);
|
|
123
|
+
if (isClientDisabled(existing))
|
|
124
|
+
payload[CLIENT_DISABLED_KEY] = true;
|
|
119
125
|
await this.#adapter.upsert(clientId, payload, 0);
|
|
120
126
|
await this.oidc.evictDynamicClientCache();
|
|
121
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* Disables or re-enables a client. A disabled client gets `invalid_client` on every
|
|
130
|
+
* endpoint from the next request on (tokens already issued stay valid until they
|
|
131
|
+
* expire). Returns `false` when the client does not exist.
|
|
132
|
+
*/
|
|
133
|
+
async setDisabled(clientId, disabled) {
|
|
134
|
+
const existing = await this.#adapter.find(clientId);
|
|
135
|
+
if (!existing)
|
|
136
|
+
return false;
|
|
137
|
+
const payload = { ...existing };
|
|
138
|
+
if (disabled)
|
|
139
|
+
payload[CLIENT_DISABLED_KEY] = true;
|
|
140
|
+
else
|
|
141
|
+
delete payload[CLIENT_DISABLED_KEY];
|
|
142
|
+
await this.#adapter.upsert(clientId, payload, 0);
|
|
143
|
+
await this.oidc.evictDynamicClientCache();
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
122
146
|
/**
|
|
123
147
|
* Regenera o client_secret de um client confidencial, preservando o resto da
|
|
124
148
|
* metadata. Retorna o novo secret em claro (mostrado UMA vez). Lança se o
|
|
@@ -148,6 +172,8 @@ export class AdminClientsService {
|
|
|
148
172
|
* `client_metadata.ts`). Lança `ClientMetadataError`.
|
|
149
173
|
*/
|
|
150
174
|
#assertValid(input) {
|
|
175
|
+
if (input.serviceTenant)
|
|
176
|
+
this.#assertServiceTenant(input);
|
|
151
177
|
assertClientMetadata({
|
|
152
178
|
applicationType: input.applicationType ?? 'web',
|
|
153
179
|
tokenEndpointAuthMethod: input.tokenEndpointAuthMethod,
|
|
@@ -156,6 +182,27 @@ export class AdminClientsService {
|
|
|
156
182
|
postLogoutRedirectUris: input.postLogoutRedirectUris,
|
|
157
183
|
});
|
|
158
184
|
}
|
|
185
|
+
/** Rules of a service-tenant client (see {@link ClientInput.serviceTenant}). */
|
|
186
|
+
#assertServiceTenant(input) {
|
|
187
|
+
if (!this.oidc.config.serviceTenants?.enabled) {
|
|
188
|
+
throw new ClientMetadataError('service tenants are not enabled: declare serviceTenants.resources in config/authkit.ts');
|
|
189
|
+
}
|
|
190
|
+
if (PUBLIC_AUTH_METHODS.has(input.tokenEndpointAuthMethod)) {
|
|
191
|
+
throw new ClientMetadataError('a service tenant must be confidential (it has a secret)');
|
|
192
|
+
}
|
|
193
|
+
if ((input.applicationType ?? 'web') !== 'web') {
|
|
194
|
+
throw new ClientMetadataError('a service tenant must be a web (server) client');
|
|
195
|
+
}
|
|
196
|
+
if (input.grantTypes.length !== 1 || input.grantTypes[0] !== 'client_credentials') {
|
|
197
|
+
throw new ClientMetadataError('a service tenant can only use the client_credentials grant');
|
|
198
|
+
}
|
|
199
|
+
if (input.redirectUris.length || input.postLogoutRedirectUris.length) {
|
|
200
|
+
throw new ClientMetadataError('a service tenant has no redirect URIs');
|
|
201
|
+
}
|
|
202
|
+
if (input.backchannelLogoutUri) {
|
|
203
|
+
throw new ClientMetadataError('a service tenant has no back-channel logout URI');
|
|
204
|
+
}
|
|
205
|
+
}
|
|
159
206
|
/**
|
|
160
207
|
* Monta o payload na forma snake_case que o oidc-provider espera/persiste —
|
|
161
208
|
* verificada contra o que o registro dinâmico (RFC 7591) grava. As chaves de
|
|
@@ -193,6 +240,12 @@ export class AdminClientsService {
|
|
|
193
240
|
payload.backchannel_logout_session_required = input.backchannelLogoutSessionRequired;
|
|
194
241
|
}
|
|
195
242
|
}
|
|
243
|
+
if (input.serviceTenant) {
|
|
244
|
+
const tenant = normalizeServiceTenant(input.serviceTenant);
|
|
245
|
+
if (typeof tenant === 'string')
|
|
246
|
+
throw new ClientMetadataError(tenant);
|
|
247
|
+
payload[SERVICE_TENANT_KEY] = tenant;
|
|
248
|
+
}
|
|
196
249
|
return payload;
|
|
197
250
|
}
|
|
198
251
|
/** Projeta um payload persistido para a forma exibida no console admin. */
|
|
@@ -209,6 +262,8 @@ export class AdminClientsService {
|
|
|
209
262
|
applicationType: p.application_type === 'native' ? 'native' : 'web',
|
|
210
263
|
clientName: typeof p.client_name === 'string' && p.client_name ? p.client_name : null,
|
|
211
264
|
logoUri: typeof p.logo_uri === 'string' && p.logo_uri ? p.logo_uri : null,
|
|
265
|
+
disabled: isClientDisabled(p),
|
|
266
|
+
serviceTenant: serviceTenantOf(p),
|
|
212
267
|
};
|
|
213
268
|
if (p.backchannel_logout_uri) {
|
|
214
269
|
result.backchannelLogoutUri = p.backchannel_logout_uri;
|