@better-auth/oauth-provider 1.5.0-beta.9 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/dist/client-resource.d.mts +6 -8
- package/dist/client-resource.mjs +4 -3
- package/dist/client-resource.mjs.map +1 -0
- package/dist/client.d.mts +4 -3
- package/dist/client.mjs +2 -1
- package/dist/client.mjs.map +1 -0
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +678 -523
- package/dist/index.mjs.map +1 -0
- package/dist/{oauth-BrFoF22H.d.mts → oauth-7Jc-EFsq.d.mts} +118 -19
- package/dist/{oauth-BW67CKnu.d.mts → oauth-C_QoLKZA.d.mts} +55 -12
- package/dist/{utils-CUVT0Bep.mjs → utils-D6kv_BUA.mjs} +54 -20
- package/dist/utils-D6kv_BUA.mjs.map +1 -0
- package/package.json +38 -35
package/dist/index.mjs
CHANGED
|
@@ -1,221 +1,58 @@
|
|
|
1
|
-
import { a as getJwtPlugin, c as
|
|
1
|
+
import { a as getJwtPlugin, c as isPKCERequired, d as storeClientSecret, f as storeToken, h as mcpHandler, i as getClient, l as parseClientMetadata, n as decryptStoredClientSecret, p as validateClientCredentials, r as deleteFromPrompt, s as getStoredToken, t as basicToClientCredentials, u as parsePrompt } from "./utils-D6kv_BUA.mjs";
|
|
2
|
+
import { APIError, createAuthEndpoint, createAuthMiddleware, getOAuthState, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
|
|
2
3
|
import { generateCodeChallenge, getJwks, verifyJwsAccessToken } from "better-auth/oauth2";
|
|
3
|
-
import { APIError } from "better-call";
|
|
4
|
-
import { BetterAuthError } from "@better-auth/core/error";
|
|
4
|
+
import { APIError as APIError$1 } from "better-call";
|
|
5
5
|
import { constantTimeEqual, generateRandomString, makeSignature } from "better-auth/crypto";
|
|
6
6
|
import { defineRequestState } from "@better-auth/core/context";
|
|
7
7
|
import { logger } from "@better-auth/core/env";
|
|
8
|
-
import {
|
|
8
|
+
import { BetterAuthError } from "@better-auth/core/error";
|
|
9
9
|
import { parseSetCookieHeader } from "better-auth/cookies";
|
|
10
10
|
import { mergeSchema } from "better-auth/db";
|
|
11
11
|
import * as z from "zod";
|
|
12
12
|
import { signJWT, toExpJWT } from "better-auth/plugins";
|
|
13
13
|
import { SignJWT, compactVerify, createLocalJWKSet, decodeJwt } from "jose";
|
|
14
14
|
|
|
15
|
-
//#region src/
|
|
16
|
-
function
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
issuer: opts?.jwt?.issuer ?? baseURL,
|
|
21
|
-
authorization_endpoint: `${baseURL}/oauth2/authorize`,
|
|
22
|
-
token_endpoint: `${baseURL}/oauth2/token`,
|
|
23
|
-
jwks_uri: overrides?.jwt_disabled ? void 0 : opts?.jwks?.remoteUrl ?? `${baseURL}${opts?.jwks?.jwksPath ?? "/jwks"}`,
|
|
24
|
-
registration_endpoint: `${baseURL}/oauth2/register`,
|
|
25
|
-
introspection_endpoint: `${baseURL}/oauth2/introspect`,
|
|
26
|
-
revocation_endpoint: `${baseURL}/oauth2/revoke`,
|
|
27
|
-
response_types_supported: overrides?.grant_types_supported && !overrides.grant_types_supported.includes("authorization_code") ? [] : ["code"],
|
|
28
|
-
response_modes_supported: ["query"],
|
|
29
|
-
grant_types_supported: overrides?.grant_types_supported ?? [
|
|
30
|
-
"authorization_code",
|
|
31
|
-
"client_credentials",
|
|
32
|
-
"refresh_token"
|
|
33
|
-
],
|
|
34
|
-
token_endpoint_auth_methods_supported: [
|
|
35
|
-
...overrides?.public_client_supported ? ["none"] : [],
|
|
36
|
-
"client_secret_basic",
|
|
37
|
-
"client_secret_post"
|
|
38
|
-
],
|
|
39
|
-
introspection_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post"],
|
|
40
|
-
revocation_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post"],
|
|
41
|
-
code_challenge_methods_supported: ["S256"]
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
function oidcServerMetadata(ctx, opts) {
|
|
45
|
-
const baseURL = ctx.context.baseURL;
|
|
46
|
-
const jwtPluginOptions = opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context).options;
|
|
47
|
-
return {
|
|
48
|
-
...authServerMetadata(ctx, jwtPluginOptions, {
|
|
49
|
-
scopes_supported: opts.advertisedMetadata?.scopes_supported ?? opts.scopes,
|
|
50
|
-
public_client_supported: opts.allowUnauthenticatedClientRegistration,
|
|
51
|
-
grant_types_supported: opts.grantTypes,
|
|
52
|
-
jwt_disabled: opts.disableJwtPlugin
|
|
53
|
-
}),
|
|
54
|
-
claims_supported: opts?.advertisedMetadata?.claims_supported ?? opts?.claims ?? [],
|
|
55
|
-
userinfo_endpoint: `${baseURL}/oauth2/userinfo`,
|
|
56
|
-
subject_types_supported: ["public"],
|
|
57
|
-
id_token_signing_alg_values_supported: jwtPluginOptions?.jwks?.keyPairConfig?.alg ? [jwtPluginOptions?.jwks?.keyPairConfig?.alg] : opts.disableJwtPlugin ? ["HS256"] : ["EdDSA"],
|
|
58
|
-
end_session_endpoint: `${baseURL}/oauth2/end-session`,
|
|
59
|
-
acr_values_supported: ["urn:mace:incommon:iap:bronze"],
|
|
60
|
-
prompt_values_supported: [
|
|
61
|
-
"login",
|
|
62
|
-
"consent",
|
|
63
|
-
"create",
|
|
64
|
-
"select_account"
|
|
65
|
-
]
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Provides an exportable `/.well-known/oauth-authorization-server`.
|
|
70
|
-
*
|
|
71
|
-
* Useful when basePath prevents the endpoint from being located at the root
|
|
72
|
-
* and must be provided manually.
|
|
73
|
-
*
|
|
74
|
-
* @external
|
|
75
|
-
*/
|
|
76
|
-
const oauthProviderAuthServerMetadata = (auth, opts) => {
|
|
77
|
-
return async (_request) => {
|
|
78
|
-
const res = await auth.api.getOAuthServerConfig();
|
|
79
|
-
return new Response(JSON.stringify(res), {
|
|
80
|
-
status: 200,
|
|
81
|
-
headers: {
|
|
82
|
-
"Cache-Control": "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400",
|
|
83
|
-
...opts?.headers,
|
|
84
|
-
"Content-Type": "application/json"
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
};
|
|
88
|
-
};
|
|
89
|
-
/**
|
|
90
|
-
* Provides an exportable `/.well-known/openid-configuration`.
|
|
91
|
-
*
|
|
92
|
-
* Useful when basePath prevents the endpoint from being located at the root
|
|
93
|
-
* and must be provided manually.
|
|
94
|
-
*
|
|
95
|
-
* @external
|
|
96
|
-
*/
|
|
97
|
-
const oauthProviderOpenIdConfigMetadata = (auth, opts) => {
|
|
98
|
-
return async (_request) => {
|
|
99
|
-
const res = await auth.api.getOpenIdConfig();
|
|
100
|
-
return new Response(JSON.stringify(res), {
|
|
101
|
-
status: 200,
|
|
102
|
-
headers: {
|
|
103
|
-
"Cache-Control": "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400",
|
|
104
|
-
...opts?.headers,
|
|
105
|
-
"Content-Type": "application/json"
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
};
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
//#endregion
|
|
112
|
-
//#region src/authorize.ts
|
|
113
|
-
/**
|
|
114
|
-
* Formats an error url
|
|
115
|
-
*/
|
|
116
|
-
function formatErrorURL(url, error, description, state) {
|
|
117
|
-
const searchParams = new URLSearchParams({
|
|
118
|
-
error,
|
|
119
|
-
error_description: description
|
|
120
|
-
});
|
|
121
|
-
state && searchParams.append("state", state);
|
|
122
|
-
return `${url}${url.includes("?") ? "&" : "?"}${searchParams.toString()}`;
|
|
123
|
-
}
|
|
124
|
-
const handleRedirect = (ctx, uri) => {
|
|
125
|
-
if (ctx.headers?.get("accept")?.includes("application/json")) return {
|
|
126
|
-
redirect: true,
|
|
127
|
-
url: uri.toString()
|
|
128
|
-
};
|
|
129
|
-
else throw ctx.redirect(uri);
|
|
130
|
-
};
|
|
131
|
-
/**
|
|
132
|
-
* Error page url if redirect_uri has not been verified yet
|
|
133
|
-
* Generates Url for custom error page
|
|
134
|
-
*/
|
|
135
|
-
function getErrorURL(ctx, error, description) {
|
|
136
|
-
return formatErrorURL(ctx.context.options.onAPIError?.errorURL || `${ctx.context.baseURL}/error`, error, description);
|
|
137
|
-
}
|
|
138
|
-
async function authorizeEndpoint(ctx, opts, settings) {
|
|
139
|
-
if (opts.grantTypes && !opts.grantTypes.includes("authorization_code")) throw new APIError("NOT_FOUND");
|
|
140
|
-
if (!ctx.request) throw new APIError("UNAUTHORIZED", {
|
|
141
|
-
error_description: "request not found",
|
|
15
|
+
//#region src/consent.ts
|
|
16
|
+
async function consentEndpoint(ctx, opts) {
|
|
17
|
+
const _query = (await oAuthState.get())?.query;
|
|
18
|
+
if (!_query) throw new APIError("BAD_REQUEST", {
|
|
19
|
+
error_description: "missing oauth query",
|
|
142
20
|
error: "invalid_request"
|
|
143
21
|
});
|
|
144
|
-
const query =
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (client.disabled) throw ctx.redirect(getErrorURL(ctx, "client_disabled", "client is disabled"));
|
|
153
|
-
if (!client.redirectUris?.find((url) => url === query.redirect_uri) || !query.redirect_uri) throw ctx.redirect(getErrorURL(ctx, "invalid_redirect", "invalid redirect uri"));
|
|
154
|
-
let requestedScopes = query.scope?.split(" ").filter((s) => s);
|
|
22
|
+
const query = new URLSearchParams(_query);
|
|
23
|
+
const originalRequestedScopes = query.get("scope")?.split(" ") ?? [];
|
|
24
|
+
const clientId = query.get("client_id");
|
|
25
|
+
if (!clientId) throw new APIError("BAD_REQUEST", {
|
|
26
|
+
error_description: "client_id is required",
|
|
27
|
+
error: "invalid_client"
|
|
28
|
+
});
|
|
29
|
+
const requestedScopes = ctx.body.scope?.split(" ");
|
|
155
30
|
if (requestedScopes) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
31
|
+
if (!requestedScopes.every((sc) => originalRequestedScopes?.includes(sc))) throw new APIError("BAD_REQUEST", {
|
|
32
|
+
error_description: "Scope not originally requested",
|
|
33
|
+
error: "invalid_request"
|
|
159
34
|
});
|
|
160
|
-
if (invalidScopes.length) throw ctx.redirect(formatErrorURL(query.redirect_uri, "invalid_scope", `The following scopes are invalid: ${invalidScopes.join(", ")}`, query.state));
|
|
161
|
-
}
|
|
162
|
-
if (!requestedScopes) {
|
|
163
|
-
requestedScopes = client.scopes ?? opts.scopes ?? [];
|
|
164
|
-
query.scope = requestedScopes.join(" ");
|
|
165
35
|
}
|
|
166
|
-
if (!
|
|
167
|
-
|
|
36
|
+
if (!(ctx.body.accept === true)) return {
|
|
37
|
+
redirect: true,
|
|
38
|
+
url: formatErrorURL(query.get("redirect_uri") ?? "", "access_denied", "User denied access", query.get("state") ?? void 0, getIssuer(ctx, opts))
|
|
39
|
+
};
|
|
168
40
|
const session = await getSessionFromCtx(ctx);
|
|
169
|
-
if (!session || promptSet?.has("login") || promptSet?.has("create")) return redirectWithPromptCode(ctx, opts, promptSet?.has("create") ? "create" : "login");
|
|
170
|
-
if (settings?.isAuthorize && promptSet?.has("select_account")) return redirectWithPromptCode(ctx, opts, "select_account");
|
|
171
|
-
if (settings?.isAuthorize && opts.selectAccount) {
|
|
172
|
-
if (await opts.selectAccount.shouldRedirect({
|
|
173
|
-
headers: ctx.request.headers,
|
|
174
|
-
user: session.user,
|
|
175
|
-
session: session.session,
|
|
176
|
-
scopes: requestedScopes
|
|
177
|
-
})) return redirectWithPromptCode(ctx, opts, "select_account");
|
|
178
|
-
}
|
|
179
|
-
if (opts.signup?.shouldRedirect) {
|
|
180
|
-
const signupRedirect = await opts.signup.shouldRedirect({
|
|
181
|
-
headers: ctx.request.headers,
|
|
182
|
-
user: session.user,
|
|
183
|
-
session: session.session,
|
|
184
|
-
scopes: requestedScopes
|
|
185
|
-
});
|
|
186
|
-
if (signupRedirect) return redirectWithPromptCode(ctx, opts, "create", typeof signupRedirect === "string" ? signupRedirect : void 0);
|
|
187
|
-
}
|
|
188
|
-
if (!settings?.postLogin && opts.postLogin) {
|
|
189
|
-
if (await opts.postLogin.shouldRedirect({
|
|
190
|
-
headers: ctx.request.headers,
|
|
191
|
-
user: session.user,
|
|
192
|
-
session: session.session,
|
|
193
|
-
scopes: requestedScopes
|
|
194
|
-
})) return redirectWithPromptCode(ctx, opts, "post_login");
|
|
195
|
-
}
|
|
196
|
-
if (promptSet?.has("consent")) return redirectWithPromptCode(ctx, opts, "consent");
|
|
197
41
|
const referenceId = await opts.postLogin?.consentReferenceId?.({
|
|
198
|
-
user: session
|
|
199
|
-
session: session
|
|
200
|
-
scopes: requestedScopes
|
|
201
|
-
});
|
|
202
|
-
if (client.skipConsent) return redirectWithAuthorizationCode(ctx, opts, {
|
|
203
|
-
query,
|
|
204
|
-
clientId: client.clientId,
|
|
205
|
-
userId: session.user.id,
|
|
206
|
-
sessionId: session.session.id,
|
|
207
|
-
referenceId
|
|
42
|
+
user: session?.user,
|
|
43
|
+
session: session?.session,
|
|
44
|
+
scopes: requestedScopes ?? originalRequestedScopes
|
|
208
45
|
});
|
|
209
|
-
const
|
|
46
|
+
const foundConsent = await ctx.context.adapter.findOne({
|
|
210
47
|
model: "oauthConsent",
|
|
211
48
|
where: [
|
|
212
49
|
{
|
|
213
50
|
field: "clientId",
|
|
214
|
-
value:
|
|
51
|
+
value: clientId
|
|
215
52
|
},
|
|
216
53
|
{
|
|
217
54
|
field: "userId",
|
|
218
|
-
value: session
|
|
55
|
+
value: session?.user.id
|
|
219
56
|
},
|
|
220
57
|
...referenceId ? [{
|
|
221
58
|
field: "referenceId",
|
|
@@ -223,142 +60,40 @@ async function authorizeEndpoint(ctx, opts, settings) {
|
|
|
223
60
|
}] : []
|
|
224
61
|
]
|
|
225
62
|
});
|
|
226
|
-
if (!consent || !requestedScopes.every((val) => consent.scopes.includes(val))) return redirectWithPromptCode(ctx, opts, "consent");
|
|
227
|
-
return redirectWithAuthorizationCode(ctx, opts, {
|
|
228
|
-
query,
|
|
229
|
-
clientId: client.clientId,
|
|
230
|
-
userId: session.user.id,
|
|
231
|
-
sessionId: session.session.id,
|
|
232
|
-
referenceId
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
async function redirectWithAuthorizationCode(ctx, opts, verificationValue) {
|
|
236
|
-
const code = generateRandomString(32, "a-z", "A-Z", "0-9");
|
|
237
63
|
const iat = Math.floor(Date.now() / 1e3);
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
64
|
+
const consent = {
|
|
65
|
+
clientId,
|
|
66
|
+
userId: session?.user.id,
|
|
67
|
+
scopes: requestedScopes ?? originalRequestedScopes,
|
|
68
|
+
createdAt: /* @__PURE__ */ new Date(iat * 1e3),
|
|
241
69
|
updatedAt: /* @__PURE__ */ new Date(iat * 1e3),
|
|
242
|
-
|
|
243
|
-
value: JSON.stringify({
|
|
244
|
-
type: "authorization_code",
|
|
245
|
-
query: ctx.query,
|
|
246
|
-
userId: verificationValue.userId,
|
|
247
|
-
sessionId: verificationValue?.sessionId,
|
|
248
|
-
referenceId: verificationValue.referenceId
|
|
249
|
-
})
|
|
70
|
+
referenceId
|
|
250
71
|
};
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
} else if (type === "consent") path = opts.consentPage;
|
|
268
|
-
else if (type === "create") path = opts.signup?.page ?? opts.loginPage;
|
|
269
|
-
return handleRedirect(ctx, `${page ?? path}?${queryParams}`);
|
|
270
|
-
}
|
|
271
|
-
async function signParams(ctx, opts) {
|
|
272
|
-
const exp = Math.floor(Date.now() / 1e3) + (opts.codeExpiresIn ?? 600);
|
|
273
|
-
const params = new URLSearchParams(ctx.query);
|
|
274
|
-
params.set("exp", String(exp));
|
|
275
|
-
const signature = await makeSignature(params.toString(), ctx.context.secret);
|
|
276
|
-
params.append("sig", signature);
|
|
277
|
-
return params.toString();
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
//#endregion
|
|
281
|
-
//#region src/consent.ts
|
|
282
|
-
async function consentEndpoint(ctx, opts) {
|
|
283
|
-
const _query = (await oAuthState.get())?.query;
|
|
284
|
-
if (!_query) throw new APIError$1("BAD_REQUEST", {
|
|
285
|
-
error_description: "missing oauth query",
|
|
286
|
-
error: "invalid_request"
|
|
287
|
-
});
|
|
288
|
-
const query = new URLSearchParams(_query);
|
|
289
|
-
const originalRequestedScopes = query.get("scope")?.split(" ") ?? [];
|
|
290
|
-
const clientId = query.get("client_id");
|
|
291
|
-
if (!clientId) throw new APIError$1("BAD_REQUEST", {
|
|
292
|
-
error_description: "client_id is required",
|
|
293
|
-
error: "invalid_client"
|
|
294
|
-
});
|
|
295
|
-
const requestedScopes = ctx.body.scope?.split(" ");
|
|
296
|
-
if (requestedScopes) {
|
|
297
|
-
if (!requestedScopes.every((sc) => originalRequestedScopes?.includes(sc))) throw new APIError$1("BAD_REQUEST", {
|
|
298
|
-
error_description: "Scope not originally requested",
|
|
299
|
-
error: "invalid_request"
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
if (!(ctx.body.accept === true)) return {
|
|
303
|
-
redirect: true,
|
|
304
|
-
uri: formatErrorURL(query.get("redirect_uri") ?? "", "access_denied", "User denied access", query.get("state") ?? void 0)
|
|
305
|
-
};
|
|
306
|
-
const session = await getSessionFromCtx(ctx);
|
|
307
|
-
const referenceId = await opts.postLogin?.consentReferenceId?.({
|
|
308
|
-
user: session?.user,
|
|
309
|
-
session: session?.session,
|
|
310
|
-
scopes: requestedScopes ?? originalRequestedScopes
|
|
311
|
-
});
|
|
312
|
-
const foundConsent = await ctx.context.adapter.findOne({
|
|
313
|
-
model: "oauthConsent",
|
|
314
|
-
where: [
|
|
315
|
-
{
|
|
316
|
-
field: "clientId",
|
|
317
|
-
value: clientId
|
|
318
|
-
},
|
|
319
|
-
{
|
|
320
|
-
field: "userId",
|
|
321
|
-
value: session?.user.id
|
|
322
|
-
},
|
|
323
|
-
...referenceId ? [{
|
|
324
|
-
field: "referenceId",
|
|
325
|
-
value: referenceId
|
|
326
|
-
}] : []
|
|
327
|
-
]
|
|
328
|
-
});
|
|
329
|
-
const iat = Math.floor(Date.now() / 1e3);
|
|
330
|
-
const consent = {
|
|
331
|
-
clientId,
|
|
332
|
-
userId: session?.user.id,
|
|
333
|
-
scopes: requestedScopes ?? originalRequestedScopes,
|
|
334
|
-
createdAt: /* @__PURE__ */ new Date(iat * 1e3),
|
|
335
|
-
updatedAt: /* @__PURE__ */ new Date(iat * 1e3),
|
|
336
|
-
referenceId
|
|
337
|
-
};
|
|
338
|
-
foundConsent?.id ? await ctx.context.adapter.update({
|
|
339
|
-
model: "oauthConsent",
|
|
340
|
-
where: [{
|
|
341
|
-
field: "id",
|
|
342
|
-
value: foundConsent.id
|
|
343
|
-
}],
|
|
344
|
-
update: {
|
|
345
|
-
scopes: consent.scopes,
|
|
346
|
-
updatedAt: /* @__PURE__ */ new Date(iat * 1e3)
|
|
347
|
-
}
|
|
348
|
-
}) : await ctx.context.adapter.create({
|
|
349
|
-
model: "oauthConsent",
|
|
350
|
-
data: {
|
|
351
|
-
...consent,
|
|
352
|
-
scopes: consent.scopes
|
|
353
|
-
}
|
|
72
|
+
foundConsent?.id ? await ctx.context.adapter.update({
|
|
73
|
+
model: "oauthConsent",
|
|
74
|
+
where: [{
|
|
75
|
+
field: "id",
|
|
76
|
+
value: foundConsent.id
|
|
77
|
+
}],
|
|
78
|
+
update: {
|
|
79
|
+
scopes: consent.scopes,
|
|
80
|
+
updatedAt: /* @__PURE__ */ new Date(iat * 1e3)
|
|
81
|
+
}
|
|
82
|
+
}) : await ctx.context.adapter.create({
|
|
83
|
+
model: "oauthConsent",
|
|
84
|
+
data: {
|
|
85
|
+
...consent,
|
|
86
|
+
scopes: consent.scopes
|
|
87
|
+
}
|
|
354
88
|
});
|
|
89
|
+
if (requestedScopes) query.set("scope", consent.scopes.join(" "));
|
|
355
90
|
ctx?.headers?.set("accept", "application/json");
|
|
356
91
|
ctx.query = deleteFromPrompt(query, "consent");
|
|
357
92
|
ctx.context.postLogin = true;
|
|
358
93
|
const { url } = await authorizeEndpoint(ctx, opts);
|
|
359
94
|
return {
|
|
360
95
|
redirect: true,
|
|
361
|
-
|
|
96
|
+
url
|
|
362
97
|
};
|
|
363
98
|
}
|
|
364
99
|
|
|
@@ -368,14 +103,14 @@ async function continueEndpoint(ctx, opts) {
|
|
|
368
103
|
if (ctx.body.selected === true) return await selected(ctx, opts);
|
|
369
104
|
else if (ctx.body.created === true) return await created(ctx, opts);
|
|
370
105
|
else if (ctx.body.postLogin === true) return await postLogin(ctx, opts);
|
|
371
|
-
else throw new APIError
|
|
106
|
+
else throw new APIError("BAD_REQUEST", {
|
|
372
107
|
error_description: "Missing parameters",
|
|
373
108
|
error: "invalid_request"
|
|
374
109
|
});
|
|
375
110
|
}
|
|
376
111
|
async function selected(ctx, opts) {
|
|
377
112
|
const _query = (await oAuthState.get())?.query;
|
|
378
|
-
if (!_query) throw new APIError
|
|
113
|
+
if (!_query) throw new APIError("BAD_REQUEST", {
|
|
379
114
|
error_description: "missing oauth query",
|
|
380
115
|
error: "invalid_request"
|
|
381
116
|
});
|
|
@@ -384,12 +119,12 @@ async function selected(ctx, opts) {
|
|
|
384
119
|
const { url } = await authorizeEndpoint(ctx, opts);
|
|
385
120
|
return {
|
|
386
121
|
redirect: true,
|
|
387
|
-
|
|
122
|
+
url
|
|
388
123
|
};
|
|
389
124
|
}
|
|
390
125
|
async function created(ctx, opts) {
|
|
391
126
|
const _query = (await oAuthState.get())?.query;
|
|
392
|
-
if (!_query) throw new APIError
|
|
127
|
+
if (!_query) throw new APIError("BAD_REQUEST", {
|
|
393
128
|
error_description: "missing oauth query",
|
|
394
129
|
error: "invalid_request"
|
|
395
130
|
});
|
|
@@ -397,12 +132,12 @@ async function created(ctx, opts) {
|
|
|
397
132
|
const { url } = await authorizeEndpoint(ctx, opts);
|
|
398
133
|
return {
|
|
399
134
|
redirect: true,
|
|
400
|
-
|
|
135
|
+
url
|
|
401
136
|
};
|
|
402
137
|
}
|
|
403
138
|
async function postLogin(ctx, opts) {
|
|
404
139
|
const _query = (await oAuthState.get())?.query;
|
|
405
|
-
if (!_query) throw new APIError
|
|
140
|
+
if (!_query) throw new APIError("BAD_REQUEST", {
|
|
406
141
|
error_description: "missing oauth query",
|
|
407
142
|
error: "invalid_request"
|
|
408
143
|
});
|
|
@@ -412,7 +147,7 @@ async function postLogin(ctx, opts) {
|
|
|
412
147
|
const { url } = await authorizeEndpoint(ctx, opts, { postLogin: true });
|
|
413
148
|
return {
|
|
414
149
|
redirect: true,
|
|
415
|
-
|
|
150
|
+
url
|
|
416
151
|
};
|
|
417
152
|
}
|
|
418
153
|
|
|
@@ -445,28 +180,28 @@ function userNormalClaims(user, scopes) {
|
|
|
445
180
|
* Handles the /oauth2/userinfo endpoint
|
|
446
181
|
*/
|
|
447
182
|
async function userInfoEndpoint(ctx, opts) {
|
|
448
|
-
if (!ctx.request) throw new APIError
|
|
183
|
+
if (!ctx.request) throw new APIError("UNAUTHORIZED", {
|
|
449
184
|
error_description: "request not found",
|
|
450
185
|
error: "invalid_request"
|
|
451
186
|
});
|
|
452
187
|
const authorization = ctx.request.headers.get("authorization");
|
|
453
188
|
const token = typeof authorization === "string" && authorization?.startsWith("Bearer ") ? authorization?.replace("Bearer ", "") : authorization;
|
|
454
|
-
if (!token?.length) throw new APIError
|
|
189
|
+
if (!token?.length) throw new APIError("UNAUTHORIZED", {
|
|
455
190
|
error_description: "authorization header not found",
|
|
456
191
|
error: "invalid_request"
|
|
457
192
|
});
|
|
458
193
|
const jwt = await validateAccessToken(ctx, opts, token);
|
|
459
194
|
const scopes = jwt.scope?.split(" ");
|
|
460
|
-
if (!scopes?.includes("openid")) throw new APIError
|
|
195
|
+
if (!scopes?.includes("openid")) throw new APIError("BAD_REQUEST", {
|
|
461
196
|
error_description: "Missing required scope",
|
|
462
197
|
error: "invalid_scope"
|
|
463
198
|
});
|
|
464
|
-
if (!jwt.sub) throw new APIError
|
|
199
|
+
if (!jwt.sub) throw new APIError("BAD_REQUEST", {
|
|
465
200
|
error_description: "user not found",
|
|
466
201
|
error: "invalid_request"
|
|
467
202
|
});
|
|
468
203
|
const user = await ctx.context.internalAdapter.findUserById(jwt.sub);
|
|
469
|
-
if (!user) throw new APIError
|
|
204
|
+
if (!user) throw new APIError("BAD_REQUEST", {
|
|
470
205
|
error_description: "user not found",
|
|
471
206
|
error: "invalid_request"
|
|
472
207
|
});
|
|
@@ -490,7 +225,7 @@ async function userInfoEndpoint(ctx, opts) {
|
|
|
490
225
|
*/
|
|
491
226
|
async function tokenEndpoint(ctx, opts) {
|
|
492
227
|
const grantType = ctx.body?.grant_type;
|
|
493
|
-
if (opts.grantTypes && grantType && !opts.grantTypes.includes(grantType)) throw new APIError
|
|
228
|
+
if (opts.grantTypes && grantType && !opts.grantTypes.includes(grantType)) throw new APIError("BAD_REQUEST", {
|
|
494
229
|
error_description: `unsupported grant_type ${grantType}`,
|
|
495
230
|
error: "unsupported_grant_type"
|
|
496
231
|
});
|
|
@@ -498,11 +233,11 @@ async function tokenEndpoint(ctx, opts) {
|
|
|
498
233
|
case "authorization_code": return handleAuthorizationCodeGrant(ctx, opts);
|
|
499
234
|
case "client_credentials": return handleClientCredentialsGrant(ctx, opts);
|
|
500
235
|
case "refresh_token": return handleRefreshTokenGrant(ctx, opts);
|
|
501
|
-
case void 0: throw new APIError
|
|
236
|
+
case void 0: throw new APIError("BAD_REQUEST", {
|
|
502
237
|
error_description: "missing required grant_type",
|
|
503
238
|
error: "unsupported_grant_type"
|
|
504
239
|
});
|
|
505
|
-
default: throw new APIError
|
|
240
|
+
default: throw new APIError("BAD_REQUEST", {
|
|
506
241
|
error_description: `unsupported grant_type ${grantType}`,
|
|
507
242
|
error: "unsupported_grant_type"
|
|
508
243
|
});
|
|
@@ -538,11 +273,11 @@ async function createJwtAccessToken(ctx, opts, user, client, audience, scopes, r
|
|
|
538
273
|
* Creates a user id token in code_authorization with scope of 'openid'
|
|
539
274
|
* and hybrid/implicit (not yet implemented) flows
|
|
540
275
|
*/
|
|
541
|
-
async function createIdToken(ctx, opts, user, client, scopes, nonce, sessionId) {
|
|
276
|
+
async function createIdToken(ctx, opts, user, client, scopes, nonce, sessionId, authTime) {
|
|
542
277
|
const iat = Math.floor(Date.now() / 1e3);
|
|
543
278
|
const exp = iat + (opts.idTokenExpiresIn ?? 36e3);
|
|
544
279
|
const userClaims = userNormalClaims(user, scopes);
|
|
545
|
-
const
|
|
280
|
+
const authTimeSec = authTime != null ? Math.floor(authTime.getTime() / 1e3) : void 0;
|
|
546
281
|
const acr = "urn:mace:incommon:iap:bronze";
|
|
547
282
|
const customClaims = opts.customIdTokenClaims ? await opts.customIdTokenClaims({
|
|
548
283
|
user,
|
|
@@ -553,7 +288,7 @@ async function createIdToken(ctx, opts, user, client, scopes, nonce, sessionId)
|
|
|
553
288
|
const payload = {
|
|
554
289
|
...customClaims,
|
|
555
290
|
...userClaims,
|
|
556
|
-
auth_time:
|
|
291
|
+
auth_time: authTimeSec,
|
|
557
292
|
acr,
|
|
558
293
|
iss: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL,
|
|
559
294
|
sub: user.id,
|
|
@@ -582,7 +317,7 @@ async function encodeRefreshToken(opts, token, sessionId) {
|
|
|
582
317
|
*/
|
|
583
318
|
async function decodeRefreshToken(opts, token) {
|
|
584
319
|
if (opts.prefix?.refreshToken) if (token.startsWith(opts.prefix.refreshToken)) token = token.replace(opts.prefix.refreshToken, "");
|
|
585
|
-
else throw new APIError
|
|
320
|
+
else throw new APIError("BAD_REQUEST", {
|
|
586
321
|
error_description: "refresh token not found",
|
|
587
322
|
error: "invalid_token"
|
|
588
323
|
});
|
|
@@ -608,7 +343,7 @@ async function createOpaqueAccessToken(ctx, opts, user, client, scopes, payload,
|
|
|
608
343
|
});
|
|
609
344
|
return (opts.prefix?.opaqueAccessToken ?? "") + token;
|
|
610
345
|
}
|
|
611
|
-
async function createRefreshToken(ctx, opts, user, referenceId, client, scopes, payload, originalRefresh) {
|
|
346
|
+
async function createRefreshToken(ctx, opts, user, referenceId, client, scopes, payload, originalRefresh, authTime) {
|
|
612
347
|
const iat = payload.iat ?? Math.floor(Date.now() / 1e3);
|
|
613
348
|
const exp = payload?.exp ?? iat + (opts.refreshTokenExpiresIn ?? 2592e3);
|
|
614
349
|
const token = opts.generateRefreshToken ? await opts.generateRefreshToken() : generateRandomString(32, "A-Z", "a-z");
|
|
@@ -630,6 +365,7 @@ async function createRefreshToken(ctx, opts, user, referenceId, client, scopes,
|
|
|
630
365
|
sessionId,
|
|
631
366
|
userId: user.id,
|
|
632
367
|
referenceId,
|
|
368
|
+
authTime,
|
|
633
369
|
scopes,
|
|
634
370
|
createdAt: /* @__PURE__ */ new Date(iat * 1e3),
|
|
635
371
|
expiresAt: /* @__PURE__ */ new Date(exp * 1e3)
|
|
@@ -648,14 +384,14 @@ async function checkResource(ctx, opts, scopes) {
|
|
|
648
384
|
if (audience) {
|
|
649
385
|
if (scopes.includes("openid")) audience.push(`${ctx.context.baseURL}/oauth2/userinfo`);
|
|
650
386
|
const validAudiences = new Set([...opts.validAudiences ?? [ctx.context.baseURL], scopes?.includes("openid") ? `${ctx.context.baseURL}/oauth2/userinfo` : void 0].flat().filter((v) => v?.length));
|
|
651
|
-
for (const aud of audience) if (!validAudiences.has(aud)) throw new APIError
|
|
387
|
+
for (const aud of audience) if (!validAudiences.has(aud)) throw new APIError("BAD_REQUEST", {
|
|
652
388
|
error_description: "requested resource invalid",
|
|
653
389
|
error: "invalid_request"
|
|
654
390
|
});
|
|
655
391
|
}
|
|
656
392
|
return audience?.length === 1 ? audience.at(0) : audience;
|
|
657
393
|
}
|
|
658
|
-
async function createUserTokens(ctx, opts, client, scopes, user, referenceId, sessionId, nonce, additional) {
|
|
394
|
+
async function createUserTokens(ctx, opts, client, scopes, user, referenceId, sessionId, nonce, additional, authTime) {
|
|
659
395
|
const iat = Math.floor(Date.now() / 1e3);
|
|
660
396
|
const defaultExp = iat + (opts.accessTokenExpiresIn ?? 3600);
|
|
661
397
|
const exp = opts.scopeExpirations ? scopes.map((sc) => opts.scopeExpirations?.[sc] ? toExpJWT(opts.scopeExpirations[sc], iat) : defaultExp).reduce((prev, curr) => {
|
|
@@ -669,7 +405,7 @@ async function createUserTokens(ctx, opts, client, scopes, user, referenceId, se
|
|
|
669
405
|
iat,
|
|
670
406
|
exp: iat + (opts.refreshTokenExpiresIn ?? 2592e3),
|
|
671
407
|
sid: sessionId
|
|
672
|
-
}, additional?.refreshToken) : void 0;
|
|
408
|
+
}, additional?.refreshToken, authTime) : void 0;
|
|
673
409
|
const [accessToken, refreshToken, idToken] = await Promise.all([
|
|
674
410
|
isJwtAccessToken ? createJwtAccessToken(ctx, opts, user, client, audience, scopes, referenceId, {
|
|
675
411
|
iat,
|
|
@@ -684,8 +420,8 @@ async function createUserTokens(ctx, opts, client, scopes, user, referenceId, se
|
|
|
684
420
|
iat,
|
|
685
421
|
exp: iat + (opts.refreshTokenExpiresIn ?? 2592e3),
|
|
686
422
|
sid: sessionId
|
|
687
|
-
}, additional?.refreshToken) : void 0,
|
|
688
|
-
isIdToken ? createIdToken(ctx, opts, user, client, scopes, nonce, sessionId) : void 0
|
|
423
|
+
}, additional?.refreshToken, authTime) : void 0,
|
|
424
|
+
isIdToken ? createIdToken(ctx, opts, user, client, scopes, nonce, sessionId, authTime) : void 0
|
|
689
425
|
]);
|
|
690
426
|
return ctx.json({
|
|
691
427
|
access_token: accessToken,
|
|
@@ -704,32 +440,32 @@ async function createUserTokens(ctx, opts, client, scopes, user, referenceId, se
|
|
|
704
440
|
async function checkVerificationValue(ctx, opts, code, client_id, redirect_uri) {
|
|
705
441
|
const verification = await ctx.context.internalAdapter.findVerificationValue(await storeToken(opts.storeTokens, code, "authorization_code"));
|
|
706
442
|
const verificationValue = verification ? JSON.parse(verification?.value) : void 0;
|
|
707
|
-
if (!verification) throw new APIError
|
|
443
|
+
if (!verification) throw new APIError("UNAUTHORIZED", {
|
|
708
444
|
error_description: "Invalid code",
|
|
709
445
|
error: "invalid_verification"
|
|
710
446
|
});
|
|
711
447
|
if (verification?.id) await ctx.context.internalAdapter.deleteVerificationValue(verification.id);
|
|
712
|
-
if (!verification.expiresAt || verification.expiresAt < /* @__PURE__ */ new Date()) throw new APIError
|
|
448
|
+
if (!verification.expiresAt || verification.expiresAt < /* @__PURE__ */ new Date()) throw new APIError("UNAUTHORIZED", {
|
|
713
449
|
error_description: "code expired",
|
|
714
450
|
error: "invalid_verification"
|
|
715
451
|
});
|
|
716
|
-
if (!verificationValue) throw new APIError
|
|
452
|
+
if (!verificationValue) throw new APIError("UNAUTHORIZED", {
|
|
717
453
|
error_description: "missing verification value content",
|
|
718
454
|
error: "invalid_verification"
|
|
719
455
|
});
|
|
720
|
-
if (verificationValue.type !== "authorization_code") throw new APIError
|
|
456
|
+
if (verificationValue.type !== "authorization_code") throw new APIError("UNAUTHORIZED", {
|
|
721
457
|
error_description: "incorrect verification type",
|
|
722
458
|
error: "invalid_verification"
|
|
723
459
|
});
|
|
724
|
-
if (verificationValue.query.client_id !== client_id) throw new APIError
|
|
460
|
+
if (verificationValue.query.client_id !== client_id) throw new APIError("UNAUTHORIZED", {
|
|
725
461
|
error_description: "invalid client_id",
|
|
726
462
|
error: "invalid_client"
|
|
727
463
|
});
|
|
728
|
-
if (!verificationValue.userId) throw new APIError
|
|
464
|
+
if (!verificationValue.userId) throw new APIError("BAD_REQUEST", {
|
|
729
465
|
error_description: "missing user_id on challenge",
|
|
730
466
|
error: "invalid_user"
|
|
731
467
|
});
|
|
732
|
-
if (verificationValue.query?.redirect_uri && verificationValue.query?.redirect_uri !== redirect_uri) throw new APIError
|
|
468
|
+
if (verificationValue.query?.redirect_uri && verificationValue.query?.redirect_uri !== redirect_uri) throw new APIError("BAD_REQUEST", {
|
|
733
469
|
error_description: "missing verification redirect_uri",
|
|
734
470
|
error: "invalid_request"
|
|
735
471
|
});
|
|
@@ -746,50 +482,66 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
|
|
|
746
482
|
client_id = res?.client_id;
|
|
747
483
|
client_secret = res?.client_secret;
|
|
748
484
|
}
|
|
749
|
-
if (!client_id) throw new APIError
|
|
485
|
+
if (!client_id) throw new APIError("BAD_REQUEST", {
|
|
750
486
|
error_description: "client_id is required",
|
|
751
487
|
error: "invalid_request"
|
|
752
488
|
});
|
|
753
|
-
if (!code) throw new APIError
|
|
489
|
+
if (!code) throw new APIError("BAD_REQUEST", {
|
|
754
490
|
error_description: "code is required",
|
|
755
491
|
error: "invalid_request"
|
|
756
492
|
});
|
|
757
|
-
if (!redirect_uri) throw new APIError
|
|
493
|
+
if (!redirect_uri) throw new APIError("BAD_REQUEST", {
|
|
758
494
|
error_description: "redirect_uri is required",
|
|
759
495
|
error: "invalid_request"
|
|
760
496
|
});
|
|
761
497
|
const isAuthCodeWithSecret = client_id && client_secret;
|
|
762
498
|
const isAuthCodeWithPkce = client_id && code && code_verifier;
|
|
763
|
-
if (!
|
|
764
|
-
error_description: "
|
|
499
|
+
if (!isAuthCodeWithSecret && !isAuthCodeWithPkce) throw new APIError("BAD_REQUEST", {
|
|
500
|
+
error_description: "Either code_verifier or client_secret is required",
|
|
765
501
|
error: "invalid_request"
|
|
766
502
|
});
|
|
767
503
|
/** Get and check Verification Value */
|
|
768
504
|
const verificationValue = await checkVerificationValue(ctx, opts, code, client_id, redirect_uri);
|
|
769
505
|
const scopes = verificationValue.query.scope?.split(" ");
|
|
770
|
-
if (!scopes) throw new APIError
|
|
506
|
+
if (!scopes) throw new APIError("INTERNAL_SERVER_ERROR", {
|
|
771
507
|
error_description: "verification scope unset",
|
|
772
508
|
error: "invalid_scope"
|
|
773
509
|
});
|
|
774
510
|
/** Verify Client */
|
|
775
511
|
const client = await validateClientCredentials(ctx, opts, client_id, client_secret, scopes);
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
})
|
|
782
|
-
|
|
783
|
-
error_description: "code verification failed",
|
|
512
|
+
if (isPKCERequired(client, (verificationValue.query?.scope)?.split(" ") || [])) {
|
|
513
|
+
if (!isAuthCodeWithPkce) throw new APIError("BAD_REQUEST", {
|
|
514
|
+
error_description: "PKCE is required for this client",
|
|
515
|
+
error: "invalid_request"
|
|
516
|
+
});
|
|
517
|
+
} else if (!(isAuthCodeWithPkce || isAuthCodeWithSecret)) throw new APIError("BAD_REQUEST", {
|
|
518
|
+
error_description: "Either PKCE (code_verifier) or client authentication (client_secret) is required",
|
|
784
519
|
error: "invalid_request"
|
|
785
520
|
});
|
|
521
|
+
/** Check PKCE challenge if verifier is provided */
|
|
522
|
+
const pkceUsedInAuth = !!verificationValue.query?.code_challenge;
|
|
523
|
+
const pkceUsedInToken = !!code_verifier;
|
|
524
|
+
if (pkceUsedInAuth || pkceUsedInToken) {
|
|
525
|
+
if (pkceUsedInAuth && !pkceUsedInToken) throw new APIError("UNAUTHORIZED", {
|
|
526
|
+
error_description: "code_verifier required because PKCE was used in authorization",
|
|
527
|
+
error: "invalid_request"
|
|
528
|
+
});
|
|
529
|
+
if (!pkceUsedInAuth && pkceUsedInToken) throw new APIError("UNAUTHORIZED", {
|
|
530
|
+
error_description: "code_verifier provided but PKCE was not used in authorization",
|
|
531
|
+
error: "invalid_request"
|
|
532
|
+
});
|
|
533
|
+
if ((verificationValue.query?.code_challenge_method === "S256" ? await generateCodeChallenge(code_verifier) : void 0) !== verificationValue.query?.code_challenge) throw new APIError("UNAUTHORIZED", {
|
|
534
|
+
error_description: "code verification failed",
|
|
535
|
+
error: "invalid_request"
|
|
536
|
+
});
|
|
537
|
+
}
|
|
786
538
|
/** Get user */
|
|
787
|
-
if (!verificationValue.userId) throw new APIError
|
|
539
|
+
if (!verificationValue.userId) throw new APIError("BAD_REQUEST", {
|
|
788
540
|
error_description: "missing user, user may have been deleted",
|
|
789
541
|
error: "invalid_user"
|
|
790
542
|
});
|
|
791
543
|
const user = await ctx.context.internalAdapter.findUserById(verificationValue.userId);
|
|
792
|
-
if (!user) throw new APIError
|
|
544
|
+
if (!user) throw new APIError("BAD_REQUEST", {
|
|
793
545
|
error_description: "missing user, user may have been deleted",
|
|
794
546
|
error: "invalid_user"
|
|
795
547
|
});
|
|
@@ -800,11 +552,12 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
|
|
|
800
552
|
value: verificationValue.sessionId
|
|
801
553
|
}]
|
|
802
554
|
});
|
|
803
|
-
if (!session || session.expiresAt < /* @__PURE__ */ new Date()) throw new APIError
|
|
555
|
+
if (!session || session.expiresAt < /* @__PURE__ */ new Date()) throw new APIError("BAD_REQUEST", {
|
|
804
556
|
error_description: "session no longer exists",
|
|
805
557
|
error: "invalid_request"
|
|
806
558
|
});
|
|
807
|
-
|
|
559
|
+
const authTime = verificationValue.authTime != null ? new Date(verificationValue.authTime) : new Date(session.createdAt);
|
|
560
|
+
return createUserTokens(ctx, opts, client, verificationValue.query.scope?.split(" ") ?? [], user, verificationValue.referenceId, session.id, verificationValue.query?.nonce, void 0, authTime);
|
|
808
561
|
}
|
|
809
562
|
/**
|
|
810
563
|
* Grant that allows direct access to an API using the application's credentials
|
|
@@ -820,11 +573,11 @@ async function handleClientCredentialsGrant(ctx, opts) {
|
|
|
820
573
|
client_id = res?.client_id;
|
|
821
574
|
client_secret = res?.client_secret;
|
|
822
575
|
}
|
|
823
|
-
if (!client_id) throw new APIError
|
|
576
|
+
if (!client_id) throw new APIError("BAD_REQUEST", {
|
|
824
577
|
error_description: "Missing required client_id",
|
|
825
578
|
error: "invalid_grant"
|
|
826
579
|
});
|
|
827
|
-
if (!client_secret) throw new APIError
|
|
580
|
+
if (!client_secret) throw new APIError("BAD_REQUEST", {
|
|
828
581
|
error_description: "Missing a required client_secret",
|
|
829
582
|
error: "invalid_grant"
|
|
830
583
|
});
|
|
@@ -838,10 +591,10 @@ async function handleClientCredentialsGrant(ctx, opts) {
|
|
|
838
591
|
"email",
|
|
839
592
|
"offline_access"
|
|
840
593
|
]);
|
|
841
|
-
const invalidScopes = requestedScopes.filter((scope
|
|
842
|
-
return !validScopes?.has(scope
|
|
594
|
+
const invalidScopes = requestedScopes.filter((scope) => {
|
|
595
|
+
return !validScopes?.has(scope) || oidcScopes.has(scope);
|
|
843
596
|
});
|
|
844
|
-
if (invalidScopes.length) throw new APIError
|
|
597
|
+
if (invalidScopes.length) throw new APIError("BAD_REQUEST", {
|
|
845
598
|
error_description: `The following scopes are invalid: ${invalidScopes.join(", ")}`,
|
|
846
599
|
error: "invalid_scope"
|
|
847
600
|
});
|
|
@@ -899,11 +652,11 @@ async function handleRefreshTokenGrant(ctx, opts) {
|
|
|
899
652
|
client_id = res?.client_id;
|
|
900
653
|
client_secret = res?.client_secret;
|
|
901
654
|
}
|
|
902
|
-
if (!client_id) throw new APIError
|
|
655
|
+
if (!client_id) throw new APIError("BAD_REQUEST", {
|
|
903
656
|
error_description: "Missing required client_id",
|
|
904
657
|
error: "invalid_grant"
|
|
905
658
|
});
|
|
906
|
-
if (!refresh_token) throw new APIError
|
|
659
|
+
if (!refresh_token) throw new APIError("BAD_REQUEST", {
|
|
907
660
|
error_description: "Missing a required refresh_token for refresh_token grant",
|
|
908
661
|
error: "invalid_grant"
|
|
909
662
|
});
|
|
@@ -915,17 +668,17 @@ async function handleRefreshTokenGrant(ctx, opts) {
|
|
|
915
668
|
value: await getStoredToken(opts.storeTokens, decodedRefresh.token, "refresh_token")
|
|
916
669
|
}]
|
|
917
670
|
});
|
|
918
|
-
if (!refreshToken) throw new APIError
|
|
671
|
+
if (!refreshToken) throw new APIError("BAD_REQUEST", {
|
|
919
672
|
error_description: "session not found",
|
|
920
|
-
error: "
|
|
673
|
+
error: "invalid_grant"
|
|
921
674
|
});
|
|
922
|
-
if (refreshToken.clientId !== client_id) throw new APIError
|
|
675
|
+
if (refreshToken.clientId !== client_id) throw new APIError("BAD_REQUEST", {
|
|
923
676
|
error_description: "invalid client_id",
|
|
924
677
|
error: "invalid_client"
|
|
925
678
|
});
|
|
926
|
-
if (refreshToken.expiresAt < /* @__PURE__ */ new Date()) throw new APIError
|
|
679
|
+
if (refreshToken.expiresAt < /* @__PURE__ */ new Date()) throw new APIError("BAD_REQUEST", {
|
|
927
680
|
error_description: "invalid refresh token",
|
|
928
|
-
error: "
|
|
681
|
+
error: "invalid_grant"
|
|
929
682
|
});
|
|
930
683
|
if (refreshToken.revoked) {
|
|
931
684
|
await ctx.context.adapter.deleteMany({
|
|
@@ -938,27 +691,28 @@ async function handleRefreshTokenGrant(ctx, opts) {
|
|
|
938
691
|
value: refreshToken.userId
|
|
939
692
|
}]
|
|
940
693
|
});
|
|
941
|
-
throw new APIError
|
|
694
|
+
throw new APIError("BAD_REQUEST", {
|
|
942
695
|
error_description: "invalid refresh token",
|
|
943
|
-
error: "
|
|
696
|
+
error: "invalid_grant"
|
|
944
697
|
});
|
|
945
698
|
}
|
|
946
699
|
const scopes = refreshToken?.scopes;
|
|
947
700
|
const requestedScopes = scope?.split(" ");
|
|
948
701
|
if (requestedScopes) {
|
|
949
702
|
const validScopes = new Set(scopes);
|
|
950
|
-
for (const requestedScope of requestedScopes) if (!validScopes.has(requestedScope)) throw new APIError
|
|
703
|
+
for (const requestedScope of requestedScopes) if (!validScopes.has(requestedScope)) throw new APIError("BAD_REQUEST", {
|
|
951
704
|
error_description: `unable to issue scope ${requestedScope}`,
|
|
952
705
|
error: "invalid_scope"
|
|
953
706
|
});
|
|
954
707
|
}
|
|
955
708
|
const client = await validateClientCredentials(ctx, opts, client_id, client_secret, requestedScopes ?? scopes);
|
|
956
709
|
const user = await ctx.context.internalAdapter.findUserById(refreshToken.userId);
|
|
957
|
-
if (!user) throw new APIError
|
|
710
|
+
if (!user) throw new APIError("BAD_REQUEST", {
|
|
958
711
|
error_description: "user not found",
|
|
959
712
|
error: "invalid_request"
|
|
960
713
|
});
|
|
961
|
-
|
|
714
|
+
const authTime = refreshToken.authTime != null ? new Date(refreshToken.authTime) : void 0;
|
|
715
|
+
return createUserTokens(ctx, opts, client, requestedScopes ?? scopes, user, refreshToken.referenceId, refreshToken.sessionId, void 0, { refreshToken }, authTime);
|
|
962
716
|
}
|
|
963
717
|
|
|
964
718
|
//#endregion
|
|
@@ -991,7 +745,7 @@ async function validateJwtAccessToken(ctx, opts, token, clientId) {
|
|
|
991
745
|
});
|
|
992
746
|
} catch (error) {
|
|
993
747
|
if (error instanceof Error) {
|
|
994
|
-
if (error.name === "TypeError" || error.name === "JWSInvalid") throw new APIError("BAD_REQUEST", {
|
|
748
|
+
if (error.name === "TypeError" || error.name === "JWSInvalid") throw new APIError$1("BAD_REQUEST", {
|
|
995
749
|
error_description: "invalid JWT signature",
|
|
996
750
|
error: "invalid_request"
|
|
997
751
|
});
|
|
@@ -1030,7 +784,7 @@ async function validateJwtAccessToken(ctx, opts, token, clientId) {
|
|
|
1030
784
|
async function validateOpaqueAccessToken(ctx, opts, token, clientId) {
|
|
1031
785
|
let tokenValue = token;
|
|
1032
786
|
if (opts.prefix?.opaqueAccessToken) if (tokenValue.startsWith(opts.prefix.opaqueAccessToken)) tokenValue = tokenValue.replace(opts.prefix.opaqueAccessToken, "");
|
|
1033
|
-
else throw new APIError("BAD_REQUEST", {
|
|
787
|
+
else throw new APIError$1("BAD_REQUEST", {
|
|
1034
788
|
error_description: "opaque access token not found",
|
|
1035
789
|
error: "invalid_request"
|
|
1036
790
|
});
|
|
@@ -1041,7 +795,7 @@ async function validateOpaqueAccessToken(ctx, opts, token, clientId) {
|
|
|
1041
795
|
value: await getStoredToken(opts.storeTokens, tokenValue, "access_token")
|
|
1042
796
|
}]
|
|
1043
797
|
});
|
|
1044
|
-
if (!accessToken) throw new APIError("BAD_REQUEST", {
|
|
798
|
+
if (!accessToken) throw new APIError$1("BAD_REQUEST", {
|
|
1045
799
|
error_description: "opaque access token not found",
|
|
1046
800
|
error: "invalid_token"
|
|
1047
801
|
});
|
|
@@ -1079,8 +833,8 @@ async function validateOpaqueAccessToken(ctx, opts, token, clientId) {
|
|
|
1079
833
|
client_id: accessToken.clientId,
|
|
1080
834
|
sub: user?.id,
|
|
1081
835
|
sid: sessionId,
|
|
1082
|
-
exp: Math.floor(accessToken.expiresAt.getTime() / 1e3),
|
|
1083
|
-
iat: Math.floor(accessToken.createdAt.getTime() / 1e3),
|
|
836
|
+
exp: Math.floor(new Date(accessToken.expiresAt).getTime() / 1e3),
|
|
837
|
+
iat: Math.floor(new Date(accessToken.createdAt).getTime() / 1e3),
|
|
1084
838
|
scope: accessToken.scopes?.join(" ")
|
|
1085
839
|
};
|
|
1086
840
|
}
|
|
@@ -1097,7 +851,7 @@ async function validateRefreshToken(ctx, opts, token, clientId) {
|
|
|
1097
851
|
value: await getStoredToken(opts.storeTokens, token, "refresh_token")
|
|
1098
852
|
}]
|
|
1099
853
|
});
|
|
1100
|
-
if (!refreshToken) throw new APIError("BAD_REQUEST", {
|
|
854
|
+
if (!refreshToken) throw new APIError$1("BAD_REQUEST", {
|
|
1101
855
|
error_description: "token not found",
|
|
1102
856
|
error: "invalid_token"
|
|
1103
857
|
});
|
|
@@ -1123,8 +877,8 @@ async function validateRefreshToken(ctx, opts, token, clientId) {
|
|
|
1123
877
|
iss: ((opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context))?.options)?.jwt?.issuer ?? ctx.context.baseURL,
|
|
1124
878
|
sub: user?.id,
|
|
1125
879
|
sid: sessionId,
|
|
1126
|
-
exp: Math.floor(refreshToken.expiresAt.getTime() / 1e3),
|
|
1127
|
-
iat: Math.floor(refreshToken.createdAt.getTime() / 1e3),
|
|
880
|
+
exp: Math.floor(new Date(refreshToken.expiresAt).getTime() / 1e3),
|
|
881
|
+
iat: Math.floor(new Date(refreshToken.createdAt).getTime() / 1e3),
|
|
1128
882
|
scope: refreshToken.scopes?.join(" ")
|
|
1129
883
|
};
|
|
1130
884
|
}
|
|
@@ -1140,16 +894,16 @@ async function validateAccessToken(ctx, opts, token, clientId) {
|
|
|
1140
894
|
try {
|
|
1141
895
|
return await validateJwtAccessToken(ctx, opts, token, clientId);
|
|
1142
896
|
} catch (err) {
|
|
1143
|
-
if (err instanceof APIError) {} else if (err instanceof Error) throw err;
|
|
897
|
+
if (err instanceof APIError$1) {} else if (err instanceof Error) throw err;
|
|
1144
898
|
else throw new Error(err);
|
|
1145
899
|
}
|
|
1146
900
|
try {
|
|
1147
901
|
return await validateOpaqueAccessToken(ctx, opts, token, clientId);
|
|
1148
902
|
} catch (err) {
|
|
1149
|
-
if (err instanceof APIError) {} else if (err instanceof Error) throw err;
|
|
903
|
+
if (err instanceof APIError$1) {} else if (err instanceof Error) throw err;
|
|
1150
904
|
else throw new Error("Unknown error validating access token");
|
|
1151
905
|
}
|
|
1152
|
-
throw new APIError("BAD_REQUEST", {
|
|
906
|
+
throw new APIError$1("BAD_REQUEST", {
|
|
1153
907
|
error_description: "Invalid access token",
|
|
1154
908
|
error: "invalid_request"
|
|
1155
909
|
});
|
|
@@ -1162,12 +916,12 @@ async function introspectEndpoint(ctx, opts) {
|
|
|
1162
916
|
client_id = res?.client_id;
|
|
1163
917
|
client_secret = res?.client_secret;
|
|
1164
918
|
}
|
|
1165
|
-
if (!client_id || !client_secret) throw new APIError("UNAUTHORIZED", {
|
|
919
|
+
if (!client_id || !client_secret) throw new APIError$1("UNAUTHORIZED", {
|
|
1166
920
|
error_description: "missing required credentials",
|
|
1167
921
|
error: "invalid_client"
|
|
1168
922
|
});
|
|
1169
923
|
if (token && typeof token === "string" && token.startsWith("Bearer ")) token = token.replace("Bearer ", "");
|
|
1170
|
-
if (!token?.length) throw new APIError("BAD_REQUEST", {
|
|
924
|
+
if (!token?.length) throw new APIError$1("BAD_REQUEST", {
|
|
1171
925
|
error_description: "missing a required token for introspection",
|
|
1172
926
|
error: "invalid_request"
|
|
1173
927
|
});
|
|
@@ -1176,7 +930,7 @@ async function introspectEndpoint(ctx, opts) {
|
|
|
1176
930
|
if (token_type_hint === void 0 || token_type_hint === "access_token") try {
|
|
1177
931
|
return await validateAccessToken(ctx, opts, token, client.clientId);
|
|
1178
932
|
} catch (error) {
|
|
1179
|
-
if (error instanceof APIError) {
|
|
933
|
+
if (error instanceof APIError$1) {
|
|
1180
934
|
if (token_type_hint === "access_token") throw error;
|
|
1181
935
|
} else if (error instanceof Error) throw error;
|
|
1182
936
|
else throw new Error(error);
|
|
@@ -1184,25 +938,25 @@ async function introspectEndpoint(ctx, opts) {
|
|
|
1184
938
|
if (token_type_hint === void 0 || token_type_hint === "refresh_token") try {
|
|
1185
939
|
return await validateRefreshToken(ctx, opts, (await decodeRefreshToken(opts, token)).token, client.clientId);
|
|
1186
940
|
} catch (error) {
|
|
1187
|
-
if (error instanceof APIError) {
|
|
941
|
+
if (error instanceof APIError$1) {
|
|
1188
942
|
if (token_type_hint === "refresh_token") throw error;
|
|
1189
943
|
} else if (error instanceof Error) throw error;
|
|
1190
944
|
else throw new Error(error);
|
|
1191
945
|
}
|
|
1192
|
-
throw new APIError("BAD_REQUEST", {
|
|
946
|
+
throw new APIError$1("BAD_REQUEST", {
|
|
1193
947
|
error_description: "token not found",
|
|
1194
948
|
error: "invalid_request"
|
|
1195
949
|
});
|
|
1196
950
|
} catch (error) {
|
|
1197
|
-
if (error instanceof APIError) {
|
|
951
|
+
if (error instanceof APIError$1) {
|
|
1198
952
|
if (error.name === "BAD_REQUEST") return { active: false };
|
|
1199
953
|
throw error;
|
|
1200
954
|
} else if (error instanceof Error) {
|
|
1201
955
|
logger.error("Introspection error:", error.message, error.stack);
|
|
1202
|
-
throw new APIError("INTERNAL_SERVER_ERROR");
|
|
956
|
+
throw new APIError$1("INTERNAL_SERVER_ERROR");
|
|
1203
957
|
} else {
|
|
1204
958
|
logger.error("Introspection error:", error);
|
|
1205
|
-
throw new APIError("INTERNAL_SERVER_ERROR");
|
|
959
|
+
throw new APIError$1("INTERNAL_SERVER_ERROR");
|
|
1206
960
|
}
|
|
1207
961
|
}
|
|
1208
962
|
}
|
|
@@ -1226,34 +980,34 @@ async function rpInitiatedLogoutEndpoint(ctx, opts) {
|
|
|
1226
980
|
try {
|
|
1227
981
|
decoded = decodeJwt(id_token_hint);
|
|
1228
982
|
} catch (_e) {
|
|
1229
|
-
throw new APIError("UNAUTHORIZED", {
|
|
983
|
+
throw new APIError$1("UNAUTHORIZED", {
|
|
1230
984
|
error_description: "invalid id token",
|
|
1231
985
|
error: "invalid_token"
|
|
1232
986
|
});
|
|
1233
987
|
}
|
|
1234
988
|
clientId = decoded?.aud;
|
|
1235
|
-
if (!clientId) throw new APIError("INTERNAL_SERVER_ERROR", {
|
|
989
|
+
if (!clientId) throw new APIError$1("INTERNAL_SERVER_ERROR", {
|
|
1236
990
|
error_description: "id token missing audience",
|
|
1237
991
|
error: "invalid_request"
|
|
1238
992
|
});
|
|
1239
993
|
}
|
|
1240
994
|
const client = await getClient(ctx, opts, clientId);
|
|
1241
|
-
if (!client) throw new APIError("BAD_REQUEST", {
|
|
995
|
+
if (!client) throw new APIError$1("BAD_REQUEST", {
|
|
1242
996
|
error_description: "client doesn't exist",
|
|
1243
997
|
error: "invalid_client"
|
|
1244
998
|
});
|
|
1245
|
-
if (client.disabled) throw new APIError("BAD_REQUEST", {
|
|
999
|
+
if (client.disabled) throw new APIError$1("BAD_REQUEST", {
|
|
1246
1000
|
error_description: "client is disabled",
|
|
1247
1001
|
error: "invalid_client"
|
|
1248
1002
|
});
|
|
1249
|
-
if (!client.enableEndSession) throw new APIError("UNAUTHORIZED", {
|
|
1003
|
+
if (!client.enableEndSession) throw new APIError$1("UNAUTHORIZED", {
|
|
1250
1004
|
error_description: "client unable to logout",
|
|
1251
1005
|
error: "invalid_client"
|
|
1252
1006
|
});
|
|
1253
1007
|
let idTokenPayload;
|
|
1254
1008
|
if (opts.disableJwtPlugin) {
|
|
1255
1009
|
const clientSecret = client.clientSecret;
|
|
1256
|
-
if (!clientSecret) throw new APIError("UNAUTHORIZED", {
|
|
1010
|
+
if (!clientSecret) throw new APIError$1("UNAUTHORIZED", {
|
|
1257
1011
|
error_description: "missing required credentials",
|
|
1258
1012
|
error: "invalid_client"
|
|
1259
1013
|
});
|
|
@@ -1266,25 +1020,25 @@ async function rpInitiatedLogoutEndpoint(ctx, opts) {
|
|
|
1266
1020
|
const idToken = new TextDecoder().decode(payload);
|
|
1267
1021
|
idTokenPayload = JSON.parse(idToken);
|
|
1268
1022
|
}
|
|
1269
|
-
if (!idTokenPayload) throw new APIError("INTERNAL_SERVER_ERROR", {
|
|
1023
|
+
if (!idTokenPayload) throw new APIError$1("INTERNAL_SERVER_ERROR", {
|
|
1270
1024
|
error_description: "missing payload",
|
|
1271
1025
|
error: "invalid_request"
|
|
1272
1026
|
});
|
|
1273
|
-
if ((jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL) !== idTokenPayload.iss) throw new APIError("INTERNAL_SERVER_ERROR", {
|
|
1027
|
+
if ((jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL) !== idTokenPayload.iss) throw new APIError$1("INTERNAL_SERVER_ERROR", {
|
|
1274
1028
|
error_description: "invalid issuer",
|
|
1275
1029
|
error: "invalid_request"
|
|
1276
1030
|
});
|
|
1277
1031
|
const idTokenAudience = typeof idTokenPayload.aud === "string" ? [idTokenPayload.aud] : idTokenPayload.aud;
|
|
1278
|
-
if (!idTokenAudience) throw new APIError("INTERNAL_SERVER_ERROR", {
|
|
1032
|
+
if (!idTokenAudience) throw new APIError$1("INTERNAL_SERVER_ERROR", {
|
|
1279
1033
|
error_description: "id token missing audience",
|
|
1280
1034
|
error: "invalid_request"
|
|
1281
1035
|
});
|
|
1282
|
-
if (client_id && !idTokenAudience.includes(client_id)) throw new APIError("BAD_REQUEST", {
|
|
1036
|
+
if (client_id && !idTokenAudience.includes(client_id)) throw new APIError$1("BAD_REQUEST", {
|
|
1283
1037
|
error_description: "audience mismatch",
|
|
1284
1038
|
error: "invalid_request"
|
|
1285
1039
|
});
|
|
1286
1040
|
const sessionId = idTokenPayload.sid;
|
|
1287
|
-
if (!sessionId) throw new APIError("INTERNAL_SERVER_ERROR", {
|
|
1041
|
+
if (!sessionId) throw new APIError$1("INTERNAL_SERVER_ERROR", {
|
|
1288
1042
|
error_description: "id token missing session",
|
|
1289
1043
|
error: "invalid_request"
|
|
1290
1044
|
});
|
|
@@ -1322,18 +1076,18 @@ async function rpInitiatedLogoutEndpoint(ctx, opts) {
|
|
|
1322
1076
|
//#endregion
|
|
1323
1077
|
//#region src/register.ts
|
|
1324
1078
|
async function registerEndpoint(ctx, opts) {
|
|
1325
|
-
if (!opts.allowDynamicClientRegistration) throw new APIError
|
|
1079
|
+
if (!opts.allowDynamicClientRegistration) throw new APIError("FORBIDDEN", {
|
|
1326
1080
|
error: "access_denied",
|
|
1327
1081
|
error_description: "Client registration is disabled"
|
|
1328
1082
|
});
|
|
1329
1083
|
const body = ctx.body;
|
|
1330
1084
|
const session = await getSessionFromCtx(ctx);
|
|
1331
|
-
if (!(session || opts.allowUnauthenticatedClientRegistration)) throw new APIError
|
|
1085
|
+
if (!(session || opts.allowUnauthenticatedClientRegistration)) throw new APIError("UNAUTHORIZED", {
|
|
1332
1086
|
error: "invalid_token",
|
|
1333
1087
|
error_description: "Authentication required for client registration"
|
|
1334
1088
|
});
|
|
1335
1089
|
const isPublic = body.token_endpoint_auth_method === "none";
|
|
1336
|
-
if (!session && !isPublic) throw new APIError
|
|
1090
|
+
if (!session && !isPublic) throw new APIError("UNAUTHORIZED", {
|
|
1337
1091
|
error: "invalid_request",
|
|
1338
1092
|
error_description: "Authentication required for confidential client registration"
|
|
1339
1093
|
});
|
|
@@ -1343,22 +1097,22 @@ async function registerEndpoint(ctx, opts) {
|
|
|
1343
1097
|
async function checkOAuthClient(client, opts, settings) {
|
|
1344
1098
|
const isPublic = client.token_endpoint_auth_method === "none";
|
|
1345
1099
|
if (client.type) {
|
|
1346
|
-
if (isPublic && !(client.type === "native" || client.type === "user-agent-based")) throw new APIError
|
|
1100
|
+
if (isPublic && !(client.type === "native" || client.type === "user-agent-based")) throw new APIError("BAD_REQUEST", {
|
|
1347
1101
|
error: "invalid_client_metadata",
|
|
1348
1102
|
error_description: `Type must be 'native' or 'user-agent-based' for public applications`
|
|
1349
1103
|
});
|
|
1350
|
-
else if (!isPublic && !(client.type === "web")) throw new APIError
|
|
1104
|
+
else if (!isPublic && !(client.type === "web")) throw new APIError("BAD_REQUEST", {
|
|
1351
1105
|
error: "invalid_client_metadata",
|
|
1352
1106
|
error_description: `Type must be 'web' for confidential applications`
|
|
1353
1107
|
});
|
|
1354
1108
|
}
|
|
1355
|
-
if ((!client.grant_types || client.grant_types.includes("authorization_code")) && (!client.redirect_uris || client.redirect_uris.length === 0)) throw new APIError
|
|
1109
|
+
if ((!client.grant_types || client.grant_types.includes("authorization_code")) && (!client.redirect_uris || client.redirect_uris.length === 0)) throw new APIError("BAD_REQUEST", {
|
|
1356
1110
|
error: "invalid_redirect_uri",
|
|
1357
1111
|
error_description: "Redirect URIs are required for authorization_code and implicit grant types"
|
|
1358
1112
|
});
|
|
1359
1113
|
const grantTypes = client.grant_types ?? ["authorization_code"];
|
|
1360
1114
|
const responseTypes = client.response_types ?? ["code"];
|
|
1361
|
-
if (grantTypes.includes("authorization_code") && !responseTypes.includes("code")) throw new APIError
|
|
1115
|
+
if (grantTypes.includes("authorization_code") && !responseTypes.includes("code")) throw new APIError("BAD_REQUEST", {
|
|
1362
1116
|
error: "invalid_client_metadata",
|
|
1363
1117
|
error_description: "When 'authorization_code' grant type is used, 'code' response type must be included"
|
|
1364
1118
|
});
|
|
@@ -1366,11 +1120,15 @@ async function checkOAuthClient(client, opts, settings) {
|
|
|
1366
1120
|
const allowedScopes = settings?.isRegister ? opts.clientRegistrationAllowedScopes ?? opts.scopes : opts.scopes;
|
|
1367
1121
|
if (allowedScopes) {
|
|
1368
1122
|
const validScopes = new Set(allowedScopes);
|
|
1369
|
-
for (const requestedScope of requestedScopes ?? []) if (!validScopes?.has(requestedScope)) throw new APIError
|
|
1123
|
+
for (const requestedScope of requestedScopes ?? []) if (!validScopes?.has(requestedScope)) throw new APIError("BAD_REQUEST", {
|
|
1370
1124
|
error: "invalid_scope",
|
|
1371
1125
|
error_description: `cannot request scope ${requestedScope}`
|
|
1372
1126
|
});
|
|
1373
1127
|
}
|
|
1128
|
+
if (settings?.isRegister && client.require_pkce === false) throw new APIError("BAD_REQUEST", {
|
|
1129
|
+
error: "invalid_client_metadata",
|
|
1130
|
+
error_description: `pkce is required for registered clients.`
|
|
1131
|
+
});
|
|
1374
1132
|
}
|
|
1375
1133
|
async function createOAuthClientEndpoint(ctx, opts, settings) {
|
|
1376
1134
|
const body = ctx.body;
|
|
@@ -1385,7 +1143,7 @@ async function createOAuthClientEndpoint(ctx, opts, settings) {
|
|
|
1385
1143
|
user: session?.user,
|
|
1386
1144
|
session: session?.session
|
|
1387
1145
|
}) : void 0;
|
|
1388
|
-
const schema
|
|
1146
|
+
const schema = oauthToSchema({
|
|
1389
1147
|
...body ?? {},
|
|
1390
1148
|
disabled: void 0,
|
|
1391
1149
|
jwks: void 0,
|
|
@@ -1400,7 +1158,11 @@ async function createOAuthClientEndpoint(ctx, opts, settings) {
|
|
|
1400
1158
|
});
|
|
1401
1159
|
const client = await ctx.context.adapter.create({
|
|
1402
1160
|
model: "oauthClient",
|
|
1403
|
-
data:
|
|
1161
|
+
data: {
|
|
1162
|
+
...schema,
|
|
1163
|
+
createdAt: /* @__PURE__ */ new Date(iat * 1e3),
|
|
1164
|
+
updatedAt: /* @__PURE__ */ new Date(iat * 1e3)
|
|
1165
|
+
}
|
|
1404
1166
|
});
|
|
1405
1167
|
return ctx.json(schemaToOAuth({
|
|
1406
1168
|
...client,
|
|
@@ -1420,7 +1182,7 @@ async function createOAuthClientEndpoint(ctx, opts, settings) {
|
|
|
1420
1182
|
* @returns
|
|
1421
1183
|
*/
|
|
1422
1184
|
function oauthToSchema(input) {
|
|
1423
|
-
const { client_id: clientId, client_secret: clientSecret, client_secret_expires_at: _expiresAt, scope: _scope, user_id: userId, client_id_issued_at: _createdAt, client_name: name, client_uri: uri, logo_uri: icon, contacts, tos_uri: tos, policy_uri: policy, jwks: _jwks, jwks_uri: _jwksUri, software_id: softwareId, software_version: softwareVersion, software_statement: softwareStatement, redirect_uris: redirectUris, post_logout_redirect_uris: postLogoutRedirectUris, token_endpoint_auth_method: tokenEndpointAuthMethod, grant_types: grantTypes, response_types: responseTypes, public: _public, type, disabled, skip_consent: skipConsent, enable_end_session: enableEndSession, reference_id: referenceId, metadata: inputMetadata, ...rest } = input;
|
|
1185
|
+
const { client_id: clientId, client_secret: clientSecret, client_secret_expires_at: _expiresAt, scope: _scope, user_id: userId, client_id_issued_at: _createdAt, client_name: name, client_uri: uri, logo_uri: icon, contacts, tos_uri: tos, policy_uri: policy, jwks: _jwks, jwks_uri: _jwksUri, software_id: softwareId, software_version: softwareVersion, software_statement: softwareStatement, redirect_uris: redirectUris, post_logout_redirect_uris: postLogoutRedirectUris, token_endpoint_auth_method: tokenEndpointAuthMethod, grant_types: grantTypes, response_types: responseTypes, public: _public, type, disabled, skip_consent: skipConsent, enable_end_session: enableEndSession, require_pkce: requirePKCE, reference_id: referenceId, metadata: inputMetadata, ...rest } = input;
|
|
1424
1186
|
const expiresAt = _expiresAt ? /* @__PURE__ */ new Date(_expiresAt * 1e3) : void 0;
|
|
1425
1187
|
const createdAt = _createdAt ? /* @__PURE__ */ new Date(_createdAt * 1e3) : void 0;
|
|
1426
1188
|
const scopes = _scope?.split(" ");
|
|
@@ -1454,6 +1216,7 @@ function oauthToSchema(input) {
|
|
|
1454
1216
|
type,
|
|
1455
1217
|
skipConsent,
|
|
1456
1218
|
enableEndSession,
|
|
1219
|
+
requirePKCE,
|
|
1457
1220
|
referenceId,
|
|
1458
1221
|
metadata: Object.keys(metadataObj).length ? JSON.stringify(metadataObj) : void 0
|
|
1459
1222
|
};
|
|
@@ -1465,9 +1228,9 @@ function oauthToSchema(input) {
|
|
|
1465
1228
|
* @returns
|
|
1466
1229
|
*/
|
|
1467
1230
|
function schemaToOAuth(input) {
|
|
1468
|
-
const { clientId, clientSecret, disabled, scopes, userId, createdAt, updatedAt: _updatedAt, expiresAt, name, uri, icon, contacts, tos, policy, softwareId, softwareVersion, softwareStatement, redirectUris, postLogoutRedirectUris, tokenEndpointAuthMethod, grantTypes, responseTypes, public: _public, type, skipConsent, enableEndSession, referenceId, metadata } = input;
|
|
1469
|
-
const _expiresAt = expiresAt ? Math.round(expiresAt.getTime() / 1e3) : void 0;
|
|
1470
|
-
const _createdAt = createdAt ? Math.round(createdAt.getTime() / 1e3) : void 0;
|
|
1231
|
+
const { clientId, clientSecret, disabled, scopes, userId, createdAt, updatedAt: _updatedAt, expiresAt, name, uri, icon, contacts, tos, policy, softwareId, softwareVersion, softwareStatement, redirectUris, postLogoutRedirectUris, tokenEndpointAuthMethod, grantTypes, responseTypes, public: _public, type, skipConsent, enableEndSession, requirePKCE, referenceId, metadata } = input;
|
|
1232
|
+
const _expiresAt = expiresAt ? Math.round(new Date(expiresAt).getTime() / 1e3) : void 0;
|
|
1233
|
+
const _createdAt = createdAt ? Math.round(new Date(createdAt).getTime() / 1e3) : void 0;
|
|
1471
1234
|
const _scopes = scopes?.join(" ");
|
|
1472
1235
|
return {
|
|
1473
1236
|
...parseClientMetadata(metadata),
|
|
@@ -1496,14 +1259,26 @@ function schemaToOAuth(input) {
|
|
|
1496
1259
|
disabled: disabled ?? void 0,
|
|
1497
1260
|
skip_consent: skipConsent ?? void 0,
|
|
1498
1261
|
enable_end_session: enableEndSession ?? void 0,
|
|
1262
|
+
require_pkce: requirePKCE ?? void 0,
|
|
1499
1263
|
reference_id: referenceId ?? void 0
|
|
1500
1264
|
};
|
|
1501
1265
|
}
|
|
1502
1266
|
|
|
1503
1267
|
//#endregion
|
|
1504
1268
|
//#region src/types/zod.ts
|
|
1269
|
+
const DANGEROUS_SCHEMES = [
|
|
1270
|
+
"javascript:",
|
|
1271
|
+
"data:",
|
|
1272
|
+
"vbscript:"
|
|
1273
|
+
];
|
|
1274
|
+
function isLocalhost(hostname) {
|
|
1275
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
1276
|
+
}
|
|
1505
1277
|
/**
|
|
1506
|
-
* Reusable URL validation
|
|
1278
|
+
* Reusable URL validation for OAuth redirect URIs.
|
|
1279
|
+
* - Blocks dangerous schemes (javascript:, data:, vbscript:)
|
|
1280
|
+
* - For http/https: requires HTTPS (HTTP allowed only for localhost)
|
|
1281
|
+
* - Allows custom schemes for mobile apps (e.g., myapp://callback)
|
|
1507
1282
|
*/
|
|
1508
1283
|
const SafeUrlSchema = z.url().superRefine((val, ctx) => {
|
|
1509
1284
|
if (!URL.canParse(val)) {
|
|
@@ -1514,33 +1289,44 @@ const SafeUrlSchema = z.url().superRefine((val, ctx) => {
|
|
|
1514
1289
|
});
|
|
1515
1290
|
return z.NEVER;
|
|
1516
1291
|
}
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1292
|
+
const u = new URL(val);
|
|
1293
|
+
if (DANGEROUS_SCHEMES.includes(u.protocol)) {
|
|
1294
|
+
ctx.addIssue({
|
|
1295
|
+
code: "custom",
|
|
1296
|
+
message: "URL cannot use javascript:, data:, or vbscript: scheme"
|
|
1297
|
+
});
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
if (u.protocol === "http:" || u.protocol === "https:") {
|
|
1301
|
+
if (u.protocol === "http:" && !isLocalhost(u.hostname)) ctx.addIssue({
|
|
1302
|
+
code: "custom",
|
|
1303
|
+
message: "Redirect URI must use HTTPS (HTTP allowed only for localhost)"
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
});
|
|
1521
1307
|
|
|
1522
1308
|
//#endregion
|
|
1523
1309
|
//#region src/oauthClient/endpoints.ts
|
|
1524
1310
|
async function getClientEndpoint(ctx, opts) {
|
|
1525
1311
|
const session = await getSessionFromCtx(ctx);
|
|
1526
|
-
if (!session) throw new APIError
|
|
1527
|
-
if (!ctx.headers) throw new APIError
|
|
1312
|
+
if (!session) throw new APIError("UNAUTHORIZED");
|
|
1313
|
+
if (!ctx.headers) throw new APIError("BAD_REQUEST");
|
|
1528
1314
|
if (opts.clientPrivileges && !await opts.clientPrivileges({
|
|
1529
1315
|
headers: ctx.headers,
|
|
1530
1316
|
action: "read",
|
|
1531
1317
|
session: session.session,
|
|
1532
1318
|
user: session.user
|
|
1533
|
-
})) throw new APIError
|
|
1319
|
+
})) throw new APIError("UNAUTHORIZED");
|
|
1534
1320
|
const client = await getClient(ctx, opts, ctx.query.client_id);
|
|
1535
|
-
if (!client) throw new APIError
|
|
1321
|
+
if (!client) throw new APIError("NOT_FOUND", {
|
|
1536
1322
|
error_description: "client not found",
|
|
1537
1323
|
error: "not_found"
|
|
1538
1324
|
});
|
|
1539
1325
|
if (client.userId) {
|
|
1540
|
-
if (client.userId !== session.user.id) throw new APIError
|
|
1326
|
+
if (client.userId !== session.user.id) throw new APIError("UNAUTHORIZED");
|
|
1541
1327
|
} else if (client.referenceId && opts.clientReference) {
|
|
1542
|
-
if (client.referenceId !== await opts.clientReference(session)) throw new APIError
|
|
1543
|
-
} else throw new APIError
|
|
1328
|
+
if (client.referenceId !== await opts.clientReference(session)) throw new APIError("UNAUTHORIZED");
|
|
1329
|
+
} else throw new APIError("UNAUTHORIZED");
|
|
1544
1330
|
const res = schemaToOAuth(client);
|
|
1545
1331
|
res.client_secret = void 0;
|
|
1546
1332
|
return res;
|
|
@@ -1551,11 +1337,11 @@ async function getClientEndpoint(ctx, opts) {
|
|
|
1551
1337
|
*/
|
|
1552
1338
|
async function getClientPublicEndpoint(ctx, opts) {
|
|
1553
1339
|
const client = await getClient(ctx, opts, ctx.query.client_id);
|
|
1554
|
-
if (!client) throw new APIError
|
|
1340
|
+
if (!client) throw new APIError("NOT_FOUND", {
|
|
1555
1341
|
error_description: "client not found",
|
|
1556
1342
|
error: "not_found"
|
|
1557
1343
|
});
|
|
1558
|
-
if (client.disabled) throw new APIError
|
|
1344
|
+
if (client.disabled) throw new APIError("NOT_FOUND", {
|
|
1559
1345
|
error_description: "client not found",
|
|
1560
1346
|
error: "not_found"
|
|
1561
1347
|
});
|
|
@@ -1571,14 +1357,14 @@ async function getClientPublicEndpoint(ctx, opts) {
|
|
|
1571
1357
|
}
|
|
1572
1358
|
async function getClientsEndpoint(ctx, opts) {
|
|
1573
1359
|
const session = await getSessionFromCtx(ctx);
|
|
1574
|
-
if (!session) throw new APIError
|
|
1575
|
-
if (!ctx.headers) throw new APIError
|
|
1360
|
+
if (!session) throw new APIError("UNAUTHORIZED");
|
|
1361
|
+
if (!ctx.headers) throw new APIError("BAD_REQUEST");
|
|
1576
1362
|
if (opts.clientPrivileges && !await opts.clientPrivileges({
|
|
1577
1363
|
headers: ctx.headers,
|
|
1578
1364
|
action: "list",
|
|
1579
1365
|
session: session.session,
|
|
1580
1366
|
user: session.user
|
|
1581
|
-
})) throw new APIError
|
|
1367
|
+
})) throw new APIError("UNAUTHORIZED");
|
|
1582
1368
|
const referenceId = await opts.clientReference?.(session);
|
|
1583
1369
|
if (referenceId) return await ctx.context.adapter.findMany({
|
|
1584
1370
|
model: "oauthClient",
|
|
@@ -1589,9 +1375,9 @@ async function getClientsEndpoint(ctx, opts) {
|
|
|
1589
1375
|
}).then((res) => {
|
|
1590
1376
|
if (!res) return null;
|
|
1591
1377
|
return res.map((v) => {
|
|
1592
|
-
const res
|
|
1593
|
-
res
|
|
1594
|
-
return res
|
|
1378
|
+
const res = schemaToOAuth(v);
|
|
1379
|
+
res.client_secret = void 0;
|
|
1380
|
+
return res;
|
|
1595
1381
|
});
|
|
1596
1382
|
});
|
|
1597
1383
|
else if (session.user.id) return await ctx.context.adapter.findMany({
|
|
@@ -1603,38 +1389,38 @@ async function getClientsEndpoint(ctx, opts) {
|
|
|
1603
1389
|
}).then((res) => {
|
|
1604
1390
|
if (!res) return null;
|
|
1605
1391
|
return res.map((v) => {
|
|
1606
|
-
const res
|
|
1607
|
-
res
|
|
1608
|
-
return res
|
|
1392
|
+
const res = schemaToOAuth(v);
|
|
1393
|
+
res.client_secret = void 0;
|
|
1394
|
+
return res;
|
|
1609
1395
|
});
|
|
1610
1396
|
});
|
|
1611
|
-
else throw new APIError
|
|
1397
|
+
else throw new APIError("BAD_REQUEST", { message: "either user_id or reference_id must be provided" });
|
|
1612
1398
|
}
|
|
1613
1399
|
async function deleteClientEndpoint(ctx, opts) {
|
|
1614
1400
|
const session = await getSessionFromCtx(ctx);
|
|
1615
|
-
if (!session) throw new APIError
|
|
1616
|
-
if (!ctx.headers) throw new APIError
|
|
1401
|
+
if (!session) throw new APIError("UNAUTHORIZED");
|
|
1402
|
+
if (!ctx.headers) throw new APIError("BAD_REQUEST");
|
|
1617
1403
|
if (opts.clientPrivileges && !await opts.clientPrivileges({
|
|
1618
1404
|
headers: ctx.headers,
|
|
1619
1405
|
action: "delete",
|
|
1620
1406
|
session: session.session,
|
|
1621
1407
|
user: session.user
|
|
1622
|
-
})) throw new APIError
|
|
1408
|
+
})) throw new APIError("UNAUTHORIZED");
|
|
1623
1409
|
const clientId = ctx.body.client_id;
|
|
1624
|
-
if (opts.cachedTrustedClients?.has(clientId)) throw new APIError
|
|
1410
|
+
if (opts.cachedTrustedClients?.has(clientId)) throw new APIError("INTERNAL_SERVER_ERROR", {
|
|
1625
1411
|
error_description: "trusted clients must be updated manually",
|
|
1626
1412
|
error: "invalid_client"
|
|
1627
1413
|
});
|
|
1628
1414
|
const client = await getClient(ctx, opts, clientId);
|
|
1629
|
-
if (!client) throw new APIError
|
|
1415
|
+
if (!client) throw new APIError("NOT_FOUND", {
|
|
1630
1416
|
error_description: "client not found",
|
|
1631
1417
|
error: "not_found"
|
|
1632
1418
|
});
|
|
1633
1419
|
if (client.userId) {
|
|
1634
|
-
if (client.userId !== session.user.id) throw new APIError
|
|
1420
|
+
if (client.userId !== session.user.id) throw new APIError("UNAUTHORIZED");
|
|
1635
1421
|
} else if (client.referenceId && opts.clientReference) {
|
|
1636
|
-
if (client.referenceId !== await opts.clientReference(session)) throw new APIError
|
|
1637
|
-
} else throw new APIError
|
|
1422
|
+
if (client.referenceId !== await opts.clientReference(session)) throw new APIError("UNAUTHORIZED");
|
|
1423
|
+
} else throw new APIError("UNAUTHORIZED");
|
|
1638
1424
|
await ctx.context.adapter.delete({
|
|
1639
1425
|
model: "oauthClient",
|
|
1640
1426
|
where: [{
|
|
@@ -1645,34 +1431,34 @@ async function deleteClientEndpoint(ctx, opts) {
|
|
|
1645
1431
|
}
|
|
1646
1432
|
async function updateClientEndpoint(ctx, opts) {
|
|
1647
1433
|
const session = await getSessionFromCtx(ctx);
|
|
1648
|
-
if (!session) throw new APIError
|
|
1649
|
-
if (!ctx.headers) throw new APIError
|
|
1434
|
+
if (!session) throw new APIError("UNAUTHORIZED");
|
|
1435
|
+
if (!ctx.headers) throw new APIError("BAD_REQUEST");
|
|
1650
1436
|
if (opts.clientPrivileges && !await opts.clientPrivileges({
|
|
1651
1437
|
headers: ctx.headers,
|
|
1652
1438
|
action: "update",
|
|
1653
1439
|
session: session.session,
|
|
1654
1440
|
user: session.user
|
|
1655
|
-
})) throw new APIError
|
|
1441
|
+
})) throw new APIError("UNAUTHORIZED");
|
|
1656
1442
|
const clientId = ctx.body.client_id;
|
|
1657
|
-
if (opts.cachedTrustedClients?.has(clientId)) throw new APIError
|
|
1443
|
+
if (opts.cachedTrustedClients?.has(clientId)) throw new APIError("INTERNAL_SERVER_ERROR", {
|
|
1658
1444
|
error_description: "trusted clients must be updated manually",
|
|
1659
1445
|
error: "invalid_client"
|
|
1660
1446
|
});
|
|
1661
1447
|
const client = await getClient(ctx, opts, clientId);
|
|
1662
|
-
if (!client) throw new APIError
|
|
1448
|
+
if (!client) throw new APIError("NOT_FOUND", {
|
|
1663
1449
|
error_description: "client not found",
|
|
1664
1450
|
error: "not_found"
|
|
1665
1451
|
});
|
|
1666
1452
|
if (client.userId) {
|
|
1667
|
-
if (client.userId !== session.user.id) throw new APIError
|
|
1453
|
+
if (client.userId !== session.user.id) throw new APIError("UNAUTHORIZED");
|
|
1668
1454
|
} else if (client.referenceId && opts.clientReference) {
|
|
1669
|
-
if (client.referenceId !== await opts.clientReference(session)) throw new APIError
|
|
1670
|
-
} else throw new APIError
|
|
1455
|
+
if (client.referenceId !== await opts.clientReference(session)) throw new APIError("UNAUTHORIZED");
|
|
1456
|
+
} else throw new APIError("UNAUTHORIZED");
|
|
1671
1457
|
const updates = ctx.body.update;
|
|
1672
1458
|
if (Object.keys(updates).length === 0) {
|
|
1673
|
-
const res
|
|
1674
|
-
res
|
|
1675
|
-
return res
|
|
1459
|
+
const res = schemaToOAuth(client);
|
|
1460
|
+
res.client_secret = void 0;
|
|
1461
|
+
return res;
|
|
1676
1462
|
}
|
|
1677
1463
|
await checkOAuthClient({
|
|
1678
1464
|
...schemaToOAuth(client),
|
|
@@ -1684,9 +1470,12 @@ async function updateClientEndpoint(ctx, opts) {
|
|
|
1684
1470
|
field: "clientId",
|
|
1685
1471
|
value: clientId
|
|
1686
1472
|
}],
|
|
1687
|
-
update:
|
|
1473
|
+
update: {
|
|
1474
|
+
...oauthToSchema(updates),
|
|
1475
|
+
updatedAt: /* @__PURE__ */ new Date(Math.floor(Date.now() / 1e3) * 1e3)
|
|
1476
|
+
}
|
|
1688
1477
|
});
|
|
1689
|
-
if (!updatedClient) throw new APIError
|
|
1478
|
+
if (!updatedClient) throw new APIError("INTERNAL_SERVER_ERROR", {
|
|
1690
1479
|
error_description: "unable to update client",
|
|
1691
1480
|
error: "invalid_client"
|
|
1692
1481
|
});
|
|
@@ -1696,30 +1485,30 @@ async function updateClientEndpoint(ctx, opts) {
|
|
|
1696
1485
|
}
|
|
1697
1486
|
async function rotateClientSecretEndpoint(ctx, opts) {
|
|
1698
1487
|
const session = await getSessionFromCtx(ctx);
|
|
1699
|
-
if (!session) throw new APIError
|
|
1700
|
-
if (!ctx.headers) throw new APIError
|
|
1488
|
+
if (!session) throw new APIError("UNAUTHORIZED");
|
|
1489
|
+
if (!ctx.headers) throw new APIError("BAD_REQUEST");
|
|
1701
1490
|
if (opts.clientPrivileges && !await opts.clientPrivileges({
|
|
1702
1491
|
headers: ctx.headers,
|
|
1703
1492
|
action: "rotate",
|
|
1704
1493
|
session: session.session,
|
|
1705
1494
|
user: session.user
|
|
1706
|
-
})) throw new APIError
|
|
1495
|
+
})) throw new APIError("UNAUTHORIZED");
|
|
1707
1496
|
const clientId = ctx.body.client_id;
|
|
1708
|
-
if (opts.cachedTrustedClients?.has(clientId)) throw new APIError
|
|
1497
|
+
if (opts.cachedTrustedClients?.has(clientId)) throw new APIError("INTERNAL_SERVER_ERROR", {
|
|
1709
1498
|
error_description: "trusted clients must be updated manually",
|
|
1710
1499
|
error: "invalid_client"
|
|
1711
1500
|
});
|
|
1712
1501
|
const client = await getClient(ctx, opts, clientId);
|
|
1713
|
-
if (!client) throw new APIError
|
|
1502
|
+
if (!client) throw new APIError("NOT_FOUND", {
|
|
1714
1503
|
error_description: "client not found",
|
|
1715
1504
|
error: "not_found"
|
|
1716
1505
|
});
|
|
1717
1506
|
if (client.userId) {
|
|
1718
|
-
if (client.userId !== session.user.id) throw new APIError
|
|
1507
|
+
if (client.userId !== session.user.id) throw new APIError("UNAUTHORIZED");
|
|
1719
1508
|
} else if (client.referenceId && opts.clientReference) {
|
|
1720
|
-
if (client.referenceId !== await opts.clientReference(session)) throw new APIError
|
|
1721
|
-
} else throw new APIError
|
|
1722
|
-
if (client.public || !client.clientSecret) throw new APIError
|
|
1509
|
+
if (client.referenceId !== await opts.clientReference(session)) throw new APIError("UNAUTHORIZED");
|
|
1510
|
+
} else throw new APIError("UNAUTHORIZED");
|
|
1511
|
+
if (client.public || !client.clientSecret) throw new APIError("BAD_REQUEST", {
|
|
1723
1512
|
error_description: "public clients cannot be updated",
|
|
1724
1513
|
error: "invalid_client"
|
|
1725
1514
|
});
|
|
@@ -1732,11 +1521,11 @@ async function rotateClientSecretEndpoint(ctx, opts) {
|
|
|
1732
1521
|
value: clientId
|
|
1733
1522
|
}],
|
|
1734
1523
|
update: {
|
|
1735
|
-
|
|
1736
|
-
|
|
1524
|
+
clientSecret: storedClientSecret,
|
|
1525
|
+
updatedAt: /* @__PURE__ */ new Date(Math.floor(Date.now() / 1e3) * 1e3)
|
|
1737
1526
|
}
|
|
1738
1527
|
});
|
|
1739
|
-
if (!updatedClient) throw new APIError
|
|
1528
|
+
if (!updatedClient) throw new APIError("INTERNAL_SERVER_ERROR", {
|
|
1740
1529
|
error_description: "unable to update client",
|
|
1741
1530
|
error: "invalid_client"
|
|
1742
1531
|
});
|
|
@@ -1782,6 +1571,7 @@ const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/creat
|
|
|
1782
1571
|
client_secret_expires_at: z.union([z.string(), z.number()]).optional().default(0),
|
|
1783
1572
|
skip_consent: z.boolean().optional(),
|
|
1784
1573
|
enable_end_session: z.boolean().optional(),
|
|
1574
|
+
require_pkce: z.boolean().optional(),
|
|
1785
1575
|
metadata: z.record(z.string(), z.unknown()).optional()
|
|
1786
1576
|
}),
|
|
1787
1577
|
metadata: {
|
|
@@ -1908,6 +1698,11 @@ const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/creat
|
|
|
1908
1698
|
type: "boolean",
|
|
1909
1699
|
description: "Whether the client is disabled"
|
|
1910
1700
|
},
|
|
1701
|
+
require_pkce: {
|
|
1702
|
+
type: "boolean",
|
|
1703
|
+
description: "Whether the client requires PKCE",
|
|
1704
|
+
default: true
|
|
1705
|
+
},
|
|
1911
1706
|
metadata: {
|
|
1912
1707
|
type: "object",
|
|
1913
1708
|
additionalProperties: true,
|
|
@@ -2221,23 +2016,23 @@ async function getConsent(ctx, opts, id) {
|
|
|
2221
2016
|
}
|
|
2222
2017
|
async function getConsentEndpoint(ctx, opts) {
|
|
2223
2018
|
const session = await getSessionFromCtx(ctx);
|
|
2224
|
-
if (!session) throw new APIError
|
|
2019
|
+
if (!session) throw new APIError("UNAUTHORIZED");
|
|
2225
2020
|
const { id } = ctx.query;
|
|
2226
|
-
if (!id) throw new APIError
|
|
2021
|
+
if (!id) throw new APIError("NOT_FOUND", {
|
|
2227
2022
|
error_description: "missing id parameter",
|
|
2228
2023
|
error: "not_found"
|
|
2229
2024
|
});
|
|
2230
2025
|
const consent = await getConsent(ctx, opts, id);
|
|
2231
|
-
if (!consent) throw new APIError
|
|
2026
|
+
if (!consent) throw new APIError("NOT_FOUND", {
|
|
2232
2027
|
error_description: "no consent",
|
|
2233
2028
|
error: "not_found"
|
|
2234
2029
|
});
|
|
2235
|
-
if (consent.userId !== session.user.id) throw new APIError
|
|
2030
|
+
if (consent.userId !== session.user.id) throw new APIError("UNAUTHORIZED");
|
|
2236
2031
|
return consent;
|
|
2237
2032
|
}
|
|
2238
2033
|
async function getConsentsEndpoint(ctx, opts) {
|
|
2239
2034
|
const session = await getSessionFromCtx(ctx);
|
|
2240
|
-
if (!session) throw new APIError
|
|
2035
|
+
if (!session) throw new APIError("UNAUTHORIZED");
|
|
2241
2036
|
return await ctx.context.adapter.findMany({
|
|
2242
2037
|
model: "oauthConsent",
|
|
2243
2038
|
where: [{
|
|
@@ -2248,18 +2043,18 @@ async function getConsentsEndpoint(ctx, opts) {
|
|
|
2248
2043
|
}
|
|
2249
2044
|
async function deleteConsentEndpoint(ctx, opts) {
|
|
2250
2045
|
const session = await getSessionFromCtx(ctx);
|
|
2251
|
-
if (!session) throw new APIError
|
|
2046
|
+
if (!session) throw new APIError("UNAUTHORIZED");
|
|
2252
2047
|
const { id } = ctx.body;
|
|
2253
|
-
if (!id) throw new APIError
|
|
2048
|
+
if (!id) throw new APIError("NOT_FOUND", {
|
|
2254
2049
|
error_description: "missing id parameter",
|
|
2255
2050
|
error: "not_found"
|
|
2256
2051
|
});
|
|
2257
2052
|
const consent = await getConsent(ctx, opts, id);
|
|
2258
|
-
if (!consent) throw new APIError
|
|
2053
|
+
if (!consent) throw new APIError("NOT_FOUND", {
|
|
2259
2054
|
error_description: "no consent",
|
|
2260
2055
|
error: "not_found"
|
|
2261
2056
|
});
|
|
2262
|
-
if (consent.userId !== session.user.id) throw new APIError
|
|
2057
|
+
if (consent.userId !== session.user.id) throw new APIError("UNAUTHORIZED");
|
|
2263
2058
|
await ctx.context.adapter.delete({
|
|
2264
2059
|
model: "oauthConsent",
|
|
2265
2060
|
where: [{
|
|
@@ -2270,27 +2065,27 @@ async function deleteConsentEndpoint(ctx, opts) {
|
|
|
2270
2065
|
}
|
|
2271
2066
|
async function updateConsentEndpoint(ctx, opts) {
|
|
2272
2067
|
const session = await getSessionFromCtx(ctx);
|
|
2273
|
-
if (!session) throw new APIError
|
|
2068
|
+
if (!session) throw new APIError("UNAUTHORIZED");
|
|
2274
2069
|
const { id } = ctx.body;
|
|
2275
|
-
if (!id) throw new APIError
|
|
2070
|
+
if (!id) throw new APIError("NOT_FOUND", {
|
|
2276
2071
|
error_description: "missing id parameter",
|
|
2277
2072
|
error: "not_found"
|
|
2278
2073
|
});
|
|
2279
2074
|
const consent = await getConsent(ctx, opts, id);
|
|
2280
|
-
if (!consent) throw new APIError
|
|
2075
|
+
if (!consent) throw new APIError("NOT_FOUND", {
|
|
2281
2076
|
error_description: "no consent",
|
|
2282
2077
|
error: "not_found"
|
|
2283
2078
|
});
|
|
2284
2079
|
const client = await getClient(ctx, opts, consent.clientId);
|
|
2285
|
-
if (!consent) throw new APIError
|
|
2080
|
+
if (!consent) throw new APIError("NOT_FOUND", {
|
|
2286
2081
|
error_description: "no consent",
|
|
2287
2082
|
error: "not_found"
|
|
2288
2083
|
});
|
|
2289
|
-
if (consent.userId !== session.user.id) throw new APIError
|
|
2084
|
+
if (consent.userId !== session.user.id) throw new APIError("UNAUTHORIZED");
|
|
2290
2085
|
const allowedScopes = client?.scopes ?? opts.scopes ?? [];
|
|
2291
2086
|
const updates = ctx.body.update;
|
|
2292
2087
|
const scopes = updates.scopes;
|
|
2293
|
-
if (scopes && !scopes.every((val) => allowedScopes?.includes(val))) throw new APIError
|
|
2088
|
+
if (scopes && !scopes.every((val) => allowedScopes?.includes(val))) throw new APIError("BAD_REQUEST", {
|
|
2294
2089
|
error_description: `unable to provide scopes to ${client?.referenceId ?? client?.userId}`,
|
|
2295
2090
|
error: "invalid_request"
|
|
2296
2091
|
});
|
|
@@ -2373,7 +2168,7 @@ async function revokeJwtAccessToken(ctx, opts, token) {
|
|
|
2373
2168
|
});
|
|
2374
2169
|
} catch (error) {
|
|
2375
2170
|
if (error instanceof Error) {
|
|
2376
|
-
if (error.name === "TypeError" || error.name === "JWSInvalid") throw new APIError("BAD_REQUEST", {
|
|
2171
|
+
if (error.name === "TypeError" || error.name === "JWSInvalid") throw new APIError$1("BAD_REQUEST", {
|
|
2377
2172
|
error_description: "invalid JWT signature",
|
|
2378
2173
|
error: "invalid_request"
|
|
2379
2174
|
});
|
|
@@ -2390,7 +2185,7 @@ async function revokeJwtAccessToken(ctx, opts, token) {
|
|
|
2390
2185
|
async function revokeOpaqueAccessToken(ctx, opts, token, clientId) {
|
|
2391
2186
|
let tokenValue = token;
|
|
2392
2187
|
if (opts.prefix?.opaqueAccessToken) if (tokenValue.startsWith(opts.prefix.opaqueAccessToken)) tokenValue = tokenValue.replace(opts.prefix.opaqueAccessToken, "");
|
|
2393
|
-
else throw new APIError("BAD_REQUEST", {
|
|
2188
|
+
else throw new APIError$1("BAD_REQUEST", {
|
|
2394
2189
|
error_description: "opaque access token not found",
|
|
2395
2190
|
error: "invalid_request"
|
|
2396
2191
|
});
|
|
@@ -2401,7 +2196,7 @@ async function revokeOpaqueAccessToken(ctx, opts, token, clientId) {
|
|
|
2401
2196
|
value: await getStoredToken(opts.storeTokens, tokenValue, "access_token")
|
|
2402
2197
|
}]
|
|
2403
2198
|
});
|
|
2404
|
-
if (!accessToken) throw new APIError("BAD_REQUEST", {
|
|
2199
|
+
if (!accessToken) throw new APIError$1("BAD_REQUEST", {
|
|
2405
2200
|
error_description: "opaque access token not found",
|
|
2406
2201
|
error: "invalid_request"
|
|
2407
2202
|
});
|
|
@@ -2431,7 +2226,7 @@ async function revokeRefreshToken(ctx, opts, token, clientId) {
|
|
|
2431
2226
|
value: await getStoredToken(opts.storeTokens, token, "refresh_token")
|
|
2432
2227
|
}]
|
|
2433
2228
|
});
|
|
2434
|
-
if (!refreshToken) throw new APIError("BAD_REQUEST", {
|
|
2229
|
+
if (!refreshToken) throw new APIError$1("BAD_REQUEST", {
|
|
2435
2230
|
error_description: "token not found",
|
|
2436
2231
|
error: "invalid_request"
|
|
2437
2232
|
});
|
|
@@ -2446,7 +2241,7 @@ async function revokeRefreshToken(ctx, opts, token, clientId) {
|
|
|
2446
2241
|
value: refreshToken.userId
|
|
2447
2242
|
}]
|
|
2448
2243
|
});
|
|
2449
|
-
throw new APIError("BAD_REQUEST", {
|
|
2244
|
+
throw new APIError$1("BAD_REQUEST", {
|
|
2450
2245
|
error_description: "refresh token revoked",
|
|
2451
2246
|
error: "invalid_request"
|
|
2452
2247
|
});
|
|
@@ -2476,16 +2271,16 @@ async function revokeAccessToken(ctx, opts, clientId, token) {
|
|
|
2476
2271
|
try {
|
|
2477
2272
|
return await revokeJwtAccessToken(ctx, opts, token);
|
|
2478
2273
|
} catch (err) {
|
|
2479
|
-
if (err instanceof APIError) {} else if (err instanceof Error) throw err;
|
|
2274
|
+
if (err instanceof APIError$1) {} else if (err instanceof Error) throw err;
|
|
2480
2275
|
else throw new Error(err);
|
|
2481
2276
|
}
|
|
2482
2277
|
try {
|
|
2483
2278
|
return await revokeOpaqueAccessToken(ctx, opts, token, clientId);
|
|
2484
2279
|
} catch (err) {
|
|
2485
|
-
if (err instanceof APIError) {} else if (err instanceof Error) throw err;
|
|
2280
|
+
if (err instanceof APIError$1) {} else if (err instanceof Error) throw err;
|
|
2486
2281
|
else throw new Error("Unknown error validating access token");
|
|
2487
2282
|
}
|
|
2488
|
-
throw new APIError("BAD_REQUEST", {
|
|
2283
|
+
throw new APIError$1("BAD_REQUEST", {
|
|
2489
2284
|
error_description: "Invalid access token",
|
|
2490
2285
|
error: "invalid_request"
|
|
2491
2286
|
});
|
|
@@ -2498,12 +2293,12 @@ async function revokeEndpoint(ctx, opts) {
|
|
|
2498
2293
|
client_id = res?.client_id;
|
|
2499
2294
|
client_secret = res?.client_secret;
|
|
2500
2295
|
}
|
|
2501
|
-
if (!client_id) throw new APIError("UNAUTHORIZED", {
|
|
2296
|
+
if (!client_id) throw new APIError$1("UNAUTHORIZED", {
|
|
2502
2297
|
error_description: "missing required credentials",
|
|
2503
2298
|
error: "invalid_client"
|
|
2504
2299
|
});
|
|
2505
2300
|
if (typeof token === "string" && token.startsWith("Bearer ")) token = token.replace("Bearer ", "");
|
|
2506
|
-
if (!token?.length) throw new APIError("BAD_REQUEST", {
|
|
2301
|
+
if (!token?.length) throw new APIError$1("BAD_REQUEST", {
|
|
2507
2302
|
error_description: "missing a required token for introspection",
|
|
2508
2303
|
error: "invalid_request"
|
|
2509
2304
|
});
|
|
@@ -2512,7 +2307,7 @@ async function revokeEndpoint(ctx, opts) {
|
|
|
2512
2307
|
if (token_type_hint === void 0 || token_type_hint === "access_token") try {
|
|
2513
2308
|
return await revokeAccessToken(ctx, opts, client.clientId, token);
|
|
2514
2309
|
} catch (error) {
|
|
2515
|
-
if (error instanceof APIError) {
|
|
2310
|
+
if (error instanceof APIError$1) {
|
|
2516
2311
|
if (token_type_hint === "access_token") throw error;
|
|
2517
2312
|
} else if (error instanceof Error) throw error;
|
|
2518
2313
|
else throw new Error(error);
|
|
@@ -2520,25 +2315,25 @@ async function revokeEndpoint(ctx, opts) {
|
|
|
2520
2315
|
if (token_type_hint === void 0 || token_type_hint === "refresh_token") try {
|
|
2521
2316
|
return await revokeRefreshToken(ctx, opts, (await decodeRefreshToken(opts, token)).token, client.clientId);
|
|
2522
2317
|
} catch (error) {
|
|
2523
|
-
if (error instanceof APIError) {
|
|
2318
|
+
if (error instanceof APIError$1) {
|
|
2524
2319
|
if (token_type_hint === "refresh_token") throw error;
|
|
2525
2320
|
} else if (error instanceof Error) throw error;
|
|
2526
2321
|
else throw new Error(error);
|
|
2527
2322
|
}
|
|
2528
|
-
throw new APIError("BAD_REQUEST", {
|
|
2323
|
+
throw new APIError$1("BAD_REQUEST", {
|
|
2529
2324
|
error_description: "token not found",
|
|
2530
2325
|
error: "invalid_request"
|
|
2531
2326
|
});
|
|
2532
2327
|
} catch (error) {
|
|
2533
|
-
if (error instanceof APIError) {
|
|
2328
|
+
if (error instanceof APIError$1) {
|
|
2534
2329
|
if (error.name === "BAD_REQUEST") return null;
|
|
2535
2330
|
throw error;
|
|
2536
2331
|
} else if (error instanceof Error) {
|
|
2537
2332
|
logger.error("Introspection error:", error.message, error.stack);
|
|
2538
|
-
throw new APIError("INTERNAL_SERVER_ERROR");
|
|
2333
|
+
throw new APIError$1("INTERNAL_SERVER_ERROR");
|
|
2539
2334
|
} else {
|
|
2540
2335
|
logger.error("Introspection error:", error);
|
|
2541
|
-
throw new APIError("INTERNAL_SERVER_ERROR");
|
|
2336
|
+
throw new APIError$1("INTERNAL_SERVER_ERROR");
|
|
2542
2337
|
}
|
|
2543
2338
|
}
|
|
2544
2339
|
}
|
|
@@ -2655,6 +2450,10 @@ const schema = {
|
|
|
2655
2450
|
type: "string",
|
|
2656
2451
|
required: false
|
|
2657
2452
|
},
|
|
2453
|
+
requirePKCE: {
|
|
2454
|
+
type: "boolean",
|
|
2455
|
+
required: false
|
|
2456
|
+
},
|
|
2658
2457
|
referenceId: {
|
|
2659
2458
|
type: "string",
|
|
2660
2459
|
required: false
|
|
@@ -2705,6 +2504,10 @@ const schema = {
|
|
|
2705
2504
|
type: "date",
|
|
2706
2505
|
required: false
|
|
2707
2506
|
},
|
|
2507
|
+
authTime: {
|
|
2508
|
+
type: "date",
|
|
2509
|
+
required: false
|
|
2510
|
+
},
|
|
2708
2511
|
scopes: {
|
|
2709
2512
|
type: "string[]",
|
|
2710
2513
|
required: true
|
|
@@ -2798,6 +2601,7 @@ const schema = {
|
|
|
2798
2601
|
//#endregion
|
|
2799
2602
|
//#region src/oauth.ts
|
|
2800
2603
|
const oAuthState = defineRequestState(() => null);
|
|
2604
|
+
const getOAuthProviderState = oAuthState.get;
|
|
2801
2605
|
/**
|
|
2802
2606
|
* oAuth 2.1 provider plugin for Better Auth.
|
|
2803
2607
|
*
|
|
@@ -2886,7 +2690,7 @@ const oauthProvider = (options) => {
|
|
|
2886
2690
|
queryParams.delete("sig");
|
|
2887
2691
|
queryParams = new URLSearchParams(queryParams);
|
|
2888
2692
|
const verifySig = await makeSignature(queryParams.toString(), ctx.context.secret);
|
|
2889
|
-
if (!sig || !constantTimeEqual(sig, verifySig) || /* @__PURE__ */ new Date(exp * 1e3) < /* @__PURE__ */ new Date()) throw new APIError
|
|
2693
|
+
if (!sig || !constantTimeEqual(sig, verifySig) || /* @__PURE__ */ new Date(exp * 1e3) < /* @__PURE__ */ new Date()) throw new APIError("BAD_REQUEST", { error: "invalid_signature" });
|
|
2890
2694
|
queryParams.delete("exp");
|
|
2891
2695
|
await oAuthState.set({ query: new URLSearchParams(queryParams).toString() });
|
|
2892
2696
|
if (ctx.path === "/sign-in/social" || ctx.path === "/sign-in/oauth2") {
|
|
@@ -2920,13 +2724,18 @@ const oauthProvider = (options) => {
|
|
|
2920
2724
|
metadata: { SERVER_ONLY: true }
|
|
2921
2725
|
}, async (ctx) => {
|
|
2922
2726
|
if (opts.scopes && opts.scopes.includes("openid")) return oidcServerMetadata(ctx, opts);
|
|
2923
|
-
else return authServerMetadata(ctx, opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context)?.options, {
|
|
2727
|
+
else return authServerMetadata(ctx, opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context)?.options, {
|
|
2728
|
+
scopes_supported: opts.advertisedMetadata?.scopes_supported ?? opts.scopes,
|
|
2729
|
+
public_client_supported: opts.allowUnauthenticatedClientRegistration,
|
|
2730
|
+
grant_types_supported: opts.grantTypes,
|
|
2731
|
+
jwt_disabled: opts.disableJwtPlugin
|
|
2732
|
+
});
|
|
2924
2733
|
}),
|
|
2925
2734
|
getOpenIdConfig: createAuthEndpoint("/.well-known/openid-configuration", {
|
|
2926
2735
|
method: "GET",
|
|
2927
2736
|
metadata: { SERVER_ONLY: true }
|
|
2928
2737
|
}, async (ctx) => {
|
|
2929
|
-
if (opts.scopes && !opts.scopes.includes("openid")) throw new APIError
|
|
2738
|
+
if (opts.scopes && !opts.scopes.includes("openid")) throw new APIError("NOT_FOUND");
|
|
2930
2739
|
return oidcServerMetadata(ctx, opts);
|
|
2931
2740
|
}),
|
|
2932
2741
|
oauth2Authorize: createAuthEndpoint("/oauth2/authorize", {
|
|
@@ -2941,6 +2750,7 @@ const oauthProvider = (options) => {
|
|
|
2941
2750
|
code_challenge_method: z.enum(["S256"]).optional(),
|
|
2942
2751
|
nonce: z.string().optional(),
|
|
2943
2752
|
prompt: z.enum([
|
|
2753
|
+
"none",
|
|
2944
2754
|
"consent",
|
|
2945
2755
|
"login",
|
|
2946
2756
|
"create",
|
|
@@ -3715,9 +3525,354 @@ const oauthProvider = (options) => {
|
|
|
3715
3525
|
updateOAuthConsent: updateOAuthConsent(opts),
|
|
3716
3526
|
deleteOAuthConsent: deleteOAuthConsent(opts)
|
|
3717
3527
|
},
|
|
3718
|
-
schema: mergeSchema(schema, opts?.schema)
|
|
3528
|
+
schema: mergeSchema(schema, opts?.schema),
|
|
3529
|
+
rateLimit: [
|
|
3530
|
+
...opts.rateLimit?.token !== false ? [{
|
|
3531
|
+
pathMatcher: (path) => path === "/oauth2/token",
|
|
3532
|
+
window: opts.rateLimit?.token?.window ?? 60,
|
|
3533
|
+
max: opts.rateLimit?.token?.max ?? 20
|
|
3534
|
+
}] : [],
|
|
3535
|
+
...opts.rateLimit?.authorize !== false ? [{
|
|
3536
|
+
pathMatcher: (path) => path === "/oauth2/authorize",
|
|
3537
|
+
window: opts.rateLimit?.authorize?.window ?? 60,
|
|
3538
|
+
max: opts.rateLimit?.authorize?.max ?? 30
|
|
3539
|
+
}] : [],
|
|
3540
|
+
...opts.rateLimit?.introspect !== false ? [{
|
|
3541
|
+
pathMatcher: (path) => path === "/oauth2/introspect",
|
|
3542
|
+
window: opts.rateLimit?.introspect?.window ?? 60,
|
|
3543
|
+
max: opts.rateLimit?.introspect?.max ?? 100
|
|
3544
|
+
}] : [],
|
|
3545
|
+
...opts.rateLimit?.revoke !== false ? [{
|
|
3546
|
+
pathMatcher: (path) => path === "/oauth2/revoke",
|
|
3547
|
+
window: opts.rateLimit?.revoke?.window ?? 60,
|
|
3548
|
+
max: opts.rateLimit?.revoke?.max ?? 30
|
|
3549
|
+
}] : [],
|
|
3550
|
+
...opts.rateLimit?.register !== false ? [{
|
|
3551
|
+
pathMatcher: (path) => path === "/oauth2/register",
|
|
3552
|
+
window: opts.rateLimit?.register?.window ?? 60,
|
|
3553
|
+
max: opts.rateLimit?.register?.max ?? 5
|
|
3554
|
+
}] : [],
|
|
3555
|
+
...opts.rateLimit?.userinfo !== false ? [{
|
|
3556
|
+
pathMatcher: (path) => path === "/oauth2/userinfo",
|
|
3557
|
+
window: opts.rateLimit?.userinfo?.window ?? 60,
|
|
3558
|
+
max: opts.rateLimit?.userinfo?.max ?? 60
|
|
3559
|
+
}] : []
|
|
3560
|
+
]
|
|
3561
|
+
};
|
|
3562
|
+
};
|
|
3563
|
+
|
|
3564
|
+
//#endregion
|
|
3565
|
+
//#region src/authorize.ts
|
|
3566
|
+
/**
|
|
3567
|
+
* Formats an error url
|
|
3568
|
+
*/
|
|
3569
|
+
function formatErrorURL(url, error, description, state, iss) {
|
|
3570
|
+
const searchParams = new URLSearchParams({
|
|
3571
|
+
error,
|
|
3572
|
+
error_description: description
|
|
3573
|
+
});
|
|
3574
|
+
state && searchParams.append("state", state);
|
|
3575
|
+
iss && searchParams.append("iss", iss);
|
|
3576
|
+
return `${url}${url.includes("?") ? "&" : "?"}${searchParams.toString()}`;
|
|
3577
|
+
}
|
|
3578
|
+
const handleRedirect = (ctx, uri) => {
|
|
3579
|
+
if (ctx.headers?.get("accept")?.includes("application/json")) return {
|
|
3580
|
+
redirect: true,
|
|
3581
|
+
url: uri.toString()
|
|
3582
|
+
};
|
|
3583
|
+
else throw ctx.redirect(uri);
|
|
3584
|
+
};
|
|
3585
|
+
/**
|
|
3586
|
+
* Validates that the issuer URL
|
|
3587
|
+
* - MUST use HTTPS scheme (HTTP allowed for localhost in dev)
|
|
3588
|
+
* - MUST NOT contain query components
|
|
3589
|
+
* - MUST NOT contain fragment components
|
|
3590
|
+
*
|
|
3591
|
+
* @returns The validated issuer URL, or a sanitized version if invalid
|
|
3592
|
+
*/
|
|
3593
|
+
function validateIssuerUrl(issuer) {
|
|
3594
|
+
try {
|
|
3595
|
+
const url = new URL(issuer);
|
|
3596
|
+
const isLocalhost = url.hostname === "localhost" || url.hostname === "127.0.0.1";
|
|
3597
|
+
if (url.protocol !== "https:" && !isLocalhost) url.protocol = "https:";
|
|
3598
|
+
url.search = "";
|
|
3599
|
+
url.hash = "";
|
|
3600
|
+
return url.toString().replace(/\/$/, "");
|
|
3601
|
+
} catch {
|
|
3602
|
+
return issuer;
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
/**
|
|
3606
|
+
* Gets the issuer identifier
|
|
3607
|
+
*/
|
|
3608
|
+
function getIssuer(ctx, opts) {
|
|
3609
|
+
let issuer;
|
|
3610
|
+
if (opts.disableJwtPlugin) issuer = ctx.context.baseURL;
|
|
3611
|
+
else try {
|
|
3612
|
+
issuer = getJwtPlugin(ctx.context).options?.jwt?.issuer ?? ctx.context.baseURL;
|
|
3613
|
+
} catch {
|
|
3614
|
+
issuer = ctx.context.baseURL;
|
|
3615
|
+
}
|
|
3616
|
+
return validateIssuerUrl(issuer);
|
|
3617
|
+
}
|
|
3618
|
+
/**
|
|
3619
|
+
* Error page url if redirect_uri has not been verified yet
|
|
3620
|
+
* Generates Url for custom error page
|
|
3621
|
+
*/
|
|
3622
|
+
function getErrorURL(ctx, error, description) {
|
|
3623
|
+
return formatErrorURL(ctx.context.options.onAPIError?.errorURL || `${ctx.context.baseURL}/error`, error, description);
|
|
3624
|
+
}
|
|
3625
|
+
async function authorizeEndpoint(ctx, opts, settings) {
|
|
3626
|
+
if (opts.grantTypes && !opts.grantTypes.includes("authorization_code")) throw new APIError$1("NOT_FOUND");
|
|
3627
|
+
if (!ctx.request) throw new APIError$1("UNAUTHORIZED", {
|
|
3628
|
+
error_description: "request not found",
|
|
3629
|
+
error: "invalid_request"
|
|
3630
|
+
});
|
|
3631
|
+
const query = ctx.query;
|
|
3632
|
+
await oAuthState.set({ query: query.toString() });
|
|
3633
|
+
if (!query.client_id) throw ctx.redirect(getErrorURL(ctx, "invalid_client", "client_id is required"));
|
|
3634
|
+
if (!query.response_type) throw ctx.redirect(getErrorURL(ctx, "invalid_request", "response_type is required"));
|
|
3635
|
+
const promptSet = ctx.query?.prompt ? parsePrompt(ctx.query?.prompt) : void 0;
|
|
3636
|
+
if (promptSet?.has("select_account") && !opts.selectAccount?.page) throw ctx.redirect(getErrorURL(ctx, `unsupported_prompt_select_account`, "unsupported prompt type"));
|
|
3637
|
+
if (!(query.response_type === "code")) throw ctx.redirect(getErrorURL(ctx, "unsupported_response_type", "unsupported response type"));
|
|
3638
|
+
const client = await getClient(ctx, opts, query.client_id);
|
|
3639
|
+
if (!client) throw ctx.redirect(getErrorURL(ctx, "invalid_client", "client_id is required"));
|
|
3640
|
+
if (client.disabled) throw ctx.redirect(getErrorURL(ctx, "client_disabled", "client is disabled"));
|
|
3641
|
+
if (!client.redirectUris?.find((url) => url === query.redirect_uri) || !query.redirect_uri) throw ctx.redirect(getErrorURL(ctx, "invalid_redirect", "invalid redirect uri"));
|
|
3642
|
+
let requestedScopes = query.scope?.split(" ").filter((s) => s);
|
|
3643
|
+
if (requestedScopes) {
|
|
3644
|
+
const validScopes = new Set(client.scopes ?? opts.scopes);
|
|
3645
|
+
const invalidScopes = requestedScopes.filter((scope) => {
|
|
3646
|
+
return !validScopes?.has(scope);
|
|
3647
|
+
});
|
|
3648
|
+
if (invalidScopes.length) throw ctx.redirect(formatErrorURL(query.redirect_uri, "invalid_scope", `The following scopes are invalid: ${invalidScopes.join(", ")}`, query.state, getIssuer(ctx, opts)));
|
|
3649
|
+
}
|
|
3650
|
+
if (!requestedScopes) {
|
|
3651
|
+
requestedScopes = client.scopes ?? opts.scopes ?? [];
|
|
3652
|
+
query.scope = requestedScopes.join(" ");
|
|
3653
|
+
}
|
|
3654
|
+
const pkceRequired = isPKCERequired(client, requestedScopes);
|
|
3655
|
+
if (pkceRequired) {
|
|
3656
|
+
if (!query.code_challenge || !query.code_challenge_method) throw ctx.redirect(formatErrorURL(query.redirect_uri, "invalid_request", pkceRequired.valueOf(), query.state, getIssuer(ctx, opts)));
|
|
3657
|
+
}
|
|
3658
|
+
if (query.code_challenge || query.code_challenge_method) {
|
|
3659
|
+
if (!query.code_challenge || !query.code_challenge_method) throw ctx.redirect(formatErrorURL(query.redirect_uri, "invalid_request", "code_challenge and code_challenge_method must both be provided", query.state, getIssuer(ctx, opts)));
|
|
3660
|
+
if (!["S256"].includes(query.code_challenge_method)) throw ctx.redirect(formatErrorURL(query.redirect_uri, "invalid_request", "invalid code_challenge method, only S256 is supported", query.state, getIssuer(ctx, opts)));
|
|
3661
|
+
}
|
|
3662
|
+
const session = await getSessionFromCtx(ctx);
|
|
3663
|
+
if (!session || promptSet?.has("login") || promptSet?.has("create")) return redirectWithPromptCode(ctx, opts, promptSet?.has("create") ? "create" : "login");
|
|
3664
|
+
if (settings?.isAuthorize && promptSet?.has("select_account")) return redirectWithPromptCode(ctx, opts, "select_account");
|
|
3665
|
+
if (settings?.isAuthorize && opts.selectAccount) {
|
|
3666
|
+
if (await opts.selectAccount.shouldRedirect({
|
|
3667
|
+
headers: ctx.request.headers,
|
|
3668
|
+
user: session.user,
|
|
3669
|
+
session: session.session,
|
|
3670
|
+
scopes: requestedScopes
|
|
3671
|
+
})) return redirectWithPromptCode(ctx, opts, "select_account");
|
|
3672
|
+
}
|
|
3673
|
+
if (opts.signup?.shouldRedirect) {
|
|
3674
|
+
const signupRedirect = await opts.signup.shouldRedirect({
|
|
3675
|
+
headers: ctx.request.headers,
|
|
3676
|
+
user: session.user,
|
|
3677
|
+
session: session.session,
|
|
3678
|
+
scopes: requestedScopes
|
|
3679
|
+
});
|
|
3680
|
+
if (signupRedirect) return redirectWithPromptCode(ctx, opts, "create", typeof signupRedirect === "string" ? signupRedirect : void 0);
|
|
3681
|
+
}
|
|
3682
|
+
if (!settings?.postLogin && opts.postLogin) {
|
|
3683
|
+
if (await opts.postLogin.shouldRedirect({
|
|
3684
|
+
headers: ctx.request.headers,
|
|
3685
|
+
user: session.user,
|
|
3686
|
+
session: session.session,
|
|
3687
|
+
scopes: requestedScopes
|
|
3688
|
+
})) return redirectWithPromptCode(ctx, opts, "post_login");
|
|
3689
|
+
}
|
|
3690
|
+
if (promptSet?.has("consent")) return redirectWithPromptCode(ctx, opts, "consent");
|
|
3691
|
+
const referenceId = await opts.postLogin?.consentReferenceId?.({
|
|
3692
|
+
user: session.user,
|
|
3693
|
+
session: session.session,
|
|
3694
|
+
scopes: requestedScopes
|
|
3695
|
+
});
|
|
3696
|
+
if (client.skipConsent) return redirectWithAuthorizationCode(ctx, opts, {
|
|
3697
|
+
query,
|
|
3698
|
+
clientId: client.clientId,
|
|
3699
|
+
userId: session.user.id,
|
|
3700
|
+
sessionId: session.session.id,
|
|
3701
|
+
authTime: new Date(session.session.createdAt).getTime(),
|
|
3702
|
+
referenceId
|
|
3703
|
+
});
|
|
3704
|
+
const consent = await ctx.context.adapter.findOne({
|
|
3705
|
+
model: "oauthConsent",
|
|
3706
|
+
where: [
|
|
3707
|
+
{
|
|
3708
|
+
field: "clientId",
|
|
3709
|
+
value: client.clientId
|
|
3710
|
+
},
|
|
3711
|
+
{
|
|
3712
|
+
field: "userId",
|
|
3713
|
+
value: session.user.id
|
|
3714
|
+
},
|
|
3715
|
+
...referenceId ? [{
|
|
3716
|
+
field: "referenceId",
|
|
3717
|
+
value: referenceId
|
|
3718
|
+
}] : []
|
|
3719
|
+
]
|
|
3720
|
+
});
|
|
3721
|
+
if (!consent || !requestedScopes.every((val) => consent.scopes.includes(val))) return redirectWithPromptCode(ctx, opts, "consent");
|
|
3722
|
+
return redirectWithAuthorizationCode(ctx, opts, {
|
|
3723
|
+
query,
|
|
3724
|
+
clientId: client.clientId,
|
|
3725
|
+
userId: session.user.id,
|
|
3726
|
+
sessionId: session.session.id,
|
|
3727
|
+
authTime: new Date(session.session.createdAt).getTime(),
|
|
3728
|
+
referenceId
|
|
3729
|
+
});
|
|
3730
|
+
}
|
|
3731
|
+
async function redirectWithAuthorizationCode(ctx, opts, verificationValue) {
|
|
3732
|
+
const code = generateRandomString(32, "a-z", "A-Z", "0-9");
|
|
3733
|
+
const iat = Math.floor(Date.now() / 1e3);
|
|
3734
|
+
const exp = iat + (opts.codeExpiresIn ?? 600);
|
|
3735
|
+
const data = {
|
|
3736
|
+
identifier: await storeToken(opts.storeTokens, code, "authorization_code"),
|
|
3737
|
+
updatedAt: /* @__PURE__ */ new Date(iat * 1e3),
|
|
3738
|
+
expiresAt: /* @__PURE__ */ new Date(exp * 1e3),
|
|
3739
|
+
value: JSON.stringify({
|
|
3740
|
+
type: "authorization_code",
|
|
3741
|
+
query: ctx.query,
|
|
3742
|
+
userId: verificationValue.userId,
|
|
3743
|
+
sessionId: verificationValue?.sessionId,
|
|
3744
|
+
referenceId: verificationValue.referenceId,
|
|
3745
|
+
authTime: verificationValue.authTime
|
|
3746
|
+
})
|
|
3747
|
+
};
|
|
3748
|
+
ctx.context.verification_id ? await ctx.context.internalAdapter.updateVerificationValue(ctx.context.verification_id, data) : await ctx.context.internalAdapter.createVerificationValue({
|
|
3749
|
+
...data,
|
|
3750
|
+
createdAt: /* @__PURE__ */ new Date(iat * 1e3)
|
|
3751
|
+
});
|
|
3752
|
+
const redirectUriWithCode = new URL(verificationValue.query.redirect_uri);
|
|
3753
|
+
redirectUriWithCode.searchParams.set("code", code);
|
|
3754
|
+
if (verificationValue.query.state) redirectUriWithCode.searchParams.set("state", verificationValue.query.state);
|
|
3755
|
+
redirectUriWithCode.searchParams.set("iss", getIssuer(ctx, opts));
|
|
3756
|
+
return handleRedirect(ctx, redirectUriWithCode.toString());
|
|
3757
|
+
}
|
|
3758
|
+
async function redirectWithPromptCode(ctx, opts, type, page) {
|
|
3759
|
+
const queryParams = await signParams(ctx, opts);
|
|
3760
|
+
let path = opts.loginPage;
|
|
3761
|
+
if (type === "select_account") path = opts.selectAccount?.page ?? opts.loginPage;
|
|
3762
|
+
else if (type === "post_login") {
|
|
3763
|
+
if (!opts.postLogin?.page) throw new APIError$1("INTERNAL_SERVER_ERROR", { error_description: "postLogin should have been defined" });
|
|
3764
|
+
path = opts.postLogin?.page;
|
|
3765
|
+
} else if (type === "consent") path = opts.consentPage;
|
|
3766
|
+
else if (type === "create") path = opts.signup?.page ?? opts.loginPage;
|
|
3767
|
+
return handleRedirect(ctx, `${page ?? path}?${queryParams}`);
|
|
3768
|
+
}
|
|
3769
|
+
async function signParams(ctx, opts) {
|
|
3770
|
+
const exp = Math.floor(Date.now() / 1e3) + (opts.codeExpiresIn ?? 600);
|
|
3771
|
+
const params = new URLSearchParams(ctx.query);
|
|
3772
|
+
params.set("exp", String(exp));
|
|
3773
|
+
const signature = await makeSignature(params.toString(), ctx.context.secret);
|
|
3774
|
+
params.append("sig", signature);
|
|
3775
|
+
return params.toString();
|
|
3776
|
+
}
|
|
3777
|
+
|
|
3778
|
+
//#endregion
|
|
3779
|
+
//#region src/metadata.ts
|
|
3780
|
+
function authServerMetadata(ctx, opts, overrides) {
|
|
3781
|
+
const baseURL = ctx.context.baseURL;
|
|
3782
|
+
return {
|
|
3783
|
+
scopes_supported: overrides?.scopes_supported,
|
|
3784
|
+
issuer: validateIssuerUrl(opts?.jwt?.issuer ?? baseURL),
|
|
3785
|
+
authorization_endpoint: `${baseURL}/oauth2/authorize`,
|
|
3786
|
+
token_endpoint: `${baseURL}/oauth2/token`,
|
|
3787
|
+
jwks_uri: overrides?.jwt_disabled ? void 0 : opts?.jwks?.remoteUrl ?? `${baseURL}${opts?.jwks?.jwksPath ?? "/jwks"}`,
|
|
3788
|
+
registration_endpoint: `${baseURL}/oauth2/register`,
|
|
3789
|
+
introspection_endpoint: `${baseURL}/oauth2/introspect`,
|
|
3790
|
+
revocation_endpoint: `${baseURL}/oauth2/revoke`,
|
|
3791
|
+
response_types_supported: overrides?.grant_types_supported && !overrides.grant_types_supported.includes("authorization_code") ? [] : ["code"],
|
|
3792
|
+
response_modes_supported: ["query"],
|
|
3793
|
+
grant_types_supported: overrides?.grant_types_supported ?? [
|
|
3794
|
+
"authorization_code",
|
|
3795
|
+
"client_credentials",
|
|
3796
|
+
"refresh_token"
|
|
3797
|
+
],
|
|
3798
|
+
token_endpoint_auth_methods_supported: [
|
|
3799
|
+
...overrides?.public_client_supported ? ["none"] : [],
|
|
3800
|
+
"client_secret_basic",
|
|
3801
|
+
"client_secret_post"
|
|
3802
|
+
],
|
|
3803
|
+
introspection_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post"],
|
|
3804
|
+
revocation_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post"],
|
|
3805
|
+
code_challenge_methods_supported: ["S256"],
|
|
3806
|
+
authorization_response_iss_parameter_supported: true
|
|
3807
|
+
};
|
|
3808
|
+
}
|
|
3809
|
+
function oidcServerMetadata(ctx, opts) {
|
|
3810
|
+
const baseURL = ctx.context.baseURL;
|
|
3811
|
+
const jwtPluginOptions = opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context).options;
|
|
3812
|
+
return {
|
|
3813
|
+
...authServerMetadata(ctx, jwtPluginOptions, {
|
|
3814
|
+
scopes_supported: opts.advertisedMetadata?.scopes_supported ?? opts.scopes,
|
|
3815
|
+
public_client_supported: opts.allowUnauthenticatedClientRegistration,
|
|
3816
|
+
grant_types_supported: opts.grantTypes,
|
|
3817
|
+
jwt_disabled: opts.disableJwtPlugin
|
|
3818
|
+
}),
|
|
3819
|
+
claims_supported: opts?.advertisedMetadata?.claims_supported ?? opts?.claims ?? [],
|
|
3820
|
+
userinfo_endpoint: `${baseURL}/oauth2/userinfo`,
|
|
3821
|
+
subject_types_supported: ["public"],
|
|
3822
|
+
id_token_signing_alg_values_supported: jwtPluginOptions?.jwks?.keyPairConfig?.alg ? [jwtPluginOptions?.jwks?.keyPairConfig?.alg] : opts.disableJwtPlugin ? ["HS256"] : ["EdDSA"],
|
|
3823
|
+
end_session_endpoint: `${baseURL}/oauth2/end-session`,
|
|
3824
|
+
acr_values_supported: ["urn:mace:incommon:iap:bronze"],
|
|
3825
|
+
prompt_values_supported: [
|
|
3826
|
+
"login",
|
|
3827
|
+
"consent",
|
|
3828
|
+
"create",
|
|
3829
|
+
"select_account"
|
|
3830
|
+
]
|
|
3831
|
+
};
|
|
3832
|
+
}
|
|
3833
|
+
/**
|
|
3834
|
+
* Provides an exportable `/.well-known/oauth-authorization-server`.
|
|
3835
|
+
*
|
|
3836
|
+
* Useful when basePath prevents the endpoint from being located at the root
|
|
3837
|
+
* and must be provided manually.
|
|
3838
|
+
*
|
|
3839
|
+
* @external
|
|
3840
|
+
*/
|
|
3841
|
+
const oauthProviderAuthServerMetadata = (auth, opts) => {
|
|
3842
|
+
return async (_request) => {
|
|
3843
|
+
const res = await auth.api.getOAuthServerConfig();
|
|
3844
|
+
return new Response(JSON.stringify(res), {
|
|
3845
|
+
status: 200,
|
|
3846
|
+
headers: {
|
|
3847
|
+
"Cache-Control": "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400",
|
|
3848
|
+
...opts?.headers,
|
|
3849
|
+
"Content-Type": "application/json"
|
|
3850
|
+
}
|
|
3851
|
+
});
|
|
3852
|
+
};
|
|
3853
|
+
};
|
|
3854
|
+
/**
|
|
3855
|
+
* Provides an exportable `/.well-known/openid-configuration`.
|
|
3856
|
+
*
|
|
3857
|
+
* Useful when basePath prevents the endpoint from being located at the root
|
|
3858
|
+
* and must be provided manually.
|
|
3859
|
+
*
|
|
3860
|
+
* @external
|
|
3861
|
+
*/
|
|
3862
|
+
const oauthProviderOpenIdConfigMetadata = (auth, opts) => {
|
|
3863
|
+
return async (_request) => {
|
|
3864
|
+
const res = await auth.api.getOpenIdConfig();
|
|
3865
|
+
return new Response(JSON.stringify(res), {
|
|
3866
|
+
status: 200,
|
|
3867
|
+
headers: {
|
|
3868
|
+
"Cache-Control": "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400",
|
|
3869
|
+
...opts?.headers,
|
|
3870
|
+
"Content-Type": "application/json"
|
|
3871
|
+
}
|
|
3872
|
+
});
|
|
3719
3873
|
};
|
|
3720
3874
|
};
|
|
3721
3875
|
|
|
3722
3876
|
//#endregion
|
|
3723
|
-
export { authServerMetadata, mcpHandler, oauthProvider, oauthProviderAuthServerMetadata, oauthProviderOpenIdConfigMetadata, oidcServerMetadata };
|
|
3877
|
+
export { authServerMetadata, getOAuthProviderState, mcpHandler, oauthProvider, oauthProviderAuthServerMetadata, oauthProviderOpenIdConfigMetadata, oidcServerMetadata };
|
|
3878
|
+
//# sourceMappingURL=index.mjs.map
|