@adonis-agora/authkit-client 1.0.0-rc.4 → 1.0.0-rc.6
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 +35 -0
- package/build/index.d.ts +4 -2
- package/build/index.js +2 -1
- package/build/providers/authkit_client_provider.d.ts +6 -0
- package/build/providers/authkit_client_provider.js +47 -0
- package/build/src/account_extensions.d.ts +49 -0
- package/build/src/account_extensions.js +105 -0
- package/build/src/define_config.d.ts +23 -1
- package/build/src/define_config.js +1 -0
- package/build/src/register_oidc_client.d.ts +13 -1
- package/build/src/register_oidc_client.js +17 -0
- package/build/src/revocation/revocation_store.d.ts +9 -3
- package/build/src/revocation/revocation_store.js +0 -0
- package/package.json +3 -3
- package/skills/authkit-rp-client/SKILL.md +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# @adonis-agora/authkit-client
|
|
2
2
|
|
|
3
|
+
## 1.0.0-rc.6
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#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).
|
|
8
|
+
|
|
9
|
+
- 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.
|
|
10
|
+
- 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 })`).
|
|
11
|
+
- authkit-client: `lucidRevocationStore({ schema })`.
|
|
12
|
+
- SQLite with `schema.name` fails the boot instead of ignoring the option.
|
|
13
|
+
- `agora:doctor` gains `authkit.db-schema`: the schema exists, the tables are in it, the connection can write there. Silent without `schema.name`.
|
|
14
|
+
- The key-rotation lock key includes the schema, so two installs in two schemas of one database do not share it.
|
|
15
|
+
|
|
16
|
+
Without `schema.name` nothing changes.
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- Updated dependencies [[`df9e49d`](https://github.com/DavideCarvalho/adonis-agora-authkit/commit/df9e49d7817c03a7cce4cac1df709c7788d6307c)]:
|
|
21
|
+
- @adonis-agora/authkit-core@1.0.0-rc.2
|
|
22
|
+
|
|
23
|
+
## 1.0.0-rc.5
|
|
24
|
+
|
|
25
|
+
### Minor Changes
|
|
26
|
+
|
|
27
|
+
- [#471](https://github.com/DavideCarvalho/adonis-agora-authkit/pull/471) [`66ccc36`](https://github.com/DavideCarvalho/adonis-agora-authkit/commit/66ccc365dbbd7d9d5bf22dba7aad196aff9f5d4b) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Account console extensions from another app. An app that signs in through an authkit IdP can now add pages to the IdP's account console:
|
|
28
|
+
|
|
29
|
+
- authkit-client binds the same `authkit.accountExtensions` registry, so packages register their pages the same way in both setups. With `accountConsole: { remote: true }` the app serves them at `/.well-known/agora-account-extensions` (mounted by `registerOidcClient`, or `registerAccountExtensionsEndpoint(router)`), only to requests the IdP signed for its client id with the scope `authkit:account-extensions`. `accountConsole.subject` maps the IdP account to the app's user.
|
|
30
|
+
- authkit-server: `accountConsole.remote: [{ clientId, url }]` lists those apps; their pages show in the console next to the in-process ones, with the same sign-in, sudo, confirmation, rate limit and audit rules.
|
|
31
|
+
- The contract types and validation now live in authkit-core (still exported from `@adonis-agora/authkit-server/account-extension`).
|
|
32
|
+
|
|
33
|
+
### Patch Changes
|
|
34
|
+
|
|
35
|
+
- Updated dependencies [[`66ccc36`](https://github.com/DavideCarvalho/adonis-agora-authkit/commit/66ccc365dbbd7d9d5bf22dba7aad196aff9f5d4b)]:
|
|
36
|
+
- @adonis-agora/authkit-core@1.0.0-rc.1
|
|
37
|
+
|
|
3
38
|
## 1.0.0-rc.4
|
|
4
39
|
|
|
5
40
|
### Minor Changes
|
package/build/index.d.ts
CHANGED
|
@@ -2,12 +2,14 @@ export type { Identity } 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
|
+
export type { AccountExtensionsAnswer, AccountExtensionsVerifier, ServeAccountExtensionsInput, } from './src/account_extensions.js';
|
|
6
|
+
export { ACCOUNT_EXTENSIONS_BINDING, ClientAccountExtensionRegistry, serveAccountExtensions, verifyAccountExtensionsToken, } from './src/account_extensions.js';
|
|
5
7
|
export { Authenticator } from './src/authenticator.js';
|
|
6
8
|
export type { AuthorizationRequest, CompleteAuthorizationParams, CompletedAuthorization, CreateAuthorizationRequestParams, OidcCallbackErrorCode, PendingAuthorization, } from './src/authorization.js';
|
|
7
9
|
export { completeAuthorization, createAuthorizationRequest, OidcCallbackError, } from './src/authorization.js';
|
|
8
10
|
export type { SessionIndex, SessionIndexEntry, ValidatedLogoutToken, ValidateLogoutTokenOptions, } from './src/backchannel_logout.js';
|
|
9
11
|
export { BACKCHANNEL_LOGOUT_EVENT, InMemorySessionIndex, InvalidLogoutTokenError, validateLogoutToken, } from './src/backchannel_logout.js';
|
|
10
|
-
export type { BackchannelLogoutCallback, BackchannelLogoutInput, ClientConfigInput, ResolvedClientConfig, } from './src/define_config.js';
|
|
12
|
+
export type { BackchannelLogoutCallback, BackchannelLogoutInput, ClientAccountConsoleConfig, ClientConfigInput, ResolvedClientConfig, } from './src/define_config.js';
|
|
11
13
|
export { defineConfig, resolvers } from './src/define_config.js';
|
|
12
14
|
export type { DiscoverOptions, OidcEndpoints } from './src/discovery.js';
|
|
13
15
|
export { conventionEndpoints, discoverEndpoints, OidcDiscoveryError } from './src/discovery.js';
|
|
@@ -34,7 +36,7 @@ export type { ClientTenancyOptions, ContextTenant, ContextTenantLink, TenantInpu
|
|
|
34
36
|
export type { AuthorizeParams, EndSessionParams, ExchangeParams, RefreshParams, } from './src/oidc_login.js';
|
|
35
37
|
export { buildAuthorizeUrl, buildEndSessionUrl, exchangeCode, exchangeToken, generatePkce, refreshTokens, } from './src/oidc_login.js';
|
|
36
38
|
export type { PostLoginRedirects, RegisterOidcClientOptions, } from './src/register_oidc_client.js';
|
|
37
|
-
export { registerOidcClient } from './src/register_oidc_client.js';
|
|
39
|
+
export { registerAccountExtensionsEndpoint, registerOidcClient, } from './src/register_oidc_client.js';
|
|
38
40
|
export type { LucidRevocationStoreOptions, RevocationStore, } from './src/revocation/revocation_store.js';
|
|
39
41
|
export { DEFAULT_REVOCATION_TABLE, lucidRevocationStore, } from './src/revocation/revocation_store.js';
|
|
40
42
|
export { AuthkitTenantToken, type AuthkitTenantTokenFn, type AuthkitTenantTokenOptions, authkitTenantToken, TenantTokenError, } from './src/tenant_token.js';
|
package/build/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { configure } from './commands/configure.js';
|
|
2
2
|
export { AuthkitClientManager } from './providers/authkit_client_provider.js';
|
|
3
|
+
export { ACCOUNT_EXTENSIONS_BINDING, ClientAccountExtensionRegistry, serveAccountExtensions, verifyAccountExtensionsToken, } from './src/account_extensions.js';
|
|
3
4
|
export { Authenticator } from './src/authenticator.js';
|
|
4
5
|
export { completeAuthorization, createAuthorizationRequest, OidcCallbackError, } from './src/authorization.js';
|
|
5
6
|
export { BACKCHANNEL_LOGOUT_EVENT, InMemorySessionIndex, InvalidLogoutTokenError, validateLogoutToken, } from './src/backchannel_logout.js';
|
|
@@ -17,7 +18,7 @@ export { default as AuthkitContextMiddleware } from './src/middleware/authkit_co
|
|
|
17
18
|
export { default as BackchannelRevocationMiddleware } from './src/middleware/backchannel_revocation_middleware.js';
|
|
18
19
|
export { default as RequireOrgMiddleware } from './src/middleware/require_org_middleware.js';
|
|
19
20
|
export { buildAuthorizeUrl, buildEndSessionUrl, exchangeCode, exchangeToken, generatePkce, refreshTokens, } from './src/oidc_login.js';
|
|
20
|
-
export { registerOidcClient } from './src/register_oidc_client.js';
|
|
21
|
+
export { registerAccountExtensionsEndpoint, registerOidcClient, } from './src/register_oidc_client.js';
|
|
21
22
|
export { DEFAULT_REVOCATION_TABLE, lucidRevocationStore, } from './src/revocation/revocation_store.js';
|
|
22
23
|
export { AuthkitTenantToken, authkitTenantToken, TenantTokenError, } from './src/tenant_token.js';
|
|
23
24
|
export { createUserinfoResolver, identityToUser, } from './src/user_resolvers.js';
|
|
@@ -97,6 +97,12 @@ export declare class AuthkitClientManager {
|
|
|
97
97
|
* Sempre seta `Cache-Control: no-store` (exigência do spec).
|
|
98
98
|
*/
|
|
99
99
|
handleBackchannelLogout(ctx: HttpContext): Promise<unknown>;
|
|
100
|
+
/**
|
|
101
|
+
* Answers the IdP's account console (`GET`/`POST /.well-known/agora-account-extensions`): the
|
|
102
|
+
* manifest of this app's account extensions, or one call to them. 404 unless
|
|
103
|
+
* `accountConsole.remote` is on. Mounted by {@link registerOidcClient}.
|
|
104
|
+
*/
|
|
105
|
+
handleAccountExtensions(ctx: HttpContext): Promise<unknown>;
|
|
100
106
|
/**
|
|
101
107
|
* Impersona `requestedSubject` via token-exchange (RFC 8693): guarda o token set
|
|
102
108
|
* atual e o substitui pelo do alvo. Encapsula o malabarismo de session keys que o
|
|
@@ -5,6 +5,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
5
5
|
import { configProvider } from '@adonisjs/core';
|
|
6
6
|
import { RuntimeException } from '@adonisjs/core/exceptions';
|
|
7
7
|
import { decodeJwt } from 'jose';
|
|
8
|
+
import { ACCOUNT_EXTENSIONS_BINDING, ClientAccountExtensionRegistry, serveAccountExtensions, } from '../src/account_extensions.js';
|
|
8
9
|
import { Authenticator } from '../src/authenticator.js';
|
|
9
10
|
import { assertCallbackBelongs, completeAuthorization, createAuthorizationRequest, } from '../src/authorization.js';
|
|
10
11
|
import { validateLogoutToken } from '../src/backchannel_logout.js';
|
|
@@ -323,6 +324,44 @@ export class AuthkitClientManager {
|
|
|
323
324
|
});
|
|
324
325
|
return {};
|
|
325
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* Answers the IdP's account console (`GET`/`POST /.well-known/agora-account-extensions`): the
|
|
329
|
+
* manifest of this app's account extensions, or one call to them. 404 unless
|
|
330
|
+
* `accountConsole.remote` is on. Mounted by {@link registerOidcClient}.
|
|
331
|
+
*/
|
|
332
|
+
async handleAccountExtensions(ctx) {
|
|
333
|
+
const { request, response } = ctx;
|
|
334
|
+
response.header('Cache-Control', 'no-store');
|
|
335
|
+
const settings = this.config.accountConsole;
|
|
336
|
+
if (!settings?.remote)
|
|
337
|
+
return response.status(404).send({ error: { code: 'not_found' } });
|
|
338
|
+
let idp;
|
|
339
|
+
try {
|
|
340
|
+
idp = await this.#idp();
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
this.#logger.warn({ err: error, issuer: this.config.issuer }, 'authkit-client: account extensions called while discovery is failing');
|
|
344
|
+
return response.status(503).send({ error: { code: 'temporarily_unavailable' } });
|
|
345
|
+
}
|
|
346
|
+
const resolver = ctx.containerResolver;
|
|
347
|
+
const registry = (await resolver.make(ACCOUNT_EXTENSIONS_BINDING));
|
|
348
|
+
const method = request.method();
|
|
349
|
+
const raw = method === 'POST' ? request.raw() : null;
|
|
350
|
+
const answer = await serveAccountExtensions({
|
|
351
|
+
method,
|
|
352
|
+
authorization: request.header('authorization'),
|
|
353
|
+
rawBody: typeof raw === 'string' ? raw : null,
|
|
354
|
+
extensions: typeof registry.extensions === 'function' ? registry.extensions() : [],
|
|
355
|
+
contract: registry.contract,
|
|
356
|
+
verifier: { issuer: idp.endpoints.issuer, clientId: this.config.clientId, keys: idp.keys },
|
|
357
|
+
config: settings,
|
|
358
|
+
make: (binding) => resolver.make(binding),
|
|
359
|
+
log: (message) => this.#logger.warn({}, message),
|
|
360
|
+
});
|
|
361
|
+
for (const [name, value] of Object.entries(answer.headers ?? {}))
|
|
362
|
+
response.header(name, value);
|
|
363
|
+
return response.status(answer.status).send(answer.body);
|
|
364
|
+
}
|
|
326
365
|
/** Chave de sessão onde o token set ORIGINAL fica guardado durante a impersonação. */
|
|
327
366
|
get #impersonatorKey() {
|
|
328
367
|
return `${this.config.sessionKey}_impersonator`;
|
|
@@ -511,6 +550,14 @@ export default class AuthkitClientProvider {
|
|
|
511
550
|
// bound by class, would silently get the real one. Not cached here, so a `swap` of the class
|
|
512
551
|
// also reaches the string key, even after its first use.
|
|
513
552
|
this.app.container.bind('authkit.client', (resolver) => resolver.make(AuthkitClientManager));
|
|
553
|
+
// Account console extensions register here from their `boot()`, exactly as next to
|
|
554
|
+
// authkit-server. When authkit-server is in this app too, its registry is the one bound.
|
|
555
|
+
if (!this.app.container.hasBinding(ACCOUNT_EXTENSIONS_BINDING)) {
|
|
556
|
+
const registry = new ClientAccountExtensionRegistry({
|
|
557
|
+
production: () => this.app.inProduction === true,
|
|
558
|
+
});
|
|
559
|
+
this.app.container.singleton(ACCOUNT_EXTENSIONS_BINDING, async () => registry);
|
|
560
|
+
}
|
|
514
561
|
}
|
|
515
562
|
}
|
|
516
563
|
/** `sub` of a stored id_token, or undefined when there is none / it is unreadable. */
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type AccountExtension, BaseAccountExtensionRegistry } from '@adonis-agora/authkit-core';
|
|
2
|
+
import { type JWTPayload, type JWTVerifyGetKey } from 'jose';
|
|
3
|
+
import type { ClientAccountConsoleConfig } from './define_config.js';
|
|
4
|
+
/** The container binding contributors register into (same name as authkit-server's). */
|
|
5
|
+
export declare const ACCOUNT_EXTENSIONS_BINDING = "authkit.accountExtensions";
|
|
6
|
+
/**
|
|
7
|
+
* The account extensions registry of an app whose IdP runs elsewhere. Contributors register
|
|
8
|
+
* exactly as they would next to authkit-server; this app then serves the pages to the IdP.
|
|
9
|
+
*/
|
|
10
|
+
export declare class ClientAccountExtensionRegistry extends BaseAccountExtensionRegistry {
|
|
11
|
+
}
|
|
12
|
+
/** What verifying a remote-extension token needs. */
|
|
13
|
+
export interface AccountExtensionsVerifier {
|
|
14
|
+
issuer: string;
|
|
15
|
+
clientId: string;
|
|
16
|
+
keys: JWTVerifyGetKey;
|
|
17
|
+
}
|
|
18
|
+
/** A refusal: `status` plus the JSON body to send. */
|
|
19
|
+
export interface AccountExtensionsAnswer {
|
|
20
|
+
status: number;
|
|
21
|
+
body: unknown;
|
|
22
|
+
headers?: Record<string, string>;
|
|
23
|
+
}
|
|
24
|
+
/** Verifies the IdP's token: signature, issuer, audience, `typ`, lifetime and scope. */
|
|
25
|
+
export declare function verifyAccountExtensionsToken(authorization: string | undefined, verifier: AccountExtensionsVerifier): Promise<{
|
|
26
|
+
ok: true;
|
|
27
|
+
claims: JWTPayload;
|
|
28
|
+
} | {
|
|
29
|
+
ok: false;
|
|
30
|
+
answer: AccountExtensionsAnswer;
|
|
31
|
+
}>;
|
|
32
|
+
export interface ServeAccountExtensionsInput {
|
|
33
|
+
method: string;
|
|
34
|
+
authorization: string | undefined;
|
|
35
|
+
/** The exact POST body (its hash is signed by the IdP). */
|
|
36
|
+
rawBody: string | null;
|
|
37
|
+
extensions: readonly AccountExtension[];
|
|
38
|
+
contract: number;
|
|
39
|
+
verifier: AccountExtensionsVerifier;
|
|
40
|
+
config: ClientAccountConsoleConfig;
|
|
41
|
+
make: (binding: string) => Promise<any>;
|
|
42
|
+
log?: (message: string) => void;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Answers the IdP: `GET` → the manifest, `POST` → one call (visible, load or action). Only a token
|
|
46
|
+
* the IdP signed for this app (audience = client id, scope `authkit:account-extensions`) gets in;
|
|
47
|
+
* a POST must also match the signed body hash and name the token's account.
|
|
48
|
+
*/
|
|
49
|
+
export declare function serveAccountExtensions(input: ServeAccountExtensionsInput): Promise<AccountExtensionsAnswer>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { ACCOUNT_EXTENSIONS_MANIFEST_SUBJECT, ACCOUNT_EXTENSIONS_SCOPE, ACCOUNT_EXTENSIONS_TOKEN_MAX_TTL, ACCOUNT_EXTENSIONS_TOKEN_TYP, accountExtensionsBodyHash, BaseAccountExtensionRegistry, buildAccountExtensionsManifest, parseAccountExtensionCall, runAccountExtensionCall, } from '@adonis-agora/authkit-core';
|
|
2
|
+
import { jwtVerify } from 'jose';
|
|
3
|
+
import { DEFAULT_ALGS } from './jwks.js';
|
|
4
|
+
/** The container binding contributors register into (same name as authkit-server's). */
|
|
5
|
+
export const ACCOUNT_EXTENSIONS_BINDING = 'authkit.accountExtensions';
|
|
6
|
+
/**
|
|
7
|
+
* The account extensions registry of an app whose IdP runs elsewhere. Contributors register
|
|
8
|
+
* exactly as they would next to authkit-server; this app then serves the pages to the IdP.
|
|
9
|
+
*/
|
|
10
|
+
export class ClientAccountExtensionRegistry extends BaseAccountExtensionRegistry {
|
|
11
|
+
}
|
|
12
|
+
const refuse = (status, code, message) => ({
|
|
13
|
+
status,
|
|
14
|
+
body: { error: { code, message } },
|
|
15
|
+
...(status === 401
|
|
16
|
+
? {
|
|
17
|
+
headers: {
|
|
18
|
+
'WWW-Authenticate': `Bearer error="${code}", scope="${ACCOUNT_EXTENSIONS_SCOPE}"`,
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
: {}),
|
|
22
|
+
});
|
|
23
|
+
/** Verifies the IdP's token: signature, issuer, audience, `typ`, lifetime and scope. */
|
|
24
|
+
export async function verifyAccountExtensionsToken(authorization, verifier) {
|
|
25
|
+
const match = /^Bearer\s+(\S+)$/i.exec(authorization ?? '');
|
|
26
|
+
if (!match?.[1]) {
|
|
27
|
+
return { ok: false, answer: refuse(401, 'invalid_token', 'A bearer token is required.') };
|
|
28
|
+
}
|
|
29
|
+
let claims;
|
|
30
|
+
try {
|
|
31
|
+
({ payload: claims } = await jwtVerify(match[1], verifier.keys, {
|
|
32
|
+
issuer: verifier.issuer,
|
|
33
|
+
audience: verifier.clientId,
|
|
34
|
+
typ: ACCOUNT_EXTENSIONS_TOKEN_TYP,
|
|
35
|
+
algorithms: DEFAULT_ALGS,
|
|
36
|
+
maxTokenAge: ACCOUNT_EXTENSIONS_TOKEN_MAX_TTL,
|
|
37
|
+
requiredClaims: ['exp', 'iat', 'sub'],
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return { ok: false, answer: refuse(401, 'invalid_token', 'The token is not valid.') };
|
|
42
|
+
}
|
|
43
|
+
if (claims.exp - claims.iat > ACCOUNT_EXTENSIONS_TOKEN_MAX_TTL) {
|
|
44
|
+
return { ok: false, answer: refuse(401, 'invalid_token', 'The token lives too long.') };
|
|
45
|
+
}
|
|
46
|
+
const scopes = typeof claims.scope === 'string' ? claims.scope.split(' ') : [];
|
|
47
|
+
if (!scopes.includes(ACCOUNT_EXTENSIONS_SCOPE)) {
|
|
48
|
+
return {
|
|
49
|
+
ok: false,
|
|
50
|
+
answer: refuse(403, 'insufficient_scope', `The token lacks "${ACCOUNT_EXTENSIONS_SCOPE}".`),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return { ok: true, claims };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Answers the IdP: `GET` → the manifest, `POST` → one call (visible, load or action). Only a token
|
|
57
|
+
* the IdP signed for this app (audience = client id, scope `authkit:account-extensions`) gets in;
|
|
58
|
+
* a POST must also match the signed body hash and name the token's account.
|
|
59
|
+
*/
|
|
60
|
+
export async function serveAccountExtensions(input) {
|
|
61
|
+
const verified = await verifyAccountExtensionsToken(input.authorization, input.verifier);
|
|
62
|
+
if (!verified.ok)
|
|
63
|
+
return verified.answer;
|
|
64
|
+
const { claims } = verified;
|
|
65
|
+
if (input.method === 'GET') {
|
|
66
|
+
if (claims.sub !== ACCOUNT_EXTENSIONS_MANIFEST_SUBJECT) {
|
|
67
|
+
return refuse(403, 'invalid_token', 'This token is not for the manifest.');
|
|
68
|
+
}
|
|
69
|
+
return { status: 200, body: buildAccountExtensionsManifest(input.extensions, input.contract) };
|
|
70
|
+
}
|
|
71
|
+
if (input.method !== 'POST') {
|
|
72
|
+
return {
|
|
73
|
+
status: 405,
|
|
74
|
+
body: { error: { code: 'method_not_allowed', message: 'GET or POST.' } },
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (input.rawBody === null || claims.bh !== accountExtensionsBodyHash(input.rawBody)) {
|
|
78
|
+
return refuse(403, 'invalid_token', 'The token does not match the body.');
|
|
79
|
+
}
|
|
80
|
+
let raw;
|
|
81
|
+
try {
|
|
82
|
+
raw = JSON.parse(input.rawBody);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
raw = null;
|
|
86
|
+
}
|
|
87
|
+
const call = parseAccountExtensionCall(raw);
|
|
88
|
+
if (!call) {
|
|
89
|
+
return { status: 400, body: { error: { code: 'invalid_request', message: 'Bad call.' } } };
|
|
90
|
+
}
|
|
91
|
+
if (call.account.id !== claims.sub) {
|
|
92
|
+
return refuse(403, 'invalid_token', 'The token is for another account.');
|
|
93
|
+
}
|
|
94
|
+
return runAccountExtensionCall(input.extensions, call, {
|
|
95
|
+
...(input.config.subject ? { subject: input.config.subject } : {}),
|
|
96
|
+
make: input.make,
|
|
97
|
+
...(input.config.loadTimeoutMs !== undefined
|
|
98
|
+
? { loadTimeoutMs: input.config.loadTimeoutMs }
|
|
99
|
+
: {}),
|
|
100
|
+
...(input.config.actionTimeoutMs !== undefined
|
|
101
|
+
? { actionTimeoutMs: input.config.actionTimeoutMs }
|
|
102
|
+
: {}),
|
|
103
|
+
...(input.log ? { log: input.log } : {}),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Identity } from '@adonis-agora/authkit-core';
|
|
1
|
+
import type { Identity, RunAccountExtensionCallOptions } from '@adonis-agora/authkit-core';
|
|
2
2
|
import type { SessionIndex } from './backchannel_logout.js';
|
|
3
3
|
import type { OidcEndpoints } from './discovery.js';
|
|
4
4
|
import type { ResiliencePolicy } from './http/resilient_fetch.js';
|
|
@@ -23,6 +23,24 @@ export interface BackchannelLogoutInput {
|
|
|
23
23
|
/** Store que persiste/consulta revogações (ex.: `lucidRevocationStore({ connection: 'auth' })`). */
|
|
24
24
|
store: RevocationStore;
|
|
25
25
|
}
|
|
26
|
+
/** `accountConsole` in `config/authkit_client.ts`: pages this app serves for the IdP's account console. */
|
|
27
|
+
export interface ClientAccountConsoleConfig {
|
|
28
|
+
/**
|
|
29
|
+
* Serve this app's account console extensions to the IdP over
|
|
30
|
+
* `/.well-known/agora-account-extensions` (the IdP lists this app in its own
|
|
31
|
+
* `accountConsole.remote`). Default `false`.
|
|
32
|
+
*/
|
|
33
|
+
remote?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Maps the IdP account to this app's subject (what billing, media and agents key on). Every
|
|
36
|
+
* extension receives it as `ctx.subject`. Default `{ type: 'User', id: account.id }`.
|
|
37
|
+
*/
|
|
38
|
+
subject?: RunAccountExtensionCallOptions['subject'];
|
|
39
|
+
/** Budget of one page's `load()` / `visible()`, in ms. Default 5000. */
|
|
40
|
+
loadTimeoutMs?: number;
|
|
41
|
+
/** Budget of one action's `handle()`, in ms. Default 15000. */
|
|
42
|
+
actionTimeoutMs?: number;
|
|
43
|
+
}
|
|
26
44
|
export interface ClientConfigInput {
|
|
27
45
|
issuer: string;
|
|
28
46
|
clientId: string;
|
|
@@ -83,6 +101,8 @@ export interface ClientConfigInput {
|
|
|
83
101
|
* `resolve`. No effect without `@adonis-agora/context`.
|
|
84
102
|
*/
|
|
85
103
|
tenancy?: ClientTenancyOptions;
|
|
104
|
+
/** Account console pages this app serves to the IdP. See {@link ClientAccountConsoleConfig}. */
|
|
105
|
+
accountConsole?: ClientAccountConsoleConfig;
|
|
86
106
|
}
|
|
87
107
|
export interface ResolvedClientConfig {
|
|
88
108
|
issuer: string;
|
|
@@ -110,5 +130,7 @@ export interface ResolvedClientConfig {
|
|
|
110
130
|
endpoints?: Partial<OidcEndpoints>;
|
|
111
131
|
/** Tenancy options. See {@link ClientConfigInput.tenancy}. */
|
|
112
132
|
tenancy?: ClientTenancyOptions;
|
|
133
|
+
/** See {@link ClientConfigInput.accountConsole}. */
|
|
134
|
+
accountConsole?: ClientAccountConsoleConfig;
|
|
113
135
|
}
|
|
114
136
|
export declare function defineConfig(config: ClientConfigInput): import("@adonisjs/core/types").ConfigProvider<ResolvedClientConfig>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Identity } from '@adonis-agora/authkit-core';
|
|
2
2
|
import type { HttpContext, Router } from '@adonisjs/core/http';
|
|
3
3
|
/**
|
|
4
4
|
* Mapa role → rota de redirect pós-login. `byGlobalRole` checa as roles globais do
|
|
@@ -24,6 +24,11 @@ export interface RegisterOidcClientOptions {
|
|
|
24
24
|
postLogoutRedirect?: string | ((ctx: HttpContext) => string);
|
|
25
25
|
/** Registra também `POST {prefix}/backchannel-logout` (OIDC Back-Channel Logout). Default: true. */
|
|
26
26
|
backchannelLogout?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Registra também `GET`/`POST /.well-known/agora-account-extensions` (account console pages this
|
|
29
|
+
* app serves to the IdP; 404 until `accountConsole.remote` is on). Default: true.
|
|
30
|
+
*/
|
|
31
|
+
accountExtensions?: boolean;
|
|
27
32
|
/**
|
|
28
33
|
* Middleware(s) aplicados à rota de login (ex.: `middleware.guest()`). Array no
|
|
29
34
|
* formato aceito por `.use()` do AdonisJS.
|
|
@@ -47,3 +52,10 @@ export interface RegisterOidcClientOptions {
|
|
|
47
52
|
* ```
|
|
48
53
|
*/
|
|
49
54
|
export declare function registerOidcClient(router: Router, options?: RegisterOidcClientOptions): void;
|
|
55
|
+
/**
|
|
56
|
+
* Mounts `GET`/`POST /.well-known/agora-account-extensions`, where the IdP's account console
|
|
57
|
+
* fetches this app's account extension pages (answers 404 until `accountConsole.remote` is on).
|
|
58
|
+
* {@link registerOidcClient} calls it; call it yourself when you mount the client routes by hand.
|
|
59
|
+
* The IdP POSTs without a CSRF token: add the path to shield's `csrf.exceptRoutes`.
|
|
60
|
+
*/
|
|
61
|
+
export declare function registerAccountExtensionsEndpoint(router: Router): void;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ACCOUNT_EXTENSIONS_WELL_KNOWN } from '@adonis-agora/authkit-core';
|
|
1
2
|
import { OidcCallbackError } from './authorization.js';
|
|
2
3
|
import { buildEndSessionUrl } from './oidc_login.js';
|
|
3
4
|
/**
|
|
@@ -114,6 +115,22 @@ export function registerOidcClient(router, options = {}) {
|
|
|
114
115
|
})
|
|
115
116
|
.as('auth.backchannel_logout');
|
|
116
117
|
}
|
|
118
|
+
if (options.accountExtensions !== false)
|
|
119
|
+
registerAccountExtensionsEndpoint(router);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Mounts `GET`/`POST /.well-known/agora-account-extensions`, where the IdP's account console
|
|
123
|
+
* fetches this app's account extension pages (answers 404 until `accountConsole.remote` is on).
|
|
124
|
+
* {@link registerOidcClient} calls it; call it yourself when you mount the client routes by hand.
|
|
125
|
+
* The IdP POSTs without a CSRF token: add the path to shield's `csrf.exceptRoutes`.
|
|
126
|
+
*/
|
|
127
|
+
export function registerAccountExtensionsEndpoint(router) {
|
|
128
|
+
const handle = async (ctx) => {
|
|
129
|
+
const manager = await ctx.containerResolver.make('authkit.client');
|
|
130
|
+
return manager.handleAccountExtensions(ctx);
|
|
131
|
+
};
|
|
132
|
+
router.get(ACCOUNT_EXTENSIONS_WELL_KNOWN, handle).as('authkit.account_extensions');
|
|
133
|
+
router.post(ACCOUNT_EXTENSIONS_WELL_KNOWN, handle).as('authkit.account_extensions.call');
|
|
117
134
|
}
|
|
118
135
|
/** Resolve o destino pós-login a partir do mapa de redirects por role global (do token). */
|
|
119
136
|
function resolveDestination(identity, redirects) {
|
|
@@ -37,11 +37,17 @@ export interface RevocationStore {
|
|
|
37
37
|
}
|
|
38
38
|
export interface LucidRevocationStoreOptions {
|
|
39
39
|
/**
|
|
40
|
-
* Conexão Lucid a usar. Default: conexão primária.
|
|
41
|
-
*
|
|
42
|
-
* compartilhável entre apps que apontam para o MESMO banco.
|
|
40
|
+
* Conexão Lucid a usar. Default: conexão primária. A tabela é compartilhável entre
|
|
41
|
+
* apps que apontam para o MESMO banco.
|
|
43
42
|
*/
|
|
44
43
|
connection?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Schema do banco onde a tabela vive (MySQL: o database) — o `schema.name` do
|
|
46
|
+
* authkit-server, ex.: `'auth'`. Escrito em cada statement (`"auth"."auth_session_revocations"`).
|
|
47
|
+
* Não use `searchPath` na conexão para isso: atrás de um PgBouncer em modo
|
|
48
|
+
* transaction, o `SET search_path` vaza para as conexões de outros clientes.
|
|
49
|
+
*/
|
|
50
|
+
schema?: string;
|
|
45
51
|
/** Nome da tabela. Default: `auth_session_revocations` (criada pelo schema auto-manage do server). */
|
|
46
52
|
table?: string;
|
|
47
53
|
/**
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adonis-agora/authkit-client",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.6",
|
|
4
4
|
"description": "AdonisJS OIDC/OAuth2 relying-party (client) toolkit: session-based authentication against an OpenID Connect identity provider, with JWT/PAT user resolvers and OpenTelemetry metrics.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "dudousxd",
|
|
@@ -77,11 +77,11 @@
|
|
|
77
77
|
}
|
|
78
78
|
},
|
|
79
79
|
"dependencies": {
|
|
80
|
-
"@adonis-agora/authkit-core": "1.0.0-rc.
|
|
80
|
+
"@adonis-agora/authkit-core": "1.0.0-rc.2",
|
|
81
81
|
"jose": "6.2.16"
|
|
82
82
|
},
|
|
83
83
|
"devDependencies": {
|
|
84
|
-
"@adonis-agora/authkit-server": "1.0.0-rc.
|
|
84
|
+
"@adonis-agora/authkit-server": "1.0.0-rc.12",
|
|
85
85
|
"@adonis-agora/resilience": "0.4.3",
|
|
86
86
|
"@adonisjs/auth": "10.1.0",
|
|
87
87
|
"@adonisjs/core": "7.6.0",
|
|
@@ -193,13 +193,15 @@ defineConfig({
|
|
|
193
193
|
clientId: env.get('AUTHKIT_CLIENT_ID'),
|
|
194
194
|
redirectUri: env.get('AUTHKIT_REDIRECT_URI'),
|
|
195
195
|
resolver: resolvers.jwt(),
|
|
196
|
-
backchannelLogout: { store: lucidRevocationStore() },
|
|
196
|
+
backchannelLogout: { store: lucidRevocationStore() }, // with IdP schema.name: lucidRevocationStore({ schema: 'auth' })
|
|
197
197
|
})
|
|
198
198
|
```
|
|
199
199
|
|
|
200
200
|
Then apply `BackchannelRevocationMiddleware` (from
|
|
201
201
|
`@adonis-agora/authkit-client/backchannel_revocation_middleware`) so revoked
|
|
202
|
-
sessions are rejected per request.
|
|
202
|
+
sessions are rejected per request. `lucidRevocationStore` accepts
|
|
203
|
+
`{ connection?, schema?, table? }`; when the IdP uses `schema.name`, pass the same
|
|
204
|
+
`schema` here so both sides use `"auth"."auth_session_revocations"`. Custom needs: `onBackchannelLogout` receives the
|
|
203
205
|
validated `{ sid, sub }` (store runs first, then the hook).
|
|
204
206
|
|
|
205
207
|
Source: `src/define_config.ts` (`BackchannelLogoutInput`), README.
|