@julr/sesame 0.0.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 (69) hide show
  1. package/LICENSE.md +9 -0
  2. package/README.md +130 -0
  3. package/build/authorize_controller-CUdEDNEi.js +136 -0
  4. package/build/client_info_controller-DeIVcW8B.js +18 -0
  5. package/build/client_service-B9fD3ZGe.js +53 -0
  6. package/build/commands/commands.json +1 -0
  7. package/build/commands/main.d.ts +4 -0
  8. package/build/commands/main.js +36 -0
  9. package/build/commands/sesame_purge.d.ts +21 -0
  10. package/build/commands/sesame_purge.js +28 -0
  11. package/build/configure.d.ts +2 -0
  12. package/build/configure.js +16 -0
  13. package/build/consent_controller-DFfx7qVs.js +87 -0
  14. package/build/decorate-2_8Ex77k.js +15 -0
  15. package/build/index.d.ts +14 -0
  16. package/build/index.js +27 -0
  17. package/build/introspect_controller-BzwfaUUE.js +63 -0
  18. package/build/main-kn40V-hF.js +2 -0
  19. package/build/metadata_controller-BSRRElQX.js +51 -0
  20. package/build/oauth_access_token-BpG8sq-c.js +18 -0
  21. package/build/oauth_client-eh0e5ql-.js +24 -0
  22. package/build/oauth_error-BQPqV-MV.js +78 -0
  23. package/build/providers/sesame_provider.d.ts +21 -0
  24. package/build/providers/sesame_provider.js +19 -0
  25. package/build/register_controller-BA7uQAgt.js +139 -0
  26. package/build/revoke_controller-CNIgNKH3.js +50 -0
  27. package/build/rolldown-runtime-BASaM9lw.js +12 -0
  28. package/build/routes-D6QCu0Pz.js +43 -0
  29. package/build/sesame_manager-B4tO2PLO.js +116 -0
  30. package/build/src/controllers/authorize_controller.d.ts +53 -0
  31. package/build/src/controllers/client_info_controller.d.ts +22 -0
  32. package/build/src/controllers/consent_controller.d.ts +27 -0
  33. package/build/src/controllers/introspect_controller.d.ts +28 -0
  34. package/build/src/controllers/metadata_controller.d.ts +64 -0
  35. package/build/src/controllers/register_controller.d.ts +91 -0
  36. package/build/src/controllers/revoke_controller.d.ts +16 -0
  37. package/build/src/controllers/token_controller.d.ts +24 -0
  38. package/build/src/decorators.d.ts +10 -0
  39. package/build/src/define_config.d.ts +16 -0
  40. package/build/src/grants/authorization_code_grant.d.ts +27 -0
  41. package/build/src/grants/refresh_token_grant.d.ts +27 -0
  42. package/build/src/guard/guard.d.ts +30 -0
  43. package/build/src/guard/main.d.ts +20 -0
  44. package/build/src/guard/main.js +17 -0
  45. package/build/src/guard/types.d.ts +46 -0
  46. package/build/src/guard/user_provider.d.ts +14 -0
  47. package/build/src/models/oauth_access_token.d.ts +23 -0
  48. package/build/src/models/oauth_authorization_code.d.ts +30 -0
  49. package/build/src/models/oauth_client.d.ts +33 -0
  50. package/build/src/models/oauth_consent.d.ts +22 -0
  51. package/build/src/models/oauth_refresh_token.d.ts +29 -0
  52. package/build/src/oauth_error.d.ts +359 -0
  53. package/build/src/routes.d.ts +28 -0
  54. package/build/src/rules.d.ts +12 -0
  55. package/build/src/services/client_service.d.ts +67 -0
  56. package/build/src/services/token_service.d.ts +42 -0
  57. package/build/src/sesame_manager.d.ts +66 -0
  58. package/build/src/types.d.ts +141 -0
  59. package/build/stubs/config/sesame.stub +30 -0
  60. package/build/stubs/main.d.ts +5 -0
  61. package/build/stubs/migrations/create_oauth_access_tokens_table.stub +25 -0
  62. package/build/stubs/migrations/create_oauth_authorization_codes_table.stub +27 -0
  63. package/build/stubs/migrations/create_oauth_clients_table.stub +31 -0
  64. package/build/stubs/migrations/create_oauth_consents_table.stub +24 -0
  65. package/build/stubs/migrations/create_oauth_refresh_tokens_table.stub +26 -0
  66. package/build/token_controller-C9wh813f.js +172 -0
  67. package/build/token_service-Czz9v5GI.js +30 -0
  68. package/build/user_provider-B3rXEUT3.js +150 -0
  69. package/package.json +144 -0
