@julr/sesame 0.5.1 → 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 (73) 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-6bRt9sZt.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-EbeMS5S9.js → main-DGBJhq3E.js} +34 -4
  20. package/build/{metadata_controller-DeaMRnUr.js → metadata_controller-BVsTo0Gp.js} +83 -6
  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-sIJ1rxdM.js → register_controller-gbq7p8a5.js} +46 -7
  26. package/build/{revoke_controller-D6isoQCi.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 +8 -1
  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 +112 -0
  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-YUfAy-R2.js +0 -138
  62. package/build/client_service-WTNMqWzY.js +0 -65
  63. package/build/consent_controller-Dprwd1ed.js +0 -85
  64. package/build/decorate-BKZEjPRg.js +0 -15
  65. package/build/oauth_client-BIoY5jBR.js +0 -24
  66. package/build/oauth_error-CnJ3L8tf.js +0 -94
  67. package/build/sesame_manager-Bu4MHqZV.js +0 -4
  68. package/build/sesame_manager-DwDZy5Vy.js +0 -167
  69. package/build/src/grants/authorization_code_grant.d.ts +0 -23
  70. package/build/src/grants/client_credentials_grant.d.ts +0 -23
  71. package/build/src/grants/refresh_token_grant.d.ts +0 -27
  72. package/build/token_controller-DzcrLMyS.js +0 -194
  73. package/build/token_service-fhoA4slP.js +0 -31
@@ -0,0 +1,693 @@
1
+ import { n as __decorate, r as json, t as OAuthClient } from "./oauth_client-BSanvSql.js";
2
+ import { o as E_INVALID_REQUEST, r as E_INVALID_CLIENT, s as E_INVALID_SCOPE } from "./oauth_error-C7UhDb2q.js";
3
+ import { t as OAuthAccessToken } from "./oauth_access_token-Cz_5gNBx.js";
4
+ import { DateTime } from "luxon";
5
+ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
6
+ import { BaseModel, column } from "@adonisjs/lucid/orm";
7
+ import { SignJWT, importJWK } from "jose";
8
+ //#region src/types.ts
9
+ /**
10
+ * Standard OAuth/OIDC scopes that are always valid regardless
11
+ * of server or client scope configuration.
12
+ *
13
+ * These scopes are:
14
+ * - Accepted during scope validation (client and server level)
15
+ * - Advertised in `scopes_supported` of all metadata endpoints
16
+ * (protected resource, OIDC discovery) so MCP clients know
17
+ * they can request them
18
+ *
19
+ * - `offline_access`: signals that the client needs a refresh token
20
+ * (OIDC Core §11). Note: Sesame issues refresh tokens by default
21
+ * when the `refresh_token` grant is enabled, regardless of whether
22
+ * the client requests this scope (per RFC 6749 §5.1). This scope
23
+ * is still advertised for clients that check metadata before
24
+ * building their authorization request.
25
+ *
26
+ * @see https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
27
+ * @see https://datatracker.ietf.org/doc/html/rfc6749#section-5.1
28
+ */
29
+ const BUILTIN_SCOPES = new Set(["offline_access"]);
30
+ /**
31
+ * OIDC-recognized scopes that are accepted by server-level validation
32
+ * without needing to be declared in the config `scopes` map.
33
+ *
34
+ * Unlike `BUILTIN_SCOPES`, these still require explicit client authorization
35
+ * via `ClientService.validateClientScopes()`.
36
+ */
37
+ const OIDC_SCOPES = new Set([
38
+ "openid",
39
+ "profile",
40
+ "email"
41
+ ]);
42
+ /**
43
+ * Protocol-managed claims that must never be overridden by `getOidcClaims()`.
44
+ */
45
+ const RESERVED_OIDC_CLAIMS = new Set([
46
+ "sub",
47
+ "iss",
48
+ "aud",
49
+ "exp",
50
+ "iat",
51
+ "nbf",
52
+ "jti",
53
+ "nonce",
54
+ "at_hash",
55
+ "auth_time",
56
+ "acr",
57
+ "azp",
58
+ "sid"
59
+ ]);
60
+ //#endregion
61
+ //#region src/services/client_service.ts
62
+ /**
63
+ * Handles OAuth client authentication and credential management.
64
+ *
65
+ * Supports the client authentication methods defined in RFC 6749 §2.3:
66
+ * - `client_secret_basic`: HTTP Basic auth with client_id:client_secret
67
+ * - `client_secret_post`: credentials in the request body
68
+ * - `none`: public clients (no secret)
69
+ *
70
+ * Client secrets are stored as SHA-256 hashes and compared using
71
+ * timing-safe equality to prevent timing attacks.
72
+ *
73
+ * @see https://datatracker.ietf.org/doc/html/rfc6749#section-2.3
74
+ */
75
+ var ClientService = class {
76
+ /**
77
+ * Parse an HTTP Basic Authorization header into client credentials.
78
+ * Follows RFC 6749 §2.3.1 — the client_id and client_secret are
79
+ * URL-decoded after base64 decoding.
80
+ *
81
+ * @see https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1
82
+ */
83
+ parseBasicAuth(header) {
84
+ if (!header.startsWith("Basic ")) return null;
85
+ try {
86
+ const decoded = Buffer.from(header.slice(6), "base64").toString("utf-8");
87
+ const colonIndex = decoded.indexOf(":");
88
+ if (colonIndex === -1) return null;
89
+ return {
90
+ clientId: decodeURIComponent(decoded.slice(0, colonIndex)),
91
+ clientSecret: decodeURIComponent(decoded.slice(colonIndex + 1))
92
+ };
93
+ } catch {
94
+ return null;
95
+ }
96
+ }
97
+ /**
98
+ * Extract client credentials from a request. Checks the
99
+ * Authorization header first (Basic auth), then falls back
100
+ * to POST body parameters (`client_id` / `client_secret`).
101
+ */
102
+ extractCredentials(options) {
103
+ const basic = options.authorizationHeader ? this.parseBasicAuth(options.authorizationHeader) : null;
104
+ if (basic && options.bodyClientId) throw new E_INVALID_REQUEST("Multiple client authentication methods are not allowed");
105
+ if (basic) return basic;
106
+ if (options.bodyClientId) return {
107
+ clientId: options.bodyClientId,
108
+ clientSecret: options.bodyClientSecret
109
+ };
110
+ return null;
111
+ }
112
+ /**
113
+ * Authenticate a client from request credentials.
114
+ * Extracts credentials, looks up the client in DB, and verifies the secret
115
+ * for confidential clients.
116
+ */
117
+ async authenticateClient(options) {
118
+ const credentials = this.extractCredentials(options);
119
+ if (!credentials) throw new E_INVALID_CLIENT("Client authentication failed");
120
+ const client = await OAuthClient.query().where("clientId", credentials.clientId).first();
121
+ if (!client || client.isDisabled) throw new E_INVALID_CLIENT("Client authentication failed");
122
+ if (!client.isPublic) {
123
+ if (!credentials.clientSecret || !this.verifySecret(credentials.clientSecret, client.clientSecret)) throw new E_INVALID_CLIENT("Client authentication failed");
124
+ }
125
+ return client;
126
+ }
127
+ /**
128
+ * Validate that requested scopes are within the client's
129
+ * allowed scopes. Throws `E_INVALID_SCOPE` if any scope
130
+ * is not permitted. An empty `clientScopes` array means the
131
+ * client has no scope permissions (RFC 6749 §2, §3.3).
132
+ *
133
+ * @see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3
134
+ */
135
+ validateClientScopes(requestedScopes, clientScopes) {
136
+ const nonBuiltinScopes = requestedScopes.filter((s) => !BUILTIN_SCOPES.has(s));
137
+ if (clientScopes.length === 0 && nonBuiltinScopes.length > 0) throw new E_INVALID_SCOPE(`Scope not allowed: ${nonBuiltinScopes.join(", ")}`);
138
+ const allowedSet = new Set(clientScopes);
139
+ const invalid = nonBuiltinScopes.filter((s) => !allowedSet.has(s));
140
+ if (invalid.length > 0) throw new E_INVALID_SCOPE(`Scope not allowed: ${invalid.join(", ")}`);
141
+ }
142
+ /**
143
+ * Hash a client secret for storage using SHA-256
144
+ * (base64url-encoded).
145
+ */
146
+ hashSecret(secret) {
147
+ return createHash("sha256").update(secret).digest("base64url");
148
+ }
149
+ /**
150
+ * Verify a client secret against its stored hash using
151
+ * timing-safe comparison to prevent timing attacks.
152
+ */
153
+ verifySecret(secret, storedHash) {
154
+ const hash = this.hashSecret(secret);
155
+ try {
156
+ return timingSafeEqual(Buffer.from(hash), Buffer.from(storedHash));
157
+ } catch {
158
+ return false;
159
+ }
160
+ }
161
+ /**
162
+ * Generate a random client ID (16 bytes, hex-encoded).
163
+ */
164
+ generateClientId() {
165
+ return randomBytes(16).toString("hex");
166
+ }
167
+ /**
168
+ * Generate a random client secret (32 bytes, base64url-encoded).
169
+ */
170
+ generateClientSecret() {
171
+ return randomBytes(32).toString("base64url");
172
+ }
173
+ };
174
+ //#endregion
175
+ //#region src/services/key_service.ts
176
+ /**
177
+ * Manages the RSA key pair used to sign ID tokens.
178
+ * Accepts a JWK from config, caches the imported private key
179
+ * and public JWK for JWKS export.
180
+ */
181
+ var KeyService = class KeyService {
182
+ #privateKey = null;
183
+ #publicJwk;
184
+ #kid;
185
+ #jwk;
186
+ constructor(jwk) {
187
+ this.#jwk = jwk;
188
+ this.#kid = jwk.kid ?? KeyService.computeKid(jwk);
189
+ this.#publicJwk = {
190
+ kty: jwk.kty,
191
+ n: jwk.n,
192
+ e: jwk.e,
193
+ kid: this.#kid,
194
+ use: "sig",
195
+ alg: "RS256"
196
+ };
197
+ }
198
+ /**
199
+ * Compute a `kid` from public key components (SHA-256, base64url).
200
+ * Same approach as node-oidc-provider.
201
+ */
202
+ static computeKid(jwk) {
203
+ const components = JSON.stringify({
204
+ e: jwk.e,
205
+ kty: jwk.kty,
206
+ n: jwk.n
207
+ });
208
+ return createHash("sha256").update(components).digest("base64url");
209
+ }
210
+ async #getPrivateKey() {
211
+ if (this.#privateKey) return this.#privateKey;
212
+ this.#privateKey = await importJWK(this.#jwk, "RS256");
213
+ return this.#privateKey;
214
+ }
215
+ async sign(payload) {
216
+ const key = await this.#getPrivateKey();
217
+ return new SignJWT(payload).setProtectedHeader({
218
+ alg: "RS256",
219
+ kid: this.#kid,
220
+ typ: "JWT"
221
+ }).sign(key);
222
+ }
223
+ getPublicJwks() {
224
+ return { keys: [this.#publicJwk] };
225
+ }
226
+ get kid() {
227
+ return this.#kid;
228
+ }
229
+ };
230
+ //#endregion
231
+ //#region src/routes.ts
232
+ /**
233
+ * Lazy-loaded controller imports for all OAuth 2.1 endpoints.
234
+ * Using lazy imports ensures controllers are only loaded when
235
+ * their routes are hit.
236
+ */
237
+ const controllers = {
238
+ token: () => import("./token_controller-DyI7oy-U.js"),
239
+ authorize: () => import("./authorize_controller-BiycO4be.js"),
240
+ consent: () => import("./consent_controller-Dsdhv6-f.js"),
241
+ introspect: () => import("./introspect_controller-DvOp9scr.js"),
242
+ revoke: () => import("./revoke_controller-z_ghrEB7.js"),
243
+ register: () => import("./register_controller-gbq7p8a5.js"),
244
+ metadata: () => import("./metadata_controller-BVsTo0Gp.js"),
245
+ clientInfo: () => import("./client_info_controller-AcOG8lWu.js"),
246
+ jwks: () => import("./jwks_controller-keo4kBZc.js"),
247
+ userinfo: () => import("./userinfo_controller-RLk8cN_o.js")
248
+ };
249
+ /**
250
+ * Register OAuth 2.1 endpoint routes on the given router.
251
+ *
252
+ * Paths are relative (no prefix) — the user wraps the call
253
+ * in a `router.group().prefix('/oauth')` to control the mount point.
254
+ *
255
+ * Endpoints registered:
256
+ * - `POST /token` — Token endpoint (RFC 6749 §3.2)
257
+ * - `GET /authorize` — Authorization endpoint (RFC 6749 §3.1)
258
+ * - `POST /consent` — User consent submission
259
+ * - `POST /introspect` — Token introspection (RFC 7662)
260
+ * - `POST /revoke` — Token revocation (RFC 7009)
261
+ * - `POST /register` — Dynamic client registration (RFC 7591)
262
+ * - `GET /client-info` — Public client information (RFC 6819 §4.4.1.4)
263
+ */
264
+ function registerOAuthRoutes(router) {
265
+ router.post("/token", [controllers.token]).as("sesame.token");
266
+ router.get("/authorize", [controllers.authorize]).as("sesame.authorize");
267
+ router.post("/consent", [controllers.consent]).as("sesame.consent");
268
+ router.get("/client-info", [controllers.clientInfo]).as("sesame.clientInfo");
269
+ router.post("/introspect", [controllers.introspect]).as("sesame.introspect");
270
+ router.post("/revoke", [controllers.revoke]).as("sesame.revoke");
271
+ router.post("/register", [controllers.register]).as("sesame.register");
272
+ router.get("/userinfo", [controllers.userinfo]).as("sesame.userinfo");
273
+ router.post("/userinfo", [controllers.userinfo]).as("sesame.userinfo.post");
274
+ }
275
+ /**
276
+ * Register well-known discovery routes at the root level.
277
+ *
278
+ * These must be registered outside any prefix group so they
279
+ * remain at `/.well-known/...`.
280
+ *
281
+ * Endpoints registered:
282
+ * - `GET /.well-known/oauth-authorization-server` — Server metadata (RFC 8414)
283
+ * - `GET /.well-known/openid-configuration` — OpenID Connect discovery
284
+ * - `GET /.well-known/oauth-protected-resource` — Protected resource metadata (RFC 9728)
285
+ */
286
+ function registerWellKnownRoutes(router, options) {
287
+ const jwksPath = options?.jwksPath ?? "/jwks";
288
+ router.get("/.well-known/oauth-authorization-server", [controllers.metadata, "authServer"]).as("sesame.metadata.authServer");
289
+ router.get("/.well-known/openid-configuration", [controllers.metadata, "oidc"]).as("sesame.metadata.oidc");
290
+ router.get("/.well-known/oauth-protected-resource", [controllers.metadata, "protectedResource"]).as("sesame.metadata.protectedResource");
291
+ router.get(jwksPath, [controllers.jwks]).as("sesame.jwks");
292
+ }
293
+ //#endregion
294
+ //#region src/models/oauth_refresh_token.ts
295
+ /**
296
+ * Database record for an OAuth 2.0 refresh token (RFC 6749 §6).
297
+ *
298
+ * The `token` column stores the SHA-256 hash of the raw refresh
299
+ * token value. Refresh token rotation is enforced: each use
300
+ * produces a new token and revokes the old one.
301
+ *
302
+ * Replay detection is implemented by checking `revokedAt` —
303
+ * if a revoked token is presented, all tokens for that
304
+ * client+user pair are deleted as a security measure.
305
+ *
306
+ * @see https://datatracker.ietf.org/doc/html/rfc6749#section-6
307
+ * @see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.14.2
308
+ */
309
+ var OAuthRefreshToken = class extends BaseModel {
310
+ static table = "oauth_refresh_tokens";
311
+ };
312
+ __decorate([column({ isPrimary: true })], OAuthRefreshToken.prototype, "id", void 0);
313
+ __decorate([column({ serializeAs: null })], OAuthRefreshToken.prototype, "token", void 0);
314
+ __decorate([column()], OAuthRefreshToken.prototype, "accessTokenId", void 0);
315
+ __decorate([column()], OAuthRefreshToken.prototype, "clientId", void 0);
316
+ __decorate([column()], OAuthRefreshToken.prototype, "userId", void 0);
317
+ __decorate([json()], OAuthRefreshToken.prototype, "scopes", void 0);
318
+ __decorate([column.dateTime()], OAuthRefreshToken.prototype, "expiresAt", void 0);
319
+ __decorate([column.dateTime()], OAuthRefreshToken.prototype, "revokedAt", void 0);
320
+ __decorate([column.dateTime({ autoCreate: true })], OAuthRefreshToken.prototype, "createdAt", void 0);
321
+ __decorate([column.dateTime({
322
+ autoCreate: true,
323
+ autoUpdate: true
324
+ })], OAuthRefreshToken.prototype, "updatedAt", void 0);
325
+ //#endregion
326
+ //#region src/models/oauth_authorization_code.ts
327
+ /**
328
+ * Database record for an OAuth 2.0 authorization code (RFC 6749 §4.1.2).
329
+ *
330
+ * Authorization codes are short-lived, single-use tokens issued
331
+ * during the authorization flow. The `code` column stores the
332
+ * SHA-256 hash of the raw code value sent to the client.
333
+ *
334
+ * When PKCE (RFC 7636) is used, the `codeChallenge` and
335
+ * `codeChallengeMethod` (always S256) are stored alongside the
336
+ * code for verification at the token endpoint.
337
+ *
338
+ * @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2
339
+ * @see https://datatracker.ietf.org/doc/html/rfc7636
340
+ */
341
+ var OAuthAuthorizationCode = class extends BaseModel {
342
+ static table = "oauth_authorization_codes";
343
+ };
344
+ __decorate([column({ isPrimary: true })], OAuthAuthorizationCode.prototype, "id", void 0);
345
+ __decorate([column()], OAuthAuthorizationCode.prototype, "code", void 0);
346
+ __decorate([column()], OAuthAuthorizationCode.prototype, "clientId", void 0);
347
+ __decorate([column()], OAuthAuthorizationCode.prototype, "userId", void 0);
348
+ __decorate([json()], OAuthAuthorizationCode.prototype, "scopes", void 0);
349
+ __decorate([column()], OAuthAuthorizationCode.prototype, "redirectUri", void 0);
350
+ __decorate([column()], OAuthAuthorizationCode.prototype, "codeChallenge", void 0);
351
+ __decorate([column()], OAuthAuthorizationCode.prototype, "codeChallengeMethod", void 0);
352
+ __decorate([column()], OAuthAuthorizationCode.prototype, "nonce", void 0);
353
+ __decorate([column.dateTime()], OAuthAuthorizationCode.prototype, "expiresAt", void 0);
354
+ __decorate([column.dateTime({ autoCreate: true })], OAuthAuthorizationCode.prototype, "createdAt", void 0);
355
+ __decorate([column.dateTime({
356
+ autoCreate: true,
357
+ autoUpdate: true
358
+ })], OAuthAuthorizationCode.prototype, "updatedAt", void 0);
359
+ //#endregion
360
+ //#region src/models/oauth_consent.ts
361
+ /**
362
+ * Tracks which scopes a user has approved for a given client.
363
+ *
364
+ * When a user grants consent during the authorization flow,
365
+ * the approved scopes are persisted here. On subsequent
366
+ * authorization requests, if the requested scopes are a subset
367
+ * of previously approved scopes, the user is not prompted again.
368
+ *
369
+ * New scopes are merged into the existing record when the user
370
+ * approves additional permissions.
371
+ */
372
+ var OAuthConsent = class extends BaseModel {
373
+ static table = "oauth_consents";
374
+ };
375
+ __decorate([column({ isPrimary: true })], OAuthConsent.prototype, "id", void 0);
376
+ __decorate([column()], OAuthConsent.prototype, "clientId", void 0);
377
+ __decorate([column()], OAuthConsent.prototype, "userId", void 0);
378
+ __decorate([json()], OAuthConsent.prototype, "scopes", void 0);
379
+ __decorate([column.dateTime({ autoCreate: true })], OAuthConsent.prototype, "createdAt", void 0);
380
+ __decorate([column.dateTime({
381
+ autoCreate: true,
382
+ autoUpdate: true
383
+ })], OAuthConsent.prototype, "updatedAt", void 0);
384
+ //#endregion
385
+ //#region src/models/oauth_pending_authorization_request.ts
386
+ /**
387
+ * Temporary server-side record for an in-flight OAuth authorization
388
+ * request, created when the user is redirected to the consent page
389
+ * and consumed (deleted) when the user approves or denies.
390
+ *
391
+ * Stored in the database instead of the HTTP session to avoid
392
+ * last-write-wins race conditions when concurrent SPA requests
393
+ * overwrite session data.
394
+ *
395
+ * The `token` column stores a SHA-256 hash of the raw auth_token
396
+ * sent to the consent page, consistent with how authorization
397
+ * codes are stored.
398
+ */
399
+ var OAuthPendingAuthorizationRequest = class extends BaseModel {
400
+ static table = "oauth_pending_authorization_requests";
401
+ };
402
+ __decorate([column({ isPrimary: true })], OAuthPendingAuthorizationRequest.prototype, "id", void 0);
403
+ __decorate([column()], OAuthPendingAuthorizationRequest.prototype, "token", void 0);
404
+ __decorate([column()], OAuthPendingAuthorizationRequest.prototype, "userId", void 0);
405
+ __decorate([column()], OAuthPendingAuthorizationRequest.prototype, "clientId", void 0);
406
+ __decorate([column()], OAuthPendingAuthorizationRequest.prototype, "redirectUri", void 0);
407
+ __decorate([json()], OAuthPendingAuthorizationRequest.prototype, "scopes", void 0);
408
+ __decorate([column()], OAuthPendingAuthorizationRequest.prototype, "state", void 0);
409
+ __decorate([column()], OAuthPendingAuthorizationRequest.prototype, "codeChallenge", void 0);
410
+ __decorate([column()], OAuthPendingAuthorizationRequest.prototype, "codeChallengeMethod", void 0);
411
+ __decorate([column()], OAuthPendingAuthorizationRequest.prototype, "nonce", void 0);
412
+ __decorate([column.dateTime()], OAuthPendingAuthorizationRequest.prototype, "expiresAt", void 0);
413
+ __decorate([column.dateTime({ autoCreate: true })], OAuthPendingAuthorizationRequest.prototype, "createdAt", void 0);
414
+ //#endregion
415
+ //#region src/sesame_manager.ts
416
+ /**
417
+ * Central manager for the Sésame OAuth 2.1 server.
418
+ *
419
+ * Holds the resolved configuration. Registered as a singleton
420
+ * in the AdonisJS IoC container by `SesameProvider`.
421
+ */
422
+ var SesameManager = class {
423
+ #config;
424
+ #router;
425
+ #keyService;
426
+ constructor(config, router) {
427
+ this.#config = config;
428
+ this.#router = router;
429
+ this.#keyService = config.jwk ? new KeyService(config.jwk) : null;
430
+ }
431
+ get config() {
432
+ return this.#config;
433
+ }
434
+ get keyService() {
435
+ if (!this.#keyService) throw new Error("OIDC requires a JWK. Set the `jwk` option in defineConfig().");
436
+ return this.#keyService;
437
+ }
438
+ get isOidcEnabled() {
439
+ return this.#keyService !== null && this.#config.oidcProvider !== void 0;
440
+ }
441
+ /**
442
+ * Load a user by ID using the configured user provider.
443
+ * Returns the original user model instance, or null if not found.
444
+ */
445
+ async findUserById(userId) {
446
+ if (!this.#config.oidcProvider) return null;
447
+ return (await this.#config.oidcProvider.findById(userId))?.getOriginal() ?? null;
448
+ }
449
+ /**
450
+ * Check if a scope is registered in the server configuration.
451
+ *
452
+ * @see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3
453
+ */
454
+ hasScope(scope) {
455
+ return scope in this.#config.scopes;
456
+ }
457
+ /**
458
+ * Check if the requested scope list uses any OIDC-specific scopes.
459
+ */
460
+ usesOidcScopes(scopes) {
461
+ return scopes.some((scope) => OIDC_SCOPES.has(scope));
462
+ }
463
+ /**
464
+ * Return the list of scopes that are not registered in the
465
+ * server configuration. When no scopes are configured, all
466
+ * requested scopes are considered unknown per RFC 6749 §3.3
467
+ * (`invalid_scope` — "The requested scope is invalid, unknown,
468
+ * or malformed").
469
+ *
470
+ * @see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3
471
+ * @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1
472
+ */
473
+ validateScopes(scopes) {
474
+ const invalidScopes = /* @__PURE__ */ new Set();
475
+ if (!scopes.includes("openid")) scopes.filter((scope) => scope !== "openid" && OIDC_SCOPES.has(scope)).forEach((scope) => invalidScopes.add(scope));
476
+ if (Object.keys(this.#config.scopes).length === 0) {
477
+ scopes.filter((scope) => !BUILTIN_SCOPES.has(scope) && !OIDC_SCOPES.has(scope)).forEach((scope) => invalidScopes.add(scope));
478
+ return [...invalidScopes];
479
+ }
480
+ scopes.filter((scope) => !BUILTIN_SCOPES.has(scope) && !OIDC_SCOPES.has(scope) && !this.hasScope(scope)).forEach((scope) => invalidScopes.add(scope));
481
+ return [...invalidScopes];
482
+ }
483
+ /**
484
+ * Check if a grant type is enabled in the server configuration.
485
+ */
486
+ isGrantTypeEnabled(grantType) {
487
+ return this.#config.grantTypes.includes(grantType);
488
+ }
489
+ /**
490
+ * Revoke all OAuth artifacts for a given user.
491
+ *
492
+ * Call this when a user is deleted or deactivated to ensure
493
+ * none of their tokens remain usable. Revokes access tokens
494
+ * and refresh tokens, and deletes authorization codes and
495
+ * consent records.
496
+ */
497
+ async revokeAllForUser(userId) {
498
+ const now = DateTime.now();
499
+ await OAuthAccessToken.query().where("userId", userId).whereNull("revokedAt").update({ revokedAt: now.toSQL() });
500
+ await OAuthRefreshToken.query().where("userId", userId).whereNull("revokedAt").update({ revokedAt: now.toSQL() });
501
+ await OAuthAuthorizationCode.query().where("userId", userId).delete();
502
+ await OAuthPendingAuthorizationRequest.query().where("userId", userId).delete();
503
+ await OAuthConsent.query().where("userId", userId).delete();
504
+ }
505
+ /**
506
+ * Purge revoked and/or expired tokens and authorization codes.
507
+ *
508
+ * Returns the total number of deleted records. Expired tokens are
509
+ * retained for `retentionHours` (default 168 = 7 days) to allow
510
+ * for debugging and audit trails.
511
+ */
512
+ async purgeTokens(options) {
513
+ const revokedOnly = options?.revokedOnly ?? false;
514
+ const expiredOnly = options?.expiredOnly ?? false;
515
+ const retentionHours = options?.retentionHours ?? 168;
516
+ const purgeRevoked = revokedOnly || !expiredOnly;
517
+ const purgeExpired = expiredOnly || !revokedOnly;
518
+ const cutoff = DateTime.now().minus({ hours: retentionHours });
519
+ let accessTokens = 0;
520
+ let refreshTokens = 0;
521
+ let authorizationCodes = 0;
522
+ let pendingRequests = 0;
523
+ if (purgeRevoked) {
524
+ accessTokens += await this.#deleteCount(OAuthAccessToken.query().whereNotNull("revokedAt").delete());
525
+ refreshTokens += await this.#deleteCount(OAuthRefreshToken.query().whereNotNull("revokedAt").delete());
526
+ }
527
+ if (purgeExpired) {
528
+ accessTokens += await this.#deleteCount(OAuthAccessToken.query().where("expiresAt", "<", cutoff.toSQL()).whereNull("revokedAt").delete());
529
+ refreshTokens += await this.#deleteCount(OAuthRefreshToken.query().where("expiresAt", "<", cutoff.toSQL()).whereNull("revokedAt").delete());
530
+ authorizationCodes += await this.#deleteCount(OAuthAuthorizationCode.query().where("expiresAt", "<", cutoff.toSQL()).delete());
531
+ }
532
+ pendingRequests += await this.#deleteCount(OAuthPendingAuthorizationRequest.query().where("expiresAt", "<", DateTime.now().toSQL()).delete());
533
+ return {
534
+ accessTokens,
535
+ refreshTokens,
536
+ authorizationCodes,
537
+ pendingRequests
538
+ };
539
+ }
540
+ /**
541
+ * Create a new OAuth client programmatically.
542
+ * Returns the client and the raw secret (only available at creation time).
543
+ */
544
+ async createClient(options) {
545
+ const clientService = new ClientService();
546
+ const isPublic = options.isPublic ?? false;
547
+ const grantTypes = options.grantTypes ?? ["authorization_code"];
548
+ const scopes = options.scopes ?? this.#config.defaultScopes;
549
+ const clientId = clientService.generateClientId();
550
+ const clientSecret = isPublic ? null : clientService.generateClientSecret();
551
+ const hashedSecret = clientSecret ? clientService.hashSecret(clientSecret) : null;
552
+ return {
553
+ client: await OAuthClient.create({
554
+ id: crypto.randomUUID(),
555
+ clientId,
556
+ clientSecret: hashedSecret,
557
+ name: options.name,
558
+ redirectUris: options.redirectUris,
559
+ scopes,
560
+ grantTypes,
561
+ isPublic,
562
+ isDisabled: false,
563
+ requirePkce: options.requirePkce ?? true,
564
+ type: isPublic ? "public" : "confidential",
565
+ metadata: options.metadata ?? null,
566
+ userId: options.userId ?? null
567
+ }),
568
+ clientSecret
569
+ };
570
+ }
571
+ /**
572
+ * Find a client by its public client_id.
573
+ */
574
+ async findClient(clientId) {
575
+ return OAuthClient.query().where("clientId", clientId).first();
576
+ }
577
+ /**
578
+ * List all clients, optionally filtered by userId.
579
+ */
580
+ async listClients(options) {
581
+ const query = OAuthClient.query().orderBy("createdAt", "desc");
582
+ if (options?.userId) query.where("userId", options.userId);
583
+ return query;
584
+ }
585
+ /**
586
+ * Update an existing client by its public client_id.
587
+ * Returns the updated client, or null if not found.
588
+ */
589
+ async updateClient(clientId, options) {
590
+ const client = await OAuthClient.query().where("clientId", clientId).first();
591
+ if (!client) return null;
592
+ if (options.name !== void 0) client.name = options.name;
593
+ if (options.redirectUris !== void 0) client.redirectUris = options.redirectUris;
594
+ if (options.scopes !== void 0) client.scopes = options.scopes;
595
+ if (options.grantTypes !== void 0) client.grantTypes = options.grantTypes;
596
+ if (options.isDisabled !== void 0) client.isDisabled = options.isDisabled;
597
+ if (options.requirePkce !== void 0) client.requirePkce = options.requirePkce;
598
+ if (options.metadata !== void 0) client.metadata = options.metadata;
599
+ await client.save();
600
+ return client;
601
+ }
602
+ /**
603
+ * Delete a client and all its associated tokens, codes, and consents.
604
+ * Returns true if the client was found and deleted.
605
+ */
606
+ async deleteClient(clientId) {
607
+ const client = await OAuthClient.query().where("clientId", clientId).first();
608
+ if (!client) return false;
609
+ await OAuthClient.transaction(async (trx) => {
610
+ await Promise.all([
611
+ OAuthRefreshToken.query({ client: trx }).where("clientId", clientId).delete(),
612
+ OAuthAccessToken.query({ client: trx }).where("clientId", clientId).delete(),
613
+ OAuthAuthorizationCode.query({ client: trx }).where("clientId", clientId).delete(),
614
+ OAuthPendingAuthorizationRequest.query({ client: trx }).where("clientId", clientId).delete(),
615
+ OAuthConsent.query({ client: trx }).where("clientId", clientId).delete()
616
+ ]);
617
+ await client.useTransaction(trx).delete();
618
+ });
619
+ return true;
620
+ }
621
+ /**
622
+ * Rotate the secret of a confidential client.
623
+ * Returns the new raw secret, or null if the client is public or not found.
624
+ */
625
+ async rotateClientSecret(clientId) {
626
+ const client = await OAuthClient.query().where("clientId", clientId).first();
627
+ if (!client || client.isPublic) return null;
628
+ const clientService = new ClientService();
629
+ const newSecret = clientService.generateClientSecret();
630
+ client.clientSecret = clientService.hashSecret(newSecret);
631
+ await client.save();
632
+ return newSecret;
633
+ }
634
+ /**
635
+ * Register OAuth 2.1 endpoint routes (token, authorize, consent, etc.).
636
+ *
637
+ * Paths are relative — wrap the call in a `router.group().prefix()`
638
+ * to control the mount point.
639
+ *
640
+ * Do not apply session-auth middleware to the entire OAuth group.
641
+ * Endpoints like `/token`, `/introspect`, `/revoke`, and `/register`
642
+ * must stay callable without a browser session.
643
+ *
644
+ * @example
645
+ * ```ts
646
+ * router.group(() => {
647
+ * sesame.registerRoutes()
648
+ * }).prefix('/oauth')
649
+ * ```
650
+ */
651
+ registerRoutes() {
652
+ registerOAuthRoutes(this.#router);
653
+ }
654
+ /**
655
+ * Register discovery routes at the root level.
656
+ *
657
+ * Must be called outside any prefix group so endpoints
658
+ * remain at `/.well-known/...`.
659
+ */
660
+ registerDiscoveryRoutes(options) {
661
+ registerWellKnownRoutes(this.#router, options);
662
+ }
663
+ /**
664
+ * @deprecated Use `registerDiscoveryRoutes()` instead.
665
+ */
666
+ registerWellKnownRoutes(options) {
667
+ this.registerDiscoveryRoutes(options);
668
+ }
669
+ /**
670
+ * Register a `/.well-known/oauth-protected-resource` endpoint
671
+ * for a specific resource path (RFC 9728). Useful for MCP
672
+ * servers that need per-resource discovery.
673
+ *
674
+ * @see https://datatracker.ietf.org/doc/html/rfc9728
675
+ */
676
+ registerProtectedResource(options) {
677
+ const wellKnownPath = `/.well-known/oauth-protected-resource${options.resource}`;
678
+ this.#router.get(wellKnownPath, async (ctx) => {
679
+ ctx.response.header("Cache-Control", "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400");
680
+ return {
681
+ resource: `${this.#config.issuer}${options.resource}`,
682
+ authorization_servers: [this.#config.issuer],
683
+ scopes_supported: [...options.scopes ?? Object.keys(this.#config.scopes), ...BUILTIN_SCOPES],
684
+ bearer_methods_supported: ["header"]
685
+ };
686
+ });
687
+ }
688
+ #deleteCount(result) {
689
+ return result.then((r) => Array.isArray(r) ? Number(r[0] ?? 0) : Number(r));
690
+ }
691
+ };
692
+ //#endregion
693
+ export { OAuthRefreshToken as a, OIDC_SCOPES as c, OAuthAuthorizationCode as i, RESERVED_OIDC_CLAIMS as l, OAuthPendingAuthorizationRequest as n, ClientService as o, OAuthConsent as r, BUILTIN_SCOPES as s, SesameManager as t };