@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
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Receives authkit-server's signed event webhook on another server and re-emits each
|
|
3
|
+
* event on the local `@adonis-agora/diagnostics` bus as `agora:authkit:<type>`, with the
|
|
4
|
+
* same payload authkit-server emits in-process. Listeners such as authz provisioning
|
|
5
|
+
* (`onDiagnostic('authkit', 'organization.created', ...)`) then work unchanged when
|
|
6
|
+
* authkit runs elsewhere.
|
|
7
|
+
*/
|
|
8
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
9
|
+
/** In-memory replay store: ids live for their TTL, at most `max` at a time. */
|
|
10
|
+
export function memoryReplayStore(max = 10_000) {
|
|
11
|
+
const seen = new Map();
|
|
12
|
+
return {
|
|
13
|
+
claim(id, ttlMs) {
|
|
14
|
+
const now = Date.now();
|
|
15
|
+
const expiresAt = seen.get(id);
|
|
16
|
+
if (expiresAt !== undefined && expiresAt > now)
|
|
17
|
+
return false;
|
|
18
|
+
seen.delete(id);
|
|
19
|
+
seen.set(id, now + ttlMs);
|
|
20
|
+
if (seen.size > max) {
|
|
21
|
+
for (const [key, exp] of seen) {
|
|
22
|
+
if (exp <= now || seen.size > max)
|
|
23
|
+
seen.delete(key);
|
|
24
|
+
else
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function validSignature(body, header, secret) {
|
|
33
|
+
if (typeof header !== 'string' || !header.startsWith('sha256='))
|
|
34
|
+
return false;
|
|
35
|
+
const expected = Buffer.from(createHmac('sha256', secret).update(body).digest('hex'));
|
|
36
|
+
const given = Buffer.from(header.slice('sha256='.length));
|
|
37
|
+
return given.length === expected.length && timingSafeEqual(given, expected);
|
|
38
|
+
}
|
|
39
|
+
const str = (v) => (typeof v === 'string' && v ? v : null);
|
|
40
|
+
/**
|
|
41
|
+
* Checks one delivery: the `x-authkit-signature` HMAC over the raw body, the shape, the
|
|
42
|
+
* `ts` window and the `id` (each id is accepted once). Pure apart from the replay store.
|
|
43
|
+
*/
|
|
44
|
+
export async function verifyAuthkitWebhook(input, options) {
|
|
45
|
+
if (!validSignature(input.body, input.signature, options.secret)) {
|
|
46
|
+
return { ok: false, reason: 'signature' };
|
|
47
|
+
}
|
|
48
|
+
let raw;
|
|
49
|
+
try {
|
|
50
|
+
raw = JSON.parse(input.body);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return { ok: false, reason: 'malformed' };
|
|
54
|
+
}
|
|
55
|
+
const id = str(raw?.id);
|
|
56
|
+
const type = str(raw?.type);
|
|
57
|
+
const ts = str(raw?.ts);
|
|
58
|
+
if (!id || !type || !ts)
|
|
59
|
+
return { ok: false, reason: 'malformed' };
|
|
60
|
+
const at = Date.parse(ts);
|
|
61
|
+
if (Number.isNaN(at))
|
|
62
|
+
return { ok: false, reason: 'malformed' };
|
|
63
|
+
const toleranceMs = (options.toleranceSec ?? 300) * 1000;
|
|
64
|
+
if (Math.abs(Date.now() - at) > toleranceMs)
|
|
65
|
+
return { ok: false, reason: 'expired' };
|
|
66
|
+
// Remember the id for as long as its `ts` stays acceptable.
|
|
67
|
+
if (!(await options.replayStore.claim(id, toleranceMs * 2))) {
|
|
68
|
+
return { ok: false, reason: 'replay' };
|
|
69
|
+
}
|
|
70
|
+
const metadata = raw.metadata && typeof raw.metadata === 'object' && !Array.isArray(raw.metadata)
|
|
71
|
+
? raw.metadata
|
|
72
|
+
: {};
|
|
73
|
+
return {
|
|
74
|
+
ok: true,
|
|
75
|
+
event: {
|
|
76
|
+
id,
|
|
77
|
+
type,
|
|
78
|
+
accountId: str(raw.accountId),
|
|
79
|
+
actorId: str(raw.actorId),
|
|
80
|
+
email: str(raw.email),
|
|
81
|
+
clientId: str(raw.clientId),
|
|
82
|
+
orgId: str(raw.orgId),
|
|
83
|
+
ip: str(raw.ip),
|
|
84
|
+
metadata,
|
|
85
|
+
ts,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const EMIT_SLOT = Symbol.for('@agora/diagnostics:emit');
|
|
90
|
+
/**
|
|
91
|
+
* Emits `event` on the diagnostics bus as `agora:authkit:<type>` with authkit-server's
|
|
92
|
+
* redacted payload (`type`, `accountId`, `actorId`, `clientId`, `orgId`). No-op without
|
|
93
|
+
* `@adonis-agora/diagnostics`.
|
|
94
|
+
*/
|
|
95
|
+
export function emitAuthkitEvent(event) {
|
|
96
|
+
try {
|
|
97
|
+
const emit = globalThis[EMIT_SLOT];
|
|
98
|
+
emit?.('authkit', event.type, {
|
|
99
|
+
type: event.type,
|
|
100
|
+
accountId: event.accountId,
|
|
101
|
+
actorId: event.actorId,
|
|
102
|
+
clientId: event.clientId,
|
|
103
|
+
orgId: event.orgId,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// best effort
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The route handler for authkit-server's webhook. Answers `204` for an accepted (or
|
|
112
|
+
* replayed) delivery, `401` for a bad signature, `400` for a malformed or expired one.
|
|
113
|
+
* Needs the raw body: keep the route under the JSON body parser (the default) and out of
|
|
114
|
+
* CSRF (shield's `csrf.exceptRoutes`).
|
|
115
|
+
*/
|
|
116
|
+
export function authkitWebhookHandler(options) {
|
|
117
|
+
if (!options?.secret)
|
|
118
|
+
throw new TypeError('authkitWebhookHandler: secret is required');
|
|
119
|
+
const replayStore = options.replayStore ?? memoryReplayStore();
|
|
120
|
+
return async (ctx) => {
|
|
121
|
+
const body = ctx.request.raw();
|
|
122
|
+
if (typeof body !== 'string' || !body) {
|
|
123
|
+
return ctx.response.badRequest({ error: 'malformed' });
|
|
124
|
+
}
|
|
125
|
+
const verdict = await verifyAuthkitWebhook({ body, signature: ctx.request.header('x-authkit-signature') }, { secret: options.secret, toleranceSec: options.toleranceSec, replayStore });
|
|
126
|
+
if (!verdict.ok) {
|
|
127
|
+
if (verdict.reason === 'replay')
|
|
128
|
+
return ctx.response.noContent();
|
|
129
|
+
if (verdict.reason === 'signature') {
|
|
130
|
+
return ctx.response.unauthorized({ error: 'invalid_signature' });
|
|
131
|
+
}
|
|
132
|
+
return ctx.response.badRequest({ error: verdict.reason });
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
if (options.onEvent)
|
|
136
|
+
await options.onEvent(verdict.event);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// A failing listener must not suppress the bus emit: the delivery was valid and its id
|
|
140
|
+
// is already claimed, so a redelivery would be ignored.
|
|
141
|
+
}
|
|
142
|
+
if (options.emit !== false)
|
|
143
|
+
emitAuthkitEvent(verdict.event);
|
|
144
|
+
return ctx.response.noContent();
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Mounts `POST path` (default `/authkit/webhook`) with {@link authkitWebhookHandler}:
|
|
149
|
+
*
|
|
150
|
+
* ```ts
|
|
151
|
+
* // start/routes.ts
|
|
152
|
+
* registerAuthkitWebhook(router, { secret: env.get('AUTHKIT_WEBHOOK_SECRET') })
|
|
153
|
+
* ```
|
|
154
|
+
*
|
|
155
|
+
* Point authkit-server's `events.webhook` at `https://<this app>/authkit/webhook` with the
|
|
156
|
+
* same secret, and add the path to shield's `csrf.exceptRoutes`.
|
|
157
|
+
*/
|
|
158
|
+
export function registerAuthkitWebhook(router, options) {
|
|
159
|
+
const { path = '/authkit/webhook', ...rest } = options;
|
|
160
|
+
router.post(path, authkitWebhookHandler(rest)).as('authkit.webhook');
|
|
161
|
+
}
|
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.7",
|
|
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.3",
|
|
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.24",
|
|
85
85
|
"@adonis-agora/resilience": "0.4.3",
|
|
86
86
|
"@adonisjs/auth": "10.1.0",
|
|
87
87
|
"@adonisjs/core": "7.6.0",
|
|
@@ -193,17 +193,43 @@ 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.
|
|
206
208
|
|
|
209
|
+
### Pattern 5 — authkit-server on another server
|
|
210
|
+
|
|
211
|
+
Nothing on this server may need authkit-server in-process. The remote-safe helpers:
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
import {
|
|
215
|
+
authkitTenantToken, // tenant side: client_credentials token for one resource; endpoint from discovery only
|
|
216
|
+
authkitTenantVerifier, // control-plane side: (request) => { partition, environment?, capabilities, clientId, expiresAt } | null
|
|
217
|
+
registerAuthkitWebhook, // signed authkit events → diagnostics bus as agora:authkit:<type>
|
|
218
|
+
bridgeAuthkitToken, // bearer token (MCP/API) → ctx.authkit, so authkitDashboardAuthorize admits it
|
|
219
|
+
} from '@adonis-agora/authkit-client'
|
|
220
|
+
|
|
221
|
+
verifyTenant: authkitTenantVerifier({ issuer: env.get('AUTHKIT_ISSUER'), resource: 'urn:agora:flags' })
|
|
222
|
+
registerAuthkitWebhook(router, { secret: env.get('AUTHKIT_WEBHOOK_SECRET') }) // + csrf.exceptRoutes
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
`registerOidcClient` already announces `POST /auth/logout` as the consoles' sign-out (skipped
|
|
226
|
+
when authkit-server runs in the app), the provider installs org labels (`orgLabels`), and
|
|
227
|
+
`identity.actor` carries the impersonating admin (`act`). Resources are `urn:agora:<product>`;
|
|
228
|
+
never hard-code `/token` or `/oauth/token`.
|
|
229
|
+
|
|
230
|
+
Source: `src/tenant_verifier.ts`, `src/webhook_receiver.ts`, `src/identity_bridge.ts`,
|
|
231
|
+
`src/org_labels.ts`; docs `topologies.mdx`.
|
|
232
|
+
|
|
207
233
|
## Common mistakes
|
|
208
234
|
|
|
209
235
|
### HIGH — Session middleware running after `authkit_context_middleware`
|