@julr/sesame 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +405 -62
  3. package/build/authorize_controller-BiycO4be.js +251 -0
  4. package/build/chunk-DF48asd8.js +9 -0
  5. package/build/{client_info_controller-BucHGx4u.js → client_info_controller-AcOG8lWu.js} +11 -3
  6. package/build/commands/sesame_client.d.ts +20 -0
  7. package/build/commands/sesame_key.d.ts +12 -0
  8. package/build/commands/sesame_purge.d.ts +0 -2
  9. package/build/commands/sesame_purge.js +15 -3
  10. package/build/configure-DkDkIlt8.js +27 -0
  11. package/build/configure.js +2 -24
  12. package/build/consent_controller-Dsdhv6-f.js +108 -0
  13. package/build/id_token_service-CpTzOUDe.js +54 -0
  14. package/build/index.d.ts +1 -1
  15. package/build/index.js +30 -10
  16. package/build/{introspect_controller-un95fs4y.js → introspect_controller-DvOp9scr.js} +21 -7
  17. package/build/issue_authorization_code-B9ERu1uO.js +40 -0
  18. package/build/jwks_controller-keo4kBZc.js +26 -0
  19. package/build/{main-B3M6ihoS.js → main-DGBJhq3E.js} +34 -4
  20. package/build/metadata_controller-BVsTo0Gp.js +158 -0
  21. package/build/{oauth_access_token-bsoM5KeU.js → oauth_access_token-Cz_5gNBx.js} +12 -1
  22. package/build/oauth_client-BSanvSql.js +63 -0
  23. package/build/oauth_error-C7UhDb2q.js +189 -0
  24. package/build/providers/sesame_provider.js +14 -3
  25. package/build/{register_controller-Dch4ecyD.js → register_controller-gbq7p8a5.js} +46 -7
  26. package/build/{revoke_controller-DnPmzYMd.js → revoke_controller-z_ghrEB7.js} +21 -8
  27. package/build/services/main.js +7 -3
  28. package/build/sesame_manager-B1Jgq1v2.js +6 -0
  29. package/build/sesame_manager-DYUSZ0NC.js +693 -0
  30. package/build/src/actions/authorize.d.ts +46 -0
  31. package/build/src/actions/exchange_authorization_code.d.ts +34 -0
  32. package/build/src/actions/exchange_client_credentials.d.ts +28 -0
  33. package/build/src/actions/exchange_refresh_token.d.ts +59 -0
  34. package/build/src/actions/issue_authorization_code.d.ts +26 -0
  35. package/build/src/controllers/authorize_controller.d.ts +13 -12
  36. package/build/src/controllers/consent_controller.d.ts +5 -0
  37. package/build/src/controllers/jwks_controller.d.ts +14 -0
  38. package/build/src/controllers/metadata_controller.d.ts +9 -2
  39. package/build/src/controllers/token_controller.d.ts +8 -5
  40. package/build/src/controllers/userinfo_controller.d.ts +14 -0
  41. package/build/src/guard/main.js +5 -5
  42. package/build/src/middleware/any_scope_middleware.js +11 -1
  43. package/build/src/middleware/scope_middleware.js +11 -1
  44. package/build/src/models/oauth_authorization_code.d.ts +1 -0
  45. package/build/src/models/oauth_pending_authorization_request.d.ts +1 -0
  46. package/build/src/oauth_error.d.ts +1 -1
  47. package/build/src/routes.d.ts +3 -1
  48. package/build/src/services/id_token_service.d.ts +30 -0
  49. package/build/src/services/key_service.d.ts +20 -0
  50. package/build/src/sesame_manager.d.ts +54 -3
  51. package/build/src/types.d.ts +124 -3
  52. package/build/stubs/main.ts +5 -0
  53. package/build/stubs/migrations/create_oauth_authorization_codes_table.stub +1 -0
  54. package/build/stubs/migrations/create_oauth_pending_authorization_requests_table.stub +1 -0
  55. package/build/stubs/migrations/create_oauth_refresh_tokens_table.stub +1 -1
  56. package/build/token_controller-DyI7oy-U.js +481 -0
  57. package/build/token_service-DwnfAR9F.js +59 -0
  58. package/build/userinfo_controller-RLk8cN_o.js +40 -0
  59. package/build/vite.config.d.ts +2 -0
  60. package/package.json +26 -41
  61. package/build/authorize_controller-BGzxPvYU.js +0 -138
  62. package/build/client_service-C3rfXGk_.js +0 -65
  63. package/build/consent_controller-BHoB9mip.js +0 -85
  64. package/build/decorate-BKZEjPRg.js +0 -15
  65. package/build/metadata_controller-CJeZG93_.js +0 -81
  66. package/build/oauth_client-BIoY5jBR.js +0 -24
  67. package/build/oauth_error-CnJ3L8tf.js +0 -94
  68. package/build/sesame_manager-BQIW2mqt.js +0 -4
  69. package/build/sesame_manager-C-eEFFHM.js +0 -167
  70. package/build/src/grants/authorization_code_grant.d.ts +0 -27
  71. package/build/src/grants/client_credentials_grant.d.ts +0 -23
  72. package/build/src/grants/refresh_token_grant.d.ts +0 -27
  73. package/build/token_controller-hGDAYuBS.js +0 -194
  74. package/build/token_service-fhoA4slP.js +0 -31
