@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,251 @@
1
+ import "./chunk-DF48asd8.js";
2
+ import { n as OAuthPendingAuthorizationRequest, o as ClientService, r as OAuthConsent, t as SesameManager } from "./sesame_manager-DYUSZ0NC.js";
3
+ import { t as OAuthClient } from "./oauth_client-BSanvSql.js";
4
+ import { d as E_UNSUPPORTED_RESPONSE_TYPE, o as E_INVALID_REQUEST, r as E_INVALID_CLIENT } from "./oauth_error-C7UhDb2q.js";
5
+ import "./oauth_access_token-Cz_5gNBx.js";
6
+ import { t as TokenService } from "./token_service-DwnfAR9F.js";
7
+ import { t as IssueAuthorizationCodeAction } from "./issue_authorization_code-B9ERu1uO.js";
8
+ import { DateTime } from "luxon";
9
+ import string from "@adonisjs/core/helpers/string";
10
+ import vine from "@vinejs/vine";
11
+ //#region src/actions/authorize.ts
12
+ /**
13
+ * Handles the OAuth 2.0 authorization request business logic.
14
+ *
15
+ * Validates the client, scopes, and PKCE parameters, checks
16
+ * existing consent, and either issues an authorization code
17
+ * or signals that login/consent is required.
18
+ *
19
+ * Errors before redirect_uri validation are thrown as exceptions.
20
+ * Errors after are returned as `redirect_error` results so the
21
+ * controller can redirect back to the client per spec.
22
+ */
23
+ var AuthorizeAction = class {
24
+ /**
25
+ * Process an authorization request. Returns a discriminated
26
+ * union that the controller interprets as the appropriate
27
+ * HTTP redirect.
28
+ */
29
+ async execute(manager, input) {
30
+ if (input.responseType !== "code") throw new E_UNSUPPORTED_RESPONSE_TYPE("Only \"code\" is supported");
31
+ const client = await OAuthClient.query().where("clientId", input.clientId).first();
32
+ if (!client) throw new E_INVALID_CLIENT("Client not found");
33
+ if (client.isDisabled) throw new E_INVALID_CLIENT("Client is disabled");
34
+ if (!client.redirectUris.includes(input.redirectUri)) throw new E_INVALID_REQUEST("Invalid redirect_uri");
35
+ if (!client.grantTypes.includes("authorization_code")) return {
36
+ type: "redirect_error",
37
+ error: "unauthorized_client",
38
+ description: "Client is not allowed to use the authorization_code grant"
39
+ };
40
+ const requestedScopes = input.scope ? input.scope.split(" ") : manager.config.defaultScopes;
41
+ const scopeError = this.#validateScopes(manager, requestedScopes, client);
42
+ if (scopeError) return scopeError;
43
+ const pkceError = this.#validatePkce(input);
44
+ if (pkceError) return pkceError;
45
+ if (!input.userId) return { type: "login_required" };
46
+ return this.#resolveConsent(manager, {
47
+ ...input,
48
+ userId: input.userId
49
+ }, client, requestedScopes);
50
+ }
51
+ /**
52
+ * Validate requested scopes against server config, client
53
+ * permissions, and OIDC availability.
54
+ */
55
+ #validateScopes(manager, scopes, client) {
56
+ const invalidScopes = manager.validateScopes(scopes);
57
+ if (invalidScopes.length > 0) return {
58
+ type: "redirect_error",
59
+ error: "invalid_scope",
60
+ description: `Invalid scopes: ${invalidScopes.join(", ")}`
61
+ };
62
+ const clientService = new ClientService();
63
+ try {
64
+ clientService.validateClientScopes(scopes, client.scopes);
65
+ } catch (err) {
66
+ return {
67
+ type: "redirect_error",
68
+ error: "invalid_scope",
69
+ description: err.message
70
+ };
71
+ }
72
+ if (manager.usesOidcScopes(scopes) && !manager.isOidcEnabled) return {
73
+ type: "redirect_error",
74
+ error: "invalid_scope",
75
+ description: "OIDC scopes require OIDC to be configured (set jwk and oidcProvider in config)"
76
+ };
77
+ return null;
78
+ }
79
+ /**
80
+ * Ensure PKCE code_challenge is present and uses S256
81
+ * (mandatory per OAuth 2.1).
82
+ */
83
+ #validatePkce(input) {
84
+ if (!input.codeChallenge) return {
85
+ type: "redirect_error",
86
+ error: "invalid_request",
87
+ description: "PKCE code_challenge is required"
88
+ };
89
+ if (input.codeChallengeMethod !== "S256") return {
90
+ type: "redirect_error",
91
+ error: "invalid_request",
92
+ description: "Only S256 code_challenge_method is supported"
93
+ };
94
+ return null;
95
+ }
96
+ /**
97
+ * Check if the user has already consented to all requested
98
+ * scopes. If so, issue the code directly. Otherwise, create
99
+ * a pending authorization request for the consent page.
100
+ */
101
+ async #resolveConsent(manager, input, client, scopes) {
102
+ const existingConsent = await OAuthConsent.query().where("clientId", client.clientId).where("userId", input.userId).first();
103
+ if (existingConsent) {
104
+ const consentedSet = new Set(existingConsent.scopes);
105
+ if (scopes.every((s) => consentedSet.has(s))) return {
106
+ type: "authorized",
107
+ code: await new IssueAuthorizationCodeAction().execute(manager, {
108
+ client,
109
+ userId: input.userId,
110
+ scopes,
111
+ redirectUri: input.redirectUri,
112
+ codeChallenge: input.codeChallenge,
113
+ codeChallengeMethod: input.codeChallengeMethod,
114
+ nonce: input.nonce
115
+ })
116
+ };
117
+ }
118
+ return {
119
+ type: "consent_required",
120
+ authToken: await this.#createPendingRequest(manager, input, client.clientId, scopes),
121
+ scopes
122
+ };
123
+ }
124
+ /**
125
+ * Store the authorization request in the database so it can
126
+ * be consumed atomically by the consent controller.
127
+ */
128
+ async #createPendingRequest(manager, input, clientId, scopes) {
129
+ const tokenService = new TokenService(manager);
130
+ const rawToken = tokenService.generateOpaqueToken();
131
+ const ttl = string.seconds.parse(manager.config.authorizationRequestTtl);
132
+ await OAuthPendingAuthorizationRequest.create({
133
+ id: crypto.randomUUID(),
134
+ token: tokenService.hashToken(rawToken),
135
+ userId: input.userId,
136
+ clientId,
137
+ redirectUri: input.redirectUri,
138
+ scopes,
139
+ state: input.state ?? null,
140
+ codeChallenge: input.codeChallenge ?? null,
141
+ codeChallengeMethod: input.codeChallengeMethod ?? null,
142
+ nonce: input.nonce ?? null,
143
+ expiresAt: DateTime.now().plus({ seconds: ttl })
144
+ });
145
+ return rawToken;
146
+ }
147
+ };
148
+ //#endregion
149
+ //#region src/controllers/authorize_controller.ts
150
+ /**
151
+ * Handles the OAuth 2.0 Authorization Endpoint (RFC 6749 §3.1).
152
+ *
153
+ * Validates HTTP parameters, delegates business logic to
154
+ * AuthorizeAction, and interprets the result as redirects.
155
+ *
156
+ * @see https://datatracker.ietf.org/doc/html/rfc6749#section-3.1
157
+ */
158
+ var AuthorizeController = class AuthorizeController {
159
+ static validator = vine.create({
160
+ client_id: vine.string(),
161
+ response_type: vine.string(),
162
+ redirect_uri: vine.string(),
163
+ scope: vine.string().optional(),
164
+ state: vine.string().optional(),
165
+ code_challenge: vine.string().optional(),
166
+ code_challenge_method: vine.string().optional(),
167
+ nonce: vine.string().optional()
168
+ });
169
+ /**
170
+ * Validate the authorization request query params, run the
171
+ * authorize action, and redirect based on the result.
172
+ */
173
+ async handle(ctx) {
174
+ const manager = await ctx.containerResolver.make(SesameManager);
175
+ const [error, query] = await AuthorizeController.validator.tryValidate(ctx.request.qs());
176
+ if (error) throw new E_INVALID_REQUEST("Invalid authorization request parameters");
177
+ await ctx.auth.check();
178
+ const user = ctx.auth.user;
179
+ const result = await new AuthorizeAction().execute(manager, {
180
+ clientId: query.client_id,
181
+ responseType: query.response_type,
182
+ redirectUri: query.redirect_uri,
183
+ scope: query.scope,
184
+ state: query.state,
185
+ codeChallenge: query.code_challenge,
186
+ codeChallengeMethod: query.code_challenge_method,
187
+ nonce: query.nonce,
188
+ userId: user ? String(user.id) : void 0
189
+ });
190
+ if (result.type === "redirect_error") return this.#redirectToClient(ctx, {
191
+ issuer: manager.config.issuer,
192
+ redirectUri: query.redirect_uri,
193
+ state: query.state,
194
+ params: {
195
+ error: result.error,
196
+ error_description: result.description
197
+ }
198
+ });
199
+ if (result.type === "login_required") {
200
+ const params = this.#buildDisplayParams(ctx);
201
+ const url = this.#resolvePageUrl(manager.config.loginPage, ctx, params);
202
+ return ctx.response.redirect().toPath(url);
203
+ }
204
+ if (result.type === "consent_required") {
205
+ const params = this.#buildDisplayParams(ctx);
206
+ params.set("auth_token", result.authToken);
207
+ params.set("scope", result.scopes.join(" "));
208
+ const url = this.#resolvePageUrl(manager.config.consentPage, ctx, params);
209
+ return ctx.response.redirect().toPath(url);
210
+ }
211
+ return this.#redirectToClient(ctx, {
212
+ issuer: manager.config.issuer,
213
+ redirectUri: query.redirect_uri,
214
+ state: query.state,
215
+ params: { code: result.code }
216
+ });
217
+ }
218
+ /**
219
+ * Build a redirect URL with OAuth params, state, and issuer,
220
+ * then redirect the user agent to the client's redirect_uri.
221
+ */
222
+ #redirectToClient(ctx, options) {
223
+ const url = new URL(options.redirectUri);
224
+ for (const [key, value] of Object.entries(options.params)) url.searchParams.set(key, value);
225
+ if (options.state) url.searchParams.set("state", options.state);
226
+ url.searchParams.set("iss", options.issuer);
227
+ return ctx.response.redirect().toPath(url.toString());
228
+ }
229
+ /**
230
+ * Forward original authorize query params so the
231
+ * login/consent page can display them.
232
+ */
233
+ #buildDisplayParams(ctx) {
234
+ const params = new URLSearchParams();
235
+ for (const [key, value] of Object.entries(ctx.request.qs())) {
236
+ if (key === "auth_token" || value == null) continue;
237
+ params.set(key, String(value));
238
+ }
239
+ return params;
240
+ }
241
+ /**
242
+ * Resolve a login/consent page URL from either a static
243
+ * string path or a dynamic function.
244
+ */
245
+ #resolvePageUrl(page, ctx, params) {
246
+ if (typeof page === "function") return page(ctx, params);
247
+ return `${page}?${params.toString()}`;
248
+ }
249
+ };
250
+ //#endregion
251
+ export { AuthorizeController as default };
@@ -0,0 +1,9 @@
1
+ import "node:module";
2
+ Object.create;
3
+ Object.defineProperty;
4
+ Object.getOwnPropertyDescriptor;
5
+ Object.getOwnPropertyNames;
6
+ Object.getPrototypeOf;
7
+ Object.prototype.hasOwnProperty;
8
+ //#endregion
9
+ export {};
@@ -1,7 +1,14 @@
1
- import "./decorate-BKZEjPRg.js";
2
- import { o as E_INVALID_REQUEST, r as E_INVALID_CLIENT } from "./oauth_error-CnJ3L8tf.js";
3
- import { t as OAuthClient } from "./oauth_client-BIoY5jBR.js";
1
+ import "./chunk-DF48asd8.js";
2
+ import { t as OAuthClient } from "./oauth_client-BSanvSql.js";
3
+ import { o as E_INVALID_REQUEST, r as E_INVALID_CLIENT } from "./oauth_error-C7UhDb2q.js";
4
4
  import vine from "@vinejs/vine";
