@julr/sesame 0.5.0 → 0.6.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 +405 -62
- package/build/authorize_controller-BiycO4be.js +251 -0
- package/build/chunk-DF48asd8.js +9 -0
- package/build/{client_info_controller-BucHGx4u.js → client_info_controller-AcOG8lWu.js} +11 -3
- 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 +15 -3
- package/build/configure-DkDkIlt8.js +27 -0
- package/build/configure.js +2 -24
- package/build/consent_controller-Dsdhv6-f.js +108 -0
- package/build/id_token_service-CpTzOUDe.js +54 -0
- package/build/index.d.ts +1 -1
- package/build/index.js +30 -10
- package/build/{introspect_controller-un95fs4y.js → introspect_controller-DvOp9scr.js} +21 -7
- package/build/issue_authorization_code-B9ERu1uO.js +40 -0
- package/build/jwks_controller-keo4kBZc.js +26 -0
- package/build/{main-B3M6ihoS.js → main-DGBJhq3E.js} +34 -4
- package/build/metadata_controller-BVsTo0Gp.js +158 -0
- package/build/{oauth_access_token-bsoM5KeU.js → oauth_access_token-Cz_5gNBx.js} +12 -1
- package/build/oauth_client-BSanvSql.js +63 -0
- package/build/oauth_error-C7UhDb2q.js +189 -0
- package/build/providers/sesame_provider.js +14 -3
- package/build/{register_controller-Dch4ecyD.js → register_controller-gbq7p8a5.js} +46 -7
- package/build/{revoke_controller-DnPmzYMd.js → revoke_controller-z_ghrEB7.js} +21 -8
- package/build/services/main.js +7 -3
- package/build/sesame_manager-B1Jgq1v2.js +6 -0
- package/build/sesame_manager-DYUSZ0NC.js +693 -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 +13 -12
- 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 +9 -2
- package/build/src/controllers/token_controller.d.ts +8 -5
- package/build/src/controllers/userinfo_controller.d.ts +14 -0
- package/build/src/guard/main.js +5 -5
- package/build/src/middleware/any_scope_middleware.js +11 -1
- package/build/src/middleware/scope_middleware.js +11 -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 +1 -1
- package/build/src/routes.d.ts +3 -1
- 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 +54 -3
- package/build/src/types.d.ts +124 -3
- 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/token_controller-DyI7oy-U.js +481 -0
- package/build/token_service-DwnfAR9F.js +59 -0
- package/build/userinfo_controller-RLk8cN_o.js +40 -0
- package/build/vite.config.d.ts +2 -0
- package/package.json +26 -41
- package/build/authorize_controller-BGzxPvYU.js +0 -138
- package/build/client_service-C3rfXGk_.js +0 -65
- package/build/consent_controller-BHoB9mip.js +0 -85
- package/build/decorate-BKZEjPRg.js +0 -15
- package/build/metadata_controller-CJeZG93_.js +0 -81
- package/build/oauth_client-BIoY5jBR.js +0 -24
- package/build/oauth_error-CnJ3L8tf.js +0 -94
- package/build/sesame_manager-BQIW2mqt.js +0 -4
- package/build/sesame_manager-C-eEFFHM.js +0 -167
- package/build/src/grants/authorization_code_grant.d.ts +0 -27
- 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-hGDAYuBS.js +0 -194
- package/build/token_service-fhoA4slP.js +0 -31
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import "./chunk-DF48asd8.js";
|
|
2
|
+
import { n as OAuthPendingAuthorizationRequest, o as ClientService, r as OAuthConsent, t as SesameManager } from "./sesame_manager-DYUSZ0NC.js";
|
|
3
|
+
import { t as OAuthClient } from "./oauth_client-BSanvSql.js";
|
|
4
|
+
import { d as E_UNSUPPORTED_RESPONSE_TYPE, o as E_INVALID_REQUEST, r as E_INVALID_CLIENT } from "./oauth_error-C7UhDb2q.js";
|
|
5
|
+
import "./oauth_access_token-Cz_5gNBx.js";
|
|
6
|
+
import { t as TokenService } from "./token_service-DwnfAR9F.js";
|
|
7
|
+
import { t as IssueAuthorizationCodeAction } from "./issue_authorization_code-B9ERu1uO.js";
|
|
8
|
+
import { DateTime } from "luxon";
|
|
9
|
+
import string from "@adonisjs/core/helpers/string";
|
|
10
|
+
import vine from "@vinejs/vine";
|
|
11
|
+
//#region src/actions/authorize.ts
|
|
12
|
+
/**
|
|
13
|
+
* Handles the OAuth 2.0 authorization request business logic.
|
|
14
|
+
*
|
|
15
|
+
* Validates the client, scopes, and PKCE parameters, checks
|
|
16
|
+
* existing consent, and either issues an authorization code
|
|
17
|
+
* or signals that login/consent is required.
|
|
18
|
+
*
|
|
19
|
+
* Errors before redirect_uri validation are thrown as exceptions.
|
|
20
|
+
* Errors after are returned as `redirect_error` results so the
|
|
21
|
+
* controller can redirect back to the client per spec.
|
|
22
|
+
*/
|
|
23
|
+
var AuthorizeAction = class {
|
|
24
|
+
/**
|
|
25
|
+
* Process an authorization request. Returns a discriminated
|
|
26
|
+
* union that the controller interprets as the appropriate
|
|
27
|
+
* HTTP redirect.
|
|
28
|
+
*/
|
|
29
|
+
async execute(manager, input) {
|
|
30
|
+
if (input.responseType !== "code") throw new E_UNSUPPORTED_RESPONSE_TYPE("Only \"code\" is supported");
|
|
31
|
+
const client = await OAuthClient.query().where("clientId", input.clientId).first();
|
|
32
|
+
if (!client) throw new E_INVALID_CLIENT("Client not found");
|
|
33
|
+
if (client.isDisabled) throw new E_INVALID_CLIENT("Client is disabled");
|
|
34
|
+
if (!client.redirectUris.includes(input.redirectUri)) throw new E_INVALID_REQUEST("Invalid redirect_uri");
|
|
35
|
+
if (!client.grantTypes.includes("authorization_code")) return {
|
|
36
|
+
type: "redirect_error",
|
|
37
|
+
error: "unauthorized_client",
|
|
38
|
+
description: "Client is not allowed to use the authorization_code grant"
|
|
39
|
+
};
|
|
40
|
+
const requestedScopes = input.scope ? input.scope.split(" ") : manager.config.defaultScopes;
|
|
41
|
+
const scopeError = this.#validateScopes(manager, requestedScopes, client);
|
|
42
|
+
if (scopeError) return scopeError;
|
|
43
|
+
const pkceError = this.#validatePkce(input);
|
|
44
|
+
if (pkceError) return pkceError;
|
|
45
|
+
if (!input.userId) return { type: "login_required" };
|
|
46
|
+
return this.#resolveConsent(manager, {
|
|
47
|
+
...input,
|
|
48
|
+
userId: input.userId
|
|
49
|
+
}, client, requestedScopes);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Validate requested scopes against server config, client
|
|
53
|
+
* permissions, and OIDC availability.
|
|
54
|
+
*/
|
|
55
|
+
#validateScopes(manager, scopes, client) {
|
|
56
|
+
const invalidScopes = manager.validateScopes(scopes);
|
|
57
|
+
if (invalidScopes.length > 0) return {
|
|
58
|
+
type: "redirect_error",
|
|
59
|
+
error: "invalid_scope",
|
|
60
|
+
description: `Invalid scopes: ${invalidScopes.join(", ")}`
|
|
61
|
+
};
|
|
62
|
+
const clientService = new ClientService();
|
|
63
|
+
try {
|
|
64
|
+
clientService.validateClientScopes(scopes, client.scopes);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
return {
|
|
67
|
+
type: "redirect_error",
|
|
68
|
+
error: "invalid_scope",
|
|
69
|
+
description: err.message
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (manager.usesOidcScopes(scopes) && !manager.isOidcEnabled) return {
|
|
73
|
+
type: "redirect_error",
|
|
74
|
+
error: "invalid_scope",
|
|
75
|
+
description: "OIDC scopes require OIDC to be configured (set jwk and oidcProvider in config)"
|
|
76
|
+
};
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Ensure PKCE code_challenge is present and uses S256
|
|
81
|
+
* (mandatory per OAuth 2.1).
|
|
82
|
+
*/
|
|
83
|
+
#validatePkce(input) {
|
|
84
|
+
if (!input.codeChallenge) return {
|
|
85
|
+
type: "redirect_error",
|
|
86
|
+
error: "invalid_request",
|
|
87
|
+
description: "PKCE code_challenge is required"
|
|
88
|
+
};
|
|
89
|
+
if (input.codeChallengeMethod !== "S256") return {
|
|
90
|
+
type: "redirect_error",
|
|
91
|
+
error: "invalid_request",
|
|
92
|
+
description: "Only S256 code_challenge_method is supported"
|
|
93
|
+
};
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Check if the user has already consented to all requested
|
|
98
|
+
* scopes. If so, issue the code directly. Otherwise, create
|
|
99
|
+
* a pending authorization request for the consent page.
|
|
100
|
+
*/
|
|
101
|
+
async #resolveConsent(manager, input, client, scopes) {
|
|
102
|
+
const existingConsent = await OAuthConsent.query().where("clientId", client.clientId).where("userId", input.userId).first();
|
|
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 OAuthPendingAuthorizationRequest.create({
|
|
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,7 +1,14 @@
|
|
|
1
|
-
import "./
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import "./chunk-DF48asd8.js";
|
|
2
|
+
import { t as OAuthClient } from "./oauth_client-BSanvSql.js";
|
|
3
|
+
import { o as E_INVALID_REQUEST, r as E_INVALID_CLIENT } from "./oauth_error-C7UhDb2q.js";
|
|
4
4
|
import vine from "@vinejs/vine";
|
|
5
|
+
//#region src/controllers/client_info_controller.ts
|
|
6
|
+
/**
|
|
7
|
+
* Returns public information about an OAuth client.
|
|
8
|
+
* Used by the consent page to display the client's name
|
|
9
|
+
* from server-side data rather than query parameters
|
|
10
|
+
* (RFC 6819 §4.4.1.4 — prevent client identity spoofing).
|
|
11
|
+
*/
|
|
5
12
|
var ClientInfoController = class ClientInfoController {
|
|
6
13
|
static validator = vine.create({ client_id: vine.string() });
|
|
7
14
|
async handle(ctx) {
|
|
@@ -15,4 +22,5 @@ var ClientInfoController = class ClientInfoController {
|
|
|
15
22
|
};
|
|
16
23
|
}
|
|
17
24
|
};
|
|
25
|
+
//#endregion
|
|
18
26
|
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<boolean | void>;
|
|
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,18 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { t as
|
|
3
|
-
import "../
|
|
1
|
+
import "../chunk-DF48asd8.js";
|
|
2
|
+
import { t as SesameManager } from "../sesame_manager-DYUSZ0NC.js";
|
|
3
|
+
import { n as __decorate } from "../oauth_client-BSanvSql.js";
|
|
4
|
+
import "../oauth_error-C7UhDb2q.js";
|
|
5
|
+
import "../oauth_access_token-Cz_5gNBx.js";
|
|
4
6
|
import { BaseCommand, flags } from "@adonisjs/core/ace";
|
|
7
|
+
//#region commands/sesame_purge.ts
|
|
8
|
+
/**
|
|
9
|
+
* Purge revoked and/or expired OAuth tokens and authorization codes.
|
|
10
|
+
*
|
|
11
|
+
* By default, purges both revoked and expired records. Use `--revoked`
|
|
12
|
+
* or `--expired` to target only one category. Expired tokens are
|
|
13
|
+
* retained for a configurable period (default 168h / 7 days) to
|
|
14
|
+
* allow for debugging and audit trails.
|
|
15
|
+
*/
|
|
5
16
|
var SesamePurge = class extends BaseCommand {
|
|
6
17
|
static commandName = "sesame:purge";
|
|
7
18
|
static description = "Purge revoked and/or expired tokens and authorization codes";
|
|
@@ -25,4 +36,5 @@ __decorate([flags.number({
|
|
|
25
36
|
description: "Number of hours to retain expired tokens (default: 168 = 7 days)",
|
|
26
37
|
default: 168
|
|
27
38
|
})], SesamePurge.prototype, "hours", void 0);
|
|
39
|
+
//#endregion
|
|
28
40
|
export { SesamePurge as default };
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
await codemods.makeUsingStub(stubsRoot, "config/sesame.stub", {});
|
|
7
|
+
for (const stub of [
|
|
8
|
+
"migrations/create_oauth_clients_table.stub",
|
|
9
|
+
"migrations/create_oauth_authorization_codes_table.stub",
|
|
10
|
+
"migrations/create_oauth_access_tokens_table.stub",
|
|
11
|
+
"migrations/create_oauth_refresh_tokens_table.stub",
|
|
12
|
+
"migrations/create_oauth_consents_table.stub",
|
|
13
|
+
"migrations/create_oauth_pending_authorization_requests_table.stub"
|
|
14
|
+
]) await codemods.makeUsingStub(stubsRoot, stub, {});
|
|
15
|
+
await codemods.updateRcFile((rcFile) => {
|
|
16
|
+
rcFile.addProvider("@julr/sesame/sesame_provider").addCommand("@julr/sesame/commands");
|
|
17
|
+
});
|
|
18
|
+
await codemods.registerMiddleware("named", [{
|
|
19
|
+
name: "scopes",
|
|
20
|
+
path: "@julr/sesame/scope_middleware"
|
|
21
|
+
}, {
|
|
22
|
+
name: "anyScope",
|
|
23
|
+
path: "@julr/sesame/any_scope_middleware"
|
|
24
|
+
}]);
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { configure as t };
|
package/build/configure.js
CHANGED
|
@@ -1,25 +1,3 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
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 "./chunk-DF48asd8.js";
|
|
2
|
+
import { t as configure } from "./configure-DkDkIlt8.js";
|
|
25
3
|
export { configure };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import "./chunk-DF48asd8.js";
|
|
2
|
+
import { n as OAuthPendingAuthorizationRequest, r as OAuthConsent, t as SesameManager } from "./sesame_manager-DYUSZ0NC.js";
|
|
3
|
+
import { t as OAuthClient } from "./oauth_client-BSanvSql.js";
|
|
4
|
+
import { a as E_INVALID_GRANT, o as E_INVALID_REQUEST, r as E_INVALID_CLIENT } from "./oauth_error-C7UhDb2q.js";
|
|
5
|
+
import "./oauth_access_token-Cz_5gNBx.js";
|
|
6
|
+
import { t as TokenService } from "./token_service-DwnfAR9F.js";
|
|
7
|
+
import { t as IssueAuthorizationCodeAction } from "./issue_authorization_code-B9ERu1uO.js";
|
|
8
|
+
import { DateTime } from "luxon";
|
|
9
|
+
import vine from "@vinejs/vine";
|
|
10
|
+
//#region src/controllers/consent_controller.ts
|
|
11
|
+
/**
|
|
12
|
+
* Handles user consent submission for the OAuth authorization flow.
|
|
13
|
+
*
|
|
14
|
+
* When a user approves access, this controller stores or updates
|
|
15
|
+
* the consent record and issues an authorization code via redirect.
|
|
16
|
+
* When the user denies access, it redirects back to the client
|
|
17
|
+
* with an `access_denied` error.
|
|
18
|
+
*
|
|
19
|
+
* Consent records are persisted so that returning users are not
|
|
20
|
+
* prompted again for previously approved scopes.
|
|
21
|
+
*
|
|
22
|
+
* @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
|
|
23
|
+
*/
|
|
24
|
+
var ConsentController = class ConsentController {
|
|
25
|
+
static validator = vine.create({ auth_token: vine.string() });
|
|
26
|
+
/**
|
|
27
|
+
* Validate the consent submission, consume the pending
|
|
28
|
+
* request, and redirect back to the client with either
|
|
29
|
+
* an authorization code or an access_denied error.
|
|
30
|
+
*/
|
|
31
|
+
async handle(ctx) {
|
|
32
|
+
const manager = await ctx.containerResolver.make(SesameManager);
|
|
33
|
+
await ctx.auth.check();
|
|
34
|
+
const user = ctx.auth.user;
|
|
35
|
+
if (!user) throw new E_INVALID_REQUEST("User must be authenticated");
|
|
36
|
+
const [error, body] = await ConsentController.validator.tryValidate(ctx.request.body());
|
|
37
|
+
if (error) throw new E_INVALID_REQUEST("Missing required parameter: auth_token");
|
|
38
|
+
const userId = String(user.id);
|
|
39
|
+
const hashedToken = new TokenService(manager).hashToken(body.auth_token);
|
|
40
|
+
const pendingRequest = await this.#consumePendingRequest(hashedToken, userId);
|
|
41
|
+
if (!pendingRequest) throw new E_INVALID_GRANT("Authorization request not found or expired");
|
|
42
|
+
const client = await OAuthClient.query().where("clientId", pendingRequest.clientId).first();
|
|
43
|
+
if (!client) throw new E_INVALID_CLIENT("Client not found");
|
|
44
|
+
if (client.isDisabled) throw new E_INVALID_CLIENT("Client is disabled");
|
|
45
|
+
if (!client.redirectUris.includes(pendingRequest.redirectUri)) throw new E_INVALID_REQUEST("Invalid redirect_uri");
|
|
46
|
+
if (!ctx.request.body().accept) return this.#redirectWithDenied(ctx, manager, pendingRequest);
|
|
47
|
+
await this.#persistConsent(client.clientId, userId, pendingRequest.scopes);
|
|
48
|
+
const code = await new IssueAuthorizationCodeAction().execute(manager, {
|
|
49
|
+
client,
|
|
50
|
+
userId,
|
|
51
|
+
scopes: pendingRequest.scopes,
|
|
52
|
+
redirectUri: pendingRequest.redirectUri,
|
|
53
|
+
codeChallenge: pendingRequest.codeChallenge ?? void 0,
|
|
54
|
+
codeChallengeMethod: pendingRequest.codeChallengeMethod ?? void 0,
|
|
55
|
+
nonce: pendingRequest.nonce ?? void 0
|
|
56
|
+
});
|
|
57
|
+
const url = new URL(pendingRequest.redirectUri);
|
|
58
|
+
url.searchParams.set("code", code);
|
|
59
|
+
if (pendingRequest.state) url.searchParams.set("state", pendingRequest.state);
|
|
60
|
+
url.searchParams.set("iss", manager.config.issuer);
|
|
61
|
+
return ctx.response.redirect().toPath(url.toString());
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Atomically consume a pending authorization request.
|
|
65
|
+
* Uses DELETE-by-PK with affected-row check to prevent
|
|
66
|
+
* concurrent consent submissions from producing two
|
|
67
|
+
* authorization codes.
|
|
68
|
+
*/
|
|
69
|
+
async #consumePendingRequest(hashedToken, userId) {
|
|
70
|
+
const row = await OAuthPendingAuthorizationRequest.query().where("token", hashedToken).where("userId", userId).where("expiresAt", ">", DateTime.now().toSQL()).first();
|
|
71
|
+
if (!row) return null;
|
|
72
|
+
const deleted = await OAuthPendingAuthorizationRequest.query().where("id", row.id).delete();
|
|
73
|
+
if ((Array.isArray(deleted) ? Number(deleted[0]) : Number(deleted)) === 0) return null;
|
|
74
|
+
return row;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Persist or merge consent so future authorization requests
|
|
78
|
+
* for the same client skip the consent screen.
|
|
79
|
+
*/
|
|
80
|
+
async #persistConsent(clientId, userId, scopes) {
|
|
81
|
+
const existingConsent = await OAuthConsent.query().where("clientId", clientId).where("userId", userId).first();
|
|
82
|
+
if (existingConsent) {
|
|
83
|
+
existingConsent.scopes = [...new Set([...existingConsent.scopes, ...scopes])];
|
|
84
|
+
await existingConsent.save();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
await OAuthConsent.create({
|
|
88
|
+
id: crypto.randomUUID(),
|
|
89
|
+
clientId,
|
|
90
|
+
userId,
|
|
91
|
+
scopes
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Redirect back to the client with an access_denied error
|
|
96
|
+
* when the user denies the authorization request.
|
|
97
|
+
*/
|
|
98
|
+
#redirectWithDenied(ctx, manager, pendingRequest) {
|
|
99
|
+
const url = new URL(pendingRequest.redirectUri);
|
|
100
|
+
url.searchParams.set("error", "access_denied");
|
|
101
|
+
url.searchParams.set("error_description", "The user denied the authorization request");
|
|
102
|
+
if (pendingRequest.state) url.searchParams.set("state", pendingRequest.state);
|
|
103
|
+
url.searchParams.set("iss", manager.config.issuer);
|
|
104
|
+
return ctx.response.redirect().toPath(url.toString());
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
//#endregion
|
|
108
|
+
export { ConsentController as default };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { l as RESERVED_OIDC_CLAIMS } from "./sesame_manager-DYUSZ0NC.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,7 +1,7 @@
|
|
|
1
1
|
export { configure } from './configure.ts';
|
|
2
2
|
export { defineConfig } from './src/define_config.ts';
|
|
3
3
|
export { SesameManager } from './src/sesame_manager.ts';
|
|
4
|
-
export {
|
|
4
|
+
export type { CreateClientOptions, CreateClientResult, UpdateClientOptions } from './src/types.ts';
|
|
5
5
|
export { OAuthClient } from './src/models/oauth_client.ts';
|
|
6
6
|
export { OAuthAccessToken } from './src/models/oauth_access_token.ts';
|
|
7
7
|
export { OAuthRefreshToken } from './src/models/oauth_refresh_token.ts';
|
package/build/index.js
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import "./
|
|
4
|
-
import { t as
|
|
5
|
-
import
|
|
6
|
-
import { t as
|
|
7
|
-
import "./token_service-
|
|
8
|
-
import { i as OAuthGuard, n as oauthUserProvider, r as OAuthLucidUserProvider, t as oauthGuard } from "./main-
|
|
1
|
+
import "./chunk-DF48asd8.js";
|
|
2
|
+
import { t as configure } from "./configure-DkDkIlt8.js";
|
|
3
|
+
import { a as OAuthRefreshToken, i as OAuthAuthorizationCode, r as OAuthConsent, t as SesameManager } from "./sesame_manager-DYUSZ0NC.js";
|
|
4
|
+
import { t as OAuthClient } from "./oauth_client-BSanvSql.js";
|
|
5
|
+
import "./oauth_error-C7UhDb2q.js";
|
|
6
|
+
import { t as OAuthAccessToken } from "./oauth_access_token-Cz_5gNBx.js";
|
|
7
|
+
import "./token_service-DwnfAR9F.js";
|
|
8
|
+
import { i as OAuthGuard, n as oauthUserProvider, r as OAuthLucidUserProvider, t as oauthGuard } from "./main-DGBJhq3E.js";
|
|
9
|
+
//#region src/define_config.ts
|
|
10
|
+
/**
|
|
11
|
+
* Resolve user-supplied `SesameConfig` into a `ResolvedSesameConfig`
|
|
12
|
+
* by applying sensible defaults for all optional fields.
|
|
13
|
+
*
|
|
14
|
+
* Defaults:
|
|
15
|
+
* - `scopes`: `{}`
|
|
16
|
+
* - `defaultScopes`: `[]`
|
|
17
|
+
* - `grantTypes`: `['authorization_code', 'refresh_token']`
|
|
18
|
+
* - `accessTokenTtl`: `'1h'`
|
|
19
|
+
* - `refreshTokenTtl`: `'30d'`
|
|
20
|
+
* - `authorizationCodeTtl`: `'10m'`
|
|
21
|
+
* - `allowDynamicRegistration`: `false`
|
|
22
|
+
* - `allowPublicRegistration`: `false`
|
|
23
|
+
*/
|
|
9
24
|
function defineConfig(config) {
|
|
10
25
|
return {
|
|
11
26
|
issuer: config.issuer,
|
|
@@ -15,12 +30,17 @@ function defineConfig(config) {
|
|
|
15
30
|
accessTokenTtl: config.accessTokenTtl ?? "1h",
|
|
16
31
|
clientCredentialsAccessTokenTtl: config.clientCredentialsAccessTokenTtl ?? config.accessTokenTtl ?? "1h",
|
|
17
32
|
refreshTokenTtl: config.refreshTokenTtl ?? "30d",
|
|
33
|
+
refreshTokenRotationGracePeriod: config.refreshTokenRotationGracePeriod ?? 120,
|
|
18
34
|
authorizationCodeTtl: config.authorizationCodeTtl ?? "10m",
|
|
19
35
|
authorizationRequestTtl: config.authorizationRequestTtl ?? config.authorizationCodeTtl ?? "10m",
|
|
20
36
|
loginPage: config.loginPage,
|
|
21
37
|
consentPage: config.consentPage,
|
|
22
38
|
allowDynamicRegistration: config.allowDynamicRegistration ?? false,
|
|
23
|
-
allowPublicRegistration: config.allowPublicRegistration ?? false
|
|
39
|
+
allowPublicRegistration: config.allowPublicRegistration ?? false,
|
|
40
|
+
jwk: config.jwk,
|
|
41
|
+
oidcProvider: config.oidcProvider,
|
|
42
|
+
idTokenTtl: config.idTokenTtl ?? "1h"
|
|
24
43
|
};
|
|
25
44
|
}
|
|
26
|
-
|
|
45
|
+
//#endregion
|
|
46
|
+
export { OAuthAccessToken, OAuthAuthorizationCode, OAuthClient, OAuthConsent, OAuthGuard, OAuthLucidUserProvider, OAuthRefreshToken, SesameManager, configure, defineConfig, oauthGuard, oauthUserProvider };
|