@@ -0,0 +1,481 @@
1
+ import "./chunk-DF48asd8.js";
2
+ import { a as OAuthRefreshToken, c as OIDC_SCOPES, i as OAuthAuthorizationCode, o as ClientService, s as BUILTIN_SCOPES, t as SesameManager } from "./sesame_manager-DYUSZ0NC.js";
3
+ import "./oauth_client-BSanvSql.js";
4
+ import { a as E_INVALID_GRANT, o as E_INVALID_REQUEST, r as E_INVALID_CLIENT, s as E_INVALID_SCOPE, u as E_UNSUPPORTED_GRANT_TYPE } from "./oauth_error-C7UhDb2q.js";
5
+ import { t as OAuthAccessToken } from "./oauth_access_token-Cz_5gNBx.js";
6
+ import { t as TokenService } from "./token_service-DwnfAR9F.js";
7
+ import { t as IdTokenService } from "./id_token_service-CpTzOUDe.js";
8
+ import { DateTime } from "luxon";
9
+ import { createHash } from "node:crypto";
10
+ import string from "@adonisjs/core/helpers/string";
11
+ import vine from "@vinejs/vine";
12
+ //#region src/actions/exchange_authorization_code.ts
13
+ /**
14
+ * Validates the code_verifier format per RFC 7636 §4.1.
15
+ * 43-128 characters from the unreserved character set [A-Za-z0-9-._~].
16
+ */
17
+ const codeVerifierValidator = vine.create({ code_verifier: vine.string().minLength(43).maxLength(128).regex(/^[A-Za-z0-9\-._~]+$/) });
18
+ /**
19
+ * Handle the Authorization Code Grant (RFC 6749 §4.1.3).
20
+ *
21
+ * Exchanges an authorization code for an access token and
22
+ * optionally a refresh token and id_token. Verifies PKCE,
23
+ * validates scopes, and atomically consumes the code to
24
+ * prevent replay.
25
+ *
26
+ * @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3
27
+ * @see https://datatracker.ietf.org/doc/html/rfc7636#section-4.6
28
+ */
29
+ var ExchangeAuthorizationCodeAction = class {
30
+ /**
31
+ * Exchange an authorization code for tokens. The code is
32
+ * consumed atomically inside a transaction.
33
+ */
34
+ async execute(manager, input) {
35
+ const tokenService = new TokenService(manager);
36
+ const clientService = new ClientService();
37
+ if (!input.client.grantTypes.includes("authorization_code")) throw new E_INVALID_CLIENT("Client is not allowed to use the authorization_code grant");
38
+ const authCode = await this.#validateAuthorizationCode(tokenService, input);
39
+ await this.#verifyPkce(input.codeVerifier, authCode);
40
+ clientService.validateClientScopes(authCode.scopes, input.client.scopes);
41
+ const accessToken = tokenService.createAccessToken();
42
+ const refreshToken = this.#prepareRefreshToken(manager, tokenService);
43
+ const idToken = await this.#prepareIdToken(manager, authCode, input.client, accessToken.raw);
44
+ await this.#atomicExchange(input, authCode, accessToken, refreshToken);
45
+ const ttlSeconds = string.seconds.parse(manager.config.accessTokenTtl);
46
+ return {
47
+ access_token: accessToken.raw,
48
+ token_type: "Bearer",
49
+ expires_in: ttlSeconds,
50
+ scope: authCode.scopes.join(" "),
51
+ ...refreshToken ? { refresh_token: refreshToken.raw } : {},
52
+ ...idToken ? { id_token: idToken } : {}
53
+ };
54
+ }
55
+ /**
56
+ * Lookup the authorization code by hash and validate
57
+ * it has not expired and matches the redirect_uri.
58
+ */
59
+ async #validateAuthorizationCode(tokenService, input) {
60
+ if (!input.code) throw new E_INVALID_REQUEST("Missing required parameter: code");
61
+ if (!input.redirectUri) throw new E_INVALID_REQUEST("Missing required parameter: redirect_uri");
62
+ const hashedCode = tokenService.hashToken(input.code);
63
+ const authCode = await OAuthAuthorizationCode.query().where("code", hashedCode).where("clientId", input.client.clientId).first();
64
+ if (!authCode) throw new E_INVALID_GRANT("Authorization code not found");
65
+ if (authCode.expiresAt < DateTime.now()) {
66
+ await OAuthAuthorizationCode.query().where("id", authCode.id).delete();
67
+ throw new E_INVALID_GRANT("Authorization code has expired");
68
+ }
69
+ if (authCode.redirectUri !== input.redirectUri) throw new E_INVALID_GRANT("Redirect URI mismatch");
70
+ return authCode;
71
+ }
72
+ /**
73
+ * Verify the PKCE code_verifier against the stored
74
+ * code_challenge using S256 (mandatory per OAuth 2.1).
75
+ */
76
+ async #verifyPkce(codeVerifier, authCode) {
77
+ const [verifierError] = await codeVerifierValidator.tryValidate({ code_verifier: codeVerifier });
78
+ if (verifierError) {
79
+ await OAuthAuthorizationCode.query().where("id", authCode.id).delete();
80
+ throw new E_INVALID_REQUEST("code_verifier must be 43-128 characters using only [A-Za-z0-9-._~] (RFC 7636 §4.1)");
81
+ }
82
+ if (!authCode.codeChallenge) {
83
+ await OAuthAuthorizationCode.query().where("id", authCode.id).delete();
84
+ throw new E_INVALID_GRANT("Authorization code is missing PKCE challenge");
85
+ }
86
+ if (createHash("sha256").update(codeVerifier).digest("base64url") !== authCode.codeChallenge) {
87
+ await OAuthAuthorizationCode.query().where("id", authCode.id).delete();
88
+ throw new E_INVALID_GRANT("PKCE verification failed");
89
+ }
90
+ }
91
+ /**
92
+ * Precompute refresh token values if the refresh_token
93
+ * grant type is enabled on the server.
94
+ */
95
+ #prepareRefreshToken(manager, tokenService) {
96
+ if (!manager.isGrantTypeEnabled("refresh_token")) return null;
97
+ const { raw, hash } = tokenService.createRefreshToken();
98
+ const refreshTtl = string.seconds.parse(manager.config.refreshTokenTtl);
99
+ return {
100
+ raw,
101
+ hash,
102
+ expiresAt: DateTime.now().plus({ seconds: refreshTtl })
103
+ };
104
+ }
105
+ /**
106
+ * Build an id_token JWT if the authorization code was
107
+ * granted the openid scope.
108
+ */
109
+ async #prepareIdToken(manager, authCode, client, accessTokenRaw) {
110
+ if (!authCode.scopes.includes("openid")) return null;
111
+ const idTokenService = new IdTokenService(manager);
112
+ const user = await manager.findUserById(authCode.userId);
113
+ if (!user) throw new E_INVALID_GRANT("OIDC user not found");
114
+ return idTokenService.sign({
115
+ sub: authCode.userId,
116
+ clientId: client.clientId,
117
+ scopes: authCode.scopes,
118
+ accessToken: accessTokenRaw,
119
+ user,
120
+ nonce: authCode.nonce ?? void 0
121
+ });
122
+ }
123
+ /**
124
+ * Atomically consume the authorization code and persist
125
+ * the new access token (and optionally refresh token)
126
+ * inside a single transaction.
127
+ */
128
+ async #atomicExchange(input, authCode, accessToken, refreshToken) {
129
+ await OAuthAuthorizationCode.transaction(async (trx) => {
130
+ const deleteResult = await OAuthAuthorizationCode.query({ client: trx }).where("id", authCode.id).delete();
131
+ if ((Array.isArray(deleteResult) ? Number(deleteResult[0] ?? 0) : Number(deleteResult)) !== 1) throw new E_INVALID_GRANT("Authorization code has already been consumed");
132
+ const accessTokenId = crypto.randomUUID();
133
+ await OAuthAccessToken.create({
134
+ id: accessTokenId,
135
+ tokenHash: accessToken.hash,
136
+ clientId: input.client.clientId,
137
+ userId: authCode.userId,
138
+ scopes: authCode.scopes,
139
+ expiresAt: DateTime.fromJSDate(accessToken.expiresAt)
140
+ }, { client: trx });
141
+ if (refreshToken) await OAuthRefreshToken.create({
142
+ id: crypto.randomUUID(),
143
+ token: refreshToken.hash,
144
+ accessTokenId,
145
+ clientId: input.client.clientId,
146
+ userId: authCode.userId,
147
+ scopes: authCode.scopes,
148
+ expiresAt: refreshToken.expiresAt
149
+ }, { client: trx });
150
+ });
151
+ }
152
+ };
153
+ //#endregion
154
+ //#region src/actions/exchange_refresh_token.ts
155
+ /**
156
+ * Handle the Refresh Token Grant (RFC 6749 §6).
157
+ *
158
+ * Exchanges a refresh token for a new access token and a new
159
+ * refresh token (rotation). The old refresh token is revoked
160
+ * immediately after use.
161
+ *
162
+ * ## Replay detection
163
+ *
164
+ * If a revoked refresh token is presented **outside** the grace
165
+ * period, all tokens for that client+user pair are nuked to
166
+ * mitigate stolen-token reuse (RFC 6819 §5.2.2.3, RFC 9700 §4.14.2).
167
+ *
168
+ * ## Grace period (rotation reuse window)
169
+ *
170
+ * OAuth 2.1 requires that rotated refresh tokens be single-use.
171
+ * However, that requirement conflicts with the realities of
172
+ * distributed systems: if the server rotates the token but the
173
+ * client never receives (or persists) the new token — due to a
174
+ * network failure, a concurrent refresh from another process, or
175
+ * a retry after timeout — the client loses its grant permanently.
176
+ *
177
+ * To handle this, we allow a recently-rotated refresh token to be
178
+ * reused within a short configurable window (`refreshTokenRotationGracePeriod`,
179
+ * defaults to 120 s). During that window the old token issues fresh
180
+ * tokens without triggering replay-attack revocation.
181
+ *
182
+ * This is the same approach used by Auth0 ("reuse interval") and
183
+ * Cloudflare workers-oauth-provider ("previous token"). It provides
184
+ * most of the security benefits of strict rotation while remaining
185
+ * reliable for real-world clients (MCP SDK, multi-process CLIs, etc.).
186
+ *
187
+ * @see https://datatracker.ietf.org/doc/html/rfc6749#section-6
188
+ * @see https://datatracker.ietf.org/doc/html/rfc6819#section-5.2.2.3
189
+ * @see https://datatracker.ietf.org/doc/html/rfc9700#section-4.14.2
190
+ */
191
+ var ExchangeRefreshTokenAction = class {
192
+ /**
193
+ * Rotate the refresh token, issue a new access token,
194
+ * and optionally reissue an id_token if openid scope
195
+ * is present.
196
+ */
197
+ async execute(manager, input) {
198
+ const tokenService = new TokenService(manager);
199
+ const clientService = new ClientService();
200
+ if (!input.refreshToken) throw new E_INVALID_REQUEST("Missing required parameter: refresh_token");
201
+ if (!input.client.grantTypes.includes("refresh_token")) throw new E_INVALID_CLIENT("Client is not allowed to use the refresh_token grant");
202
+ const hashedToken = tokenService.hashToken(input.refreshToken);
203
+ const refreshToken = await OAuthRefreshToken.query().where("token", hashedToken).where("clientId", input.client.clientId).first();
204
+ if (!refreshToken) throw new E_INVALID_GRANT("Refresh token not found");
205
+ if (refreshToken.revokedAt) {
206
+ const gracePeriodSeconds = manager.config.refreshTokenRotationGracePeriod;
207
+ const revokedSecondsAgo = DateTime.now().diff(refreshToken.revokedAt, "seconds").seconds;
208
+ if (gracePeriodSeconds <= 0 || revokedSecondsAgo > gracePeriodSeconds) {
209
+ await this.#nukeTokensForReplay(input.client.clientId, refreshToken.userId);
210
+ throw new E_INVALID_GRANT("Refresh token has been revoked (possible replay attack)");
211
+ }
212
+ /**
213
+ * Within grace period — the old refresh token was recently
214
+ * rotated and a concurrent client is reusing it. Issue new
215
+ * tokens without nuking the family. This matches Auth0 / Okta
216
+ * behavior for handling race conditions in multi-process
217
+ * clients (e.g. MCP SDK proactive refresh + SDK 401 retry).
218
+ */
219
+ return this.#issueFreshTokens(manager, input, refreshToken);
220
+ }
221
+ if (refreshToken.expiresAt < DateTime.now()) throw new E_INVALID_GRANT("Refresh token has expired");
222
+ const scopes = this.#resolveScopes(manager, input, refreshToken, clientService);
223
+ const accessToken = tokenService.createAccessToken();
224
+ const newRefreshToken = this.#prepareRefreshToken(manager, tokenService);
225
+ const idToken = await this.#prepareIdToken(manager, scopes, refreshToken, input.client, accessToken.raw);
226
+ await this.#atomicRotation(input, refreshToken, accessToken, newRefreshToken, scopes);
227
+ const ttlSeconds = string.seconds.parse(manager.config.accessTokenTtl);
228
+ return {
229
+ access_token: accessToken.raw,
230
+ token_type: "Bearer",
231
+ expires_in: ttlSeconds,
232
+ scope: scopes.join(" "),
233
+ refresh_token: newRefreshToken.raw,
234
+ ...idToken ? { id_token: idToken } : {}
235
+ };
236
+ }
237
+ /**
238
+ * Grace-period reuse: the old refresh token was rotated
239
+ * recently and a concurrent client replayed it. Issue a
240
+ * brand-new AT + RT pair directly (the old pair is already
241
+ * revoked from the first rotation).
242
+ */
243
+ async #issueFreshTokens(manager, input, revokedRefreshToken) {
244
+ const tokenService = new TokenService(manager);
245
+ const clientService = new ClientService();
246
+ const scopes = this.#resolveScopes(manager, input, revokedRefreshToken, clientService);
247
+ const accessToken = tokenService.createAccessToken();
248
+ const newRefreshToken = this.#prepareRefreshToken(manager, tokenService);
249
+ const idToken = await this.#prepareIdToken(manager, scopes, revokedRefreshToken, input.client, accessToken.raw);
250
+ const accessTokenId = crypto.randomUUID();
251
+ await OAuthAccessToken.create({
252
+ id: accessTokenId,
253
+ tokenHash: accessToken.hash,
254
+ clientId: input.client.clientId,
255
+ userId: revokedRefreshToken.userId,
256
+ scopes,
257
+ expiresAt: DateTime.fromJSDate(accessToken.expiresAt)
258
+ });
259
+ await OAuthRefreshToken.create({
260
+ id: crypto.randomUUID(),
261
+ token: newRefreshToken.hash,
262
+ accessTokenId,
263
+ clientId: input.client.clientId,
264
+ userId: revokedRefreshToken.userId,
265
+ scopes,
266
+ expiresAt: newRefreshToken.expiresAt
267
+ });
268
+ const ttlSeconds = string.seconds.parse(manager.config.accessTokenTtl);
269
+ return {
270
+ access_token: accessToken.raw,
271
+ token_type: "Bearer",
272
+ expires_in: ttlSeconds,
273
+ scope: scopes.join(" "),
274
+ refresh_token: newRefreshToken.raw,
275
+ ...idToken ? { id_token: idToken } : {}
276
+ };
277
+ }
278
+ /**
279
+ * Replay detection: nuke all tokens for this client+user
280
+ * pair when a revoked token is reused.
281
+ */
282
+ async #nukeTokensForReplay(clientId, userId) {
283
+ await OAuthRefreshToken.query().where("clientId", clientId).where("userId", userId).delete();
284
+ await OAuthAccessToken.query().where("clientId", clientId).where("userId", userId).whereNull("revokedAt").update({ revokedAt: DateTime.now().toSQL() });
285
+ }
286
+ /**
287
+ * Resolve the effective scopes for the new token. Supports
288
+ * scope narrowing but rejects scope escalation.
289
+ */
290
+ #resolveScopes(manager, input, refreshToken, clientService) {
291
+ if (!input.scope) {
292
+ clientService.validateClientScopes(refreshToken.scopes, input.client.scopes);
293
+ return refreshToken.scopes;
294
+ }
295
+ const requested = input.scope.split(" ");
296
+ const originalSet = new Set(refreshToken.scopes);
297
+ const invalid = requested.filter((s) => !originalSet.has(s));
298
+ if (invalid.length > 0) throw new E_INVALID_SCOPE(`Scope not in original grant: ${invalid.join(", ")}`);
299
+ const invalidScopes = manager.validateScopes(requested);
300
+ if (invalidScopes.length > 0) throw new E_INVALID_SCOPE(`Invalid scopes: ${invalidScopes.join(", ")}`);
301
+ if (manager.usesOidcScopes(requested) && !manager.isOidcEnabled) throw new E_INVALID_SCOPE("OIDC scopes require OIDC to be configured (set jwk and oidcProvider in config)");
302
+ clientService.validateClientScopes(requested, input.client.scopes);
303
+ return requested;
304
+ }
305
+ /**
306
+ * Precompute the new refresh token values for rotation.
307
+ */
308
+ #prepareRefreshToken(manager, tokenService) {
309
+ const { raw, hash } = tokenService.createRefreshToken();
310
+ const refreshTtl = string.seconds.parse(manager.config.refreshTokenTtl);
311
+ return {
312
+ raw,
313
+ hash,
314
+ expiresAt: DateTime.now().plus({ seconds: refreshTtl })
315
+ };
316
+ }
317
+ /**
318
+ * Reissue an id_token if openid scope is present.
319
+ * No nonce on refresh per OIDC Core §12.2.
320
+ */
321
+ async #prepareIdToken(manager, scopes, refreshToken, client, accessTokenRaw) {
322
+ if (!scopes.includes("openid")) return null;
323
+ const idTokenService = new IdTokenService(manager);
324
+ const user = await manager.findUserById(refreshToken.userId);
325
+ if (!user) throw new E_INVALID_GRANT("OIDC user not found");
326
+ return idTokenService.sign({
327
+ sub: refreshToken.userId,
328
+ clientId: client.clientId,
329
+ scopes,
330
+ accessToken: accessTokenRaw,
331
+ user
332
+ });
333
+ }
334
+ /**
335
+ * Atomically revoke the old token pair and persist the
336
+ * new access + refresh tokens inside a single transaction.
337
+ */
338
+ async #atomicRotation(input, oldRefreshToken, accessToken, newRefreshToken, scopes) {
339
+ await OAuthRefreshToken.transaction(async (trx) => {
340
+ const revokedAt = DateTime.now();
341
+ const updateResult = await OAuthRefreshToken.query({ client: trx }).where("id", oldRefreshToken.id).whereNull("revokedAt").update({ revokedAt: revokedAt.toSQL() });
342
+ if ((Array.isArray(updateResult) ? Number(updateResult[0] ?? 0) : Number(updateResult)) !== 1) throw new E_INVALID_GRANT("Refresh token has already been consumed");
343
+ await OAuthAccessToken.query({ client: trx }).where("id", oldRefreshToken.accessTokenId).whereNull("revokedAt").update({ revokedAt: revokedAt.toSQL() });
344
+ const accessTokenId = crypto.randomUUID();
345
+ await OAuthAccessToken.create({
346
+ id: accessTokenId,
347
+ tokenHash: accessToken.hash,
348
+ clientId: input.client.clientId,
349
+ userId: oldRefreshToken.userId,
350
+ scopes,
351
+ expiresAt: DateTime.fromJSDate(accessToken.expiresAt)
352
+ }, { client: trx });
353
+ await OAuthRefreshToken.create({
354
+ id: crypto.randomUUID(),
355
+ token: newRefreshToken.hash,
356
+ accessTokenId,
357
+ clientId: input.client.clientId,
358
+ userId: oldRefreshToken.userId,
359
+ scopes,
360
+ expiresAt: newRefreshToken.expiresAt
361
+ }, { client: trx });
362
+ });
363
+ }
364
+ };
365
+ //#endregion
366
+ //#region src/actions/exchange_client_credentials.ts
367
+ /**
368
+ * Handle the Client Credentials Grant (RFC 6749 §4.4).
369
+ *
370
+ * Issues an access token directly to a confidential client
371
+ * for machine-to-machine communication. No refresh token
372
+ * is issued. User-centric OIDC scopes are rejected.
373
+ *
374
+ * @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.4
375
+ */
376
+ var ExchangeClientCredentialsAction = class {
377
+ /**
378
+ * Validate the client, resolve scopes, and issue an
379
+ * access token for M2M usage.
380
+ */
381
+ async execute(manager, input) {
382
+ const clientService = new ClientService();
383
+ if (input.client.isPublic) throw new E_INVALID_CLIENT("Public clients cannot use the client_credentials grant");
384
+ if (!input.client.grantTypes.includes("client_credentials")) throw new E_INVALID_CLIENT("Client is not allowed to use the client_credentials grant");
385
+ const scopes = this.#resolveScopes(manager, input, clientService);
386
+ if (!input.client.userId) throw new E_INVALID_CLIENT("Client must be associated with a user to use the client_credentials grant");
387
+ const tokenService = new TokenService(manager);
388
+ const ttlSeconds = string.seconds.parse(manager.config.clientCredentialsAccessTokenTtl);
389
+ const { raw: accessTokenRaw, hash: tokenHash } = tokenService.createAccessToken();
390
+ const expiresAt = new Date(Date.now() + ttlSeconds * 1e3);
391
+ await OAuthAccessToken.create({
392
+ id: crypto.randomUUID(),
393
+ tokenHash,
394
+ clientId: input.client.clientId,
395
+ userId: input.client.userId,
396
+ scopes,
397
+ expiresAt: DateTime.fromJSDate(expiresAt)
398
+ });
399
+ return {
400
+ access_token: accessTokenRaw,
401
+ token_type: "Bearer",
402
+ expires_in: ttlSeconds,
403
+ scope: scopes.join(" ")
404
+ };
405
+ }
406
+ /**
407
+ * Resolve and validate scopes. Falls back to the client's
408
+ * non-OIDC scopes when none are requested. Rejects any
409
+ * user-centric OIDC scopes.
410
+ */
411
+ #resolveScopes(manager, input, clientService) {
412
+ const requestedScopes = input.scope ? input.scope.split(" ") : input.client.scopes.filter((scope) => !BUILTIN_SCOPES.has(scope) && !OIDC_SCOPES.has(scope));
413
+ const forbiddenRequested = requestedScopes.filter((scope) => BUILTIN_SCOPES.has(scope) || OIDC_SCOPES.has(scope));
414
+ if (forbiddenRequested.length > 0) throw new E_INVALID_SCOPE(`Scopes not allowed for client_credentials: ${forbiddenRequested.join(", ")}`);
415
+ const invalidScopes = manager.validateScopes(requestedScopes);
416
+ if (invalidScopes.length > 0) throw new E_INVALID_SCOPE(`Invalid scopes: ${invalidScopes.join(", ")}`);
417
+ clientService.validateClientScopes(requestedScopes, input.client.scopes);
418
+ return requestedScopes;
419
+ }
420
+ };
421
+ //#endregion
422
+ //#region src/controllers/token_controller.ts
423
+ /**
424
+ * Handles the OAuth 2.0 Token Endpoint (RFC 6749 §3.2).
425
+ *
426
+ * Authenticates the client, extracts grant-specific params,
427
+ * and dispatches to the appropriate action. Sets required
428
+ * cache headers on all token responses.
429
+ *
430
+ * @see https://datatracker.ietf.org/doc/html/rfc6749#section-3.2
431
+ */
432
+ var TokenController = class TokenController {
433
+ static validator = vine.create({ grant_type: vine.string() });
434
+ /**
435
+ * Validate the grant type, authenticate the client,
436
+ * and dispatch to the appropriate grant action.
437
+ */
438
+ async handle(ctx) {
439
+ const manager = await ctx.containerResolver.make(SesameManager);
440
+ const body = ctx.request.body();
441
+ const [error, validated] = await TokenController.validator.tryValidate(body);
442
+ if (error) throw new E_UNSUPPORTED_GRANT_TYPE("Missing required parameter: grant_type");
443
+ if (!manager.isGrantTypeEnabled(validated.grant_type)) throw new E_UNSUPPORTED_GRANT_TYPE(`Grant type "${validated.grant_type}" is not enabled`);
444
+ const client = await new ClientService().authenticateClient({
445
+ authorizationHeader: ctx.request.header("authorization"),
446
+ bodyClientId: body.client_id,
447
+ bodyClientSecret: body.client_secret
448
+ });
449
+ const result = await this.#dispatchGrant(validated.grant_type, manager, client, body);
450
+ ctx.response.header("Cache-Control", "no-store");
451
+ ctx.response.header("Pragma", "no-cache");
452
+ return result;
453
+ }
454
+ /**
455
+ * Map each grant type to its action, extracting the
456
+ * relevant params from the request body.
457
+ */
458
+ #dispatchGrant(grantType, manager, client, body) {
459
+ const handler = {
460
+ authorization_code: () => new ExchangeAuthorizationCodeAction().execute(manager, {
461
+ client,
462
+ code: body.code,
463
+ redirectUri: body.redirect_uri,
464
+ codeVerifier: body.code_verifier
465
+ }),
466
+ refresh_token: () => new ExchangeRefreshTokenAction().execute(manager, {
467
+ client,
468
+ refreshToken: body.refresh_token,
469
+ scope: body.scope
470
+ }),
471
+ client_credentials: () => new ExchangeClientCredentialsAction().execute(manager, {
472
+ client,
473
+ scope: body.scope
474
+ })
475
+ }[grantType];
476
+ if (!handler) throw new E_UNSUPPORTED_GRANT_TYPE(`Unsupported grant type: ${grantType}`);
477
+ return handler();
478
+ }
479
+ };
480
+ //#endregion
481
+ export { TokenController as default };
@@ -0,0 +1,59 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import string from "@adonisjs/core/helpers/string";
3
+ //#region src/services/token_service.ts
4
+ /**
5
+ * Handles opaque token generation for access tokens, refresh tokens,
6
+ * and authorization codes.
7
+ *
8
+ * All tokens are random opaque values. Only their SHA-256 hashes
9
+ * are stored in the database, so the raw tokens cannot be
10
+ * reconstructed from a database leak.
11
+ */
12
+ var TokenService = class {
13
+ #manager;
14
+ constructor(manager) {
15
+ this.#manager = manager;
16
+ }
17
+ /**
18
+ * Create an opaque access token. Returns the raw token
19
+ * (sent to the client), its SHA-256 hash (stored in DB),
20
+ * and the computed expiration date.
21
+ */
22
+ createAccessToken() {
23
+ const raw = this.generateOpaqueToken();
24
+ const ttlSeconds = string.seconds.parse(this.#manager.config.accessTokenTtl);
25
+ return {
26
+ raw,
27
+ hash: this.hashToken(raw),
28
+ expiresAt: new Date(Date.now() + ttlSeconds * 1e3)
29
+ };
30
+ }
31
+ /**
32
+ * Create an opaque refresh token. Returns the raw token
33
+ * (sent to the client) and its SHA-256 hash (stored in DB).
34
+ */
35
+ createRefreshToken() {
36
+ const raw = this.generateOpaqueToken();
37
+ return {
38
+ raw,
39
+ hash: this.hashToken(raw)
40
+ };
41
+ }
42
+ /**
43
+ * Generate a cryptographically random opaque token
44
+ * (32 bytes, base64url-encoded).
45
+ */
46
+ generateOpaqueToken() {
47
+ return randomBytes(32).toString("base64url");
48
+ }
49
+ /**
50
+ * SHA-256 hash a token value for secure storage.
51
+ * All tokens (codes, access tokens, refresh tokens) are stored
52
+ * as hashes so raw values cannot be reconstructed from a DB leak.
53
+ */
54
+ hashToken(token) {
55
+ return createHash("sha256").update(token).digest("base64url");
56
+ }
57
+ };
58
+ //#endregion
59
+ export { TokenService as t };
@@ -0,0 +1,40 @@
1
+ import "./chunk-DF48asd8.js";
2
+ import { t as SesameManager } from "./sesame_manager-DYUSZ0NC.js";
3
+ import "./oauth_client-BSanvSql.js";
4
+ import { c as E_INVALID_TOKEN, n as E_INSUFFICIENT_SCOPE, o as E_INVALID_REQUEST } from "./oauth_error-C7UhDb2q.js";
5
+ import { t as OAuthAccessToken } from "./oauth_access_token-Cz_5gNBx.js";
6
+ import { t as TokenService } from "./token_service-DwnfAR9F.js";
7
+ import { t as IdTokenService } from "./id_token_service-CpTzOUDe.js";
8
+ //#region src/controllers/userinfo_controller.ts
9
+ /**
10
+ * OpenID Connect UserInfo endpoint (OIDC Core §5.3).
11
+ *
12
+ * Returns claims about the authenticated user based on the
13
+ * access token's scopes. Supports both GET and POST.
14
+ *
15
+ * @see https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
16
+ */
17
+ var UserinfoController = class {
18
+ async handle(ctx) {
19
+ const manager = await ctx.containerResolver.make(SesameManager);
20
+ const authHeader = ctx.request.header("authorization");
21
+ const rawToken = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : ctx.request.body().access_token;
22
+ if (!rawToken) throw new E_INVALID_REQUEST("Missing Bearer token");
23
+ const hashed = new TokenService(manager).hashToken(rawToken);
24
+ const token = await OAuthAccessToken.query().where("tokenHash", hashed).first();
25
+ if (!token) throw new E_INVALID_TOKEN("Invalid access token");
26
+ if (token.revokedAt) throw new E_INVALID_TOKEN("Access token has been revoked");
27
+ if (token.expiresAt.toJSDate() < /* @__PURE__ */ new Date()) throw new E_INVALID_TOKEN("Access token has expired");
28
+ if (!token.scopes.includes("openid")) throw new E_INSUFFICIENT_SCOPE(["openid"], "Token does not have openid scope");
29
+ const user = await manager.findUserById(token.userId);
30
+ if (!user) throw new E_INVALID_TOKEN("Invalid access token");
31
+ const userClaims = await IdTokenService.resolveUserClaims(user, token.scopes);
32
+ ctx.response.header("Content-Type", "application/json");
33
+ return {
34
+ sub: token.userId,
35
+ ...userClaims
36
+ };
37
+ }
38
+ };
39
+ //#endregion
40
+ export { UserinfoController as default };
@@ -0,0 +1,2 @@
1
+ declare const _default: UserConfig;
2
+ export default _default;