@adonis-agora/authkit-server 0.42.0 → 0.44.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/build/src/define_config.d.ts +27 -0
- package/build/src/define_config.js +1 -0
- package/build/src/host/account_roles.d.ts +26 -0
- package/build/src/host/account_roles.js +18 -0
- package/build/src/host/admin_console/admin_shell_controller.js +2 -1
- package/build/src/host/register_auth_host.js +5 -1
- package/build/src/provider/oidc_service.js +4 -1
- package/build/src/provider/token_exchange.d.ts +15 -0
- package/build/src/provider/token_exchange.js +9 -1
- package/package.json +1 -1
|
@@ -561,6 +561,21 @@ export interface AuthServerConfigInput {
|
|
|
561
561
|
ttl?: TtlConfig;
|
|
562
562
|
/** Nome da CLAIM (não do scope) onde os papéis globais são emitidos. Default: 'roles'. */
|
|
563
563
|
globalRolesClaim?: string;
|
|
564
|
+
/**
|
|
565
|
+
* Resolves the global-roles claim at token-mint time. Return the roles to embed
|
|
566
|
+
* in the `<globalRolesClaim>` claim for a first-party client. Lets the host source
|
|
567
|
+
* roles from an external authority (e.g. @adonis-agora/authz) or a custom store
|
|
568
|
+
* instead of the account's stored `globalRoles`. Default when omitted:
|
|
569
|
+
* `account.globalRoles ?? []` (unchanged behavior).
|
|
570
|
+
*/
|
|
571
|
+
resolveTokenRoles?: (account: AuthAccount, context: {
|
|
572
|
+
clientId?: string;
|
|
573
|
+
activeOrg?: {
|
|
574
|
+
orgId: string;
|
|
575
|
+
orgSlug: string;
|
|
576
|
+
orgRole: string;
|
|
577
|
+
} | null;
|
|
578
|
+
}) => string[] | Promise<string[]>;
|
|
564
579
|
cookieKeys?: string[];
|
|
565
580
|
observability?: ObservabilityConfig;
|
|
566
581
|
/** Contrato primário de identidade. Deriva findAccount/verifyCredentials do provider. */
|
|
@@ -769,6 +784,18 @@ export interface ResolvedServerConfig {
|
|
|
769
784
|
session: number;
|
|
770
785
|
};
|
|
771
786
|
globalRolesClaim: string;
|
|
787
|
+
/**
|
|
788
|
+
* Resolves the global-roles claim at token-mint time (first-party only). When
|
|
789
|
+
* omitted, the mint falls back to `account.globalRoles ?? []` (unchanged behavior).
|
|
790
|
+
*/
|
|
791
|
+
resolveTokenRoles?: (account: AuthAccount, context: {
|
|
792
|
+
clientId?: string;
|
|
793
|
+
activeOrg?: {
|
|
794
|
+
orgId: string;
|
|
795
|
+
orgSlug: string;
|
|
796
|
+
orgRole: string;
|
|
797
|
+
} | null;
|
|
798
|
+
}) => string[] | Promise<string[]>;
|
|
772
799
|
cookieKeys: string[];
|
|
773
800
|
observability: ObservabilityConfig;
|
|
774
801
|
findAccount: (sub: string) => Promise<AuthAccount | null>;
|
|
@@ -303,6 +303,7 @@ export function defineConfig(config) {
|
|
|
303
303
|
session: toSeconds(config.ttl?.session, 604800),
|
|
304
304
|
},
|
|
305
305
|
globalRolesClaim: config.globalRolesClaim ?? "roles",
|
|
306
|
+
resolveTokenRoles: config.resolveTokenRoles,
|
|
306
307
|
cookieKeys: config.cookieKeys ?? [],
|
|
307
308
|
observability: config.observability ?? {},
|
|
308
309
|
findAccount: (sub) => config.accountStore.findById(sub),
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AuthAccount } from '../accounts/account_store.js';
|
|
2
|
+
/** The slice of the resolved config the role resolution needs (structurally typed). */
|
|
3
|
+
type RoleResolverConfig = {
|
|
4
|
+
resolveTokenRoles?: (account: AuthAccount, context: {
|
|
5
|
+
clientId?: string;
|
|
6
|
+
activeOrg?: {
|
|
7
|
+
orgId: string;
|
|
8
|
+
orgSlug: string;
|
|
9
|
+
orgRole: string;
|
|
10
|
+
} | null;
|
|
11
|
+
}) => string[] | Promise<string[]>;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* An account's effective roles for host-side gating (the admin console).
|
|
15
|
+
*
|
|
16
|
+
* When the host configures `resolveTokenRoles`, that hook is the single source of an account's roles
|
|
17
|
+
* — the same one the OIDC `roles` claim is minted from — so console access tracks the host's role
|
|
18
|
+
* authority (e.g. an app that keeps roles in its own table via `@adonis-agora/authz`) instead of the
|
|
19
|
+
* account's stored `globalRoles`. Falls back to `account.globalRoles ?? []` when no hook is set, so
|
|
20
|
+
* default behavior is unchanged.
|
|
21
|
+
*
|
|
22
|
+
* The console is not OIDC-client- or org-scoped for role purposes, so the hook is called with an
|
|
23
|
+
* empty context (`clientId: undefined`, `activeOrg: null`).
|
|
24
|
+
*/
|
|
25
|
+
export declare function resolveAccountRoles(cfg: RoleResolverConfig, account: AuthAccount): Promise<string[]>;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An account's effective roles for host-side gating (the admin console).
|
|
3
|
+
*
|
|
4
|
+
* When the host configures `resolveTokenRoles`, that hook is the single source of an account's roles
|
|
5
|
+
* — the same one the OIDC `roles` claim is minted from — so console access tracks the host's role
|
|
6
|
+
* authority (e.g. an app that keeps roles in its own table via `@adonis-agora/authz`) instead of the
|
|
7
|
+
* account's stored `globalRoles`. Falls back to `account.globalRoles ?? []` when no hook is set, so
|
|
8
|
+
* default behavior is unchanged.
|
|
9
|
+
*
|
|
10
|
+
* The console is not OIDC-client- or org-scoped for role purposes, so the hook is called with an
|
|
11
|
+
* empty context (`clientId: undefined`, `activeOrg: null`).
|
|
12
|
+
*/
|
|
13
|
+
export async function resolveAccountRoles(cfg, account) {
|
|
14
|
+
if (cfg.resolveTokenRoles) {
|
|
15
|
+
return cfg.resolveTokenRoles(account, { clientId: undefined, activeOrg: null });
|
|
16
|
+
}
|
|
17
|
+
return account.globalRoles ?? [];
|
|
18
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { extname } from 'node:path';
|
|
3
3
|
import { getAdminPrefix } from '../admin_prefix.js';
|
|
4
|
+
import { resolveAccountRoles } from '../account_roles.js';
|
|
4
5
|
// ─── Shell HTML cache ─────────────────────────────────────────────────────────
|
|
5
6
|
/**
|
|
6
7
|
* Points to the Vite-built index.html inside build/host/ui-dist/.
|
|
@@ -120,7 +121,7 @@ export default class AdminShellController {
|
|
|
120
121
|
currentUser = {
|
|
121
122
|
id: account.id,
|
|
122
123
|
email: account.email,
|
|
123
|
-
roles:
|
|
124
|
+
roles: await resolveAccountRoles(cfg, account),
|
|
124
125
|
};
|
|
125
126
|
}
|
|
126
127
|
}
|
|
@@ -2,6 +2,7 @@ import { resolveRateLimit } from '../define_config.js';
|
|
|
2
2
|
import { createAuthThrottles } from './rate_limit.js';
|
|
3
3
|
import { ACCOUNT_SESSION_KEY } from './middleware/account_auth.js';
|
|
4
4
|
import { accountHome } from './account_home.js';
|
|
5
|
+
import { resolveAccountRoles } from './account_roles.js';
|
|
5
6
|
import { adminApiGuard } from './admin_api/admin_api_guard.js';
|
|
6
7
|
import { setAdminPrefix, normalizeAdminPrefix, setAdminApiPrefix, normalizeAdminApiPrefix, } from './admin_prefix.js';
|
|
7
8
|
import { resolveRuntimeSettings } from './runtime_settings.js';
|
|
@@ -112,7 +113,10 @@ export const adminGuard = async (ctx, next) => {
|
|
|
112
113
|
}
|
|
113
114
|
const allowed = cfg.admin.roles;
|
|
114
115
|
const account = await cfg.accountStore.findById(accountId);
|
|
115
|
-
|
|
116
|
+
// Resolve roles through the host's role authority (`resolveTokenRoles`) when set — the same source
|
|
117
|
+
// the token claim is minted from — so an app-role admin reaches the console. Falls back to the
|
|
118
|
+
// account's stored `globalRoles` when no hook is configured.
|
|
119
|
+
const roles = account ? await resolveAccountRoles(cfg, account) : [];
|
|
116
120
|
const isAdmin = roles.some((r) => allowed.includes(r));
|
|
117
121
|
if (!isAdmin) {
|
|
118
122
|
// Evita vazar a existência do console admin: redireciona para o accountHome
|
|
@@ -98,7 +98,9 @@ export class OidcService {
|
|
|
98
98
|
};
|
|
99
99
|
// roles/org_* são dados de autorização interna: só para first-party.
|
|
100
100
|
if (firstParty) {
|
|
101
|
-
base[config.globalRolesClaim] =
|
|
101
|
+
base[config.globalRolesClaim] = config.resolveTokenRoles
|
|
102
|
+
? await config.resolveTokenRoles(user, { clientId, activeOrg })
|
|
103
|
+
: (user.globalRoles ?? []);
|
|
102
104
|
// Emite claims de org somente quando há uma org ativa na sessão.
|
|
103
105
|
if (activeOrg) {
|
|
104
106
|
base['org_id'] = activeOrg.orgId;
|
|
@@ -115,6 +117,7 @@ export class OidcService {
|
|
|
115
117
|
registerTokenExchange(provider, {
|
|
116
118
|
findAccount: config.findAccount,
|
|
117
119
|
globalRolesClaim: config.globalRolesClaim,
|
|
120
|
+
resolveTokenRoles: config.resolveTokenRoles,
|
|
118
121
|
// Resource indicators (RFC 8707) suportados: o `audience` default + cada
|
|
119
122
|
// resource declarado. Usado para validar `audience`/`resource` no pedido de
|
|
120
123
|
// token-exchange — alvos fora desta lista são rejeitados (invalid_target).
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AuditSink } from '../audit/audit_sink.js';
|
|
2
|
+
import type { AuthAccount } from '../accounts/account_store.js';
|
|
2
3
|
export interface TokenExchangeAccount {
|
|
3
4
|
id: string;
|
|
4
5
|
email?: string;
|
|
@@ -8,6 +9,20 @@ export interface TokenExchangeAccount {
|
|
|
8
9
|
export interface TokenExchangeDeps {
|
|
9
10
|
findAccount: (sub: string) => Promise<TokenExchangeAccount | null>;
|
|
10
11
|
globalRolesClaim: string;
|
|
12
|
+
/**
|
|
13
|
+
* Resolves the global-roles claim at token-mint time. When omitted, falls back to
|
|
14
|
+
* `target.globalRoles ?? []` (unchanged behavior). Mirrors the mint-time hook used
|
|
15
|
+
* by the authorization-code flow so impersonated tokens source roles from the same
|
|
16
|
+
* authority (e.g. @adonis-agora/authz) or custom store.
|
|
17
|
+
*/
|
|
18
|
+
resolveTokenRoles?: (account: AuthAccount, context: {
|
|
19
|
+
clientId?: string;
|
|
20
|
+
activeOrg?: {
|
|
21
|
+
orgId: string;
|
|
22
|
+
orgSlug: string;
|
|
23
|
+
orgRole: string;
|
|
24
|
+
} | null;
|
|
25
|
+
}) => string[] | Promise<string[]>;
|
|
11
26
|
adminRole?: string;
|
|
12
27
|
/**
|
|
13
28
|
* Resource indicators (RFC 8707) suportados pelo provider. Quando o pedido traz
|
|
@@ -82,12 +82,20 @@ export function registerTokenExchange(provider, deps) {
|
|
|
82
82
|
}
|
|
83
83
|
const at = new provider.AccessToken({ accountId: target.id, client, scope });
|
|
84
84
|
const accessToken = await at.save();
|
|
85
|
+
// Token exchange is not tied to a browser session, so there is no active org
|
|
86
|
+
// context here — roles are resolved for the impersonated target with clientId only.
|
|
87
|
+
const roles = deps.resolveTokenRoles
|
|
88
|
+
? await deps.resolveTokenRoles(target, {
|
|
89
|
+
clientId: client?.clientId,
|
|
90
|
+
activeOrg: null,
|
|
91
|
+
})
|
|
92
|
+
: (target.globalRoles ?? []);
|
|
85
93
|
const idToken = new provider.IdToken({
|
|
86
94
|
sub: target.id,
|
|
87
95
|
email: target.email,
|
|
88
96
|
email_verified: true,
|
|
89
97
|
name: target.name,
|
|
90
|
-
[deps.globalRolesClaim]:
|
|
98
|
+
[deps.globalRolesClaim]: roles,
|
|
91
99
|
}, { ctx });
|
|
92
100
|
idToken.scope = scope;
|
|
93
101
|
idToken.set('act', { sub: actor.id });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adonis-agora/authkit-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.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",
|