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