@adonis-agora/authkit-server 0.73.0 → 0.75.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/define_config.d.ts +8 -0
- package/build/src/define_config.js +1 -0
- package/build/src/host/access_token_verifier.d.ts +107 -0
- package/build/src/host/access_token_verifier.js +248 -0
- 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/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_validators.d.ts +11 -1
- package/build/src/host/admin_validators.js +36 -8
- package/build/src/host/bearer_account.d.ts +26 -0
- package/build/src/host/bearer_account.js +38 -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 +20 -10
- package/build/src/host/impersonation_session.d.ts +5 -0
- package/build/src/host/impersonation_session.js +37 -4
- package/build/src/host/oidc_bearer_guard.d.ts +113 -0
- package/build/src/host/oidc_bearer_guard.js +232 -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/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 +47 -17
- package/build/src/provider/oidc_service.d.ts +8 -0
- package/build/src/provider/oidc_service.js +12 -1
- package/build/src/provider/token_exchange.d.ts +26 -0
- package/build/src/provider/token_exchange.js +44 -3
- package/package.json +3 -3
|
@@ -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,
|
|
@@ -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,
|
|
@@ -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
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import vine from '@vinejs/vine';
|
|
2
|
+
import { APPLICATION_TYPES, redirectUriProblem } from './client_metadata.js';
|
|
2
3
|
import { normalizeEmailIdentifier } from './email_identifier.js';
|
|
3
4
|
/**
|
|
4
5
|
* Validators VineJS dos recursos administrativos (Admin REST API + console admin).
|
|
@@ -42,8 +43,25 @@ const authMethod = vine.enum(['client_secret_basic', 'client_secret_post', 'none
|
|
|
42
43
|
* (`require_tld:false`) p/ não rejeitar `localhost` ou hostnames internos legítimos.
|
|
43
44
|
*/
|
|
44
45
|
const URL_OPTS = { require_protocol: true, require_tld: false, protocols: ['http', 'https'] };
|
|
45
|
-
/**
|
|
46
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Regra de redirect URI ciente do `applicationType` do MESMO payload (RFC 8252):
|
|
48
|
+
* client web continua restrito a http/https (L10); client nativo aceita esquema
|
|
49
|
+
* privado (`com.example.app:/cb`), https claimed e loopback http. Sem o tipo no
|
|
50
|
+
* payload (PATCH parcial) só as regras universais valem aqui — URI absoluta, sem
|
|
51
|
+
* fragmento, sem `javascript:`/`data:`/… — e o `AdminClientsService` confere o
|
|
52
|
+
* resto contra o tipo EFETIVO do client (ver `client_metadata.ts`).
|
|
53
|
+
*/
|
|
54
|
+
const redirectUriRule = vine.createRule((value, _options, field) => {
|
|
55
|
+
if (typeof value !== 'string')
|
|
56
|
+
return;
|
|
57
|
+
const declared = field.data?.applicationType;
|
|
58
|
+
const type = declared === 'web' || declared === 'native' ? declared : undefined;
|
|
59
|
+
const problem = redirectUriProblem(value, type);
|
|
60
|
+
if (problem)
|
|
61
|
+
field.report(`The {{ field }} field ${problem}`, 'redirectUri', field);
|
|
62
|
+
});
|
|
63
|
+
/** Array de redirect URIs (ver {@link redirectUriRule}). Rejeita strings que não são URI válida. */
|
|
64
|
+
const redirectUriArray = vine.array(vine.string().trim().use(redirectUriRule()));
|
|
47
65
|
/**
|
|
48
66
|
* Allowlist de grant_types aceitos no registro/edição de client (M10). Bloqueia
|
|
49
67
|
* `implicit` (tokens no fragment da URL — sem PKCE/refresh) e qualquer valor fora
|
|
@@ -70,10 +88,12 @@ const grantTypeArray = vine.array(vine.enum(ALLOWED_GRANT_TYPES));
|
|
|
70
88
|
*/
|
|
71
89
|
export const clientInputValidator = vine.compile(vine.object({
|
|
72
90
|
clientId: vine.string().trim().optional(),
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
91
|
+
// `web` (default) ou `native` (RFC 8252 — app mobile/desktop, sempre público).
|
|
92
|
+
applicationType: vine.enum(APPLICATION_TYPES).optional(),
|
|
93
|
+
// redirect/postLogout: URIs ABSOLUTAS (L10), http/https para client web;
|
|
94
|
+
// esquema privado/loopback só para `native` (ver `redirectUriRule`).
|
|
95
|
+
redirectUris: redirectUriArray.optional(),
|
|
96
|
+
postLogoutRedirectUris: redirectUriArray.optional(),
|
|
77
97
|
// grant_types restrito à allowlist; `implicit` e desconhecidos viram 422 (M10).
|
|
78
98
|
grantTypes: grantTypeArray.optional(),
|
|
79
99
|
grants: grantTypeArray.optional(),
|
|
@@ -82,14 +102,20 @@ export const clientInputValidator = vine.compile(vine.object({
|
|
|
82
102
|
backchannelLogoutUri: vine.string().trim().url(URL_OPTS).optional(),
|
|
83
103
|
backchannelLogoutSessionRequired: vine.boolean().optional(),
|
|
84
104
|
}));
|
|
85
|
-
/**
|
|
105
|
+
/**
|
|
106
|
+
* Mapeia o input validado para um {@link ClientInput} COMPLETO (create), com defaults.
|
|
107
|
+
* Client nativo sem `tokenEndpointAuthMethod` explícito vira público (`none`); um
|
|
108
|
+
* nativo confidencial explícito é recusado pelo service (422).
|
|
109
|
+
*/
|
|
86
110
|
export function clientCreateInput(v) {
|
|
111
|
+
const applicationType = v.applicationType ?? 'web';
|
|
87
112
|
return {
|
|
88
113
|
clientId: v.clientId?.trim() || undefined,
|
|
114
|
+
applicationType,
|
|
89
115
|
redirectUris: v.redirectUris ?? [],
|
|
90
116
|
postLogoutRedirectUris: v.postLogoutRedirectUris ?? [],
|
|
91
117
|
grantTypes: v.grantTypes ?? v.grants ?? [],
|
|
92
|
-
tokenEndpointAuthMethod: v.tokenEndpointAuthMethod ?? 'client_secret_basic',
|
|
118
|
+
tokenEndpointAuthMethod: v.tokenEndpointAuthMethod ?? (applicationType === 'native' ? 'none' : 'client_secret_basic'),
|
|
93
119
|
backchannelLogoutUri: v.backchannelLogoutUri || undefined,
|
|
94
120
|
backchannelLogoutSessionRequired: v.backchannelLogoutSessionRequired,
|
|
95
121
|
};
|
|
@@ -100,6 +126,8 @@ export function clientCreateInput(v) {
|
|
|
100
126
|
*/
|
|
101
127
|
export function clientPartialInput(v) {
|
|
102
128
|
const out = {};
|
|
129
|
+
if (v.applicationType !== undefined)
|
|
130
|
+
out.applicationType = v.applicationType;
|
|
103
131
|
if (v.redirectUris !== undefined)
|
|
104
132
|
out.redirectUris = v.redirectUris;
|
|
105
133
|
if (v.postLogoutRedirectUris !== undefined)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Registra a conta autenticada via bearer nesta request. */
|
|
2
|
+
export declare function setBearerAccountId(ctx: object, accountId: string): void;
|
|
3
|
+
/** Esquece a conta bearer desta request (falha de autenticação). */
|
|
4
|
+
export declare function clearBearerAccountId(ctx: object): void;
|
|
5
|
+
/**
|
|
6
|
+
* Id da conta autenticada via bearer nesta request, ou `null` quando nenhum
|
|
7
|
+
* `oidcBearerGuard` autenticou (ainda) a request.
|
|
8
|
+
*/
|
|
9
|
+
export declare function bearerAccountId(ctx: object): string | null;
|
|
10
|
+
/**
|
|
11
|
+
* Impersonation carregada pelo access token bearer desta request: o ator
|
|
12
|
+
* (`act.sub`, o admin) e a expiração do token. Gravada pelo `oidcBearerGuard`
|
|
13
|
+
* só quando o token é de impersonation; lida por `impersonationState` e
|
|
14
|
+
* `realAccountId` quando não há sessão.
|
|
15
|
+
*/
|
|
16
|
+
export interface BearerImpersonation {
|
|
17
|
+
actorId: string;
|
|
18
|
+
/** Epoch em segundos do `exp` do token, quando conhecido. */
|
|
19
|
+
exp: number | null;
|
|
20
|
+
/** `jti` do token trocado, quando conhecido (correlaciona com a auditoria). */
|
|
21
|
+
jti: string | null;
|
|
22
|
+
}
|
|
23
|
+
/** Registra que o token bearer desta request é de impersonation. */
|
|
24
|
+
export declare function setBearerImpersonation(ctx: object, value: BearerImpersonation): void;
|
|
25
|
+
/** Impersonation do token bearer desta request, ou `null`. */
|
|
26
|
+
export declare function bearerImpersonation(ctx: object): BearerImpersonation | null;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conta autenticada por `Authorization: Bearer` NESTA request — gravada pelo
|
|
3
|
+
* `oidcBearerGuard` quando ele autentica e lida pelo `getAccountId` como
|
|
4
|
+
* fallback da sessão.
|
|
5
|
+
*
|
|
6
|
+
* Um `WeakMap` keyed pelo próprio `HttpContext` (e não uma prop em `ctx`): nada
|
|
7
|
+
* de augmentation de `HttpContext` vazando pelo barrel (ver o porquê em
|
|
8
|
+
* `account_session_key.ts`), e o valor morre junto com a request.
|
|
9
|
+
*
|
|
10
|
+
* Mantenha este arquivo sem imports: `console_session.ts` (reexportado pelo
|
|
11
|
+
* barrel) depende dele.
|
|
12
|
+
*/
|
|
13
|
+
const bearerAccounts = new WeakMap();
|
|
14
|
+
const bearerImpersonations = new WeakMap();
|
|
15
|
+
/** Registra a conta autenticada via bearer nesta request. */
|
|
16
|
+
export function setBearerAccountId(ctx, accountId) {
|
|
17
|
+
bearerAccounts.set(ctx, accountId);
|
|
18
|
+
}
|
|
19
|
+
/** Esquece a conta bearer desta request (falha de autenticação). */
|
|
20
|
+
export function clearBearerAccountId(ctx) {
|
|
21
|
+
bearerAccounts.delete(ctx);
|
|
22
|
+
bearerImpersonations.delete(ctx);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Id da conta autenticada via bearer nesta request, ou `null` quando nenhum
|
|
26
|
+
* `oidcBearerGuard` autenticou (ainda) a request.
|
|
27
|
+
*/
|
|
28
|
+
export function bearerAccountId(ctx) {
|
|
29
|
+
return bearerAccounts.get(ctx) ?? null;
|
|
30
|
+
}
|
|
31
|
+
/** Registra que o token bearer desta request é de impersonation. */
|
|
32
|
+
export function setBearerImpersonation(ctx, value) {
|
|
33
|
+
bearerImpersonations.set(ctx, value);
|
|
34
|
+
}
|
|
35
|
+
/** Impersonation do token bearer desta request, ou `null`. */
|
|
36
|
+
export function bearerImpersonation(ctx) {
|
|
37
|
+
return bearerImpersonations.get(ctx) ?? null;
|
|
38
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regras de metadata de client que dependem do TIPO de aplicação (OIDC
|
|
3
|
+
* Registration §2, `application_type`) — compartilhadas pelo
|
|
4
|
+
* {@link AdminClientsService} (console admin, Admin REST API, CLI, import) e pelo
|
|
5
|
+
* validator Vine de `admin_validators.ts`.
|
|
6
|
+
*
|
|
7
|
+
* Espelham as regras que o próprio oidc-provider aplica ao montar o `Client`
|
|
8
|
+
* (`helpers/client_schema.js#redirectUris`). Checamos ANTES de persistir porque o
|
|
9
|
+
* provider só valida no `Client.find` — um client inválido gravado no adapter
|
|
10
|
+
* vira "client não encontrado" em runtime, sem mensagem útil para quem o criou.
|
|
11
|
+
*
|
|
12
|
+
* `native` segue a RFC 8252 (OAuth 2.0 for Native Apps):
|
|
13
|
+
* - esquema privado (`com.example.app:/callback`, `myapp://auth`) — §7.1;
|
|
14
|
+
* - https "claimed" (universal links / app links), nunca em loopback — §7.2;
|
|
15
|
+
* - loopback `http://127.0.0.1:<qualquer porta>/…` (e `localhost`/`[::1]`) — §7.3.
|
|
16
|
+
* O provider ignora a porta do loopback na comparação do redirect (§7.3), então
|
|
17
|
+
* basta registrar `http://127.0.0.1/callback`.
|
|
18
|
+
* - client PÚBLICO: um segredo embarcado num app instalado não é segredo (§8.5).
|
|
19
|
+
* PKCE (S256) já é obrigatório para todo client neste issuer.
|
|
20
|
+
*/
|
|
21
|
+
/** Tipo de aplicação do client. `web` é o default histórico. */
|
|
22
|
+
export type ApplicationType = 'web' | 'native';
|
|
23
|
+
export declare const APPLICATION_TYPES: readonly ['web', 'native'];
|
|
24
|
+
/**
|
|
25
|
+
* Metadata inválida para o tipo de client (ex.: client nativo com secret, redirect
|
|
26
|
+
* `myapp://` num client web). Os controllers traduzem em 422.
|
|
27
|
+
*/
|
|
28
|
+
export declare class ClientMetadataError extends Error {
|
|
29
|
+
constructor(message: string);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Problema de UMA redirect URI para o tipo de client, ou `null` quando ok.
|
|
33
|
+
*
|
|
34
|
+
* `applicationType` ausente = "ainda não sei" (PATCH que não mandou o tipo): só as
|
|
35
|
+
* regras universais (URI absoluta, sem fragmento, esquema não proibido). A regra
|
|
36
|
+
* por tipo roda depois, no service, com o tipo efetivo do client.
|
|
37
|
+
*/
|
|
38
|
+
export declare function redirectUriProblem(uri: string, applicationType?: ApplicationType): string | null;
|
|
39
|
+
/** Entrada mínima para {@link assertClientMetadata}. */
|
|
40
|
+
export interface ClientMetadataInput {
|
|
41
|
+
applicationType: ApplicationType;
|
|
42
|
+
tokenEndpointAuthMethod: string;
|
|
43
|
+
grantTypes: string[];
|
|
44
|
+
redirectUris: string[];
|
|
45
|
+
postLogoutRedirectUris: string[];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Valida a combinação tipo × autenticação × grants × redirects. Lança
|
|
49
|
+
* {@link ClientMetadataError} na primeira violação.
|
|
50
|
+
*/
|
|
51
|
+
export declare function assertClientMetadata(input: ClientMetadataInput): void;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regras de metadata de client que dependem do TIPO de aplicação (OIDC
|
|
3
|
+
* Registration §2, `application_type`) — compartilhadas pelo
|
|
4
|
+
* {@link AdminClientsService} (console admin, Admin REST API, CLI, import) e pelo
|
|
5
|
+
* validator Vine de `admin_validators.ts`.
|
|
6
|
+
*
|
|
7
|
+
* Espelham as regras que o próprio oidc-provider aplica ao montar o `Client`
|
|
8
|
+
* (`helpers/client_schema.js#redirectUris`). Checamos ANTES de persistir porque o
|
|
9
|
+
* provider só valida no `Client.find` — um client inválido gravado no adapter
|
|
10
|
+
* vira "client não encontrado" em runtime, sem mensagem útil para quem o criou.
|
|
11
|
+
*
|
|
12
|
+
* `native` segue a RFC 8252 (OAuth 2.0 for Native Apps):
|
|
13
|
+
* - esquema privado (`com.example.app:/callback`, `myapp://auth`) — §7.1;
|
|
14
|
+
* - https "claimed" (universal links / app links), nunca em loopback — §7.2;
|
|
15
|
+
* - loopback `http://127.0.0.1:<qualquer porta>/…` (e `localhost`/`[::1]`) — §7.3.
|
|
16
|
+
* O provider ignora a porta do loopback na comparação do redirect (§7.3), então
|
|
17
|
+
* basta registrar `http://127.0.0.1/callback`.
|
|
18
|
+
* - client PÚBLICO: um segredo embarcado num app instalado não é segredo (§8.5).
|
|
19
|
+
* PKCE (S256) já é obrigatório para todo client neste issuer.
|
|
20
|
+
*/
|
|
21
|
+
export const APPLICATION_TYPES = ['web', 'native'];
|
|
22
|
+
/** Hosts loopback reconhecidos (mesma lista do oidc-provider). */
|
|
23
|
+
const LOOPBACKS = new Set(['localhost', '127.0.0.1', '[::1]']);
|
|
24
|
+
/** Esquemas NUNCA aceitos como redirect, qualquer que seja o tipo de client. */
|
|
25
|
+
const FORBIDDEN_SCHEMES = new Set([
|
|
26
|
+
'javascript:',
|
|
27
|
+
'vbscript:',
|
|
28
|
+
'data:',
|
|
29
|
+
'blob:',
|
|
30
|
+
'file:',
|
|
31
|
+
'about:',
|
|
32
|
+
]);
|
|
33
|
+
/**
|
|
34
|
+
* Metadata inválida para o tipo de client (ex.: client nativo com secret, redirect
|
|
35
|
+
* `myapp://` num client web). Os controllers traduzem em 422.
|
|
36
|
+
*/
|
|
37
|
+
export class ClientMetadataError extends Error {
|
|
38
|
+
constructor(message) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = 'ClientMetadataError';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Problema de UMA redirect URI para o tipo de client, ou `null` quando ok.
|
|
45
|
+
*
|
|
46
|
+
* `applicationType` ausente = "ainda não sei" (PATCH que não mandou o tipo): só as
|
|
47
|
+
* regras universais (URI absoluta, sem fragmento, esquema não proibido). A regra
|
|
48
|
+
* por tipo roda depois, no service, com o tipo efetivo do client.
|
|
49
|
+
*/
|
|
50
|
+
export function redirectUriProblem(uri, applicationType) {
|
|
51
|
+
const parsed = URL.parse(uri);
|
|
52
|
+
if (!parsed)
|
|
53
|
+
return 'deve ser uma URI absoluta';
|
|
54
|
+
if (uri.includes('#'))
|
|
55
|
+
return 'não pode conter fragmento (#)';
|
|
56
|
+
const { protocol, hostname } = parsed;
|
|
57
|
+
if (FORBIDDEN_SCHEMES.has(protocol))
|
|
58
|
+
return `não pode usar o esquema ${protocol.slice(0, -1)}`;
|
|
59
|
+
if (applicationType === 'web') {
|
|
60
|
+
if (protocol !== 'http:' && protocol !== 'https:') {
|
|
61
|
+
return 'client web só aceita URIs http/https (use applicationType "native" para apps mobile)';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else if (applicationType === 'native') {
|
|
65
|
+
if (protocol === 'http:' && !LOOPBACKS.has(hostname)) {
|
|
66
|
+
return 'client nativo com http só aceita loopback (127.0.0.1, [::1] ou localhost)';
|
|
67
|
+
}
|
|
68
|
+
if (protocol === 'https:' && LOOPBACKS.has(hostname)) {
|
|
69
|
+
return 'client nativo com https "claimed" não pode usar host loopback';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Valida a combinação tipo × autenticação × grants × redirects. Lança
|
|
76
|
+
* {@link ClientMetadataError} na primeira violação.
|
|
77
|
+
*/
|
|
78
|
+
export function assertClientMetadata(input) {
|
|
79
|
+
const type = input.applicationType;
|
|
80
|
+
if (type === 'native') {
|
|
81
|
+
if (input.tokenEndpointAuthMethod !== 'none') {
|
|
82
|
+
throw new ClientMetadataError('client nativo precisa ser público (tokenEndpointAuthMethod "none", sem secret) — RFC 8252 §8.5');
|
|
83
|
+
}
|
|
84
|
+
if (input.grantTypes.includes('client_credentials')) {
|
|
85
|
+
throw new ClientMetadataError('client nativo não pode usar client_credentials (não há secret para autenticar)');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
for (const [label, uris] of [
|
|
89
|
+
['redirectUris', input.redirectUris],
|
|
90
|
+
['postLogoutRedirectUris', input.postLogoutRedirectUris],
|
|
91
|
+
]) {
|
|
92
|
+
for (const uri of uris) {
|
|
93
|
+
const problem = redirectUriProblem(uri, type);
|
|
94
|
+
if (problem)
|
|
95
|
+
throw new ClientMetadataError(`${label}: "${uri}" ${problem}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -9,8 +9,18 @@ import type { HttpContext } from '@adonisjs/core/http';
|
|
|
9
9
|
* sessão mudar.
|
|
10
10
|
*/
|
|
11
11
|
/**
|
|
12
|
-
* Retorna o id da conta que o request está representando
|
|
13
|
-
*
|
|
12
|
+
* Retorna o id da conta que o request está representando (ou `null` quando não
|
|
13
|
+
* há identidade).
|
|
14
|
+
*
|
|
15
|
+
* Fonte, nesta ordem:
|
|
16
|
+
* 1. a sessão do console (`ACCOUNT_SESSION_KEY`) — o comportamento de sempre;
|
|
17
|
+
* 2. SEM sessão logada: a conta que um `oidcBearerGuard` autenticou NESTA
|
|
18
|
+
* request via `Authorization: Bearer` (app nativo, SPA, serviço).
|
|
19
|
+
*
|
|
20
|
+
* O fallback (2) só existe depois que o guard bearer RODOU na request (ex.:
|
|
21
|
+
* `middleware.auth({ guards: ['web', 'api'] })` ou `auth.authenticateUsing`);
|
|
22
|
+
* um app que não usa o guard bearer continua exatamente como antes. A sessão
|
|
23
|
+
* sempre ganha: request com cookie de sessão E bearer responde pela sessão.
|
|
14
24
|
*
|
|
15
25
|
* ATENÇÃO: com impersonation ativa isto é a conta PERSONIFICADA, não o admin
|
|
16
26
|
* que a personificou. É o comportamento certo para "como qual conta este
|
|
@@ -50,7 +60,8 @@ export declare function getAccountId(ctx: HttpContext): string | null;
|
|
|
50
60
|
*/
|
|
51
61
|
export declare function realAccountId(ctx: HttpContext): string | null;
|
|
52
62
|
/**
|
|
53
|
-
* `true` quando o request carrega uma sessão de conta do console.
|
|
63
|
+
* `true` quando o request carrega uma sessão de conta do console. Só a SESSÃO:
|
|
64
|
+
* uma identidade bearer (`oidcBearerGuard`) não conta — use `getAccountId`.
|
|
54
65
|
*/
|
|
55
66
|
export declare function hasAccountSession(ctx: HttpContext): boolean;
|
|
56
67
|
/**
|