@julr/sesame 0.3.1 → 0.5.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 +10 -11
- package/build/{authorize_controller-CfV9v3R2.js → authorize_controller-BGzxPvYU.js} +3 -2
- package/build/client_service-C3rfXGk_.js +65 -0
- package/build/commands/sesame_purge.js +1 -1
- package/build/{consent_controller-DBtvczID.js → consent_controller-BHoB9mip.js} +1 -1
- package/build/index.js +3 -2
- package/build/{introspect_controller-D2SihAxt.js → introspect_controller-un95fs4y.js} +5 -14
- package/build/{main-ix9EOujk.js → main-B3M6ihoS.js} +4 -4
- package/build/{metadata_controller-CekEP9i9.js → metadata_controller-CJeZG93_.js} +11 -3
- package/build/providers/sesame_provider.js +1 -1
- package/build/{register_controller-Cmkyy0Pv.js → register_controller-Dch4ecyD.js} +5 -18
- package/build/{revoke_controller-CzRid0SB.js → revoke_controller-DnPmzYMd.js} +5 -14
- package/build/services/main.js +1 -1
- package/build/{sesame_manager-Blf8pkgS.js → sesame_manager-BQIW2mqt.js} +1 -1
- package/build/{sesame_manager-Br0DIJgM.js → sesame_manager-C-eEFFHM.js} +11 -10
- package/build/src/controllers/register_controller.d.ts +17 -11
- package/build/src/grants/client_credentials_grant.d.ts +23 -0
- package/build/src/guard/guard.d.ts +7 -0
- package/build/src/guard/main.js +1 -1
- package/build/src/rules.d.ts +10 -2
- package/build/src/services/client_service.d.ts +11 -0
- package/build/src/sesame_manager.d.ts +1 -1
- package/build/src/types.d.ts +22 -2
- package/build/{token_controller-BEiR1lGn.js → token_controller-hGDAYuBS.js} +43 -22
- package/package.json +1 -1
- package/build/client_service-BqPSlaTS.js +0 -53
package/README.md
CHANGED
|
@@ -24,6 +24,7 @@ node ace add @julr/sesame
|
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
This will:
|
|
27
|
+
|
|
27
28
|
- Publish the configuration file to `config/sesame.ts`
|
|
28
29
|
- Publish database migrations (6 tables)
|
|
29
30
|
- Register the service provider and commands
|
|
@@ -47,8 +48,8 @@ const sesameConfig = defineConfig({
|
|
|
47
48
|
issuer: env.get('APP_URL'),
|
|
48
49
|
|
|
49
50
|
scopes: {
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
read: 'Read access',
|
|
52
|
+
write: 'Write access',
|
|
52
53
|
},
|
|
53
54
|
|
|
54
55
|
defaultScopes: ['read'],
|
|
@@ -83,9 +84,11 @@ Register OAuth routes from your `start/routes.ts` file:
|
|
|
83
84
|
import sesame from '@julr/sesame/services/main'
|
|
84
85
|
|
|
85
86
|
// OAuth endpoints under /oauth
|
|
86
|
-
router
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
router
|
|
88
|
+
.group(() => {
|
|
89
|
+
sesame.registerRoutes()
|
|
90
|
+
})
|
|
91
|
+
.prefix('/oauth')
|
|
89
92
|
|
|
90
93
|
// Discovery endpoints at the root
|
|
91
94
|
sesame.registerWellKnownRoutes()
|
|
@@ -144,14 +147,10 @@ Two named middleware are available for checking scopes on authenticated requests
|
|
|
144
147
|
|
|
145
148
|
```ts
|
|
146
149
|
// Requires ALL listed scopes
|
|
147
|
-
router
|
|
148
|
-
.get('/admin', [AdminController])
|
|
149
|
-
.use(middleware.scopes({ scopes: ['admin', 'write'] }))
|
|
150
|
+
router.get('/admin', [AdminController]).use(middleware.scopes({ scopes: ['admin', 'write'] }))
|
|
150
151
|
|
|
151
152
|
// Requires AT LEAST ONE of the listed scopes
|
|
152
|
-
router
|
|
153
|
-
.get('/data', [DataController])
|
|
154
|
-
.use(middleware.anyScope({ scopes: ['read', 'write'] }))
|
|
153
|
+
router.get('/data', [DataController]).use(middleware.anyScope({ scopes: ['read', 'write'] }))
|
|
155
154
|
```
|
|
156
155
|
|
|
157
156
|
## MCP Support
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { i as OAuthAuthorizationCode, n as OAuthPendingAuthorizationRequest, r as OAuthConsent, t as SesameManager } from "./sesame_manager-
|
|
1
|
+
import { i as OAuthAuthorizationCode, n as OAuthPendingAuthorizationRequest, r as OAuthConsent, t as SesameManager } from "./sesame_manager-C-eEFFHM.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-
|
|
7
|
+
import { t as ClientService } from "./client_service-C3rfXGk_.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
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { o as BUILTIN_SCOPES } from "./sesame_manager-C-eEFFHM.js";
|
|
2
|
+
import { o as E_INVALID_REQUEST, r as E_INVALID_CLIENT, s as E_INVALID_SCOPE } from "./oauth_error-CnJ3L8tf.js";
|
|
3
|
+
import { t as OAuthClient } from "./oauth_client-BIoY5jBR.js";
|
|
4
|
+
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
5
|
+
var ClientService = class {
|
|
6
|
+
parseBasicAuth(header) {
|
|
7
|
+
if (!header.startsWith("Basic ")) return null;
|
|
8
|
+
try {
|
|
9
|
+
const decoded = Buffer.from(header.slice(6), "base64").toString("utf-8");
|
|
10
|
+
const colonIndex = decoded.indexOf(":");
|
|
11
|
+
if (colonIndex === -1) return null;
|
|
12
|
+
return {
|
|
13
|
+
clientId: decodeURIComponent(decoded.slice(0, colonIndex)),
|
|
14
|
+
clientSecret: decodeURIComponent(decoded.slice(colonIndex + 1))
|
|
15
|
+
};
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
extractCredentials(options) {
|
|
21
|
+
const basic = options.authorizationHeader ? this.parseBasicAuth(options.authorizationHeader) : null;
|
|
22
|
+
if (basic && options.bodyClientId) throw new E_INVALID_REQUEST("Multiple client authentication methods are not allowed");
|
|
23
|
+
if (basic) return basic;
|
|
24
|
+
if (options.bodyClientId) return {
|
|
25
|
+
clientId: options.bodyClientId,
|
|
26
|
+
clientSecret: options.bodyClientSecret
|
|
27
|
+
};
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
async authenticateClient(options) {
|
|
31
|
+
const credentials = this.extractCredentials(options);
|
|
32
|
+
if (!credentials) throw new E_INVALID_CLIENT("Client authentication failed");
|
|
33
|
+
const client = await OAuthClient.query().where("clientId", credentials.clientId).first();
|
|
34
|
+
if (!client || client.isDisabled) throw new E_INVALID_CLIENT("Client authentication failed");
|
|
35
|
+
if (!client.isPublic) {
|
|
36
|
+
if (!credentials.clientSecret || !this.verifySecret(credentials.clientSecret, client.clientSecret)) throw new E_INVALID_CLIENT("Client authentication failed");
|
|
37
|
+
}
|
|
38
|
+
return client;
|
|
39
|
+
}
|
|
40
|
+
validateClientScopes(requestedScopes, clientScopes) {
|
|
41
|
+
const nonBuiltinScopes = requestedScopes.filter((s) => !BUILTIN_SCOPES.has(s));
|
|
42
|
+
if (clientScopes.length === 0 && nonBuiltinScopes.length > 0) throw new E_INVALID_SCOPE(`Scope not allowed: ${nonBuiltinScopes.join(", ")}`);
|
|
43
|
+
const allowedSet = new Set(clientScopes);
|
|
44
|
+
const invalid = nonBuiltinScopes.filter((s) => !allowedSet.has(s));
|
|
45
|
+
if (invalid.length > 0) throw new E_INVALID_SCOPE(`Scope not allowed: ${invalid.join(", ")}`);
|
|
46
|
+
}
|
|
47
|
+
hashSecret(secret) {
|
|
48
|
+
return createHash("sha256").update(secret).digest("base64url");
|
|
49
|
+
}
|
|
50
|
+
verifySecret(secret, storedHash) {
|
|
51
|
+
const hash = this.hashSecret(secret);
|
|
52
|
+
try {
|
|
53
|
+
return timingSafeEqual(Buffer.from(hash), Buffer.from(storedHash));
|
|
54
|
+
} catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
generateClientId() {
|
|
59
|
+
return randomBytes(16).toString("hex");
|
|
60
|
+
}
|
|
61
|
+
generateClientSecret() {
|
|
62
|
+
return randomBytes(32).toString("base64url");
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
export { ClientService as t };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as SesameManager } from "../sesame_manager-
|
|
1
|
+
import { t as SesameManager } from "../sesame_manager-C-eEFFHM.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-
|
|
1
|
+
import { i as OAuthAuthorizationCode, n as OAuthPendingAuthorizationRequest, r as OAuthConsent, t as SesameManager } from "./sesame_manager-C-eEFFHM.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-
|
|
2
|
+
import { a as OAuthRefreshToken, i as OAuthAuthorizationCode, r as OAuthConsent, t as SesameManager } from "./sesame_manager-C-eEFFHM.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-
|
|
8
|
+
import { i as OAuthGuard, n as oauthUserProvider, r as OAuthLucidUserProvider, t as oauthGuard } from "./main-B3M6ihoS.js";
|
|
9
9
|
function defineConfig(config) {
|
|
10
10
|
return {
|
|
11
11
|
issuer: config.issuer,
|
|
@@ -13,6 +13,7 @@ function defineConfig(config) {
|
|
|
13
13
|
defaultScopes: config.defaultScopes ?? [],
|
|
14
14
|
grantTypes: config.grantTypes ?? ["authorization_code", "refresh_token"],
|
|
15
15
|
accessTokenTtl: config.accessTokenTtl ?? "1h",
|
|
16
|
+
clientCredentialsAccessTokenTtl: config.clientCredentialsAccessTokenTtl ?? config.accessTokenTtl ?? "1h",
|
|
16
17
|
refreshTokenTtl: config.refreshTokenTtl ?? "30d",
|
|
17
18
|
authorizationCodeTtl: config.authorizationCodeTtl ?? "10m",
|
|
18
19
|
authorizationRequestTtl: config.authorizationRequestTtl ?? config.authorizationCodeTtl ?? "10m",
|
|
@@ -1,28 +1,19 @@
|
|
|
1
|
-
import { a as OAuthRefreshToken, t as SesameManager } from "./sesame_manager-
|
|
1
|
+
import { a as OAuthRefreshToken, t as SesameManager } from "./sesame_manager-C-eEFFHM.js";
|
|
2
2
|
import "./decorate-BKZEjPRg.js";
|
|
3
3
|
import { t as OAuthAccessToken } from "./oauth_access_token-bsoM5KeU.js";
|
|
4
|
-
import
|
|
5
|
-
import
|
|
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-
|
|
7
|
+
import { t as ClientService } from "./client_service-C3rfXGk_.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
|
|
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("
|
|
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("
|
|
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("
|
|
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-
|
|
153
|
+
const { SesameManager } = await import("./sesame_manager-BQIW2mqt.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-
|
|
1
|
+
import { t as SesameManager } from "./sesame_manager-C-eEFFHM.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: [
|
|
47
|
-
|
|
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,9 +1,9 @@
|
|
|
1
|
-
import { t as SesameManager } from "./sesame_manager-
|
|
1
|
+
import { t as SesameManager } from "./sesame_manager-C-eEFFHM.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-
|
|
6
|
+
import { t as ClientService } from "./client_service-C3rfXGk_.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-
|
|
1
|
+
import { a as OAuthRefreshToken, t as SesameManager } from "./sesame_manager-C-eEFFHM.js";
|
|
2
2
|
import "./decorate-BKZEjPRg.js";
|
|
3
3
|
import { t as OAuthAccessToken } from "./oauth_access_token-bsoM5KeU.js";
|
|
4
|
-
import
|
|
5
|
-
import
|
|
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-
|
|
7
|
+
import { t as ClientService } from "./client_service-C3rfXGk_.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
|
|
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;
|
package/build/services/main.js
CHANGED
|
@@ -2,14 +2,15 @@ import { n as json, t as __decorate } from "./decorate-BKZEjPRg.js";
|
|
|
2
2
|
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
|
+
const BUILTIN_SCOPES = new Set(["offline_access"]);
|
|
5
6
|
const controllers = {
|
|
6
|
-
token: () => import("./token_controller-
|
|
7
|
-
authorize: () => import("./authorize_controller-
|
|
8
|
-
consent: () => import("./consent_controller-
|
|
9
|
-
introspect: () => import("./introspect_controller-
|
|
10
|
-
revoke: () => import("./revoke_controller-
|
|
11
|
-
register: () => import("./register_controller-
|
|
12
|
-
metadata: () => import("./metadata_controller-
|
|
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"),
|
|
13
14
|
clientInfo: () => import("./client_info_controller-BucHGx4u.js")
|
|
14
15
|
};
|
|
15
16
|
function registerOAuthRoutes(router) {
|
|
@@ -99,8 +100,8 @@ var SesameManager = class {
|
|
|
99
100
|
return scope in this.#config.scopes;
|
|
100
101
|
}
|
|
101
102
|
validateScopes(scopes) {
|
|
102
|
-
if (Object.keys(this.#config.scopes).length === 0) return scopes;
|
|
103
|
-
return scopes.filter((s) => !this.hasScope(s));
|
|
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));
|
|
104
105
|
}
|
|
105
106
|
isGrantTypeEnabled(grantType) {
|
|
106
107
|
return this.#config.grantTypes.includes(grantType);
|
|
@@ -163,4 +164,4 @@ var SesameManager = class {
|
|
|
163
164
|
return result.then((r) => Array.isArray(r) ? Number(r[0] ?? 0) : Number(r));
|
|
164
165
|
}
|
|
165
166
|
};
|
|
166
|
-
export { OAuthRefreshToken as a, OAuthAuthorizationCode as i, OAuthPendingAuthorizationRequest as n, OAuthConsent as r, SesameManager as t };
|
|
167
|
+
export { OAuthRefreshToken as a, OAuthAuthorizationCode as i, OAuthPendingAuthorizationRequest as n, BUILTIN_SCOPES as o, OAuthConsent as r, SesameManager as t };
|
|
@@ -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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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;
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
}>;
|
|
@@ -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
|
}
|
package/build/src/guard/main.js
CHANGED
|
@@ -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-
|
|
5
|
+
import { i as OAuthGuard, n as oauthUserProvider, r as OAuthLucidUserProvider, t as oauthGuard } from "../../main-B3M6ihoS.js";
|
|
6
6
|
export { OAuthGuard, OAuthLucidUserProvider, oauthGuard, oauthUserProvider };
|
package/build/src/rules.d.ts
CHANGED
|
@@ -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
|
|
8
|
-
*
|
|
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
|
package/build/src/types.d.ts
CHANGED
|
@@ -27,15 +27,29 @@ export type InferScopes<T extends {
|
|
|
27
27
|
[K in keyof T['scopes'] & string]: true;
|
|
28
28
|
};
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
30
|
+
* Standard OAuth/OIDC scopes that are always valid regardless
|
|
31
|
+
* of server or client scope configuration.
|
|
32
|
+
*
|
|
33
|
+
* - `offline_access`: signals that the client needs a refresh token
|
|
34
|
+
* (OIDC Core §11, OAuth 2.1). Without this built-in treatment,
|
|
35
|
+
* MCP clients that don't explicitly configure scopes would never
|
|
36
|
+
* receive a refresh token and would expire after the access token TTL.
|
|
37
|
+
*
|
|
38
|
+
* @see https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
|
|
39
|
+
*/
|
|
40
|
+
export declare const BUILTIN_SCOPES: Set<string>;
|
|
41
|
+
/**
|
|
42
|
+
* Supported OAuth 2.1 grant types.
|
|
31
43
|
*
|
|
32
44
|
* - `authorization_code`: RFC 6749 §4.1 — Authorization Code Grant
|
|
33
45
|
* - `refresh_token`: RFC 6749 §6 — Refreshing an Access Token
|
|
46
|
+
* - `client_credentials`: RFC 6749 §4.4 — Client Credentials Grant (M2M)
|
|
34
47
|
*
|
|
35
48
|
* @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1
|
|
49
|
+
* @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.4
|
|
36
50
|
* @see https://datatracker.ietf.org/doc/html/rfc6749#section-6
|
|
37
51
|
*/
|
|
38
|
-
export type GrantType = 'authorization_code' | 'refresh_token';
|
|
52
|
+
export type GrantType = 'authorization_code' | 'refresh_token' | 'client_credentials';
|
|
39
53
|
/**
|
|
40
54
|
* User-facing configuration interface for Sésame.
|
|
41
55
|
*
|
|
@@ -77,6 +91,11 @@ export interface SesameConfig {
|
|
|
77
91
|
* Defaults to '30d'.
|
|
78
92
|
*/
|
|
79
93
|
refreshTokenTtl?: string;
|
|
94
|
+
/**
|
|
95
|
+
* Access token TTL for the client_credentials grant (M2M).
|
|
96
|
+
* Defaults to `accessTokenTtl`.
|
|
97
|
+
*/
|
|
98
|
+
clientCredentialsAccessTokenTtl?: string;
|
|
80
99
|
/**
|
|
81
100
|
* Authorization code TTL as a string duration.
|
|
82
101
|
* Defaults to '10m'.
|
|
@@ -125,6 +144,7 @@ export interface ResolvedSesameConfig {
|
|
|
125
144
|
defaultScopes: string[];
|
|
126
145
|
grantTypes: GrantType[];
|
|
127
146
|
accessTokenTtl: string;
|
|
147
|
+
clientCredentialsAccessTokenTtl: string;
|
|
128
148
|
refreshTokenTtl: string;
|
|
129
149
|
authorizationCodeTtl: string;
|
|
130
150
|
authorizationRequestTtl: string;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { a as OAuthRefreshToken, i as OAuthAuthorizationCode, t as SesameManager } from "./sesame_manager-
|
|
1
|
+
import { a as OAuthRefreshToken, i as OAuthAuthorizationCode, o as BUILTIN_SCOPES, t as SesameManager } from "./sesame_manager-C-eEFFHM.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
|
|
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-
|
|
7
|
+
import { t as ClientService } from "./client_service-C3rfXGk_.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
|
|
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
|
|
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();
|
|
@@ -151,9 +135,46 @@ async function handleRefreshTokenGrant(ctx, manager) {
|
|
|
151
135
|
refresh_token: newRefreshTokenRaw
|
|
152
136
|
};
|
|
153
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
|
+
}
|
|
154
174
|
const grantHandlers = {
|
|
155
175
|
authorization_code: handleAuthorizationCodeGrant,
|
|
156
|
-
refresh_token: handleRefreshTokenGrant
|
|
176
|
+
refresh_token: handleRefreshTokenGrant,
|
|
177
|
+
client_credentials: handleClientCredentialsGrant
|
|
157
178
|
};
|
|
158
179
|
var TokenController = class TokenController {
|
|
159
180
|
static validator = vine.create({ grant_type: vine.string() });
|
package/package.json
CHANGED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { s as E_INVALID_SCOPE } from "./oauth_error-CnJ3L8tf.js";
|
|
2
|
-
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
3
|
-
var ClientService = class {
|
|
4
|
-
parseBasicAuth(header) {
|
|
5
|
-
if (!header.startsWith("Basic ")) return null;
|
|
6
|
-
try {
|
|
7
|
-
const decoded = Buffer.from(header.slice(6), "base64").toString("utf-8");
|
|
8
|
-
const colonIndex = decoded.indexOf(":");
|
|
9
|
-
if (colonIndex === -1) return null;
|
|
10
|
-
return {
|
|
11
|
-
clientId: decodeURIComponent(decoded.slice(0, colonIndex)),
|
|
12
|
-
clientSecret: decodeURIComponent(decoded.slice(colonIndex + 1))
|
|
13
|
-
};
|
|
14
|
-
} catch {
|
|
15
|
-
return null;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
extractCredentials(options) {
|
|
19
|
-
if (options.authorizationHeader) {
|
|
20
|
-
const basic = this.parseBasicAuth(options.authorizationHeader);
|
|
21
|
-
if (basic) return basic;
|
|
22
|
-
}
|
|
23
|
-
if (options.bodyClientId) return {
|
|
24
|
-
clientId: options.bodyClientId,
|
|
25
|
-
clientSecret: options.bodyClientSecret
|
|
26
|
-
};
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
validateClientScopes(requestedScopes, clientScopes) {
|
|
30
|
-
if (clientScopes.length === 0 && requestedScopes.length > 0) throw new E_INVALID_SCOPE(`Scope not allowed: ${requestedScopes.join(", ")}`);
|
|
31
|
-
const allowedSet = new Set(clientScopes);
|
|
32
|
-
const invalid = requestedScopes.filter((s) => !allowedSet.has(s));
|
|
33
|
-
if (invalid.length > 0) throw new E_INVALID_SCOPE(`Scope not allowed: ${invalid.join(", ")}`);
|
|
34
|
-
}
|
|
35
|
-
hashSecret(secret) {
|
|
36
|
-
return createHash("sha256").update(secret).digest("base64url");
|
|
37
|
-
}
|
|
38
|
-
verifySecret(secret, storedHash) {
|
|
39
|
-
const hash = this.hashSecret(secret);
|
|
40
|
-
try {
|
|
41
|
-
return timingSafeEqual(Buffer.from(hash), Buffer.from(storedHash));
|
|
42
|
-
} catch {
|
|
43
|
-
return false;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
generateClientId() {
|
|
47
|
-
return randomBytes(16).toString("hex");
|
|
48
|
-
}
|
|
49
|
-
generateClientSecret() {
|
|
50
|
-
return randomBytes(32).toString("base64url");
|
|
51
|
-
}
|
|
52
|
-
};
|
|
53
|
-
export { ClientService as t };
|