@adonis-agora/authkit-client 1.0.0-rc.5 → 1.0.0-rc.7
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/CHANGELOG.md +39 -0
- package/build/index.d.ts +12 -2
- package/build/index.js +6 -0
- package/build/providers/authkit_client_provider.d.ts +12 -0
- package/build/providers/authkit_client_provider.js +36 -0
- package/build/src/agora_consoles.d.ts +31 -0
- package/build/src/agora_consoles.js +40 -0
- package/build/src/authenticator.js +4 -1
- package/build/src/define_config.d.ts +17 -0
- package/build/src/define_config.js +1 -0
- package/build/src/identity_bridge.d.ts +46 -0
- package/build/src/identity_bridge.js +99 -0
- package/build/src/org_labels.d.ts +49 -0
- package/build/src/org_labels.js +145 -0
- package/build/src/register_oidc_client.d.ts +9 -0
- package/build/src/register_oidc_client.js +22 -0
- package/build/src/resolvers/identity.d.ts +3 -3
- package/build/src/resolvers/identity.js +6 -4
- package/build/src/revocation/revocation_store.d.ts +9 -3
- package/build/src/revocation/revocation_store.js +0 -0
- package/build/src/tenant_token.d.ts +5 -1
- package/build/src/tenant_token.js +7 -4
- package/build/src/tenant_verifier.d.ts +88 -0
- package/build/src/tenant_verifier.js +104 -0
- package/build/src/webhook_receiver.d.ts +101 -0
- package/build/src/webhook_receiver.js +161 -0
- package/package.json +3 -3
- package/skills/authkit-rp-client/SKILL.md +28 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# @adonis-agora/authkit-client
|
|
2
2
|
|
|
3
|
+
## 1.0.0-rc.7
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#495](https://github.com/DavideCarvalho/adonis-agora-authkit/pull/495) [`4830805`](https://github.com/DavideCarvalho/adonis-agora-authkit/commit/4830805039c3f0e978c403d3062275a892704eb7) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Separate servers: every piece of an Agora deployment can run without authkit-server in its process.
|
|
8
|
+
|
|
9
|
+
- `authkitTenantVerifier({ issuer, resource, environment? })` (authkit-client) verifies service-tenant tokens on any server against the issuer's discovered JWKS and returns `{ partition, environment?, capabilities, clientId, expiresAt }` or `null`. `authkitTenantToken` now takes the token endpoint only from discovery (no guessed path).
|
|
10
|
+
- Service tenants get an optional `environment` (console, Admin API, SDK, `--environment`), emitted as the `environment` claim. Both verifiers take `environment` and, when it is set, accept only tokens claiming exactly that environment (fail closed); authkit-server's in-process verifier also returns `environment`. One service-tenant client gets tokens for every `urn:agora:*` resource declared.
|
|
11
|
+
- `registerOidcClient` announces `POST /auth/logout` as `account.signOut` on the `agora.consoles` registry (skipped when authkit-server runs in the app), so every console shows a sign-out.
|
|
12
|
+
- `Identity.actor` (`{ sub, email? }`) from the RFC 8693 `act` claim of an impersonated token (`deriveActor` in authkit-core).
|
|
13
|
+
- Org labels: authkit-server serves `GET /api/orgs/labels?ids=` (first-party confidential clients, Basic auth) and emits `org_name` for the active org; authkit-client installs a cached `Context.setTenantLabeler` (`orgLabels` option, `authkitOrgLabels`).
|
|
14
|
+
- Webhooks: the body carries `id` and `actorId`, `events.webhook` accepts a list; authkit-client's `registerAuthkitWebhook` / `authkitWebhookHandler` verify the HMAC, the `ts` window and the id, and re-emit on the diagnostics bus as `agora:authkit:<type>` with the in-process payload.
|
|
15
|
+
- `identityFromAccessToken`, `bindAuthkitIdentity` and `bridgeAuthkitToken` put a verified authkit bearer token on `ctx.authkit`.
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- Updated dependencies [[`4830805`](https://github.com/DavideCarvalho/adonis-agora-authkit/commit/4830805039c3f0e978c403d3062275a892704eb7)]:
|
|
20
|
+
- @adonis-agora/authkit-core@1.0.0-rc.3
|
|
21
|
+
|
|
22
|
+
## 1.0.0-rc.6
|
|
23
|
+
|
|
24
|
+
### Minor Changes
|
|
25
|
+
|
|
26
|
+
- [#474](https://github.com/DavideCarvalho/adonis-agora-authkit/pull/474) [`df9e49d`](https://github.com/DavideCarvalho/adonis-agora-authkit/commit/df9e49d7817c03a7cce4cac1df709c7788d6307c) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - AuthKit's tables can live in their own database schema: `schema: { name: 'auth' }` in `defineConfig` puts every table AuthKit owns in `auth` (`"auth"."auth_settings"`) while the app stays in `public`. The schema is written into every statement, so it works on any connection and inside the app's own transaction, with no `searchPath` (which leaks across clients behind PgBouncer in transaction mode).
|
|
27
|
+
|
|
28
|
+
- The boot creates the schema (`CREATE SCHEMA IF NOT EXISTS`; MySQL: `CREATE DATABASE IF NOT EXISTS`) when `autoManage` is on, and fails with a message naming the schema and the grant when the role cannot create it. `ensureAuthkitSchema(db, { schema })` does the same from a migration.
|
|
29
|
+
- Covers the OIDC adapter table (`adapters.database({ schema })` overrides), the Lucid keystore (`jwks.store: { driver: 'lucid', schema }` overrides), the account store's tables (MFA, password history, organizations), runtime settings, session revocations, the personal-agent tables and agent extensions (`PersonalAgentHost.table(name)`, `AuthkitTable.create(table, { table })`, `ensureAuthkitTables(db, tables, { schema })`; Poppy uses them and `ensurePoppySchema(db, { schema })`).
|
|
30
|
+
- authkit-client: `lucidRevocationStore({ schema })`.
|
|
31
|
+
- SQLite with `schema.name` fails the boot instead of ignoring the option.
|
|
32
|
+
- `agora:doctor` gains `authkit.db-schema`: the schema exists, the tables are in it, the connection can write there. Silent without `schema.name`.
|
|
33
|
+
- The key-rotation lock key includes the schema, so two installs in two schemas of one database do not share it.
|
|
34
|
+
|
|
35
|
+
Without `schema.name` nothing changes.
|
|
36
|
+
|
|
37
|
+
### Patch Changes
|
|
38
|
+
|
|
39
|
+
- Updated dependencies [[`df9e49d`](https://github.com/DavideCarvalho/adonis-agora-authkit/commit/df9e49d7817c03a7cce4cac1df709c7788d6307c)]:
|
|
40
|
+
- @adonis-agora/authkit-core@1.0.0-rc.2
|
|
41
|
+
|
|
3
42
|
## 1.0.0-rc.5
|
|
4
43
|
|
|
5
44
|
### Minor Changes
|
package/build/index.d.ts
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
export type { Identity } from '@adonis-agora/authkit-core';
|
|
1
|
+
export type { Identity, IdentityActor } from '@adonis-agora/authkit-core';
|
|
2
2
|
export { configure } from './commands/configure.js';
|
|
3
3
|
export type { AuthkitClientLogger, AuthkitClientManagerDeps, } from './providers/authkit_client_provider.js';
|
|
4
4
|
export { AuthkitClientManager } from './providers/authkit_client_provider.js';
|
|
5
5
|
export type { AccountExtensionsAnswer, AccountExtensionsVerifier, ServeAccountExtensionsInput, } from './src/account_extensions.js';
|
|
6
6
|
export { ACCOUNT_EXTENSIONS_BINDING, ClientAccountExtensionRegistry, serveAccountExtensions, verifyAccountExtensionsToken, } from './src/account_extensions.js';
|
|
7
|
+
export type { AgoraConsoleSignOut, ConsolesAppLike } from './src/agora_consoles.js';
|
|
8
|
+
export { AGORA_CONSOLES_BINDING, announceAgoraSignOut } from './src/agora_consoles.js';
|
|
7
9
|
export { Authenticator } from './src/authenticator.js';
|
|
8
10
|
export type { AuthorizationRequest, CompleteAuthorizationParams, CompletedAuthorization, CreateAuthorizationRequestParams, OidcCallbackErrorCode, PendingAuthorization, } from './src/authorization.js';
|
|
9
11
|
export { completeAuthorization, createAuthorizationRequest, OidcCallbackError, } from './src/authorization.js';
|
|
10
12
|
export type { SessionIndex, SessionIndexEntry, ValidatedLogoutToken, ValidateLogoutTokenOptions, } from './src/backchannel_logout.js';
|
|
11
13
|
export { BACKCHANNEL_LOGOUT_EVENT, InMemorySessionIndex, InvalidLogoutTokenError, validateLogoutToken, } from './src/backchannel_logout.js';
|
|
12
|
-
export type { BackchannelLogoutCallback, BackchannelLogoutInput, ClientAccountConsoleConfig, ClientConfigInput, ResolvedClientConfig, } from './src/define_config.js';
|
|
14
|
+
export type { BackchannelLogoutCallback, BackchannelLogoutInput, ClientAccountConsoleConfig, ClientConfigInput, ClientOrgLabelsConfig, ResolvedClientConfig, } from './src/define_config.js';
|
|
13
15
|
export { defineConfig, resolvers } from './src/define_config.js';
|
|
14
16
|
export type { DiscoverOptions, OidcEndpoints } from './src/discovery.js';
|
|
15
17
|
export { conventionEndpoints, discoverEndpoints, OidcDiscoveryError } from './src/discovery.js';
|
|
@@ -25,6 +27,8 @@ export type { ResiliencePolicy } from './src/http/resilient_fetch.js';
|
|
|
25
27
|
export { resilientFetch } from './src/http/resilient_fetch.js';
|
|
26
28
|
export type { IdTokenClaims, ValidateIdTokenOptions } from './src/id_token.js';
|
|
27
29
|
export { InvalidIdTokenError, validateIdToken } from './src/id_token.js';
|
|
30
|
+
export type { AuthkitAccessTokenIdentityOptions } from './src/identity_bridge.js';
|
|
31
|
+
export { bindAuthkitIdentity, bridgeAuthkitToken, identityFromAccessToken, } from './src/identity_bridge.js';
|
|
28
32
|
export { clearJwksCache } from './src/jwks.js';
|
|
29
33
|
export type { LucidMirrorOptions } from './src/lucid_mirror.js';
|
|
30
34
|
export { lucidMirror } from './src/lucid_mirror.js';
|
|
@@ -35,13 +39,19 @@ export { default as RequireOrgMiddleware } from './src/middleware/require_org_mi
|
|
|
35
39
|
export type { ClientTenancyOptions, ContextTenant, ContextTenantLink, TenantInput, } from './src/observability/context_bridge.js';
|
|
36
40
|
export type { AuthorizeParams, EndSessionParams, ExchangeParams, RefreshParams, } from './src/oidc_login.js';
|
|
37
41
|
export { buildAuthorizeUrl, buildEndSessionUrl, exchangeCode, exchangeToken, generatePkce, refreshTokens, } from './src/oidc_login.js';
|
|
42
|
+
export type { AuthkitOrgLabelsOptions, OrgLabelResolver } from './src/org_labels.js';
|
|
43
|
+
export { authkitOrgLabels, installOrgLabeler, ORG_LABELS_PATH } from './src/org_labels.js';
|
|
38
44
|
export type { PostLoginRedirects, RegisterOidcClientOptions, } from './src/register_oidc_client.js';
|
|
39
45
|
export { registerAccountExtensionsEndpoint, registerOidcClient, } from './src/register_oidc_client.js';
|
|
46
|
+
export { buildIdentityFromClaims as identityFromClaims } from './src/resolvers/identity.js';
|
|
40
47
|
export type { LucidRevocationStoreOptions, RevocationStore, } from './src/revocation/revocation_store.js';
|
|
41
48
|
export { DEFAULT_REVOCATION_TABLE, lucidRevocationStore, } from './src/revocation/revocation_store.js';
|
|
42
49
|
export { AuthkitTenantToken, type AuthkitTenantTokenFn, type AuthkitTenantTokenOptions, authkitTenantToken, TenantTokenError, } from './src/tenant_token.js';
|
|
50
|
+
export { AuthkitTenantVerifier, type AuthkitTenantVerifierFn, type AuthkitTenantVerifierOptions, authkitTenantVerifier, type TenantVerifier, type TenantVerifierRequest, type VerifiedTenantToken, } from './src/tenant_verifier.js';
|
|
43
51
|
export type { TokenSet } from './src/types.js';
|
|
44
52
|
export type { ClaimsUser, ResolveUserContext, UserinfoResolverOptions, } from './src/user_resolvers.js';
|
|
45
53
|
export { createUserinfoResolver, identityToUser, } from './src/user_resolvers.js';
|
|
46
54
|
export type { JwtAccessTokenClaims, VerifyJwtAccessTokenOptions, } from './src/verify_access_token.js';
|
|
47
55
|
export { verifyJwtAccessToken } from './src/verify_access_token.js';
|
|
56
|
+
export type { AuthkitDiagnosticPayload, AuthkitWebhookEvent, AuthkitWebhookOptions, WebhookRejection, WebhookReplayStore, WebhookVerification, } from './src/webhook_receiver.js';
|
|
57
|
+
export { authkitWebhookHandler, emitAuthkitEvent, memoryReplayStore, registerAuthkitWebhook, verifyAuthkitWebhook, } from './src/webhook_receiver.js';
|
package/build/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { configure } from './commands/configure.js';
|
|
2
2
|
export { AuthkitClientManager } from './providers/authkit_client_provider.js';
|
|
3
3
|
export { ACCOUNT_EXTENSIONS_BINDING, ClientAccountExtensionRegistry, serveAccountExtensions, verifyAccountExtensionsToken, } from './src/account_extensions.js';
|
|
4
|
+
export { AGORA_CONSOLES_BINDING, announceAgoraSignOut } from './src/agora_consoles.js';
|
|
4
5
|
export { Authenticator } from './src/authenticator.js';
|
|
5
6
|
export { completeAuthorization, createAuthorizationRequest, OidcCallbackError, } from './src/authorization.js';
|
|
6
7
|
export { BACKCHANNEL_LOGOUT_EVENT, InMemorySessionIndex, InvalidLogoutTokenError, validateLogoutToken, } from './src/backchannel_logout.js';
|
|
@@ -12,14 +13,19 @@ export { authkitDashboardAuthorize, isAuthkitAdmin, } from './src/host/dashboard
|
|
|
12
13
|
export { getAuthkit } from './src/host/request_authenticator.js';
|
|
13
14
|
export { resilientFetch } from './src/http/resilient_fetch.js';
|
|
14
15
|
export { InvalidIdTokenError, validateIdToken } from './src/id_token.js';
|
|
16
|
+
export { bindAuthkitIdentity, bridgeAuthkitToken, identityFromAccessToken, } from './src/identity_bridge.js';
|
|
15
17
|
export { clearJwksCache } from './src/jwks.js';
|
|
16
18
|
export { lucidMirror } from './src/lucid_mirror.js';
|
|
17
19
|
export { default as AuthkitContextMiddleware } from './src/middleware/authkit_context_middleware.js';
|
|
18
20
|
export { default as BackchannelRevocationMiddleware } from './src/middleware/backchannel_revocation_middleware.js';
|
|
19
21
|
export { default as RequireOrgMiddleware } from './src/middleware/require_org_middleware.js';
|
|
20
22
|
export { buildAuthorizeUrl, buildEndSessionUrl, exchangeCode, exchangeToken, generatePkce, refreshTokens, } from './src/oidc_login.js';
|
|
23
|
+
export { authkitOrgLabels, installOrgLabeler, ORG_LABELS_PATH } from './src/org_labels.js';
|
|
21
24
|
export { registerAccountExtensionsEndpoint, registerOidcClient, } from './src/register_oidc_client.js';
|
|
25
|
+
export { buildIdentityFromClaims as identityFromClaims } from './src/resolvers/identity.js';
|
|
22
26
|
export { DEFAULT_REVOCATION_TABLE, lucidRevocationStore, } from './src/revocation/revocation_store.js';
|
|
23
27
|
export { AuthkitTenantToken, authkitTenantToken, TenantTokenError, } from './src/tenant_token.js';
|
|
28
|
+
export { AuthkitTenantVerifier, authkitTenantVerifier, } from './src/tenant_verifier.js';
|
|
24
29
|
export { createUserinfoResolver, identityToUser, } from './src/user_resolvers.js';
|
|
25
30
|
export { verifyJwtAccessToken } from './src/verify_access_token.js';
|
|
31
|
+
export { authkitWebhookHandler, emitAuthkitEvent, memoryReplayStore, registerAuthkitWebhook, verifyAuthkitWebhook, } from './src/webhook_receiver.js';
|
|
@@ -175,6 +175,12 @@ export declare class AuthkitClientManager {
|
|
|
175
175
|
* is `resolvers.jwt()` (local verify, no network).
|
|
176
176
|
*/
|
|
177
177
|
eagerTenancy(): boolean;
|
|
178
|
+
/**
|
|
179
|
+
* Registers the org labeler (see `orgLabels` in `config/authkit_client.ts`) on
|
|
180
|
+
* `@adonis-agora/context`. `false` when it is off, the client has no secret, or the
|
|
181
|
+
* context is not installed.
|
|
182
|
+
*/
|
|
183
|
+
installOrgLabeler(): boolean;
|
|
178
184
|
/**
|
|
179
185
|
* The request's {@link Authenticator}. `options.resolver` replaces the configured
|
|
180
186
|
* resolver for this authenticator only (e.g. a guard for bearer-token API routes).
|
|
@@ -187,4 +193,10 @@ export default class AuthkitClientProvider {
|
|
|
187
193
|
protected app: ApplicationService;
|
|
188
194
|
constructor(app: ApplicationService);
|
|
189
195
|
register(): void;
|
|
196
|
+
/**
|
|
197
|
+
* Org labels for the consoles' org pickers, when authkit-server is not in this app (it
|
|
198
|
+
* serves them locally through `tenancy.label: authkitTenantLabels()`). Best effort: a
|
|
199
|
+
* config that cannot resolve here fails later, on first use, as before.
|
|
200
|
+
*/
|
|
201
|
+
ready(): Promise<void>;
|
|
190
202
|
}
|
|
@@ -14,6 +14,7 @@ import { validateIdToken, } from '../src/id_token.js';
|
|
|
14
14
|
import { getJwks } from '../src/jwks.js';
|
|
15
15
|
import { contextTenancyEnabled } from '../src/observability/context_bridge.js';
|
|
16
16
|
import { exchangeToken, refreshTokens } from '../src/oidc_login.js';
|
|
17
|
+
import { authkitOrgLabels, installOrgLabeler } from '../src/org_labels.js';
|
|
17
18
|
/** Margem (ms) antes do `expiresAt` em que o access token é renovado proativamente. */
|
|
18
19
|
const REFRESH_SKEW_MS = 60_000;
|
|
19
20
|
/** Aceita só o formato COM vínculo; o formato legado (TokenSet cru) vira `null`. */
|
|
@@ -509,6 +510,25 @@ export class AuthkitClientManager {
|
|
|
509
510
|
return eager;
|
|
510
511
|
return this.config.resolverFactory?.kind === 'jwt' && contextTenancyEnabled();
|
|
511
512
|
}
|
|
513
|
+
/**
|
|
514
|
+
* Registers the org labeler (see `orgLabels` in `config/authkit_client.ts`) on
|
|
515
|
+
* `@adonis-agora/context`. `false` when it is off, the client has no secret, or the
|
|
516
|
+
* context is not installed.
|
|
517
|
+
*/
|
|
518
|
+
installOrgLabeler() {
|
|
519
|
+
const cfg = this.config;
|
|
520
|
+
if (cfg.orgLabels === false || !cfg.clientSecret)
|
|
521
|
+
return false;
|
|
522
|
+
const options = cfg.orgLabels ?? {};
|
|
523
|
+
return installOrgLabeler(authkitOrgLabels({
|
|
524
|
+
issuer: cfg.issuer,
|
|
525
|
+
clientId: cfg.clientId,
|
|
526
|
+
clientSecret: cfg.clientSecret,
|
|
527
|
+
...(options.endpoint ? { endpoint: options.endpoint } : {}),
|
|
528
|
+
...(options.ttlMs !== undefined ? { ttlMs: options.ttlMs } : {}),
|
|
529
|
+
...(this.deps.fetchImpl ? { fetch: this.deps.fetchImpl } : {}),
|
|
530
|
+
}));
|
|
531
|
+
}
|
|
512
532
|
/**
|
|
513
533
|
* The request's {@link Authenticator}. `options.resolver` replaces the configured
|
|
514
534
|
* resolver for this authenticator only (e.g. a guard for bearer-token API routes).
|
|
@@ -559,6 +579,22 @@ export default class AuthkitClientProvider {
|
|
|
559
579
|
this.app.container.singleton(ACCOUNT_EXTENSIONS_BINDING, async () => registry);
|
|
560
580
|
}
|
|
561
581
|
}
|
|
582
|
+
/**
|
|
583
|
+
* Org labels for the consoles' org pickers, when authkit-server is not in this app (it
|
|
584
|
+
* serves them locally through `tenancy.label: authkitTenantLabels()`). Best effort: a
|
|
585
|
+
* config that cannot resolve here fails later, on first use, as before.
|
|
586
|
+
*/
|
|
587
|
+
async ready() {
|
|
588
|
+
if (this.app.container.hasBinding('authkit.server'))
|
|
589
|
+
return;
|
|
590
|
+
try {
|
|
591
|
+
const manager = await this.app.container.make(AuthkitClientManager);
|
|
592
|
+
manager.installOrgLabeler();
|
|
593
|
+
}
|
|
594
|
+
catch {
|
|
595
|
+
// no usable config yet: nothing to install
|
|
596
|
+
}
|
|
597
|
+
}
|
|
562
598
|
}
|
|
563
599
|
/** `sub` of a stored id_token, or undefined when there is none / it is unreadable. */
|
|
564
600
|
function subjectOf(idToken) {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `'agora.consoles'` registry every `@adonis-agora` console reads (declared
|
|
3
|
+
* structurally, like in every other lib: nothing is imported). authkit-client only
|
|
4
|
+
* writes `account.signOut` into it, so every console on a relying-party app shows a
|
|
5
|
+
* working sign-out with no per-console config.
|
|
6
|
+
*/
|
|
7
|
+
/** The container binding the registry lives at. */
|
|
8
|
+
export declare const AGORA_CONSOLES_BINDING = "agora.consoles";
|
|
9
|
+
/** Where every console's footer signs the viewer out. */
|
|
10
|
+
export type AgoraConsoleSignOut = {
|
|
11
|
+
action: string;
|
|
12
|
+
method?: 'POST';
|
|
13
|
+
} | {
|
|
14
|
+
href: string;
|
|
15
|
+
};
|
|
16
|
+
/** The slice of an AdonisJS application the announcement needs. */
|
|
17
|
+
export interface ConsolesAppLike {
|
|
18
|
+
container: {
|
|
19
|
+
hasBinding(binding: string): boolean;
|
|
20
|
+
bindValue(binding: string, value: unknown): void;
|
|
21
|
+
make(binding: string): Promise<unknown>;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Announces `action` (a same-origin `POST` route, e.g. `/auth/logout`) as
|
|
26
|
+
* `account.signOut` on the `'agora.consoles'` registry. Creates the registry when no
|
|
27
|
+
* console bound it yet. Keeps a sign-out something else announced first (authkit-server
|
|
28
|
+
* in the same app, or the app itself), skips a registry of another version, and never
|
|
29
|
+
* throws on one. Returns `true` when it wrote the announcement.
|
|
30
|
+
*/
|
|
31
|
+
export declare function announceAgoraSignOut(app: ConsolesAppLike, action: string): Promise<boolean>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `'agora.consoles'` registry every `@adonis-agora` console reads (declared
|
|
3
|
+
* structurally, like in every other lib: nothing is imported). authkit-client only
|
|
4
|
+
* writes `account.signOut` into it, so every console on a relying-party app shows a
|
|
5
|
+
* working sign-out with no per-console config.
|
|
6
|
+
*/
|
|
7
|
+
/** The container binding the registry lives at. */
|
|
8
|
+
export const AGORA_CONSOLES_BINDING = 'agora.consoles';
|
|
9
|
+
function isSameOriginPath(path) {
|
|
10
|
+
return (typeof path === 'string' &&
|
|
11
|
+
path.startsWith('/') &&
|
|
12
|
+
!path.startsWith('//') &&
|
|
13
|
+
!path.includes('\\') &&
|
|
14
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: rejecting control characters is the point
|
|
15
|
+
!/[\u0000-\u001f\u007f]/.test(path));
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Announces `action` (a same-origin `POST` route, e.g. `/auth/logout`) as
|
|
19
|
+
* `account.signOut` on the `'agora.consoles'` registry. Creates the registry when no
|
|
20
|
+
* console bound it yet. Keeps a sign-out something else announced first (authkit-server
|
|
21
|
+
* in the same app, or the app itself), skips a registry of another version, and never
|
|
22
|
+
* throws on one. Returns `true` when it wrote the announcement.
|
|
23
|
+
*/
|
|
24
|
+
export async function announceAgoraSignOut(app, action) {
|
|
25
|
+
if (!isSameOriginPath(action)) {
|
|
26
|
+
throw new Error(`announceAgoraSignOut: sign-out action ${JSON.stringify(action)} must be a same-origin absolute path`);
|
|
27
|
+
}
|
|
28
|
+
const container = app.container;
|
|
29
|
+
if (!container.hasBinding(AGORA_CONSOLES_BINDING)) {
|
|
30
|
+
container.bindValue(AGORA_CONSOLES_BINDING, { version: 1, entries: [] });
|
|
31
|
+
}
|
|
32
|
+
const registry = (await container.make(AGORA_CONSOLES_BINDING));
|
|
33
|
+
if (!registry || registry.version !== 1 || !Array.isArray(registry.entries))
|
|
34
|
+
return false;
|
|
35
|
+
const account = typeof registry.account === 'object' && registry.account !== null ? registry.account : {};
|
|
36
|
+
if (account.signOut !== undefined)
|
|
37
|
+
return false;
|
|
38
|
+
registry.account = { ...account, signOut: { action, method: 'POST' } };
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AUTHKIT_METRICS, NoopRecorder } from '@adonis-agora/authkit-core';
|
|
2
2
|
import { populateContext } from './observability/context_bridge.js';
|
|
3
|
+
import { rememberOrgLabel } from './org_labels.js';
|
|
3
4
|
/**
|
|
4
5
|
* Autenticador do request. O parâmetro `TUser` é o tipo do usuário do app que `resolveUser`
|
|
5
6
|
* devolve — o app o fixa pela interface `AuthkitUserType`, e aí `getUser()` passa a devolver
|
|
@@ -35,8 +36,10 @@ export class Authenticator {
|
|
|
35
36
|
// Ponte estrutural: popula o contexto do Agora quando há sessão de fato
|
|
36
37
|
// (best-effort, no-op sem o slot `@agora/context:set`). Apenas no caminho
|
|
37
38
|
// de sucesso — não força resolução para requests anônimas.
|
|
38
|
-
if (this.#identity)
|
|
39
|
+
if (this.#identity) {
|
|
39
40
|
populateContext(this.#identity, this.deps.tenancy);
|
|
41
|
+
rememberOrgLabel(this.#identity);
|
|
42
|
+
}
|
|
40
43
|
}
|
|
41
44
|
return this.#identity;
|
|
42
45
|
}
|
|
@@ -103,6 +103,21 @@ export interface ClientConfigInput {
|
|
|
103
103
|
tenancy?: ClientTenancyOptions;
|
|
104
104
|
/** Account console pages this app serves to the IdP. See {@link ClientAccountConsoleConfig}. */
|
|
105
105
|
accountConsole?: ClientAccountConsoleConfig;
|
|
106
|
+
/**
|
|
107
|
+
* Organization names for every console's org picker (`Context.tenantLabels`): the active
|
|
108
|
+
* org from the `org_name` claim, any other from the authkit server's
|
|
109
|
+
* `GET /api/orgs/labels` (authenticated with `clientId`/`clientSecret`), cached.
|
|
110
|
+
* Installed at boot when `@adonis-agora/context` is present, the client is confidential
|
|
111
|
+
* and authkit-server does not run in this app. `false` turns it off. Default: on.
|
|
112
|
+
*/
|
|
113
|
+
orgLabels?: false | ClientOrgLabelsConfig;
|
|
114
|
+
}
|
|
115
|
+
/** `orgLabels` in `config/authkit_client.ts`. */
|
|
116
|
+
export interface ClientOrgLabelsConfig {
|
|
117
|
+
/** The labels endpoint. Default: `/api/orgs/labels` on the issuer's origin. */
|
|
118
|
+
endpoint?: string;
|
|
119
|
+
/** How long a label is reused, in ms. Default: 5 minutes. */
|
|
120
|
+
ttlMs?: number;
|
|
106
121
|
}
|
|
107
122
|
export interface ResolvedClientConfig {
|
|
108
123
|
issuer: string;
|
|
@@ -132,5 +147,7 @@ export interface ResolvedClientConfig {
|
|
|
132
147
|
tenancy?: ClientTenancyOptions;
|
|
133
148
|
/** See {@link ClientConfigInput.accountConsole}. */
|
|
134
149
|
accountConsole?: ClientAccountConsoleConfig;
|
|
150
|
+
/** See {@link ClientConfigInput.orgLabels}. Absent means on with the defaults. */
|
|
151
|
+
orgLabels?: false | ClientOrgLabelsConfig;
|
|
135
152
|
}
|
|
136
153
|
export declare function defineConfig(config: ClientConfigInput): import("@adonisjs/core/types").ConfigProvider<ResolvedClientConfig>;
|
|
@@ -37,6 +37,7 @@ export function defineConfig(config) {
|
|
|
37
37
|
endpoints: config.endpoints,
|
|
38
38
|
tenancy: config.tenancy ?? {},
|
|
39
39
|
...(config.accountConsole ? { accountConsole: config.accountConsole } : {}),
|
|
40
|
+
...(config.orgLabels !== undefined ? { orgLabels: config.orgLabels } : {}),
|
|
40
41
|
};
|
|
41
42
|
});
|
|
42
43
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Puts an identity verified somewhere else (an MCP bearer token, a remote JWT) on the
|
|
3
|
+
* request the way a browser session would, so `ctx.authkit` / `getAuthkit(ctx)` and an
|
|
4
|
+
* `authkitClientGuard` default guard see it. A console gated by `authkitDashboardAuthorize`
|
|
5
|
+
* then accepts the caller.
|
|
6
|
+
*/
|
|
7
|
+
import type { Identity } from '@adonis-agora/authkit-core';
|
|
8
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
9
|
+
import { type DiscoverOptions } from './discovery.js';
|
|
10
|
+
export interface AuthkitAccessTokenIdentityOptions {
|
|
11
|
+
/** Issuer URL of the authkit server. Its `jwks_uri` comes from discovery. */
|
|
12
|
+
issuer: string;
|
|
13
|
+
/** The `aud` the token must carry (the resource URL of the endpoint that received it). */
|
|
14
|
+
audience: string | string[];
|
|
15
|
+
/** Claim holding the global roles. Default: `'roles'`. */
|
|
16
|
+
globalRolesClaim?: string;
|
|
17
|
+
/** Accept a token without `typ: at+jwt`. Default: `false`. */
|
|
18
|
+
allowAnyTyp?: boolean;
|
|
19
|
+
/** Discovery options (custom `fetchImpl`, cache TTL, resilience policy). */
|
|
20
|
+
discovery?: Pick<DiscoverOptions, 'fetchImpl' | 'cacheTtlMs' | 'resilience' | 'overrides'>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Verifies an authkit JWT access token (RFC 9068) against the issuer's discovered JWKS and
|
|
24
|
+
* maps it to an {@link Identity} (`userId`, `email`, `globalRoles`, org, `actor`). `null`
|
|
25
|
+
* for an invalid token or a `client_credentials` token (no user behind it). Throws when the
|
|
26
|
+
* issuer cannot be discovered.
|
|
27
|
+
*/
|
|
28
|
+
export declare function identityFromAccessToken(token: string, options: AuthkitAccessTokenIdentityOptions): Promise<Identity | null>;
|
|
29
|
+
/**
|
|
30
|
+
* Binds `identity` to the request:
|
|
31
|
+
*
|
|
32
|
+
* 1. `ctx.authkit` becomes an `Authenticator` resolving to it, built by this app's authkit
|
|
33
|
+
* client when there is one (so `resolveUser`, e.g. `lucidMirror`, gives `getUser()` the
|
|
34
|
+
* app's user, and the tenant reaches `@adonis-agora/context`), else a plain one.
|
|
35
|
+
* 2. When `ctx.auth`'s default guard is `authkitClientGuard`, it is authenticated from it,
|
|
36
|
+
* so `ctx.auth.user` sees the account too.
|
|
37
|
+
*
|
|
38
|
+
* Best effort past step 1.
|
|
39
|
+
*/
|
|
40
|
+
export declare function bindAuthkitIdentity(ctx: HttpContext, identity: Identity): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* {@link identityFromAccessToken} then {@link bindAuthkitIdentity}: the one call an MCP or
|
|
43
|
+
* API endpoint makes after reading a bearer token issued by a remote authkit. Returns the
|
|
44
|
+
* identity, or `null` (nothing bound) when the token does not verify.
|
|
45
|
+
*/
|
|
46
|
+
export declare function bridgeAuthkitToken(ctx: HttpContext, token: string, options: AuthkitAccessTokenIdentityOptions): Promise<Identity | null>;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Puts an identity verified somewhere else (an MCP bearer token, a remote JWT) on the
|
|
3
|
+
* request the way a browser session would, so `ctx.authkit` / `getAuthkit(ctx)` and an
|
|
4
|
+
* `authkitClientGuard` default guard see it. A console gated by `authkitDashboardAuthorize`
|
|
5
|
+
* then accepts the caller.
|
|
6
|
+
*/
|
|
7
|
+
import { Authenticator } from './authenticator.js';
|
|
8
|
+
import { discoverEndpoints } from './discovery.js';
|
|
9
|
+
import { buildIdentityFromClaims } from './resolvers/identity.js';
|
|
10
|
+
import { verifyJwtAccessToken } from './verify_access_token.js';
|
|
11
|
+
/**
|
|
12
|
+
* Verifies an authkit JWT access token (RFC 9068) against the issuer's discovered JWKS and
|
|
13
|
+
* maps it to an {@link Identity} (`userId`, `email`, `globalRoles`, org, `actor`). `null`
|
|
14
|
+
* for an invalid token or a `client_credentials` token (no user behind it). Throws when the
|
|
15
|
+
* issuer cannot be discovered.
|
|
16
|
+
*/
|
|
17
|
+
export async function identityFromAccessToken(token, options) {
|
|
18
|
+
if (typeof token !== 'string' || !token)
|
|
19
|
+
return null;
|
|
20
|
+
const endpoints = await discoverEndpoints(options.issuer, {
|
|
21
|
+
...options.discovery,
|
|
22
|
+
onError: 'throw',
|
|
23
|
+
});
|
|
24
|
+
let claims;
|
|
25
|
+
try {
|
|
26
|
+
claims = (await verifyJwtAccessToken(token, {
|
|
27
|
+
issuer: endpoints.issuer,
|
|
28
|
+
jwksUri: endpoints.jwksUri,
|
|
29
|
+
audience: options.audience,
|
|
30
|
+
...(options.allowAnyTyp ? { allowAnyTyp: true } : {}),
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
if (typeof claims.sub !== 'string' || !claims.sub)
|
|
37
|
+
return null;
|
|
38
|
+
// An access token without an expiry is never accepted, whatever the issuer mints.
|
|
39
|
+
if (typeof claims.exp !== 'number')
|
|
40
|
+
return null;
|
|
41
|
+
// A client_credentials token authenticates a client, never a user.
|
|
42
|
+
if (typeof claims.client_id === 'string' && claims.sub === claims.client_id)
|
|
43
|
+
return null;
|
|
44
|
+
return buildIdentityFromClaims(claims, options.globalRolesClaim ?? 'roles');
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Binds `identity` to the request:
|
|
48
|
+
*
|
|
49
|
+
* 1. `ctx.authkit` becomes an `Authenticator` resolving to it, built by this app's authkit
|
|
50
|
+
* client when there is one (so `resolveUser`, e.g. `lucidMirror`, gives `getUser()` the
|
|
51
|
+
* app's user, and the tenant reaches `@adonis-agora/context`), else a plain one.
|
|
52
|
+
* 2. When `ctx.auth`'s default guard is `authkitClientGuard`, it is authenticated from it,
|
|
53
|
+
* so `ctx.auth.user` sees the account too.
|
|
54
|
+
*
|
|
55
|
+
* Best effort past step 1.
|
|
56
|
+
*/
|
|
57
|
+
export async function bindAuthkitIdentity(ctx, identity) {
|
|
58
|
+
const resolver = { resolve: async () => identity };
|
|
59
|
+
let authenticator;
|
|
60
|
+
try {
|
|
61
|
+
const container = ctx.containerResolver;
|
|
62
|
+
const manager = (await container?.make?.('authkit.client'));
|
|
63
|
+
if (manager && typeof manager.createAuthenticator === 'function') {
|
|
64
|
+
authenticator = await manager.createAuthenticator(ctx, {
|
|
65
|
+
resolver: { resolver: async () => resolver },
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
authenticator = undefined;
|
|
71
|
+
}
|
|
72
|
+
ctx.authkit =
|
|
73
|
+
authenticator ?? new Authenticator(ctx, { resolver });
|
|
74
|
+
const auth = ctx.auth;
|
|
75
|
+
if (auth === undefined || typeof auth.use !== 'function')
|
|
76
|
+
return;
|
|
77
|
+
try {
|
|
78
|
+
const guard = auth.use();
|
|
79
|
+
if (guard?.driverName !== 'authkit_client' || guard.isAuthenticated === true)
|
|
80
|
+
return;
|
|
81
|
+
// A silent-auth middleware may already have tried (and found no browser session).
|
|
82
|
+
guard.authenticationAttempted = false;
|
|
83
|
+
await auth.check();
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// leave ctx.auth alone
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* {@link identityFromAccessToken} then {@link bindAuthkitIdentity}: the one call an MCP or
|
|
91
|
+
* API endpoint makes after reading a bearer token issued by a remote authkit. Returns the
|
|
92
|
+
* identity, or `null` (nothing bound) when the token does not verify.
|
|
93
|
+
*/
|
|
94
|
+
export async function bridgeAuthkitToken(ctx, token, options) {
|
|
95
|
+
const identity = await identityFromAccessToken(token, options);
|
|
96
|
+
if (identity)
|
|
97
|
+
await bindAuthkitIdentity(ctx, identity);
|
|
98
|
+
return identity;
|
|
99
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Organization display labels on a relying-party app, for the org pickers of every
|
|
3
|
+
* `@adonis-agora` console (`Context.tenantLabels`). The active org's name comes from the
|
|
4
|
+
* session's `org_name` claim; any other id is asked to the authkit server
|
|
5
|
+
* (`GET /api/orgs/labels?ids=`, authenticated with this app's client id and secret) and
|
|
6
|
+
* cached.
|
|
7
|
+
*/
|
|
8
|
+
import type { Identity } from '@adonis-agora/authkit-core';
|
|
9
|
+
/** `(ids, { kind? }) => { [id]: label }`, the shape `@adonis-agora/context` expects. */
|
|
10
|
+
export type OrgLabelResolver = (ids: readonly string[], options?: {
|
|
11
|
+
kind?: string;
|
|
12
|
+
}) => Promise<Record<string, string>>;
|
|
13
|
+
export interface AuthkitOrgLabelsOptions {
|
|
14
|
+
/** Issuer URL of the authkit server. */
|
|
15
|
+
issuer: string;
|
|
16
|
+
/** This app's client id (a confidential, first-party client of the authkit server). */
|
|
17
|
+
clientId: string;
|
|
18
|
+
/** This app's client secret. */
|
|
19
|
+
clientSecret: string;
|
|
20
|
+
/** The labels endpoint. Default: `/api/orgs/labels` on the issuer's origin. */
|
|
21
|
+
endpoint?: string;
|
|
22
|
+
/** How long a fetched label (or a miss) is reused, in ms. Default: 5 minutes. */
|
|
23
|
+
ttlMs?: number;
|
|
24
|
+
/** Abort a labels request after this many ms. Default: 5000. */
|
|
25
|
+
timeoutMs?: number;
|
|
26
|
+
/** Custom `fetch` (proxy, tests). Default: the global `fetch`. */
|
|
27
|
+
fetch?: typeof fetch;
|
|
28
|
+
}
|
|
29
|
+
/** Default path of authkit-server's labels endpoint. */
|
|
30
|
+
export declare const ORG_LABELS_PATH = "/api/orgs/labels";
|
|
31
|
+
/**
|
|
32
|
+
* Records the active org's name from a resolved session (`org_name` claim), so the
|
|
33
|
+
* labeler answers it with no call. Called by the `Authenticator`; best effort.
|
|
34
|
+
*/
|
|
35
|
+
export declare function rememberOrgLabel(identity: Identity | null | undefined): void;
|
|
36
|
+
/** Clears the labels learned from claims (tests). */
|
|
37
|
+
export declare function __clearOrgLabelsForTests(): void;
|
|
38
|
+
/**
|
|
39
|
+
* A label resolver backed by the authkit server. Ids of another tenant kind (`kind` other
|
|
40
|
+
* than `'organization'`) get no label. Never throws: an unreachable server answers what is
|
|
41
|
+
* already known, and the ids it could not resolve show as ids.
|
|
42
|
+
*/
|
|
43
|
+
export declare function authkitOrgLabels(options: AuthkitOrgLabelsOptions): OrgLabelResolver;
|
|
44
|
+
/**
|
|
45
|
+
* Registers `resolver` as the context's tenant labeler (`Context.setTenantLabeler`), so
|
|
46
|
+
* console org pickers show names. `tenancy.label` in `config/context.ts` still wins.
|
|
47
|
+
* Returns `false` (and does nothing) without `@adonis-agora/context`.
|
|
48
|
+
*/
|
|
49
|
+
export declare function installOrgLabeler(resolver: OrgLabelResolver): boolean;
|