@adonis-agora/authkit-server 0.73.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/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 +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/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/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 +11 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -11,6 +11,10 @@ wrapper around [`oidc-provider`](https://github.com/panva/node-oidc-provider).
|
|
|
11
11
|
- **Passwordless** — magic-link email login and passkey-first login.
|
|
12
12
|
- **Protocol extensions** — Device Flow (RFC 8628), DPoP (RFC 9449), PAR (RFC 9126), step-up
|
|
13
13
|
auth via `acr_values`, Dynamic Client Registration (RFC 7591/7592).
|
|
14
|
+
- **Native apps** — RFC 8252 native clients (`authkit:clients:create --native`: private-use
|
|
15
|
+
scheme / claimed https / loopback redirects, always public + PKCE) and the `@adonisjs/auth`
|
|
16
|
+
guards `oidcRpGuard` (session) and `oidcBearerGuard` (`Authorization: Bearer` access tokens,
|
|
17
|
+
in-process or remote resource server).
|
|
14
18
|
- **Admin console** — user CRUD (+ disable), client CRUD, sessions, audit log (opt-in,
|
|
15
19
|
role-gated).
|
|
16
20
|
- **Account console** — self-service apps/consent, security (password/email/sessions),
|
|
@@ -10,6 +10,7 @@ import type { CommandOptions } from '@adonisjs/core/types/ace';
|
|
|
10
10
|
* node ace authkit:clients:create --client-id=my-spa --redirect-uri=https://app/cb --public
|
|
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
|
+
* node ace authkit:clients:create --client-id=my-mobile --native --redirect-uri=com.example.app:/oauth
|
|
13
14
|
*/
|
|
14
15
|
export default class AuthkitClientsCreate extends BaseCommand {
|
|
15
16
|
static commandName: string;
|
|
@@ -21,6 +22,7 @@ export default class AuthkitClientsCreate extends BaseCommand {
|
|
|
21
22
|
postLogoutUri?: string[];
|
|
22
23
|
grant?: string[];
|
|
23
24
|
public?: boolean;
|
|
25
|
+
native?: boolean;
|
|
24
26
|
backchannelLogoutUri?: string;
|
|
25
27
|
json?: boolean;
|
|
26
28
|
run(): Promise<void>;
|
|
@@ -15,6 +15,7 @@ import { BaseCommand, flags } from '@adonisjs/core/ace';
|
|
|
15
15
|
* node ace authkit:clients:create --client-id=my-spa --redirect-uri=https://app/cb --public
|
|
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
|
+
* node ace authkit:clients:create --client-id=my-mobile --native --redirect-uri=com.example.app:/oauth
|
|
18
19
|
*/
|
|
19
20
|
export default class AuthkitClientsCreate extends BaseCommand {
|
|
20
21
|
static commandName = 'authkit:clients:create';
|
|
@@ -31,10 +32,15 @@ export default class AuthkitClientsCreate extends BaseCommand {
|
|
|
31
32
|
' --post-logout-uri URI(s) de post-logout redirect.',
|
|
32
33
|
' --grant Grant types. Default: authorization_code + refresh_token.',
|
|
33
34
|
'',
|
|
35
|
+
'App nativo (React Native/Expo, iOS, Android, desktop — RFC 8252): use --native.',
|
|
36
|
+
'Implica client público (sem secret; PKCE já é obrigatório) e aceita redirect de',
|
|
37
|
+
'esquema privado (com.example.app:/oauth), https claimed e loopback http://127.0.0.1.',
|
|
38
|
+
'',
|
|
34
39
|
'Exemplos:',
|
|
35
40
|
' node ace authkit:clients:create --client-id=my-spa --redirect-uri=https://app/cb --public',
|
|
36
41
|
' node ace authkit:clients:create --redirect-uri=https://app/cb --backchannel-logout-uri=https://app/bc',
|
|
37
42
|
' node ace authkit:clients:create --client-id=my-app --redirect-uri=https://a/cb --redirect-uri=https://b/cb --json',
|
|
43
|
+
' node ace authkit:clients:create --client-id=my-mobile --native --redirect-uri=com.example.app:/oauth',
|
|
38
44
|
];
|
|
39
45
|
static options = { startApp: true };
|
|
40
46
|
async run() {
|
|
@@ -48,17 +54,31 @@ export default class AuthkitClientsCreate extends BaseCommand {
|
|
|
48
54
|
const { AdminClientsService } = await import('../src/host/admin_clients_service.js');
|
|
49
55
|
const svc = new AdminClientsService(service);
|
|
50
56
|
const grantTypes = this.grant && this.grant.length > 0 ? this.grant : ['authorization_code', 'refresh_token'];
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
57
|
+
// App nativo é sempre público (RFC 8252 §8.5): `--native` implica `--public`.
|
|
58
|
+
const isPublic = !!this.public || !!this.native;
|
|
59
|
+
const applicationType = this.native ? 'native' : 'web';
|
|
60
|
+
const tokenEndpointAuthMethod = isPublic ? 'none' : 'client_secret_basic';
|
|
61
|
+
const { ClientMetadataError } = await import('../src/host/client_metadata.js');
|
|
62
|
+
let created;
|
|
63
|
+
try {
|
|
64
|
+
created = await svc.create({
|
|
65
|
+
clientId: this.clientId,
|
|
66
|
+
applicationType,
|
|
67
|
+
redirectUris,
|
|
68
|
+
postLogoutRedirectUris: this.postLogoutUri ?? [],
|
|
69
|
+
grantTypes,
|
|
70
|
+
tokenEndpointAuthMethod,
|
|
71
|
+
backchannelLogoutUri: this.backchannelLogoutUri,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
if (err instanceof ClientMetadataError) {
|
|
76
|
+
this.logger.logError(`❌ ${err.message}`);
|
|
77
|
+
this.exitCode = 1;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
62
82
|
if (this.json) {
|
|
63
83
|
const out = {
|
|
64
84
|
clientId: created.clientId,
|
|
@@ -66,7 +86,8 @@ export default class AuthkitClientsCreate extends BaseCommand {
|
|
|
66
86
|
postLogoutRedirectUris: this.postLogoutUri ?? [],
|
|
67
87
|
grantTypes,
|
|
68
88
|
tokenEndpointAuthMethod,
|
|
69
|
-
|
|
89
|
+
applicationType,
|
|
90
|
+
confidential: !isPublic,
|
|
70
91
|
};
|
|
71
92
|
if (created.clientSecret)
|
|
72
93
|
out.clientSecret = created.clientSecret;
|
|
@@ -78,7 +99,9 @@ export default class AuthkitClientsCreate extends BaseCommand {
|
|
|
78
99
|
this.logger.success(`Client criado: ${created.clientId}`);
|
|
79
100
|
this.logger.info(` redirect_uris: ${redirectUris.join(', ')}`);
|
|
80
101
|
this.logger.info(` grant_types: ${grantTypes.join(', ')}`);
|
|
81
|
-
this.logger.info(` type: ${
|
|
102
|
+
this.logger.info(` type: ${isPublic ? 'publico (sem secret)' : 'confidencial'}`);
|
|
103
|
+
if (this.native)
|
|
104
|
+
this.logger.info(' application: native (RFC 8252)');
|
|
82
105
|
if (this.backchannelLogoutUri) {
|
|
83
106
|
this.logger.info(` backchannel: ${this.backchannelLogoutUri}`);
|
|
84
107
|
}
|
|
@@ -109,6 +132,11 @@ __decorate([
|
|
|
109
132
|
description: 'Cria um client público (sem secret; token_endpoint_auth_method=none). Default: false (confidencial).',
|
|
110
133
|
})
|
|
111
134
|
], AuthkitClientsCreate.prototype, "public", void 0);
|
|
135
|
+
__decorate([
|
|
136
|
+
flags.boolean({
|
|
137
|
+
description: 'Client de app nativo (application_type=native, RFC 8252): aceita redirect de esquema privado/loopback. Implica --public.',
|
|
138
|
+
})
|
|
139
|
+
], AuthkitClientsCreate.prototype, "native", void 0);
|
|
112
140
|
__decorate([
|
|
113
141
|
flags.string({
|
|
114
142
|
description: 'Endpoint de OIDC Back-Channel Logout do RP (POST de logout_token).',
|
|
@@ -109,6 +109,13 @@
|
|
|
109
109
|
"type": "boolean",
|
|
110
110
|
"description": "Cria um client público (sem secret; token_endpoint_auth_method=none)."
|
|
111
111
|
},
|
|
112
|
+
{
|
|
113
|
+
"name": "native",
|
|
114
|
+
"flagName": "native",
|
|
115
|
+
"required": false,
|
|
116
|
+
"type": "boolean",
|
|
117
|
+
"description": "Client de app nativo (application_type=native, RFC 8252): aceita redirect de esquema privado/loopback. Implica --public."
|
|
118
|
+
},
|
|
112
119
|
{
|
|
113
120
|
"name": "backchannelLogoutUri",
|
|
114
121
|
"flagName": "backchannel-logout-uri",
|
package/build/index.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export type { AdminApiConfigInput, AdminConfigInput, AuthHostRenderer, AuthMetho
|
|
|
16
16
|
export { adapters, defineConfig, resolveAdmin, resolveAdminApi, resolveAuthMethodsConfig, resolveDynamicRegistration, resolveLogin, resolveNotifications, resolveOrganizations, resolvePasswordless, resolveRateLimit, resolveRegistration, resolveWebauthn, toSeconds, } from './src/define_config.js';
|
|
17
17
|
export type { EventsConfigInput, ResolvedEventsConfig } from './src/events/dispatcher.js';
|
|
18
18
|
export { buildWebhookBody, composeAuditSink, resolveEvents, signWebhookBody, } from './src/events/dispatcher.js';
|
|
19
|
+
export type { AccessTokenVerifier, InProcessIssuer, IssueAccessTokenOptions, RemoteAccessTokenVerifierOptions, VerifiedAccessToken, } from './src/host/access_token_verifier.js';
|
|
20
|
+
export { inProcessAccessTokenVerifier, remoteAccessTokenVerifier, } from './src/host/access_token_verifier.js';
|
|
19
21
|
export type { DeletionActor, DeletionResult, } from './src/host/account_deletion_service.js';
|
|
20
22
|
export { AccountDeletionService } from './src/host/account_deletion_service.js';
|
|
21
23
|
export type { AccountExport } from './src/host/account_export_service.js';
|
|
@@ -59,6 +61,8 @@ export type { BotProtectionAction, BotProtectionConfigInput, BotProtectionSettin
|
|
|
59
61
|
export { botProtectionApplies, DEFAULT_BOT_TOKEN_FIELDS, extractBotToken, guardBotProtection, resolveBotProtection, resolveEffectiveBotProtection, verifyBotProtection, } from './src/host/bot_protection.js';
|
|
60
62
|
export type { BrandingConfig, ClientBrand } from './src/host/branding.js';
|
|
61
63
|
export { brandFor, isFirstParty, isFirstPartyClient } from './src/host/branding.js';
|
|
64
|
+
export type { ApplicationType } from './src/host/client_metadata.js';
|
|
65
|
+
export { assertClientMetadata, ClientMetadataError, redirectUriProblem, } from './src/host/client_metadata.js';
|
|
62
66
|
export type { PolicyRouteOption } from './src/host/config_locks.js';
|
|
63
67
|
export { deriveLockedSettingKeys, isSettingLocked, lockedSettingKeys, POLICY_ROUTE_OPTIONS, resetLockedSettingKeys, SettingLockedError, setLockedSettingKeys, } from './src/host/config_locks.js';
|
|
64
68
|
export { consoleLoginUrl, getAccountId, hasAccountSession, realAccountId, } from './src/host/console_session.js';
|
|
@@ -76,6 +80,8 @@ export type { ImpersonationStartErrorCode, ImpersonationState, StartImpersonatio
|
|
|
76
80
|
export { ImpersonationStartError, impersonationState, refreshAccessToken, rememberAccessToken, rememberRefreshToken, startImpersonation, stopImpersonation, } from './src/host/impersonation_session.js';
|
|
77
81
|
export type { KeysStatus as ServerKeysStatus } from './src/host/key_rotation_actions.js';
|
|
78
82
|
export { buildKeysStatus, rotateNow } from './src/host/key_rotation_actions.js';
|
|
83
|
+
export type { OidcBearerGuardEvents, OidcBearerGuardOptions, OidcBearerGuardPolicy, } from './src/host/oidc_bearer_guard.js';
|
|
84
|
+
export { OidcBearerGuard, oidcBearerGuard } from './src/host/oidc_bearer_guard.js';
|
|
79
85
|
export type { OidcRpGuardEvents, OidcRpGuardOptions } from './src/host/oidc_rp_guard.js';
|
|
80
86
|
export { OidcRpGuard, oidcRpGuard } from './src/host/oidc_rp_guard.js';
|
|
81
87
|
export { evaluateLoginOtp, generateOtpCode, OTP_LOGIN_DEFAULTS, type OtpLoginConfigInput, type OtpVerifyOutcome, type ResolvedOtpLoginConfig, resolveOtpLoginConfig, } from './src/host/otp_login.js';
|
package/build/index.js
CHANGED
|
@@ -10,6 +10,7 @@ export { lucidStores } from './src/accounts/lucid_stores.js';
|
|
|
10
10
|
export { lucidAuditSink } from './src/audit/lucid_audit_sink.js';
|
|
11
11
|
export { adapters, defineConfig, resolveAdmin, resolveAdminApi, resolveAuthMethodsConfig, resolveDynamicRegistration, resolveLogin, resolveNotifications, resolveOrganizations, resolvePasswordless, resolveRateLimit, resolveRegistration, resolveWebauthn, toSeconds, } from './src/define_config.js';
|
|
12
12
|
export { buildWebhookBody, composeAuditSink, resolveEvents, signWebhookBody, } from './src/events/dispatcher.js';
|
|
13
|
+
export { inProcessAccessTokenVerifier, remoteAccessTokenVerifier, } from './src/host/access_token_verifier.js';
|
|
13
14
|
export { AccountDeletionService } from './src/host/account_deletion_service.js';
|
|
14
15
|
export { AccountExportService } from './src/host/account_export_service.js';
|
|
15
16
|
/**
|
|
@@ -46,6 +47,7 @@ export { computeAdminStats } from './src/host/admin_stats_service.js';
|
|
|
46
47
|
export { authkitUserProvider } from './src/host/adonis_auth_user_provider.js';
|
|
47
48
|
export { botProtectionApplies, DEFAULT_BOT_TOKEN_FIELDS, extractBotToken, guardBotProtection, resolveBotProtection, resolveEffectiveBotProtection, verifyBotProtection, } from './src/host/bot_protection.js';
|
|
48
49
|
export { brandFor, isFirstParty, isFirstPartyClient } from './src/host/branding.js';
|
|
50
|
+
export { assertClientMetadata, ClientMetadataError, redirectUriProblem, } from './src/host/client_metadata.js';
|
|
49
51
|
export { deriveLockedSettingKeys, isSettingLocked, lockedSettingKeys, POLICY_ROUTE_OPTIONS, resetLockedSettingKeys, SettingLockedError, setLockedSettingKeys, } from './src/host/config_locks.js';
|
|
50
52
|
export { consoleLoginUrl, getAccountId, hasAccountSession, realAccountId, } from './src/host/console_session.js';
|
|
51
53
|
export { authkitCsrfExceptions } from './src/host/csrf.js';
|
|
@@ -60,6 +62,11 @@ export { buildImpersonationPanel } from './src/host/impersonation.js';
|
|
|
60
62
|
export { ImpersonationStartError, impersonationState, refreshAccessToken, rememberAccessToken, rememberRefreshToken, startImpersonation, stopImpersonation, } from './src/host/impersonation_session.js';
|
|
61
63
|
// Key rotation actions — shared between the Admin REST API controller and the SDK embedded driver.
|
|
62
64
|
export { buildKeysStatus, rotateNow } from './src/host/key_rotation_actions.js';
|
|
65
|
+
// @adonisjs/auth integration (opt-in) — guard de access token (Authorization:
|
|
66
|
+
// Bearer) pra APIs chamadas por apps nativos (RFC 8252), SPAs e serviços. Par do
|
|
67
|
+
// oidcRpGuard para clientes sem sessão web; verifica in-process (issuer embarcado)
|
|
68
|
+
// ou como resource server remoto (JWKS / introspecção).
|
|
69
|
+
export { OidcBearerGuard, oidcBearerGuard } from './src/host/oidc_bearer_guard.js';
|
|
63
70
|
// @adonisjs/auth integration (opt-in) — guard pra Relying Parties OIDC.
|
|
64
71
|
// O RP não autentica ninguém (sem senha); a identidade vem da sessão gravada
|
|
65
72
|
// pelo callback OIDC. Plugado em config/auth.ts, faz ctx.auth.user funcionar
|
|
@@ -44,6 +44,7 @@ export async function importClients(clients, svc, options = {}) {
|
|
|
44
44
|
}
|
|
45
45
|
// Determina o auth method a partir do config estático.
|
|
46
46
|
const authMethod = client.tokenEndpointAuthMethod ?? (client.clientSecret ? 'client_secret_basic' : 'none');
|
|
47
|
+
const applicationType = client.applicationType ?? 'web';
|
|
47
48
|
try {
|
|
48
49
|
// Cria via AdminClientsService, que usa o mesmo caminho do console admin / registro dinâmico.
|
|
49
50
|
// Para preservar o client_secret do config estático, escrevemos diretamente via adapter —
|
|
@@ -52,6 +53,7 @@ export async function importClients(clients, svc, options = {}) {
|
|
|
52
53
|
const preservedSecret = client.clientSecret;
|
|
53
54
|
const created = await svc.createWithSecret({
|
|
54
55
|
clientId,
|
|
56
|
+
applicationType,
|
|
55
57
|
redirectUris: client.redirectUris ?? [],
|
|
56
58
|
postLogoutRedirectUris: client.postLogoutRedirectUris ?? [],
|
|
57
59
|
grantTypes: client.grants ?? ['authorization_code', 'refresh_token'],
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Access token já verificado — a forma comum que o `oidcBearerGuard` consome,
|
|
3
|
+
* qualquer que seja o modo de verificação (in-process ou resource server remoto)
|
|
4
|
+
* e o formato do token (opaco ou JWT RFC 9068).
|
|
5
|
+
*/
|
|
6
|
+
export interface VerifiedAccessToken {
|
|
7
|
+
/** `opaque` (referência no adapter / introspecção) ou `jwt` (RFC 9068, auto-contido). */
|
|
8
|
+
format: 'opaque' | 'jwt';
|
|
9
|
+
/** `sub` — o id da conta. */
|
|
10
|
+
sub: string;
|
|
11
|
+
/** Client que recebeu o token. */
|
|
12
|
+
clientId: string | null;
|
|
13
|
+
/** Escopos concedidos. */
|
|
14
|
+
scopes: string[];
|
|
15
|
+
/** Audience(s) do token (resource indicators, RFC 8707). Vazio = sem `aud`. */
|
|
16
|
+
audience: string[];
|
|
17
|
+
/** Expiração (epoch em segundos), quando conhecida. */
|
|
18
|
+
exp: number | null;
|
|
19
|
+
/** Id do token (`jti`), quando conhecido. */
|
|
20
|
+
jti: string | null;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Estratégia de verificação de access token plugável no `oidcBearerGuard`.
|
|
24
|
+
* `verify` devolve `null` para QUALQUER token não aceitável (desconhecido,
|
|
25
|
+
* expirado, revogado, assinatura inválida, sender-constrained) — o guard não
|
|
26
|
+
* distingue os motivos na resposta (401 genérico).
|
|
27
|
+
*/
|
|
28
|
+
export interface AccessTokenVerifier {
|
|
29
|
+
verify(token: string): Promise<VerifiedAccessToken | null>;
|
|
30
|
+
/**
|
|
31
|
+
* Emite um access token de verdade para `accountId` — usado pelo
|
|
32
|
+
* `authenticateAsClient` do guard em testes. Só existe no modo in-process.
|
|
33
|
+
*/
|
|
34
|
+
issue?(accountId: string, options?: IssueAccessTokenOptions): Promise<string>;
|
|
35
|
+
}
|
|
36
|
+
/** Opções de {@link AccessTokenVerifier.issue}. */
|
|
37
|
+
export interface IssueAccessTokenOptions {
|
|
38
|
+
/** Client "dono" do token. Default: `'authkit-test-client'`. */
|
|
39
|
+
clientId?: string;
|
|
40
|
+
/** Escopos do token. Default: `['openid']`. */
|
|
41
|
+
scopes?: string[];
|
|
42
|
+
}
|
|
43
|
+
/** O mínimo do `OidcService` que a verificação in-process usa. */
|
|
44
|
+
export interface InProcessIssuer {
|
|
45
|
+
readonly provider: any;
|
|
46
|
+
readonly publicJwks: {
|
|
47
|
+
keys: Record<string, any>[];
|
|
48
|
+
};
|
|
49
|
+
readonly config: {
|
|
50
|
+
issuer: string;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Verificação IN-PROCESS: o app hospeda o issuer (`authkit.server`).
|
|
55
|
+
*
|
|
56
|
+
* - token opaco → `AccessToken.find` do oidc-provider (mesma rota do
|
|
57
|
+
* {@link TokenVerifyService}). Revogação (`/revoke`, logout, revogação de
|
|
58
|
+
* grant/sessão pelo admin) apaga o artefato, então revogado = não encontrado.
|
|
59
|
+
* - JWT (RFC 9068, `accessTokens.format: 'jwt'`) → assinatura contra o JWKS
|
|
60
|
+
* PÚBLICO do provider atual, `iss` e `typ: at+jwt`. JWT AT é auto-contido: o
|
|
61
|
+
* oidc-provider não o persiste, então ele vale até o `exp` mesmo após revogar o
|
|
62
|
+
* grant — mantenha o TTL do access token curto.
|
|
63
|
+
*
|
|
64
|
+
* `resolveIssuer` é chamado a cada verificação (e não guardado): o `OidcService`
|
|
65
|
+
* troca a instância do provider em `reloadKeys`/rotação de chaves.
|
|
66
|
+
*/
|
|
67
|
+
export declare function inProcessAccessTokenVerifier(resolveIssuer: () => Promise<InProcessIssuer>): AccessTokenVerifier;
|
|
68
|
+
/** Opções do modo resource server REMOTO. */
|
|
69
|
+
export interface RemoteAccessTokenVerifierOptions {
|
|
70
|
+
/** Issuer do authkit (o mesmo `issuer` do `config/authkit.ts` do IdP). */
|
|
71
|
+
issuer: string;
|
|
72
|
+
/**
|
|
73
|
+
* URL do JWKS. Default: o `jwks_uri` da discovery
|
|
74
|
+
* (`{issuer}/.well-known/openid-configuration`).
|
|
75
|
+
*/
|
|
76
|
+
jwksUri?: string;
|
|
77
|
+
/**
|
|
78
|
+
* Introspecção (RFC 7662) para tokens OPACOS, autenticada com as credenciais de
|
|
79
|
+
* um client CONFIDENCIAL do issuer (o resource server). Sem isto, só JWT access
|
|
80
|
+
* tokens (RFC 9068) são aceitos.
|
|
81
|
+
*/
|
|
82
|
+
introspection?: {
|
|
83
|
+
clientId: string;
|
|
84
|
+
clientSecret: string;
|
|
85
|
+
/** Default: o `introspection_endpoint` da discovery. */
|
|
86
|
+
endpoint?: string;
|
|
87
|
+
};
|
|
88
|
+
/** `fetch` alternativo (testes, proxy). Default: o global. */
|
|
89
|
+
fetch?: typeof fetch;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Verificação de RESOURCE SERVER remoto: a API NÃO hospeda o issuer.
|
|
93
|
+
*
|
|
94
|
+
* - JWT (RFC 9068) → assinatura contra o JWKS remoto (cacheado e recarregado
|
|
95
|
+
* pelo `jose` quando aparece um `kid` novo), `iss` e `typ: at+jwt`.
|
|
96
|
+
* - opaco → introspecção (RFC 7662) com client credentials; exige
|
|
97
|
+
* `active: true` e `token_type` Bearer (refresh token introspectado vem sem
|
|
98
|
+
* `token_type` e é recusado).
|
|
99
|
+
*/
|
|
100
|
+
export declare function remoteAccessTokenVerifier(options: RemoteAccessTokenVerifierOptions): AccessTokenVerifier;
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { createLocalJWKSet, createRemoteJWKSet, customFetch, errors as joseErrors, jwtVerify, } from 'jose';
|
|
2
|
+
import { errors as providerErrors } from 'oidc-provider';
|
|
3
|
+
/**
|
|
4
|
+
* Token recusado (assinatura, `iss`, `exp`, formato…) vira `null` → 401. Falha de
|
|
5
|
+
* infraestrutura (banco fora, rede, JWKS inalcançável) SOBE: responder
|
|
6
|
+
* `invalid_token` num apagão ensinaria o cliente a jogar fora um token bom.
|
|
7
|
+
*/
|
|
8
|
+
function rejectedJwt(error) {
|
|
9
|
+
return (error instanceof joseErrors.JOSEError &&
|
|
10
|
+
!(error instanceof joseErrors.JWKSTimeout) &&
|
|
11
|
+
!(error instanceof joseErrors.JWKSInvalid));
|
|
12
|
+
}
|
|
13
|
+
/** `header.payload.signature` em base64url — o formato compacto de um JWS. */
|
|
14
|
+
const JWS_COMPACT = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
|
|
15
|
+
function looksLikeJwt(token) {
|
|
16
|
+
return JWS_COMPACT.test(token);
|
|
17
|
+
}
|
|
18
|
+
function splitScope(scope) {
|
|
19
|
+
return typeof scope === 'string' ? scope.split(' ').filter(Boolean) : [];
|
|
20
|
+
}
|
|
21
|
+
function toAudience(aud) {
|
|
22
|
+
if (typeof aud === 'string')
|
|
23
|
+
return [aud];
|
|
24
|
+
if (Array.isArray(aud))
|
|
25
|
+
return aud.filter((a) => typeof a === 'string');
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* JWT access token (RFC 9068) → {@link VerifiedAccessToken}. Recusa token com
|
|
30
|
+
* `cnf` (DPoP / mTLS): ele é sender-constrained e apresentá-lo como Bearer puro,
|
|
31
|
+
* sem a prova de posse, é exatamente o que o `cnf` existe para impedir.
|
|
32
|
+
*/
|
|
33
|
+
function fromJwtPayload(payload) {
|
|
34
|
+
if (payload.cnf)
|
|
35
|
+
return null;
|
|
36
|
+
if (typeof payload.sub !== 'string' || !payload.sub)
|
|
37
|
+
return null;
|
|
38
|
+
const clientId = payload.client_id;
|
|
39
|
+
return {
|
|
40
|
+
format: 'jwt',
|
|
41
|
+
sub: payload.sub,
|
|
42
|
+
clientId: typeof clientId === 'string' ? clientId : null,
|
|
43
|
+
scopes: splitScope(payload.scope),
|
|
44
|
+
audience: toAudience(payload.aud),
|
|
45
|
+
exp: typeof payload.exp === 'number' ? payload.exp : null,
|
|
46
|
+
jti: typeof payload.jti === 'string' ? payload.jti : null,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Verificação IN-PROCESS: o app hospeda o issuer (`authkit.server`).
|
|
51
|
+
*
|
|
52
|
+
* - token opaco → `AccessToken.find` do oidc-provider (mesma rota do
|
|
53
|
+
* {@link TokenVerifyService}). Revogação (`/revoke`, logout, revogação de
|
|
54
|
+
* grant/sessão pelo admin) apaga o artefato, então revogado = não encontrado.
|
|
55
|
+
* - JWT (RFC 9068, `accessTokens.format: 'jwt'`) → assinatura contra o JWKS
|
|
56
|
+
* PÚBLICO do provider atual, `iss` e `typ: at+jwt`. JWT AT é auto-contido: o
|
|
57
|
+
* oidc-provider não o persiste, então ele vale até o `exp` mesmo após revogar o
|
|
58
|
+
* grant — mantenha o TTL do access token curto.
|
|
59
|
+
*
|
|
60
|
+
* `resolveIssuer` é chamado a cada verificação (e não guardado): o `OidcService`
|
|
61
|
+
* troca a instância do provider em `reloadKeys`/rotação de chaves.
|
|
62
|
+
*/
|
|
63
|
+
export function inProcessAccessTokenVerifier(resolveIssuer) {
|
|
64
|
+
// JWKS local memoizado por identidade: o `publicJwks` só muda (novo objeto)
|
|
65
|
+
// quando o provider é reconstruído.
|
|
66
|
+
let cachedJwks;
|
|
67
|
+
let cachedKeySet;
|
|
68
|
+
const keySetFor = (jwks) => {
|
|
69
|
+
if (jwks !== cachedJwks || !cachedKeySet) {
|
|
70
|
+
cachedJwks = jwks;
|
|
71
|
+
cachedKeySet = createLocalJWKSet(jwks);
|
|
72
|
+
}
|
|
73
|
+
return cachedKeySet;
|
|
74
|
+
};
|
|
75
|
+
return {
|
|
76
|
+
async verify(token) {
|
|
77
|
+
if (!token)
|
|
78
|
+
return null;
|
|
79
|
+
const issuer = await resolveIssuer();
|
|
80
|
+
if (looksLikeJwt(token)) {
|
|
81
|
+
try {
|
|
82
|
+
const { payload } = await jwtVerify(token, keySetFor(issuer.publicJwks), {
|
|
83
|
+
issuer: issuer.config.issuer,
|
|
84
|
+
typ: 'at+jwt',
|
|
85
|
+
});
|
|
86
|
+
return fromJwtPayload(payload);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (rejectedJwt(error))
|
|
90
|
+
return null;
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
let at;
|
|
95
|
+
try {
|
|
96
|
+
at = await issuer.provider?.AccessToken?.find(token);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
// Só erro do próprio provider (token malformado) é recusa; erro do adapter sobe.
|
|
100
|
+
if (error instanceof providerErrors.OIDCProviderError)
|
|
101
|
+
return null;
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
if (!at)
|
|
105
|
+
return null;
|
|
106
|
+
// O oidc-provider já recusa artefato expirado no `find`; isto é guarda extra.
|
|
107
|
+
if (at.isExpired === true)
|
|
108
|
+
return null;
|
|
109
|
+
if (typeof at.exp === 'number' && at.exp <= Math.floor(Date.now() / 1000))
|
|
110
|
+
return null;
|
|
111
|
+
// Sender-constrained (DPoP `jkt` / mTLS `x5t#S256`): sem a prova, não aceita.
|
|
112
|
+
if (at.jkt || at['x5t#S256'])
|
|
113
|
+
return null;
|
|
114
|
+
if (typeof at.accountId !== 'string' || !at.accountId)
|
|
115
|
+
return null;
|
|
116
|
+
return {
|
|
117
|
+
format: 'opaque',
|
|
118
|
+
sub: at.accountId,
|
|
119
|
+
clientId: typeof at.clientId === 'string' ? at.clientId : null,
|
|
120
|
+
scopes: splitScope(at.scope),
|
|
121
|
+
audience: toAudience(at.aud),
|
|
122
|
+
exp: typeof at.exp === 'number' ? at.exp : null,
|
|
123
|
+
jti: typeof at.jti === 'string' ? at.jti : null,
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
async issue(accountId, options = {}) {
|
|
127
|
+
const { provider } = await resolveIssuer();
|
|
128
|
+
const at = new provider.AccessToken({
|
|
129
|
+
accountId,
|
|
130
|
+
clientId: options.clientId ?? 'authkit-test-client',
|
|
131
|
+
scope: (options.scopes ?? ['openid']).join(' '),
|
|
132
|
+
gty: 'authorization_code',
|
|
133
|
+
});
|
|
134
|
+
return at.save();
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Verificação de RESOURCE SERVER remoto: a API NÃO hospeda o issuer.
|
|
140
|
+
*
|
|
141
|
+
* - JWT (RFC 9068) → assinatura contra o JWKS remoto (cacheado e recarregado
|
|
142
|
+
* pelo `jose` quando aparece um `kid` novo), `iss` e `typ: at+jwt`.
|
|
143
|
+
* - opaco → introspecção (RFC 7662) com client credentials; exige
|
|
144
|
+
* `active: true` e `token_type` Bearer (refresh token introspectado vem sem
|
|
145
|
+
* `token_type` e é recusado).
|
|
146
|
+
*/
|
|
147
|
+
export function remoteAccessTokenVerifier(options) {
|
|
148
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
149
|
+
const issuer = options.issuer.replace(/\/+$/, '');
|
|
150
|
+
let discovery;
|
|
151
|
+
const discover = () => {
|
|
152
|
+
discovery ??= (async () => {
|
|
153
|
+
const res = await doFetch(`${issuer}/.well-known/openid-configuration`);
|
|
154
|
+
if (!res.ok)
|
|
155
|
+
throw new Error(`discovery ${res.status}`);
|
|
156
|
+
return (await res.json());
|
|
157
|
+
})().catch((error) => {
|
|
158
|
+
// Falha transitória não fica memoizada: a próxima request tenta de novo.
|
|
159
|
+
discovery = undefined;
|
|
160
|
+
throw error;
|
|
161
|
+
});
|
|
162
|
+
return discovery;
|
|
163
|
+
};
|
|
164
|
+
let keySet;
|
|
165
|
+
const remoteKeySet = async () => {
|
|
166
|
+
if (keySet)
|
|
167
|
+
return keySet;
|
|
168
|
+
const jwksUri = options.jwksUri ?? (await discover()).jwks_uri;
|
|
169
|
+
if (!jwksUri)
|
|
170
|
+
throw new Error('issuer sem jwks_uri');
|
|
171
|
+
keySet = createRemoteJWKSet(new URL(jwksUri), options.fetch ? { [customFetch]: options.fetch } : undefined);
|
|
172
|
+
return keySet;
|
|
173
|
+
};
|
|
174
|
+
const introspect = async (token) => {
|
|
175
|
+
const creds = options.introspection;
|
|
176
|
+
if (!creds)
|
|
177
|
+
return null;
|
|
178
|
+
const endpoint = creds.endpoint ?? (await discover()).introspection_endpoint;
|
|
179
|
+
if (!endpoint)
|
|
180
|
+
return null;
|
|
181
|
+
// RFC 6749 §2.3.1: client_id/secret form-urlencoded antes do Basic.
|
|
182
|
+
const basic = Buffer.from(`${encodeURIComponent(creds.clientId)}:${encodeURIComponent(creds.clientSecret)}`).toString('base64');
|
|
183
|
+
const res = await doFetch(endpoint, {
|
|
184
|
+
method: 'POST',
|
|
185
|
+
headers: {
|
|
186
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
187
|
+
accept: 'application/json',
|
|
188
|
+
authorization: `Basic ${basic}`,
|
|
189
|
+
},
|
|
190
|
+
body: new URLSearchParams({ token, token_type_hint: 'access_token' }).toString(),
|
|
191
|
+
});
|
|
192
|
+
// 5xx ou credencial do resource server recusada: problema de infra/config, não do token.
|
|
193
|
+
if (!res.ok)
|
|
194
|
+
throw new Error(`authkit: introspecção falhou (HTTP ${res.status})`);
|
|
195
|
+
const body = (await res.json());
|
|
196
|
+
if (body.active !== true)
|
|
197
|
+
return null;
|
|
198
|
+
if (typeof body.token_type !== 'string' || body.token_type.toLowerCase() !== 'bearer') {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
if (body.cnf)
|
|
202
|
+
return null;
|
|
203
|
+
if (typeof body.sub !== 'string' || !body.sub)
|
|
204
|
+
return null;
|
|
205
|
+
if (typeof body.exp === 'number' && body.exp <= Math.floor(Date.now() / 1000))
|
|
206
|
+
return null;
|
|
207
|
+
return {
|
|
208
|
+
format: 'opaque',
|
|
209
|
+
sub: body.sub,
|
|
210
|
+
clientId: typeof body.client_id === 'string' ? body.client_id : null,
|
|
211
|
+
scopes: splitScope(body.scope),
|
|
212
|
+
audience: toAudience(body.aud),
|
|
213
|
+
exp: typeof body.exp === 'number' ? body.exp : null,
|
|
214
|
+
jti: typeof body.jti === 'string' ? body.jti : null,
|
|
215
|
+
};
|
|
216
|
+
};
|
|
217
|
+
return {
|
|
218
|
+
async verify(token) {
|
|
219
|
+
if (!token)
|
|
220
|
+
return null;
|
|
221
|
+
if (looksLikeJwt(token)) {
|
|
222
|
+
try {
|
|
223
|
+
const { payload } = await jwtVerify(token, await remoteKeySet(), {
|
|
224
|
+
// Mesma forma da discovery (sem barra final), aceitando o `iss` com ou sem ela.
|
|
225
|
+
issuer: [issuer, `${issuer}/`],
|
|
226
|
+
typ: 'at+jwt',
|
|
227
|
+
});
|
|
228
|
+
return fromJwtPayload(payload);
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
if (rejectedJwt(error))
|
|
232
|
+
return null;
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// Introspecção fora do ar sobe como erro (não é "token inválido").
|
|
237
|
+
return introspect(token);
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
}
|
|
@@ -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,
|
|
@@ -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
|
};
|