5
+ //#region src/controllers/client_info_controller.ts
6
+ /**
7
+ * Returns public information about an OAuth client.
8
+ * Used by the consent page to display the client's name
9
+ * from server-side data rather than query parameters
10
+ * (RFC 6819 §4.4.1.4 — prevent client identity spoofing).
11
+ */
5
12
  var ClientInfoController = class ClientInfoController {
6
13
  static validator = vine.create({ client_id: vine.string() });
7
14
  async handle(ctx) {
@@ -15,4 +22,5 @@ var ClientInfoController = class ClientInfoController {
15
22
  };
16
23
  }
17
24
  };
25
+ //#endregion
18
26
  export { ClientInfoController as default };
@@ -0,0 +1,20 @@
1
+ import { BaseCommand } from '@adonisjs/core/ace';
2
+ import type { CommandOptions } from '@adonisjs/core/types/ace';
3
+ /**
4
+ * Interactively create a new OAuth client.
5
+ *
6
+ * Prompts for name, redirect URIs, client type, and scopes,
7
+ * then outputs the generated client_id and client_secret.
8
+ */
9
+ export default class SesameClient extends BaseCommand {
10
+ static commandName: string;
11
+ static description: string;
12
+ static options: CommandOptions;
13
+ public: boolean;
14
+ name: string;
15
+ redirectUris: string[];
16
+ scopes: string[];
17
+ grantTypes: string[];
18
+ userId: string;
19
+ run(): Promise<void>;
20
+ }
@@ -0,0 +1,12 @@
1
+ import { BaseCommand } from '@adonisjs/core/ace';
2
+ /**
3
+ * Generate an RSA JWK key pair for signing OIDC ID tokens.
4
+ */
5
+ export default class SesameKey extends BaseCommand {
6
+ #private;
7
+ static commandName: string;
8
+ static description: string;
9
+ raw: boolean;
10
+ writeEnv: boolean;
11
+ run(): Promise<boolean | void>;
12
+ }
@@ -7,8 +7,6 @@ import type { CommandOptions } from '@adonisjs/core/types/ace';
7
7
  * or `--expired` to target only one category. Expired tokens are
