@adonis-agora/authkit-server 0.68.0 → 0.68.1
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.
|
@@ -26,3 +26,20 @@ export declare function decodeActiveOrgCookie(value: string | null | undefined):
|
|
|
26
26
|
* NOTA: o oidc-provider ctx.cookies.get() nunca lança — retorna null se ausente.
|
|
27
27
|
*/
|
|
28
28
|
export declare function readActiveOrgFromKoaCtx(koaCtx: any): ActiveOrgInfo | null;
|
|
29
|
+
/**
|
|
30
|
+
* Normaliza um valor potencialmente vindo de um payload PERSISTIDO (o `activeOrg`
|
|
31
|
+
* do Grant) para `ActiveOrgInfo`. Retorna null quando a forma não bate — nunca
|
|
32
|
+
* confiamos num objeto só porque ele veio do banco.
|
|
33
|
+
*/
|
|
34
|
+
export declare function normalizeActiveOrg(value: unknown): ActiveOrgInfo | null;
|
|
35
|
+
/**
|
|
36
|
+
* Lê a org ativa de um contexto de interaction do HOST (o `HttpContext` do
|
|
37
|
+
* AdonisJS usado pelas `InteractionActions`).
|
|
38
|
+
*
|
|
39
|
+
* No consent a request É do browser, então o cookie está presente — ao contrário
|
|
40
|
+
* do mint do id_token no `/token` (server-a-servidor, sem cookies). O cookie é
|
|
41
|
+
* gravado UNSIGNED por `account_orgs_controller.activate` e lido aqui via
|
|
42
|
+
* `request.cookie` (o MESMO caminho das leituras do console/account API), com
|
|
43
|
+
* fallback para o caminho Koa caso o contexto recebido seja um ctx Koa.
|
|
44
|
+
*/
|
|
45
|
+
export declare function readActiveOrgFromHostCtx(ctx: unknown): ActiveOrgInfo | null;
|
|
@@ -26,6 +26,27 @@ export function decodeActiveOrgCookie(value) {
|
|
|
26
26
|
return null;
|
|
27
27
|
return { orgId, orgSlug, orgRole };
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Parseia um valor de cookie possivelmente URL-encoded.
|
|
31
|
+
*
|
|
32
|
+
* O jar Koa do oidc-provider (`cookies`) devolve o valor COMO ESTÁ no header — sem
|
|
33
|
+
* URL-decode. Como o host grava o cookie via `response.cookie` (que serializa com
|
|
34
|
+
* `encodeURIComponent`, transformando os TABs em `%09`), é preciso tentar decodificar.
|
|
35
|
+
* Tentamos o valor cru primeiro (hosts que gravem sem encode) e o decodificado depois.
|
|
36
|
+
*/
|
|
37
|
+
function parseActiveOrgCookieValue(raw) {
|
|
38
|
+
if (typeof raw !== 'string')
|
|
39
|
+
return null;
|
|
40
|
+
const direct = decodeActiveOrgCookie(raw);
|
|
41
|
+
if (direct)
|
|
42
|
+
return direct;
|
|
43
|
+
try {
|
|
44
|
+
return decodeActiveOrgCookie(decodeURIComponent(raw));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
29
50
|
/**
|
|
30
51
|
* Lê a org ativa de um contexto Koa (oidc-provider). O oidc-provider usa o Keygrip
|
|
31
52
|
* das `cookieKeys` para assinar os cookies — lemos via `ctx.cookies.get(name, { signed: false })`
|
|
@@ -39,9 +60,50 @@ export function decodeActiveOrgCookie(value) {
|
|
|
39
60
|
export function readActiveOrgFromKoaCtx(koaCtx) {
|
|
40
61
|
try {
|
|
41
62
|
const raw = koaCtx?.cookies?.get?.(ACTIVE_ORG_COOKIE, { signed: false });
|
|
42
|
-
return
|
|
63
|
+
return parseActiveOrgCookieValue(raw);
|
|
43
64
|
}
|
|
44
65
|
catch {
|
|
45
66
|
return null;
|
|
46
67
|
}
|
|
47
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Normaliza um valor potencialmente vindo de um payload PERSISTIDO (o `activeOrg`
|
|
71
|
+
* do Grant) para `ActiveOrgInfo`. Retorna null quando a forma não bate — nunca
|
|
72
|
+
* confiamos num objeto só porque ele veio do banco.
|
|
73
|
+
*/
|
|
74
|
+
export function normalizeActiveOrg(value) {
|
|
75
|
+
if (!value || typeof value !== 'object')
|
|
76
|
+
return null;
|
|
77
|
+
const { orgId, orgSlug, orgRole } = value;
|
|
78
|
+
if (typeof orgId !== 'string' ||
|
|
79
|
+
!orgId ||
|
|
80
|
+
typeof orgSlug !== 'string' ||
|
|
81
|
+
!orgSlug ||
|
|
82
|
+
typeof orgRole !== 'string' ||
|
|
83
|
+
!orgRole) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
return { orgId, orgSlug, orgRole };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Lê a org ativa de um contexto de interaction do HOST (o `HttpContext` do
|
|
90
|
+
* AdonisJS usado pelas `InteractionActions`).
|
|
91
|
+
*
|
|
92
|
+
* No consent a request É do browser, então o cookie está presente — ao contrário
|
|
93
|
+
* do mint do id_token no `/token` (server-a-servidor, sem cookies). O cookie é
|
|
94
|
+
* gravado UNSIGNED por `account_orgs_controller.activate` e lido aqui via
|
|
95
|
+
* `request.cookie` (o MESMO caminho das leituras do console/account API), com
|
|
96
|
+
* fallback para o caminho Koa caso o contexto recebido seja um ctx Koa.
|
|
97
|
+
*/
|
|
98
|
+
export function readActiveOrgFromHostCtx(ctx) {
|
|
99
|
+
try {
|
|
100
|
+
const raw = ctx?.request?.cookie?.(ACTIVE_ORG_COOKIE);
|
|
101
|
+
const parsed = parseActiveOrgCookieValue(raw);
|
|
102
|
+
if (parsed)
|
|
103
|
+
return parsed;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// contexto sem `request.cookie` — tenta o caminho Koa abaixo
|
|
107
|
+
}
|
|
108
|
+
return readActiveOrgFromKoaCtx(ctx);
|
|
109
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as oidc from 'oidc-provider';
|
|
2
2
|
import { pickModelAdapterClass } from '../adapters/factory.js';
|
|
3
|
+
import { normalizeActiveOrg, readActiveOrgFromKoaCtx } from '../host/active_org_cookie.js';
|
|
3
4
|
import { createDeviceSources } from './device_sources.js';
|
|
4
5
|
import { createLogoutSources } from './logout_sources.js';
|
|
5
6
|
/** Atualiza o holder mutável do TTL de sessão com os valores da setting. */
|
|
@@ -117,6 +118,35 @@ export function buildProvider(config, options, sessionTtlHolder, tokenTtlHolder)
|
|
|
117
118
|
: {}),
|
|
118
119
|
})),
|
|
119
120
|
findAccount: options.findAccount,
|
|
121
|
+
// Grant REAPROVEITADO (consent já lembrado): o org capturado no primeiro
|
|
122
|
+
// consent ficaria VELHO se o usuário trocasse de org no console e voltasse a
|
|
123
|
+
// autenticar sem novo consent. Esta função roda numa request do BROWSER (o
|
|
124
|
+
// authorize), então relemos o cookie aqui e atualizamos o Grant quando
|
|
125
|
+
// divergir — inclusive LIMPANDO quando o cookie some (org desativada). Sem
|
|
126
|
+
// isto o token continuaria emitindo a org antiga até o grant expirar.
|
|
127
|
+
// Assinatura/default confirmados no fonte instalado: `actions/authorization/
|
|
128
|
+
// session.js` chama `configuration.loadExistingGrant(ctx)`.
|
|
129
|
+
loadExistingGrant: async (ctx) => {
|
|
130
|
+
const grantId = ctx.oidc.result?.consent?.grantId || ctx.oidc.session.grantIdFor(ctx.oidc.client.clientId);
|
|
131
|
+
if (!grantId)
|
|
132
|
+
return undefined;
|
|
133
|
+
const grant = await ctx.oidc.provider.Grant.find(grantId);
|
|
134
|
+
if (!grant)
|
|
135
|
+
return undefined;
|
|
136
|
+
const activeOrg = readActiveOrgFromKoaCtx(ctx);
|
|
137
|
+
const current = normalizeActiveOrg(grant.activeOrg);
|
|
138
|
+
const changed = (activeOrg?.orgId ?? null) !== (current?.orgId ?? null) ||
|
|
139
|
+
(activeOrg?.orgSlug ?? null) !== (current?.orgSlug ?? null) ||
|
|
140
|
+
(activeOrg?.orgRole ?? null) !== (current?.orgRole ?? null);
|
|
141
|
+
if (changed) {
|
|
142
|
+
if (activeOrg)
|
|
143
|
+
grant.activeOrg = activeOrg;
|
|
144
|
+
else
|
|
145
|
+
delete grant.activeOrg;
|
|
146
|
+
await grant.save();
|
|
147
|
+
}
|
|
148
|
+
return grant;
|
|
149
|
+
},
|
|
120
150
|
jwks: config.jwks,
|
|
121
151
|
cookies: {
|
|
122
152
|
keys: cookieKeys,
|
|
@@ -220,6 +250,29 @@ export function buildProvider(config, options, sessionTtlHolder, tokenTtlHolder)
|
|
|
220
250
|
url: (_ctx, interaction) => `/auth/interaction/${interaction.uid}`,
|
|
221
251
|
},
|
|
222
252
|
});
|
|
253
|
+
// ── Modelo Grant estendido: persiste a org ativa ─────────────────────────────
|
|
254
|
+
// O oidc-provider v9 NÃO expõe uma opção `models` no config (confirmado no fonte
|
|
255
|
+
// instalado: `provider.js` instancia os modelos por `models.getGrant(this)` e
|
|
256
|
+
// `helpers/defaults.js` não tem chave alguma `models`), e o `Grant` filtra o
|
|
257
|
+
// payload por `IN_PAYLOAD` (`lib/models/grant.js` + `base_model.js`), então
|
|
258
|
+
// qualquer prop de topo fora da lista é DESCARTADA no `save()`/`find()`.
|
|
259
|
+
// Estendemos `IN_PAYLOAD` com `activeOrg` e injetamos a subclasse na INSTÂNCIA
|
|
260
|
+
// do provider via `Object.defineProperty`, que sombreia o getter `Grant` do
|
|
261
|
+
// protótipo. O `name` é fixado em `'Grant'` para que o nome do ADAPTER e o
|
|
262
|
+
// `kind` persistido continuem sendo os MESMOS registros (`BaseModel` usa
|
|
263
|
+
// `constructor.name` para ambos) — sem isso, grants novos/antigos divergiriam.
|
|
264
|
+
const BaseGrant = provider.Grant;
|
|
265
|
+
const GrantWithOrg = class GrantWithOrg extends BaseGrant {
|
|
266
|
+
static get IN_PAYLOAD() {
|
|
267
|
+
return [...BaseGrant.IN_PAYLOAD, 'activeOrg'];
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
Object.defineProperty(GrantWithOrg, 'name', { value: 'Grant', configurable: true });
|
|
271
|
+
Object.defineProperty(provider, 'Grant', {
|
|
272
|
+
value: GrantWithOrg,
|
|
273
|
+
writable: true,
|
|
274
|
+
configurable: true,
|
|
275
|
+
});
|
|
223
276
|
provider.proxy = true;
|
|
224
277
|
return provider;
|
|
225
278
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readActiveOrgFromHostCtx } from '../host/active_org_cookie.js';
|
|
1
2
|
import { InteractionSessionLostException, isInteractionSessionLost, } from '../host/interaction_recovery.js';
|
|
2
3
|
/**
|
|
3
4
|
* Choke point ÚNICO de `interactionDetails`: converte o `SessionNotFound` do
|
|
@@ -51,9 +52,15 @@ export function createInteractionActions(provider, deps) {
|
|
|
51
52
|
},
|
|
52
53
|
async consent(ctx) {
|
|
53
54
|
const details = await interactionDetailsOrRecover(provider, ctx);
|
|
55
|
+
// O consent roda numa request do BROWSER, então a org ativa (cookie) está
|
|
56
|
+
// disponível AQUI — ao contrário do mint do id_token no /token, que é
|
|
57
|
+
// server-a-servidor e não carrega os cookies do usuário. Persistimos a org
|
|
58
|
+
// no Grant para que o fluxo authorization code volte a emitir org_*.
|
|
59
|
+
const activeOrg = readActiveOrgFromHostCtx(ctx);
|
|
54
60
|
const grant = new provider.Grant({
|
|
55
61
|
accountId: details.session.accountId,
|
|
56
62
|
clientId: details.params.client_id,
|
|
63
|
+
...(activeOrg ? { activeOrg } : {}),
|
|
57
64
|
});
|
|
58
65
|
grant.addOIDCScope(String(details.params.scope ?? 'openid'));
|
|
59
66
|
// Resource Indicators (RFC 8707): quando o authorize/token pede um `resource`
|
|
@@ -2,7 +2,7 @@ import { timingSafeEqual } from 'node:crypto';
|
|
|
2
2
|
import { NoopRecorder } from '@adonis-agora/authkit-core';
|
|
3
3
|
import Koa from 'koa';
|
|
4
4
|
import mount from 'koa-mount';
|
|
5
|
-
import { readActiveOrgFromKoaCtx } from '../host/active_org_cookie.js';
|
|
5
|
+
import { normalizeActiveOrg, readActiveOrgFromKoaCtx } from '../host/active_org_cookie.js';
|
|
6
6
|
import { isFirstPartyClient } from '../host/branding.js';
|
|
7
7
|
import { listKeyInfos, signingKeyAgeDays } from '../keys/keystore.js';
|
|
8
8
|
import { wireProviderEvents } from '../observability/wire_provider_events.js';
|
|
@@ -82,8 +82,13 @@ export class OidcService {
|
|
|
82
82
|
const user = await config.findAccount(sub);
|
|
83
83
|
if (!user)
|
|
84
84
|
return undefined;
|
|
85
|
-
//
|
|
86
|
-
|
|
85
|
+
// Fonte PRIMÁRIA: a org persistida no Grant no consent (request do
|
|
86
|
+
// browser). No fluxo authorization code o id_token é mintado no /token
|
|
87
|
+
// — server-a-servidor, SEM cookies — então o cookie abaixo não existe
|
|
88
|
+
// ali. O cookie continua como FALLBACK para fluxos em que o id_token
|
|
89
|
+
// sai no próprio authorize (implicit/hybrid), onde a request é do browser.
|
|
90
|
+
const activeOrg = normalizeActiveOrg(ctx?.oidc?.entities?.Grant?.activeOrg) ??
|
|
91
|
+
readActiveOrgFromKoaCtx(ctx);
|
|
87
92
|
// GATE de least-privilege: roles globais e claims de org só são emitidas
|
|
88
93
|
// para clients FIRST-PARTY. Capturamos o clientId aqui (fora do closure
|
|
89
94
|
// `claims()`, pois o `ctx` da request vive neste escopo).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adonis-agora/authkit-server",
|
|
3
|
-
"version": "0.68.
|
|
3
|
+
"version": "0.68.1",
|
|
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",
|