@better-auth/oauth-provider 1.5.0-beta.13 → 1.5.0-beta.15
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 +439 -398
- package/dist/index.mjs.map +1 -1
- package/dist/{oauth-BHiPnA5P.d.mts → oauth-BkM9Zp3N.d.mts} +18 -5
- package/dist/{oauth-DGg-M8uO.d.mts → oauth-CAVzHW54.d.mts} +23 -1
- package/dist/{utils-DWE-cPWY.mjs → utils-fj4yCZS4.mjs} +28 -2
- package/dist/{utils-DWE-cPWY.mjs.map → utils-fj4yCZS4.mjs.map} +1 -1
- package/package.json +7 -7
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-fj4yCZS4.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
|
+
uri: 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,264 +60,63 @@ 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
|
+
uri: 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
|
uri: url
|
|
@@ -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;
|
|
@@ -1456,7 +1175,7 @@ async function createOAuthClientEndpoint(ctx, opts, settings) {
|
|
|
1456
1175
|
* @returns
|
|
1457
1176
|
*/
|
|
1458
1177
|
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;
|
|
1178
|
+
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
1179
|
const expiresAt = _expiresAt ? /* @__PURE__ */ new Date(_expiresAt * 1e3) : void 0;
|
|
1461
1180
|
const createdAt = _createdAt ? /* @__PURE__ */ new Date(_createdAt * 1e3) : void 0;
|
|
1462
1181
|
const scopes = _scope?.split(" ");
|
|
@@ -1490,6 +1209,7 @@ function oauthToSchema(input) {
|
|
|
1490
1209
|
type,
|
|
1491
1210
|
skipConsent,
|
|
1492
1211
|
enableEndSession,
|
|
1212
|
+
requirePKCE,
|
|
1493
1213
|
referenceId,
|
|
1494
1214
|
metadata: Object.keys(metadataObj).length ? JSON.stringify(metadataObj) : void 0
|
|
1495
1215
|
};
|
|
@@ -1501,9 +1221,9 @@ function oauthToSchema(input) {
|
|
|
1501
1221
|
* @returns
|
|
1502
1222
|
*/
|
|
1503
1223
|
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;
|
|
1224
|
+
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;
|
|
1225
|
+
const _expiresAt = expiresAt ? Math.round(new Date(expiresAt).getTime() / 1e3) : void 0;
|
|
1226
|
+
const _createdAt = createdAt ? Math.round(new Date(createdAt).getTime() / 1e3) : void 0;
|
|
1507
1227
|
const _scopes = scopes?.join(" ");
|
|
1508
1228
|
return {
|
|
1509
1229
|
...parseClientMetadata(metadata),
|
|
@@ -1532,6 +1252,7 @@ function schemaToOAuth(input) {
|
|
|
1532
1252
|
disabled: disabled ?? void 0,
|
|
1533
1253
|
skip_consent: skipConsent ?? void 0,
|
|
1534
1254
|
enable_end_session: enableEndSession ?? void 0,
|
|
1255
|
+
require_pkce: requirePKCE ?? void 0,
|
|
1535
1256
|
reference_id: referenceId ?? void 0
|
|
1536
1257
|
};
|
|
1537
1258
|
}
|
|
@@ -1840,6 +1561,7 @@ const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/creat
|
|
|
1840
1561
|
client_secret_expires_at: z.union([z.string(), z.number()]).optional().default(0),
|
|
1841
1562
|
skip_consent: z.boolean().optional(),
|
|
1842
1563
|
enable_end_session: z.boolean().optional(),
|
|
1564
|
+
require_pkce: z.boolean().optional(),
|
|
1843
1565
|
metadata: z.record(z.string(), z.unknown()).optional()
|
|
1844
1566
|
}),
|
|
1845
1567
|
metadata: {
|
|
@@ -1966,6 +1688,11 @@ const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/creat
|
|
|
1966
1688
|
type: "boolean",
|
|
1967
1689
|
description: "Whether the client is disabled"
|
|
1968
1690
|
},
|
|
1691
|
+
require_pkce: {
|
|
1692
|
+
type: "boolean",
|
|
1693
|
+
description: "Whether the client requires PKCE",
|
|
1694
|
+
default: true
|
|
1695
|
+
},
|
|
1969
1696
|
metadata: {
|
|
1970
1697
|
type: "object",
|
|
1971
1698
|
additionalProperties: true,
|
|
@@ -2713,6 +2440,10 @@ const schema = {
|
|
|
2713
2440
|
type: "string",
|
|
2714
2441
|
required: false
|
|
2715
2442
|
},
|
|
2443
|
+
requirePKCE: {
|
|
2444
|
+
type: "boolean",
|
|
2445
|
+
required: false
|
|
2446
|
+
},
|
|
2716
2447
|
referenceId: {
|
|
2717
2448
|
type: "string",
|
|
2718
2449
|
required: false
|
|
@@ -2856,6 +2587,7 @@ const schema = {
|
|
|
2856
2587
|
//#endregion
|
|
2857
2588
|
//#region src/oauth.ts
|
|
2858
2589
|
const oAuthState = defineRequestState(() => null);
|
|
2590
|
+
const getOAuthProviderState = oAuthState.get;
|
|
2859
2591
|
/**
|
|
2860
2592
|
* oAuth 2.1 provider plugin for Better Auth.
|
|
2861
2593
|
*
|
|
@@ -3811,5 +3543,314 @@ const oauthProvider = (options) => {
|
|
|
3811
3543
|
};
|
|
3812
3544
|
|
|
3813
3545
|
//#endregion
|
|
3814
|
-
|
|
3546
|
+
//#region src/authorize.ts
|
|
3547
|
+
/**
|
|
3548
|
+
* Formats an error url
|
|
3549
|
+
*/
|
|
3550
|
+
function formatErrorURL(url, error, description, state, iss) {
|
|
3551
|
+
const searchParams = new URLSearchParams({
|
|
3552
|
+
error,
|
|
3553
|
+
error_description: description
|
|
3554
|
+
});
|
|
3555
|
+
state && searchParams.append("state", state);
|
|
3556
|
+
iss && searchParams.append("iss", iss);
|
|
3557
|
+
return `${url}${url.includes("?") ? "&" : "?"}${searchParams.toString()}`;
|
|
3558
|
+
}
|
|
3559
|
+
const handleRedirect = (ctx, uri) => {
|
|
3560
|
+
if (ctx.headers?.get("accept")?.includes("application/json")) return {
|
|
3561
|
+
redirect: true,
|
|
3562
|
+
url: uri.toString()
|
|
3563
|
+
};
|
|
3564
|
+
else throw ctx.redirect(uri);
|
|
3565
|
+
};
|
|
3566
|
+
/**
|
|
3567
|
+
* Validates that the issuer URL
|
|
3568
|
+
* - MUST use HTTPS scheme (HTTP allowed for localhost in dev)
|
|
3569
|
+
* - MUST NOT contain query components
|
|
3570
|
+
* - MUST NOT contain fragment components
|
|
3571
|
+
*
|
|
3572
|
+
* @returns The validated issuer URL, or a sanitized version if invalid
|
|
3573
|
+
*/
|
|
3574
|
+
function validateIssuerUrl(issuer) {
|
|
3575
|
+
try {
|
|
3576
|
+
const url = new URL(issuer);
|
|
3577
|
+
const isLocalhost = url.hostname === "localhost" || url.hostname === "127.0.0.1";
|
|
3578
|
+
if (url.protocol !== "https:" && !isLocalhost) url.protocol = "https:";
|
|
3579
|
+
url.search = "";
|
|
3580
|
+
url.hash = "";
|
|
3581
|
+
return url.toString().replace(/\/$/, "");
|
|
3582
|
+
} catch {
|
|
3583
|
+
return issuer;
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3586
|
+
/**
|
|
3587
|
+
* Gets the issuer identifier
|
|
3588
|
+
*/
|
|
3589
|
+
function getIssuer(ctx, opts) {
|
|
3590
|
+
let issuer;
|
|
3591
|
+
if (opts.disableJwtPlugin) issuer = ctx.context.baseURL;
|
|
3592
|
+
else try {
|
|
3593
|
+
issuer = getJwtPlugin(ctx.context).options?.jwt?.issuer ?? ctx.context.baseURL;
|
|
3594
|
+
} catch {
|
|
3595
|
+
issuer = ctx.context.baseURL;
|
|
3596
|
+
}
|
|
3597
|
+
return validateIssuerUrl(issuer);
|
|
3598
|
+
}
|
|
3599
|
+
/**
|
|
3600
|
+
* Error page url if redirect_uri has not been verified yet
|
|
3601
|
+
* Generates Url for custom error page
|
|
3602
|
+
*/
|
|
3603
|
+
function getErrorURL(ctx, error, description) {
|
|
3604
|
+
return formatErrorURL(ctx.context.options.onAPIError?.errorURL || `${ctx.context.baseURL}/error`, error, description);
|
|
3605
|
+
}
|
|
3606
|
+
async function authorizeEndpoint(ctx, opts, settings) {
|
|
3607
|
+
if (opts.grantTypes && !opts.grantTypes.includes("authorization_code")) throw new APIError$1("NOT_FOUND");
|
|
3608
|
+
if (!ctx.request) throw new APIError$1("UNAUTHORIZED", {
|
|
3609
|
+
error_description: "request not found",
|
|
3610
|
+
error: "invalid_request"
|
|
3611
|
+
});
|
|
3612
|
+
const query = ctx.query;
|
|
3613
|
+
await oAuthState.set({ query: query.toString() });
|
|
3614
|
+
if (!query.client_id) throw ctx.redirect(getErrorURL(ctx, "invalid_client", "client_id is required"));
|
|
3615
|
+
if (!query.response_type) throw ctx.redirect(getErrorURL(ctx, "invalid_request", "response_type is required"));
|
|
3616
|
+
const promptSet = ctx.query?.prompt ? parsePrompt(ctx.query?.prompt) : void 0;
|
|
3617
|
+
if (promptSet?.has("select_account") && !opts.selectAccount?.page) throw ctx.redirect(getErrorURL(ctx, `unsupported_prompt_select_account`, "unsupported prompt type"));
|
|
3618
|
+
if (!(query.response_type === "code")) throw ctx.redirect(getErrorURL(ctx, "unsupported_response_type", "unsupported response type"));
|
|
3619
|
+
const client = await getClient(ctx, opts, query.client_id);
|
|
3620
|
+
if (!client) throw ctx.redirect(getErrorURL(ctx, "invalid_client", "client_id is required"));
|
|
3621
|
+
if (client.disabled) throw ctx.redirect(getErrorURL(ctx, "client_disabled", "client is disabled"));
|
|
3622
|
+
if (!client.redirectUris?.find((url) => url === query.redirect_uri) || !query.redirect_uri) throw ctx.redirect(getErrorURL(ctx, "invalid_redirect", "invalid redirect uri"));
|
|
3623
|
+
let requestedScopes = query.scope?.split(" ").filter((s) => s);
|
|
3624
|
+
if (requestedScopes) {
|
|
3625
|
+
const validScopes = new Set(client.scopes ?? opts.scopes);
|
|
3626
|
+
const invalidScopes = requestedScopes.filter((scope) => {
|
|
3627
|
+
return !validScopes?.has(scope);
|
|
3628
|
+
});
|
|
3629
|
+
if (invalidScopes.length) throw ctx.redirect(formatErrorURL(query.redirect_uri, "invalid_scope", `The following scopes are invalid: ${invalidScopes.join(", ")}`, query.state, getIssuer(ctx, opts)));
|
|
3630
|
+
}
|
|
3631
|
+
if (!requestedScopes) {
|
|
3632
|
+
requestedScopes = client.scopes ?? opts.scopes ?? [];
|
|
3633
|
+
query.scope = requestedScopes.join(" ");
|
|
3634
|
+
}
|
|
3635
|
+
const pkceRequired = isPKCERequired(client, requestedScopes);
|
|
3636
|
+
if (pkceRequired) {
|
|
3637
|
+
if (!query.code_challenge || !query.code_challenge_method) throw ctx.redirect(formatErrorURL(query.redirect_uri, "invalid_request", pkceRequired.valueOf(), query.state, getIssuer(ctx, opts)));
|
|
3638
|
+
}
|
|
3639
|
+
if (query.code_challenge || query.code_challenge_method) {
|
|
3640
|
+
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)));
|
|
3641
|
+
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)));
|
|
3642
|
+
}
|
|
3643
|
+
const session = await getSessionFromCtx(ctx);
|
|
3644
|
+
if (!session || promptSet?.has("login") || promptSet?.has("create")) return redirectWithPromptCode(ctx, opts, promptSet?.has("create") ? "create" : "login");
|
|
3645
|
+
if (settings?.isAuthorize && promptSet?.has("select_account")) return redirectWithPromptCode(ctx, opts, "select_account");
|
|
3646
|
+
if (settings?.isAuthorize && opts.selectAccount) {
|
|
3647
|
+
if (await opts.selectAccount.shouldRedirect({
|
|
3648
|
+
headers: ctx.request.headers,
|
|
3649
|
+
user: session.user,
|
|
3650
|
+
session: session.session,
|
|
3651
|
+
scopes: requestedScopes
|
|
3652
|
+
})) return redirectWithPromptCode(ctx, opts, "select_account");
|
|
3653
|
+
}
|
|
3654
|
+
if (opts.signup?.shouldRedirect) {
|
|
3655
|
+
const signupRedirect = await opts.signup.shouldRedirect({
|
|
3656
|
+
headers: ctx.request.headers,
|
|
3657
|
+
user: session.user,
|
|
3658
|
+
session: session.session,
|
|
3659
|
+
scopes: requestedScopes
|
|
3660
|
+
});
|
|
3661
|
+
if (signupRedirect) return redirectWithPromptCode(ctx, opts, "create", typeof signupRedirect === "string" ? signupRedirect : void 0);
|
|
3662
|
+
}
|
|
3663
|
+
if (!settings?.postLogin && opts.postLogin) {
|
|
3664
|
+
if (await opts.postLogin.shouldRedirect({
|
|
3665
|
+
headers: ctx.request.headers,
|
|
3666
|
+
user: session.user,
|
|
3667
|
+
session: session.session,
|
|
3668
|
+
scopes: requestedScopes
|
|
3669
|
+
})) return redirectWithPromptCode(ctx, opts, "post_login");
|
|
3670
|
+
}
|
|
3671
|
+
if (promptSet?.has("consent")) return redirectWithPromptCode(ctx, opts, "consent");
|
|
3672
|
+
const referenceId = await opts.postLogin?.consentReferenceId?.({
|
|
3673
|
+
user: session.user,
|
|
3674
|
+
session: session.session,
|
|
3675
|
+
scopes: requestedScopes
|
|
3676
|
+
});
|
|
3677
|
+
if (client.skipConsent) return redirectWithAuthorizationCode(ctx, opts, {
|
|
3678
|
+
query,
|
|
3679
|
+
clientId: client.clientId,
|
|
3680
|
+
userId: session.user.id,
|
|
3681
|
+
sessionId: session.session.id,
|
|
3682
|
+
referenceId
|
|
3683
|
+
});
|
|
3684
|
+
const consent = await ctx.context.adapter.findOne({
|
|
3685
|
+
model: "oauthConsent",
|
|
3686
|
+
where: [
|
|
3687
|
+
{
|
|
3688
|
+
field: "clientId",
|
|
3689
|
+
value: client.clientId
|
|
3690
|
+
},
|
|
3691
|
+
{
|
|
3692
|
+
field: "userId",
|
|
3693
|
+
value: session.user.id
|
|
3694
|
+
},
|
|
3695
|
+
...referenceId ? [{
|
|
3696
|
+
field: "referenceId",
|
|
3697
|
+
value: referenceId
|
|
3698
|
+
}] : []
|
|
3699
|
+
]
|
|
3700
|
+
});
|
|
3701
|
+
if (!consent || !requestedScopes.every((val) => consent.scopes.includes(val))) return redirectWithPromptCode(ctx, opts, "consent");
|
|
3702
|
+
return redirectWithAuthorizationCode(ctx, opts, {
|
|
3703
|
+
query,
|
|
3704
|
+
clientId: client.clientId,
|
|
3705
|
+
userId: session.user.id,
|
|
3706
|
+
sessionId: session.session.id,
|
|
3707
|
+
referenceId
|
|
3708
|
+
});
|
|
3709
|
+
}
|
|
3710
|
+
async function redirectWithAuthorizationCode(ctx, opts, verificationValue) {
|
|
3711
|
+
const code = generateRandomString(32, "a-z", "A-Z", "0-9");
|
|
3712
|
+
const iat = Math.floor(Date.now() / 1e3);
|
|
3713
|
+
const exp = iat + (opts.codeExpiresIn ?? 600);
|
|
3714
|
+
const data = {
|
|
3715
|
+
identifier: await storeToken(opts.storeTokens, code, "authorization_code"),
|
|
3716
|
+
updatedAt: /* @__PURE__ */ new Date(iat * 1e3),
|
|
3717
|
+
expiresAt: /* @__PURE__ */ new Date(exp * 1e3),
|
|
3718
|
+
value: JSON.stringify({
|
|
3719
|
+
type: "authorization_code",
|
|
3720
|
+
query: ctx.query,
|
|
3721
|
+
userId: verificationValue.userId,
|
|
3722
|
+
sessionId: verificationValue?.sessionId,
|
|
3723
|
+
referenceId: verificationValue.referenceId
|
|
3724
|
+
})
|
|
3725
|
+
};
|
|
3726
|
+
ctx.context.verification_id ? await ctx.context.internalAdapter.updateVerificationValue(ctx.context.verification_id, data) : await ctx.context.internalAdapter.createVerificationValue({
|
|
3727
|
+
...data,
|
|
3728
|
+
createdAt: /* @__PURE__ */ new Date(iat * 1e3)
|
|
3729
|
+
});
|
|
3730
|
+
const redirectUriWithCode = new URL(verificationValue.query.redirect_uri);
|
|
3731
|
+
redirectUriWithCode.searchParams.set("code", code);
|
|
3732
|
+
if (verificationValue.query.state) redirectUriWithCode.searchParams.set("state", verificationValue.query.state);
|
|
3733
|
+
redirectUriWithCode.searchParams.set("iss", getIssuer(ctx, opts));
|
|
3734
|
+
return handleRedirect(ctx, redirectUriWithCode.toString());
|
|
3735
|
+
}
|
|
3736
|
+
async function redirectWithPromptCode(ctx, opts, type, page) {
|
|
3737
|
+
const queryParams = await signParams(ctx, opts);
|
|
3738
|
+
let path = opts.loginPage;
|
|
3739
|
+
if (type === "select_account") path = opts.selectAccount?.page ?? opts.loginPage;
|
|
3740
|
+
else if (type === "post_login") {
|
|
3741
|
+
if (!opts.postLogin?.page) throw new APIError$1("INTERNAL_SERVER_ERROR", { error_description: "postLogin should have been defined" });
|
|
3742
|
+
path = opts.postLogin?.page;
|
|
3743
|
+
} else if (type === "consent") path = opts.consentPage;
|
|
3744
|
+
else if (type === "create") path = opts.signup?.page ?? opts.loginPage;
|
|
3745
|
+
return handleRedirect(ctx, `${page ?? path}?${queryParams}`);
|
|
3746
|
+
}
|
|
3747
|
+
async function signParams(ctx, opts) {
|
|
3748
|
+
const exp = Math.floor(Date.now() / 1e3) + (opts.codeExpiresIn ?? 600);
|
|
3749
|
+
const params = new URLSearchParams(ctx.query);
|
|
3750
|
+
params.set("exp", String(exp));
|
|
3751
|
+
const signature = await makeSignature(params.toString(), ctx.context.secret);
|
|
3752
|
+
params.append("sig", signature);
|
|
3753
|
+
return params.toString();
|
|
3754
|
+
}
|
|
3755
|
+
|
|
3756
|
+
//#endregion
|
|
3757
|
+
//#region src/metadata.ts
|
|
3758
|
+
function authServerMetadata(ctx, opts, overrides) {
|
|
3759
|
+
const baseURL = ctx.context.baseURL;
|
|
3760
|
+
return {
|
|
3761
|
+
scopes_supported: overrides?.scopes_supported,
|
|
3762
|
+
issuer: validateIssuerUrl(opts?.jwt?.issuer ?? baseURL),
|
|
3763
|
+
authorization_endpoint: `${baseURL}/oauth2/authorize`,
|
|
3764
|
+
token_endpoint: `${baseURL}/oauth2/token`,
|
|
3765
|
+
jwks_uri: overrides?.jwt_disabled ? void 0 : opts?.jwks?.remoteUrl ?? `${baseURL}${opts?.jwks?.jwksPath ?? "/jwks"}`,
|
|
3766
|
+
registration_endpoint: `${baseURL}/oauth2/register`,
|
|
3767
|
+
introspection_endpoint: `${baseURL}/oauth2/introspect`,
|
|
3768
|
+
revocation_endpoint: `${baseURL}/oauth2/revoke`,
|
|
3769
|
+
response_types_supported: overrides?.grant_types_supported && !overrides.grant_types_supported.includes("authorization_code") ? [] : ["code"],
|
|
3770
|
+
response_modes_supported: ["query"],
|
|
3771
|
+
grant_types_supported: overrides?.grant_types_supported ?? [
|
|
3772
|
+
"authorization_code",
|
|
3773
|
+
"client_credentials",
|
|
3774
|
+
"refresh_token"
|
|
3775
|
+
],
|
|
3776
|
+
token_endpoint_auth_methods_supported: [
|
|
3777
|
+
...overrides?.public_client_supported ? ["none"] : [],
|
|
3778
|
+
"client_secret_basic",
|
|
3779
|
+
"client_secret_post"
|
|
3780
|
+
],
|
|
3781
|
+
introspection_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post"],
|
|
3782
|
+
revocation_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post"],
|
|
3783
|
+
code_challenge_methods_supported: ["S256"],
|
|
3784
|
+
authorization_response_iss_parameter_supported: true
|
|
3785
|
+
};
|
|
3786
|
+
}
|
|
3787
|
+
function oidcServerMetadata(ctx, opts) {
|
|
3788
|
+
const baseURL = ctx.context.baseURL;
|
|
3789
|
+
const jwtPluginOptions = opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context).options;
|
|
3790
|
+
return {
|
|
3791
|
+
...authServerMetadata(ctx, jwtPluginOptions, {
|
|
3792
|
+
scopes_supported: opts.advertisedMetadata?.scopes_supported ?? opts.scopes,
|
|
3793
|
+
public_client_supported: opts.allowUnauthenticatedClientRegistration,
|
|
3794
|
+
grant_types_supported: opts.grantTypes,
|
|
3795
|
+
jwt_disabled: opts.disableJwtPlugin
|
|
3796
|
+
}),
|
|
3797
|
+
claims_supported: opts?.advertisedMetadata?.claims_supported ?? opts?.claims ?? [],
|
|
3798
|
+
userinfo_endpoint: `${baseURL}/oauth2/userinfo`,
|
|
3799
|
+
subject_types_supported: ["public"],
|
|
3800
|
+
id_token_signing_alg_values_supported: jwtPluginOptions?.jwks?.keyPairConfig?.alg ? [jwtPluginOptions?.jwks?.keyPairConfig?.alg] : opts.disableJwtPlugin ? ["HS256"] : ["EdDSA"],
|
|
3801
|
+
end_session_endpoint: `${baseURL}/oauth2/end-session`,
|
|
3802
|
+
acr_values_supported: ["urn:mace:incommon:iap:bronze"],
|
|
3803
|
+
prompt_values_supported: [
|
|
3804
|
+
"login",
|
|
3805
|
+
"consent",
|
|
3806
|
+
"create",
|
|
3807
|
+
"select_account"
|
|
3808
|
+
]
|
|
3809
|
+
};
|
|
3810
|
+
}
|
|
3811
|
+
/**
|
|
3812
|
+
* Provides an exportable `/.well-known/oauth-authorization-server`.
|
|
3813
|
+
*
|
|
3814
|
+
* Useful when basePath prevents the endpoint from being located at the root
|
|
3815
|
+
* and must be provided manually.
|
|
3816
|
+
*
|
|
3817
|
+
* @external
|
|
3818
|
+
*/
|
|
3819
|
+
const oauthProviderAuthServerMetadata = (auth, opts) => {
|
|
3820
|
+
return async (_request) => {
|
|
3821
|
+
const res = await auth.api.getOAuthServerConfig();
|
|
3822
|
+
return new Response(JSON.stringify(res), {
|
|
3823
|
+
status: 200,
|
|
3824
|
+
headers: {
|
|
3825
|
+
"Cache-Control": "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400",
|
|
3826
|
+
...opts?.headers,
|
|
3827
|
+
"Content-Type": "application/json"
|
|
3828
|
+
}
|
|
3829
|
+
});
|
|
3830
|
+
};
|
|
3831
|
+
};
|
|
3832
|
+
/**
|
|
3833
|
+
* Provides an exportable `/.well-known/openid-configuration`.
|
|
3834
|
+
*
|
|
3835
|
+
* Useful when basePath prevents the endpoint from being located at the root
|
|
3836
|
+
* and must be provided manually.
|
|
3837
|
+
*
|
|
3838
|
+
* @external
|
|
3839
|
+
*/
|
|
3840
|
+
const oauthProviderOpenIdConfigMetadata = (auth, opts) => {
|
|
3841
|
+
return async (_request) => {
|
|
3842
|
+
const res = await auth.api.getOpenIdConfig();
|
|
3843
|
+
return new Response(JSON.stringify(res), {
|
|
3844
|
+
status: 200,
|
|
3845
|
+
headers: {
|
|
3846
|
+
"Cache-Control": "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400",
|
|
3847
|
+
...opts?.headers,
|
|
3848
|
+
"Content-Type": "application/json"
|
|
3849
|
+
}
|
|
3850
|
+
});
|
|
3851
|
+
};
|
|
3852
|
+
};
|
|
3853
|
+
|
|
3854
|
+
//#endregion
|
|
3855
|
+
export { authServerMetadata, getOAuthProviderState, mcpHandler, oauthProvider, oauthProviderAuthServerMetadata, oauthProviderOpenIdConfigMetadata, oidcServerMetadata };
|
|
3815
3856
|
//# sourceMappingURL=index.mjs.map
|