@simonsbs/keylore 1.0.0-rc4

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.
Files changed (81) hide show
  1. package/.env.example +64 -0
  2. package/LICENSE +176 -0
  3. package/NOTICE +5 -0
  4. package/README.md +424 -0
  5. package/bin/keylore-http.js +3 -0
  6. package/bin/keylore-stdio.js +3 -0
  7. package/data/auth-clients.json +54 -0
  8. package/data/catalog.json +53 -0
  9. package/data/policies.json +25 -0
  10. package/dist/adapters/adapter-registry.js +143 -0
  11. package/dist/adapters/aws-secrets-manager-adapter.js +99 -0
  12. package/dist/adapters/command-runner.js +17 -0
  13. package/dist/adapters/env-secret-adapter.js +42 -0
  14. package/dist/adapters/gcp-secret-manager-adapter.js +129 -0
  15. package/dist/adapters/local-secret-adapter.js +54 -0
  16. package/dist/adapters/onepassword-secret-adapter.js +83 -0
  17. package/dist/adapters/reference-utils.js +44 -0
  18. package/dist/adapters/types.js +1 -0
  19. package/dist/adapters/vault-secret-adapter.js +103 -0
  20. package/dist/app.js +132 -0
  21. package/dist/cli/args.js +51 -0
  22. package/dist/cli/run.js +483 -0
  23. package/dist/cli.js +18 -0
  24. package/dist/config.js +295 -0
  25. package/dist/domain/types.js +967 -0
  26. package/dist/http/admin-ui.js +3010 -0
  27. package/dist/http/server.js +1210 -0
  28. package/dist/index.js +40 -0
  29. package/dist/mcp/create-server.js +388 -0
  30. package/dist/mcp/stdio.js +7 -0
  31. package/dist/repositories/credential-repository.js +109 -0
  32. package/dist/repositories/interfaces.js +1 -0
  33. package/dist/repositories/json-file.js +20 -0
  34. package/dist/repositories/pg-access-token-repository.js +118 -0
  35. package/dist/repositories/pg-approval-repository.js +157 -0
  36. package/dist/repositories/pg-audit-log.js +62 -0
  37. package/dist/repositories/pg-auth-client-repository.js +98 -0
  38. package/dist/repositories/pg-authorization-code-repository.js +95 -0
  39. package/dist/repositories/pg-break-glass-repository.js +174 -0
  40. package/dist/repositories/pg-credential-repository.js +163 -0
  41. package/dist/repositories/pg-oauth-client-assertion-repository.js +25 -0
  42. package/dist/repositories/pg-policy-repository.js +62 -0
  43. package/dist/repositories/pg-refresh-token-repository.js +125 -0
  44. package/dist/repositories/pg-rotation-run-repository.js +127 -0
  45. package/dist/repositories/pg-tenant-repository.js +56 -0
  46. package/dist/repositories/policy-repository.js +24 -0
  47. package/dist/runtime/sandbox-runner.js +114 -0
  48. package/dist/services/access-fingerprint.js +13 -0
  49. package/dist/services/approval-service.js +148 -0
  50. package/dist/services/audit-log.js +38 -0
  51. package/dist/services/auth-context.js +43 -0
  52. package/dist/services/auth-secrets.js +14 -0
  53. package/dist/services/auth-service.js +784 -0
  54. package/dist/services/backup-service.js +610 -0
  55. package/dist/services/break-glass-service.js +207 -0
  56. package/dist/services/broker-service.js +557 -0
  57. package/dist/services/core-mode-service.js +154 -0
  58. package/dist/services/egress-policy.js +119 -0
  59. package/dist/services/local-secret-store.js +119 -0
  60. package/dist/services/maintenance-service.js +99 -0
  61. package/dist/services/notification-service.js +83 -0
  62. package/dist/services/policy-engine.js +85 -0
  63. package/dist/services/rate-limit-service.js +80 -0
  64. package/dist/services/rotation-service.js +271 -0
  65. package/dist/services/telemetry.js +149 -0
  66. package/dist/services/tenant-service.js +127 -0
  67. package/dist/services/trace-export-service.js +126 -0
  68. package/dist/services/trace-service.js +87 -0
  69. package/dist/storage/bootstrap.js +68 -0
  70. package/dist/storage/database.js +39 -0
  71. package/dist/storage/in-memory-database.js +40 -0
  72. package/dist/storage/migrations.js +27 -0
  73. package/migrations/001_init.sql +49 -0
  74. package/migrations/002_phase2_auth.sql +53 -0
  75. package/migrations/003_v05_operations.sql +9 -0
  76. package/migrations/004_v07_security.sql +28 -0
  77. package/migrations/005_v08_reviews.sql +11 -0
  78. package/migrations/006_v09_auth_trace_rotation.sql +51 -0
  79. package/migrations/007_v010_multi_tenant.sql +32 -0
  80. package/migrations/008_v011_auth_tenant_ops.sql +95 -0
  81. package/package.json +78 -0
