@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
@@ -1,167 +0,0 @@
1
- import { n as json, t as __decorate } from "./decorate-BKZEjPRg.js";
2
- import { t as OAuthAccessToken } from "./oauth_access_token-bsoM5KeU.js";
3
- import { DateTime } from "luxon";
4
- import { BaseModel, column } from "@adonisjs/lucid/orm";
5
- const BUILTIN_SCOPES = new Set(["offline_access"]);
6
- const controllers = {
7
- token: () => import("./token_controller-hGDAYuBS.js"),
8
- authorize: () => import("./authorize_controller-BGzxPvYU.js"),
9
- consent: () => import("./consent_controller-BHoB9mip.js"),
10
- introspect: () => import("./introspect_controller-un95fs4y.js"),
11
- revoke: () => import("./revoke_controller-DnPmzYMd.js"),
12
- register: () => import("./register_controller-Dch4ecyD.js"),
13
- metadata: () => import("./metadata_controller-CJeZG93_.js"),
14
- clientInfo: () => import("./client_info_controller-BucHGx4u.js")
15
- };
16
- function registerOAuthRoutes(router) {
17
- router.post("/token", [controllers.token]).as("sesame.token");
18
- router.get("/authorize", [controllers.authorize]).as("sesame.authorize");
19
- router.post("/consent", [controllers.consent]).as("sesame.consent");
20
- router.get("/client-info", [controllers.clientInfo]).as("sesame.clientInfo");
21
- router.post("/introspect", [controllers.introspect]).as("sesame.introspect");
22
- router.post("/revoke", [controllers.revoke]).as("sesame.revoke");
23
- router.post("/register", [controllers.register]).as("sesame.register");
24
- }
25
- function registerWellKnownRoutes(router) {
26
- router.get("/.well-known/oauth-authorization-server", [controllers.metadata, "authServer"]).as("sesame.metadata.authServer");
27
- router.get("/.well-known/openid-configuration", [controllers.metadata, "oidc"]).as("sesame.metadata.oidc");
28
- router.get("/.well-known/oauth-protected-resource", [controllers.metadata, "protectedResource"]).as("sesame.metadata.protectedResource");
29
- }
30
- var OAuthRefreshToken = class extends BaseModel {
31
- static table = "oauth_refresh_tokens";
32
- };
33
- __decorate([column({ isPrimary: true })], OAuthRefreshToken.prototype, "id", void 0);
34
- __decorate([column({ serializeAs: null })], OAuthRefreshToken.prototype, "token", void 0);
35
- __decorate([column()], OAuthRefreshToken.prototype, "accessTokenId", void 0);
36
- __decorate([column()], OAuthRefreshToken.prototype, "clientId", void 0);
37
- __decorate([column()], OAuthRefreshToken.prototype, "userId", void 0);
38
- __decorate([json()], OAuthRefreshToken.prototype, "scopes", void 0);
39
- __decorate([column.dateTime()], OAuthRefreshToken.prototype, "expiresAt", void 0);
40
- __decorate([column.dateTime()], OAuthRefreshToken.prototype, "revokedAt", void 0);
41
- __decorate([column.dateTime({ autoCreate: true })], OAuthRefreshToken.prototype, "createdAt", void 0);
42
- __decorate([column.dateTime({
43
- autoCreate: true,
44
- autoUpdate: true
45
- })], OAuthRefreshToken.prototype, "updatedAt", void 0);
46
- var OAuthAuthorizationCode = class extends BaseModel {
47
- static table = "oauth_authorization_codes";
48
- };
49
- __decorate([column({ isPrimary: true })], OAuthAuthorizationCode.prototype, "id", void 0);
50
- __decorate([column()], OAuthAuthorizationCode.prototype, "code", void 0);
51
- __decorate([column()], OAuthAuthorizationCode.prototype, "clientId", void 0);
52
- __decorate([column()], OAuthAuthorizationCode.prototype, "userId", void 0);
53
- __decorate([json()], OAuthAuthorizationCode.prototype, "scopes", void 0);
54
- __decorate([column()], OAuthAuthorizationCode.prototype, "redirectUri", void 0);
55
- __decorate([column()], OAuthAuthorizationCode.prototype, "codeChallenge", void 0);
56
- __decorate([column()], OAuthAuthorizationCode.prototype, "codeChallengeMethod", void 0);
57
- __decorate([column.dateTime()], OAuthAuthorizationCode.prototype, "expiresAt", void 0);
58
- __decorate([column.dateTime({ autoCreate: true })], OAuthAuthorizationCode.prototype, "createdAt", void 0);
59
- __decorate([column.dateTime({
60
- autoCreate: true,
61
- autoUpdate: true
62
- })], OAuthAuthorizationCode.prototype, "updatedAt", void 0);
63
- var OAuthConsent = class extends BaseModel {
64
- static table = "oauth_consents";
65
- };
66
- __decorate([column({ isPrimary: true })], OAuthConsent.prototype, "id", void 0);
67
- __decorate([column()], OAuthConsent.prototype, "clientId", void 0);
68
- __decorate([column()], OAuthConsent.prototype, "userId", void 0);
69
- __decorate([json()], OAuthConsent.prototype, "scopes", void 0);
70
- __decorate([column.dateTime({ autoCreate: true })], OAuthConsent.prototype, "createdAt", void 0);
71
- __decorate([column.dateTime({
72
- autoCreate: true,
73
- autoUpdate: true
74
- })], OAuthConsent.prototype, "updatedAt", void 0);
75
- var OAuthPendingAuthorizationRequest = class extends BaseModel {
76
- static table = "oauth_pending_authorization_requests";
77
- };
78
- __decorate([column({ isPrimary: true })], OAuthPendingAuthorizationRequest.prototype, "id", void 0);
79
- __decorate([column()], OAuthPendingAuthorizationRequest.prototype, "token", void 0);
80
- __decorate([column()], OAuthPendingAuthorizationRequest.prototype, "userId", void 0);
81
- __decorate([column()], OAuthPendingAuthorizationRequest.prototype, "clientId", void 0);
82
- __decorate([column()], OAuthPendingAuthorizationRequest.prototype, "redirectUri", void 0);
83
- __decorate([json()], OAuthPendingAuthorizationRequest.prototype, "scopes", void 0);
84
- __decorate([column()], OAuthPendingAuthorizationRequest.prototype, "state", void 0);
85
- __decorate([column()], OAuthPendingAuthorizationRequest.prototype, "codeChallenge", void 0);
86
- __decorate([column()], OAuthPendingAuthorizationRequest.prototype, "codeChallengeMethod", void 0);
87
- __decorate([column.dateTime()], OAuthPendingAuthorizationRequest.prototype, "expiresAt", void 0);
88
- __decorate([column.dateTime({ autoCreate: true })], OAuthPendingAuthorizationRequest.prototype, "createdAt", void 0);
89
- var SesameManager = class {
90
- #config;
91
- #router;
92
- constructor(config, router) {
93
- this.#config = config;
94
- this.#router = router;
95
- }
96
- get config() {
97
- return this.#config;
98
- }
99
- hasScope(scope) {
100
- return scope in this.#config.scopes;
101
- }
102
- validateScopes(scopes) {
103
- if (Object.keys(this.#config.scopes).length === 0) return scopes.filter((s) => !BUILTIN_SCOPES.has(s));
104
- return scopes.filter((s) => !BUILTIN_SCOPES.has(s) && !this.hasScope(s));
105
- }
106
- isGrantTypeEnabled(grantType) {
107
- return this.#config.grantTypes.includes(grantType);
108
- }
109
- async revokeAllForUser(userId) {
110
- const now = DateTime.now();
111
- await OAuthAccessToken.query().where("userId", userId).whereNull("revokedAt").update({ revokedAt: now.toSQL() });
112
- await OAuthRefreshToken.query().where("userId", userId).whereNull("revokedAt").update({ revokedAt: now.toSQL() });
113
- await OAuthAuthorizationCode.query().where("userId", userId).delete();
114
- await OAuthPendingAuthorizationRequest.query().where("userId", userId).delete();
115
- await OAuthConsent.query().where("userId", userId).delete();
116
- }
117
- async purgeTokens(options) {
118
- const revokedOnly = options?.revokedOnly ?? false;
119
- const expiredOnly = options?.expiredOnly ?? false;
120
- const retentionHours = options?.retentionHours ?? 168;
121
- const purgeRevoked = revokedOnly || !expiredOnly;
122
- const purgeExpired = expiredOnly || !revokedOnly;
123
- const cutoff = DateTime.now().minus({ hours: retentionHours });
124
- let accessTokens = 0;
125
- let refreshTokens = 0;
126
- let authorizationCodes = 0;
127
- let pendingRequests = 0;
128
- if (purgeRevoked) {
129
- accessTokens += await this.#deleteCount(OAuthAccessToken.query().whereNotNull("revokedAt").delete());
130
- refreshTokens += await this.#deleteCount(OAuthRefreshToken.query().whereNotNull("revokedAt").delete());
131
- }
132
- if (purgeExpired) {
133
- accessTokens += await this.#deleteCount(OAuthAccessToken.query().where("expiresAt", "<", cutoff.toSQL()).whereNull("revokedAt").delete());
134
- refreshTokens += await this.#deleteCount(OAuthRefreshToken.query().where("expiresAt", "<", cutoff.toSQL()).whereNull("revokedAt").delete());
135
- authorizationCodes += await this.#deleteCount(OAuthAuthorizationCode.query().where("expiresAt", "<", cutoff.toSQL()).delete());
136
- }
137
- pendingRequests += await this.#deleteCount(OAuthPendingAuthorizationRequest.query().where("expiresAt", "<", DateTime.now().toSQL()).delete());
138
- return {
139
- accessTokens,
140
- refreshTokens,
141
- authorizationCodes,
142
- pendingRequests
143
- };
144
- }
145
- registerRoutes() {
146
- registerOAuthRoutes(this.#router);
147
- }
148
- registerWellKnownRoutes() {
149
- registerWellKnownRoutes(this.#router);
150
- }
151
- registerProtectedResource(options) {
152
- const wellKnownPath = `/.well-known/oauth-protected-resource${options.resource}`;
153
- this.#router.get(wellKnownPath, async (ctx) => {
154
- ctx.response.header("Cache-Control", "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400");
155
- return {
156
- resource: `${this.#config.issuer}${options.resource}`,
157
- authorization_servers: [this.#config.issuer],
158
- scopes_supported: options.scopes ?? Object.keys(this.#config.scopes),
159
- bearer_methods_supported: ["header"]
160
- };
161
- });
162
- }
163
- #deleteCount(result) {
164
- return result.then((r) => Array.isArray(r) ? Number(r[0] ?? 0) : Number(r));
165
- }
166
- };
167
- export { OAuthRefreshToken as a, OAuthAuthorizationCode as i, OAuthPendingAuthorizationRequest as n, BUILTIN_SCOPES as o, OAuthConsent as r, SesameManager as t };
@@ -1,27 +0,0 @@
1
- import type { HttpContext } from '@adonisjs/core/http';
2
- import type { SesameManager } from '../sesame_manager.ts';
3
- /**
4
- * Handle the Authorization Code Grant (RFC 6749 §4.1.3).
5
- *
6
- * Exchanges an authorization code for an access token (and optionally
7
- * a refresh token if the `offline_access` scope was granted).
8
- *
9
- * Performs the following validations:
10
- * - Client authentication (Basic header or POST body credentials)
11
- * - Authorization code existence, expiration, and single-use enforcement
12
- * - Redirect URI matching against the original authorization request
13
- * - PKCE code_verifier verification using S256 (RFC 7636 §4.6)
14
- *
15
- * All tokens (access tokens, refresh tokens, authorization codes)
16
- * are opaque values stored as SHA-256 hashes.
17
- *
18
- * @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3
19
- * @see https://datatracker.ietf.org/doc/html/rfc7636#section-4.6
20
- */
21
- export declare function handleAuthorizationCodeGrant(ctx: HttpContext, manager: SesameManager): Promise<{
22
- refresh_token?: string | undefined;
23
- access_token: string;
24
- token_type: string;
25
- expires_in: number;
26
- scope: string;
27
- }>;
@@ -1,23 +0,0 @@
1
- import type { HttpContext } from '@adonisjs/core/http';
2
- import type { SesameManager } from '../sesame_manager.ts';
3
- /**
4
- * Handle the Client Credentials Grant (RFC 6749 §4.4).
5
- *
6
- * Issues an access token directly to the client for
7
- * machine-to-machine (M2M) communication. The client
8
- * authenticates with its own credentials and receives an
9
- * access token without an interactive user step.
10
- *
11
- * No refresh token is issued (per spec and convention).
12
- *
13
- * Built-in OIDC scopes (e.g. `offline_access`) are rejected
14
- * since they are user-centric and meaningless in an M2M context.
15
- *
16
- * @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.4
17
- */
18
- export declare function handleClientCredentialsGrant(ctx: HttpContext, manager: SesameManager): Promise<{
19
- access_token: string;
20
- token_type: string;
21
- expires_in: number;
22
- scope: string;
23
- }>;
@@ -1,27 +0,0 @@
1
- import type { HttpContext } from '@adonisjs/core/http';
2
- import type { SesameManager } from '../sesame_manager.ts';
3
- /**
4
- * Handle the Refresh Token Grant (RFC 6749 §6).
5
- *
6
- * Exchanges a refresh token for a new access token and a new
7
- * refresh token (rotation). The old refresh token is revoked
8
- * immediately after use.
9
- *
10
- * Implements replay detection: if a revoked refresh token is
11
- * presented, all tokens for that client+user pair are nuked
12
- * (both access and refresh tokens) to mitigate stolen token
13
- * reuse attacks.
14
- *
15
- * Scope narrowing is supported: the client may request a subset
16
- * of the originally granted scopes, but cannot request new ones.
17
- *
18
- * @see https://datatracker.ietf.org/doc/html/rfc6749#section-6
19
- * @see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.14.2
20
- */
21
- export declare function handleRefreshTokenGrant(ctx: HttpContext, manager: SesameManager): Promise<{
22
- access_token: string;
23
- token_type: string;
24
- expires_in: number;
25
- scope: string;
26
- refresh_token: string;
27
- }>;
@@ -1,194 +0,0 @@
1
- import { a as OAuthRefreshToken, i as OAuthAuthorizationCode, o as BUILTIN_SCOPES, t as SesameManager } from "./sesame_manager-C-eEFFHM.js";
2
- import "./decorate-BKZEjPRg.js";
3
- import { t as OAuthAccessToken } from "./oauth_access_token-bsoM5KeU.js";
4
- import { a as E_INVALID_GRANT, o as E_INVALID_REQUEST, r as E_INVALID_CLIENT, s as E_INVALID_SCOPE, u as E_UNSUPPORTED_GRANT_TYPE } from "./oauth_error-CnJ3L8tf.js";
5
- import "./oauth_client-BIoY5jBR.js";
6
- import { t as TokenService } from "./token_service-fhoA4slP.js";
7
- import { t as ClientService } from "./client_service-C3rfXGk_.js";
8
- import { DateTime } from "luxon";
9
- import { createHash } from "node:crypto";
10
- import string from "@adonisjs/core/helpers/string";
11
- import vine from "@vinejs/vine";
12
- const codeVerifierValidator = vine.create({ code_verifier: vine.string().minLength(43).maxLength(128).regex(/^[A-Za-z0-9\-._~]+$/) });
13
- async function handleAuthorizationCodeGrant(ctx, manager) {
14
- const tokenService = new TokenService(manager);
15
- const clientService = new ClientService();
16
- const body = ctx.request.body();
17
- const code = body.code;
18
- const redirectUri = body.redirect_uri;
19
- const codeVerifier = body.code_verifier;
20
- if (!code) throw new E_INVALID_REQUEST("Missing required parameter: code");
21
- if (!redirectUri) throw new E_INVALID_REQUEST("Missing required parameter: redirect_uri");
22
- const client = await clientService.authenticateClient({
23
- authorizationHeader: ctx.request.header("authorization"),
24
- bodyClientId: body.client_id,
25
- bodyClientSecret: body.client_secret
26
- });
27
- if (!client.grantTypes.includes("authorization_code")) throw new E_INVALID_CLIENT("Client is not allowed to use the authorization_code grant");
28
- const hashedCode = tokenService.hashToken(code);
29
- const authCode = await OAuthAuthorizationCode.query().where("code", hashedCode).where("clientId", client.clientId).first();
30
- if (!authCode) throw new E_INVALID_GRANT("Authorization code not found");
31
- if (authCode.expiresAt < DateTime.now()) {
32
- await OAuthAuthorizationCode.query().where("id", authCode.id).delete();
33
- throw new E_INVALID_GRANT("Authorization code has expired");
34
- }
35
- if (authCode.redirectUri !== redirectUri) throw new E_INVALID_GRANT("Redirect URI mismatch");
36
- const deleteResult = await OAuthAuthorizationCode.query().where("id", authCode.id).delete();
37
- if ((Array.isArray(deleteResult) ? Number(deleteResult[0] ?? 0) : Number(deleteResult)) !== 1) throw new E_INVALID_GRANT("Authorization code has already been consumed");
38
- const [verifierError] = await codeVerifierValidator.tryValidate({ code_verifier: codeVerifier });
39
- if (verifierError) throw new E_INVALID_REQUEST("code_verifier must be 43-128 characters using only [A-Za-z0-9-._~] (RFC 7636 §4.1)");
40
- if (!authCode.codeChallenge) throw new E_INVALID_GRANT("Authorization code is missing PKCE challenge");
41
- if (createHash("sha256").update(codeVerifier).digest("base64url") !== authCode.codeChallenge) throw new E_INVALID_GRANT("PKCE verification failed");
42
- clientService.validateClientScopes(authCode.scopes, client.scopes);
43
- const { raw: accessTokenRaw, hash: tokenHash, expiresAt } = tokenService.createAccessToken();
44
- await OAuthAccessToken.create({
45
- id: crypto.randomUUID(),
46
- tokenHash,
47
- clientId: client.clientId,
48
- userId: authCode.userId,
49
- scopes: authCode.scopes,
50
- expiresAt: DateTime.fromJSDate(expiresAt)
51
- });
52
- let refreshTokenRaw;
53
- if (authCode.scopes.includes("offline_access")) {
54
- const { raw, hash } = tokenService.createRefreshToken();
55
- const refreshTtl = string.seconds.parse(manager.config.refreshTokenTtl);
56
- await OAuthRefreshToken.create({
57
- id: crypto.randomUUID(),
58
- token: hash,
59
- accessTokenId: tokenHash,
60
- clientId: client.clientId,
61
- userId: authCode.userId,
62
- scopes: authCode.scopes,
63
- expiresAt: DateTime.now().plus({ seconds: refreshTtl })
64
- });
65
- refreshTokenRaw = raw;
66
- }
67
- return {
68
- access_token: accessTokenRaw,
69
- token_type: "Bearer",
70
- expires_in: string.seconds.parse(manager.config.accessTokenTtl),
71
- scope: authCode.scopes.join(" "),
72
- ...refreshTokenRaw ? { refresh_token: refreshTokenRaw } : {}
73
- };
74
- }
75
- async function handleRefreshTokenGrant(ctx, manager) {
76
- const tokenService = new TokenService(manager);
77
- const clientService = new ClientService();
78
- const body = ctx.request.body();
79
- const refreshTokenRaw = body.refresh_token;
80
- if (!refreshTokenRaw) throw new E_INVALID_REQUEST("Missing required parameter: refresh_token");
81
- const client = await clientService.authenticateClient({
82
- authorizationHeader: ctx.request.header("authorization"),
83
- bodyClientId: body.client_id,
84
- bodyClientSecret: body.client_secret
85
- });
86
- if (!client.grantTypes.includes("refresh_token")) throw new E_INVALID_CLIENT("Client is not allowed to use the refresh_token grant");
87
- const hashedToken = tokenService.hashToken(refreshTokenRaw);
88
- const refreshToken = await OAuthRefreshToken.query().where("token", hashedToken).where("clientId", client.clientId).first();
89
- if (!refreshToken) throw new E_INVALID_GRANT("Refresh token not found");
90
- if (refreshToken.revokedAt) {
91
- await OAuthRefreshToken.query().where("clientId", client.clientId).where("userId", refreshToken.userId).delete();
92
- await OAuthAccessToken.query().where("clientId", client.clientId).where("userId", refreshToken.userId).whereNull("revokedAt").update({ revokedAt: DateTime.now().toSQL() });
93
- throw new E_INVALID_GRANT("Refresh token has been revoked (possible replay attack)");
94
- }
95
- if (refreshToken.expiresAt < DateTime.now()) throw new E_INVALID_GRANT("Refresh token has expired");
96
- const requestedScope = body.scope;
97
- let scopes = refreshToken.scopes;
98
- if (requestedScope) {
99
- const requested = requestedScope.split(" ");
100
- const originalSet = new Set(refreshToken.scopes);
101
- const invalid = requested.filter((s) => !originalSet.has(s));
102
- if (invalid.length > 0) throw new E_INVALID_SCOPE(`Scope not in original grant: ${invalid.join(", ")}`);
103
- scopes = requested;
104
- }
105
- clientService.validateClientScopes(scopes, client.scopes);
106
- const revokedAt = DateTime.now();
107
- const updateResult = await OAuthRefreshToken.query().where("id", refreshToken.id).whereNull("revokedAt").update({ revokedAt: revokedAt.toSQL() });
108
- if ((Array.isArray(updateResult) ? Number(updateResult[0] ?? 0) : Number(updateResult)) !== 1) throw new E_INVALID_GRANT("Refresh token has already been consumed");
109
- await OAuthAccessToken.query().where("tokenHash", refreshToken.accessTokenId).whereNull("revokedAt").update({ revokedAt: revokedAt.toSQL() });
110
- const { raw: accessTokenRaw, hash: tokenHash, expiresAt } = tokenService.createAccessToken();
111
- await OAuthAccessToken.create({
112
- id: crypto.randomUUID(),
113
- tokenHash,
114
- clientId: client.clientId,
115
- userId: refreshToken.userId,
116
- scopes,
117
- expiresAt: DateTime.fromJSDate(expiresAt)
118
- });
119
- const { raw: newRefreshTokenRaw, hash: newRefreshTokenHash } = tokenService.createRefreshToken();
120
- const refreshTtl = string.seconds.parse(manager.config.refreshTokenTtl);
121
- await OAuthRefreshToken.create({
122
- id: crypto.randomUUID(),
123
- token: newRefreshTokenHash,
124
- accessTokenId: tokenHash,
125
- clientId: client.clientId,
126
- userId: refreshToken.userId,
127
- scopes,
128
- expiresAt: DateTime.now().plus({ seconds: refreshTtl })
129
- });
130
- return {
131
- access_token: accessTokenRaw,
132
- token_type: "Bearer",
133
- expires_in: string.seconds.parse(manager.config.accessTokenTtl),
134
- scope: scopes.join(" "),
135
- refresh_token: newRefreshTokenRaw
136
- };
137
- }
138
- async function handleClientCredentialsGrant(ctx, manager) {
139
- const tokenService = new TokenService(manager);
140
- const clientService = new ClientService();
141
- const body = ctx.request.body();
142
- const client = await clientService.authenticateClient({
143
- authorizationHeader: ctx.request.header("authorization"),
144
- bodyClientId: body.client_id,
145
- bodyClientSecret: body.client_secret
146
- });
147
- if (client.isPublic) throw new E_INVALID_CLIENT("Public clients cannot use the client_credentials grant");
148
- if (!client.grantTypes.includes("client_credentials")) throw new E_INVALID_CLIENT("Client is not allowed to use the client_credentials grant");
149
- const requestedScopes = body.scope ? body.scope.split(" ") : client.scopes.filter((scope) => !BUILTIN_SCOPES.has(scope));
150
- const builtinRequested = requestedScopes.filter((s) => BUILTIN_SCOPES.has(s));
151
- if (builtinRequested.length > 0) throw new E_INVALID_SCOPE(`Scopes not allowed for client_credentials: ${builtinRequested.join(", ")}`);
152
- const invalidScopes = manager.validateScopes(requestedScopes);
153
- if (invalidScopes.length > 0) throw new E_INVALID_SCOPE(`Invalid scopes: ${invalidScopes.join(", ")}`);
154
- clientService.validateClientScopes(requestedScopes, client.scopes);
155
- if (!client.userId) throw new E_INVALID_CLIENT("Client must be associated with a user to use the client_credentials grant");
156
- const ttlSeconds = string.seconds.parse(manager.config.clientCredentialsAccessTokenTtl);
157
- const { raw: accessTokenRaw, hash: tokenHash } = tokenService.createAccessToken();
158
- const expiresAt = new Date(Date.now() + ttlSeconds * 1e3);
159
- await OAuthAccessToken.create({
160
- id: crypto.randomUUID(),
161
- tokenHash,
162
- clientId: client.clientId,
163
- userId: client.userId,
164
- scopes: requestedScopes,
165
- expiresAt: DateTime.fromJSDate(expiresAt)
166
- });
167
- return {
168
- access_token: accessTokenRaw,
169
- token_type: "Bearer",
170
- expires_in: ttlSeconds,
171
- scope: requestedScopes.join(" ")
172
- };
173
- }
174
- const grantHandlers = {
175
- authorization_code: handleAuthorizationCodeGrant,
176
- refresh_token: handleRefreshTokenGrant,
177
- client_credentials: handleClientCredentialsGrant
178
- };
179
- var TokenController = class TokenController {
180
- static validator = vine.create({ grant_type: vine.string() });
181
- async handle(ctx) {
182
- const manager = await ctx.containerResolver.make(SesameManager);
183
- const [error, body] = await TokenController.validator.tryValidate(ctx.request.body());
184
- if (error) throw new E_UNSUPPORTED_GRANT_TYPE("Missing required parameter: grant_type");
185
- if (!manager.isGrantTypeEnabled(body.grant_type)) throw new E_UNSUPPORTED_GRANT_TYPE(`Grant type "${body.grant_type}" is not enabled`);
186
- const handler = grantHandlers[body.grant_type];
187
- if (!handler) throw new E_UNSUPPORTED_GRANT_TYPE(`Unsupported grant type: ${body.grant_type}`);
188
- const result = await handler(ctx, manager);
189
- ctx.response.header("Cache-Control", "no-store");
190
- ctx.response.header("Pragma", "no-cache");
191
- return result;
192
- }
193
- };
194
- export { TokenController as default };
@@ -1,31 +0,0 @@
1
- import { createHash, randomBytes } from "node:crypto";
2
- import string from "@adonisjs/core/helpers/string";
3
- var TokenService = class {
4
- #manager;
5
- constructor(manager) {
6
- this.#manager = manager;
7
- }
8
- createAccessToken() {
9
- const raw = this.generateOpaqueToken();
10
- const ttlSeconds = string.seconds.parse(this.#manager.config.accessTokenTtl);
11
- return {
12
- raw,
13
- hash: this.hashToken(raw),
14
- expiresAt: new Date(Date.now() + ttlSeconds * 1e3)
15
- };
16
- }
17
- createRefreshToken() {
18
- const raw = this.generateOpaqueToken();
19
- return {
20
- raw,
21
- hash: this.hashToken(raw)
22
- };
23
- }
24
- generateOpaqueToken() {
25
- return randomBytes(32).toString("base64url");
26
- }
27
- hashToken(token) {
28
- return createHash("sha256").update(token).digest("base64url");
29
- }
30
- };
31
- export { TokenService as t };