@julr/sesame 0.5.1 → 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/LICENSE.md +1 -1
- package/README.md +470 -64
- package/build/authorize_controller-sCZIjnc-.js +251 -0
- package/build/{client_info_controller-BucHGx4u.js → client_info_controller-B5A3tpbk.js} +11 -4
- package/build/commands/sesame_client.d.ts +20 -0
- package/build/commands/sesame_key.d.ts +12 -0
- package/build/commands/sesame_purge.d.ts +0 -2
- package/build/commands/sesame_purge.js +12 -3
- package/build/configure-t9q3KonY.js +31 -0
- package/build/configure.js +1 -24
- package/build/consent_controller-ChBboaSd.js +96 -0
- package/build/consent_retry-DMkfhmk9.js +41 -0
- package/build/{decorate-BKZEjPRg.js → decorate-RQD1h28J.js} +3 -9
- package/build/id_token_service-BtBUiL_n.js +54 -0
- package/build/index.d.ts +2 -8
- package/build/index.js +59 -10
- package/build/{introspect_controller-6bRt9sZt.js → introspect_controller-LVYGWhMj.js} +28 -10
- package/build/issue_authorization_code-Dr4-bbMp.js +39 -0
- package/build/jwks_controller-4HUVS_HC.js +22 -0
- package/build/{main-EbeMS5S9.js → main-Dx9kZv07.js} +35 -43
- package/build/{metadata_controller-DeaMRnUr.js → metadata_controller-hwfmxlF7.js} +81 -6
- package/build/oauth_error-CvxtZvjp.js +189 -0
- package/build/providers/sesame_provider.d.ts +1 -1
- package/build/providers/sesame_provider.js +17 -6
- package/build/{register_controller-sIJ1rxdM.js → register_controller-Cdk3c32j.js} +44 -8
- package/build/revoke_controller-Daz4Yqfs.js +48 -0
- package/build/services/main.js +3 -3
- package/build/sesame_manager-CeEmlzba.js +515 -0
- package/build/sesame_manager-DQxvsTEq.js +2 -0
- package/build/src/actions/authorize.d.ts +46 -0
- package/build/src/actions/exchange_authorization_code.d.ts +34 -0
- package/build/src/actions/exchange_client_credentials.d.ts +28 -0
- package/build/src/actions/exchange_refresh_token.d.ts +59 -0
- package/build/src/actions/issue_authorization_code.d.ts +26 -0
- package/build/src/controllers/authorize_controller.d.ts +18 -17
- package/build/src/controllers/consent_controller.d.ts +5 -0
- package/build/src/controllers/jwks_controller.d.ts +14 -0
- package/build/src/controllers/metadata_controller.d.ts +10 -3
- package/build/src/controllers/register_controller.d.ts +30 -30
- package/build/src/controllers/token_controller.d.ts +8 -5
- package/build/src/controllers/userinfo_controller.d.ts +14 -0
- 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 +10 -1
- package/build/src/middleware/scope_middleware.js +10 -1
- package/build/src/models/oauth_authorization_code.d.ts +1 -0
- package/build/src/models/oauth_pending_authorization_request.d.ts +1 -0
- package/build/src/oauth_error.d.ts +74 -74
- package/build/src/routes.d.ts +3 -1
- package/build/src/services/client_service.d.ts +5 -2
- package/build/src/services/id_token_service.d.ts +30 -0
- package/build/src/services/key_service.d.ts +20 -0
- package/build/src/sesame_manager.d.ts +56 -4
- 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 +120 -0
- 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/main.ts +5 -0
- package/build/stubs/migrations/create_oauth_authorization_codes_table.stub +1 -0
- package/build/stubs/migrations/create_oauth_pending_authorization_requests_table.stub +1 -0
- package/build/stubs/migrations/create_oauth_refresh_tokens_table.stub +1 -1
- package/build/stubs/migrations/kysely/create_oauth_tables.stub +151 -0
- package/build/taze.config.d.ts +2 -0
- package/build/token_controller-DRatCQNT.js +492 -0
- package/build/token_service-DDQ3Dxaj.js +59 -0
- package/build/types-DEtn2Zdb.js +75 -0
- package/build/userinfo_controller-YwN26wK-.js +37 -0
- package/build/vite.config.d.ts +2 -0
- package/package.json +55 -54
- package/build/authorize_controller-YUfAy-R2.js +0 -138
- package/build/client_service-WTNMqWzY.js +0 -65
- package/build/consent_controller-Dprwd1ed.js +0 -85
- package/build/oauth_access_token-bsoM5KeU.js +0 -18
- package/build/oauth_client-BIoY5jBR.js +0 -24
- package/build/oauth_error-CnJ3L8tf.js +0 -94
- package/build/revoke_controller-D6isoQCi.js +0 -41
- package/build/sesame_manager-Bu4MHqZV.js +0 -4
- package/build/sesame_manager-DwDZy5Vy.js +0 -167
- package/build/src/grants/authorization_code_grant.d.ts +0 -23
- package/build/src/grants/client_credentials_grant.d.ts +0 -23
- package/build/src/grants/refresh_token_grant.d.ts +0 -27
- package/build/token_controller-DzcrLMyS.js +0 -194
- package/build/token_service-fhoA4slP.js +0 -31
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { d as E_UNSUPPORTED_RESPONSE_TYPE, o as E_INVALID_REQUEST, r as E_INVALID_CLIENT } from "./oauth_error-CvxtZvjp.js";
|
|
2
|
+
import { n as ClientService, t as SesameManager } from "./sesame_manager-CeEmlzba.js";
|
|
3
|
+
import { t as TokenService } from "./token_service-DDQ3Dxaj.js";
|
|
4
|
+
import { t as IssueAuthorizationCodeAction } from "./issue_authorization_code-Dr4-bbMp.js";
|
|
5
|
+
import { DateTime } from "luxon";
|
|
6
|
+
import string from "@adonisjs/core/helpers/string";
|
|
7
|
+
import vine from "@vinejs/vine";
|
|
8
|
+
//#region src/actions/authorize.ts
|
|
9
|
+
/**
|
|
10
|
+
* Handles the OAuth 2.0 authorization request business logic.
|
|
11
|
+
*
|
|
12
|
+
* Validates the client, scopes, and PKCE parameters, checks
|
|
13
|
+
* existing consent, and either issues an authorization code
|
|
14
|
+
* or signals that login/consent is required.
|
|
15
|
+
*
|
|
16
|
+
* Errors before redirect_uri validation are thrown as exceptions.
|
|
17
|
+
* Errors after are returned as `redirect_error` results so the
|
|
18
|
+
* controller can redirect back to the client per spec.
|
|
19
|
+
*/
|
|
20
|
+
var AuthorizeAction = class {
|
|
21
|
+
/**
|
|
22
|
+
* Process an authorization request. Returns a discriminated
|
|
23
|
+
* union that the controller interprets as the appropriate
|
|
24
|
+
* HTTP redirect.
|
|
25
|
+
*/
|
|
26
|
+
async execute(manager, input) {
|
|
27
|
+
if (input.responseType !== "code") throw new E_UNSUPPORTED_RESPONSE_TYPE("Only \"code\" is supported");
|
|
28
|
+
const client = await manager.store.findClient(input.clientId);
|
|
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.redirectUris.includes(input.redirectUri)) throw new E_INVALID_REQUEST("Invalid redirect_uri");
|
|
32
|
+
if (!client.grantTypes.includes("authorization_code")) return {
|
|
33
|
+
type: "redirect_error",
|
|
34
|
+
error: "unauthorized_client",
|
|
35
|
+
description: "Client is not allowed to use the authorization_code grant"
|
|
36
|
+
};
|
|
37
|
+
const requestedScopes = input.scope ? input.scope.split(" ") : manager.config.defaultScopes;
|
|
38
|
+
const scopeError = this.#validateScopes(manager, requestedScopes, client);
|
|
39
|
+
if (scopeError) return scopeError;
|
|
40
|
+
const pkceError = this.#validatePkce(input);
|
|
41
|
+
if (pkceError) return pkceError;
|
|
42
|
+
if (!input.userId) return { type: "login_required" };
|
|
43
|
+
return this.#resolveConsent(manager, {
|
|
44
|
+
...input,
|
|
45
|
+
userId: input.userId
|
|
46
|
+
}, client, requestedScopes);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Validate requested scopes against server config, client
|
|
50
|
+
* permissions, and OIDC availability.
|
|
51
|
+
*/
|
|
52
|
+
#validateScopes(manager, scopes, client) {
|
|
53
|
+
const invalidScopes = manager.validateScopes(scopes);
|
|
54
|
+
if (invalidScopes.length > 0) return {
|
|
55
|
+
type: "redirect_error",
|
|
56
|
+
error: "invalid_scope",
|
|
57
|
+
description: `Invalid scopes: ${invalidScopes.join(", ")}`
|
|
58
|
+
};
|
|
59
|
+
const clientService = new ClientService();
|
|
60
|
+
try {
|
|
61
|
+
clientService.validateClientScopes(scopes, client.scopes);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
return {
|
|
64
|
+
type: "redirect_error",
|
|
65
|
+
error: "invalid_scope",
|
|
66
|
+
description: err.message
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (manager.usesOidcScopes(scopes) && !manager.isOidcEnabled) return {
|
|
70
|
+
type: "redirect_error",
|
|
71
|
+
error: "invalid_scope",
|
|
72
|
+
description: "OIDC scopes require OIDC to be configured (set jwk and oidcProvider in config)"
|
|
73
|
+
};
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Ensure PKCE code_challenge is present and uses S256
|
|
78
|
+
* (mandatory per OAuth 2.1).
|
|
79
|
+
*/
|
|
80
|
+
#validatePkce(input) {
|
|
81
|
+
if (!input.codeChallenge) return {
|
|
82
|
+
type: "redirect_error",
|
|
83
|
+
error: "invalid_request",
|
|
84
|
+
description: "PKCE code_challenge is required"
|
|
85
|
+
};
|
|
86
|
+
if (input.codeChallengeMethod !== "S256") return {
|
|
87
|
+
type: "redirect_error",
|
|
88
|
+
error: "invalid_request",
|
|
89
|
+
description: "Only S256 code_challenge_method is supported"
|
|
90
|
+
};
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Check if the user has already consented to all requested
|
|
95
|
+
* scopes. If so, issue the code directly. Otherwise, create
|
|
96
|
+
* a pending authorization request for the consent page.
|
|
97
|
+
*/
|
|
98
|
+
async #resolveConsent(manager, input, client, scopes) {
|
|
99
|
+
const existingConsent = await manager.store.findConsent({
|
|
100
|
+
clientId: client.clientId,
|
|
101
|
+
userId: input.userId
|
|
102
|
+
});
|
|
103
|
+
if (existingConsent) {
|
|
104
|
+
const consentedSet = new Set(existingConsent.scopes);
|
|
105
|
+
if (scopes.every((s) => consentedSet.has(s))) return {
|
|
106
|
+
type: "authorized",
|
|
107
|
+
code: await new IssueAuthorizationCodeAction().execute(manager, {
|
|
108
|
+
client,
|
|
109
|
+
userId: input.userId,
|
|
110
|
+
scopes,
|
|
111
|
+
redirectUri: input.redirectUri,
|
|
112
|
+
codeChallenge: input.codeChallenge,
|
|
113
|
+
codeChallengeMethod: input.codeChallengeMethod,
|
|
114
|
+
nonce: input.nonce
|
|
115
|
+
})
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
type: "consent_required",
|
|
120
|
+
authToken: await this.#createPendingRequest(manager, input, client.clientId, scopes),
|
|
121
|
+
scopes
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Store the authorization request in the database so it can
|
|
126
|
+
* be consumed atomically by the consent controller.
|
|
127
|
+
*/
|
|
128
|
+
async #createPendingRequest(manager, input, clientId, scopes) {
|
|
129
|
+
const tokenService = new TokenService(manager);
|
|
130
|
+
const rawToken = tokenService.generateOpaqueToken();
|
|
131
|
+
const ttl = string.seconds.parse(manager.config.authorizationRequestTtl);
|
|
132
|
+
await manager.store.createPendingAuthorizationRequest({
|
|
133
|
+
id: crypto.randomUUID(),
|
|
134
|
+
token: tokenService.hashToken(rawToken),
|
|
135
|
+
userId: input.userId,
|
|
136
|
+
clientId,
|
|
137
|
+
redirectUri: input.redirectUri,
|
|
138
|
+
scopes,
|
|
139
|
+
state: input.state ?? null,
|
|
140
|
+
codeChallenge: input.codeChallenge ?? null,
|
|
141
|
+
codeChallengeMethod: input.codeChallengeMethod ?? null,
|
|
142
|
+
nonce: input.nonce ?? null,
|
|
143
|
+
expiresAt: DateTime.now().plus({ seconds: ttl })
|
|
144
|
+
});
|
|
145
|
+
return rawToken;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
//#endregion
|
|
149
|
+
//#region src/controllers/authorize_controller.ts
|
|
150
|
+
/**
|
|
151
|
+
* Handles the OAuth 2.0 Authorization Endpoint (RFC 6749 §3.1).
|
|
152
|
+
*
|
|
153
|
+
* Validates HTTP parameters, delegates business logic to
|
|
154
|
+
* AuthorizeAction, and interprets the result as redirects.
|
|
155
|
+
*
|
|
156
|
+
* @see https://datatracker.ietf.org/doc/html/rfc6749#section-3.1
|
|
157
|
+
*/
|
|
158
|
+
var AuthorizeController = class AuthorizeController {
|
|
159
|
+
static validator = vine.create({
|
|
160
|
+
client_id: vine.string(),
|
|
161
|
+
response_type: vine.string(),
|
|
162
|
+
redirect_uri: vine.string(),
|
|
163
|
+
scope: vine.string().optional(),
|
|
164
|
+
state: vine.string().optional(),
|
|
165
|
+
code_challenge: vine.string().optional(),
|
|
166
|
+
code_challenge_method: vine.string().optional(),
|
|
167
|
+
nonce: vine.string().optional()
|
|
168
|
+
});
|
|
169
|
+
/**
|
|
170
|
+
* Validate the authorization request query params, run the
|
|
171
|
+
* authorize action, and redirect based on the result.
|
|
172
|
+
*/
|
|
173
|
+
async handle(ctx) {
|
|
174
|
+
const manager = await ctx.containerResolver.make(SesameManager);
|
|
175
|
+
const [error, query] = await AuthorizeController.validator.tryValidate(ctx.request.qs());
|
|
176
|
+
if (error) throw new E_INVALID_REQUEST("Invalid authorization request parameters");
|
|
177
|
+
await ctx.auth.check();
|
|
178
|
+
const user = ctx.auth.user;
|
|
179
|
+
const result = await new AuthorizeAction().execute(manager, {
|
|
180
|
+
clientId: query.client_id,
|
|
181
|
+
responseType: query.response_type,
|
|
182
|
+
redirectUri: query.redirect_uri,
|
|
183
|
+
scope: query.scope,
|
|
184
|
+
state: query.state,
|
|
185
|
+
codeChallenge: query.code_challenge,
|
|
186
|
+
codeChallengeMethod: query.code_challenge_method,
|
|
187
|
+
nonce: query.nonce,
|
|
188
|
+
userId: user ? String(user.id) : void 0
|
|
189
|
+
});
|
|
190
|
+
if (result.type === "redirect_error") return this.#redirectToClient(ctx, {
|
|
191
|
+
issuer: manager.config.issuer,
|
|
192
|
+
redirectUri: query.redirect_uri,
|
|
193
|
+
state: query.state,
|
|
194
|
+
params: {
|
|
195
|
+
error: result.error,
|
|
196
|
+
error_description: result.description
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
if (result.type === "login_required") {
|
|
200
|
+
const params = this.#buildDisplayParams(ctx);
|
|
201
|
+
const url = this.#resolvePageUrl(manager.config.loginPage, ctx, params);
|
|
202
|
+
return ctx.response.redirect().toPath(url);
|
|
203
|
+
}
|
|
204
|
+
if (result.type === "consent_required") {
|
|
205
|
+
const params = this.#buildDisplayParams(ctx);
|
|
206
|
+
params.set("auth_token", result.authToken);
|
|
207
|
+
params.set("scope", result.scopes.join(" "));
|
|
208
|
+
const url = this.#resolvePageUrl(manager.config.consentPage, ctx, params);
|
|
209
|
+
return ctx.response.redirect().toPath(url);
|
|
210
|
+
}
|
|
211
|
+
return this.#redirectToClient(ctx, {
|
|
212
|
+
issuer: manager.config.issuer,
|
|
213
|
+
redirectUri: query.redirect_uri,
|
|
214
|
+
state: query.state,
|
|
215
|
+
params: { code: result.code }
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Build a redirect URL with OAuth params, state, and issuer,
|
|
220
|
+
* then redirect the user agent to the client's redirect_uri.
|
|
221
|
+
*/
|
|
222
|
+
#redirectToClient(ctx, options) {
|
|
223
|
+
const url = new URL(options.redirectUri);
|
|
224
|
+
for (const [key, value] of Object.entries(options.params)) url.searchParams.set(key, value);
|
|
225
|
+
if (options.state) url.searchParams.set("state", options.state);
|
|
226
|
+
url.searchParams.set("iss", options.issuer);
|
|
227
|
+
return ctx.response.redirect().toPath(url.toString());
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Forward original authorize query params so the
|
|
231
|
+
* login/consent page can display them.
|
|
232
|
+
*/
|
|
233
|
+
#buildDisplayParams(ctx) {
|
|
234
|
+
const params = new URLSearchParams();
|
|
235
|
+
for (const [key, value] of Object.entries(ctx.request.qs())) {
|
|
236
|
+
if (key === "auth_token" || value == null) continue;
|
|
237
|
+
params.set(key, String(value));
|
|
238
|
+
}
|
|
239
|
+
return params;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Resolve a login/consent page URL from either a static
|
|
243
|
+
* string path or a dynamic function.
|
|
244
|
+
*/
|
|
245
|
+
#resolvePageUrl(page, ctx, params) {
|
|
246
|
+
if (typeof page === "function") return page(ctx, params);
|
|
247
|
+
return `${page}?${params.toString()}`;
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
//#endregion
|
|
251
|
+
export { AuthorizeController as default };
|
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
import "./
|
|
2
|
-
import {
|
|
3
|
-
import { t as OAuthClient } from "./oauth_client-BIoY5jBR.js";
|
|
1
|
+
import { o as E_INVALID_REQUEST, r as E_INVALID_CLIENT } from "./oauth_error-CvxtZvjp.js";
|
|
2
|
+
import { t as SesameManager } from "./sesame_manager-CeEmlzba.js";
|
|
4
3
|
import vine from "@vinejs/vine";
|
|
4
|
+
//#region src/controllers/client_info_controller.ts
|
|
5
|
+
/**
|
|
6
|
+
* Returns public information about an OAuth client.
|
|
7
|
+
* Used by the consent page to display the client's name
|
|
8
|
+
* from server-side data rather than query parameters
|
|
9
|
+
* (RFC 6819 §4.4.1.4 — prevent client identity spoofing).
|
|
10
|
+
*/
|
|
5
11
|
var ClientInfoController = class ClientInfoController {
|
|
6
12
|
static validator = vine.create({ client_id: vine.string() });
|
|
7
13
|
async handle(ctx) {
|
|
8
14
|
const [error, query] = await ClientInfoController.validator.tryValidate(ctx.request.qs());
|
|
9
15
|
if (error) throw new E_INVALID_REQUEST("Missing client_id");
|
|
10
|
-
const client = await
|
|
16
|
+
const client = await (await ctx.containerResolver.make(SesameManager)).store.findClient(query.client_id);
|
|
11
17
|
if (!client) throw new E_INVALID_CLIENT("Client not found");
|
|
12
18
|
return {
|
|
13
19
|
client_id: client.clientId,
|
|
@@ -15,4 +21,5 @@ var ClientInfoController = class ClientInfoController {
|
|
|
15
21
|
};
|
|
16
22
|
}
|
|
17
23
|
};
|
|
24
|
+
//#endregion
|
|
18
25
|
export { ClientInfoController as default };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { BaseCommand } from '@adonisjs/core/ace';
|
|
2
|
+
import type { CommandOptions } from '@adonisjs/core/types/ace';
|
|
3
|
+
/**
|
|
4
|
+
* Interactively create a new OAuth client.
|
|
5
|
+
*
|
|
6
|
+
* Prompts for name, redirect URIs, client type, and scopes,
|
|
7
|
+
* then outputs the generated client_id and client_secret.
|
|
8
|
+
*/
|
|
9
|
+
export default class SesameClient extends BaseCommand {
|
|
10
|
+
static commandName: string;
|
|
11
|
+
static description: string;
|
|
12
|
+
static options: CommandOptions;
|
|
13
|
+
public: boolean;
|
|
14
|
+
name: string;
|
|
15
|
+
redirectUris: string[];
|
|
16
|
+
scopes: string[];
|
|
17
|
+
grantTypes: string[];
|
|
18
|
+
userId: string;
|
|
19
|
+
run(): Promise<void>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { BaseCommand } from '@adonisjs/core/ace';
|
|
2
|
+
/**
|
|
3
|
+
* Generate an RSA JWK key pair for signing OIDC ID tokens.
|
|
4
|
+
*/
|
|
5
|
+
export default class SesameKey extends BaseCommand {
|
|
6
|
+
#private;
|
|
7
|
+
static commandName: string;
|
|
8
|
+
static description: string;
|
|
9
|
+
raw: boolean;
|
|
10
|
+
writeEnv: boolean;
|
|
11
|
+
run(): Promise<void | boolean>;
|
|
12
|
+
}
|
|
@@ -7,8 +7,6 @@ import type { CommandOptions } from '@adonisjs/core/types/ace';
|
|
|
7
7
|
* or `--expired` to target only one category. Expired tokens are
|
|
8
8
|
* retained for a configurable period (default 168h / 7 days) to
|
|
9
9
|
* allow for debugging and audit trails.
|
|
10
|
-
*
|
|
11
|
-
* @see https://datatracker.ietf.org/doc/html/rfc6749
|
|
12
10
|
*/
|
|
13
11
|
export default class SesamePurge extends BaseCommand {
|
|
14
12
|
static commandName: string;
|
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
import { t as SesameManager } from "../sesame_manager-
|
|
2
|
-
import { t as __decorate } from "../decorate-
|
|
3
|
-
import "../oauth_access_token-bsoM5KeU.js";
|
|
1
|
+
import { t as SesameManager } from "../sesame_manager-CeEmlzba.js";
|
|
2
|
+
import { t as __decorate } from "../decorate-RQD1h28J.js";
|
|
4
3
|
import { BaseCommand, flags } from "@adonisjs/core/ace";
|
|
4
|
+
//#region commands/sesame_purge.ts
|
|
5
|
+
/**
|
|
6
|
+
* Purge revoked and/or expired OAuth tokens and authorization codes.
|
|
7
|
+
*
|
|
8
|
+
* By default, purges both revoked and expired records. Use `--revoked`
|
|
9
|
+
* or `--expired` to target only one category. Expired tokens are
|
|
10
|
+
* retained for a configurable period (default 168h / 7 days) to
|
|
11
|
+
* allow for debugging and audit trails.
|
|
12
|
+
*/
|
|
5
13
|
var SesamePurge = class extends BaseCommand {
|
|
6
14
|
static commandName = "sesame:purge";
|
|
7
15
|
static description = "Purge revoked and/or expired tokens and authorization codes";
|
|
@@ -25,4 +33,5 @@ __decorate([flags.number({
|
|
|
25
33
|
description: "Number of hours to retain expired tokens (default: 168 = 7 days)",
|
|
26
34
|
default: 168
|
|
27
35
|
})], SesamePurge.prototype, "hours", void 0);
|
|
36
|
+
//#endregion
|
|
28
37
|
export { SesamePurge as default };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
//#region configure.ts
|
|
3
|
+
async function configure(command) {
|
|
4
|
+
const codemods = await command.createCodemods();
|
|
5
|
+
const stubsRoot = join(import.meta.dirname, "stubs");
|
|
6
|
+
const store = command.parsedFlags.store ?? "lucid";
|
|
7
|
+
if (store !== "lucid" && store !== "kysely") throw new Error("Invalid Sesame store. Use --store=lucid or --store=kysely");
|
|
8
|
+
const configStub = store === "kysely" ? "config/sesame_kysely.stub" : "config/sesame.stub";
|
|
9
|
+
await codemods.makeUsingStub(stubsRoot, configStub, {});
|
|
10
|
+
const migrationStubs = store === "kysely" ? ["migrations/kysely/create_oauth_tables.stub"] : [
|
|
11
|
+
"migrations/create_oauth_clients_table.stub",
|
|
12
|
+
"migrations/create_oauth_authorization_codes_table.stub",
|
|
13
|
+
"migrations/create_oauth_access_tokens_table.stub",
|
|
14
|
+
"migrations/create_oauth_refresh_tokens_table.stub",
|
|
15
|
+
"migrations/create_oauth_consents_table.stub",
|
|
16
|
+
"migrations/create_oauth_pending_authorization_requests_table.stub"
|
|
17
|
+
];
|
|
18
|
+
for (const stub of migrationStubs) await codemods.makeUsingStub(stubsRoot, stub, {});
|
|
19
|
+
await codemods.updateRcFile((rcFile) => {
|
|
20
|
+
rcFile.addProvider("@julr/sesame/sesame_provider").addCommand("@julr/sesame/commands");
|
|
21
|
+
});
|
|
22
|
+
await codemods.registerMiddleware("named", [{
|
|
23
|
+
name: "scopes",
|
|
24
|
+
path: "@julr/sesame/scope_middleware"
|
|
25
|
+
}, {
|
|
26
|
+
name: "anyScope",
|
|
27
|
+
path: "@julr/sesame/any_scope_middleware"
|
|
28
|
+
}]);
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { configure as t };
|
package/build/configure.js
CHANGED
|
@@ -1,25 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
async function configure(command) {
|
|
3
|
-
const codemods = await command.createCodemods();
|
|
4
|
-
const stubsRoot = join(import.meta.url, "./stubs");
|
|
5
|
-
await codemods.makeUsingStub(stubsRoot, "config/sesame.stub", {});
|
|
6
|
-
for (const stub of [
|
|
7
|
-
"migrations/create_oauth_clients_table.stub",
|
|
8
|
-
"migrations/create_oauth_authorization_codes_table.stub",
|
|
9
|
-
"migrations/create_oauth_access_tokens_table.stub",
|
|
10
|
-
"migrations/create_oauth_refresh_tokens_table.stub",
|
|
11
|
-
"migrations/create_oauth_consents_table.stub",
|
|
12
|
-
"migrations/create_oauth_pending_authorization_requests_table.stub"
|
|
13
|
-
]) await codemods.makeUsingStub(stubsRoot, stub, {});
|
|
14
|
-
await codemods.updateRcFile((rcFile) => {
|
|
15
|
-
rcFile.addProvider("@julr/sesame/sesame_provider").addCommand("@julr/sesame/commands");
|
|
16
|
-
});
|
|
17
|
-
await codemods.registerMiddleware("named", [{
|
|
18
|
-
name: "scopes",
|
|
19
|
-
path: "@julr/sesame/scope_middleware"
|
|
20
|
-
}, {
|
|
21
|
-
name: "anyScope",
|
|
22
|
-
path: "@julr/sesame/any_scope_middleware"
|
|
23
|
-
}]);
|
|
24
|
-
}
|
|
1
|
+
import { t as configure } from "./configure-t9q3KonY.js";
|
|
25
2
|
export { configure };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { a as E_INVALID_GRANT, o as E_INVALID_REQUEST, r as E_INVALID_CLIENT } from "./oauth_error-CvxtZvjp.js";
|
|
2
|
+
import { t as SesameManager } from "./sesame_manager-CeEmlzba.js";
|
|
3
|
+
import { t as TokenService } from "./token_service-DDQ3Dxaj.js";
|
|
4
|
+
import { t as IssueAuthorizationCodeAction } from "./issue_authorization_code-Dr4-bbMp.js";
|
|
5
|
+
import { DateTime } from "luxon";
|
|
6
|
+
import vine from "@vinejs/vine";
|
|
7
|
+
//#region src/controllers/consent_controller.ts
|
|
8
|
+
/**
|
|
9
|
+
* Handles user consent submission for the OAuth authorization flow.
|
|
10
|
+
*
|
|
11
|
+
* When a user approves access, this controller stores or updates
|
|
12
|
+
* the consent record and issues an authorization code via redirect.
|
|
13
|
+
* When the user denies access, it redirects back to the client
|
|
14
|
+
* with an `access_denied` error.
|
|
15
|
+
*
|
|
16
|
+
* Consent records are persisted so that returning users are not
|
|
17
|
+
* prompted again for previously approved scopes.
|
|
18
|
+
*
|
|
19
|
+
* @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
|
|
20
|
+
*/
|
|
21
|
+
var ConsentController = class ConsentController {
|
|
22
|
+
static validator = vine.create({ auth_token: vine.string() });
|
|
23
|
+
/**
|
|
24
|
+
* Validate the consent submission, consume the pending
|
|
25
|
+
* request, and redirect back to the client with either
|
|
26
|
+
* an authorization code or an access_denied error.
|
|
27
|
+
*/
|
|
28
|
+
async handle(ctx) {
|
|
29
|
+
const manager = await ctx.containerResolver.make(SesameManager);
|
|
30
|
+
await ctx.auth.check();
|
|
31
|
+
const user = ctx.auth.user;
|
|
32
|
+
if (!user) throw new E_INVALID_REQUEST("User must be authenticated");
|
|
33
|
+
const [error, body] = await ConsentController.validator.tryValidate(ctx.request.body());
|
|
34
|
+
if (error) throw new E_INVALID_REQUEST("Missing required parameter: auth_token");
|
|
35
|
+
const userId = String(user.id);
|
|
36
|
+
const hashedToken = new TokenService(manager).hashToken(body.auth_token);
|
|
37
|
+
const pendingRequest = await this.#consumePendingRequest(manager, hashedToken, userId);
|
|
38
|
+
if (!pendingRequest) throw new E_INVALID_GRANT("Authorization request not found or expired");
|
|
39
|
+
const client = await manager.store.findClient(pendingRequest.clientId);
|
|
40
|
+
if (!client) throw new E_INVALID_CLIENT("Client not found");
|
|
41
|
+
if (client.isDisabled) throw new E_INVALID_CLIENT("Client is disabled");
|
|
42
|
+
if (!client.redirectUris.includes(pendingRequest.redirectUri)) throw new E_INVALID_REQUEST("Invalid redirect_uri");
|
|
43
|
+
if (!ctx.request.body().accept) return this.#redirectWithDenied(ctx, manager, pendingRequest);
|
|
44
|
+
await this.#persistConsent(manager, client.clientId, userId, pendingRequest.scopes);
|
|
45
|
+
const code = await new IssueAuthorizationCodeAction().execute(manager, {
|
|
46
|
+
client,
|
|
47
|
+
userId,
|
|
48
|
+
scopes: pendingRequest.scopes,
|
|
49
|
+
redirectUri: pendingRequest.redirectUri,
|
|
50
|
+
codeChallenge: pendingRequest.codeChallenge ?? void 0,
|
|
51
|
+
codeChallengeMethod: pendingRequest.codeChallengeMethod ?? void 0,
|
|
52
|
+
nonce: pendingRequest.nonce ?? void 0
|
|
53
|
+
});
|
|
54
|
+
const url = new URL(pendingRequest.redirectUri);
|
|
55
|
+
url.searchParams.set("code", code);
|
|
56
|
+
if (pendingRequest.state) url.searchParams.set("state", pendingRequest.state);
|
|
57
|
+
url.searchParams.set("iss", manager.config.issuer);
|
|
58
|
+
return ctx.response.redirect().toPath(url.toString());
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Atomically consume a pending authorization request so concurrent
|
|
62
|
+
* consent submissions cannot produce two authorization codes.
|
|
63
|
+
*/
|
|
64
|
+
async #consumePendingRequest(manager, hashedToken, userId) {
|
|
65
|
+
return manager.store.consumePendingAuthorizationRequest({
|
|
66
|
+
token: hashedToken,
|
|
67
|
+
userId,
|
|
68
|
+
now: DateTime.now()
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Persist or merge consent so future authorization requests
|
|
73
|
+
* for the same client skip the consent screen.
|
|
74
|
+
*/
|
|
75
|
+
async #persistConsent(manager, clientId, userId, scopes) {
|
|
76
|
+
await manager.store.grantConsent({
|
|
77
|
+
clientId,
|
|
78
|
+
userId,
|
|
79
|
+
scopes
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Redirect back to the client with an access_denied error
|
|
84
|
+
* when the user denies the authorization request.
|
|
85
|
+
*/
|
|
86
|
+
#redirectWithDenied(ctx, manager, pendingRequest) {
|
|
87
|
+
const url = new URL(pendingRequest.redirectUri);
|
|
88
|
+
url.searchParams.set("error", "access_denied");
|
|
89
|
+
url.searchParams.set("error_description", "The user denied the authorization request");
|
|
90
|
+
if (pendingRequest.state) url.searchParams.set("state", pendingRequest.state);
|
|
91
|
+
url.searchParams.set("iss", manager.config.issuer);
|
|
92
|
+
return ctx.response.redirect().toPath(url.toString());
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
//#endregion
|
|
96
|
+
export { ConsentController as default };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
//#region src/storage/consent_retry.ts
|
|
2
|
+
const RETRYABLE_CODES = /* @__PURE__ */ new Set([
|
|
3
|
+
"23505",
|
|
4
|
+
"40001",
|
|
5
|
+
"40P01",
|
|
6
|
+
"ER_DUP_ENTRY",
|
|
7
|
+
"ER_LOCK_DEADLOCK",
|
|
8
|
+
"ER_LOCK_WAIT_TIMEOUT",
|
|
9
|
+
"SQLITE_BUSY",
|
|
10
|
+
"SQLITE_BUSY_SNAPSHOT",
|
|
11
|
+
"SQLITE_CONSTRAINT_UNIQUE"
|
|
12
|
+
]);
|
|
13
|
+
const RETRYABLE_ERRNOS = /* @__PURE__ */ new Set([
|
|
14
|
+
1062,
|
|
15
|
+
1205,
|
|
16
|
+
1213
|
|
17
|
+
]);
|
|
18
|
+
/**
|
|
19
|
+
* Identify database conflicts that can be retried in a fresh transaction.
|
|
20
|
+
*/
|
|
21
|
+
function isRetryableConsentConflict(error) {
|
|
22
|
+
if (!error || typeof error !== "object") return false;
|
|
23
|
+
const candidate = error;
|
|
24
|
+
if (typeof candidate.code === "string" && RETRYABLE_CODES.has(candidate.code)) return true;
|
|
25
|
+
if (typeof candidate.errno === "number" && RETRYABLE_ERRNOS.has(candidate.errno)) return true;
|
|
26
|
+
return candidate.cause ? isRetryableConsentConflict(candidate.cause) : false;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Retry consent writes after uniqueness, serialization, or lock conflicts.
|
|
30
|
+
*/
|
|
31
|
+
async function retryConsentConflict(operation) {
|
|
32
|
+
for (let attempt = 1; attempt <= 3; attempt++) try {
|
|
33
|
+
return await operation();
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (attempt === 3 || !isRetryableConsentConflict(error)) throw error;
|
|
36
|
+
await new Promise((resolve) => setTimeout(resolve, attempt * 5));
|
|
37
|
+
}
|
|
38
|
+
throw new Error("Consent retry limit exceeded");
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
export { retryConsentConflict as t };
|
|
@@ -1,15 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
function json(options) {
|
|
3
|
-
return column({
|
|
4
|
-
...options,
|
|
5
|
-
prepare: (value) => value == null ? null : JSON.stringify(value),
|
|
6
|
-
consume: (value) => value == null ? null : typeof value === "string" ? JSON.parse(value) : value
|
|
7
|
-
});
|
|
8
|
-
}
|
|
1
|
+
//#region \0@oxc-project+runtime@0.150.0/helpers/esm/decorate.js
|
|
9
2
|
function __decorate(decorators, target, key, desc) {
|
|
10
3
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
11
4
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
12
5
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
13
6
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
14
7
|
}
|
|
15
|
-
|
|
8
|
+
//#endregion
|
|
9
|
+
export { __decorate as t };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { r as RESERVED_OIDC_CLAIMS } from "./types-DEtn2Zdb.js";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import string from "@adonisjs/core/helpers/string";
|
|
4
|
+
//#region src/services/id_token_service.ts
|
|
5
|
+
/**
|
|
6
|
+
* Builds and signs OIDC `id_token` JWTs.
|
|
7
|
+
*/
|
|
8
|
+
var IdTokenService = class IdTokenService {
|
|
9
|
+
#manager;
|
|
10
|
+
constructor(manager) {
|
|
11
|
+
this.#manager = manager;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Compute `at_hash` — left half of SHA-256 hash of the access token, base64url-encoded.
|
|
15
|
+
* @see OIDC Core §3.1.3.6
|
|
16
|
+
*/
|
|
17
|
+
static computeAtHash(accessToken) {
|
|
18
|
+
const hash = createHash("sha256").update(accessToken).digest();
|
|
19
|
+
return hash.subarray(0, hash.length / 2).toString("base64url");
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Filter out reserved claims that the server owns.
|
|
23
|
+
*/
|
|
24
|
+
static filterReservedClaims(claims) {
|
|
25
|
+
const filtered = {};
|
|
26
|
+
for (const [key, value] of Object.entries(claims)) if (!RESERVED_OIDC_CLAIMS.has(key)) filtered[key] = value;
|
|
27
|
+
return filtered;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Resolve user OIDC claims by calling `getOidcClaims` if present,
|
|
31
|
+
* then filtering out reserved protocol claims.
|
|
32
|
+
*/
|
|
33
|
+
static async resolveUserClaims(user, scopes) {
|
|
34
|
+
const rawClaims = typeof user?.getOidcClaims === "function" ? await user.getOidcClaims(scopes) : {};
|
|
35
|
+
return IdTokenService.filterReservedClaims(rawClaims);
|
|
36
|
+
}
|
|
37
|
+
async sign(options) {
|
|
38
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
39
|
+
const ttlSeconds = string.seconds.parse(this.#manager.config.idTokenTtl);
|
|
40
|
+
const payload = {
|
|
41
|
+
...await IdTokenService.resolveUserClaims(options.user, options.scopes),
|
|
42
|
+
iss: this.#manager.config.issuer,
|
|
43
|
+
sub: String(options.sub),
|
|
44
|
+
aud: options.clientId,
|
|
45
|
+
iat: now,
|
|
46
|
+
exp: now + ttlSeconds,
|
|
47
|
+
at_hash: IdTokenService.computeAtHash(options.accessToken)
|
|
48
|
+
};
|
|
49
|
+
if (options.nonce) payload.nonce = options.nonce;
|
|
50
|
+
return this.#manager.keyService.sign(payload);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
//#endregion
|
|
54
|
+
export { IdTokenService as t };
|
package/build/index.d.ts
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
export { configure } from './configure.ts';
|
|
2
2
|
export { defineConfig } from './src/define_config.ts';
|
|
3
|
+
export { stores } from './src/stores.ts';
|
|
3
4
|
export { SesameManager } from './src/sesame_manager.ts';
|
|
4
|
-
export { OAuthError, E_INVALID_REQUEST, E_INVALID_CLIENT, E_INVALID_GRANT, E_INVALID_SCOPE, E_INVALID_TOKEN, E_UNSUPPORTED_GRANT_TYPE, E_UNSUPPORTED_RESPONSE_TYPE, E_ACCESS_DENIED, E_INVALID_CLIENT_METADATA, E_SERVER_ERROR, E_INSUFFICIENT_SCOPE, } from './src/oauth_error.ts';
|
|
5
|
-
export { OAuthClient } from './src/models/oauth_client.ts';
|
|
6
|
-
export { OAuthAccessToken } from './src/models/oauth_access_token.ts';
|
|
7
|
-
export { OAuthRefreshToken } from './src/models/oauth_refresh_token.ts';
|
|
8
|
-
export { OAuthAuthorizationCode } from './src/models/oauth_authorization_code.ts';
|
|
9
|
-
export { OAuthConsent } from './src/models/oauth_consent.ts';
|
|
10
5
|
export { OAuthGuard } from './src/guard/guard.ts';
|
|
11
|
-
export {
|
|
12
|
-
export { oauthGuard, oauthUserProvider } from './src/guard/main.ts';
|
|
6
|
+
export { oauthGuard } from './src/guard/main.ts';
|