@@ -0,0 +1,784 @@
1
+ import { createHash, createPublicKey, randomBytes, randomUUID, verify as verifySignature, } from "node:crypto";
2
+ import { accessScopeSchema, authClientRecordSchema, authClientSecretOutputSchema, authContextSchema, authorizationRequestOutputSchema, publicJwkSchema, tokenIssueOutputSchema, } from "../domain/types.js";
3
+ import { hashOpaqueToken, hashSecret, verifySecret } from "./auth-secrets.js";
4
+ const clientAssertionType = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
5
+ function uniqueScopes(scopes) {
6
+ return Array.from(new Set(scopes)).map((scope) => accessScopeSchema.parse(scope));
7
+ }
8
+ function uniqueRoles(roles) {
9
+ return Array.from(new Set(roles));
10
+ }
11
+ function normalizeResource(resource) {
12
+ return resource.endsWith("/") ? resource.slice(0, -1) : resource;
13
+ }
14
+ function decodeBase64Url(value) {
15
+ const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
16
+ const padding = normalized.length % 4 === 0 ? "" : "=".repeat(4 - (normalized.length % 4));
17
+ return Buffer.from(`${normalized}${padding}`, "base64");
18
+ }
19
+ function encodeBase64Url(value) {
20
+ return value
21
+ .toString("base64")
22
+ .replace(/\+/g, "-")
23
+ .replace(/\//g, "_")
24
+ .replace(/=+$/g, "");
25
+ }
26
+ function parseJson(value) {
27
+ return JSON.parse(value);
28
+ }
29
+ function signatureAlgorithm(alg) {
30
+ if (alg === "RS256") {
31
+ return "RSA-SHA256";
32
+ }
33
+ if (alg === "ES256") {
34
+ return "sha256";
35
+ }
36
+ throw new Error(`Unsupported client assertion algorithm: ${alg}`);
37
+ }
38
+ export class AuthService {
39
+ clients;
40
+ tokens;
41
+ refreshTokens;
42
+ authorizationCodes;
43
+ assertions;
44
+ tenants;
45
+ audit;
46
+ issuerUrl;
47
+ publicBaseUrl;
48
+ accessTokenTtlSeconds;
49
+ authorizationCodeTtlSeconds;
50
+ refreshTokenTtlSeconds;
51
+ telemetry;
52
+ constructor(clients, tokens, refreshTokens, authorizationCodes, assertions, tenants, audit, issuerUrl, publicBaseUrl, accessTokenTtlSeconds, authorizationCodeTtlSeconds, refreshTokenTtlSeconds, telemetry) {
53
+ this.clients = clients;
54
+ this.tokens = tokens;
55
+ this.refreshTokens = refreshTokens;
56
+ this.authorizationCodes = authorizationCodes;
57
+ this.assertions = assertions;
58
+ this.tenants = tenants;
59
+ this.audit = audit;
60
+ this.issuerUrl = issuerUrl;
61
+ this.publicBaseUrl = publicBaseUrl;
62
+ this.accessTokenTtlSeconds = accessTokenTtlSeconds;
63
+ this.authorizationCodeTtlSeconds = authorizationCodeTtlSeconds;
64
+ this.refreshTokenTtlSeconds = refreshTokenTtlSeconds;
65
+ this.telemetry = telemetry;
66
+ }
67
+ generateClientSecret() {
68
+ return `kls_${randomBytes(24).toString("base64url")}`;
69
+ }
70
+ generateOpaqueToken(prefix) {
71
+ return `${prefix}_${randomBytes(32).toString("base64url")}`;
72
+ }
73
+ tokenEndpointUrl() {
74
+ return `${this.publicBaseUrl}/oauth/token`;
75
+ }
76
+ authorizationEndpointUrl() {
77
+ return `${this.publicBaseUrl}/oauth/authorize`;
78
+ }
79
+ buildClientRecord(client) {
80
+ return authClientRecordSchema.parse({
81
+ clientId: client.clientId,
82
+ tenantId: client.tenantId,
83
+ displayName: client.displayName,
84
+ roles: client.roles,
85
+ allowedScopes: client.allowedScopes,
86
+ status: client.status,
87
+ tokenEndpointAuthMethod: client.tokenEndpointAuthMethod,
88
+ grantTypes: client.grantTypes,
89
+ redirectUris: client.redirectUris,
90
+ jwks: client.jwks ?? [],
91
+ });
92
+ }
93
+ assertClientConfiguration(client) {
94
+ if (client.tokenEndpointAuthMethod === "none") {
95
+ return;
96
+ }
97
+ if (client.tokenEndpointAuthMethod === "private_key_jwt") {
98
+ if (!client.jwks.length) {
99
+ throw new Error(`Client ${client.clientId} is missing public JWKs.`);
100
+ }
101
+ return;
102
+ }
103
+ if (!client.secretHash || !client.secretSalt) {
104
+ throw new Error(`Client ${client.clientId} is missing shared-secret material.`);
105
+ }
106
+ }
107
+ async authenticateClientCredentials(client, input) {
108
+ this.assertClientConfiguration(client);
109
+ if (client.tokenEndpointAuthMethod === "none") {
110
+ return;
111
+ }
112
+ if (client.tokenEndpointAuthMethod === "private_key_jwt") {
113
+ if (input.clientAssertionType !== clientAssertionType || !input.clientAssertion) {
114
+ throw new Error("Missing private_key_jwt client assertion.");
115
+ }
116
+ await this.verifyClientAssertion(client, input.clientAssertion);
117
+ return;
118
+ }
119
+ if (!input.clientSecret || !client.secretHash || !client.secretSalt) {
120
+ throw new Error("Invalid client credentials.");
121
+ }
122
+ if (!verifySecret(input.clientSecret, client.secretSalt, client.secretHash)) {
123
+ throw new Error("Invalid client credentials.");
124
+ }
125
+ }
126
+ async verifyClientAssertion(client, assertion) {
127
+ const [encodedHeader, encodedPayload, encodedSignature] = assertion.split(".");
128
+ if (!encodedHeader || !encodedPayload || !encodedSignature) {
129
+ throw new Error("Invalid client assertion.");
130
+ }
131
+ const header = parseJson(decodeBase64Url(encodedHeader).toString("utf8"));
132
+ const claims = parseJson(decodeBase64Url(encodedPayload).toString("utf8"));
133
+ if (!header.alg) {
134
+ throw new Error("Client assertion is missing alg.");
135
+ }
136
+ const jwks = client.jwks.map((entry) => publicJwkSchema.parse(entry));
137
+ const selectedJwk = (header.kid
138
+ ? jwks.find((entry) => entry.kid === header.kid)
139
+ : jwks.length === 1
140
+ ? jwks[0]
141
+ : undefined) ?? jwks[0];
142
+ if (!selectedJwk) {
143
+ throw new Error("No matching public JWK for client assertion.");
144
+ }
145
+ const verified = verifySignature(signatureAlgorithm(header.alg), Buffer.from(`${encodedHeader}.${encodedPayload}`), createPublicKey({ key: selectedJwk, format: "jwk" }), decodeBase64Url(encodedSignature));
146
+ if (!verified) {
147
+ throw new Error("Invalid client assertion signature.");
148
+ }
149
+ const nowSeconds = Math.floor(Date.now() / 1000);
150
+ const audiences = Array.isArray(claims.aud) ? claims.aud : claims.aud ? [claims.aud] : [];
151
+ if (claims.iss !== client.clientId || claims.sub !== client.clientId) {
152
+ throw new Error("Client assertion subject does not match client.");
153
+ }
154
+ if (!audiences.includes(this.issuerUrl) && !audiences.includes(this.tokenEndpointUrl())) {
155
+ throw new Error("Client assertion audience is invalid.");
156
+ }
157
+ if (!claims.exp || claims.exp <= nowSeconds) {
158
+ throw new Error("Client assertion expired.");
159
+ }
160
+ if (claims.iat && claims.iat < nowSeconds - 300) {
161
+ throw new Error("Client assertion is too old.");
162
+ }
163
+ if (claims.nbf && claims.nbf > nowSeconds + 30) {
164
+ throw new Error("Client assertion is not valid yet.");
165
+ }
166
+ if (!claims.jti || claims.jti.length < 8) {
167
+ throw new Error("Client assertion jti is required.");
168
+ }
169
+ const registered = await this.assertions.register(client.clientId, claims.jti, new Date(claims.exp * 1000).toISOString());
170
+ if (!registered) {
171
+ throw new Error("Client assertion replay detected.");
172
+ }
173
+ }
174
+ async requireActiveTenant(tenantId) {
175
+ const tenant = await this.tenants.getById(tenantId);
176
+ if (!tenant) {
177
+ throw new Error(`Unknown tenant: ${tenantId}`);
178
+ }
179
+ if (tenant.status !== "active") {
180
+ throw new Error(`Tenant is disabled: ${tenantId}`);
181
+ }
182
+ }
183
+ codeChallengeForVerifier(verifier) {
184
+ return encodeBase64Url(createHash("sha256").update(verifier).digest());
185
+ }
186
+ async issueAccessTokenRecord(input) {
187
+ const token = this.generateOpaqueToken("kl");
188
+ const expiresAt = new Date(Date.now() + this.accessTokenTtlSeconds * 1000).toISOString();
189
+ await this.tokens.issue({
190
+ tokenHash: hashOpaqueToken(token),
191
+ clientId: input.clientId,
192
+ tenantId: input.tenantId,
193
+ subject: input.subject,
194
+ scopes: input.scopes,
195
+ roles: input.roles,
196
+ resource: input.resource,
197
+ expiresAt,
198
+ });
199
+ await this.audit.record({
200
+ type: "auth.token",
201
+ action: "auth.token.issue",
202
+ outcome: "success",
203
+ tenantId: input.tenantId,
204
+ principal: input.subject,
205
+ metadata: {
206
+ clientId: input.clientId,
207
+ tenantId: input.tenantId,
208
+ scopes: input.scopes,
209
+ resource: input.resource ?? null,
210
+ expiresAt,
211
+ grantType: input.grantType,
212
+ },
213
+ });
214
+ return token;
215
+ }
216
+ async issueRefreshTokenRecord(input) {
217
+ const refreshToken = this.generateOpaqueToken("klr");
218
+ const expiresAt = new Date(Date.now() + this.refreshTokenTtlSeconds * 1000).toISOString();
219
+ await this.refreshTokens.issue({
220
+ tokenHash: hashOpaqueToken(refreshToken),
221
+ clientId: input.clientId,
222
+ tenantId: input.tenantId,
223
+ subject: input.subject,
224
+ scopes: input.scopes,
225
+ roles: input.roles,
226
+ resource: input.resource,
227
+ expiresAt,
228
+ });
229
+ await this.audit.record({
230
+ type: "auth.token",
231
+ action: "auth.refresh.issue",
232
+ outcome: "success",
233
+ tenantId: input.tenantId,
234
+ principal: input.subject,
235
+ metadata: {
236
+ clientId: input.clientId,
237
+ tenantId: input.tenantId,
238
+ scopes: input.scopes,
239
+ resource: input.resource ?? null,
240
+ expiresAt,
241
+ },
242
+ });
243
+ return refreshToken;
244
+ }
245
+ oauthMetadata() {
246
+ return {
247
+ issuer: this.issuerUrl,
248
+ token_endpoint: this.tokenEndpointUrl(),
249
+ authorization_endpoint: this.authorizationEndpointUrl(),
250
+ grant_types_supported: ["client_credentials", "authorization_code", "refresh_token"],
251
+ token_endpoint_auth_methods_supported: [
252
+ "client_secret_post",
253
+ "client_secret_basic",
254
+ "private_key_jwt",
255
+ "none",
256
+ ],
257
+ token_endpoint_auth_signing_alg_values_supported: ["RS256", "ES256"],
258
+ scopes_supported: accessScopeSchema.options,
259
+ code_challenge_methods_supported: ["S256"],
260
+ };
261
+ }
262
+ protectedResourceMetadata(resourcePath) {
263
+ const resource = `${this.publicBaseUrl}${resourcePath}`;
264
+ return {
265
+ resource,
266
+ authorization_servers: [this.issuerUrl],
267
+ scopes_supported: accessScopeSchema.options,
268
+ bearer_methods_supported: ["header"],
269
+ resource_name: resourcePath === "/mcp" ? "KeyLore MCP" : "KeyLore REST API",
270
+ };
271
+ }
272
+ async authorize(actor, input) {
273
+ const client = await this.clients.getByClientId(input.clientId);
274
+ if (!client || client.status !== "active") {
275
+ throw new Error("Unknown authorization client.");
276
+ }
277
+ if (!client.grantTypes.includes("authorization_code")) {
278
+ throw new Error("Client does not support authorization_code.");
279
+ }
280
+ if (!client.redirectUris.includes(input.redirectUri)) {
281
+ throw new Error("Invalid redirect URI.");
282
+ }
283
+ if (actor.tenantId && client.tenantId !== actor.tenantId) {
284
+ throw new Error("Tenant access denied.");
285
+ }
286
+ if (actor.resource && input.resource && normalizeResource(actor.resource) !== normalizeResource(input.resource)) {
287
+ throw new Error("Requested resource exceeds the caller resource binding.");
288
+ }
289
+ await this.requireActiveTenant(client.tenantId);
290
+ const requestedScopes = input.scope?.length ? input.scope : client.allowedScopes;
291
+ const grantedScopes = uniqueScopes(requestedScopes.filter((scope) => actor.scopes.includes(scope) && client.allowedScopes.includes(scope)));
292
+ if (grantedScopes.length === 0) {
293
+ throw new Error("No valid scopes were granted.");
294
+ }
295
+ const grantedRoles = uniqueRoles(actor.roles.filter((role) => client.roles.includes(role)));
296
+ if (grantedRoles.length === 0) {
297
+ throw new Error("No valid roles were granted.");
298
+ }
299
+ const code = this.generateOpaqueToken("klc");
300
+ const expiresAt = new Date(Date.now() + this.authorizationCodeTtlSeconds * 1000).toISOString();
301
+ await this.authorizationCodes.create({
302
+ codeId: randomUUID(),
303
+ codeHash: hashOpaqueToken(code),
304
+ clientId: client.clientId,
305
+ tenantId: client.tenantId,
306
+ subject: actor.principal,
307
+ scopes: grantedScopes,
308
+ roles: grantedRoles,
309
+ resource: input.resource ?? actor.resource,
310
+ redirectUri: input.redirectUri,
311
+ codeChallenge: input.codeChallenge,
312
+ codeChallengeMethod: input.codeChallengeMethod,
313
+ expiresAt,
314
+ });
315
+ await this.audit.record({
316
+ type: "auth.token",
317
+ action: "auth.code.authorize",
318
+ outcome: "success",
319
+ tenantId: client.tenantId,
320
+ principal: actor.principal,
321
+ metadata: {
322
+ clientId: client.clientId,
323
+ tenantId: client.tenantId,
324
+ redirectUri: input.redirectUri,
325
+ scopes: grantedScopes,
326
+ resource: input.resource ?? actor.resource ?? null,
327
+ expiresAt,
328
+ },
329
+ });
330
+ return authorizationRequestOutputSchema.parse({
331
+ code,
332
+ clientId: client.clientId,
333
+ tenantId: client.tenantId,
334
+ subject: actor.principal,
335
+ redirectUri: input.redirectUri,
336
+ expiresIn: this.authorizationCodeTtlSeconds,
337
+ scope: grantedScopes.join(" "),
338
+ state: input.state,
339
+ });
340
+ }
341
+ async issueToken(input) {
342
+ const client = await this.clients.getByClientId(input.clientId);
343
+ if (!client || client.status !== "active") {
344
+ this.telemetry.recordAuthTokenIssued("error");
345
+ throw new Error("Invalid client credentials.");
346
+ }
347
+ await this.requireActiveTenant(client.tenantId);
348
+ try {
349
+ if (input.grantType === "client_credentials") {
350
+ await this.authenticateClientCredentials(client, input);
351
+ if (!client.grantTypes.includes("client_credentials")) {
352
+ throw new Error("Unsupported grant type for client.");
353
+ }
354
+ const requestedScopes = input.scope?.length ? input.scope : client.allowedScopes;
355
+ const grantedScopes = uniqueScopes(requestedScopes.filter((scope) => client.allowedScopes.includes(scope)));
356
+ if (grantedScopes.length === 0) {
357
+ throw new Error("No valid scopes were granted.");
358
+ }
359
+ const accessToken = await this.issueAccessTokenRecord({
360
+ clientId: client.clientId,
361
+ tenantId: client.tenantId,
362
+ subject: client.clientId,
363
+ scopes: grantedScopes,
364
+ roles: client.roles,
365
+ resource: input.resource,
366
+ grantType: "client_credentials",
367
+ });
368
+ this.telemetry.recordAuthTokenIssued("success");
369
+ return tokenIssueOutputSchema.parse({
370
+ access_token: accessToken,
371
+ token_type: "Bearer",
372
+ expires_in: this.accessTokenTtlSeconds,
373
+ scope: grantedScopes.join(" "),
374
+ });
375
+ }
376
+ if (input.grantType === "authorization_code") {
377
+ await this.authenticateClientCredentials(client, input);
378
+ if (!client.grantTypes.includes("authorization_code")) {
379
+ throw new Error("Unsupported grant type for client.");
380
+ }
381
+ const authorizationCode = await this.authorizationCodes.consumeByHash(hashOpaqueToken(input.code ?? ""));
382
+ if (!authorizationCode) {
383
+ throw new Error("Invalid authorization code.");
384
+ }
385
+ if (authorizationCode.clientId !== client.clientId) {
386
+ throw new Error("Invalid authorization code.");
387
+ }
388
+ if (authorizationCode.redirectUri !== input.redirectUri) {
389
+ throw new Error("Invalid redirect URI.");
390
+ }
391
+ if (authorizationCode.codeChallengeMethod !== "S256") {
392
+ throw new Error("Unsupported code challenge method.");
393
+ }
394
+ if (authorizationCode.codeChallenge !== this.codeChallengeForVerifier(input.codeVerifier ?? "")) {
395
+ throw new Error("Invalid code verifier.");
396
+ }
397
+ const grantedScopes = uniqueScopes((input.scope?.length ? input.scope : authorizationCode.scopes).filter((scope) => authorizationCode.scopes.includes(scope)));
398
+ if (grantedScopes.length === 0) {
399
+ throw new Error("No valid scopes were granted.");
400
+ }
401
+ const accessToken = await this.issueAccessTokenRecord({
402
+ clientId: client.clientId,
403
+ tenantId: authorizationCode.tenantId,
404
+ subject: authorizationCode.subject,
405
+ scopes: grantedScopes,
406
+ roles: authorizationCode.roles,
407
+ resource: authorizationCode.resource,
408
+ grantType: "authorization_code",
409
+ });
410
+ const refreshToken = client.grantTypes.includes("refresh_token")
411
+ ? await this.issueRefreshTokenRecord({
412
+ clientId: client.clientId,
413
+ tenantId: authorizationCode.tenantId,
414
+ subject: authorizationCode.subject,
415
+ scopes: grantedScopes,
416
+ roles: authorizationCode.roles,
417
+ resource: authorizationCode.resource,
418
+ })
419
+ : undefined;
420
+ this.telemetry.recordAuthTokenIssued("success");
421
+ return tokenIssueOutputSchema.parse({
422
+ access_token: accessToken,
423
+ refresh_token: refreshToken,
424
+ token_type: "Bearer",
425
+ expires_in: this.accessTokenTtlSeconds,
426
+ scope: grantedScopes.join(" "),
427
+ });
428
+ }
429
+ await this.authenticateClientCredentials(client, input);
430
+ if (!client.grantTypes.includes("refresh_token")) {
431
+ throw new Error("Unsupported grant type for client.");
432
+ }
433
+ const refreshTokenRecord = await this.refreshTokens.getByHash(hashOpaqueToken(input.refreshToken ?? ""));
434
+ if (!refreshTokenRecord || refreshTokenRecord.status !== "active") {
435
+ throw new Error("Invalid refresh token.");
436
+ }
437
+ if (refreshTokenRecord.clientId !== client.clientId) {
438
+ throw new Error("Invalid refresh token.");
439
+ }
440
+ if (new Date(refreshTokenRecord.expiresAt).getTime() <= Date.now()) {
441
+ throw new Error("Refresh token expired.");
442
+ }
443
+ if (refreshTokenRecord.resource &&
444
+ input.resource &&
445
+ normalizeResource(refreshTokenRecord.resource) !== normalizeResource(input.resource)) {
446
+ throw new Error("Refresh token resource does not match this protected resource.");
447
+ }
448
+ await this.requireActiveTenant(refreshTokenRecord.tenantId);
449
+ const grantedScopes = uniqueScopes((input.scope?.length ? input.scope : refreshTokenRecord.scopes).filter((scope) => refreshTokenRecord.scopes.includes(scope)));
450
+ if (grantedScopes.length === 0) {
451
+ throw new Error("No valid scopes were granted.");
452
+ }
453
+ const accessToken = await this.issueAccessTokenRecord({
454
+ clientId: client.clientId,
455
+ tenantId: refreshTokenRecord.tenantId,
456
+ subject: refreshTokenRecord.subject,
457
+ scopes: grantedScopes,
458
+ roles: refreshTokenRecord.roles,
459
+ resource: refreshTokenRecord.resource,
460
+ grantType: "refresh_token",
461
+ });
462
+ const rotatedRefreshToken = await this.issueRefreshTokenRecord({
463
+ clientId: client.clientId,
464
+ tenantId: refreshTokenRecord.tenantId,
465
+ subject: refreshTokenRecord.subject,
466
+ scopes: grantedScopes,
467
+ roles: refreshTokenRecord.roles,
468
+ resource: refreshTokenRecord.resource,
469
+ });
470
+ const replacementRecord = await this.refreshTokens.getByHash(hashOpaqueToken(rotatedRefreshToken));
471
+ if (!replacementRecord) {
472
+ throw new Error("Failed to rotate refresh token.");
473
+ }
474
+ await this.refreshTokens.replace(hashOpaqueToken(input.refreshToken ?? ""), replacementRecord.refreshTokenId);
475
+ await this.audit.record({
476
+ type: "auth.token",
477
+ action: "auth.refresh.rotate",
478
+ outcome: "success",
479
+ tenantId: refreshTokenRecord.tenantId,
480
+ principal: refreshTokenRecord.subject,
481
+ metadata: {
482
+ clientId: client.clientId,
483
+ tenantId: refreshTokenRecord.tenantId,
484
+ refreshTokenId: refreshTokenRecord.refreshTokenId,
485
+ },
486
+ });
487
+ this.telemetry.recordAuthTokenIssued("success");
488
+ return tokenIssueOutputSchema.parse({
489
+ access_token: accessToken,
490
+ refresh_token: rotatedRefreshToken,
491
+ token_type: "Bearer",
492
+ expires_in: this.accessTokenTtlSeconds,
493
+ scope: grantedScopes.join(" "),
494
+ });
495
+ }
496
+ catch (error) {
497
+ this.telemetry.recordAuthTokenIssued("error");
498
+ throw error;
499
+ }
500
+ }
501
+ async authenticateBearerToken(token, requestedResource) {
502
+ const stored = await this.tokens.getByHash(hashOpaqueToken(token));
503
+ if (!stored || stored.status !== "active") {
504
+ this.telemetry.recordAuthTokenValidation("error");
505
+ throw new Error("Invalid access token.");
506
+ }
507
+ await this.requireActiveTenant(stored.tenantId);
508
+ if (new Date(stored.expiresAt).getTime() <= Date.now()) {
509
+ this.telemetry.recordAuthTokenValidation("error");
510
+ throw new Error("Access token expired.");
511
+ }
512
+ if (stored.resource &&
513
+ requestedResource &&
514
+ normalizeResource(stored.resource) !== normalizeResource(requestedResource)) {
515
+ this.telemetry.recordAuthTokenValidation("error");
516
+ throw new Error("Access token resource does not match this protected resource.");
517
+ }
518
+ await this.tokens.touch(stored.tokenHash);
519
+ this.telemetry.recordAuthTokenValidation("success");
520
+ return authContextSchema.parse({
521
+ principal: stored.subject,
522
+ clientId: stored.clientId,
523
+ tenantId: stored.tenantId,
524
+ roles: stored.roles,
525
+ scopes: stored.scopes,
526
+ resource: stored.resource,
527
+ });
528
+ }
529
+ requireScopes(context, requiredScopes) {
530
+ const missing = requiredScopes.filter((scope) => !context.scopes.includes(scope));
531
+ if (missing.length > 0) {
532
+ throw new Error(`Missing required scopes: ${missing.join(", ")}`);
533
+ }
534
+ }
535
+ requireAnyScope(context, allowedScopes) {
536
+ if (!allowedScopes.some((scope) => context.scopes.includes(scope))) {
537
+ throw new Error(`Missing one of the required scopes: ${allowedScopes.join(", ")}`);
538
+ }
539
+ }
540
+ requireRoles(context, requiredRoles) {
541
+ if (!requiredRoles.some((role) => context.roles.includes(role))) {
542
+ throw new Error(`Missing required role. Need one of: ${requiredRoles.join(", ")}`);
543
+ }
544
+ }
545
+ stripClientSecrets(client) {
546
+ return client;
547
+ }
548
+ async listClients() {
549
+ return this.clients.list();
550
+ }
551
+ async createClient(actor, input) {
552
+ const existing = await this.clients.getByClientId(input.clientId);
553
+ if (existing) {
554
+ throw new Error(`Client already exists: ${input.clientId}`);
555
+ }
556
+ if (actor.tenantId && input.tenantId !== actor.tenantId) {
557
+ throw new Error("Tenant access denied.");
558
+ }
559
+ const tenant = await this.tenants.getById(input.tenantId);
560
+ if (!tenant) {
561
+ throw new Error(`Unknown tenant: ${input.tenantId}`);
562
+ }
563
+ const isSharedSecretClient = !["private_key_jwt", "none"].includes(input.tokenEndpointAuthMethod);
564
+ const clientSecret = isSharedSecretClient ? input.clientSecret ?? this.generateClientSecret() : undefined;
565
+ const hashed = clientSecret ? hashSecret(clientSecret) : undefined;
566
+ await this.clients.upsert({
567
+ clientId: input.clientId,
568
+ tenantId: input.tenantId,
569
+ displayName: input.displayName,
570
+ secretHash: hashed?.hash,
571
+ secretSalt: hashed?.salt,
572
+ roles: input.roles,
573
+ allowedScopes: input.allowedScopes,
574
+ status: input.status,
575
+ tokenEndpointAuthMethod: input.tokenEndpointAuthMethod,
576
+ grantTypes: input.grantTypes,
577
+ redirectUris: input.redirectUris,
578
+ jwks: input.jwks ?? [],
579
+ });
580
+ const client = this.buildClientRecord({
581
+ clientId: input.clientId,
582
+ tenantId: input.tenantId,
583
+ displayName: input.displayName,
584
+ roles: input.roles,
585
+ allowedScopes: input.allowedScopes,
586
+ status: input.status,
587
+ tokenEndpointAuthMethod: input.tokenEndpointAuthMethod,
588
+ grantTypes: input.grantTypes,
589
+ redirectUris: input.redirectUris,
590
+ jwks: input.jwks,
591
+ });
592
+ await this.audit.record({
593
+ type: "auth.client",
594
+ action: "auth.client.create",
595
+ outcome: "success",
596
+ tenantId: client.tenantId,
597
+ principal: actor.principal,
598
+ metadata: {
599
+ clientId: client.clientId,
600
+ tenantId: client.tenantId,
601
+ roles: client.roles,
602
+ allowedScopes: client.allowedScopes,
603
+ status: client.status,
604
+ authMethod: client.tokenEndpointAuthMethod,
605
+ grantTypes: client.grantTypes,
606
+ redirectUris: client.redirectUris,
607
+ },
608
+ });
609
+ return authClientSecretOutputSchema.parse({
610
+ client,
611
+ clientSecret,
612
+ });
613
+ }
614
+ async updateClient(actor, clientId, patch) {
615
+ const existing = await this.clients.getByClientId(clientId);
616
+ if (!existing) {
617
+ return undefined;
618
+ }
619
+ if (actor.tenantId && existing.tenantId !== actor.tenantId) {
620
+ throw new Error("Tenant access denied.");
621
+ }
622
+ const merged = this.buildClientRecord({
623
+ clientId,
624
+ tenantId: existing.tenantId,
625
+ displayName: patch.displayName ?? existing.displayName,
626
+ roles: patch.roles ?? existing.roles,
627
+ allowedScopes: patch.allowedScopes ?? existing.allowedScopes,
628
+ status: patch.status ?? existing.status,
629
+ tokenEndpointAuthMethod: patch.tokenEndpointAuthMethod ?? existing.tokenEndpointAuthMethod,
630
+ grantTypes: patch.grantTypes ?? existing.grantTypes,
631
+ redirectUris: patch.redirectUris ?? existing.redirectUris,
632
+ jwks: patch.jwks ?? existing.jwks,
633
+ });
634
+ const sharedSecretClient = !["private_key_jwt", "none"].includes(merged.tokenEndpointAuthMethod);
635
+ await this.clients.upsert({
636
+ clientId,
637
+ tenantId: merged.tenantId,
638
+ displayName: merged.displayName,
639
+ secretHash: sharedSecretClient ? existing.secretHash : undefined,
640
+ secretSalt: sharedSecretClient ? existing.secretSalt : undefined,
641
+ roles: merged.roles,
642
+ allowedScopes: merged.allowedScopes,
643
+ status: merged.status,
644
+ tokenEndpointAuthMethod: merged.tokenEndpointAuthMethod,
645
+ grantTypes: merged.grantTypes,
646
+ redirectUris: merged.redirectUris,
647
+ jwks: merged.jwks,
648
+ });
649
+ if (patch.roles ||
650
+ patch.allowedScopes ||
651
+ patch.status ||
652
+ patch.tokenEndpointAuthMethod ||
653
+ patch.grantTypes ||
654
+ patch.redirectUris ||
655
+ patch.jwks) {
656
+ await this.tokens.revokeByClientId(clientId);
657
+ await this.refreshTokens.revokeByClientId(clientId);
658
+ await this.authorizationCodes.revokeByClientId(clientId);
659
+ }
660
+ await this.audit.record({
661
+ type: "auth.client",
662
+ action: "auth.client.update",
663
+ outcome: "success",
664
+ tenantId: merged.tenantId,
665
+ principal: actor.principal,
666
+ metadata: {
667
+ clientId,
668
+ tenantId: merged.tenantId,
669
+ fields: Object.keys(patch),
670
+ status: merged.status,
671
+ authMethod: merged.tokenEndpointAuthMethod,
672
+ grantTypes: merged.grantTypes,
673
+ },
674
+ });
675
+ return merged;
676
+ }
677
+ async rotateClientSecret(actor, clientId, clientSecret) {
678
+ const existing = await this.clients.getByClientId(clientId);
679
+ if (!existing) {
680
+ return undefined;
681
+ }
682
+ if (actor.tenantId && existing.tenantId !== actor.tenantId) {
683
+ throw new Error("Tenant access denied.");
684
+ }
685
+ if (["private_key_jwt", "none"].includes(existing.tokenEndpointAuthMethod)) {
686
+ throw new Error(`${existing.tokenEndpointAuthMethod} clients do not support shared-secret rotation.`);
687
+ }
688
+ const secret = clientSecret ?? this.generateClientSecret();
689
+ const hashed = hashSecret(secret);
690
+ await this.clients.upsert({
691
+ clientId,
692
+ tenantId: existing.tenantId,
693
+ displayName: existing.displayName,
694
+ secretHash: hashed.hash,
695
+ secretSalt: hashed.salt,
696
+ roles: existing.roles,
697
+ allowedScopes: existing.allowedScopes,
698
+ status: existing.status,
699
+ tokenEndpointAuthMethod: existing.tokenEndpointAuthMethod,
700
+ grantTypes: existing.grantTypes,
701
+ redirectUris: existing.redirectUris,
702
+ jwks: existing.jwks,
703
+ });
704
+ await this.tokens.revokeByClientId(clientId);
705
+ await this.refreshTokens.revokeByClientId(clientId);
706
+ await this.authorizationCodes.revokeByClientId(clientId);
707
+ const client = this.buildClientRecord(existing);
708
+ await this.audit.record({
709
+ type: "auth.client",
710
+ action: "auth.client.rotate_secret",
711
+ outcome: "success",
712
+ tenantId: client.tenantId,
713
+ principal: actor.principal,
714
+ metadata: {
715
+ clientId,
716
+ tenantId: client.tenantId,
717
+ },
718
+ });
719
+ return authClientSecretOutputSchema.parse({
720
+ client,
721
+ clientSecret: secret,
722
+ });
723
+ }
724
+ async listTokens(filter) {
725
+ return this.tokens.list(filter);
726
+ }
727
+ async listRefreshTokens(filter) {
728
+ return this.refreshTokens.list(filter);
729
+ }
730
+ async revokeToken(actor, tokenId) {
731
+ const existing = await this.tokens.getById(tokenId);
732
+ if (!existing) {
733
+ return undefined;
734
+ }
735
+ if (actor.tenantId && existing.tenantId !== actor.tenantId) {
736
+ throw new Error("Tenant access denied.");
737
+ }
738
+ const token = await this.tokens.revokeById(tokenId);
739
+ if (!token) {
740
+ return undefined;
741
+ }
742
+ await this.audit.record({
743
+ type: "auth.token",
744
+ action: "auth.token.revoke",
745
+ outcome: "success",
746
+ tenantId: token.tenantId,
747
+ principal: actor.principal,
748
+ metadata: {
749
+ tokenId: token.tokenId,
750
+ tenantId: token.tenantId,
751
+ clientId: token.clientId,
752
+ subject: token.subject,
753
+ },
754
+ });
755
+ return token;
756
+ }
757
+ async revokeRefreshToken(actor, refreshTokenId) {
758
+ const existing = await this.refreshTokens.getById(refreshTokenId);
759
+ if (!existing) {
760
+ return undefined;
761
+ }
762
+ if (actor.tenantId && existing.tenantId !== actor.tenantId) {
763
+ throw new Error("Tenant access denied.");
764
+ }
765
+ const token = await this.refreshTokens.revokeById(refreshTokenId);
766
+ if (!token) {
767
+ return undefined;
768
+ }
769
+ await this.audit.record({
770
+ type: "auth.token",
771
+ action: "auth.refresh.revoke",
772
+ outcome: "success",
773
+ tenantId: token.tenantId,
774
+ principal: actor.principal,
775
+ metadata: {
776
+ refreshTokenId: token.refreshTokenId,
777
+ tenantId: token.tenantId,
778
+ clientId: token.clientId,
779
+ subject: token.subject,
780
+ },
781
+ });
782
+ return token;
783
+ }
784
+ }