@julr/sesame 0.3.0 → 0.4.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.
package/README.md CHANGED
@@ -84,11 +84,11 @@ import sesame from '@julr/sesame/services/main'
84
84
 
85
85
  // OAuth endpoints under /oauth
86
86
  router.group(() => {
87
- sesame.registerRoutes(router)
87
+ sesame.registerRoutes()
88
88
  }).prefix('/oauth')
89
89
 
90
90
  // Discovery endpoints at the root
91
- sesame.registerWellKnownRoutes(router)
91
+ sesame.registerWellKnownRoutes()
92
92
  ```
93
93
 
94
94
  This registers the following endpoints:
@@ -159,7 +159,7 @@ router
159
159
  For MCP (Model Context Protocol) servers, register per-resource discovery:
160
160
 
161
161
  ```ts
162
- sesame.registerProtectedResource(router, {
162
+ sesame.registerProtectedResource({
163
163
  resource: '/api/mcp',
164
164
  scopes: ['read:mcp'],
165
165
  })
@@ -1,10 +1,10 @@
1
- import { i as OAuthAuthorizationCode, n as OAuthPendingAuthorizationRequest, r as OAuthConsent, t as SesameManager } from "./sesame_manager-2iG9p7_F.js";
1
+ import { i as OAuthAuthorizationCode, n as OAuthPendingAuthorizationRequest, r as OAuthConsent, t as SesameManager } from "./sesame_manager-BIHBwkqK.js";
2
2
  import "./decorate-BKZEjPRg.js";
3
3
  import "./oauth_access_token-bsoM5KeU.js";
4
4
  import { d as E_UNSUPPORTED_RESPONSE_TYPE, o as E_INVALID_REQUEST, r as E_INVALID_CLIENT } from "./oauth_error-CnJ3L8tf.js";
5
5
  import { t as OAuthClient } from "./oauth_client-BIoY5jBR.js";
6
6
  import { t as TokenService } from "./token_service-fhoA4slP.js";
7
- import { t as ClientService } from "./client_service-BqPSlaTS.js";
7
+ import { t as ClientService } from "./client_service-BD1CDQuC.js";
8
8
  import { DateTime } from "luxon";
9
9
  import string from "@adonisjs/core/helpers/string";
10
10
  import vine from "@vinejs/vine";
@@ -130,6 +130,7 @@ var AuthorizeController = class AuthorizeController {
130
130
  codeChallengeMethod: query.code_challenge_method
131
131
  });
132
132
  this.#copyAuthorizeDisplayParams(params, ctx.request.qs());
133
+ params.set("scope", requestedScopes.join(" "));
133
134
  const consentPage = this.#resolvePageUrl(manager.config.consentPage, ctx, params);
134
135
  return ctx.response.redirect().toPath(consentPage);
135
136
  }
@@ -1,4 +1,5 @@
1
- import { s as E_INVALID_SCOPE } from "./oauth_error-CnJ3L8tf.js";
1
+ import { o as E_INVALID_REQUEST, r as E_INVALID_CLIENT, s as E_INVALID_SCOPE } from "./oauth_error-CnJ3L8tf.js";
2
+ import { t as OAuthClient } from "./oauth_client-BIoY5jBR.js";
2
3
  import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
