@better-auth/oauth-provider 1.5.0-beta.13 → 1.5.0-beta.16
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/dist/client-resource.d.mts +1 -1
- package/dist/client-resource.mjs +2 -2
- package/dist/client.d.mts +2 -2
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +459 -406
- package/dist/index.mjs.map +1 -1
- package/dist/{oauth-DGg-M8uO.d.mts → oauth-CAVzHW54.d.mts} +23 -1
- package/dist/{oauth-BHiPnA5P.d.mts → oauth-DJ7bwizI.d.mts} +20 -7
- package/dist/{utils-DWE-cPWY.mjs → utils-DqGkkPq2.mjs} +35 -3
- package/dist/utils-DqGkkPq2.mjs.map +1 -0
- package/package.json +7 -7
- package/dist/utils-DWE-cPWY.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,158 +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-DqGkkPq2.mjs";
|
|
2
2
|
import { APIError, createAuthEndpoint, createAuthMiddleware, getOAuthState, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
|
|
3
3
|
import { generateCodeChallenge, getJwks, verifyJwsAccessToken } from "better-auth/oauth2";
|
|
4
4
|
import { APIError as APIError$1 } from "better-call";
|
|
5
5
|
import { constantTimeEqual, generateRandomString, makeSignature } from "better-auth/crypto";
|
|
6
|
-
import { BetterAuthError } from "@better-auth/core/error";
|
|
7
6
|
import { defineRequestState } from "@better-auth/core/context";
|
|
8
7
|
import { logger } from "@better-auth/core/env";
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const searchParams = new URLSearchParams({
|
|
21
|
-
error,
|
|
22
|
-
error_description: description
|
|
23
|
-
});
|
|
24
|
-
state && searchParams.append("state", state);
|
|
25
|
-
iss && searchParams.append("iss", iss);
|
|
26
|
-
return `${url}${url.includes("?") ? "&" : "?"}${searchParams.toString()}`;
|
|
27
|
-
}
|
|
28
|
-
const handleRedirect = (ctx, uri) => {
|
|
29
|
-
if (ctx.headers?.get("accept")?.includes("application/json")) return {
|
|
30
|
-
redirect: true,
|
|
31
|
-
url: uri.toString()
|
|
32
|
-
};
|
|
33
|
-
else throw ctx.redirect(uri);
|
|
34
|
-
};
|
|
35
|
-
/**
|
|
36
|
-
* Validates that the issuer URL
|
|
37
|
-
* - MUST use HTTPS scheme (HTTP allowed for localhost in dev)
|
|
38
|
-
* - MUST NOT contain query components
|
|
39
|
-
* - MUST NOT contain fragment components
|
|
40
|
-
*
|
|
41
|
-
* @returns The validated issuer URL, or a sanitized version if invalid
|
|
42
|
-
*/
|
|
43
|
-
function validateIssuerUrl(issuer) {
|
|
44
|
-
try {
|
|
45
|
-
const url = new URL(issuer);
|
|
46
|
-
const isLocalhost = url.hostname === "localhost" || url.hostname === "127.0.0.1";
|
|
47
|
-
if (url.protocol !== "https:" && !isLocalhost) url.protocol = "https:";
|
|
48
|
-
url.search = "";
|
|
49
|
-
url.hash = "";
|
|
50
|
-
return url.toString().replace(/\/$/, "");
|
|
51
|
-
} catch {
|
|
52
|
-
return issuer;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Gets the issuer identifier
|
|
57
|
-
*/
|
|
58
|
-
function getIssuer(ctx, opts) {
|
|
59
|
-
let issuer;
|
|
60
|
-
if (opts.disableJwtPlugin) issuer = ctx.context.baseURL;
|
|
61
|
-
else try {
|
|
62
|
-
issuer = getJwtPlugin(ctx.context).options?.jwt?.issuer ?? ctx.context.baseURL;
|
|
63
|
-
} catch {
|
|
64
|
-
issuer = ctx.context.baseURL;
|
|
65
|
-
}
|
|
66
|
-
return validateIssuerUrl(issuer);
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Error page url if redirect_uri has not been verified yet
|
|
70
|
-
* Generates Url for custom error page
|
|
71
|
-
*/
|
|
72
|
-
function getErrorURL(ctx, error, description) {
|
|
73
|
-
return formatErrorURL(ctx.context.options.onAPIError?.errorURL || `${ctx.context.baseURL}/error`, error, description);
|
|
74
|
-
}
|
|
75
|
-
async function authorizeEndpoint(ctx, opts, settings) {
|
|
76
|
-
if (opts.grantTypes && !opts.grantTypes.includes("authorization_code")) throw new APIError$1("NOT_FOUND");
|
|
77
|
-
if (!ctx.request) throw new APIError$1("UNAUTHORIZED", {
|
|
78
|
-
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",
|
|
79
20
|
error: "invalid_request"
|
|
80
21
|
});
|
|
81
|
-
const query =
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (client.disabled) throw ctx.redirect(getErrorURL(ctx, "client_disabled", "client is disabled"));
|
|
90
|
-
if (!client.redirectUris?.find((url) => url === query.redirect_uri) || !query.redirect_uri) throw ctx.redirect(getErrorURL(ctx, "invalid_redirect", "invalid redirect uri"));
|
|
91
|
-
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(" ");
|
|
92
30
|
if (requestedScopes) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
31
|
+
if (!requestedScopes.every((sc) => originalRequestedScopes?.includes(sc))) throw new APIError("BAD_REQUEST", {
|
|
32
|
+
error_description: "Scope not originally requested",
|
|
33
|
+
error: "invalid_request"
|
|
96
34
|
});
|
|
97
|
-
if (invalidScopes.length) throw ctx.redirect(formatErrorURL(query.redirect_uri, "invalid_scope", `The following scopes are invalid: ${invalidScopes.join(", ")}`, query.state, getIssuer(ctx, opts)));
|
|
98
35
|
}
|
|
99
|
-
if (!
|
|
100
|
-
|
|
101
|
-
query.
|
|
102
|
-
}
|
|
103
|
-
if (!query.code_challenge || !query.code_challenge_method) throw ctx.redirect(formatErrorURL(query.redirect_uri, "invalid_request", "pkce is required", query.state, getIssuer(ctx, opts)));
|
|
104
|
-
if (!["S256"].includes(query.code_challenge_method)) throw ctx.redirect(formatErrorURL(query.redirect_uri, "invalid_request", "invalid code_challenge method", query.state, getIssuer(ctx, opts)));
|
|
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
|
+
};
|
|
105
40
|
const session = await getSessionFromCtx(ctx);
|
|
106
|
-
if (!session || promptSet?.has("login") || promptSet?.has("create")) return redirectWithPromptCode(ctx, opts, promptSet?.has("create") ? "create" : "login");
|
|
107
|
-
if (settings?.isAuthorize && promptSet?.has("select_account")) return redirectWithPromptCode(ctx, opts, "select_account");
|
|
108
|
-
if (settings?.isAuthorize && opts.selectAccount) {
|
|
109
|
-
if (await opts.selectAccount.shouldRedirect({
|
|
110
|
-
headers: ctx.request.headers,
|
|
111
|
-
user: session.user,
|
|
112
|
-
session: session.session,
|
|
113
|
-
scopes: requestedScopes
|
|
114
|
-
})) return redirectWithPromptCode(ctx, opts, "select_account");
|
|
115
|
-
}
|
|
116
|
-
if (opts.signup?.shouldRedirect) {
|
|
117
|
-
const signupRedirect = await opts.signup.shouldRedirect({
|
|
118
|
-
headers: ctx.request.headers,
|
|
119
|
-
user: session.user,
|
|
120
|
-
session: session.session,
|
|
121
|
-
scopes: requestedScopes
|
|
122
|
-
});
|
|
123
|
-
if (signupRedirect) return redirectWithPromptCode(ctx, opts, "create", typeof signupRedirect === "string" ? signupRedirect : void 0);
|
|
124
|
-
}
|
|
125
|
-
if (!settings?.postLogin && opts.postLogin) {
|
|
126
|
-
if (await opts.postLogin.shouldRedirect({
|
|
127
|
-
headers: ctx.request.headers,
|
|
128
|
-
user: session.user,
|
|
129
|
-
session: session.session,
|
|
130
|
-
scopes: requestedScopes
|
|
131
|
-
})) return redirectWithPromptCode(ctx, opts, "post_login");
|
|
132
|
-
}
|
|
133
|
-
if (promptSet?.has("consent")) return redirectWithPromptCode(ctx, opts, "consent");
|
|
134
41
|
const referenceId = await opts.postLogin?.consentReferenceId?.({
|
|
135
|
-
user: session
|
|
136
|
-
session: session
|
|
137
|
-
scopes: requestedScopes
|
|
138
|
-
});
|
|
139
|
-
if (client.skipConsent) return redirectWithAuthorizationCode(ctx, opts, {
|
|
140
|
-
query,
|
|
141
|
-
clientId: client.clientId,
|
|
142
|
-
userId: session.user.id,
|
|
143
|
-
sessionId: session.session.id,
|
|
144
|
-
referenceId
|
|
42
|
+
user: session?.user,
|
|
43
|
+
session: session?.session,
|
|
44
|
+
scopes: requestedScopes ?? originalRequestedScopes
|
|
145
45
|
});
|
|
146
|
-
const
|
|
46
|
+
const foundConsent = await ctx.context.adapter.findOne({
|
|
147
47
|
model: "oauthConsent",
|
|
148
48
|
where: [
|
|
149
49
|
{
|
|
150
50
|
field: "clientId",
|
|
151
|
-
value:
|
|
51
|
+
value: clientId
|
|
152
52
|
},
|
|
153
53
|
{
|
|
154
54
|
field: "userId",
|
|
155
|
-
value: session
|
|
55
|
+
value: session?.user.id
|
|
156
56
|
},
|
|
157
57
|
...referenceId ? [{
|
|
158
58
|
field: "referenceId",
|
|
@@ -160,267 +60,66 @@ async function authorizeEndpoint(ctx, opts, settings) {
|
|
|
160
60
|
}] : []
|
|
161
61
|
]
|
|
162
62
|
});
|
|
163
|
-
if (!consent || !requestedScopes.every((val) => consent.scopes.includes(val))) return redirectWithPromptCode(ctx, opts, "consent");
|
|
164
|
-
return redirectWithAuthorizationCode(ctx, opts, {
|
|
165
|
-
query,
|
|
166
|
-
clientId: client.clientId,
|
|
167
|
-
userId: session.user.id,
|
|
168
|
-
sessionId: session.session.id,
|
|
169
|
-
referenceId
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
async function redirectWithAuthorizationCode(ctx, opts, verificationValue) {
|
|
173
|
-
const code = generateRandomString(32, "a-z", "A-Z", "0-9");
|
|
174
63
|
const iat = Math.floor(Date.now() / 1e3);
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
64
|
+
const consent = {
|
|
65
|
+
clientId,
|
|
66
|
+
userId: session?.user.id,
|
|
67
|
+
scopes: requestedScopes ?? originalRequestedScopes,
|
|
68
|
+
createdAt: /* @__PURE__ */ new Date(iat * 1e3),
|
|
178
69
|
updatedAt: /* @__PURE__ */ new Date(iat * 1e3),
|
|
179
|
-
|
|
180
|
-
value: JSON.stringify({
|
|
181
|
-
type: "authorization_code",
|
|
182
|
-
query: ctx.query,
|
|
183
|
-
userId: verificationValue.userId,
|
|
184
|
-
sessionId: verificationValue?.sessionId,
|
|
185
|
-
referenceId: verificationValue.referenceId
|
|
186
|
-
})
|
|
70
|
+
referenceId
|
|
187
71
|
};
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
+
}
|
|
191
88
|
});
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (type === "select_account") path = opts.selectAccount?.page ?? opts.loginPage;
|
|
202
|
-
else if (type === "post_login") {
|
|
203
|
-
if (!opts.postLogin?.page) throw new APIError$1("INTERNAL_SERVER_ERROR", { error_description: "postLogin should have been defined" });
|
|
204
|
-
path = opts.postLogin?.page;
|
|
205
|
-
} else if (type === "consent") path = opts.consentPage;
|
|
206
|
-
else if (type === "create") path = opts.signup?.page ?? opts.loginPage;
|
|
207
|
-
return handleRedirect(ctx, `${page ?? path}?${queryParams}`);
|
|
208
|
-
}
|
|
209
|
-
async function signParams(ctx, opts) {
|
|
210
|
-
const exp = Math.floor(Date.now() / 1e3) + (opts.codeExpiresIn ?? 600);
|
|
211
|
-
const params = new URLSearchParams(ctx.query);
|
|
212
|
-
params.set("exp", String(exp));
|
|
213
|
-
const signature = await makeSignature(params.toString(), ctx.context.secret);
|
|
214
|
-
params.append("sig", signature);
|
|
215
|
-
return params.toString();
|
|
89
|
+
if (requestedScopes) query.set("scope", consent.scopes.join(" "));
|
|
90
|
+
ctx?.headers?.set("accept", "application/json");
|
|
91
|
+
ctx.query = deleteFromPrompt(query, "consent");
|
|
92
|
+
ctx.context.postLogin = true;
|
|
93
|
+
const { url } = await authorizeEndpoint(ctx, opts);
|
|
94
|
+
return {
|
|
95
|
+
redirect: true,
|
|
96
|
+
url
|
|
97
|
+
};
|
|
216
98
|
}
|
|
217
99
|
|
|
218
100
|
//#endregion
|
|
219
|
-
//#region src/
|
|
220
|
-
function
|
|
221
|
-
|
|
222
|
-
return
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
token_endpoint_auth_methods_supported: [
|
|
239
|
-
...overrides?.public_client_supported ? ["none"] : [],
|
|
240
|
-
"client_secret_basic",
|
|
241
|
-
"client_secret_post"
|
|
242
|
-
],
|
|
243
|
-
introspection_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post"],
|
|
244
|
-
revocation_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post"],
|
|
245
|
-
code_challenge_methods_supported: ["S256"],
|
|
246
|
-
authorization_response_iss_parameter_supported: true
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
function oidcServerMetadata(ctx, opts) {
|
|
250
|
-
const baseURL = ctx.context.baseURL;
|
|
251
|
-
const jwtPluginOptions = opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context).options;
|
|
252
|
-
return {
|
|
253
|
-
...authServerMetadata(ctx, jwtPluginOptions, {
|
|
254
|
-
scopes_supported: opts.advertisedMetadata?.scopes_supported ?? opts.scopes,
|
|
255
|
-
public_client_supported: opts.allowUnauthenticatedClientRegistration,
|
|
256
|
-
grant_types_supported: opts.grantTypes,
|
|
257
|
-
jwt_disabled: opts.disableJwtPlugin
|
|
258
|
-
}),
|
|
259
|
-
claims_supported: opts?.advertisedMetadata?.claims_supported ?? opts?.claims ?? [],
|
|
260
|
-
userinfo_endpoint: `${baseURL}/oauth2/userinfo`,
|
|
261
|
-
subject_types_supported: ["public"],
|
|
262
|
-
id_token_signing_alg_values_supported: jwtPluginOptions?.jwks?.keyPairConfig?.alg ? [jwtPluginOptions?.jwks?.keyPairConfig?.alg] : opts.disableJwtPlugin ? ["HS256"] : ["EdDSA"],
|
|
263
|
-
end_session_endpoint: `${baseURL}/oauth2/end-session`,
|
|
264
|
-
acr_values_supported: ["urn:mace:incommon:iap:bronze"],
|
|
265
|
-
prompt_values_supported: [
|
|
266
|
-
"login",
|
|
267
|
-
"consent",
|
|
268
|
-
"create",
|
|
269
|
-
"select_account"
|
|
270
|
-
]
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
/**
|
|
274
|
-
* Provides an exportable `/.well-known/oauth-authorization-server`.
|
|
275
|
-
*
|
|
276
|
-
* Useful when basePath prevents the endpoint from being located at the root
|
|
277
|
-
* and must be provided manually.
|
|
278
|
-
*
|
|
279
|
-
* @external
|
|
280
|
-
*/
|
|
281
|
-
const oauthProviderAuthServerMetadata = (auth, opts) => {
|
|
282
|
-
return async (_request) => {
|
|
283
|
-
const res = await auth.api.getOAuthServerConfig();
|
|
284
|
-
return new Response(JSON.stringify(res), {
|
|
285
|
-
status: 200,
|
|
286
|
-
headers: {
|
|
287
|
-
"Cache-Control": "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400",
|
|
288
|
-
...opts?.headers,
|
|
289
|
-
"Content-Type": "application/json"
|
|
290
|
-
}
|
|
291
|
-
});
|
|
292
|
-
};
|
|
293
|
-
};
|
|
294
|
-
/**
|
|
295
|
-
* Provides an exportable `/.well-known/openid-configuration`.
|
|
296
|
-
*
|
|
297
|
-
* Useful when basePath prevents the endpoint from being located at the root
|
|
298
|
-
* and must be provided manually.
|
|
299
|
-
*
|
|
300
|
-
* @external
|
|
301
|
-
*/
|
|
302
|
-
const oauthProviderOpenIdConfigMetadata = (auth, opts) => {
|
|
303
|
-
return async (_request) => {
|
|
304
|
-
const res = await auth.api.getOpenIdConfig();
|
|
305
|
-
return new Response(JSON.stringify(res), {
|
|
306
|
-
status: 200,
|
|
307
|
-
headers: {
|
|
308
|
-
"Cache-Control": "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400",
|
|
309
|
-
...opts?.headers,
|
|
310
|
-
"Content-Type": "application/json"
|
|
311
|
-
}
|
|
312
|
-
});
|
|
313
|
-
};
|
|
314
|
-
};
|
|
315
|
-
|
|
316
|
-
//#endregion
|
|
317
|
-
//#region src/consent.ts
|
|
318
|
-
async function consentEndpoint(ctx, opts) {
|
|
319
|
-
const _query = (await oAuthState.get())?.query;
|
|
320
|
-
if (!_query) throw new APIError("BAD_REQUEST", {
|
|
321
|
-
error_description: "missing oauth query",
|
|
322
|
-
error: "invalid_request"
|
|
323
|
-
});
|
|
324
|
-
const query = new URLSearchParams(_query);
|
|
325
|
-
const originalRequestedScopes = query.get("scope")?.split(" ") ?? [];
|
|
326
|
-
const clientId = query.get("client_id");
|
|
327
|
-
if (!clientId) throw new APIError("BAD_REQUEST", {
|
|
328
|
-
error_description: "client_id is required",
|
|
329
|
-
error: "invalid_client"
|
|
330
|
-
});
|
|
331
|
-
const requestedScopes = ctx.body.scope?.split(" ");
|
|
332
|
-
if (requestedScopes) {
|
|
333
|
-
if (!requestedScopes.every((sc) => originalRequestedScopes?.includes(sc))) throw new APIError("BAD_REQUEST", {
|
|
334
|
-
error_description: "Scope not originally requested",
|
|
335
|
-
error: "invalid_request"
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
|
-
if (!(ctx.body.accept === true)) return {
|
|
339
|
-
redirect: true,
|
|
340
|
-
uri: formatErrorURL(query.get("redirect_uri") ?? "", "access_denied", "User denied access", query.get("state") ?? void 0, getIssuer(ctx, opts))
|
|
341
|
-
};
|
|
342
|
-
const session = await getSessionFromCtx(ctx);
|
|
343
|
-
const referenceId = await opts.postLogin?.consentReferenceId?.({
|
|
344
|
-
user: session?.user,
|
|
345
|
-
session: session?.session,
|
|
346
|
-
scopes: requestedScopes ?? originalRequestedScopes
|
|
347
|
-
});
|
|
348
|
-
const foundConsent = await ctx.context.adapter.findOne({
|
|
349
|
-
model: "oauthConsent",
|
|
350
|
-
where: [
|
|
351
|
-
{
|
|
352
|
-
field: "clientId",
|
|
353
|
-
value: clientId
|
|
354
|
-
},
|
|
355
|
-
{
|
|
356
|
-
field: "userId",
|
|
357
|
-
value: session?.user.id
|
|
358
|
-
},
|
|
359
|
-
...referenceId ? [{
|
|
360
|
-
field: "referenceId",
|
|
361
|
-
value: referenceId
|
|
362
|
-
}] : []
|
|
363
|
-
]
|
|
364
|
-
});
|
|
365
|
-
const iat = Math.floor(Date.now() / 1e3);
|
|
366
|
-
const consent = {
|
|
367
|
-
clientId,
|
|
368
|
-
userId: session?.user.id,
|
|
369
|
-
scopes: requestedScopes ?? originalRequestedScopes,
|
|
370
|
-
createdAt: /* @__PURE__ */ new Date(iat * 1e3),
|
|
371
|
-
updatedAt: /* @__PURE__ */ new Date(iat * 1e3),
|
|
372
|
-
referenceId
|
|
373
|
-
};
|
|
374
|
-
foundConsent?.id ? await ctx.context.adapter.update({
|
|
375
|
-
model: "oauthConsent",
|
|
376
|
-
where: [{
|
|
377
|
-
field: "id",
|
|
378
|
-
value: foundConsent.id
|
|
379
|
-
}],
|
|
380
|
-
update: {
|
|
381
|
-
scopes: consent.scopes,
|
|
382
|
-
updatedAt: /* @__PURE__ */ new Date(iat * 1e3)
|
|
383
|
-
}
|
|
384
|
-
}) : await ctx.context.adapter.create({
|
|
385
|
-
model: "oauthConsent",
|
|
386
|
-
data: {
|
|
387
|
-
...consent,
|
|
388
|
-
scopes: consent.scopes
|
|
389
|
-
}
|
|
390
|
-
});
|
|
391
|
-
ctx?.headers?.set("accept", "application/json");
|
|
392
|
-
ctx.query = deleteFromPrompt(query, "consent");
|
|
393
|
-
ctx.context.postLogin = true;
|
|
394
|
-
const { url } = await authorizeEndpoint(ctx, opts);
|
|
395
|
-
return {
|
|
396
|
-
redirect: true,
|
|
397
|
-
uri: url
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
//#endregion
|
|
402
|
-
//#region src/continue.ts
|
|
403
|
-
async function continueEndpoint(ctx, opts) {
|
|
404
|
-
if (ctx.body.selected === true) return await selected(ctx, opts);
|
|
405
|
-
else if (ctx.body.created === true) return await created(ctx, opts);
|
|
406
|
-
else if (ctx.body.postLogin === true) return await postLogin(ctx, opts);
|
|
407
|
-
else throw new APIError("BAD_REQUEST", {
|
|
408
|
-
error_description: "Missing parameters",
|
|
409
|
-
error: "invalid_request"
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
async function selected(ctx, opts) {
|
|
413
|
-
const _query = (await oAuthState.get())?.query;
|
|
414
|
-
if (!_query) throw new APIError("BAD_REQUEST", {
|
|
415
|
-
error_description: "missing oauth query",
|
|
416
|
-
error: "invalid_request"
|
|
417
|
-
});
|
|
418
|
-
ctx.headers?.set("accept", "application/json");
|
|
419
|
-
ctx.query = deleteFromPrompt(new URLSearchParams(_query), "select_account");
|
|
420
|
-
const { url } = await authorizeEndpoint(ctx, opts);
|
|
101
|
+
//#region src/continue.ts
|
|
102
|
+
async function continueEndpoint(ctx, opts) {
|
|
103
|
+
if (ctx.body.selected === true) return await selected(ctx, opts);
|
|
104
|
+
else if (ctx.body.created === true) return await created(ctx, opts);
|
|
105
|
+
else if (ctx.body.postLogin === true) return await postLogin(ctx, opts);
|
|
106
|
+
else throw new APIError("BAD_REQUEST", {
|
|
107
|
+
error_description: "Missing parameters",
|
|
108
|
+
error: "invalid_request"
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
async function selected(ctx, opts) {
|
|
112
|
+
const _query = (await oAuthState.get())?.query;
|
|
113
|
+
if (!_query) throw new APIError("BAD_REQUEST", {
|
|
114
|
+
error_description: "missing oauth query",
|
|
115
|
+
error: "invalid_request"
|
|
116
|
+
});
|
|
117
|
+
ctx.headers?.set("accept", "application/json");
|
|
118
|
+
ctx.query = deleteFromPrompt(new URLSearchParams(_query), "select_account");
|
|
119
|
+
const { url } = await authorizeEndpoint(ctx, opts);
|
|
421
120
|
return {
|
|
422
121
|
redirect: true,
|
|
423
|
-
|
|
122
|
+
url
|
|
424
123
|
};
|
|
425
124
|
}
|
|
426
125
|
async function created(ctx, opts) {
|
|
@@ -433,7 +132,7 @@ async function created(ctx, opts) {
|
|
|
433
132
|
const { url } = await authorizeEndpoint(ctx, opts);
|
|
434
133
|
return {
|
|
435
134
|
redirect: true,
|
|
436
|
-
|
|
135
|
+
url
|
|
437
136
|
};
|
|
438
137
|
}
|
|
439
138
|
async function postLogin(ctx, opts) {
|
|
@@ -448,7 +147,7 @@ async function postLogin(ctx, opts) {
|
|
|
448
147
|
const { url } = await authorizeEndpoint(ctx, opts, { postLogin: true });
|
|
449
148
|
return {
|
|
450
149
|
redirect: true,
|
|
451
|
-
|
|
150
|
+
url
|
|
452
151
|
};
|
|
453
152
|
}
|
|
454
153
|
|
|
@@ -796,8 +495,8 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
|
|
|
796
495
|
});
|
|
797
496
|
const isAuthCodeWithSecret = client_id && client_secret;
|
|
798
497
|
const isAuthCodeWithPkce = client_id && code && code_verifier;
|
|
799
|
-
if (!
|
|
800
|
-
error_description: "
|
|
498
|
+
if (!isAuthCodeWithSecret && !isAuthCodeWithPkce) throw new APIError("BAD_REQUEST", {
|
|
499
|
+
error_description: "Either code_verifier or client_secret is required",
|
|
801
500
|
error: "invalid_request"
|
|
802
501
|
});
|
|
803
502
|
/** Get and check Verification Value */
|
|
@@ -809,16 +508,32 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
|
|
|
809
508
|
});
|
|
810
509
|
/** Verify Client */
|
|
811
510
|
const client = await validateClientCredentials(ctx, opts, client_id, client_secret, scopes);
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
})
|
|
818
|
-
|
|
819
|
-
error_description: "code verification failed",
|
|
511
|
+
if (isPKCERequired(client, (verificationValue.query?.scope)?.split(" ") || [])) {
|
|
512
|
+
if (!isAuthCodeWithPkce) throw new APIError("BAD_REQUEST", {
|
|
513
|
+
error_description: "PKCE is required for this client",
|
|
514
|
+
error: "invalid_request"
|
|
515
|
+
});
|
|
516
|
+
} else if (!(isAuthCodeWithPkce || isAuthCodeWithSecret)) throw new APIError("BAD_REQUEST", {
|
|
517
|
+
error_description: "Either PKCE (code_verifier) or client authentication (client_secret) is required",
|
|
820
518
|
error: "invalid_request"
|
|
821
519
|
});
|
|
520
|
+
/** Check PKCE challenge if verifier is provided */
|
|
521
|
+
const pkceUsedInAuth = !!verificationValue.query?.code_challenge;
|
|
522
|
+
const pkceUsedInToken = !!code_verifier;
|
|
523
|
+
if (pkceUsedInAuth || pkceUsedInToken) {
|
|
524
|
+
if (pkceUsedInAuth && !pkceUsedInToken) throw new APIError("UNAUTHORIZED", {
|
|
525
|
+
error_description: "code_verifier required because PKCE was used in authorization",
|
|
526
|
+
error: "invalid_request"
|
|
527
|
+
});
|
|
528
|
+
if (!pkceUsedInAuth && pkceUsedInToken) throw new APIError("UNAUTHORIZED", {
|
|
529
|
+
error_description: "code_verifier provided but PKCE was not used in authorization",
|
|
530
|
+
error: "invalid_request"
|
|
531
|
+
});
|
|
532
|
+
if ((verificationValue.query?.code_challenge_method === "S256" ? await generateCodeChallenge(code_verifier) : void 0) !== verificationValue.query?.code_challenge) throw new APIError("UNAUTHORIZED", {
|
|
533
|
+
error_description: "code verification failed",
|
|
534
|
+
error: "invalid_request"
|
|
535
|
+
});
|
|
536
|
+
}
|
|
822
537
|
/** Get user */
|
|
823
538
|
if (!verificationValue.userId) throw new APIError("BAD_REQUEST", {
|
|
824
539
|
error_description: "missing user, user may have been deleted",
|
|
@@ -1115,8 +830,8 @@ async function validateOpaqueAccessToken(ctx, opts, token, clientId) {
|
|
|
1115
830
|
client_id: accessToken.clientId,
|
|
1116
831
|
sub: user?.id,
|
|
1117
832
|
sid: sessionId,
|
|
1118
|
-
exp: Math.floor(accessToken.expiresAt.getTime() / 1e3),
|
|
1119
|
-
iat: Math.floor(accessToken.createdAt.getTime() / 1e3),
|
|
833
|
+
exp: Math.floor(new Date(accessToken.expiresAt).getTime() / 1e3),
|
|
834
|
+
iat: Math.floor(new Date(accessToken.createdAt).getTime() / 1e3),
|
|
1120
835
|
scope: accessToken.scopes?.join(" ")
|
|
1121
836
|
};
|
|
1122
837
|
}
|
|
@@ -1159,8 +874,8 @@ async function validateRefreshToken(ctx, opts, token, clientId) {
|
|
|
1159
874
|
iss: ((opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context))?.options)?.jwt?.issuer ?? ctx.context.baseURL,
|
|
1160
875
|
sub: user?.id,
|
|
1161
876
|
sid: sessionId,
|
|
1162
|
-
exp: Math.floor(refreshToken.expiresAt.getTime() / 1e3),
|
|
1163
|
-
iat: Math.floor(refreshToken.createdAt.getTime() / 1e3),
|
|
877
|
+
exp: Math.floor(new Date(refreshToken.expiresAt).getTime() / 1e3),
|
|
878
|
+
iat: Math.floor(new Date(refreshToken.createdAt).getTime() / 1e3),
|
|
1164
879
|
scope: refreshToken.scopes?.join(" ")
|
|
1165
880
|
};
|
|
1166
881
|
}
|
|
@@ -1407,6 +1122,10 @@ async function checkOAuthClient(client, opts, settings) {
|
|
|
1407
1122
|
error_description: `cannot request scope ${requestedScope}`
|
|
1408
1123
|
});
|
|
1409
1124
|
}
|
|
1125
|
+
if (settings?.isRegister && client.require_pkce === false) throw new APIError("BAD_REQUEST", {
|
|
1126
|
+
error: "invalid_client_metadata",
|
|
1127
|
+
error_description: `pkce is required for registered clients.`
|
|
1128
|
+
});
|
|
1410
1129
|
}
|
|
1411
1130
|
async function createOAuthClientEndpoint(ctx, opts, settings) {
|
|
1412
1131
|
const body = ctx.body;
|
|
@@ -1436,7 +1155,11 @@ async function createOAuthClientEndpoint(ctx, opts, settings) {
|
|
|
1436
1155
|
});
|
|
1437
1156
|
const client = await ctx.context.adapter.create({
|
|
1438
1157
|
model: "oauthClient",
|
|
1439
|
-
data:
|
|
1158
|
+
data: {
|
|
1159
|
+
...schema,
|
|
1160
|
+
createdAt: /* @__PURE__ */ new Date(iat * 1e3),
|
|
1161
|
+
updatedAt: /* @__PURE__ */ new Date(iat * 1e3)
|
|
1162
|
+
}
|
|
1440
1163
|
});
|
|
1441
1164
|
return ctx.json(schemaToOAuth({
|
|
1442
1165
|
...client,
|
|
@@ -1456,7 +1179,7 @@ async function createOAuthClientEndpoint(ctx, opts, settings) {
|
|
|
1456
1179
|
* @returns
|
|
1457
1180
|
*/
|
|
1458
1181
|
function oauthToSchema(input) {
|
|
1459
|
-
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;
|
|
1182
|
+
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;
|
|
1460
1183
|
const expiresAt = _expiresAt ? /* @__PURE__ */ new Date(_expiresAt * 1e3) : void 0;
|
|
1461
1184
|
const createdAt = _createdAt ? /* @__PURE__ */ new Date(_createdAt * 1e3) : void 0;
|
|
1462
1185
|
const scopes = _scope?.split(" ");
|
|
@@ -1490,6 +1213,7 @@ function oauthToSchema(input) {
|
|
|
1490
1213
|
type,
|
|
1491
1214
|
skipConsent,
|
|
1492
1215
|
enableEndSession,
|
|
1216
|
+
requirePKCE,
|
|
1493
1217
|
referenceId,
|
|
1494
1218
|
metadata: Object.keys(metadataObj).length ? JSON.stringify(metadataObj) : void 0
|
|
1495
1219
|
};
|
|
@@ -1501,9 +1225,9 @@ function oauthToSchema(input) {
|
|
|
1501
1225
|
* @returns
|
|
1502
1226
|
*/
|
|
1503
1227
|
function schemaToOAuth(input) {
|
|
1504
|
-
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;
|
|
1505
|
-
const _expiresAt = expiresAt ? Math.round(expiresAt.getTime() / 1e3) : void 0;
|
|
1506
|
-
const _createdAt = createdAt ? Math.round(createdAt.getTime() / 1e3) : void 0;
|
|
1228
|
+
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;
|
|
1229
|
+
const _expiresAt = expiresAt ? Math.round(new Date(expiresAt).getTime() / 1e3) : void 0;
|
|
1230
|
+
const _createdAt = createdAt ? Math.round(new Date(createdAt).getTime() / 1e3) : void 0;
|
|
1507
1231
|
const _scopes = scopes?.join(" ");
|
|
1508
1232
|
return {
|
|
1509
1233
|
...parseClientMetadata(metadata),
|
|
@@ -1532,6 +1256,7 @@ function schemaToOAuth(input) {
|
|
|
1532
1256
|
disabled: disabled ?? void 0,
|
|
1533
1257
|
skip_consent: skipConsent ?? void 0,
|
|
1534
1258
|
enable_end_session: enableEndSession ?? void 0,
|
|
1259
|
+
require_pkce: requirePKCE ?? void 0,
|
|
1535
1260
|
reference_id: referenceId ?? void 0
|
|
1536
1261
|
};
|
|
1537
1262
|
}
|
|
@@ -1742,7 +1467,10 @@ async function updateClientEndpoint(ctx, opts) {
|
|
|
1742
1467
|
field: "clientId",
|
|
1743
1468
|
value: clientId
|
|
1744
1469
|
}],
|
|
1745
|
-
update:
|
|
1470
|
+
update: {
|
|
1471
|
+
...oauthToSchema(updates),
|
|
1472
|
+
updatedAt: /* @__PURE__ */ new Date(Math.floor(Date.now() / 1e3) * 1e3)
|
|
1473
|
+
}
|
|
1746
1474
|
});
|
|
1747
1475
|
if (!updatedClient) throw new APIError("INTERNAL_SERVER_ERROR", {
|
|
1748
1476
|
error_description: "unable to update client",
|
|
@@ -1790,8 +1518,8 @@ async function rotateClientSecretEndpoint(ctx, opts) {
|
|
|
1790
1518
|
value: clientId
|
|
1791
1519
|
}],
|
|
1792
1520
|
update: {
|
|
1793
|
-
|
|
1794
|
-
|
|
1521
|
+
clientSecret: storedClientSecret,
|
|
1522
|
+
updatedAt: /* @__PURE__ */ new Date(Math.floor(Date.now() / 1e3) * 1e3)
|
|
1795
1523
|
}
|
|
1796
1524
|
});
|
|
1797
1525
|
if (!updatedClient) throw new APIError("INTERNAL_SERVER_ERROR", {
|
|
@@ -1840,6 +1568,7 @@ const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/creat
|
|
|
1840
1568
|
client_secret_expires_at: z.union([z.string(), z.number()]).optional().default(0),
|
|
1841
1569
|
skip_consent: z.boolean().optional(),
|
|
1842
1570
|
enable_end_session: z.boolean().optional(),
|
|
1571
|
+
require_pkce: z.boolean().optional(),
|
|
1843
1572
|
metadata: z.record(z.string(), z.unknown()).optional()
|
|
1844
1573
|
}),
|
|
1845
1574
|
metadata: {
|
|
@@ -1966,6 +1695,11 @@ const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/creat
|
|
|
1966
1695
|
type: "boolean",
|
|
1967
1696
|
description: "Whether the client is disabled"
|
|
1968
1697
|
},
|
|
1698
|
+
require_pkce: {
|
|
1699
|
+
type: "boolean",
|
|
1700
|
+
description: "Whether the client requires PKCE",
|
|
1701
|
+
default: true
|
|
1702
|
+
},
|
|
1969
1703
|
metadata: {
|
|
1970
1704
|
type: "object",
|
|
1971
1705
|
additionalProperties: true,
|
|
@@ -2713,6 +2447,10 @@ const schema = {
|
|
|
2713
2447
|
type: "string",
|
|
2714
2448
|
required: false
|
|
2715
2449
|
},
|
|
2450
|
+
requirePKCE: {
|
|
2451
|
+
type: "boolean",
|
|
2452
|
+
required: false
|
|
2453
|
+
},
|
|
2716
2454
|
referenceId: {
|
|
2717
2455
|
type: "string",
|
|
2718
2456
|
required: false
|
|
@@ -2856,6 +2594,7 @@ const schema = {
|
|
|
2856
2594
|
//#endregion
|
|
2857
2595
|
//#region src/oauth.ts
|
|
2858
2596
|
const oAuthState = defineRequestState(() => null);
|
|
2597
|
+
const getOAuthProviderState = oAuthState.get;
|
|
2859
2598
|
/**
|
|
2860
2599
|
* oAuth 2.1 provider plugin for Better Auth.
|
|
2861
2600
|
*
|
|
@@ -2978,7 +2717,12 @@ const oauthProvider = (options) => {
|
|
|
2978
2717
|
metadata: { SERVER_ONLY: true }
|
|
2979
2718
|
}, async (ctx) => {
|
|
2980
2719
|
if (opts.scopes && opts.scopes.includes("openid")) return oidcServerMetadata(ctx, opts);
|
|
2981
|
-
else return authServerMetadata(ctx, opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context)?.options, {
|
|
2720
|
+
else return authServerMetadata(ctx, opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context)?.options, {
|
|
2721
|
+
scopes_supported: opts.advertisedMetadata?.scopes_supported ?? opts.scopes,
|
|
2722
|
+
public_client_supported: opts.allowUnauthenticatedClientRegistration,
|
|
2723
|
+
grant_types_supported: opts.grantTypes,
|
|
2724
|
+
jwt_disabled: opts.disableJwtPlugin
|
|
2725
|
+
});
|
|
2982
2726
|
}),
|
|
2983
2727
|
getOpenIdConfig: createAuthEndpoint("/.well-known/openid-configuration", {
|
|
2984
2728
|
method: "GET",
|
|
@@ -3811,5 +3555,314 @@ const oauthProvider = (options) => {
|
|
|
3811
3555
|
};
|
|
3812
3556
|
|
|
3813
3557
|
//#endregion
|
|
3814
|
-
|
|
3558
|
+
//#region src/authorize.ts
|
|
3559
|
+
/**
|
|
3560
|
+
* Formats an error url
|
|
3561
|
+
*/
|
|
3562
|
+
function formatErrorURL(url, error, description, state, iss) {
|
|
3563
|
+
const searchParams = new URLSearchParams({
|
|
3564
|
+
error,
|
|
3565
|
+
error_description: description
|
|
3566
|
+
});
|
|
3567
|
+
state && searchParams.append("state", state);
|
|
3568
|
+
iss && searchParams.append("iss", iss);
|
|
3569
|
+
return `${url}${url.includes("?") ? "&" : "?"}${searchParams.toString()}`;
|
|
3570
|
+
}
|
|
3571
|
+
const handleRedirect = (ctx, uri) => {
|
|
3572
|
+
if (ctx.headers?.get("accept")?.includes("application/json")) return {
|
|
3573
|
+
redirect: true,
|
|
3574
|
+
url: uri.toString()
|
|
3575
|
+
};
|
|
3576
|
+
else throw ctx.redirect(uri);
|
|
3577
|
+
};
|
|
3578
|
+
/**
|
|
3579
|
+
* Validates that the issuer URL
|
|
3580
|
+
* - MUST use HTTPS scheme (HTTP allowed for localhost in dev)
|
|
3581
|
+
* - MUST NOT contain query components
|
|
3582
|
+
* - MUST NOT contain fragment components
|
|
3583
|
+
*
|
|
3584
|
+
* @returns The validated issuer URL, or a sanitized version if invalid
|
|
3585
|
+
*/
|
|
3586
|
+
function validateIssuerUrl(issuer) {
|
|
3587
|
+
try {
|
|
3588
|
+
const url = new URL(issuer);
|
|
3589
|
+
const isLocalhost = url.hostname === "localhost" || url.hostname === "127.0.0.1";
|
|
3590
|
+
if (url.protocol !== "https:" && !isLocalhost) url.protocol = "https:";
|
|
3591
|
+
url.search = "";
|
|
3592
|
+
url.hash = "";
|
|
3593
|
+
return url.toString().replace(/\/$/, "");
|
|
3594
|
+
} catch {
|
|
3595
|
+
return issuer;
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3598
|
+
/**
|
|
3599
|
+
* Gets the issuer identifier
|
|
3600
|
+
*/
|
|
3601
|
+
function getIssuer(ctx, opts) {
|
|
3602
|
+
let issuer;
|
|
3603
|
+
if (opts.disableJwtPlugin) issuer = ctx.context.baseURL;
|
|
3604
|
+
else try {
|
|
3605
|
+
issuer = getJwtPlugin(ctx.context).options?.jwt?.issuer ?? ctx.context.baseURL;
|
|
3606
|
+
} catch {
|
|
3607
|
+
issuer = ctx.context.baseURL;
|
|
3608
|
+
}
|
|
3609
|
+
return validateIssuerUrl(issuer);
|
|
3610
|
+
}
|
|
3611
|
+
/**
|
|
3612
|
+
* Error page url if redirect_uri has not been verified yet
|
|
3613
|
+
* Generates Url for custom error page
|
|
3614
|
+
*/
|
|
3615
|
+
function getErrorURL(ctx, error, description) {
|
|
3616
|
+
return formatErrorURL(ctx.context.options.onAPIError?.errorURL || `${ctx.context.baseURL}/error`, error, description);
|
|
3617
|
+
}
|
|
3618
|
+
async function authorizeEndpoint(ctx, opts, settings) {
|
|
3619
|
+
if (opts.grantTypes && !opts.grantTypes.includes("authorization_code")) throw new APIError$1("NOT_FOUND");
|
|
3620
|
+
if (!ctx.request) throw new APIError$1("UNAUTHORIZED", {
|
|
3621
|
+
error_description: "request not found",
|
|
3622
|
+
error: "invalid_request"
|
|
3623
|
+
});
|
|
3624
|
+
const query = ctx.query;
|
|
3625
|
+
await oAuthState.set({ query: query.toString() });
|
|
3626
|
+
if (!query.client_id) throw ctx.redirect(getErrorURL(ctx, "invalid_client", "client_id is required"));
|
|
3627
|
+
if (!query.response_type) throw ctx.redirect(getErrorURL(ctx, "invalid_request", "response_type is required"));
|
|
3628
|
+
const promptSet = ctx.query?.prompt ? parsePrompt(ctx.query?.prompt) : void 0;
|
|
3629
|
+
if (promptSet?.has("select_account") && !opts.selectAccount?.page) throw ctx.redirect(getErrorURL(ctx, `unsupported_prompt_select_account`, "unsupported prompt type"));
|
|
3630
|
+
if (!(query.response_type === "code")) throw ctx.redirect(getErrorURL(ctx, "unsupported_response_type", "unsupported response type"));
|
|
3631
|
+
const client = await getClient(ctx, opts, query.client_id);
|
|
3632
|
+
if (!client) throw ctx.redirect(getErrorURL(ctx, "invalid_client", "client_id is required"));
|
|
3633
|
+
if (client.disabled) throw ctx.redirect(getErrorURL(ctx, "client_disabled", "client is disabled"));
|
|
3634
|
+
if (!client.redirectUris?.find((url) => url === query.redirect_uri) || !query.redirect_uri) throw ctx.redirect(getErrorURL(ctx, "invalid_redirect", "invalid redirect uri"));
|
|
3635
|
+
let requestedScopes = query.scope?.split(" ").filter((s) => s);
|
|
3636
|
+
if (requestedScopes) {
|
|
3637
|
+
const validScopes = new Set(client.scopes ?? opts.scopes);
|
|
3638
|
+
const invalidScopes = requestedScopes.filter((scope) => {
|
|
3639
|
+
return !validScopes?.has(scope);
|
|
3640
|
+
});
|
|
3641
|
+
if (invalidScopes.length) throw ctx.redirect(formatErrorURL(query.redirect_uri, "invalid_scope", `The following scopes are invalid: ${invalidScopes.join(", ")}`, query.state, getIssuer(ctx, opts)));
|
|
3642
|
+
}
|
|
3643
|
+
if (!requestedScopes) {
|
|
3644
|
+
requestedScopes = client.scopes ?? opts.scopes ?? [];
|
|
3645
|
+
query.scope = requestedScopes.join(" ");
|
|
3646
|
+
}
|
|
3647
|
+
const pkceRequired = isPKCERequired(client, requestedScopes);
|
|
3648
|
+
if (pkceRequired) {
|
|
3649
|
+
if (!query.code_challenge || !query.code_challenge_method) throw ctx.redirect(formatErrorURL(query.redirect_uri, "invalid_request", pkceRequired.valueOf(), query.state, getIssuer(ctx, opts)));
|
|
3650
|
+
}
|
|
3651
|
+
if (query.code_challenge || query.code_challenge_method) {
|
|
3652
|
+
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)));
|
|
3653
|
+
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)));
|
|
3654
|
+
}
|
|
3655
|
+
const session = await getSessionFromCtx(ctx);
|
|
3656
|
+
if (!session || promptSet?.has("login") || promptSet?.has("create")) return redirectWithPromptCode(ctx, opts, promptSet?.has("create") ? "create" : "login");
|
|
3657
|
+
if (settings?.isAuthorize && promptSet?.has("select_account")) return redirectWithPromptCode(ctx, opts, "select_account");
|
|
3658
|
+
if (settings?.isAuthorize && opts.selectAccount) {
|
|
3659
|
+
if (await opts.selectAccount.shouldRedirect({
|
|
3660
|
+
headers: ctx.request.headers,
|
|
3661
|
+
user: session.user,
|
|
3662
|
+
session: session.session,
|
|
3663
|
+
scopes: requestedScopes
|
|
3664
|
+
})) return redirectWithPromptCode(ctx, opts, "select_account");
|
|
3665
|
+
}
|
|
3666
|
+
if (opts.signup?.shouldRedirect) {
|
|
3667
|
+
const signupRedirect = await opts.signup.shouldRedirect({
|
|
3668
|
+
headers: ctx.request.headers,
|
|
3669
|
+
user: session.user,
|
|
3670
|
+
session: session.session,
|
|
3671
|
+
scopes: requestedScopes
|
|
3672
|
+
});
|
|
3673
|
+
if (signupRedirect) return redirectWithPromptCode(ctx, opts, "create", typeof signupRedirect === "string" ? signupRedirect : void 0);
|
|
3674
|
+
}
|
|
3675
|
+
if (!settings?.postLogin && opts.postLogin) {
|
|
3676
|
+
if (await opts.postLogin.shouldRedirect({
|
|
3677
|
+
headers: ctx.request.headers,
|
|
3678
|
+
user: session.user,
|
|
3679
|
+
session: session.session,
|
|
3680
|
+
scopes: requestedScopes
|
|
3681
|
+
})) return redirectWithPromptCode(ctx, opts, "post_login");
|
|
3682
|
+
}
|
|
3683
|
+
if (promptSet?.has("consent")) return redirectWithPromptCode(ctx, opts, "consent");
|
|
3684
|
+
const referenceId = await opts.postLogin?.consentReferenceId?.({
|
|
3685
|
+
user: session.user,
|
|
3686
|
+
session: session.session,
|
|
3687
|
+
scopes: requestedScopes
|
|
3688
|
+
});
|
|
3689
|
+
if (client.skipConsent) return redirectWithAuthorizationCode(ctx, opts, {
|
|
3690
|
+
query,
|
|
3691
|
+
clientId: client.clientId,
|
|
3692
|
+
userId: session.user.id,
|
|
3693
|
+
sessionId: session.session.id,
|
|
3694
|
+
referenceId
|
|
3695
|
+
});
|
|
3696
|
+
const consent = await ctx.context.adapter.findOne({
|
|
3697
|
+
model: "oauthConsent",
|
|
3698
|
+
where: [
|
|
3699
|
+
{
|
|
3700
|
+
field: "clientId",
|
|
3701
|
+
value: client.clientId
|
|
3702
|
+
},
|
|
3703
|
+
{
|
|
3704
|
+
field: "userId",
|
|
3705
|
+
value: session.user.id
|
|
3706
|
+
},
|
|
3707
|
+
...referenceId ? [{
|
|
3708
|
+
field: "referenceId",
|
|
3709
|
+
value: referenceId
|
|
3710
|
+
}] : []
|
|
3711
|
+
]
|
|
3712
|
+
});
|
|
3713
|
+
if (!consent || !requestedScopes.every((val) => consent.scopes.includes(val))) return redirectWithPromptCode(ctx, opts, "consent");
|
|
3714
|
+
return redirectWithAuthorizationCode(ctx, opts, {
|
|
3715
|
+
query,
|
|
3716
|
+
clientId: client.clientId,
|
|
3717
|
+
userId: session.user.id,
|
|
3718
|
+
sessionId: session.session.id,
|
|
3719
|
+
referenceId
|
|
3720
|
+
});
|
|
3721
|
+
}
|
|
3722
|
+
async function redirectWithAuthorizationCode(ctx, opts, verificationValue) {
|
|
3723
|
+
const code = generateRandomString(32, "a-z", "A-Z", "0-9");
|
|
3724
|
+
const iat = Math.floor(Date.now() / 1e3);
|
|
3725
|
+
const exp = iat + (opts.codeExpiresIn ?? 600);
|
|
3726
|
+
const data = {
|
|
3727
|
+
identifier: await storeToken(opts.storeTokens, code, "authorization_code"),
|
|
3728
|
+
updatedAt: /* @__PURE__ */ new Date(iat * 1e3),
|
|
3729
|
+
expiresAt: /* @__PURE__ */ new Date(exp * 1e3),
|
|
3730
|
+
value: JSON.stringify({
|
|
3731
|
+
type: "authorization_code",
|
|
3732
|
+
query: ctx.query,
|
|
3733
|
+
userId: verificationValue.userId,
|
|
3734
|
+
sessionId: verificationValue?.sessionId,
|
|
3735
|
+
referenceId: verificationValue.referenceId
|
|
3736
|
+
})
|
|
3737
|
+
};
|
|
3738
|
+
ctx.context.verification_id ? await ctx.context.internalAdapter.updateVerificationValue(ctx.context.verification_id, data) : await ctx.context.internalAdapter.createVerificationValue({
|
|
3739
|
+
...data,
|
|
3740
|
+
createdAt: /* @__PURE__ */ new Date(iat * 1e3)
|
|
3741
|
+
});
|
|
3742
|
+
const redirectUriWithCode = new URL(verificationValue.query.redirect_uri);
|
|
3743
|
+
redirectUriWithCode.searchParams.set("code", code);
|
|
3744
|
+
if (verificationValue.query.state) redirectUriWithCode.searchParams.set("state", verificationValue.query.state);
|
|
3745
|
+
redirectUriWithCode.searchParams.set("iss", getIssuer(ctx, opts));
|
|
3746
|
+
return handleRedirect(ctx, redirectUriWithCode.toString());
|
|
3747
|
+
}
|
|
3748
|
+
async function redirectWithPromptCode(ctx, opts, type, page) {
|
|
3749
|
+
const queryParams = await signParams(ctx, opts);
|
|
3750
|
+
let path = opts.loginPage;
|
|
3751
|
+
if (type === "select_account") path = opts.selectAccount?.page ?? opts.loginPage;
|
|
3752
|
+
else if (type === "post_login") {
|
|
3753
|
+
if (!opts.postLogin?.page) throw new APIError$1("INTERNAL_SERVER_ERROR", { error_description: "postLogin should have been defined" });
|
|
3754
|
+
path = opts.postLogin?.page;
|
|
3755
|
+
} else if (type === "consent") path = opts.consentPage;
|
|
3756
|
+
else if (type === "create") path = opts.signup?.page ?? opts.loginPage;
|
|
3757
|
+
return handleRedirect(ctx, `${page ?? path}?${queryParams}`);
|
|
3758
|
+
}
|
|
3759
|
+
async function signParams(ctx, opts) {
|
|
3760
|
+
const exp = Math.floor(Date.now() / 1e3) + (opts.codeExpiresIn ?? 600);
|
|
3761
|
+
const params = new URLSearchParams(ctx.query);
|
|
3762
|
+
params.set("exp", String(exp));
|
|
3763
|
+
const signature = await makeSignature(params.toString(), ctx.context.secret);
|
|
3764
|
+
params.append("sig", signature);
|
|
3765
|
+
return params.toString();
|
|
3766
|
+
}
|
|
3767
|
+
|
|
3768
|
+
//#endregion
|
|
3769
|
+
//#region src/metadata.ts
|
|
3770
|
+
function authServerMetadata(ctx, opts, overrides) {
|
|
3771
|
+
const baseURL = ctx.context.baseURL;
|
|
3772
|
+
return {
|
|
3773
|
+
scopes_supported: overrides?.scopes_supported,
|
|
3774
|
+
issuer: validateIssuerUrl(opts?.jwt?.issuer ?? baseURL),
|
|
3775
|
+
authorization_endpoint: `${baseURL}/oauth2/authorize`,
|
|
3776
|
+
token_endpoint: `${baseURL}/oauth2/token`,
|
|
3777
|
+
jwks_uri: overrides?.jwt_disabled ? void 0 : opts?.jwks?.remoteUrl ?? `${baseURL}${opts?.jwks?.jwksPath ?? "/jwks"}`,
|
|
3778
|
+
registration_endpoint: `${baseURL}/oauth2/register`,
|
|
3779
|
+
introspection_endpoint: `${baseURL}/oauth2/introspect`,
|
|
3780
|
+
revocation_endpoint: `${baseURL}/oauth2/revoke`,
|
|
3781
|
+
response_types_supported: overrides?.grant_types_supported && !overrides.grant_types_supported.includes("authorization_code") ? [] : ["code"],
|
|
3782
|
+
response_modes_supported: ["query"],
|
|
3783
|
+
grant_types_supported: overrides?.grant_types_supported ?? [
|
|
3784
|
+
"authorization_code",
|
|
3785
|
+
"client_credentials",
|
|
3786
|
+
"refresh_token"
|
|
3787
|
+
],
|
|
3788
|
+
token_endpoint_auth_methods_supported: [
|
|
3789
|
+
...overrides?.public_client_supported ? ["none"] : [],
|
|
3790
|
+
"client_secret_basic",
|
|
3791
|
+
"client_secret_post"
|
|
3792
|
+
],
|
|
3793
|
+
introspection_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post"],
|
|
3794
|
+
revocation_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post"],
|
|
3795
|
+
code_challenge_methods_supported: ["S256"],
|
|
3796
|
+
authorization_response_iss_parameter_supported: true
|
|
3797
|
+
};
|
|
3798
|
+
}
|
|
3799
|
+
function oidcServerMetadata(ctx, opts) {
|
|
3800
|
+
const baseURL = ctx.context.baseURL;
|
|
3801
|
+
const jwtPluginOptions = opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context).options;
|
|
3802
|
+
return {
|
|
3803
|
+
...authServerMetadata(ctx, jwtPluginOptions, {
|
|
3804
|
+
scopes_supported: opts.advertisedMetadata?.scopes_supported ?? opts.scopes,
|
|
3805
|
+
public_client_supported: opts.allowUnauthenticatedClientRegistration,
|
|
3806
|
+
grant_types_supported: opts.grantTypes,
|
|
3807
|
+
jwt_disabled: opts.disableJwtPlugin
|
|
3808
|
+
}),
|
|
3809
|
+
claims_supported: opts?.advertisedMetadata?.claims_supported ?? opts?.claims ?? [],
|
|
3810
|
+
userinfo_endpoint: `${baseURL}/oauth2/userinfo`,
|
|
3811
|
+
subject_types_supported: ["public"],
|
|
3812
|
+
id_token_signing_alg_values_supported: jwtPluginOptions?.jwks?.keyPairConfig?.alg ? [jwtPluginOptions?.jwks?.keyPairConfig?.alg] : opts.disableJwtPlugin ? ["HS256"] : ["EdDSA"],
|
|
3813
|
+
end_session_endpoint: `${baseURL}/oauth2/end-session`,
|
|
3814
|
+
acr_values_supported: ["urn:mace:incommon:iap:bronze"],
|
|
3815
|
+
prompt_values_supported: [
|
|
3816
|
+
"login",
|
|
3817
|
+
"consent",
|
|
3818
|
+
"create",
|
|
3819
|
+
"select_account"
|
|
3820
|
+
]
|
|
3821
|
+
};
|
|
3822
|
+
}
|
|
3823
|
+
/**
|
|
3824
|
+
* Provides an exportable `/.well-known/oauth-authorization-server`.
|
|
3825
|
+
*
|
|
3826
|
+
* Useful when basePath prevents the endpoint from being located at the root
|
|
3827
|
+
* and must be provided manually.
|
|
3828
|
+
*
|
|
3829
|
+
* @external
|
|
3830
|
+
*/
|
|
3831
|
+
const oauthProviderAuthServerMetadata = (auth, opts) => {
|
|
3832
|
+
return async (_request) => {
|
|
3833
|
+
const res = await auth.api.getOAuthServerConfig();
|
|
3834
|
+
return new Response(JSON.stringify(res), {
|
|
3835
|
+
status: 200,
|
|
3836
|
+
headers: {
|
|
3837
|
+
"Cache-Control": "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400",
|
|
3838
|
+
...opts?.headers,
|
|
3839
|
+
"Content-Type": "application/json"
|
|
3840
|
+
}
|
|
3841
|
+
});
|
|
3842
|
+
};
|
|
3843
|
+
};
|
|
3844
|
+
/**
|
|
3845
|
+
* Provides an exportable `/.well-known/openid-configuration`.
|
|
3846
|
+
*
|
|
3847
|
+
* Useful when basePath prevents the endpoint from being located at the root
|
|
3848
|
+
* and must be provided manually.
|
|
3849
|
+
*
|
|
3850
|
+
* @external
|
|
3851
|
+
*/
|
|
3852
|
+
const oauthProviderOpenIdConfigMetadata = (auth, opts) => {
|
|
3853
|
+
return async (_request) => {
|
|
3854
|
+
const res = await auth.api.getOpenIdConfig();
|
|
3855
|
+
return new Response(JSON.stringify(res), {
|
|
3856
|
+
status: 200,
|
|
3857
|
+
headers: {
|
|
3858
|
+
"Cache-Control": "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400",
|
|
3859
|
+
...opts?.headers,
|
|
3860
|
+
"Content-Type": "application/json"
|
|
3861
|
+
}
|
|
3862
|
+
});
|
|
3863
|
+
};
|
|
3864
|
+
};
|
|
3865
|
+
|
|
3866
|
+
//#endregion
|
|
3867
|
+
export { authServerMetadata, getOAuthProviderState, mcpHandler, oauthProvider, oauthProviderAuthServerMetadata, oauthProviderOpenIdConfigMetadata, oidcServerMetadata };
|
|
3815
3868
|
//# sourceMappingURL=index.mjs.map
|