@@ -0,0 +1,141 @@
1
+ import type { HttpContext } from '@adonisjs/core/http';
2
+ /**
3
+ * Supported grant types for v1 (MCP-focused).
4
+ *
5
+ * - `authorization_code`: RFC 6749 §4.1 — Authorization Code Grant
6
+ * - `refresh_token`: RFC 6749 §6 — Refreshing an Access Token
7
+ *
8
+ * @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1
9
+ * @see https://datatracker.ietf.org/doc/html/rfc6749#section-6
10
+ */
11
+ export type GrantType = 'authorization_code' | 'refresh_token';
12
+ /**
13
+ * User-facing configuration interface for Sésame.
14
+ *
15
+ * Provides all options needed to set up the OAuth 2.1 authorization
16
+ * server, including issuer identity, scopes, token lifetimes, and
17
+ * page redirects for the authorization flow.
18
+ */
19
+ export interface SesameConfig {
20
+ /**
21
+ * The issuer URL (must be HTTPS in production).
22
+ * Used in JWT `iss` claim and discovery metadata.
23
+ *
24
+ * @see https://datatracker.ietf.org/doc/html/rfc8414#section-2
25
+ */
26
+ issuer: string;
27
+ /**
28
+ * Available scopes as a record of scope name to description.
29
+ *
30
+ * @see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3
31
+ */
32
+ scopes?: Record<string, string>;
33
+ /**
34
+ * Default scopes assigned when none are requested
35
+ * in the authorization request.
36
+ */
37
+ defaultScopes?: string[];
38
+ /**
39
+ * Enabled grant types.
40
+ * Defaults to `['authorization_code', 'refresh_token']`.
41
+ */
42
+ grantTypes?: GrantType[];
43
+ /**
44
+ * Access token TTL as a string duration (e.g. '1h', '30m').
45
+ * Defaults to '1h'.
46
+ */
47
+ accessTokenTtl?: string;
48
+ /**
49
+ * Refresh token TTL as a string duration.
50
+ * Defaults to '30d'.
51
+ */
52
+ refreshTokenTtl?: string;
53
+ /**
54
+ * Authorization code TTL as a string duration.
55
+ * Defaults to '10m'.
56
+ */
57
+ authorizationCodeTtl?: string;
58
+ /**
59
+ * Route or URL where unauthenticated users are redirected
60
+ * to log in during the authorization flow. Can be a string
61
+ * path or a function receiving the HttpContext and authorize
62
+ * query parameters.
63
+ */
64
+ loginPage: string | ((ctx: HttpContext, params: URLSearchParams) => string);
65
+ /**
66
+ * Route or URL where users are redirected to approve/deny
67
+ * client access during the authorization flow. Can be a string
68
+ * path or a function receiving the HttpContext and authorize
69
+ * query parameters.
70
+ */
71
+ consentPage: string | ((ctx: HttpContext, params: URLSearchParams) => string);
72
+ /**
73
+ * Allow dynamic client registration.
74
+ * Defaults to `false`.
75
+ *
76
+ * @see https://datatracker.ietf.org/doc/html/rfc7591
77
+ */
78
+ allowDynamicRegistration?: boolean;
79
+ /**
80
+ * Allow unauthenticated client registration (needed for MCP).
81
+ * Defaults to `false`.
82
+ */
83
+ allowPublicRegistration?: boolean;
84
+ }
85
+ /**
86
+ * Fully resolved configuration with defaults applied.
87
+ * Created by `defineConfig()` from user-supplied `SesameConfig`.
88
+ */
89
+ export interface ResolvedSesameConfig {
90
+ issuer: string;
91
+ scopes: Record<string, string>;
92
+ defaultScopes: string[];
93
+ grantTypes: GrantType[];
94
+ accessTokenTtl: string;
95
+ refreshTokenTtl: string;
96
+ authorizationCodeTtl: string;
97
+ loginPage: string | ((ctx: HttpContext, params: URLSearchParams) => string);
98
+ consentPage: string | ((ctx: HttpContext, params: URLSearchParams) => string);
99
+ allowDynamicRegistration: boolean;
100
+ allowPublicRegistration: boolean;
101
+ }
102
+ /**
103
+ * OAuth 2.0 Authorization Server Metadata as defined by RFC 8414.
104
+ *
105
+ * Returned by the `/.well-known/oauth-authorization-server` endpoint
106
+ * to allow clients to discover server capabilities.
107
+ *
108
+ * @see https://datatracker.ietf.org/doc/html/rfc8414#section-2
109
+ */
110
+ export interface AuthServerMetadata {
111
+ issuer: string;
112
+ authorization_endpoint: string;
113
+ token_endpoint: string;
114
+ registration_endpoint?: string;
115
+ introspection_endpoint?: string;
116
+ revocation_endpoint?: string;
117
+ response_types_supported: string[];
118
+ response_modes_supported: string[];
119
+ grant_types_supported: string[];
120
+ token_endpoint_auth_methods_supported: string[];
121
+ introspection_endpoint_auth_methods_supported?: string[];
122
+ revocation_endpoint_auth_methods_supported?: string[];
123
+ code_challenge_methods_supported: string[];
124
+ authorization_response_iss_parameter_supported: boolean;
125
+ }
126
+ /**
127
+ * OAuth 2.0 Protected Resource Metadata as defined by RFC 9728.
128
+ *
129
+ * Returned by the `/.well-known/oauth-protected-resource` endpoint.
130
+ * Used by MCP clients to discover which authorization servers
131
+ * protect a given resource.
132
+ *
133
+ * @see https://datatracker.ietf.org/doc/html/rfc9728
134
+ */
135
+ export interface ResourceServerMetadata {
136
+ resource: string;
137
+ authorization_servers: string[];
138
+ scopes_supported?: string[];
139
+ bearer_methods_supported?: string[];
140
+ resource_documentation?: string;
141
+ }
@@ -0,0 +1,30 @@
1
+ {{{
2
+ exports: { defineConfig: 'import { defineConfig } from \'@adonisjs/sesame\'' }
3
+ }}}
4
+ import env from '#start/env'
5
+
6
+ const sesameConfig = defineConfig({
7
+ issuer: env.get('APP_URL'),
8
+
9
+ scopes: {
10
+ // Define your available scopes here
11
+ // 'read': 'Read access',
12
+ // 'write': 'Write access',
13
+ },
14
+
15
+ defaultScopes: [],
16
+
17
+ grantTypes: ['authorization_code', 'refresh_token'],
18
+
19
+ accessTokenTtl: '1h',
20
+ refreshTokenTtl: '30d',
21
+ authorizationCodeTtl: '10m',
22
+
23
+ loginPage: '/login',
24
+ consentPage: '/oauth/consent',
25
+
26
+ allowDynamicRegistration: false,
27
+ allowPublicRegistration: false,
28
+ })
29
+
30
+ export default sesameConfig
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Path to the root directory where the stubs are stored. We use
3
+ * this path within commands and the configure hook
4
+ */
5
+ export declare const stubsRoot: string;
@@ -0,0 +1,25 @@
1
+ {{{
2
+ exports: { migration: "import { BaseSchema } from '@adonisjs/lucid/schema'" }
3
+ }}}
4
+
5
+ export default class extends BaseSchema {
6
+ protected tableName = 'oauth_access_tokens'
7
+
8
+ async up() {
9
+ this.schema.createTable(this.tableName, (table) => {
10
+ table.uuid('id').primary()
11
+ table.string('token_hash').unique().notNullable()
12
+ table.string('client_id').notNullable().references('client_id').inTable('oauth_clients').onDelete('CASCADE')
13
+ table.string('user_id').nullable()
14
+ table.json('scopes').notNullable()
15
+ table.timestamp('expires_at').notNullable()
16
+ table.timestamp('revoked_at').nullable()
17
+ table.timestamp('created_at').notNullable()
18
+ table.timestamp('updated_at').notNullable()
19
+ })
20
+ }
21
+
22
+ async down() {
23
+ this.schema.dropTable(this.tableName)
24
+ }
25
+ }
@@ -0,0 +1,27 @@
1
+ {{{
2
+ exports: { migration: "import { BaseSchema } from '@adonisjs/lucid/schema'" }
3
+ }}}
4
+
5
+ export default class extends BaseSchema {
6
+ protected tableName = 'oauth_authorization_codes'
7
+
8
+ async up() {
9
+ this.schema.createTable(this.tableName, (table) => {
10
+ table.uuid('id').primary()
11
+ table.string('code').notNullable().index()
12
+ table.string('client_id').notNullable().references('client_id').inTable('oauth_clients').onDelete('CASCADE')
13
+ table.string('user_id').notNullable()
14
+ table.json('scopes').notNullable()
15
+ table.text('redirect_uri').notNullable()
16
+ table.string('code_challenge').nullable()
17
+ table.string('code_challenge_method').nullable()
18
+ table.timestamp('expires_at').notNullable()
19
+ table.timestamp('created_at').notNullable()
20
+ table.timestamp('updated_at').notNullable()
21
+ })
22
+ }
23
+
24
+ async down() {
25
+ this.schema.dropTable(this.tableName)
26
+ }
27
+ }
@@ -0,0 +1,31 @@
1
+ {{{
2
+ exports: { migration: "import { BaseSchema } from '@adonisjs/lucid/schema'" }
3
+ }}}
4
+
5
+ export default class extends BaseSchema {
6
+ protected tableName = 'oauth_clients'
7
+
8
+ async up() {
9
+ this.schema.createTable(this.tableName, (table) => {
10
+ table.uuid('id').primary()
11
+ table.string('client_id').unique().notNullable()
12
+ table.text('client_secret').nullable()
13
+ table.string('name').notNullable()
14
+ table.json('redirect_uris').notNullable()
15
+ table.json('scopes').notNullable().defaultTo('[]')
16
+ table.json('grant_types').notNullable().defaultTo('[]')
17
+ table.boolean('is_public').notNullable().defaultTo(false)
18
+ table.boolean('is_disabled').notNullable().defaultTo(false)
19
+ table.boolean('require_pkce').notNullable().defaultTo(true)
20
+ table.string('type').nullable()
21
+ table.json('metadata').nullable()
22
+ table.string('user_id').nullable()
23
+ table.timestamp('created_at').notNullable()
24
+ table.timestamp('updated_at').notNullable()
25
+ })
26
+ }
27
+
28
+ async down() {
29
+ this.schema.dropTable(this.tableName)
30
+ }
31
+ }
@@ -0,0 +1,24 @@
1
+ {{{
2
+ exports: { migration: "import { BaseSchema } from '@adonisjs/lucid/schema'" }
3
+ }}}
4
+
5
+ export default class extends BaseSchema {
6
+ protected tableName = 'oauth_consents'
7
+
8
+ async up() {
9
+ this.schema.createTable(this.tableName, (table) => {
10
+ table.uuid('id').primary()
11
+ table.string('client_id').notNullable().references('client_id').inTable('oauth_clients').onDelete('CASCADE')
12
+ table.string('user_id').notNullable()
13
+ table.json('scopes').notNullable()
14
+ table.timestamp('created_at').notNullable()
15
+ table.timestamp('updated_at').notNullable()
16
+
17
+ table.unique(['client_id', 'user_id'])
18
+ })
19
+ }
20
+
21
+ async down() {
22
+ this.schema.dropTable(this.tableName)
23
+ }
24
+ }
@@ -0,0 +1,26 @@
1
+ {{{
2
+ exports: { migration: "import { BaseSchema } from '@adonisjs/lucid/schema'" }
3
+ }}}
4
+
5
+ export default class extends BaseSchema {
6
+ protected tableName = 'oauth_refresh_tokens'
7
+
8
+ async up() {
9
+ this.schema.createTable(this.tableName, (table) => {
10
+ table.uuid('id').primary()
11
+ table.string('token').notNullable().index()
12
+ table.string('access_token_id').notNullable()
13
+ table.string('client_id').notNullable().references('client_id').inTable('oauth_clients').onDelete('CASCADE')
14
+ table.string('user_id').notNullable()
15
+ table.json('scopes').notNullable()
16
+ table.timestamp('expires_at').notNullable()
17
+ table.timestamp('revoked_at').nullable()
18
+ table.timestamp('created_at').notNullable()
19
+ table.timestamp('updated_at').notNullable()
20
+ })
21
+ }
22
+
23
+ async down() {
24
+ this.schema.dropTable(this.tableName)
25
+ }
26
+ }
@@ -0,0 +1,172 @@
1
+ import "./decorate-2_8Ex77k.js";
2
+ import { t as OAuthAccessToken } from "./oauth_access_token-BpG8sq-c.js";
3
+ import { a as OAuthRefreshToken, i as OAuthAuthorizationCode, t as SesameManager } from "./sesame_manager-B4tO2PLO.js";
4
+ import { a as E_INVALID_REQUEST, i as E_INVALID_GRANT, l as E_UNSUPPORTED_GRANT_TYPE, n as E_INVALID_CLIENT, o as E_INVALID_SCOPE } from "./oauth_error-BQPqV-MV.js";
5
+ import { t as OAuthClient } from "./oauth_client-eh0e5ql-.js";
6
+ import { t as TokenService } from "./token_service-Czz9v5GI.js";
7
+ import { t as ClientService } from "./client_service-B9fD3ZGe.js";
8
+ import { DateTime } from "luxon";
9
+ import { createHash } from "node:crypto";
10
+ import vine from "@vinejs/vine";
11
+ const codeVerifierValidator = vine.create({ code_verifier: vine.string().minLength(43).maxLength(128).regex(/^[A-Za-z0-9\-._~]+$/) });
12
+ async function handleAuthorizationCodeGrant(ctx, manager) {
13
+ const tokenService = new TokenService(manager);
14
+ const clientService = new ClientService();
15
+ const body = ctx.request.body();
16
+ const code = body.code;
17
+ const redirectUri = body.redirect_uri;
18
+ const codeVerifier = body.code_verifier;
19
+ if (!code) throw new E_INVALID_REQUEST("Missing required parameter: code");
20
+ if (!redirectUri) throw new E_INVALID_REQUEST("Missing required parameter: redirect_uri");
21
+ const credentials = clientService.extractCredentials({
22
+ authorizationHeader: ctx.request.header("authorization"),
23
+ bodyClientId: body.client_id,
24
+ bodyClientSecret: body.client_secret
25
+ });
26
+ if (!credentials) throw new E_INVALID_CLIENT("Missing client credentials");
27
+ const client = await OAuthClient.query().where("clientId", credentials.clientId).first();
28
+ if (!client) throw new E_INVALID_CLIENT("Client not found");
29
+ if (client.isDisabled) throw new E_INVALID_CLIENT("Client is disabled");
30
+ if (!client.isPublic) {
31
+ if (!credentials.clientSecret) throw new E_INVALID_CLIENT("Missing client secret");
32
+ if (!clientService.verifySecret(credentials.clientSecret, client.clientSecret)) throw new E_INVALID_CLIENT("Invalid client secret");
33
+ }
34
+ if (!client.grantTypes.includes("authorization_code")) throw new E_INVALID_CLIENT("Client is not allowed to use the authorization_code grant");
35
+ const hashedCode = tokenService.hashToken(code);
36
+ const authCode = await OAuthAuthorizationCode.query().where("code", hashedCode).where("clientId", client.clientId).first();
37
+ if (!authCode) throw new E_INVALID_GRANT("Authorization code not found");
38
+ if (authCode.expiresAt < DateTime.now()) {
39
+ await OAuthAuthorizationCode.query().where("id", authCode.id).delete();
40
+ throw new E_INVALID_GRANT("Authorization code has expired");
41
+ }
42
+ if (authCode.redirectUri !== redirectUri) throw new E_INVALID_GRANT("Redirect URI mismatch");
43
+ const deleteResult = await OAuthAuthorizationCode.query().where("id", authCode.id).delete();
44
+ if ((Array.isArray(deleteResult) ? Number(deleteResult[0] ?? 0) : Number(deleteResult)) !== 1) throw new E_INVALID_GRANT("Authorization code has already been consumed");
45
+ const [verifierError] = await codeVerifierValidator.tryValidate({ code_verifier: codeVerifier });
46
+ if (verifierError) throw new E_INVALID_REQUEST("code_verifier must be 43-128 characters using only [A-Za-z0-9-._~] (RFC 7636 §4.1)");
47
+ if (!authCode.codeChallenge) throw new E_INVALID_GRANT("Authorization code is missing PKCE challenge");
48
+ if (createHash("sha256").update(codeVerifier).digest("base64url") !== authCode.codeChallenge) throw new E_INVALID_GRANT("PKCE verification failed");
49
+ clientService.validateClientScopes(authCode.scopes, client.scopes);
50
+ const { raw: accessTokenRaw, hash: tokenHash, expiresAt } = tokenService.createAccessToken();
51
+ await OAuthAccessToken.create({
52
+ id: crypto.randomUUID(),
53
+ tokenHash,
54
+ clientId: client.clientId,
55
+ userId: authCode.userId,
56
+ scopes: authCode.scopes,
57
+ expiresAt: DateTime.fromJSDate(expiresAt)
58
+ });
59
+ let refreshTokenRaw;
60
+ if (authCode.scopes.includes("offline_access")) {
61
+ const { raw, hash } = tokenService.createRefreshToken();
62
+ const refreshTtl = manager.parseTtl(manager.config.refreshTokenTtl);
63
+ await OAuthRefreshToken.create({
64
+ id: crypto.randomUUID(),
65
+ token: hash,
66
+ accessTokenId: tokenHash,
67
+ clientId: client.clientId,
68
+ userId: authCode.userId,
69
+ scopes: authCode.scopes,
70
+ expiresAt: DateTime.now().plus({ seconds: refreshTtl })
71
+ });
72
+ refreshTokenRaw = raw;
73
+ }
74
+ return {
75
+ access_token: accessTokenRaw,
76
+ token_type: "Bearer",
77
+ expires_in: manager.parseTtl(manager.config.accessTokenTtl),
78
+ scope: authCode.scopes.join(" "),
79
+ ...refreshTokenRaw ? { refresh_token: refreshTokenRaw } : {}
80
+ };
81
+ }
82
+ async function handleRefreshTokenGrant(ctx, manager) {
83
+ const tokenService = new TokenService(manager);
84
+ const clientService = new ClientService();
85
+ const body = ctx.request.body();
86
+ const refreshTokenRaw = body.refresh_token;
87
+ if (!refreshTokenRaw) throw new E_INVALID_REQUEST("Missing required parameter: refresh_token");
88
+ const credentials = clientService.extractCredentials({
89
+ authorizationHeader: ctx.request.header("authorization"),
90
+ bodyClientId: body.client_id,
91
+ bodyClientSecret: body.client_secret
92
+ });
93
+ if (!credentials) throw new E_INVALID_CLIENT("Missing client credentials");
94
+ const client = await OAuthClient.query().where("clientId", credentials.clientId).first();
95
+ if (!client) throw new E_INVALID_CLIENT("Client not found");
96
+ if (client.isDisabled) throw new E_INVALID_CLIENT("Client is disabled");
97
+ if (!client.isPublic) {
98
+ if (!credentials.clientSecret) throw new E_INVALID_CLIENT("Missing client secret");
99
+ if (!clientService.verifySecret(credentials.clientSecret, client.clientSecret)) throw new E_INVALID_CLIENT("Invalid client secret");
100
+ }
101
+ if (!client.grantTypes.includes("refresh_token")) throw new E_INVALID_CLIENT("Client is not allowed to use the refresh_token grant");
102
+ const hashedToken = tokenService.hashToken(refreshTokenRaw);
103
+ const refreshToken = await OAuthRefreshToken.query().where("token", hashedToken).where("clientId", client.clientId).first();
104
+ if (!refreshToken) throw new E_INVALID_GRANT("Refresh token not found");
105
+ if (refreshToken.revokedAt) {
106
+ await OAuthRefreshToken.query().where("clientId", client.clientId).where("userId", refreshToken.userId).delete();
107
+ await OAuthAccessToken.query().where("clientId", client.clientId).where("userId", refreshToken.userId).whereNull("revokedAt").update({ revokedAt: DateTime.now().toSQL() });
108
+ throw new E_INVALID_GRANT("Refresh token has been revoked (possible replay attack)");
109
+ }
110
+ if (refreshToken.expiresAt < DateTime.now()) throw new E_INVALID_GRANT("Refresh token has expired");
111
+ const requestedScope = body.scope;
112
+ let scopes = refreshToken.scopes;
113
+ if (requestedScope) {
114
+ const requested = requestedScope.split(" ");
115
+ const originalSet = new Set(refreshToken.scopes);
116
+ const invalid = requested.filter((s) => !originalSet.has(s));
117
+ if (invalid.length > 0) throw new E_INVALID_SCOPE(`Scope not in original grant: ${invalid.join(", ")}`);
118
+ scopes = requested;
119
+ }
120
+ clientService.validateClientScopes(scopes, client.scopes);
121
+ const revokedAt = DateTime.now();
122
+ const updateResult = await OAuthRefreshToken.query().where("id", refreshToken.id).whereNull("revokedAt").update({ revokedAt: revokedAt.toSQL() });
123
+ if ((Array.isArray(updateResult) ? Number(updateResult[0] ?? 0) : Number(updateResult)) !== 1) throw new E_INVALID_GRANT("Refresh token has already been consumed");
124
+ await OAuthAccessToken.query().where("tokenHash", refreshToken.accessTokenId).whereNull("revokedAt").update({ revokedAt: revokedAt.toSQL() });
125
+ const { raw: accessTokenRaw, hash: tokenHash, expiresAt } = tokenService.createAccessToken();
126
+ await OAuthAccessToken.create({
127
+ id: crypto.randomUUID(),
128
+ tokenHash,
129
+ clientId: client.clientId,
130
+ userId: refreshToken.userId,
131
+ scopes,
132
+ expiresAt: DateTime.fromJSDate(expiresAt)
133
+ });
134
+ const { raw: newRefreshTokenRaw, hash: newRefreshTokenHash } = tokenService.createRefreshToken();
135
+ const refreshTtl = manager.parseTtl(manager.config.refreshTokenTtl);
136
+ await OAuthRefreshToken.create({
137
+ id: crypto.randomUUID(),
138
+ token: newRefreshTokenHash,
139
+ accessTokenId: tokenHash,
140
+ clientId: client.clientId,
141
+ userId: refreshToken.userId,
142
+ scopes,
143
+ expiresAt: DateTime.now().plus({ seconds: refreshTtl })
144
+ });
145
+ return {
146
+ access_token: accessTokenRaw,
147
+ token_type: "Bearer",
148
+ expires_in: manager.parseTtl(manager.config.accessTokenTtl),
149
+ scope: scopes.join(" "),
150
+ refresh_token: newRefreshTokenRaw
151
+ };
152
+ }
153
+ const grantHandlers = {
154
+ authorization_code: handleAuthorizationCodeGrant,
155
+ refresh_token: handleRefreshTokenGrant
156
+ };
157
+ var TokenController = class TokenController {
158
+ static validator = vine.create({ grant_type: vine.string() });
159
+ async handle(ctx) {
160
+ const manager = await ctx.containerResolver.make(SesameManager);
161
+ const [error, body] = await TokenController.validator.tryValidate(ctx.request.body());
162
+ if (error) throw new E_UNSUPPORTED_GRANT_TYPE("Missing required parameter: grant_type");
163
+ if (!manager.isGrantTypeEnabled(body.grant_type)) throw new E_UNSUPPORTED_GRANT_TYPE(`Grant type "${body.grant_type}" is not enabled`);
164
+ const handler = grantHandlers[body.grant_type];
165
+ if (!handler) throw new E_UNSUPPORTED_GRANT_TYPE(`Unsupported grant type: ${body.grant_type}`);
166
+ const result = await handler(ctx, manager);
167
+ ctx.response.header("Cache-Control", "no-store");
168
+ ctx.response.header("Pragma", "no-cache");
169
+ return result;
170
+ }
171
+ };
172
+ export { TokenController as default };
@@ -0,0 +1,30 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ var TokenService = class {
3
+ #manager;
4
+ constructor(manager) {
5
+ this.#manager = manager;
6
+ }
7
+ createAccessToken() {
8
+ const raw = this.generateOpaqueToken();
9
+ const ttlSeconds = this.#manager.parseTtl(this.#manager.config.accessTokenTtl);
10
+ return {
11
+ raw,
12
+ hash: this.hashToken(raw),
13
+ expiresAt: new Date(Date.now() + ttlSeconds * 1e3)
14
+ };
15
+ }
16
+ createRefreshToken() {
17
+ const raw = this.generateOpaqueToken();
18
+ return {
19
+ raw,
20
+ hash: this.hashToken(raw)
21
+ };
22
+ }
23
+ generateOpaqueToken() {
24
+ return randomBytes(32).toString("base64url");
25
+ }
26
+ hashToken(token) {
27
+ return createHash("sha256").update(token).digest("base64url");
28
+ }
29
+ };
30
+ export { TokenService as t };
@@ -0,0 +1,150 @@
1
+ import { t as OAuthAccessToken } from "./oauth_access_token-BpG8sq-c.js";
2
+ import { t as OAuthClient } from "./oauth_client-eh0e5ql-.js";
3
+ import { t as TokenService } from "./token_service-Czz9v5GI.js";
4
+ import { DateTime } from "luxon";
5
+ import { RuntimeException } from "@adonisjs/core/exceptions";
6
+ import { errors } from "@adonisjs/auth";
7
+ var OAuthGuard = class {
8
+ driverName = "oauth";
9
+ authenticationAttempted = false;
10
+ isAuthenticated = false;
11
+ user;
12
+ scopes = [];
13
+ clientId;
14
+ #name;
15
+ #ctx;
16
+ #emitter;
17
+ #userProvider;
18
+ #manager;
19
+ #resource;
20
+ constructor(name, ctx, emitter, userProvider, manager, resource) {
21
+ this.#name = name;
22
+ this.#ctx = ctx;
23
+ this.#emitter = emitter;
24
+ this.#userProvider = userProvider;
25
+ this.#manager = manager;
26
+ this.#resource = resource;
27
+ }
28
+ #extractBearerToken() {
29
+ const [type, token] = (this.#ctx.request.header("authorization") ?? "").split(" ");
30
+ if (!type || type.toLowerCase() !== "bearer" || !token) throw this.#authenticationFailed("Missing Bearer token");
31
+ return token;
32
+ }
33
+ #authenticationFailed(description, options) {
34
+ const suffix = this.#resource ?? "";
35
+ let header = `Bearer resource_metadata="${`${this.#manager.config.issuer}/.well-known/oauth-protected-resource${suffix}`}"`;
36
+ if (options?.includeError) header += `, error="invalid_token", error_description="${description}"`;
37
+ this.#ctx.response.header("WWW-Authenticate", header);
38
+ const error = new errors.E_UNAUTHORIZED_ACCESS(description, { guardDriverName: this.driverName });
39
+ this.#emitter.emit("oauth_auth:authentication_failed", {
40
+ ctx: this.#ctx,
41
+ guardName: this.#name,
42
+ error
43
+ });
44
+ return error;
45
+ }
46
+ getUserOrFail() {
47
+ if (!this.user) throw new errors.E_UNAUTHORIZED_ACCESS("Unauthorized access", { guardDriverName: this.driverName });
48
+ return this.user;
49
+ }
50
+ async authenticate() {
51
+ if (this.authenticationAttempted) return this.getUserOrFail();
52
+ this.authenticationAttempted = true;
53
+ this.#emitter.emit("oauth_auth:authentication_attempted", {
54
+ ctx: this.#ctx,
55
+ guardName: this.#name
56
+ });
57
+ const rawToken = this.#extractBearerToken();
58
+ const tokenService = new TokenService(this.#manager);
59
+ const includeError = { includeError: true };
60
+ const hashed = tokenService.hashToken(rawToken);
61
+ const record = await OAuthAccessToken.query().where("tokenHash", hashed).first();
62
+ if (!record) throw this.#authenticationFailed("Invalid or expired token", includeError);
63
+ if (record.revokedAt) throw this.#authenticationFailed("Token has been revoked", includeError);
64
+ if (record.expiresAt.toJSDate() < /* @__PURE__ */ new Date()) throw this.#authenticationFailed("Invalid or expired token", includeError);
65
+ if (!record.userId) throw this.#authenticationFailed("M2M tokens are not supported", includeError);
66
+ const providerUser = await this.#userProvider.findById(record.userId);
67
+ if (!providerUser) throw this.#authenticationFailed("User not found", includeError);
68
+ this.isAuthenticated = true;
69
+ this.user = providerUser.getOriginal();
70
+ this.scopes = record.scopes;
71
+ this.clientId = record.clientId;
72
+ this.#emitter.emit("oauth_auth:authentication_succeeded", {
73
+ ctx: this.#ctx,
74
+ guardName: this.#name,
75
+ user: this.user
76
+ });
77
+ return this.user;
78
+ }
79
+ async check() {
80
+ try {
81
+ await this.authenticate();
82
+ return true;
83
+ } catch (error) {
84
+ if (error instanceof errors.E_UNAUTHORIZED_ACCESS) return false;
85
+ throw error;
86
+ }
87
+ }
88
+ hasScope(...scopes) {
89
+ return scopes.every((s) => this.scopes.includes(s));
90
+ }
91
+ hasAnyScope(...scopes) {
92
+ return scopes.some((s) => this.scopes.includes(s));
93
+ }
94
+ async authenticateAsClient(user) {
95
+ const tokenService = new TokenService(this.#manager);
96
+ const defaultScopes = this.#manager.config.defaultScopes;
97
+ const testClient = await OAuthClient.firstOrCreate({ clientId: "__test_client__" }, {
98
+ id: crypto.randomUUID(),
99
+ clientId: "__test_client__",
100
+ name: "Test Client",
101
+ redirectUris: ["http://localhost/callback"],
102
+ grantTypes: ["authorization_code"],
103
+ scopes: defaultScopes,
104
+ isPublic: true,
105
+ requirePkce: false
106
+ });
107
+ const userId = String(user.id ?? user.getId?.() ?? "test-user");
108
+ const { raw, hash, expiresAt } = tokenService.createAccessToken();
109
+ await OAuthAccessToken.create({
110
+ id: crypto.randomUUID(),
111
+ tokenHash: hash,
112
+ clientId: testClient.clientId,
113
+ userId,
114
+ scopes: defaultScopes,
115
+ expiresAt: DateTime.fromJSDate(expiresAt)
116
+ });
117
+ return { headers: { authorization: `Bearer ${raw}` } };
118
+ }
119
+ };
120
+ var OAuthLucidUserProvider = class {
121
+ #model;
122
+ #options;
123
+ constructor(options) {
124
+ this.#options = options;
125
+ }
126
+ async #getModel() {
127
+ if (this.#model && !("hot" in import.meta)) return this.#model;
128
+ this.#model = (await this.#options.model()).default;
129
+ return this.#model;
130
+ }
131
+ async createUserForGuard(user) {
132
+ const model = await this.#getModel();
133
+ if (user instanceof model === false) throw new RuntimeException(`Invalid user object. It must be an instance of the "${model.name}" model`);
134
+ return {
135
+ getId() {
136
+ if (!user.$primaryKeyValue) throw new RuntimeException(`Cannot use "${model.name}" model for authentication. The value of column "${model.primaryKey}" is undefined or null`);
137
+ return user.$primaryKeyValue;
138
+ },
139
+ getOriginal() {
140
+ return user;
141
+ }
142
+ };
143
+ }
144
+ async findById(identifier) {
145
+ const user = await (await this.#getModel()).find(identifier);
146
+ if (!user) return null;
147
+ return this.createUserForGuard(user);
148
+ }
149
+ };
150
+ export { OAuthGuard as n, OAuthLucidUserProvider as t };