@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,9 +1,11 @@
|
|
|
1
1
|
import * as oidc from 'oidc-provider';
|
|
2
2
|
import { pickModelAdapterClass } from '../adapters/factory.js';
|
|
3
3
|
import { normalizeActiveOrg, readActiveOrgFromKoaCtx } from '../host/active_org_cookie.js';
|
|
4
|
+
import { assertClientMetadata } from '../host/client_metadata.js';
|
|
4
5
|
import { createDeviceSources } from './device_sources.js';
|
|
5
6
|
import { createLogoutSources } from './logout_sources.js';
|
|
6
7
|
import { registrationPolicyMiddleware } from './registration_policy.js';
|
|
8
|
+
import { impersonationExtraClaims } from './token_exchange.js';
|
|
7
9
|
/** Atualiza o holder mutável do TTL de sessão com os valores da setting. */
|
|
8
10
|
export function updateSessionTtlHolder(holder, policy) {
|
|
9
11
|
holder.rememberSec = Math.max(1, Math.floor(policy.rememberDays * 86400));
|
|
@@ -117,29 +119,49 @@ export function buildProvider(config, options, sessionTtlHolder, tokenTtlHolder)
|
|
|
117
119
|
}
|
|
118
120
|
: {};
|
|
119
121
|
const provider = new oidc.Provider(config.issuer, {
|
|
122
|
+
// O ator (`act`, RFC 8693) dos access tokens de impersonation — ver
|
|
123
|
+
// `impersonationExtraClaims` em `token_exchange.ts`. Sem isto o token trocado
|
|
124
|
+
// não se distingue de um token do próprio alvo no resource server.
|
|
125
|
+
extraTokenClaims: async (_ctx, token) => impersonationExtraClaims(token),
|
|
120
126
|
// Dispatcher por modelo (suportado pelo oidc-provider: `Adapter` aceita
|
|
121
127
|
// função `(name) => adapter` além de classe). Session-scoped vai pro
|
|
122
128
|
// `SessionAdapterClass`, o resto pro `AdapterClass` — mesma regra de
|
|
123
129
|
// `pickModelAdapterClass`, usada também pelos serviços que instanciam
|
|
124
130
|
// adapter na mão. Sem `session.adapter`, as duas classes são a mesma.
|
|
125
131
|
adapter: ((name) => new (pickModelAdapterClass(name, config.AdapterClass, config.SessionAdapterClass))(name)),
|
|
126
|
-
clients: config.clients.map((c) =>
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
:
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
:
|
|
142
|
-
|
|
132
|
+
clients: config.clients.map((c) => {
|
|
133
|
+
const grants = c.grants ?? ['authorization_code', 'refresh_token'];
|
|
134
|
+
const tokenEndpointAuthMethod = c.tokenEndpointAuthMethod ?? (c.clientSecret ? 'client_secret_basic' : 'none');
|
|
135
|
+
// Clients do `defineConfig` passam pela MESMA validação do admin/CLI/import:
|
|
136
|
+
// um nativo com secret (ou redirect inválido pro tipo) falha no boot, não em runtime.
|
|
137
|
+
assertClientMetadata({
|
|
138
|
+
applicationType: c.applicationType ?? 'web',
|
|
139
|
+
tokenEndpointAuthMethod,
|
|
140
|
+
grantTypes: grants,
|
|
141
|
+
redirectUris: c.redirectUris,
|
|
142
|
+
postLogoutRedirectUris: c.postLogoutRedirectUris ?? [],
|
|
143
|
+
});
|
|
144
|
+
return {
|
|
145
|
+
client_id: c.clientId,
|
|
146
|
+
client_secret: c.clientSecret,
|
|
147
|
+
redirect_uris: c.redirectUris,
|
|
148
|
+
post_logout_redirect_uris: c.postLogoutRedirectUris ?? [],
|
|
149
|
+
grant_types: grants,
|
|
150
|
+
response_types: (c.grants ?? ['authorization_code']).includes('authorization_code')
|
|
151
|
+
? ['code']
|
|
152
|
+
: [],
|
|
153
|
+
token_endpoint_auth_method: tokenEndpointAuthMethod,
|
|
154
|
+
// App nativo (RFC 8252): libera redirect de esquema privado/loopback. Sem a
|
|
155
|
+
// chave, o oidc-provider assume `web` — o comportamento de sempre.
|
|
156
|
+
...(c.applicationType === 'native' ? { application_type: 'native' } : {}),
|
|
157
|
+
// OIDC Back-Channel Logout: só envia as chaves quando o client as declara,
|
|
158
|
+
// p/ não forçar metadata vazio em clients que não usam o recurso.
|
|
159
|
+
...(c.backchannelLogoutUri ? { backchannel_logout_uri: c.backchannelLogoutUri } : {}),
|
|
160
|
+
...(c.backchannelLogoutSessionRequired !== undefined
|
|
161
|
+
? { backchannel_logout_session_required: c.backchannelLogoutSessionRequired }
|
|
162
|
+
: {}),
|
|
163
|
+
};
|
|
164
|
+
}),
|
|
143
165
|
findAccount: options.findAccount,
|
|
144
166
|
// Grant REAPROVEITADO (consent já lembrado): o org capturado no primeiro
|
|
145
167
|
// consent ficaria VELHO se o usuário trocasse de org no console e voltasse a
|
|
@@ -220,6 +242,14 @@ export function buildProvider(config, options, sessionTtlHolder, tokenTtlHolder)
|
|
|
220
242
|
// sobreviva ao authorize e fique disponivel em interactionDetails().params.audience.
|
|
221
243
|
extraParams: ['audience'],
|
|
222
244
|
pkce: { methods: ['S256'], required: () => true },
|
|
245
|
+
// Rotação SEMPRE: todo uso do refresh token emite um novo e invalida o
|
|
246
|
+
// anterior (reuso de um RT já rotacionado revoga o grant inteiro — detecção de
|
|
247
|
+
// replay do oidc-provider). Vale igual para clients públicos (SPA, app nativo
|
|
248
|
+
// RFC 8252), para os quais a rotação é a defesa exigida pelo OAuth 2.0 Security
|
|
249
|
+
// BCP, já que o RT não é amarrado a um secret. O `issueRefreshToken` fica no
|
|
250
|
+
// default do provider: emite RT quando o client tem o grant `refresh_token` e o
|
|
251
|
+
// escopo `offline_access` foi concedido (o que exige `prompt=consent` no
|
|
252
|
+
// authorize — OIDC Core §11), sem distinção entre client público e confidencial.
|
|
223
253
|
rotateRefreshToken: true,
|
|
224
254
|
features: {
|
|
225
255
|
devInteractions: { enabled: false },
|
|
@@ -9,6 +9,14 @@ export declare class OidcService {
|
|
|
9
9
|
get provider(): ReturnType<typeof buildProvider>;
|
|
10
10
|
get callback(): (req: any, res: any) => void;
|
|
11
11
|
get interactions(): InteractionActions;
|
|
12
|
+
/**
|
|
13
|
+
* JWKS PÚBLICO do keystore em uso pelo provider ATUAL (o mesmo que o `jwks_uri`
|
|
14
|
+
* serve). Troca junto com o provider num `reloadKeys`/rotação — quem verifica JWT
|
|
15
|
+
* in-process (ex.: `oidcBearerGuard`) lê daqui a cada verificação.
|
|
16
|
+
*/
|
|
17
|
+
get publicJwks(): {
|
|
18
|
+
keys: Record<string, any>[];
|
|
19
|
+
};
|
|
12
20
|
/** Pathname do issuer sem barra final (ex.: `/oidc`). Vazio quando montado na raiz. */
|
|
13
21
|
readonly mountPath: string;
|
|
14
22
|
readonly recorder: MetricsRecorder;
|
|
@@ -4,7 +4,7 @@ import Koa from 'koa';
|
|
|
4
4
|
import mount from 'koa-mount';
|
|
5
5
|
import { normalizeActiveOrg, readActiveOrgFromKoaCtx, verifyActiveOrgMembership, } from '../host/active_org_cookie.js';
|
|
6
6
|
import { isFirstPartyClient } from '../host/branding.js';
|
|
7
|
-
import { listKeyInfos, signingKeyAgeDays } from '../keys/keystore.js';
|
|
7
|
+
import { listKeyInfos, signingKeyAgeDays, toPublicJwks, } from '../keys/keystore.js';
|
|
8
8
|
import { wireProviderEvents } from '../observability/wire_provider_events.js';
|
|
9
9
|
import { buildProvider } from './build_provider.js';
|
|
10
10
|
import { createInteractionActions } from './interaction_actions.js';
|
|
@@ -13,6 +13,7 @@ export class OidcService {
|
|
|
13
13
|
#provider;
|
|
14
14
|
#callback;
|
|
15
15
|
#interactions;
|
|
16
|
+
#publicJwks;
|
|
16
17
|
#appKey;
|
|
17
18
|
get provider() {
|
|
18
19
|
return this.#provider;
|
|
@@ -23,6 +24,14 @@ export class OidcService {
|
|
|
23
24
|
get interactions() {
|
|
24
25
|
return this.#interactions;
|
|
25
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* JWKS PÚBLICO do keystore em uso pelo provider ATUAL (o mesmo que o `jwks_uri`
|
|
29
|
+
* serve). Troca junto com o provider num `reloadKeys`/rotação — quem verifica JWT
|
|
30
|
+
* in-process (ex.: `oidcBearerGuard`) lê daqui a cada verificação.
|
|
31
|
+
*/
|
|
32
|
+
get publicJwks() {
|
|
33
|
+
return this.#publicJwks;
|
|
34
|
+
}
|
|
26
35
|
/** Pathname do issuer sem barra final (ex.: `/oidc`). Vazio quando montado na raiz. */
|
|
27
36
|
mountPath;
|
|
28
37
|
recorder;
|
|
@@ -172,6 +181,7 @@ export class OidcService {
|
|
|
172
181
|
// conta que acabou de desabilitar, recebendo tokens plenamente funcionais.
|
|
173
182
|
accountStore: config.accountStore,
|
|
174
183
|
audit: config.audit,
|
|
184
|
+
impersonateAdmins: config.admin.impersonateAdmins,
|
|
175
185
|
});
|
|
176
186
|
}
|
|
177
187
|
// Quando o issuer tem um path (ex.: http://host/oidc), o provider precisa ser
|
|
@@ -205,6 +215,7 @@ export class OidcService {
|
|
|
205
215
|
this.#provider = provider;
|
|
206
216
|
this.#callback = callback;
|
|
207
217
|
this.#interactions = interactions;
|
|
218
|
+
this.#publicJwks = toPublicJwks(jwks);
|
|
208
219
|
}
|
|
209
220
|
/**
|
|
210
221
|
* Recarrega as chaves de assinatura AO VIVO: relê o keystore do cofre e reconstrói
|
|
@@ -56,5 +56,31 @@ export interface TokenExchangeDeps {
|
|
|
56
56
|
* regra de todo o resto da lib — nunca quebra hosts com um store mínimo).
|
|
57
57
|
*/
|
|
58
58
|
accountStore?: AccountStore;
|
|
59
|
+
/**
|
|
60
|
+
* Permite impersonar uma conta que TAMBÉM tem um dos `adminRoles`. Default
|
|
61
|
+
* `true` (back-compat). Com `false`, alvo admin vira `invalid_grant`: um admin
|
|
62
|
+
* não assume a identidade (e os tokens) de outro admin.
|
|
63
|
+
*/
|
|
64
|
+
impersonateAdmins?: boolean;
|
|
59
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Propriedade (não persistida) onde a troca marca o ator ANTES do `save()`. O
|
|
68
|
+
* `extraTokenClaims` do provider (`build_provider.ts`) a transforma em
|
|
69
|
+
* `extra.act` — é o único jeito de pôr claims no `extra` de um token opaco: o
|
|
70
|
+
* oidc-provider SOBRESCREVE `extra` com o retorno desse hook ao salvar.
|
|
71
|
+
*/
|
|
72
|
+
export declare const IMPERSONATION_ACTOR_PROP = "authkitImpersonationActor";
|
|
73
|
+
/** `extraTokenClaims` do provider: `{ act }` para token de impersonation, nada para o resto. */
|
|
74
|
+
export declare function impersonationExtraClaims(token: unknown): {
|
|
75
|
+
act: {
|
|
76
|
+
sub: string;
|
|
77
|
+
};
|
|
78
|
+
} | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* O ator de um access token de impersonation: o `act` (RFC 8693 §4.1) gravado
|
|
81
|
+
* no `extra` do token trocado. `null` quando o token não é de impersonation.
|
|
82
|
+
*/
|
|
83
|
+
export declare function impersonationActorOf(token: {
|
|
84
|
+
extra?: unknown;
|
|
85
|
+
} | null | undefined): string | null;
|
|
60
86
|
export declare function registerTokenExchange(provider: any, deps: TokenExchangeDeps): void;
|
|
@@ -2,6 +2,26 @@ import { errors } from 'oidc-provider';
|
|
|
2
2
|
import { assertAccountEnabled } from '../host/login_attempt.js';
|
|
3
3
|
const TOKEN_EXCHANGE = 'urn:ietf:params:oauth:grant-type:token-exchange';
|
|
4
4
|
const ACCESS_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token';
|
|
5
|
+
/**
|
|
6
|
+
* Propriedade (não persistida) onde a troca marca o ator ANTES do `save()`. O
|
|
7
|
+
* `extraTokenClaims` do provider (`build_provider.ts`) a transforma em
|
|
8
|
+
* `extra.act` — é o único jeito de pôr claims no `extra` de um token opaco: o
|
|
9
|
+
* oidc-provider SOBRESCREVE `extra` com o retorno desse hook ao salvar.
|
|
10
|
+
*/
|
|
11
|
+
export const IMPERSONATION_ACTOR_PROP = 'authkitImpersonationActor';
|
|
12
|
+
/** `extraTokenClaims` do provider: `{ act }` para token de impersonation, nada para o resto. */
|
|
13
|
+
export function impersonationExtraClaims(token) {
|
|
14
|
+
const actor = token?.[IMPERSONATION_ACTOR_PROP];
|
|
15
|
+
return typeof actor === 'string' && actor ? { act: { sub: actor } } : undefined;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* O ator de um access token de impersonation: o `act` (RFC 8693 §4.1) gravado
|
|
19
|
+
* no `extra` do token trocado. `null` quando o token não é de impersonation.
|
|
20
|
+
*/
|
|
21
|
+
export function impersonationActorOf(token) {
|
|
22
|
+
const act = token?.extra?.act;
|
|
23
|
+
return typeof act?.sub === 'string' && act.sub ? act.sub : null;
|
|
24
|
+
}
|
|
5
25
|
/**
|
|
6
26
|
* Interseção entre os scopes pedidos e os scopes permitidos do client (allowlist).
|
|
7
27
|
* Preserva a ordem do pedido. Nunca excede a allowlist do client.
|
|
@@ -30,6 +50,12 @@ export function registerTokenExchange(provider, deps) {
|
|
|
30
50
|
if (!subjectAt || subjectAt.isExpired) {
|
|
31
51
|
throw new errors.InvalidGrant('subject_token invalid or expired');
|
|
32
52
|
}
|
|
53
|
+
// Sem impersonation encadeada: um token que JÁ é de impersonation não vira
|
|
54
|
+
// subject de outra troca (o "ator" seria a conta personificada, e a trilha
|
|
55
|
+
// perderia quem de fato está agindo).
|
|
56
|
+
if (impersonationActorOf(subjectAt)) {
|
|
57
|
+
throw new errors.InvalidGrant('subject_token is already an impersonation token');
|
|
58
|
+
}
|
|
33
59
|
// O subject_token DEVE ter sido emitido para o MESMO client autenticado: senão
|
|
34
60
|
// um client B poderia trocar um AT emitido para o client A (cross-client).
|
|
35
61
|
if (subjectAt.clientId !== client?.clientId) {
|
|
@@ -62,6 +88,9 @@ export function registerTokenExchange(provider, deps) {
|
|
|
62
88
|
if (!target) {
|
|
63
89
|
throw new errors.InvalidGrant('requested_subject not found');
|
|
64
90
|
}
|
|
91
|
+
if (target.id === actor.id) {
|
|
92
|
+
throw new errors.InvalidGrant('requested_subject must be another account');
|
|
93
|
+
}
|
|
65
94
|
// Status do alvo (disabled/expirado): mesmo gate de `attemptPasswordLogin`.
|
|
66
95
|
// Sem isso, impersonar um alvo desabilitado mintava tokens funcionais para
|
|
67
96
|
// uma identidade que o admin acreditava ter revogado. `accountStore` é
|
|
@@ -107,8 +136,6 @@ export function registerTokenExchange(provider, deps) {
|
|
|
107
136
|
// Client sem allowlist declarada: comportamento atual preservado.
|
|
108
137
|
scope = params.scope || DEFAULT_SCOPE;
|
|
109
138
|
}
|
|
110
|
-
const at = new provider.AccessToken({ accountId: target.id, client, scope });
|
|
111
|
-
const accessToken = await at.save();
|
|
112
139
|
// Token exchange is not tied to a browser session, so there is no active org
|
|
113
140
|
// context here — roles are resolved for the impersonated target with clientId only.
|
|
114
141
|
const roles = deps.resolveTokenRoles
|
|
@@ -117,6 +144,17 @@ export function registerTokenExchange(provider, deps) {
|
|
|
117
144
|
activeOrg: null,
|
|
118
145
|
})
|
|
119
146
|
: (target.globalRoles ?? []);
|
|
147
|
+
if (deps.impersonateAdmins === false && roles.some((r) => adminRoles.includes(r))) {
|
|
148
|
+
throw new errors.InvalidGrant('requested_subject is not impersonable');
|
|
149
|
+
}
|
|
150
|
+
// O ACCESS token (não só o id_token) carrega o ator: é o que o resource server
|
|
151
|
+
// vê a cada request (`oidcBearerGuard` → `impersonationState`/`realAccountId`).
|
|
152
|
+
// Sem isso, o token trocado era indistinguível de um token do próprio alvo, e
|
|
153
|
+
// toda regra "negado durante impersonation" deixava passar o app nativo.
|
|
154
|
+
// `extra` sai no JWT (formato jwt) e na introspecção (RFC 7662).
|
|
155
|
+
const at = new provider.AccessToken({ accountId: target.id, client, scope });
|
|
156
|
+
at[IMPERSONATION_ACTOR_PROP] = actor.id;
|
|
157
|
+
const accessToken = await at.save();
|
|
120
158
|
const idToken = new provider.IdToken({
|
|
121
159
|
sub: target.id,
|
|
122
160
|
email: target.email,
|
|
@@ -134,7 +172,7 @@ export function registerTokenExchange(provider, deps) {
|
|
|
134
172
|
email: target.email ?? null,
|
|
135
173
|
clientId: client?.clientId ?? null,
|
|
136
174
|
ip: ctx.req?.socket?.remoteAddress ?? null,
|
|
137
|
-
metadata: { scope },
|
|
175
|
+
metadata: { scope, jti: at.jti ?? null },
|
|
138
176
|
});
|
|
139
177
|
ctx.body = {
|
|
140
178
|
access_token: accessToken,
|
|
@@ -143,6 +181,9 @@ export function registerTokenExchange(provider, deps) {
|
|
|
143
181
|
expires_in: at.expiration ?? 3600,
|
|
144
182
|
id_token: idTokenJwt,
|
|
145
183
|
scope,
|
|
184
|
+
// RFC 8693 §4.1: o ator também no corpo da resposta (o `actSub` que o
|
|
185
|
+
// `requestTokenExchange` do host lê — antes ficava sempre vazio).
|
|
186
|
+
act: { sub: actor.id },
|
|
146
187
|
};
|
|
147
188
|
};
|
|
148
189
|
provider.registerGrantType(TOKEN_EXCHANGE, handler, [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adonis-agora/authkit-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.75.0",
|
|
4
4
|
"description": "AdonisJS OIDC/OAuth2 provider (Identity Provider) toolkit: ejectable auth server with sessions, rate-limiting, MFA/TOTP, audit log, federated logout and OpenTelemetry metrics.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "dudousxd",
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
}
|
|
103
103
|
},
|
|
104
104
|
"dependencies": {
|
|
105
|
-
"@adonis-agora/authkit-core": "0.
|
|
105
|
+
"@adonis-agora/authkit-core": "0.9.0",
|
|
106
106
|
"@simplewebauthn/browser": "14.0.0",
|
|
107
107
|
"@simplewebauthn/server": "14.0.2",
|
|
108
108
|
"jose": "6.2.12",
|
|
@@ -113,7 +113,7 @@
|
|
|
113
113
|
"qrcode": "1.5.4"
|
|
114
114
|
},
|
|
115
115
|
"devDependencies": {
|
|
116
|
-
"@adonis-agora/authkit-react": "0.
|
|
116
|
+
"@adonis-agora/authkit-react": "0.23.0",
|
|
117
117
|
"@adonis-agora/durable": "0.40.0",
|
|
118
118
|
"@adonis-agora/telescope": "0.21.1",
|
|
119
119
|
"@adonisjs/ally": "6.3.0",
|