8
8
  * retained for a configurable period (default 168h / 7 days) to
9
9
  * allow for debugging and audit trails.
10
- *
11
- * @see https://datatracker.ietf.org/doc/html/rfc6749
12
10
  */
13
11
  export default class SesamePurge extends BaseCommand {
14
12
  static commandName: string;
@@ -1,7 +1,18 @@
1
- import { t as SesameManager } from "../sesame_manager-C-eEFFHM.js";
2
- import { t as __decorate } from "../decorate-BKZEjPRg.js";
3
- import "../oauth_access_token-bsoM5KeU.js";
1
+ import "../chunk-DF48asd8.js";
2
+ import { t as SesameManager } from "../sesame_manager-DYUSZ0NC.js";
3
+ import { n as __decorate } from "../oauth_client-BSanvSql.js";
4
+ import "../oauth_error-C7UhDb2q.js";
5
+ import "../oauth_access_token-Cz_5gNBx.js";
4
6
  import { BaseCommand, flags } from "@adonisjs/core/ace";
7
+ //#region commands/sesame_purge.ts
8
+ /**
9
+ * Purge revoked and/or expired OAuth tokens and authorization codes.
10
+ *
11
+ * By default, purges both revoked and expired records. Use `--revoked`
12
+ * or `--expired` to target only one category. Expired tokens are
13
+ * retained for a configurable period (default 168h / 7 days) to
14
+ * allow for debugging and audit trails.
15
+ */
5
16
  var SesamePurge = class extends BaseCommand {
6
17
  static commandName = "sesame:purge";
7
18
  static description = "Purge revoked and/or expired tokens and authorization codes";
@@ -25,4 +36,5 @@ __decorate([flags.number({
25
36
  description: "Number of hours to retain expired tokens (default: 168 = 7 days)",
26
37
  default: 168
27
38
  })], SesamePurge.prototype, "hours", void 0);
39
+ //#endregion
28
40
  export { SesamePurge as default };
@@ -0,0 +1,27 @@
1
+ import { join } from "node:path";
2
+ //#region configure.ts
3
+ async function configure(command) {
4
+ const codemods = await command.createCodemods();
5
+ const stubsRoot = join(import.meta.dirname, "stubs");
6
+ await codemods.makeUsingStub(stubsRoot, "config/sesame.stub", {});
7
+ for (const stub of [
8
+ "migrations/create_oauth_clients_table.stub",
9
+ "migrations/create_oauth_authorization_codes_table.stub",
10
+ "migrations/create_oauth_access_tokens_table.stub",
11
+ "migrations/create_oauth_refresh_tokens_table.stub",
12
+ "migrations/create_oauth_consents_table.stub",
13
+ "migrations/create_oauth_pending_authorization_requests_table.stub"
14
+ ]) await codemods.makeUsingStub(stubsRoot, stub, {});
15
+ await codemods.updateRcFile((rcFile) => {
16
+ rcFile.addProvider("@julr/sesame/sesame_provider").addCommand("@julr/sesame/commands");
17
+ });
18
+ await codemods.registerMiddleware("named", [{
19
+ name: "scopes",
20
+ path: "@julr/sesame/scope_middleware"
21
+ }, {
22
+ name: "anyScope",
23
+ path: "@julr/sesame/any_scope_middleware"
24
+ }]);
25
+ }
26
+ //#endregion
27
+ export { configure as t };
@@ -1,25 +1,3 @@
1
- import { join } from "node:path";
2
- async function configure(command) {
3
- const codemods = await command.createCodemods();
4
- const stubsRoot = join(import.meta.url, "./stubs");
5
- await codemods.makeUsingStub(stubsRoot, "config/sesame.stub", {});
6
- for (const stub of [
7
- "migrations/create_oauth_clients_table.stub",
8
- "migrations/create_oauth_authorization_codes_table.stub",
9
- "migrations/create_oauth_access_tokens_table.stub",
10
- "migrations/create_oauth_refresh_tokens_table.stub",
11
- "migrations/create_oauth_consents_table.stub",
12
- "migrations/create_oauth_pending_authorization_requests_table.stub"
13
- ]) await codemods.makeUsingStub(stubsRoot, stub, {});
14
- await codemods.updateRcFile((rcFile) => {
15
- rcFile.addProvider("@julr/sesame/sesame_provider").addCommand("@julr/sesame/commands");
16
- });
17
- await codemods.registerMiddleware("named", [{
18
- name: "scopes",
19
- path: "@julr/sesame/scope_middleware"
20
- }, {
21
- name: "anyScope",
22
- path: "@julr/sesame/any_scope_middleware"
23
- }]);
24
- }
1
+ import "./chunk-DF48asd8.js";
2
+ import { t as configure } from "./configure-DkDkIlt8.js";
25
3
  export { configure };
@@ -0,0 +1,108 @@
1
+ import "./chunk-DF48asd8.js";
2
+ import { n as OAuthPendingAuthorizationRequest, r as OAuthConsent, t as SesameManager } from "./sesame_manager-DYUSZ0NC.js";
3
+ import { t as OAuthClient } from "./oauth_client-BSanvSql.js";
4
+ import { a as E_INVALID_GRANT, o as E_INVALID_REQUEST, r as E_INVALID_CLIENT } from "./oauth_error-C7UhDb2q.js";
5
+ import "./oauth_access_token-Cz_5gNBx.js";
6
+ import { t as TokenService } from "./token_service-DwnfAR9F.js";
7
+ import { t as IssueAuthorizationCodeAction } from "./issue_authorization_code-B9ERu1uO.js";
8
+ import { DateTime } from "luxon";
9
+ import vine from "@vinejs/vine";
10
+ //#region src/controllers/consent_controller.ts
11
+ /**
12
+ * Handles user consent submission for the OAuth authorization flow.
13
+ *
14
+ * When a user approves access, this controller stores or updates
15
+ * the consent record and issues an authorization code via redirect.
16
+ * When the user denies access, it redirects back to the client
17
+ * with an `access_denied` error.
18
+ *
19
+ * Consent records are persisted so that returning users are not
20
+ * prompted again for previously approved scopes.
21
+ *
22
+ * @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
23
+ */
24
+ var ConsentController = class ConsentController {
25
+ static validator = vine.create({ auth_token: vine.string() });
26
+ /**
27
+ * Validate the consent submission, consume the pending
28
+ * request, and redirect back to the client with either
29
+ * an authorization code or an access_denied error.
30
+ */
31
+ async handle(ctx) {
32
+ const manager = await ctx.containerResolver.make(SesameManager);
33
+ await ctx.auth.check();
34
+ const user = ctx.auth.user;
35
+ if (!user) throw new E_INVALID_REQUEST("User must be authenticated");
36
+ const [error, body] = await ConsentController.validator.tryValidate(ctx.request.body());
37
+ if (error) throw new E_INVALID_REQUEST("Missing required parameter: auth_token");
38
+ const userId = String(user.id);
39
+ const hashedToken = new TokenService(manager).hashToken(body.auth_token);
40
+ const pendingRequest = await this.#consumePendingRequest(hashedToken, userId);
41
+ if (!pendingRequest) throw new E_INVALID_GRANT("Authorization request not found or expired");
42
+ const client = await OAuthClient.query().where("clientId", pendingRequest.clientId).first();
43
+ if (!client) throw new E_INVALID_CLIENT("Client not found");
44
+ if (client.isDisabled) throw new E_INVALID_CLIENT("Client is disabled");
45
+ if (!client.redirectUris.includes(pendingRequest.redirectUri)) throw new E_INVALID_REQUEST("Invalid redirect_uri");
46
+ if (!ctx.request.body().accept) return this.#redirectWithDenied(ctx, manager, pendingRequest);
47
+ await this.#persistConsent(client.clientId, userId, pendingRequest.scopes);
48
+ const code = await new IssueAuthorizationCodeAction().execute(manager, {
49
+ client,
50
+ userId,
51
+ scopes: pendingRequest.scopes,
52
+ redirectUri: pendingRequest.redirectUri,
53
+ codeChallenge: pendingRequest.codeChallenge ?? void 0,
54
+ codeChallengeMethod: pendingRequest.codeChallengeMethod ?? void 0,
55
+ nonce: pendingRequest.nonce ?? void 0
56
+ });
57
+ const url = new URL(pendingRequest.redirectUri);
58
+ url.searchParams.set("code", code);
59
+ if (pendingRequest.state) url.searchParams.set("state", pendingRequest.state);
60
+ url.searchParams.set("iss", manager.config.issuer);
61
+ return ctx.response.redirect().toPath(url.toString());
62
+ }
63
+ /**
64
+ * Atomically consume a pending authorization request.
65
+ * Uses DELETE-by-PK with affected-row check to prevent
66
+ * concurrent consent submissions from producing two
67
+ * authorization codes.
68
+ */
69
+ async #consumePendingRequest(hashedToken, userId) {
70
+ const row = await OAuthPendingAuthorizationRequest.query().where("token", hashedToken).where("userId", userId).where("expiresAt", ">", DateTime.now().toSQL()).first();
71
+ if (!row) return null;
72
+ const deleted = await OAuthPendingAuthorizationRequest.query().where("id", row.id).delete();
73
+ if ((Array.isArray(deleted) ? Number(deleted[0]) : Number(deleted)) === 0) return null;
74
+ return row;
75
+ }
76
+ /**
77
+ * Persist or merge consent so future authorization requests
78
+ * for the same client skip the consent screen.
79
+ */
80
+ async #persistConsent(clientId, userId, scopes) {
81
+ const existingConsent = await OAuthConsent.query().where("clientId", clientId).where("userId", userId).first();
82
+ if (existingConsent) {
83
+ existingConsent.scopes = [...new Set([...existingConsent.scopes, ...scopes])];
84
+ await existingConsent.save();
85
+ return;
86
+ }
87
+ await OAuthConsent.create({
88
+ id: crypto.randomUUID(),
89
+ clientId,
90
+ userId,
91
+ scopes
92
+ });
93
+ }
94
+ /**
95
+ * Redirect back to the client with an access_denied error
96
+ * when the user denies the authorization request.
97
+ */
98
+ #redirectWithDenied(ctx, manager, pendingRequest) {
99
+ const url = new URL(pendingRequest.redirectUri);
100
+ url.searchParams.set("error", "access_denied");
101
+ url.searchParams.set("error_description", "The user denied the authorization request");
102
+ if (pendingRequest.state) url.searchParams.set("state", pendingRequest.state);
103
+ url.searchParams.set("iss", manager.config.issuer);
104
+ return ctx.response.redirect().toPath(url.toString());
105
+ }
106
+ };
107
+ //#endregion
108
+ export { ConsentController as default };
@@ -0,0 +1,54 @@
1
+ import { l as RESERVED_OIDC_CLAIMS } from "./sesame_manager-DYUSZ0NC.js";
2
+ import { createHash } from "node:crypto";
3
+ import string from "@adonisjs/core/helpers/string";
4
+ //#region src/services/id_token_service.ts
5
+ /**
6
+ * Builds and signs OIDC `id_token` JWTs.
7
+ */
8
+ var IdTokenService = class IdTokenService {
9
+ #manager;
10
+ constructor(manager) {
11
+ this.#manager = manager;
12
+ }
13
+ /**
14
+ * Compute `at_hash` — left half of SHA-256 hash of the access token, base64url-encoded.
15
+ * @see OIDC Core §3.1.3.6
16
+ */
17
+ static computeAtHash(accessToken) {
18
+ const hash = createHash("sha256").update(accessToken).digest();
19
+ return hash.subarray(0, hash.length / 2).toString("base64url");
20
+ }
21
+ /**
22
+ * Filter out reserved claims that the server owns.
23
+ */
24
+ static filterReservedClaims(claims) {
25
+ const filtered = {};
26
+ for (const [key, value] of Object.entries(claims)) if (!RESERVED_OIDC_CLAIMS.has(key)) filtered[key] = value;
27
+ return filtered;
28
+ }
29
+ /**
30
+ * Resolve user OIDC claims by calling `getOidcClaims` if present,
31
+ * then filtering out reserved protocol claims.
32
+ */
33
+ static async resolveUserClaims(user, scopes) {
34
+ const rawClaims = typeof user?.getOidcClaims === "function" ? await user.getOidcClaims(scopes) : {};
35
+ return IdTokenService.filterReservedClaims(rawClaims);
36
+ }
37
+ async sign(options) {
38
+ const now = Math.floor(Date.now() / 1e3);
39
+ const ttlSeconds = string.seconds.parse(this.#manager.config.idTokenTtl);
40
+ const payload = {
41
+ ...await IdTokenService.resolveUserClaims(options.user, options.scopes),
42
+ iss: this.#manager.config.issuer,
43
+ sub: String(options.sub),
44
+ aud: options.clientId,
45
+ iat: now,
46
+ exp: now + ttlSeconds,
47
+ at_hash: IdTokenService.computeAtHash(options.accessToken)
48
+ };
49
+ if (options.nonce) payload.nonce = options.nonce;
50
+ return this.#manager.keyService.sign(payload);
51
+ }
52
+ };
53
+ //#endregion
54
+ export { IdTokenService as t };
package/build/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { configure } from './configure.ts';
2
2
  export { defineConfig } from './src/define_config.ts';
3
3
  export { SesameManager } from './src/sesame_manager.ts';
4
- export { OAuthError, E_INVALID_REQUEST, E_INVALID_CLIENT, E_INVALID_GRANT, E_INVALID_SCOPE, E_INVALID_TOKEN, E_UNSUPPORTED_GRANT_TYPE, E_UNSUPPORTED_RESPONSE_TYPE, E_ACCESS_DENIED, E_INVALID_CLIENT_METADATA, E_SERVER_ERROR, E_INSUFFICIENT_SCOPE, } from './src/oauth_error.ts';
4
+ export type { CreateClientOptions, CreateClientResult, UpdateClientOptions } from './src/types.ts';
5
5
  export { OAuthClient } from './src/models/oauth_client.ts';
6
6
  export { OAuthAccessToken } from './src/models/oauth_access_token.ts';
7
7
  export { OAuthRefreshToken } from './src/models/oauth_refresh_token.ts';
package/build/index.js CHANGED
@@ -1,11 +1,26 @@
1
- import { configure } from "./configure.js";
2
- import { a as OAuthRefreshToken, i as OAuthAuthorizationCode, r as OAuthConsent, t as SesameManager } from "./sesame_manager-C-eEFFHM.js";
3
- import "./decorate-BKZEjPRg.js";
4
- import { t as OAuthAccessToken } from "./oauth_access_token-bsoM5KeU.js";
5
- import { a as E_INVALID_GRANT, c as E_INVALID_TOKEN, d as E_UNSUPPORTED_RESPONSE_TYPE, f as OAuthError, i as E_INVALID_CLIENT_METADATA, l as E_SERVER_ERROR, n as E_INSUFFICIENT_SCOPE, o as E_INVALID_REQUEST, r as E_INVALID_CLIENT, s as E_INVALID_SCOPE, t as E_ACCESS_DENIED, u as E_UNSUPPORTED_GRANT_TYPE } from "./oauth_error-CnJ3L8tf.js";
6
- import { t as OAuthClient } from "./oauth_client-BIoY5jBR.js";
7
- import "./token_service-fhoA4slP.js";
8
- import { i as OAuthGuard, n as oauthUserProvider, r as OAuthLucidUserProvider, t as oauthGuard } from "./main-B3M6ihoS.js";
1
+ import "./chunk-DF48asd8.js";
2
+ import { t as configure } from "./configure-DkDkIlt8.js";
3
+ import { a as OAuthRefreshToken, i as OAuthAuthorizationCode, r as OAuthConsent, t as SesameManager } from "./sesame_manager-DYUSZ0NC.js";
4
+ import { t as OAuthClient } from "./oauth_client-BSanvSql.js";
5
+ import "./oauth_error-C7UhDb2q.js";
6
+ import { t as OAuthAccessToken } from "./oauth_access_token-Cz_5gNBx.js";
7
+ import "./token_service-DwnfAR9F.js";
8
+ import { i as OAuthGuard, n as oauthUserProvider, r as OAuthLucidUserProvider, t as oauthGuard } from "./main-DGBJhq3E.js";
9
+ //#region src/define_config.ts
10
+ /**
11
+ * Resolve user-supplied `SesameConfig` into a `ResolvedSesameConfig`
12
+ * by applying sensible defaults for all optional fields.
13
+ *
14
+ * Defaults:
15
+ * - `scopes`: `{}`
16
+ * - `defaultScopes`: `[]`
17
+ * - `grantTypes`: `['authorization_code', 'refresh_token']`
18
+ * - `accessTokenTtl`: `'1h'`
19
+ * - `refreshTokenTtl`: `'30d'`
20
+ * - `authorizationCodeTtl`: `'10m'`
21
+ * - `allowDynamicRegistration`: `false`
22
+ * - `allowPublicRegistration`: `false`
23
+ */
9
24
  function defineConfig(config) {
10
25
  return {
11
26
  issuer: config.issuer,
@@ -15,12 +30,17 @@ function defineConfig(config) {
15
30
  accessTokenTtl: config.accessTokenTtl ?? "1h",
16
31
  clientCredentialsAccessTokenTtl: config.clientCredentialsAccessTokenTtl ?? config.accessTokenTtl ?? "1h",
17
32
  refreshTokenTtl: config.refreshTokenTtl ?? "30d",
33
+ refreshTokenRotationGracePeriod: config.refreshTokenRotationGracePeriod ?? 120,
18
34
  authorizationCodeTtl: config.authorizationCodeTtl ?? "10m",
19
35
  authorizationRequestTtl: config.authorizationRequestTtl ?? config.authorizationCodeTtl ?? "10m",
20
36
  loginPage: config.loginPage,
21
37
  consentPage: config.consentPage,
22
38
  allowDynamicRegistration: config.allowDynamicRegistration ?? false,
23
- allowPublicRegistration: config.allowPublicRegistration ?? false
39
+ allowPublicRegistration: config.allowPublicRegistration ?? false,
40
+ jwk: config.jwk,
41
+ oidcProvider: config.oidcProvider,
42
+ idTokenTtl: config.idTokenTtl ?? "1h"
24
43
  };
25
44
  }
26
- export { E_ACCESS_DENIED, E_INSUFFICIENT_SCOPE, E_INVALID_CLIENT, E_INVALID_CLIENT_METADATA, E_INVALID_GRANT, E_INVALID_REQUEST, E_INVALID_SCOPE, E_INVALID_TOKEN, E_SERVER_ERROR, E_UNSUPPORTED_GRANT_TYPE, E_UNSUPPORTED_RESPONSE_TYPE, OAuthAccessToken, OAuthAuthorizationCode, OAuthClient, OAuthConsent, OAuthError, OAuthGuard, OAuthLucidUserProvider, OAuthRefreshToken, SesameManager, configure, defineConfig, oauthGuard, oauthUserProvider };
45
+ //#endregion
46
+ export { OAuthAccessToken, OAuthAuthorizationCode, OAuthClient, OAuthConsent, OAuthGuard, OAuthLucidUserProvider, OAuthRefreshToken, SesameManager, configure, defineConfig, oauthGuard, oauthUserProvider };