@adonis-agora/authkit-server 0.67.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.
- package/build/src/accounts/lucid_account_store.d.ts +17 -10
- package/build/src/accounts/lucid_account_store.js +25 -11
- package/build/src/accounts/lucid_stores.d.ts +9 -8
- package/build/src/accounts/lucid_stores.js +4 -4
- package/build/src/doctor/checks.d.ts +5 -1
- package/build/src/doctor/checks.js +18 -5
- package/build/src/host/active_org_cookie.d.ts +17 -0
- package/build/src/host/active_org_cookie.js +63 -1
- package/build/src/provider/build_provider.js +53 -0
- package/build/src/provider/interaction_actions.js +7 -0
- package/build/src/provider/oidc_service.js +8 -3
- package/package.json +1 -1
|
@@ -138,20 +138,27 @@ export interface LucidAccountStoreOptions {
|
|
|
138
138
|
/**
|
|
139
139
|
* Models Lucid para organizations (multi-tenancy).
|
|
140
140
|
*
|
|
141
|
-
* -
|
|
142
|
-
* que já mapeiam as três tabelas
|
|
143
|
-
* `
|
|
144
|
-
*
|
|
145
|
-
*
|
|
141
|
+
* - **Ausente (default)** → usa os models DEFAULT da lib
|
|
142
|
+
* ({@link defaultOrganizationModels}), que já mapeiam as três tabelas
|
|
143
|
+
* lib-owned (`auth_organizations`, `auth_organization_members`,
|
|
144
|
+
* `auth_organization_invitations`). A `OrganizationsCapability` passa a
|
|
145
|
+
* existir por padrão. Isso é seguro porque as rotas do console de conta para
|
|
146
|
+
* orgs JÁ eram montadas por default (`register_auth_host`, bloco `if
|
|
147
|
+
* (mountOrgs)`, com controller capability-probed); antes, sem os models, a
|
|
148
|
+
* tela montada respondia 403; agora ela funciona. Não há tela nova — só uma
|
|
149
|
+
* que deixou de estar quebrada. As tabelas são criadas/evoluídas pelo
|
|
150
|
+
* `ensureAuthkitSchema`, então o mapeamento não é decisão do host.
|
|
151
|
+
* - `true` → idêntico ao default (atalho explícito; é o que apps existentes
|
|
152
|
+
* já usam e continua valendo).
|
|
146
153
|
* - `{ OrgModel, MemberModel, InvitationModel }` → escape hatch, para quem
|
|
147
154
|
* guarda as tabelas de auth numa conexão/schema próprios (os defaults não
|
|
148
155
|
* declaram `static connection`).
|
|
149
|
-
* -
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
* `
|
|
156
|
+
* - `false` → opt-out explícito: `OrganizationsCapability` AUSENTE no store.
|
|
157
|
+
* Use quando a dependência atual é `supportsOrganizations === false` (ex.:
|
|
158
|
+
* store/rotas que assumem a ausência). O `authkit:doctor` avisa se
|
|
159
|
+
* `organizations.enabled: true` e o store não tem a capability.
|
|
153
160
|
*/
|
|
154
|
-
organizationModels?: true | {
|
|
161
|
+
organizationModels?: true | false | {
|
|
155
162
|
OrgModel: any;
|
|
156
163
|
MemberModel: any;
|
|
157
164
|
InvitationModel: any;
|
|
@@ -130,7 +130,15 @@ export function lucidAccountStore(Model, options = {}) {
|
|
|
130
130
|
const encrypter = options.encrypter === false ? undefined : (options.encrypter ?? appKeyEncrypter());
|
|
131
131
|
const ProviderIdentityModel = options.providerIdentityModel;
|
|
132
132
|
const WebauthnCredentialModel = options.webauthnCredentialModel;
|
|
133
|
-
|
|
133
|
+
// Organizations é padrão: ausente OU `true` → models default da lib. O
|
|
134
|
+
// opt-out explícito é `false`. O trio explícito segue como escape hatch.
|
|
135
|
+
const orgModelsOption = options.organizationModels;
|
|
136
|
+
const usesDefaultOrgModels = orgModelsOption === undefined || orgModelsOption === true;
|
|
137
|
+
const OrgModels = orgModelsOption === false
|
|
138
|
+
? undefined
|
|
139
|
+
: usesDefaultOrgModels
|
|
140
|
+
? defaultOrganizationModels
|
|
141
|
+
: orgModelsOption;
|
|
134
142
|
// RP do WebAuthn: usado nas cerimônias. Default do rpName cai no mfaIssuer.
|
|
135
143
|
const webauthn = options.webauthn ?? {
|
|
136
144
|
rpName: mfaIssuer,
|
|
@@ -222,17 +230,23 @@ export function lucidAccountStore(Model, options = {}) {
|
|
|
222
230
|
// Preferência por usuário de tipos de login: só quando o model tem a coluna
|
|
223
231
|
// `login_methods` (JSONB). Sem a coluna → capacidade ausente (feature no-op).
|
|
224
232
|
...(supportsLoginMethodsColumn(Model) ? buildLoginMethods(ctx) : {}),
|
|
225
|
-
// Organizations (multi-tenancy):
|
|
233
|
+
// Organizations (multi-tenancy): por padrão usa os models default da lib;
|
|
234
|
+
// ausente só com `organizationModels: false` (opt-out explícito).
|
|
226
235
|
...(OrgModels
|
|
227
|
-
?
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
+
? {
|
|
237
|
+
// Origem dos models, para o `authkit:doctor` reportar sem re-inferir.
|
|
238
|
+
// NÃO faz parte do contrato AccountStore.
|
|
239
|
+
__organizationModelsSource: usesDefaultOrgModels ? 'default' : 'explicit',
|
|
240
|
+
...buildOrganizations({
|
|
241
|
+
OrgModel: OrgModels.OrgModel,
|
|
242
|
+
MemberModel: OrgModels.MemberModel,
|
|
243
|
+
InvitationModel: OrgModels.InvitationModel,
|
|
244
|
+
findAccountEmail: async (accountId) => {
|
|
245
|
+
const row = await Model.find(accountId);
|
|
246
|
+
return row?.email ?? null;
|
|
247
|
+
},
|
|
248
|
+
}),
|
|
249
|
+
}
|
|
236
250
|
: {}),
|
|
237
251
|
// Config de senha resolvida — exposta (não-enumerável) para o authkit:doctor
|
|
238
252
|
// inspecionar policy/checkPwned. NÃO faz parte do contrato AccountStore.
|
|
@@ -17,12 +17,13 @@ export interface LucidStoresModels {
|
|
|
17
17
|
/**
|
|
18
18
|
* Organizations (multi-tenancy) — habilita a `OrganizationsCapability`.
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
* pelo `ensureAuthkitSchema`)
|
|
22
|
-
*
|
|
20
|
+
* **Ausente → ligada por padrão** (models default da lib; as três tabelas são
|
|
21
|
+
* lib-owned, criadas pelo `ensureAuthkitSchema`). `true` é o atalho explícito
|
|
22
|
+
* para o mesmo caminho; `false` é o opt-out (capability ausente). O trio
|
|
23
|
+
* explícito fica como escape hatch para quem guarda as tabelas de auth numa
|
|
23
24
|
* conexão/schema próprios.
|
|
24
25
|
*/
|
|
25
|
-
organizations?: true | {
|
|
26
|
+
organizations?: true | false | {
|
|
26
27
|
OrgModel: any;
|
|
27
28
|
MemberModel: any;
|
|
28
29
|
InvitationModel: any;
|
|
@@ -49,9 +50,9 @@ export type LucidStoresOptions = Omit<LucidAccountStoreOptions, 'providerIdentit
|
|
|
49
50
|
* )
|
|
50
51
|
* ```
|
|
51
52
|
*
|
|
52
|
-
*
|
|
53
|
-
* vivem numa conexão/schema próprios, passe
|
|
54
|
-
* (`{ OrgModel, MemberModel, InvitationModel }`) — os defaults
|
|
55
|
-
* `static connection`.
|
|
53
|
+
* Organizations é ligada por padrão (models default da lib); `organizations:
|
|
54
|
+
* false` desliga. Se as tabelas de auth vivem numa conexão/schema próprios, passe
|
|
55
|
+
* o trio explícito (`{ OrgModel, MemberModel, InvitationModel }`) — os defaults
|
|
56
|
+
* não declaram `static connection`.
|
|
56
57
|
*/
|
|
57
58
|
export declare function lucidStores(models: LucidStoresModels, options?: LucidStoresOptions): LucidStoresResult;
|
|
@@ -15,10 +15,10 @@ import { lucidAccountStore } from './lucid_account_store.js';
|
|
|
15
15
|
* )
|
|
16
16
|
* ```
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
* vivem numa conexão/schema próprios, passe
|
|
20
|
-
* (`{ OrgModel, MemberModel, InvitationModel }`) — os defaults
|
|
21
|
-
* `static connection`.
|
|
18
|
+
* Organizations é ligada por padrão (models default da lib); `organizations:
|
|
19
|
+
* false` desliga. Se as tabelas de auth vivem numa conexão/schema próprios, passe
|
|
20
|
+
* o trio explícito (`{ OrgModel, MemberModel, InvitationModel }`) — os defaults
|
|
21
|
+
* não declaram `static connection`.
|
|
22
22
|
*/
|
|
23
23
|
export function lucidStores(models, options = {}) {
|
|
24
24
|
const accountStore = lucidAccountStore(models.account, {
|
|
@@ -119,7 +119,11 @@ export declare function checkAccessTokens(input: DoctorInput): Finding | null;
|
|
|
119
119
|
/**
|
|
120
120
|
* Organizations (multi-tenancy). Informa se a capacidade está disponível
|
|
121
121
|
* (store expõe createOrg) e avisa se `organizations.enabled: true` no config mas
|
|
122
|
-
* a capacidade não está presente no store
|
|
122
|
+
* a capacidade não está presente no store.
|
|
123
|
+
*
|
|
124
|
+
* Com `lucidAccountStore()`, a capability existe POR PADRÃO (models default da
|
|
125
|
+
* lib); o aviso abaixo só é alcançável para store não-lucid que não a implementa
|
|
126
|
+
* ou para um store lucid construído com `organizationModels: false` (opt-out).
|
|
123
127
|
*/
|
|
124
128
|
export declare function checkOrganizations(input: DoctorInput): Finding | null;
|
|
125
129
|
/**
|
|
@@ -488,7 +488,11 @@ export function checkAccessTokens(input) {
|
|
|
488
488
|
/**
|
|
489
489
|
* Organizations (multi-tenancy). Informa se a capacidade está disponível
|
|
490
490
|
* (store expõe createOrg) e avisa se `organizations.enabled: true` no config mas
|
|
491
|
-
* a capacidade não está presente no store
|
|
491
|
+
* a capacidade não está presente no store.
|
|
492
|
+
*
|
|
493
|
+
* Com `lucidAccountStore()`, a capability existe POR PADRÃO (models default da
|
|
494
|
+
* lib); o aviso abaixo só é alcançável para store não-lucid que não a implementa
|
|
495
|
+
* ou para um store lucid construído com `organizationModels: false` (opt-out).
|
|
492
496
|
*/
|
|
493
497
|
export function checkOrganizations(input) {
|
|
494
498
|
const cfg = input.authkitConfig;
|
|
@@ -501,17 +505,26 @@ export function checkOrganizations(input) {
|
|
|
501
505
|
return {
|
|
502
506
|
level: 'warn',
|
|
503
507
|
message: 'organizations.enabled: true, but the accountStore has no OrganizationsCapability — ' +
|
|
504
|
-
'
|
|
505
|
-
'
|
|
506
|
-
'
|
|
508
|
+
'with `lucidAccountStore()` it is present by default; if you passed ' +
|
|
509
|
+
'`organizationModels: false`, drop the opt-out (or pass the explicit ' +
|
|
510
|
+
'`{ OrgModel, MemberModel, InvitationModel }` trio when the auth tables live on their ' +
|
|
511
|
+
'own connection/schema). Expected tables: auth_organizations, ' +
|
|
507
512
|
'auth_organization_members, auth_organization_invitations.',
|
|
508
513
|
};
|
|
509
514
|
}
|
|
510
515
|
if (storeSupports) {
|
|
511
516
|
const roles = cfg.organizations?.roles ?? ['owner', 'admin', 'member'];
|
|
517
|
+
// Origem dos models: default da lib (organizationModels ausente/true) ou
|
|
518
|
+
// trio explícito do host. Ausente em store não-lucid → wording genérica.
|
|
519
|
+
const source = store.__organizationModelsSource;
|
|
520
|
+
const models = source === 'default'
|
|
521
|
+
? 'lib defaults (organizationModels omitted or true)'
|
|
522
|
+
: source === 'explicit'
|
|
523
|
+
? 'host-provided explicit trio'
|
|
524
|
+
: 'lib defaults or host-provided models';
|
|
512
525
|
return {
|
|
513
526
|
level: 'ok',
|
|
514
|
-
message: `organizations capability present (roles: ${roles.join(', ')}).`,
|
|
527
|
+
message: `organizations capability present (models: ${models}; roles: ${roles.join(', ')}).`,
|
|
515
528
|
};
|
|
516
529
|
}
|
|
517
530
|
// Auto mode (enabled === undefined) and store doesn't support — silently ok (opt-in).
|
|
@@ -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.
|
|
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",
|