@julr/sesame 0.6.0 → 0.7.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 +69 -6
- package/build/{authorize_controller-BiycO4be.js → authorize_controller-sCZIjnc-.js} +10 -10
- package/build/{client_info_controller-AcOG8lWu.js → client_info_controller-B5A3tpbk.js} +3 -4
- package/build/commands/sesame_key.d.ts +1 -1
- package/build/commands/sesame_purge.js +2 -5
- package/build/{configure-DkDkIlt8.js → configure-t9q3KonY.js} +7 -3
- package/build/configure.js +1 -2
- package/build/{consent_controller-Dsdhv6-f.js → consent_controller-ChBboaSd.js} +17 -29
- package/build/consent_retry-DMkfhmk9.js +41 -0
- package/build/decorate-RQD1h28J.js +9 -0
- package/build/{id_token_service-CpTzOUDe.js → id_token_service-BtBUiL_n.js} +1 -1
- package/build/index.d.ts +2 -8
- package/build/index.js +38 -9
- package/build/{introspect_controller-DvOp9scr.js → introspect_controller-LVYGWhMj.js} +13 -9
- package/build/{issue_authorization_code-B9ERu1uO.js → issue_authorization_code-Dr4-bbMp.js} +2 -3
- package/build/{jwks_controller-keo4kBZc.js → jwks_controller-4HUVS_HC.js} +1 -5
- package/build/{main-DGBJhq3E.js → main-Dx9kZv07.js} +14 -52
- package/build/{metadata_controller-BVsTo0Gp.js → metadata_controller-hwfmxlF7.js} +4 -6
- package/build/providers/sesame_provider.d.ts +1 -1
- package/build/providers/sesame_provider.js +8 -8
- package/build/{register_controller-gbq7p8a5.js → register_controller-Cdk3c32j.js} +3 -6
- package/build/{revoke_controller-z_ghrEB7.js → revoke_controller-Daz4Yqfs.js} +15 -21
- package/build/services/main.js +1 -5
- package/build/{sesame_manager-DYUSZ0NC.js → sesame_manager-CeEmlzba.js} +78 -256
- package/build/sesame_manager-DQxvsTEq.js +2 -0
- package/build/src/actions/exchange_authorization_code.d.ts +5 -5
- package/build/src/actions/exchange_client_credentials.d.ts +3 -3
- package/build/src/actions/exchange_refresh_token.d.ts +4 -4
- package/build/src/actions/issue_authorization_code.d.ts +2 -2
- package/build/src/controllers/authorize_controller.d.ts +11 -11
- package/build/src/controllers/metadata_controller.d.ts +7 -7
- package/build/src/controllers/register_controller.d.ts +30 -30
- package/build/src/guard/guard.d.ts +1 -1
- package/build/src/guard/lucid.d.ts +8 -0
- package/build/src/guard/lucid.js +46 -0
- package/build/src/guard/main.d.ts +2 -9
- package/build/src/guard/main.js +2 -6
- package/build/src/guard/types.d.ts +0 -9
- package/build/src/guard/user_provider.d.ts +9 -1
- package/build/src/middleware/any_scope_middleware.js +1 -2
- package/build/src/middleware/scope_middleware.js +1 -2
- package/build/src/oauth_error.d.ts +74 -74
- package/build/src/services/client_service.d.ts +5 -2
- package/build/src/sesame_manager.d.ts +6 -5
- package/build/src/storage/consent_retry.d.ts +4 -0
- package/build/src/storage/drivers/kysely.d.ts +166 -0
- package/build/src/storage/drivers/kysely.js +480 -0
- package/build/src/storage/drivers/lucid.d.ts +162 -0
- package/build/src/storage/drivers/lucid.js +549 -0
- package/build/src/storage/migrations/kysely.d.ts +6 -0
- package/build/src/storage/types.d.ts +197 -0
- package/build/src/storage/types.js +1 -0
- package/build/src/stores.d.ts +21 -0
- package/build/src/types.d.ts +9 -1
- package/build/src/types.js +2 -0
- package/build/stubs/config/sesame.stub +4 -1
- package/build/stubs/config/sesame_kysely.stub +35 -0
- package/build/stubs/migrations/kysely/create_oauth_tables.stub +151 -0
- package/build/taze.config.d.ts +2 -0
- package/build/{token_controller-DyI7oy-U.js → token_controller-DRatCQNT.js} +74 -63
- package/build/types-DEtn2Zdb.js +75 -0
- package/build/{userinfo_controller-RLk8cN_o.js → userinfo_controller-YwN26wK-.js} +5 -8
- package/build/vite.config.d.ts +1 -1
- package/package.json +33 -17
- package/build/chunk-DF48asd8.js +0 -9
- package/build/oauth_access_token-Cz_5gNBx.js +0 -29
- package/build/oauth_client-BSanvSql.js +0 -63
- package/build/sesame_manager-B1Jgq1v2.js +0 -6
- /package/build/{oauth_error-C7UhDb2q.js → oauth_error-CvxtZvjp.js} +0 -0
- /package/build/{token_service-DwnfAR9F.js → token_service-DDQ3Dxaj.js} +0 -0
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import "./
|
|
2
|
-
import {
|
|
3
|
-
import "./
|
|
4
|
-
import { l as E_SERVER_ERROR } from "./oauth_error-C7UhDb2q.js";
|
|
5
|
-
import "./oauth_access_token-Cz_5gNBx.js";
|
|
1
|
+
import { t as BUILTIN_SCOPES } from "./types-DEtn2Zdb.js";
|
|
2
|
+
import { l as E_SERVER_ERROR } from "./oauth_error-CvxtZvjp.js";
|
|
3
|
+
import { t as SesameManager } from "./sesame_manager-CeEmlzba.js";
|
|
6
4
|
//#region src/controllers/metadata_controller.ts
|
|
7
5
|
const DISCOVERY_ROUTE_NAMES = {
|
|
8
6
|
authorization_endpoint: "sesame.authorize",
|
|
@@ -113,7 +111,7 @@ var MetadataController = class {
|
|
|
113
111
|
jwks_uri: router.makeUrl("sesame.jwks", {}, { prefixUrl }),
|
|
114
112
|
subject_types_supported: ["public"],
|
|
115
113
|
id_token_signing_alg_values_supported: ["RS256"],
|
|
116
|
-
scopes_supported: [
|
|
114
|
+
scopes_supported: [.../* @__PURE__ */ new Set([
|
|
117
115
|
"openid",
|
|
118
116
|
"profile",
|
|
119
117
|
"email",
|
|
@@ -3,7 +3,7 @@ import type { ApplicationService } from '@adonisjs/core/types';
|
|
|
3
3
|
* AdonisJS service provider for the Sésame OAuth 2.1 server.
|
|
4
4
|
*/
|
|
5
5
|
export default class SesameProvider {
|
|
6
|
-
|
|
6
|
+
#private;
|
|
7
7
|
constructor(app: ApplicationService);
|
|
8
8
|
/**
|
|
9
9
|
* Register `SesameManager` as a singleton binding.
|
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
import "../
|
|
2
|
-
import { t as SesameManager } from "../sesame_manager-DYUSZ0NC.js";
|
|
3
|
-
import "../oauth_client-BSanvSql.js";
|
|
4
|
-
import "../oauth_error-C7UhDb2q.js";
|
|
5
|
-
import "../oauth_access_token-Cz_5gNBx.js";
|
|
1
|
+
import { t as SesameManager } from "../sesame_manager-CeEmlzba.js";
|
|
6
2
|
//#region providers/sesame_provider.ts
|
|
7
3
|
/**
|
|
8
4
|
* AdonisJS service provider for the Sésame OAuth 2.1 server.
|
|
9
5
|
*/
|
|
10
6
|
var SesameProvider = class {
|
|
7
|
+
#app;
|
|
11
8
|
constructor(app) {
|
|
12
|
-
this
|
|
9
|
+
this.#app = app;
|
|
13
10
|
}
|
|
14
11
|
/**
|
|
15
12
|
* Register `SesameManager` as a singleton binding.
|
|
16
13
|
* The manager is resolved from the `sesame` config key.
|
|
17
14
|
*/
|
|
18
15
|
register() {
|
|
19
|
-
this
|
|
20
|
-
|
|
16
|
+
this.#app.container.singleton(SesameManager, async () => {
|
|
17
|
+
const config = this.#app.config.get("sesame");
|
|
18
|
+
const router = await this.#app.container.make("router");
|
|
19
|
+
const store = await config.store.resolver(this.#app);
|
|
20
|
+
return new SesameManager(config, router, store);
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
23
|
};
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import "./
|
|
2
|
-
import {
|
|
3
|
-
import { t as OAuthClient } from "./oauth_client-BSanvSql.js";
|
|
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-C7UhDb2q.js";
|
|
5
|
-
import "./oauth_access_token-Cz_5gNBx.js";
|
|
1
|
+
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-CvxtZvjp.js";
|
|
2
|
+
import { n as ClientService, t as SesameManager } from "./sesame_manager-CeEmlzba.js";
|
|
6
3
|
import vine from "@vinejs/vine";
|
|
7
4
|
//#region src/rules.ts
|
|
8
5
|
const DANGEROUS_SCHEMES = [
|
|
@@ -128,7 +125,7 @@ var RegisterController = class RegisterController {
|
|
|
128
125
|
...body.software_id ? { software_id: body.software_id } : {},
|
|
129
126
|
...body.software_version ? { software_version: body.software_version } : {}
|
|
130
127
|
};
|
|
131
|
-
await
|
|
128
|
+
await manager.store.createClient({
|
|
132
129
|
id: crypto.randomUUID(),
|
|
133
130
|
clientId,
|
|
134
131
|
clientSecret: hashedSecret,
|
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import "./
|
|
2
|
-
import {
|
|
3
|
-
import "./oauth_client-BSanvSql.js";
|
|
4
|
-
import "./oauth_error-C7UhDb2q.js";
|
|
5
|
-
import { t as OAuthAccessToken } from "./oauth_access_token-Cz_5gNBx.js";
|
|
6
|
-
import { t as TokenService } from "./token_service-DwnfAR9F.js";
|
|
1
|
+
import { n as ClientService, t as SesameManager } from "./sesame_manager-CeEmlzba.js";
|
|
2
|
+
import { t as TokenService } from "./token_service-DDQ3Dxaj.js";
|
|
7
3
|
import { DateTime } from "luxon";
|
|
8
4
|
//#region src/controllers/revoke_controller.ts
|
|
9
5
|
/**
|
|
@@ -21,7 +17,9 @@ import { DateTime } from "luxon";
|
|
|
21
17
|
var RevokeController = class {
|
|
22
18
|
async handle(ctx) {
|
|
23
19
|
const manager = await ctx.containerResolver.make(SesameManager);
|
|
24
|
-
const
|
|
20
|
+
const clientService = new ClientService(manager);
|
|
21
|
+
const store = manager.store;
|
|
22
|
+
const client = await clientService.authenticateClient({
|
|
25
23
|
authorizationHeader: ctx.request.header("authorization"),
|
|
26
24
|
bodyClientId: ctx.request.body().client_id,
|
|
27
25
|
bodyClientSecret: ctx.request.body().client_secret
|
|
@@ -31,22 +29,18 @@ var RevokeController = class {
|
|
|
31
29
|
const tokenTypeHint = ctx.request.body().token_type_hint;
|
|
32
30
|
const hashed = new TokenService(manager).hashToken(token);
|
|
33
31
|
if (!tokenTypeHint || tokenTypeHint === "access_token") {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
32
|
+
if (await store.revokeAccessToken({
|
|
33
|
+
hash: hashed,
|
|
34
|
+
clientId: client.clientId,
|
|
35
|
+
now: DateTime.now()
|
|
36
|
+
})) return ctx.response.ok({});
|
|
40
37
|
if (tokenTypeHint === "access_token") return ctx.response.ok({});
|
|
41
38
|
}
|
|
42
|
-
if (!tokenTypeHint || tokenTypeHint === "refresh_token") {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
await OAuthAccessToken.query().where("id", refreshToken.accessTokenId).whereNull("revokedAt").update({ revokedAt: DateTime.now().toSQL() });
|
|
48
|
-
}
|
|
49
|
-
}
|
|
39
|
+
if (!tokenTypeHint || tokenTypeHint === "refresh_token") await store.revokeRefreshToken({
|
|
40
|
+
hash: hashed,
|
|
41
|
+
clientId: client.clientId,
|
|
42
|
+
now: DateTime.now()
|
|
43
|
+
});
|
|
50
44
|
return ctx.response.ok({});
|
|
51
45
|
}
|
|
52
46
|
};
|
package/build/services/main.js
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import "../
|
|
2
|
-
import { t as SesameManager } from "../sesame_manager-DYUSZ0NC.js";
|
|
3
|
-
import "../oauth_client-BSanvSql.js";
|
|
4
|
-
import "../oauth_error-C7UhDb2q.js";
|
|
5
|
-
import "../oauth_access_token-Cz_5gNBx.js";
|
|
1
|
+
import { t as SesameManager } from "../sesame_manager-CeEmlzba.js";
|
|
6
2
|
import app from "@adonisjs/core/services/app";
|
|
7
3
|
//#region services/main.ts
|
|
8
4
|
let sesame;
|
|
@@ -1,63 +1,8 @@
|
|
|
1
|
-
import { n as
|
|
2
|
-
import { o as E_INVALID_REQUEST, r as E_INVALID_CLIENT, s as E_INVALID_SCOPE } from "./oauth_error-
|
|
3
|
-
import { t as OAuthAccessToken } from "./oauth_access_token-Cz_5gNBx.js";
|
|
1
|
+
import { n as OIDC_SCOPES, t as BUILTIN_SCOPES } from "./types-DEtn2Zdb.js";
|
|
2
|
+
import { o as E_INVALID_REQUEST, r as E_INVALID_CLIENT, s as E_INVALID_SCOPE } from "./oauth_error-CvxtZvjp.js";
|
|
4
3
|
import { DateTime } from "luxon";
|
|
5
4
|
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
6
|
-
import { BaseModel, column } from "@adonisjs/lucid/orm";
|
|
7
5
|
import { SignJWT, importJWK } from "jose";
|
|
8
|
-
//#region src/types.ts
|
|
9
|
-
/**
|
|
10
|
-
* Standard OAuth/OIDC scopes that are always valid regardless
|
|
11
|
-
* of server or client scope configuration.
|
|
12
|
-
*
|
|
13
|
-
* These scopes are:
|
|
14
|
-
* - Accepted during scope validation (client and server level)
|
|
15
|
-
* - Advertised in `scopes_supported` of all metadata endpoints
|
|
16
|
-
* (protected resource, OIDC discovery) so MCP clients know
|
|
17
|
-
* they can request them
|
|
18
|
-
*
|
|
19
|
-
* - `offline_access`: signals that the client needs a refresh token
|
|
20
|
-
* (OIDC Core §11). Note: Sesame issues refresh tokens by default
|
|
21
|
-
* when the `refresh_token` grant is enabled, regardless of whether
|
|
22
|
-
* the client requests this scope (per RFC 6749 §5.1). This scope
|
|
23
|
-
* is still advertised for clients that check metadata before
|
|
24
|
-
* building their authorization request.
|
|
25
|
-
*
|
|
26
|
-
* @see https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
|
|
27
|
-
* @see https://datatracker.ietf.org/doc/html/rfc6749#section-5.1
|
|
28
|
-
*/
|
|
29
|
-
const BUILTIN_SCOPES = new Set(["offline_access"]);
|
|
30
|
-
/**
|
|
31
|
-
* OIDC-recognized scopes that are accepted by server-level validation
|
|
32
|
-
* without needing to be declared in the config `scopes` map.
|
|
33
|
-
*
|
|
34
|
-
* Unlike `BUILTIN_SCOPES`, these still require explicit client authorization
|
|
35
|
-
* via `ClientService.validateClientScopes()`.
|
|
36
|
-
*/
|
|
37
|
-
const OIDC_SCOPES = new Set([
|
|
38
|
-
"openid",
|
|
39
|
-
"profile",
|
|
40
|
-
"email"
|
|
41
|
-
]);
|
|
42
|
-
/**
|
|
43
|
-
* Protocol-managed claims that must never be overridden by `getOidcClaims()`.
|
|
44
|
-
*/
|
|
45
|
-
const RESERVED_OIDC_CLAIMS = new Set([
|
|
46
|
-
"sub",
|
|
47
|
-
"iss",
|
|
48
|
-
"aud",
|
|
49
|
-
"exp",
|
|
50
|
-
"iat",
|
|
51
|
-
"nbf",
|
|
52
|
-
"jti",
|
|
53
|
-
"nonce",
|
|
54
|
-
"at_hash",
|
|
55
|
-
"auth_time",
|
|
56
|
-
"acr",
|
|
57
|
-
"azp",
|
|
58
|
-
"sid"
|
|
59
|
-
]);
|
|
60
|
-
//#endregion
|
|
61
6
|
//#region src/services/client_service.ts
|
|
62
7
|
/**
|
|
63
8
|
* Handles OAuth client authentication and credential management.
|
|
@@ -73,6 +18,10 @@ const RESERVED_OIDC_CLAIMS = new Set([
|
|
|
73
18
|
* @see https://datatracker.ietf.org/doc/html/rfc6749#section-2.3
|
|
74
19
|
*/
|
|
75
20
|
var ClientService = class {
|
|
21
|
+
#manager;
|
|
22
|
+
constructor(manager) {
|
|
23
|
+
this.#manager = manager;
|
|
24
|
+
}
|
|
76
25
|
/**
|
|
77
26
|
* Parse an HTTP Basic Authorization header into client credentials.
|
|
78
27
|
* Follows RFC 6749 §2.3.1 — the client_id and client_secret are
|
|
@@ -117,7 +66,8 @@ var ClientService = class {
|
|
|
117
66
|
async authenticateClient(options) {
|
|
118
67
|
const credentials = this.extractCredentials(options);
|
|
119
68
|
if (!credentials) throw new E_INVALID_CLIENT("Client authentication failed");
|
|
120
|
-
|
|
69
|
+
if (!this.#manager) throw new Error("ClientService requires SesameManager for authentication");
|
|
70
|
+
const client = await this.#manager.store.findClient(credentials.clientId);
|
|
121
71
|
if (!client || client.isDisabled) throw new E_INVALID_CLIENT("Client authentication failed");
|
|
122
72
|
if (!client.isPublic) {
|
|
123
73
|
if (!credentials.clientSecret || !this.verifySecret(credentials.clientSecret, client.clientSecret)) throw new E_INVALID_CLIENT("Client authentication failed");
|
|
@@ -235,16 +185,16 @@ var KeyService = class KeyService {
|
|
|
235
185
|
* their routes are hit.
|
|
236
186
|
*/
|
|
237
187
|
const controllers = {
|
|
238
|
-
token: () => import("./token_controller-
|
|
239
|
-
authorize: () => import("./authorize_controller-
|
|
240
|
-
consent: () => import("./consent_controller-
|
|
241
|
-
introspect: () => import("./introspect_controller-
|
|
242
|
-
revoke: () => import("./revoke_controller-
|
|
243
|
-
register: () => import("./register_controller-
|
|
244
|
-
metadata: () => import("./metadata_controller-
|
|
245
|
-
clientInfo: () => import("./client_info_controller-
|
|
246
|
-
jwks: () => import("./jwks_controller-
|
|
247
|
-
userinfo: () => import("./userinfo_controller-
|
|
188
|
+
token: () => import("./token_controller-DRatCQNT.js"),
|
|
189
|
+
authorize: () => import("./authorize_controller-sCZIjnc-.js"),
|
|
190
|
+
consent: () => import("./consent_controller-ChBboaSd.js"),
|
|
191
|
+
introspect: () => import("./introspect_controller-LVYGWhMj.js"),
|
|
192
|
+
revoke: () => import("./revoke_controller-Daz4Yqfs.js"),
|
|
193
|
+
register: () => import("./register_controller-Cdk3c32j.js"),
|
|
194
|
+
metadata: () => import("./metadata_controller-hwfmxlF7.js"),
|
|
195
|
+
clientInfo: () => import("./client_info_controller-B5A3tpbk.js"),
|
|
196
|
+
jwks: () => import("./jwks_controller-4HUVS_HC.js"),
|
|
197
|
+
userinfo: () => import("./userinfo_controller-YwN26wK-.js")
|
|
248
198
|
};
|
|
249
199
|
/**
|
|
250
200
|
* Register OAuth 2.1 endpoint routes on the given router.
|
|
@@ -291,127 +241,6 @@ function registerWellKnownRoutes(router, options) {
|
|
|
291
241
|
router.get(jwksPath, [controllers.jwks]).as("sesame.jwks");
|
|
292
242
|
}
|
|
293
243
|
//#endregion
|
|
294
|
-
//#region src/models/oauth_refresh_token.ts
|
|
295
|
-
/**
|
|
296
|
-
* Database record for an OAuth 2.0 refresh token (RFC 6749 §6).
|
|
297
|
-
*
|
|
298
|
-
* The `token` column stores the SHA-256 hash of the raw refresh
|
|
299
|
-
* token value. Refresh token rotation is enforced: each use
|
|
300
|
-
* produces a new token and revokes the old one.
|
|
301
|
-
*
|
|
302
|
-
* Replay detection is implemented by checking `revokedAt` —
|
|
303
|
-
* if a revoked token is presented, all tokens for that
|
|
304
|
-
* client+user pair are deleted as a security measure.
|
|
305
|
-
*
|
|
306
|
-
* @see https://datatracker.ietf.org/doc/html/rfc6749#section-6
|
|
307
|
-
* @see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.14.2
|
|
308
|
-
*/
|
|
309
|
-
var OAuthRefreshToken = class extends BaseModel {
|
|
310
|
-
static table = "oauth_refresh_tokens";
|
|
311
|
-
};
|
|
312
|
-
__decorate([column({ isPrimary: true })], OAuthRefreshToken.prototype, "id", void 0);
|
|
313
|
-
__decorate([column({ serializeAs: null })], OAuthRefreshToken.prototype, "token", void 0);
|
|
314
|
-
__decorate([column()], OAuthRefreshToken.prototype, "accessTokenId", void 0);
|
|
315
|
-
__decorate([column()], OAuthRefreshToken.prototype, "clientId", void 0);
|
|
316
|
-
__decorate([column()], OAuthRefreshToken.prototype, "userId", void 0);
|
|
317
|
-
__decorate([json()], OAuthRefreshToken.prototype, "scopes", void 0);
|
|
318
|
-
__decorate([column.dateTime()], OAuthRefreshToken.prototype, "expiresAt", void 0);
|
|
319
|
-
__decorate([column.dateTime()], OAuthRefreshToken.prototype, "revokedAt", void 0);
|
|
320
|
-
__decorate([column.dateTime({ autoCreate: true })], OAuthRefreshToken.prototype, "createdAt", void 0);
|
|
321
|
-
__decorate([column.dateTime({
|
|
322
|
-
autoCreate: true,
|
|
323
|
-
autoUpdate: true
|
|
324
|
-
})], OAuthRefreshToken.prototype, "updatedAt", void 0);
|
|
325
|
-
//#endregion
|
|
326
|
-
//#region src/models/oauth_authorization_code.ts
|
|
327
|
-
/**
|
|
328
|
-
* Database record for an OAuth 2.0 authorization code (RFC 6749 §4.1.2).
|
|
329
|
-
*
|
|
330
|
-
* Authorization codes are short-lived, single-use tokens issued
|
|
331
|
-
* during the authorization flow. The `code` column stores the
|
|
332
|
-
* SHA-256 hash of the raw code value sent to the client.
|
|
333
|
-
*
|
|
334
|
-
* When PKCE (RFC 7636) is used, the `codeChallenge` and
|
|
335
|
-
* `codeChallengeMethod` (always S256) are stored alongside the
|
|
336
|
-
* code for verification at the token endpoint.
|
|
337
|
-
*
|
|
338
|
-
* @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2
|
|
339
|
-
* @see https://datatracker.ietf.org/doc/html/rfc7636
|
|
340
|
-
*/
|
|
341
|
-
var OAuthAuthorizationCode = class extends BaseModel {
|
|
342
|
-
static table = "oauth_authorization_codes";
|
|
343
|
-
};
|
|
344
|
-
__decorate([column({ isPrimary: true })], OAuthAuthorizationCode.prototype, "id", void 0);
|
|
345
|
-
__decorate([column()], OAuthAuthorizationCode.prototype, "code", void 0);
|
|
346
|
-
__decorate([column()], OAuthAuthorizationCode.prototype, "clientId", void 0);
|
|
347
|
-
__decorate([column()], OAuthAuthorizationCode.prototype, "userId", void 0);
|
|
348
|
-
__decorate([json()], OAuthAuthorizationCode.prototype, "scopes", void 0);
|
|
349
|
-
__decorate([column()], OAuthAuthorizationCode.prototype, "redirectUri", void 0);
|
|
350
|
-
__decorate([column()], OAuthAuthorizationCode.prototype, "codeChallenge", void 0);
|
|
351
|
-
__decorate([column()], OAuthAuthorizationCode.prototype, "codeChallengeMethod", void 0);
|
|
352
|
-
__decorate([column()], OAuthAuthorizationCode.prototype, "nonce", void 0);
|
|
353
|
-
__decorate([column.dateTime()], OAuthAuthorizationCode.prototype, "expiresAt", void 0);
|
|
354
|
-
__decorate([column.dateTime({ autoCreate: true })], OAuthAuthorizationCode.prototype, "createdAt", void 0);
|
|
355
|
-
__decorate([column.dateTime({
|
|
356
|
-
autoCreate: true,
|
|
357
|
-
autoUpdate: true
|
|
358
|
-
})], OAuthAuthorizationCode.prototype, "updatedAt", void 0);
|
|
359
|
-
//#endregion
|
|
360
|
-
//#region src/models/oauth_consent.ts
|
|
361
|
-
/**
|
|
362
|
-
* Tracks which scopes a user has approved for a given client.
|
|
363
|
-
*
|
|
364
|
-
* When a user grants consent during the authorization flow,
|
|
365
|
-
* the approved scopes are persisted here. On subsequent
|
|
366
|
-
* authorization requests, if the requested scopes are a subset
|
|
367
|
-
* of previously approved scopes, the user is not prompted again.
|
|
368
|
-
*
|
|
369
|
-
* New scopes are merged into the existing record when the user
|
|
370
|
-
* approves additional permissions.
|
|
371
|
-
*/
|
|
372
|
-
var OAuthConsent = class extends BaseModel {
|
|
373
|
-
static table = "oauth_consents";
|
|
374
|
-
};
|
|
375
|
-
__decorate([column({ isPrimary: true })], OAuthConsent.prototype, "id", void 0);
|
|
376
|
-
__decorate([column()], OAuthConsent.prototype, "clientId", void 0);
|
|
377
|
-
__decorate([column()], OAuthConsent.prototype, "userId", void 0);
|
|
378
|
-
__decorate([json()], OAuthConsent.prototype, "scopes", void 0);
|
|
379
|
-
__decorate([column.dateTime({ autoCreate: true })], OAuthConsent.prototype, "createdAt", void 0);
|
|
380
|
-
__decorate([column.dateTime({
|
|
381
|
-
autoCreate: true,
|
|
382
|
-
autoUpdate: true
|
|
383
|
-
})], OAuthConsent.prototype, "updatedAt", void 0);
|
|
384
|
-
//#endregion
|
|
385
|
-
//#region src/models/oauth_pending_authorization_request.ts
|
|
386
|
-
/**
|
|
387
|
-
* Temporary server-side record for an in-flight OAuth authorization
|
|
388
|
-
* request, created when the user is redirected to the consent page
|
|
389
|
-
* and consumed (deleted) when the user approves or denies.
|
|
390
|
-
*
|
|
391
|
-
* Stored in the database instead of the HTTP session to avoid
|
|
392
|
-
* last-write-wins race conditions when concurrent SPA requests
|
|
393
|
-
* overwrite session data.
|
|
394
|
-
*
|
|
395
|
-
* The `token` column stores a SHA-256 hash of the raw auth_token
|
|
396
|
-
* sent to the consent page, consistent with how authorization
|
|
397
|
-
* codes are stored.
|
|
398
|
-
*/
|
|
399
|
-
var OAuthPendingAuthorizationRequest = class extends BaseModel {
|
|
400
|
-
static table = "oauth_pending_authorization_requests";
|
|
401
|
-
};
|
|
402
|
-
__decorate([column({ isPrimary: true })], OAuthPendingAuthorizationRequest.prototype, "id", void 0);
|
|
403
|
-
__decorate([column()], OAuthPendingAuthorizationRequest.prototype, "token", void 0);
|
|
404
|
-
__decorate([column()], OAuthPendingAuthorizationRequest.prototype, "userId", void 0);
|
|
405
|
-
__decorate([column()], OAuthPendingAuthorizationRequest.prototype, "clientId", void 0);
|
|
406
|
-
__decorate([column()], OAuthPendingAuthorizationRequest.prototype, "redirectUri", void 0);
|
|
407
|
-
__decorate([json()], OAuthPendingAuthorizationRequest.prototype, "scopes", void 0);
|
|
408
|
-
__decorate([column()], OAuthPendingAuthorizationRequest.prototype, "state", void 0);
|
|
409
|
-
__decorate([column()], OAuthPendingAuthorizationRequest.prototype, "codeChallenge", void 0);
|
|
410
|
-
__decorate([column()], OAuthPendingAuthorizationRequest.prototype, "codeChallengeMethod", void 0);
|
|
411
|
-
__decorate([column()], OAuthPendingAuthorizationRequest.prototype, "nonce", void 0);
|
|
412
|
-
__decorate([column.dateTime()], OAuthPendingAuthorizationRequest.prototype, "expiresAt", void 0);
|
|
413
|
-
__decorate([column.dateTime({ autoCreate: true })], OAuthPendingAuthorizationRequest.prototype, "createdAt", void 0);
|
|
414
|
-
//#endregion
|
|
415
244
|
//#region src/sesame_manager.ts
|
|
416
245
|
/**
|
|
417
246
|
* Central manager for the Sésame OAuth 2.1 server.
|
|
@@ -423,14 +252,28 @@ var SesameManager = class {
|
|
|
423
252
|
#config;
|
|
424
253
|
#router;
|
|
425
254
|
#keyService;
|
|
426
|
-
|
|
255
|
+
#store;
|
|
256
|
+
constructor(config, router, store) {
|
|
427
257
|
this.#config = config;
|
|
428
258
|
this.#router = router;
|
|
259
|
+
this.#store = store;
|
|
429
260
|
this.#keyService = config.jwk ? new KeyService(config.jwk) : null;
|
|
430
261
|
}
|
|
262
|
+
#publicClient(client) {
|
|
263
|
+
const publicClient = { ...client };
|
|
264
|
+
Object.defineProperty(publicClient, "clientSecret", {
|
|
265
|
+
value: client.clientSecret,
|
|
266
|
+
enumerable: false,
|
|
267
|
+
writable: false
|
|
268
|
+
});
|
|
269
|
+
return publicClient;
|
|
270
|
+
}
|
|
431
271
|
get config() {
|
|
432
272
|
return this.#config;
|
|
433
273
|
}
|
|
274
|
+
get store() {
|
|
275
|
+
return this.#store;
|
|
276
|
+
}
|
|
434
277
|
get keyService() {
|
|
435
278
|
if (!this.#keyService) throw new Error("OIDC requires a JWK. Set the `jwk` option in defineConfig().");
|
|
436
279
|
return this.#keyService;
|
|
@@ -496,11 +339,10 @@ var SesameManager = class {
|
|
|
496
339
|
*/
|
|
497
340
|
async revokeAllForUser(userId) {
|
|
498
341
|
const now = DateTime.now();
|
|
499
|
-
await
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
await OAuthConsent.query().where("userId", userId).delete();
|
|
342
|
+
await this.#store.revokeAllForUser({
|
|
343
|
+
userId,
|
|
344
|
+
now
|
|
345
|
+
});
|
|
504
346
|
}
|
|
505
347
|
/**
|
|
506
348
|
* Purge revoked and/or expired tokens and authorization codes.
|
|
@@ -516,26 +358,12 @@ var SesameManager = class {
|
|
|
516
358
|
const purgeRevoked = revokedOnly || !expiredOnly;
|
|
517
359
|
const purgeExpired = expiredOnly || !revokedOnly;
|
|
518
360
|
const cutoff = DateTime.now().minus({ hours: retentionHours });
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
refreshTokens += await this.#deleteCount(OAuthRefreshToken.query().whereNotNull("revokedAt").delete());
|
|
526
|
-
}
|
|
527
|
-
if (purgeExpired) {
|
|
528
|
-
accessTokens += await this.#deleteCount(OAuthAccessToken.query().where("expiresAt", "<", cutoff.toSQL()).whereNull("revokedAt").delete());
|
|
529
|
-
refreshTokens += await this.#deleteCount(OAuthRefreshToken.query().where("expiresAt", "<", cutoff.toSQL()).whereNull("revokedAt").delete());
|
|
530
|
-
authorizationCodes += await this.#deleteCount(OAuthAuthorizationCode.query().where("expiresAt", "<", cutoff.toSQL()).delete());
|
|
531
|
-
}
|
|
532
|
-
pendingRequests += await this.#deleteCount(OAuthPendingAuthorizationRequest.query().where("expiresAt", "<", DateTime.now().toSQL()).delete());
|
|
533
|
-
return {
|
|
534
|
-
accessTokens,
|
|
535
|
-
refreshTokens,
|
|
536
|
-
authorizationCodes,
|
|
537
|
-
pendingRequests
|
|
538
|
-
};
|
|
361
|
+
return this.#store.purgeTokens({
|
|
362
|
+
purgeRevoked,
|
|
363
|
+
purgeExpired,
|
|
364
|
+
cutoff,
|
|
365
|
+
now: DateTime.now()
|
|
366
|
+
});
|
|
539
367
|
}
|
|
540
368
|
/**
|
|
541
369
|
* Create a new OAuth client programmatically.
|
|
@@ -549,22 +377,23 @@ var SesameManager = class {
|
|
|
549
377
|
const clientId = clientService.generateClientId();
|
|
550
378
|
const clientSecret = isPublic ? null : clientService.generateClientSecret();
|
|
551
379
|
const hashedSecret = clientSecret ? clientService.hashSecret(clientSecret) : null;
|
|
380
|
+
const client = await this.#store.createClient({
|
|
381
|
+
id: crypto.randomUUID(),
|
|
382
|
+
clientId,
|
|
383
|
+
clientSecret: hashedSecret,
|
|
384
|
+
name: options.name,
|
|
385
|
+
redirectUris: options.redirectUris,
|
|
386
|
+
scopes,
|
|
387
|
+
grantTypes,
|
|
388
|
+
isPublic,
|
|
389
|
+
isDisabled: false,
|
|
390
|
+
requirePkce: options.requirePkce ?? true,
|
|
391
|
+
type: isPublic ? "public" : "confidential",
|
|
392
|
+
metadata: options.metadata ?? null,
|
|
393
|
+
userId: options.userId ?? null
|
|
394
|
+
});
|
|
552
395
|
return {
|
|
553
|
-
client:
|
|
554
|
-
id: crypto.randomUUID(),
|
|
555
|
-
clientId,
|
|
556
|
-
clientSecret: hashedSecret,
|
|
557
|
-
name: options.name,
|
|
558
|
-
redirectUris: options.redirectUris,
|
|
559
|
-
scopes,
|
|
560
|
-
grantTypes,
|
|
561
|
-
isPublic,
|
|
562
|
-
isDisabled: false,
|
|
563
|
-
requirePkce: options.requirePkce ?? true,
|
|
564
|
-
type: isPublic ? "public" : "confidential",
|
|
565
|
-
metadata: options.metadata ?? null,
|
|
566
|
-
userId: options.userId ?? null
|
|
567
|
-
}),
|
|
396
|
+
client: this.#publicClient(client),
|
|
568
397
|
clientSecret
|
|
569
398
|
};
|
|
570
399
|
}
|
|
@@ -572,22 +401,22 @@ var SesameManager = class {
|
|
|
572
401
|
* Find a client by its public client_id.
|
|
573
402
|
*/
|
|
574
403
|
async findClient(clientId) {
|
|
575
|
-
|
|
404
|
+
const client = await this.#store.findClient(clientId);
|
|
405
|
+
return client ? this.#publicClient(client) : null;
|
|
576
406
|
}
|
|
577
407
|
/**
|
|
578
408
|
* List all clients, optionally filtered by userId.
|
|
579
409
|
*/
|
|
580
410
|
async listClients(options) {
|
|
581
|
-
|
|
582
|
-
if (options?.userId) query.where("userId", options.userId);
|
|
583
|
-
return query;
|
|
411
|
+
return (await this.#store.listClients(options)).map((client) => this.#publicClient(client));
|
|
584
412
|
}
|
|
585
413
|
/**
|
|
586
414
|
* Update an existing client by its public client_id.
|
|
587
415
|
* Returns the updated client, or null if not found.
|
|
588
416
|
*/
|
|
589
417
|
async updateClient(clientId, options) {
|
|
590
|
-
const
|
|
418
|
+
const store = this.#store;
|
|
419
|
+
const client = await store.findClient(clientId);
|
|
591
420
|
if (!client) return null;
|
|
592
421
|
if (options.name !== void 0) client.name = options.name;
|
|
593
422
|
if (options.redirectUris !== void 0) client.redirectUris = options.redirectUris;
|
|
@@ -596,39 +425,35 @@ var SesameManager = class {
|
|
|
596
425
|
if (options.isDisabled !== void 0) client.isDisabled = options.isDisabled;
|
|
597
426
|
if (options.requirePkce !== void 0) client.requirePkce = options.requirePkce;
|
|
598
427
|
if (options.metadata !== void 0) client.metadata = options.metadata;
|
|
599
|
-
await
|
|
600
|
-
|
|
428
|
+
await store.updateClient({
|
|
429
|
+
id: client.id,
|
|
430
|
+
data: options
|
|
431
|
+
});
|
|
432
|
+
const updated = await store.findClient(clientId);
|
|
433
|
+
return updated ? this.#publicClient(updated) : null;
|
|
601
434
|
}
|
|
602
435
|
/**
|
|
603
436
|
* Delete a client and all its associated tokens, codes, and consents.
|
|
604
437
|
* Returns true if the client was found and deleted.
|
|
605
438
|
*/
|
|
606
439
|
async deleteClient(clientId) {
|
|
607
|
-
|
|
608
|
-
if (!client) return false;
|
|
609
|
-
await OAuthClient.transaction(async (trx) => {
|
|
610
|
-
await Promise.all([
|
|
611
|
-
OAuthRefreshToken.query({ client: trx }).where("clientId", clientId).delete(),
|
|
612
|
-
OAuthAccessToken.query({ client: trx }).where("clientId", clientId).delete(),
|
|
613
|
-
OAuthAuthorizationCode.query({ client: trx }).where("clientId", clientId).delete(),
|
|
614
|
-
OAuthPendingAuthorizationRequest.query({ client: trx }).where("clientId", clientId).delete(),
|
|
615
|
-
OAuthConsent.query({ client: trx }).where("clientId", clientId).delete()
|
|
616
|
-
]);
|
|
617
|
-
await client.useTransaction(trx).delete();
|
|
618
|
-
});
|
|
619
|
-
return true;
|
|
440
|
+
return this.#store.deleteClient(clientId);
|
|
620
441
|
}
|
|
621
442
|
/**
|
|
622
443
|
* Rotate the secret of a confidential client.
|
|
623
444
|
* Returns the new raw secret, or null if the client is public or not found.
|
|
624
445
|
*/
|
|
625
446
|
async rotateClientSecret(clientId) {
|
|
626
|
-
const
|
|
447
|
+
const store = this.#store;
|
|
448
|
+
const client = await store.findClient(clientId);
|
|
627
449
|
if (!client || client.isPublic) return null;
|
|
628
450
|
const clientService = new ClientService();
|
|
629
451
|
const newSecret = clientService.generateClientSecret();
|
|
630
452
|
client.clientSecret = clientService.hashSecret(newSecret);
|
|
631
|
-
await
|
|
453
|
+
await store.updateClientSecret({
|
|
454
|
+
id: client.id,
|
|
455
|
+
secret: client.clientSecret
|
|
456
|
+
});
|
|
632
457
|
return newSecret;
|
|
633
458
|
}
|
|
634
459
|
/**
|
|
@@ -685,9 +510,6 @@ var SesameManager = class {
|
|
|
685
510
|
};
|
|
686
511
|
});
|
|
687
512
|
}
|
|
688
|
-
#deleteCount(result) {
|
|
689
|
-
return result.then((r) => Array.isArray(r) ? Number(r[0] ?? 0) : Number(r));
|
|
690
|
-
}
|
|
691
513
|
};
|
|
692
514
|
//#endregion
|
|
693
|
-
export {
|
|
515
|
+
export { ClientService as n, SesameManager as t };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { SesameManager } from '../sesame_manager.ts';
|
|
2
|
-
import type {
|
|
2
|
+
import type { OAuthClientRecord } from '../storage/types.ts';
|
|
3
3
|
export interface ExchangeAuthorizationCodeInput {
|
|
4
|
-
client:
|
|
4
|
+
client: OAuthClientRecord;
|
|
5
5
|
code: string;
|
|
6
6
|
redirectUri: string;
|
|
7
7
|
codeVerifier: string;
|
|
@@ -24,11 +24,11 @@ export declare class ExchangeAuthorizationCodeAction {
|
|
|
24
24
|
* consumed atomically inside a transaction.
|
|
25
25
|
*/
|
|
26
26
|
execute(manager: SesameManager, input: ExchangeAuthorizationCodeInput): Promise<{
|
|
27
|
-
id_token?: string | undefined;
|
|
28
|
-
refresh_token?: string | undefined;
|
|
29
27
|
access_token: string;
|
|
30
|
-
token_type:
|
|
28
|
+
token_type: 'Bearer';
|
|
31
29
|
expires_in: number;
|
|
32
30
|
scope: string;
|
|
31
|
+
refresh_token?: string | undefined;
|
|
32
|
+
id_token?: string | undefined;
|
|
33
33
|
}>;
|
|
34
34
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { SesameManager } from '../sesame_manager.ts';
|
|
2
|
-
import type {
|
|
2
|
+
import type { OAuthClientRecord } from '../storage/types.ts';
|
|
3
3
|
export interface ExchangeClientCredentialsInput {
|
|
4
|
-
client:
|
|
4
|
+
client: OAuthClientRecord;
|
|
5
5
|
scope?: string;
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
@@ -21,7 +21,7 @@ export declare class ExchangeClientCredentialsAction {
|
|
|
21
21
|
*/
|
|
22
22
|
execute(manager: SesameManager, input: ExchangeClientCredentialsInput): Promise<{
|
|
23
23
|
access_token: string;
|
|
24
|
-
token_type:
|
|
24
|
+
token_type: 'Bearer';
|
|
25
25
|
expires_in: number;
|
|
26
26
|
scope: string;
|
|
27
27
|
}>;
|