3
4
  var ClientService = class {
4
5
  parseBasicAuth(header) {
@@ -16,16 +17,25 @@ var ClientService = class {
16
17
  }
17
18
  }
18
19
  extractCredentials(options) {
19
- if (options.authorizationHeader) {
20
- const basic = this.parseBasicAuth(options.authorizationHeader);
21
- if (basic) return basic;
22
- }
20
+ const basic = options.authorizationHeader ? this.parseBasicAuth(options.authorizationHeader) : null;
21
+ if (basic && options.bodyClientId) throw new E_INVALID_REQUEST("Multiple client authentication methods are not allowed");
22
+ if (basic) return basic;
23
23
  if (options.bodyClientId) return {
24
24
  clientId: options.bodyClientId,
25
25
  clientSecret: options.bodyClientSecret
26
26
  };
27
27
  return null;
28
28
  }
29
+ async authenticateClient(options) {
30
+ const credentials = this.extractCredentials(options);
31
+ if (!credentials) throw new E_INVALID_CLIENT("Client authentication failed");
32
+ const client = await OAuthClient.query().where("clientId", credentials.clientId).first();
33
+ if (!client || client.isDisabled) throw new E_INVALID_CLIENT("Client authentication failed");
34
+ if (!client.isPublic) {
35
+ if (!credentials.clientSecret || !this.verifySecret(credentials.clientSecret, client.clientSecret)) throw new E_INVALID_CLIENT("Client authentication failed");
36
+ }
37
+ return client;
38
+ }
29
39
  validateClientScopes(requestedScopes, clientScopes) {
30
40
  if (clientScopes.length === 0 && requestedScopes.length > 0) throw new E_INVALID_SCOPE(`Scope not allowed: ${requestedScopes.join(", ")}`);
31
41
  const allowedSet = new Set(clientScopes);
@@ -1,4 +1,4 @@
1
- import { t as SesameManager } from "../sesame_manager-2iG9p7_F.js";
1
+ import { t as SesameManager } from "../sesame_manager-BIHBwkqK.js";
2
2
  import { t as __decorate } from "../decorate-BKZEjPRg.js";
3
3
  import "../oauth_access_token-bsoM5KeU.js";
4
4
  import { BaseCommand, flags } from "@adonisjs/core/ace";
@@ -1,4 +1,4 @@
1
- import { i as OAuthAuthorizationCode, n as OAuthPendingAuthorizationRequest, r as OAuthConsent, t as SesameManager } from "./sesame_manager-2iG9p7_F.js";
1
+ import { i as OAuthAuthorizationCode, n as OAuthPendingAuthorizationRequest, r as OAuthConsent, t as SesameManager } from "./sesame_manager-BIHBwkqK.js";
2
2
  import "./decorate-BKZEjPRg.js";
3
3
  import "./oauth_access_token-bsoM5KeU.js";
4
4
  import { a as E_INVALID_GRANT, o as E_INVALID_REQUEST, r as E_INVALID_CLIENT } from "./oauth_error-CnJ3L8tf.js";
package/build/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { configure } from "./configure.js";
2
- import { a as OAuthRefreshToken, i as OAuthAuthorizationCode, r as OAuthConsent, t as SesameManager } from "./sesame_manager-2iG9p7_F.js";
2
+ import { a as OAuthRefreshToken, i as OAuthAuthorizationCode, r as OAuthConsent, t as SesameManager } from "./sesame_manager-BIHBwkqK.js";
3
3
  import "./decorate-BKZEjPRg.js";
4
4
  import { t as OAuthAccessToken } from "./oauth_access_token-bsoM5KeU.js";
5
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
6
  import { t as OAuthClient } from "./oauth_client-BIoY5jBR.js";
7
7
  import "./token_service-fhoA4slP.js";
8
- import { i as OAuthGuard, n as oauthUserProvider, r as OAuthLucidUserProvider, t as oauthGuard } from "./main-C6VqRjlK.js";
8
+ import { i as OAuthGuard, n as oauthUserProvider, r as OAuthLucidUserProvider, t as oauthGuard } from "./main-BJRXMGrZ.js";
9
9
  function defineConfig(config) {
10
10
  return {
11
11
  issuer: config.issuer,
@@ -1,28 +1,19 @@
1
- import { a as OAuthRefreshToken, t as SesameManager } from "./sesame_manager-2iG9p7_F.js";
1
+ import { a as OAuthRefreshToken, t as SesameManager } from "./sesame_manager-BIHBwkqK.js";
2
2
  import "./decorate-BKZEjPRg.js";
3
3
  import { t as OAuthAccessToken } from "./oauth_access_token-bsoM5KeU.js";
4
- import { r as E_INVALID_CLIENT } from "./oauth_error-CnJ3L8tf.js";
5
- import { t as OAuthClient } from "./oauth_client-BIoY5jBR.js";
4
+ import "./oauth_error-CnJ3L8tf.js";
5
+ import "./oauth_client-BIoY5jBR.js";
6
6
  import { t as TokenService } from "./token_service-fhoA4slP.js";
7
- import { t as ClientService } from "./client_service-BqPSlaTS.js";
7
+ import { t as ClientService } from "./client_service-BD1CDQuC.js";
8
8
  const INACTIVE = { active: false };
9
9
  var IntrospectController = class {
10
10
  async handle(ctx) {
11
11
  const manager = await ctx.containerResolver.make(SesameManager);
12
- const clientService = new ClientService();
13
- const credentials = clientService.extractCredentials({
12
+ const client = await new ClientService().authenticateClient({
14
13
  authorizationHeader: ctx.request.header("authorization"),
15
14
  bodyClientId: ctx.request.body().client_id,
16
15
  bodyClientSecret: ctx.request.body().client_secret
17
16
  });
18
- if (!credentials) throw new E_INVALID_CLIENT("Client authentication required");
19
- const client = await OAuthClient.query().where("clientId", credentials.clientId).first();
20
- if (!client) throw new E_INVALID_CLIENT("Client not found");
21
- if (client.isDisabled) throw new E_INVALID_CLIENT("Client is disabled");
22
- if (!client.isPublic) {
23
- if (!credentials.clientSecret) throw new E_INVALID_CLIENT("Missing client secret");
24
- if (!clientService.verifySecret(credentials.clientSecret, client.clientSecret)) throw new E_INVALID_CLIENT("Invalid client secret");
25
- }
26
17
  const token = ctx.request.body().token;
27
18
  if (!token) return INACTIVE;
28
19
  const tokenTypeHint = ctx.request.body().token_type_hint;
@@ -60,11 +60,11 @@ var OAuthGuard = class {
60
60
  const hashed = tokenService.hashToken(rawToken);
61
61
  const record = await OAuthAccessToken.query().where("tokenHash", hashed).first();
62
62
  if (!record) throw this.#authenticationFailed("Invalid or expired token", includeError);
63
- if (record.revokedAt) throw this.#authenticationFailed("Token has been revoked", includeError);
63
+ if (record.revokedAt) throw this.#authenticationFailed("Invalid or expired token", includeError);
64
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);
65
+ if (!record.userId) throw this.#authenticationFailed("Invalid or expired token", includeError);
66
66
  const providerUser = await this.#userProvider.findById(record.userId);
67
- if (!providerUser) throw this.#authenticationFailed("User not found", includeError);
67
+ if (!providerUser) throw this.#authenticationFailed("Invalid or expired token", includeError);
68
68
  this.isAuthenticated = true;
69
69
  this.user = providerUser.getOriginal();
70
70
  this.scopes = record.scopes;
@@ -150,7 +150,7 @@ var OAuthLucidUserProvider = class {
150
150
  function oauthGuard(config) {
151
151
  return { async resolver(name, app) {
152
152
  const emitter = await app.container.make("emitter");
153
- const { SesameManager } = await import("./sesame_manager-BLmhC1jV.js");
153
+ const { SesameManager } = await import("./sesame_manager-BWqeaqn9.js");
154
154
  const manager = await app.container.make(SesameManager);
155
155
  return (ctx) => new OAuthGuard(name, ctx, emitter, config.provider, manager, config.resource);
156
156
  } };
@@ -1,4 +1,4 @@
1
- import { t as SesameManager } from "./sesame_manager-2iG9p7_F.js";
1
+ import { t as SesameManager } from "./sesame_manager-BIHBwkqK.js";
2
2
  import "./decorate-BKZEjPRg.js";
3
3
  import "./oauth_access_token-bsoM5KeU.js";
4
4
  import { l as E_SERVER_ERROR } from "./oauth_error-CnJ3L8tf.js";
@@ -43,8 +43,16 @@ var MetadataController = class {
43
43
  "client_secret_basic",
44
44
  "client_secret_post"
45
45
  ],
46
- introspection_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post"],
47
- revocation_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post"],
46
+ introspection_endpoint_auth_methods_supported: [
47
+ "none",
48
+ "client_secret_basic",
49
+ "client_secret_post"
50
+ ],
51
+ revocation_endpoint_auth_methods_supported: [
52
+ "none",
53
+ "client_secret_basic",
54
+ "client_secret_post"
55
+ ],
48
56
  code_challenge_methods_supported: ["S256"],
49
57
  authorization_response_iss_parameter_supported: true
50
58
  };
@@ -1,4 +1,4 @@
1
- import { t as SesameManager } from "../sesame_manager-2iG9p7_F.js";
1
+ import { t as SesameManager } from "../sesame_manager-BIHBwkqK.js";
2
2
  import "../decorate-BKZEjPRg.js";
3
3
  import "../oauth_access_token-bsoM5KeU.js";
4
4
  var SesameProvider = class {
@@ -6,8 +6,8 @@ var SesameProvider = class {
6
6
  this.app = app;
7
7
  }
8
8
  register() {
9
- this.app.container.singleton(SesameManager, () => {
10
- return new SesameManager(this.app.config.get("sesame"));
9
+ this.app.container.singleton(SesameManager, async () => {
10
+ return new SesameManager(this.app.config.get("sesame"), await this.app.container.make("router"));
11
11
  });
12
12
  }
13
13
  };
@@ -1,9 +1,9 @@
1
- import { t as SesameManager } from "./sesame_manager-2iG9p7_F.js";
1
+ import { t as SesameManager } from "./sesame_manager-BIHBwkqK.js";
2
2
  import "./decorate-BKZEjPRg.js";
3
3
  import "./oauth_access_token-bsoM5KeU.js";
4
4
  import { i as E_INVALID_CLIENT_METADATA, o as E_INVALID_REQUEST, s as E_INVALID_SCOPE, t as E_ACCESS_DENIED } from "./oauth_error-CnJ3L8tf.js";
5
5
  import { t as OAuthClient } from "./oauth_client-BIoY5jBR.js";
6
- import { t as ClientService } from "./client_service-BqPSlaTS.js";
6
+ import { t as ClientService } from "./client_service-BD1CDQuC.js";
7
7
  import vine from "@vinejs/vine";
8
8
  const DANGEROUS_SCHEMES = [
9
9
  "javascript:",
@@ -35,24 +35,11 @@ const redirectUriRule = vine.createRule((value, _options, field) => {
35
35
  });
36
36
  const metadataUriRule = vine.createRule((value, _options, field) => {
37
37
  const parsed = new URL(value);
38
- if (DANGEROUS_SCHEMES.includes(parsed.protocol)) {
39
- field.report("{{ field }} uses a disallowed scheme", "metadataUri", field);
40
- return;
41
- }
42
- const redirectUris = field.data.redirect_uris ?? [];
43
- const redirectOrigins = new Set(redirectUris.map((u) => {
44
- try {
45
- return new URL(u);
46
- } catch {
47
- return null;
48
- }
49
- }).filter(Boolean).map((u) => `${u.protocol}//${u.hostname}`));
50
- const metaOrigin = `${parsed.protocol}//${parsed.hostname}`;
51
- if (!redirectOrigins.has(metaOrigin)) field.report("{{ field }} host and scheme must match at least one redirect_uri", "metadataUri", field);
38
+ if (DANGEROUS_SCHEMES.includes(parsed.protocol)) field.report("{{ field }} uses a disallowed scheme", "metadataUri", field);
52
39
  });
53
40
  const metadataUrl = vine.string().url({ require_protocol: true }).use(metadataUriRule()).optional();
54
41
  var RegisterController = class RegisterController {
55
- static validator = vine.create({
42
+ static validator = vine.create(vine.object({
56
43
  redirect_uris: vine.array(vine.string().use(redirectUriRule())).minLength(1),
57
44
  token_endpoint_auth_method: vine.string().in([
58
45
  "client_secret_basic",
@@ -70,7 +57,7 @@ var RegisterController = class RegisterController {
70
57
  contacts: vine.array(vine.string().email()).optional(),
71
58
  software_id: vine.string().optional(),
72
59
  software_version: vine.string().optional()
73
- });
60
+ }).allowUnknownProperties());
74
61
  async handle(ctx) {
75
62
  const manager = await ctx.containerResolver.make(SesameManager);
76
63
  if (!manager.config.allowDynamicRegistration) throw new E_ACCESS_DENIED("Dynamic client registration is disabled");
@@ -1,28 +1,19 @@
1
- import { a as OAuthRefreshToken, t as SesameManager } from "./sesame_manager-2iG9p7_F.js";
1
+ import { a as OAuthRefreshToken, t as SesameManager } from "./sesame_manager-BIHBwkqK.js";
2
2
  import "./decorate-BKZEjPRg.js";
3
3
  import { t as OAuthAccessToken } from "./oauth_access_token-bsoM5KeU.js";
4
- import { r as E_INVALID_CLIENT } from "./oauth_error-CnJ3L8tf.js";
5
- import { t as OAuthClient } from "./oauth_client-BIoY5jBR.js";
4
+ import "./oauth_error-CnJ3L8tf.js";
5
+ import "./oauth_client-BIoY5jBR.js";
6
6
  import { t as TokenService } from "./token_service-fhoA4slP.js";
7
- import { t as ClientService } from "./client_service-BqPSlaTS.js";
7
+ import { t as ClientService } from "./client_service-BD1CDQuC.js";
8
8
  import { DateTime } from "luxon";
9
9
  var RevokeController = class {
10
10
  async handle(ctx) {
11
11
  const manager = await ctx.containerResolver.make(SesameManager);
12
- const clientService = new ClientService();
13
- const credentials = clientService.extractCredentials({
12
+ const client = await new ClientService().authenticateClient({
14
13
  authorizationHeader: ctx.request.header("authorization"),
15
14
  bodyClientId: ctx.request.body().client_id,
16
15
  bodyClientSecret: ctx.request.body().client_secret
17
16
  });
18
- if (!credentials) throw new E_INVALID_CLIENT("Client authentication required");
19
- const client = await OAuthClient.query().where("clientId", credentials.clientId).first();
20
- if (!client) throw new E_INVALID_CLIENT("Client not found");
21
- if (client.isDisabled) throw new E_INVALID_CLIENT("Client is disabled");
22
- if (!client.isPublic) {
23
- if (!credentials.clientSecret) throw new E_INVALID_CLIENT("Missing client secret");
24
- if (!clientService.verifySecret(credentials.clientSecret, client.clientSecret)) throw new E_INVALID_CLIENT("Invalid client secret");
25
- }
26
17
  const token = ctx.request.body().token;
27
18
  if (!token) return ctx.response.ok({});
28
19
  const tokenTypeHint = ctx.request.body().token_type_hint;
@@ -1,4 +1,4 @@
1
- import { t as SesameManager } from "../sesame_manager-2iG9p7_F.js";
1
+ import { t as SesameManager } from "../sesame_manager-BIHBwkqK.js";
2
2
  import "../decorate-BKZEjPRg.js";
3
3
  import "../oauth_access_token-bsoM5KeU.js";
4
4
  import app from "@adonisjs/core/services/app";
@@ -3,13 +3,13 @@ import { t as OAuthAccessToken } from "./oauth_access_token-bsoM5KeU.js";
3
3
  import { DateTime } from "luxon";
4
4
  import { BaseModel, column } from "@adonisjs/lucid/orm";
5
5
  const controllers = {
6
- token: () => import("./token_controller-qFmejgux.js"),
7
- authorize: () => import("./authorize_controller-ekxbVGSh.js"),
8
- consent: () => import("./consent_controller-CqE3-kWO.js"),
9
- introspect: () => import("./introspect_controller-Dx3Hz87G.js"),
10
- revoke: () => import("./revoke_controller-E15HmMCv.js"),
11
- register: () => import("./register_controller-B7IT9U1P.js"),
12
- metadata: () => import("./metadata_controller-CrR-rU1y.js"),
6
+ token: () => import("./token_controller-BijtWj5C.js"),
7
+ authorize: () => import("./authorize_controller-Bb54Bi_s.js"),
8
+ consent: () => import("./consent_controller-gMuS9nSx.js"),
9
+ introspect: () => import("./introspect_controller-bKg1T2Xx.js"),
10
+ revoke: () => import("./revoke_controller-dJtMG1zK.js"),
11
+ register: () => import("./register_controller-CnzYR-iH.js"),
12
+ metadata: () => import("./metadata_controller-CGaKw3UM.js"),
13
13
  clientInfo: () => import("./client_info_controller-BucHGx4u.js")
14
14
  };
15
15
  function registerOAuthRoutes(router) {
@@ -87,8 +87,10 @@ __decorate([column.dateTime()], OAuthPendingAuthorizationRequest.prototype, "exp
87
87
  __decorate([column.dateTime({ autoCreate: true })], OAuthPendingAuthorizationRequest.prototype, "createdAt", void 0);
88
88
  var SesameManager = class {
89
89
  #config;
90
- constructor(config) {
90
+ #router;
91
+ constructor(config, router) {
91
92
  this.#config = config;
93
+ this.#router = router;
92
94
  }
93
95
  get config() {
94
96
  return this.#config;
@@ -139,15 +141,15 @@ var SesameManager = class {
139
141
  pendingRequests
140
142
  };
141
143
  }
142
- registerRoutes(router) {
143
- registerOAuthRoutes(router);
144
+ registerRoutes() {
145
+ registerOAuthRoutes(this.#router);
144
146
  }
145
- registerWellKnownRoutes(router) {
146
- registerWellKnownRoutes(router);
147
+ registerWellKnownRoutes() {
148
+ registerWellKnownRoutes(this.#router);
147
149
  }
148
- registerProtectedResource(router, options) {
150
+ registerProtectedResource(options) {
149
151
  const wellKnownPath = `/.well-known/oauth-protected-resource${options.resource}`;
150
- router.get(wellKnownPath, async (ctx) => {
152
+ this.#router.get(wellKnownPath, async (ctx) => {
151
153
  ctx.response.header("Cache-Control", "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400");
152
154
  return {
153
155
  resource: `${this.#config.issuer}${options.resource}`,
@@ -1,4 +1,4 @@
1
- import { t as SesameManager } from "./sesame_manager-2iG9p7_F.js";
1
+ import { t as SesameManager } from "./sesame_manager-BIHBwkqK.js";
2
2
  import "./decorate-BKZEjPRg.js";
3
3
  import "./oauth_access_token-bsoM5KeU.js";
4
4
  export { SesameManager };
@@ -41,6 +41,8 @@ export default class RegisterController {
41
41
  software_id?: string | null | undefined;
42
42
  software_version?: string | null | undefined;
43
43
  redirect_uris: string[];
44
+ } & {
45
+ [K: string]: unknown;
44
46
  }, {
45
47
  scope?: string | undefined;
46
48
  token_endpoint_auth_method?: string | undefined;
@@ -55,20 +57,24 @@ export default class RegisterController {
55
57
  software_id?: string | undefined;
56
58
  software_version?: string | undefined;
57
59
  redirect_uris: string[];
60
+ } & {
61
+ [K: string]: unknown;
58
62
  }, {
63
+ grantTypes?: string[] | undefined;
59
64
  scope?: string | undefined;
60
- token_endpoint_auth_method?: string | undefined;
61
- grant_types?: string[] | undefined;
62
- response_types?: string[] | undefined;
63
- client_name?: string | undefined;
64
- client_uri?: string | undefined;
65
- logo_uri?: string | undefined;
66
- tos_uri?: string | undefined;
67
- policy_uri?: string | undefined;
68
65
  contacts?: string[] | undefined;
69
- software_id?: string | undefined;
70
- software_version?: string | undefined;
71
- redirect_uris: string[];
66
+ tokenEndpointAuthMethod?: string | undefined;
67
+ responseTypes?: string[] | undefined;
68
+ clientName?: string | undefined;
69
+ clientUri?: string | undefined;
70
+ logoUri?: string | undefined;
71
+ tosUri?: string | undefined;
72
+ policyUri?: string | undefined;
73
+ softwareId?: string | undefined;
74
+ softwareVersion?: string | undefined;
75
+ redirectUris: string[];
76
+ } & {
77
+ [K: string]: unknown;
72
78
  }>, Record<string, any> | undefined>;
73
79
  handle(ctx: HttpContext): Promise<{
74
80
  software_version?: string | undefined;
@@ -27,5 +27,12 @@ export declare class OAuthGuard<UserProvider extends OAuthUserProviderContract<u
27
27
  check(): Promise<boolean>;
28
28
  hasScope(...scopes: Scope[]): boolean;
29
29
  hasAnyScope(...scopes: Scope[]): boolean;
30
+ /**
31
+ * Used internally by Japa's `loginAs` helper during testing.
32
+ * Creates a test client and access token in DB, then returns
33
+ * the authorization headers for the test HTTP client to use.
34
+ *
35
+ * @see https://docs.adonisjs.com/guides/auth/custom-auth-guard#implementing-the-guard
36
+ */
30
37
  authenticateAsClient(user: UserProvider[typeof symbols.PROVIDER_REAL_USER]): Promise<AuthClientResponse>;
31
38
  }
@@ -2,5 +2,5 @@ import "../../decorate-BKZEjPRg.js";
2
2
  import "../../oauth_access_token-bsoM5KeU.js";
3
3
  import "../../oauth_client-BIoY5jBR.js";
4
4
  import "../../token_service-fhoA4slP.js";
5
- import { i as OAuthGuard, n as oauthUserProvider, r as OAuthLucidUserProvider, t as oauthGuard } from "../../main-C6VqRjlK.js";
5
+ import { i as OAuthGuard, n as oauthUserProvider, r as OAuthLucidUserProvider, t as oauthGuard } from "../../main-BJRXMGrZ.js";
6
6
  export { OAuthGuard, OAuthLucidUserProvider, oauthGuard, oauthUserProvider };
@@ -4,8 +4,16 @@
4
4
  */
5
5
  export declare const redirectUriRule: (options?: undefined) => import("@vinejs/vine/types").Validation<undefined>;
6
6
  /**
7
- * Blocks dangerous URI schemes and enforces same host+scheme
8
- * matching with redirect_uris (RFC 7591 §5).
7
+ * Blocks dangerous URI schemes for metadata URIs (client_uri, logo_uri, etc.).
8
+ *
9
+ * RFC 7591 §5 says the server MAY verify that metadata URIs match the
10
+ * host+scheme of redirect_uris, but this is NOT required.
11
+ *
12
+ * We intentionally skip this check because it breaks legitimate CLI/desktop clients
13
+ * (e.g. OpenCode, Claude Code) that use localhost redirect URIs but have a
14
+ * different `client_uri` pointing to their website.
15
+ *
16
+ * Maybe we can add an option to enable this check in the future if needed.
9
17
  *
10
18
  * @see https://datatracker.ietf.org/doc/html/rfc7591#section-5
11
19
  */
@@ -1,3 +1,4 @@
1
+ import { OAuthClient } from '../models/oauth_client.ts';
1
2
  /**
2
3
  * Extracted client credentials from a request.
3
4
  */
@@ -37,6 +38,16 @@ export declare class ClientService {
37
38
  bodyClientId?: string;
38
39
  bodyClientSecret?: string;
39
40
  }): ClientCredentials | null;
41
+ /**
42
+ * Authenticate a client from request credentials.
43
+ * Extracts credentials, looks up the client in DB, and verifies the secret
44
+ * for confidential clients.
45
+ */
46
+ authenticateClient(options: {
47
+ authorizationHeader?: string;
48
+ bodyClientId?: string;
49
+ bodyClientSecret?: string;
50
+ }): Promise<OAuthClient>;
40
51
  /**
41
52
  * Validate that requested scopes are within the client's
42
53
  * allowed scopes. Throws `E_INVALID_SCOPE` if any scope
@@ -14,7 +14,7 @@ export interface PurgeResult {
14
14
  */
15
15
  export declare class SesameManager {
16
16
  #private;
17
- constructor(config: ResolvedSesameConfig);
17
+ constructor(config: ResolvedSesameConfig, router: Router);
18
18
  get config(): ResolvedSesameConfig;
19
19
  /**
20
20
  * Check if a scope is registered in the server configuration.
@@ -71,18 +71,18 @@ export declare class SesameManager {
71
71
  * @example
72
72
  * ```ts
73
73
  * router.group(() => {
74
- * sesame.registerRoutes(router)
74
+ * sesame.registerRoutes()
75
75
  * }).prefix('/oauth')
76
76
  * ```
77
77
  */
78
- registerRoutes(router: Router): void;
78
+ registerRoutes(): void;
79
79
  /**
80
80
  * Register well-known discovery routes at the root level.
81
81
  *
82
82
  * Must be called outside any prefix group so endpoints
83
83
  * remain at `/.well-known/...`.
84
84
  */
85
- registerWellKnownRoutes(router: Router): void;
85
+ registerWellKnownRoutes(): void;
86
86
  /**
87
87
  * Register a `/.well-known/oauth-protected-resource` endpoint
88
88
  * for a specific resource path (RFC 9728). Useful for MCP
@@ -90,7 +90,7 @@ export declare class SesameManager {
90
90
  *
91
91
  * @see https://datatracker.ietf.org/doc/html/rfc9728
92
92
  */
93
- registerProtectedResource(router: Router, options: {
93
+ registerProtectedResource(options: {
94
94
  resource: string;
95
95
  scopes?: Scope[];
96
96
  }): void;
@@ -1,10 +1,10 @@
1
- import { a as OAuthRefreshToken, i as OAuthAuthorizationCode, t as SesameManager } from "./sesame_manager-2iG9p7_F.js";
1
+ import { a as OAuthRefreshToken, i as OAuthAuthorizationCode, t as SesameManager } from "./sesame_manager-BIHBwkqK.js";
2
2
  import "./decorate-BKZEjPRg.js";
3
3
  import { t as OAuthAccessToken } from "./oauth_access_token-bsoM5KeU.js";
4
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 { t as OAuthClient } from "./oauth_client-BIoY5jBR.js";
5
+ import "./oauth_client-BIoY5jBR.js";
6
6
  import { t as TokenService } from "./token_service-fhoA4slP.js";
7
- import { t as ClientService } from "./client_service-BqPSlaTS.js";
7
+ import { t as ClientService } from "./client_service-BD1CDQuC.js";
8
8
  import { DateTime } from "luxon";
9
9
  import { createHash } from "node:crypto";
10
10
  import string from "@adonisjs/core/helpers/string";
@@ -19,19 +19,11 @@ async function handleAuthorizationCodeGrant(ctx, manager) {
19
19
  const codeVerifier = body.code_verifier;
20
20
  if (!code) throw new E_INVALID_REQUEST("Missing required parameter: code");
21
21
  if (!redirectUri) throw new E_INVALID_REQUEST("Missing required parameter: redirect_uri");
22
- const credentials = clientService.extractCredentials({
22
+ const client = await clientService.authenticateClient({
23
23
  authorizationHeader: ctx.request.header("authorization"),
24
24
  bodyClientId: body.client_id,
25
25
  bodyClientSecret: body.client_secret
26
26
  });
27
- if (!credentials) throw new E_INVALID_CLIENT("Missing client credentials");
28
- const client = await OAuthClient.query().where("clientId", credentials.clientId).first();
29
- if (!client) throw new E_INVALID_CLIENT("Client not found");
30
- if (client.isDisabled) throw new E_INVALID_CLIENT("Client is disabled");
31
- if (!client.isPublic) {
32
- if (!credentials.clientSecret) throw new E_INVALID_CLIENT("Missing client secret");
33
- if (!clientService.verifySecret(credentials.clientSecret, client.clientSecret)) throw new E_INVALID_CLIENT("Invalid client secret");
34
- }
35
27
  if (!client.grantTypes.includes("authorization_code")) throw new E_INVALID_CLIENT("Client is not allowed to use the authorization_code grant");
36
28
  const hashedCode = tokenService.hashToken(code);
37
29
  const authCode = await OAuthAuthorizationCode.query().where("code", hashedCode).where("clientId", client.clientId).first();
@@ -86,19 +78,11 @@ async function handleRefreshTokenGrant(ctx, manager) {
86
78
  const body = ctx.request.body();
87
79
  const refreshTokenRaw = body.refresh_token;
88
80
  if (!refreshTokenRaw) throw new E_INVALID_REQUEST("Missing required parameter: refresh_token");
89
- const credentials = clientService.extractCredentials({
81
+ const client = await clientService.authenticateClient({
90
82
  authorizationHeader: ctx.request.header("authorization"),
91
83
  bodyClientId: body.client_id,
92
84
  bodyClientSecret: body.client_secret
93
85
  });
94
- if (!credentials) throw new E_INVALID_CLIENT("Missing client credentials");
95
- const client = await OAuthClient.query().where("clientId", credentials.clientId).first();
96
- if (!client) throw new E_INVALID_CLIENT("Client not found");
97
- if (client.isDisabled) throw new E_INVALID_CLIENT("Client is disabled");
98
- if (!client.isPublic) {
99
- if (!credentials.clientSecret) throw new E_INVALID_CLIENT("Missing client secret");
100
- if (!clientService.verifySecret(credentials.clientSecret, client.clientSecret)) throw new E_INVALID_CLIENT("Invalid client secret");
101
- }
102
86
  if (!client.grantTypes.includes("refresh_token")) throw new E_INVALID_CLIENT("Client is not allowed to use the refresh_token grant");
103
87
  const hashedToken = tokenService.hashToken(refreshTokenRaw);
104
88
  const refreshToken = await OAuthRefreshToken.query().where("token", hashedToken).where("clientId", client.clientId).first();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@julr/sesame",
3
3
  "description": "OAuth 2.1 + OIDC server for AdonisJS",
4
- "version": "0.3.0",
4
+ "version": "0.4.0",
5
5
  "engines": {
6
6
  "node": ">=24.0.0"
7
7
  },