better-auth 1.6.11 → 1.6.12
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/api/index.d.mts +8 -2
- package/dist/api/routes/callback.d.mts +1 -1
- package/dist/api/routes/callback.mjs +36 -40
- package/dist/api/routes/email-verification.d.mts +1 -0
- package/dist/api/routes/email-verification.mjs +4 -3
- package/dist/api/routes/session.mjs +14 -9
- package/dist/api/routes/sign-in.d.mts +1 -0
- package/dist/api/routes/sign-in.mjs +2 -1
- package/dist/api/routes/sign-up.d.mts +1 -0
- package/dist/api/routes/sign-up.mjs +9 -7
- package/dist/api/routes/update-user.mjs +4 -4
- package/dist/client/parser.mjs +0 -1
- package/dist/client/plugins/index.d.mts +3 -3
- package/dist/client/proxy.mjs +2 -1
- package/dist/context/helpers.mjs +3 -2
- package/dist/cookies/cookie-utils.d.mts +24 -1
- package/dist/cookies/cookie-utils.mjs +85 -22
- package/dist/cookies/index.d.mts +2 -3
- package/dist/cookies/index.mjs +39 -11
- package/dist/cookies/session-store.mjs +4 -23
- package/dist/db/get-migration.mjs +4 -4
- package/dist/db/index.d.mts +2 -2
- package/dist/db/index.mjs +3 -2
- package/dist/db/internal-adapter.mjs +37 -30
- package/dist/db/schema.d.mts +15 -2
- package/dist/db/schema.mjs +26 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/oauth2/errors.mjs +16 -1
- package/dist/oauth2/link-account.mjs +3 -3
- package/dist/oauth2/state.mjs +8 -2
- package/dist/package.mjs +1 -1
- package/dist/plugins/access/access.mjs +11 -6
- package/dist/plugins/admin/admin.mjs +0 -4
- package/dist/plugins/admin/client.d.mts +1 -1
- package/dist/plugins/bearer/index.mjs +4 -9
- package/dist/plugins/captcha/index.mjs +2 -2
- package/dist/plugins/generic-oauth/index.d.mts +1 -1
- package/dist/plugins/generic-oauth/index.mjs +6 -6
- package/dist/plugins/generic-oauth/routes.mjs +34 -32
- package/dist/plugins/generic-oauth/types.d.mts +7 -0
- package/dist/plugins/last-login-method/client.mjs +2 -2
- package/dist/plugins/magic-link/index.mjs +0 -1
- package/dist/plugins/mcp/index.mjs +0 -4
- package/dist/plugins/multi-session/index.mjs +2 -2
- package/dist/plugins/oauth-proxy/index.mjs +44 -31
- package/dist/plugins/oauth-proxy/utils.mjs +3 -10
- package/dist/plugins/oidc-provider/index.mjs +0 -4
- package/dist/plugins/open-api/generator.mjs +16 -5
- package/dist/plugins/organization/adapter.mjs +61 -56
- package/dist/plugins/organization/client.d.mts +2 -1
- package/dist/plugins/organization/error-codes.d.mts +1 -0
- package/dist/plugins/organization/error-codes.mjs +2 -1
- package/dist/plugins/organization/routes/crud-invites.mjs +3 -0
- package/dist/plugins/two-factor/index.mjs +3 -2
- package/dist/plugins/username/index.d.mts +24 -2
- package/dist/plugins/username/index.mjs +49 -3
- package/dist/state.d.mts +2 -2
- package/dist/state.mjs +18 -4
- package/dist/test-utils/headers.mjs +2 -7
- package/dist/test-utils/test-instance.d.mts +24 -6
- package/dist/utils/index.d.mts +1 -1
- package/dist/utils/url.d.mts +2 -1
- package/dist/utils/url.mjs +9 -3
- package/package.json +15 -14
package/dist/oauth2/state.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { generateRandomString } from "../crypto/random.mjs";
|
|
2
|
+
import { redirectOnError } from "./errors.mjs";
|
|
2
3
|
import { setOAuthState } from "../api/state/oauth.mjs";
|
|
3
4
|
import { StateError, generateGenericState, parseGenericState } from "../state.mjs";
|
|
4
5
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
@@ -36,8 +37,13 @@ async function parseState(c) {
|
|
|
36
37
|
parsedData = await parseGenericState(c, state);
|
|
37
38
|
} catch (error) {
|
|
38
39
|
c.context.logger.error("Failed to parse state", error);
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
let code = "internal_server_error";
|
|
41
|
+
let redirectErrorURL = errorURL;
|
|
42
|
+
if (error instanceof StateError) {
|
|
43
|
+
code = error.code === "state_security_mismatch" ? "state_mismatch" : error.code;
|
|
44
|
+
redirectErrorURL = error.errorURL ?? errorURL;
|
|
45
|
+
}
|
|
46
|
+
redirectOnError(c, redirectErrorURL, code);
|
|
41
47
|
}
|
|
42
48
|
if (!parsedData.errorURL) parsedData.errorURL = errorURL;
|
|
43
49
|
if (parsedData) await setOAuthState(parsedData);
|
package/dist/package.mjs
CHANGED
|
@@ -6,14 +6,19 @@ function role(statements) {
|
|
|
6
6
|
let success = false;
|
|
7
7
|
for (const [requestedResource, requestedActions] of Object.entries(request)) {
|
|
8
8
|
const allowedActions = statements[requestedResource];
|
|
9
|
-
if (!allowedActions)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
if (!allowedActions) {
|
|
10
|
+
if (connector === "AND") return {
|
|
11
|
+
success: false,
|
|
12
|
+
error: `You are not allowed to access resource: ${requestedResource}`
|
|
13
|
+
};
|
|
14
|
+
success = false;
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (Array.isArray(requestedActions)) success = requestedActions.length > 0 && requestedActions.every((requestedAction) => allowedActions.includes(requestedAction));
|
|
14
18
|
else if (typeof requestedActions === "object") {
|
|
15
19
|
const actions = requestedActions;
|
|
16
|
-
if (actions.
|
|
20
|
+
if (!Array.isArray(actions.actions) || actions.actions.length === 0) success = false;
|
|
21
|
+
else if (actions.connector === "OR") success = actions.actions.some((requestedAction) => allowedActions.includes(requestedAction));
|
|
17
22
|
else success = actions.actions.every((requestedAction) => allowedActions.includes(requestedAction));
|
|
18
23
|
} else throw new BetterAuthError("Invalid access control request");
|
|
19
24
|
if (success && connector === "OR") return { success };
|
|
@@ -42,10 +42,6 @@ const admin = (options) => {
|
|
|
42
42
|
});
|
|
43
43
|
return;
|
|
44
44
|
}
|
|
45
|
-
if (ctx && (ctx.path.startsWith("/callback") || ctx.path.startsWith("/oauth2/callback"))) {
|
|
46
|
-
const redirectURI = ctx.context.options.onAPIError?.errorURL || `${ctx.context.baseURL}/error`;
|
|
47
|
-
throw ctx.redirect(`${redirectURI}?error=banned&error_description=${opts.bannedUserMessage}`);
|
|
48
|
-
}
|
|
49
45
|
throw APIError.from("FORBIDDEN", {
|
|
50
46
|
message: opts.bannedUserMessage,
|
|
51
47
|
code: "BANNED_USER"
|
|
@@ -30,16 +30,11 @@ const bearer = (options) => {
|
|
|
30
30
|
if (authHeader.slice(0, 7).toLowerCase() !== BEARER_SCHEME) return;
|
|
31
31
|
const token = authHeader.slice(7).trim();
|
|
32
32
|
if (!token) return;
|
|
33
|
-
let signedToken;
|
|
34
33
|
let decodedToken;
|
|
35
|
-
if (token.includes("."))
|
|
36
|
-
|
|
37
|
-
signedToken = isEncoded ? token : encodeURIComponent(token);
|
|
38
|
-
decodedToken = isEncoded ? tryDecode(token) : token;
|
|
39
|
-
} else {
|
|
34
|
+
if (token.includes(".")) decodedToken = token.includes("%") ? tryDecode(token) : token;
|
|
35
|
+
else {
|
|
40
36
|
if (options?.requireSignature) return;
|
|
41
|
-
|
|
42
|
-
decodedToken = tryDecode(signedToken);
|
|
37
|
+
decodedToken = tryDecode((await serializeSignedCookie("", token, c.context.secret)).replace("=", ""));
|
|
43
38
|
}
|
|
44
39
|
try {
|
|
45
40
|
if (!await createHMAC("SHA-256", "base64urlnopad").verify(c.context.secret, decodedToken.split(".")[0], decodedToken.split(".")[1])) return;
|
|
@@ -48,7 +43,7 @@ const bearer = (options) => {
|
|
|
48
43
|
}
|
|
49
44
|
const existingHeaders = c.request?.headers || c.headers;
|
|
50
45
|
const headers = new Headers({ ...Object.fromEntries(existingHeaders?.entries()) });
|
|
51
|
-
setRequestCookie(headers, c.context.authCookies.sessionToken.name,
|
|
46
|
+
setRequestCookie(headers, c.context.authCookies.sessionToken.name, decodedToken);
|
|
52
47
|
return { context: { headers } };
|
|
53
48
|
})
|
|
54
49
|
}],
|
|
@@ -21,12 +21,12 @@ const captcha = (options) => ({
|
|
|
21
21
|
if (pathname.endsWith("//")) pathname = pathname.slice(0, -1);
|
|
22
22
|
if (pathname.startsWith("//")) pathname = pathname.slice(1);
|
|
23
23
|
if (!pathname.startsWith("/")) pathname = "/" + pathname;
|
|
24
|
-
const
|
|
24
|
+
const exemptPaths = ["/sign-in/email-otp"].reduce((acc, curr) => {
|
|
25
25
|
if (options.endpoints?.length && options.endpoints.includes(curr)) return acc;
|
|
26
26
|
return [...acc, curr];
|
|
27
27
|
}, []);
|
|
28
28
|
if (!endpoints.some((endpoint) => {
|
|
29
|
-
return pathname.includes(endpoint) && !
|
|
29
|
+
return pathname.includes(endpoint) && !exemptPaths.some((p) => pathname.includes(p));
|
|
30
30
|
})) return;
|
|
31
31
|
if (!options.secretKey) throw new Error(INTERNAL_ERROR_CODES.MISSING_SECRET_KEY.message);
|
|
32
32
|
const captchaResponse = request.headers.get("x-captcha-response");
|
|
@@ -11,7 +11,7 @@ import { okta } from "./providers/okta.mjs";
|
|
|
11
11
|
import { patreon } from "./providers/patreon.mjs";
|
|
12
12
|
import { slack } from "./providers/slack.mjs";
|
|
13
13
|
import { APIError } from "@better-auth/core/error";
|
|
14
|
-
import { createAuthorizationURL, refreshAccessToken, validateAuthorizationCode } from "@better-auth/core/oauth2";
|
|
14
|
+
import { applyDefaultAccessTokenExpiry, createAuthorizationURL, refreshAccessToken, validateAuthorizationCode } from "@better-auth/core/oauth2";
|
|
15
15
|
import { betterFetch } from "@better-fetch/fetch";
|
|
16
16
|
//#region src/plugins/generic-oauth/index.ts
|
|
17
17
|
/**
|
|
@@ -63,7 +63,7 @@ const genericOAuth = (options) => {
|
|
|
63
63
|
});
|
|
64
64
|
},
|
|
65
65
|
async validateAuthorizationCode(data) {
|
|
66
|
-
if (c.getToken) return c.getToken(data);
|
|
66
|
+
if (c.getToken) return applyDefaultAccessTokenExpiry(await c.getToken(data), c.accessTokenExpiresIn);
|
|
67
67
|
let finalTokenUrl = c.tokenUrl;
|
|
68
68
|
if (c.discoveryUrl) {
|
|
69
69
|
const discovery = await betterFetch(c.discoveryUrl, {
|
|
@@ -76,7 +76,7 @@ const genericOAuth = (options) => {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
if (!finalTokenUrl) throw APIError.from("BAD_REQUEST", GENERIC_OAUTH_ERROR_CODES.TOKEN_URL_NOT_FOUND);
|
|
79
|
-
return validateAuthorizationCode({
|
|
79
|
+
return applyDefaultAccessTokenExpiry(await validateAuthorizationCode({
|
|
80
80
|
headers: c.authorizationHeaders,
|
|
81
81
|
code: data.code,
|
|
82
82
|
codeVerifier: data.codeVerifier,
|
|
@@ -88,7 +88,7 @@ const genericOAuth = (options) => {
|
|
|
88
88
|
},
|
|
89
89
|
tokenEndpoint: finalTokenUrl,
|
|
90
90
|
authentication: c.authentication
|
|
91
|
-
});
|
|
91
|
+
}), c.accessTokenExpiresIn);
|
|
92
92
|
},
|
|
93
93
|
async refreshAccessToken(refreshToken) {
|
|
94
94
|
let finalTokenUrl = c.tokenUrl;
|
|
@@ -100,7 +100,7 @@ const genericOAuth = (options) => {
|
|
|
100
100
|
if (discovery.data) finalTokenUrl = discovery.data.token_endpoint;
|
|
101
101
|
}
|
|
102
102
|
if (!finalTokenUrl) throw APIError.from("BAD_REQUEST", GENERIC_OAUTH_ERROR_CODES.TOKEN_URL_NOT_FOUND);
|
|
103
|
-
return refreshAccessToken({
|
|
103
|
+
return applyDefaultAccessTokenExpiry(await refreshAccessToken({
|
|
104
104
|
refreshToken,
|
|
105
105
|
options: {
|
|
106
106
|
clientId: c.clientId,
|
|
@@ -108,7 +108,7 @@ const genericOAuth = (options) => {
|
|
|
108
108
|
},
|
|
109
109
|
authentication: c.authentication,
|
|
110
110
|
tokenEndpoint: finalTokenUrl
|
|
111
|
-
});
|
|
111
|
+
}), c.accessTokenExpiresIn);
|
|
112
112
|
},
|
|
113
113
|
async getUserInfo(tokens) {
|
|
114
114
|
const userInfo = c.getUserInfo ? await c.getUserInfo(tokens) : await getUserInfo(tokens, finalUserInfoUrl);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { isAPIError } from "../../utils/is-api-error.mjs";
|
|
1
2
|
import { setSessionCookie } from "../../cookies/index.mjs";
|
|
2
|
-
import { missingEmailLogMessage } from "../../oauth2/errors.mjs";
|
|
3
|
+
import { missingEmailLogMessage, redirectOnError } from "../../oauth2/errors.mjs";
|
|
3
4
|
import { generateState, parseState } from "../../oauth2/state.mjs";
|
|
4
5
|
import { setTokenUtil } from "../../oauth2/utils.mjs";
|
|
5
6
|
import { sessionMiddleware } from "../../api/routes/session.mjs";
|
|
@@ -8,7 +9,7 @@ import { HIDE_METADATA } from "../../utils/hide-metadata.mjs";
|
|
|
8
9
|
import { APIError as APIError$1 } from "../../api/index.mjs";
|
|
9
10
|
import { GENERIC_OAUTH_ERROR_CODES } from "./error-codes.mjs";
|
|
10
11
|
import { BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
11
|
-
import { createAuthorizationURL, validateAuthorizationCode } from "@better-auth/core/oauth2";
|
|
12
|
+
import { applyDefaultAccessTokenExpiry, createAuthorizationURL, validateAuthorizationCode } from "@better-auth/core/oauth2";
|
|
12
13
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
13
14
|
import * as z from "zod";
|
|
14
15
|
import { decodeJwt } from "jose";
|
|
@@ -132,7 +133,7 @@ const oAuth2Callback = (options) => createAuthEndpoint("/oauth2/callback/:provid
|
|
|
132
133
|
}
|
|
133
134
|
}, async (ctx) => {
|
|
134
135
|
const defaultErrorURL = ctx.context.options.onAPIError?.errorURL || `${ctx.context.baseURL}/error`;
|
|
135
|
-
if (ctx.query.error || !ctx.query.code)
|
|
136
|
+
if (ctx.query.error || !ctx.query.code) redirectOnError(ctx, defaultErrorURL, ctx.query.error || "oAuth_code_missing", ctx.query.error_description || void 0);
|
|
136
137
|
const providerId = ctx.params?.providerId;
|
|
137
138
|
if (!providerId) throw APIError$1.from("BAD_REQUEST", GENERIC_OAUTH_ERROR_CODES.PROVIDER_ID_REQUIRED);
|
|
138
139
|
const providerConfig = options.config.find((p) => p.providerId === providerId);
|
|
@@ -140,13 +141,7 @@ const oAuth2Callback = (options) => createAuthEndpoint("/oauth2/callback/:provid
|
|
|
140
141
|
let tokens = void 0;
|
|
141
142
|
const { callbackURL, codeVerifier, errorURL, requestSignUp, newUserURL, link } = await parseState(ctx);
|
|
142
143
|
const code = ctx.query.code;
|
|
143
|
-
|
|
144
|
-
const defaultErrorURL = ctx.context.options.onAPIError?.errorURL || `${ctx.context.baseURL}/error`;
|
|
145
|
-
let url = errorURL || defaultErrorURL;
|
|
146
|
-
if (url.includes("?")) url = `${url}&error=${encodeURIComponent(error)}`;
|
|
147
|
-
else url = `${url}?error=${encodeURIComponent(error)}`;
|
|
148
|
-
throw ctx.redirect(url);
|
|
149
|
-
}
|
|
144
|
+
const resolvedErrorURL = errorURL || defaultErrorURL;
|
|
150
145
|
let finalTokenUrl = providerConfig.tokenUrl;
|
|
151
146
|
let finalUserInfoUrl = providerConfig.userInfoUrl;
|
|
152
147
|
let expectedIssuer = providerConfig.issuer;
|
|
@@ -168,11 +163,11 @@ const oAuth2Callback = (options) => createAuthEndpoint("/oauth2/callback/:provid
|
|
|
168
163
|
expected: expectedIssuer,
|
|
169
164
|
received: ctx.query.iss
|
|
170
165
|
});
|
|
171
|
-
|
|
166
|
+
redirectOnError(ctx, resolvedErrorURL, "issuer_mismatch");
|
|
172
167
|
}
|
|
173
168
|
} else if (providerConfig.requireIssuerValidation) {
|
|
174
169
|
ctx.context.logger.error("OAuth issuer parameter missing", { expected: expectedIssuer });
|
|
175
|
-
|
|
170
|
+
redirectOnError(ctx, resolvedErrorURL, "issuer_missing");
|
|
176
171
|
}
|
|
177
172
|
}
|
|
178
173
|
try {
|
|
@@ -199,25 +194,26 @@ const oAuth2Callback = (options) => createAuthEndpoint("/oauth2/callback/:provid
|
|
|
199
194
|
additionalParams
|
|
200
195
|
});
|
|
201
196
|
}
|
|
197
|
+
tokens = applyDefaultAccessTokenExpiry(tokens, providerConfig.accessTokenExpiresIn);
|
|
202
198
|
} catch (e) {
|
|
203
199
|
ctx.context.logger.error(e && typeof e === "object" && "name" in e ? e.name : "", e);
|
|
204
|
-
|
|
200
|
+
redirectOnError(ctx, resolvedErrorURL, "oauth_code_verification_failed");
|
|
205
201
|
}
|
|
206
202
|
if (!tokens) throw APIError$1.from("BAD_REQUEST", GENERIC_OAUTH_ERROR_CODES.INVALID_OAUTH_CONFIG);
|
|
207
203
|
const userInfo = await (async function handleUserInfo() {
|
|
208
204
|
const userInfo = providerConfig.getUserInfo ? await providerConfig.getUserInfo(tokens) : await getUserInfo(tokens, finalUserInfoUrl);
|
|
209
|
-
if (!userInfo)
|
|
205
|
+
if (!userInfo) redirectOnError(ctx, resolvedErrorURL, "user_info_is_missing");
|
|
210
206
|
const mapUser = providerConfig.mapProfileToUser ? await providerConfig.mapProfileToUser(userInfo) : userInfo;
|
|
211
207
|
const email = mapUser.email ? mapUser.email.toLowerCase() : userInfo.email?.toLowerCase();
|
|
212
208
|
if (!email) {
|
|
213
209
|
ctx.context.logger.error(missingEmailLogMessage(providerConfig.providerId, { source: "generic" }), userInfo);
|
|
214
|
-
|
|
210
|
+
redirectOnError(ctx, resolvedErrorURL, "email_is_missing");
|
|
215
211
|
}
|
|
216
212
|
const id = mapUser.id ? String(mapUser.id) : String(userInfo.id);
|
|
217
213
|
const name = mapUser.name ? mapUser.name : userInfo.name;
|
|
218
214
|
if (!name) {
|
|
219
215
|
ctx.context.logger.error("Unable to get user info", userInfo);
|
|
220
|
-
|
|
216
|
+
redirectOnError(ctx, resolvedErrorURL, "name_is_missing");
|
|
221
217
|
}
|
|
222
218
|
return {
|
|
223
219
|
...userInfo,
|
|
@@ -228,10 +224,10 @@ const oAuth2Callback = (options) => createAuthEndpoint("/oauth2/callback/:provid
|
|
|
228
224
|
};
|
|
229
225
|
})();
|
|
230
226
|
if (link) {
|
|
231
|
-
if (ctx.context.options.account?.accountLinking?.allowDifferentEmails !== true && link.email.toLowerCase() !== userInfo.email.toLowerCase())
|
|
227
|
+
if (ctx.context.options.account?.accountLinking?.allowDifferentEmails !== true && link.email.toLowerCase() !== userInfo.email.toLowerCase()) redirectOnError(ctx, resolvedErrorURL, "email_doesn't_match");
|
|
232
228
|
const existingAccount = await ctx.context.internalAdapter.findAccountByProviderId(String(userInfo.id), providerConfig.providerId);
|
|
233
229
|
if (existingAccount) {
|
|
234
|
-
if (existingAccount.userId !== link.userId)
|
|
230
|
+
if (existingAccount.userId !== link.userId) redirectOnError(ctx, resolvedErrorURL, "account_already_linked_to_different_user");
|
|
235
231
|
const updateData = Object.fromEntries(Object.entries({
|
|
236
232
|
accessToken: await setTokenUtil(tokens.accessToken, ctx.context),
|
|
237
233
|
idToken: tokens.idToken,
|
|
@@ -251,7 +247,7 @@ const oAuth2Callback = (options) => createAuthEndpoint("/oauth2/callback/:provid
|
|
|
251
247
|
scope: tokens.scopes?.join(","),
|
|
252
248
|
refreshToken: await setTokenUtil(tokens.refreshToken, ctx.context),
|
|
253
249
|
idToken: tokens.idToken
|
|
254
|
-
}))
|
|
250
|
+
})) redirectOnError(ctx, resolvedErrorURL, "unable_to_link_account");
|
|
255
251
|
let toRedirectTo;
|
|
256
252
|
try {
|
|
257
253
|
toRedirectTo = callbackURL.toString();
|
|
@@ -260,19 +256,25 @@ const oAuth2Callback = (options) => createAuthEndpoint("/oauth2/callback/:provid
|
|
|
260
256
|
}
|
|
261
257
|
throw ctx.redirect(toRedirectTo);
|
|
262
258
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
259
|
+
let result;
|
|
260
|
+
try {
|
|
261
|
+
result = await handleOAuthUserInfo(ctx, {
|
|
262
|
+
userInfo,
|
|
263
|
+
account: {
|
|
264
|
+
providerId: providerConfig.providerId,
|
|
265
|
+
accountId: userInfo.id,
|
|
266
|
+
...tokens,
|
|
267
|
+
scope: tokens.scopes?.join(",")
|
|
268
|
+
},
|
|
269
|
+
callbackURL,
|
|
270
|
+
disableSignUp: providerConfig.disableImplicitSignUp && !requestSignUp || providerConfig.disableSignUp,
|
|
271
|
+
overrideUserInfo: providerConfig.overrideUserInfo
|
|
272
|
+
});
|
|
273
|
+
} catch (e) {
|
|
274
|
+
if (isAPIError(e) && e.body?.code) redirectOnError(ctx, resolvedErrorURL, e.body.code, e.body.message);
|
|
275
|
+
throw e;
|
|
276
|
+
}
|
|
277
|
+
if (result.error) redirectOnError(ctx, resolvedErrorURL, result.error.split(" ").join("_"));
|
|
276
278
|
const { session, user } = result.data;
|
|
277
279
|
await setSessionCookie(ctx, {
|
|
278
280
|
session,
|
|
@@ -87,6 +87,13 @@ interface GenericOAuthConfig {
|
|
|
87
87
|
* Use "offline" to request a refresh token.
|
|
88
88
|
*/
|
|
89
89
|
accessType?: string | undefined;
|
|
90
|
+
/**
|
|
91
|
+
* Fallback access-token lifetime, in seconds, used only when the provider's
|
|
92
|
+
* token response omits `expires_in`. Set this so `getAccessToken` can track
|
|
93
|
+
* expiry and refresh the token; leave unset if the provider returns
|
|
94
|
+
* `expires_in`.
|
|
95
|
+
*/
|
|
96
|
+
accessTokenExpiresIn?: number | undefined;
|
|
90
97
|
/**
|
|
91
98
|
* Custom function to exchange authorization code for tokens.
|
|
92
99
|
* If provided, this function will be used instead of the default token exchange logic.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import { parseCookies } from "../../cookies/cookie-utils.mjs";
|
|
1
2
|
import { PACKAGE_VERSION } from "../../version.mjs";
|
|
2
3
|
//#region src/plugins/last-login-method/client.ts
|
|
3
4
|
function getCookieValue(name) {
|
|
4
5
|
if (typeof document === "undefined") return null;
|
|
5
|
-
|
|
6
|
-
return cookie ? cookie.split("=")[1] : null;
|
|
6
|
+
return parseCookies(document.cookie).get(name) ?? null;
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
9
9
|
* Client-side plugin to retrieve the last used login method
|
|
@@ -121,7 +121,6 @@ const magicLink = (options) => {
|
|
|
121
121
|
const storedToken = await storeToken(ctx, token);
|
|
122
122
|
const tokenValue = await ctx.context.internalAdapter.consumeVerificationValue(storedToken);
|
|
123
123
|
if (!tokenValue) redirectWithError("INVALID_TOKEN");
|
|
124
|
-
if (tokenValue.expiresAt < /* @__PURE__ */ new Date()) redirectWithError("EXPIRED_TOKEN");
|
|
125
124
|
const { email, name } = JSON.parse(tokenValue.value);
|
|
126
125
|
let isNewUser = false;
|
|
127
126
|
let user = await ctx.context.internalAdapter.findUserByEmail(email).then((res) => res?.user);
|
|
@@ -372,10 +372,6 @@ const mcp = (options) => {
|
|
|
372
372
|
error_description: "invalid code",
|
|
373
373
|
error: "invalid_grant"
|
|
374
374
|
});
|
|
375
|
-
if (verificationValue.expiresAt < /* @__PURE__ */ new Date()) throw new APIError("UNAUTHORIZED", {
|
|
376
|
-
error_description: "code expired",
|
|
377
|
-
error: "invalid_grant"
|
|
378
|
-
});
|
|
379
375
|
if (!client_id) throw new APIError("UNAUTHORIZED", {
|
|
380
376
|
error_description: "client_id is required",
|
|
381
377
|
error: "invalid_client"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseSessionOutput, parseUserOutput } from "../../db/schema.mjs";
|
|
2
|
-
import { SECURE_COOKIE_PREFIX, parseSetCookieHeader } from "../../cookies/cookie-utils.mjs";
|
|
3
|
-
import { deleteSessionCookie, expireCookie,
|
|
2
|
+
import { SECURE_COOKIE_PREFIX, parseCookies, parseSetCookieHeader } from "../../cookies/cookie-utils.mjs";
|
|
3
|
+
import { deleteSessionCookie, expireCookie, setSessionCookie } from "../../cookies/index.mjs";
|
|
4
4
|
import { sessionMiddleware } from "../../api/routes/session.mjs";
|
|
5
5
|
import { APIError } from "../../api/index.mjs";
|
|
6
6
|
import { PACKAGE_VERSION } from "../../version.mjs";
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
+
import { isAPIError } from "../../utils/is-api-error.mjs";
|
|
1
2
|
import { getOrigin } from "../../utils/url.mjs";
|
|
2
3
|
import { originCheck } from "../../api/middlewares/origin-check.mjs";
|
|
3
4
|
import { parseSetCookieHeader } from "../../cookies/cookie-utils.mjs";
|
|
4
5
|
import { symmetricDecrypt, symmetricEncrypt } from "../../crypto/index.mjs";
|
|
5
6
|
import { setSessionCookie } from "../../cookies/index.mjs";
|
|
7
|
+
import { redirectOnError } from "../../oauth2/errors.mjs";
|
|
6
8
|
import { parseGenericState } from "../../state.mjs";
|
|
7
9
|
import { handleOAuthUserInfo } from "../../oauth2/link-account.mjs";
|
|
8
10
|
import { PACKAGE_VERSION } from "../../version.mjs";
|
|
9
11
|
import { parseJSON } from "../../client/parser.mjs";
|
|
10
|
-
import { checkSkipProxy,
|
|
12
|
+
import { checkSkipProxy, resolveCurrentURL, stripTrailingSlash } from "./utils.mjs";
|
|
11
13
|
import { defu } from "defu";
|
|
12
14
|
import { createAuthEndpoint, createAuthMiddleware } from "@better-auth/core/api";
|
|
13
15
|
import * as z from "zod";
|
|
@@ -90,18 +92,28 @@ const oAuthProxy = (opts) => {
|
|
|
90
92
|
throw redirectOnError(ctx, errorURL, "payload_expired");
|
|
91
93
|
}
|
|
92
94
|
try {
|
|
93
|
-
await parseGenericState(ctx, payload.state);
|
|
95
|
+
await parseGenericState(ctx, payload.state, { skipStateCookieCheck: true });
|
|
94
96
|
} catch (e) {
|
|
95
97
|
ctx.context.logger.warn("Failed to clean up OAuth state", e);
|
|
96
98
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
99
|
+
let result;
|
|
100
|
+
try {
|
|
101
|
+
result = await handleOAuthUserInfo(ctx, {
|
|
102
|
+
userInfo: payload.userInfo,
|
|
103
|
+
account: payload.account,
|
|
104
|
+
callbackURL: payload.callbackURL,
|
|
105
|
+
disableSignUp: payload.disableSignUp
|
|
106
|
+
});
|
|
107
|
+
} catch (e) {
|
|
108
|
+
if (isAPIError(e) && e.body?.code) throw redirectOnError(ctx, errorURL, e.body.code, e.body.message);
|
|
109
|
+
throw e;
|
|
110
|
+
}
|
|
111
|
+
if (result.error) {
|
|
112
|
+
ctx.context.logger.error("OAuth proxy callback error", result.error);
|
|
113
|
+
throw redirectOnError(ctx, errorURL, result.error.split(" ").join("_"));
|
|
114
|
+
}
|
|
115
|
+
if (!result.data) {
|
|
116
|
+
ctx.context.logger.error("OAuth proxy callback missing session data");
|
|
105
117
|
throw redirectOnError(ctx, errorURL, "user_creation_failed");
|
|
106
118
|
}
|
|
107
119
|
await setSessionCookie(ctx, result.data);
|
|
@@ -141,6 +153,7 @@ const oAuthProxy = (opts) => {
|
|
|
141
153
|
data: state
|
|
142
154
|
}));
|
|
143
155
|
} catch {
|
|
156
|
+
ctx.context.logger.debug("OAuth proxy: could not decrypt state package, falling back to regular callback");
|
|
144
157
|
return;
|
|
145
158
|
}
|
|
146
159
|
if (!statePackage.isOAuthProxy || !statePackage.state || !statePackage.stateCookie) {
|
|
@@ -248,29 +261,29 @@ const oAuthProxy = (opts) => {
|
|
|
248
261
|
const oauthURL = new URL(providerURL);
|
|
249
262
|
const originalState = oauthURL.searchParams.get("state");
|
|
250
263
|
if (!originalState) return;
|
|
251
|
-
let stateCookieValue;
|
|
252
|
-
if (ctx.context.oauthConfig.storeStateStrategy === "cookie") {
|
|
253
|
-
const setCookieHeader = ctx.context.responseHeaders?.get("set-cookie");
|
|
254
|
-
if (setCookieHeader) {
|
|
255
|
-
const parsedCookies = parseSetCookieHeader(setCookieHeader);
|
|
256
|
-
const stateCookie = ctx.context.createAuthCookie("oauth_state");
|
|
257
|
-
stateCookieValue = parsedCookies.get(stateCookie.name)?.value;
|
|
258
|
-
}
|
|
259
|
-
} else {
|
|
260
|
-
const verification = await ctx.context.internalAdapter.findVerificationValue(originalState);
|
|
261
|
-
if (verification) stateCookieValue = await symmetricEncrypt({
|
|
262
|
-
key: getEncryptionKey(ctx),
|
|
263
|
-
data: verification.value
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
if (!stateCookieValue) {
|
|
267
|
-
ctx.context.logger.warn("No OAuth state cookie value found");
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
264
|
try {
|
|
265
|
+
let plaintextState;
|
|
266
|
+
if (ctx.context.oauthConfig.storeStateStrategy === "cookie") {
|
|
267
|
+
const setCookieHeader = ctx.context.responseHeaders?.get("set-cookie");
|
|
268
|
+
if (setCookieHeader) {
|
|
269
|
+
const oauthStateCookie = ctx.context.createAuthCookie("oauth_state");
|
|
270
|
+
const encryptedCookieValue = parseSetCookieHeader(setCookieHeader).get(oauthStateCookie.name)?.value;
|
|
271
|
+
if (encryptedCookieValue) plaintextState = await symmetricDecrypt({
|
|
272
|
+
key: ctx.context.secretConfig,
|
|
273
|
+
data: encryptedCookieValue
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
} else plaintextState = (await ctx.context.internalAdapter.findVerificationValue(originalState))?.value;
|
|
277
|
+
if (!plaintextState) {
|
|
278
|
+
ctx.context.logger.warn("No OAuth state found for proxy");
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
271
281
|
const statePackage = {
|
|
272
282
|
state: originalState,
|
|
273
|
-
stateCookie:
|
|
283
|
+
stateCookie: await symmetricEncrypt({
|
|
284
|
+
key: getEncryptionKey(ctx),
|
|
285
|
+
data: plaintextState
|
|
286
|
+
}),
|
|
274
287
|
isOAuthProxy: true
|
|
275
288
|
};
|
|
276
289
|
const encryptedPackage = await symmetricEncrypt({
|
|
@@ -283,7 +296,7 @@ const oAuthProxy = (opts) => {
|
|
|
283
296
|
url: oauthURL.toString()
|
|
284
297
|
};
|
|
285
298
|
} catch (e) {
|
|
286
|
-
ctx.context.logger.error("Failed to
|
|
299
|
+
ctx.context.logger.error("Failed to prepare OAuth proxy state:", e);
|
|
287
300
|
}
|
|
288
301
|
})
|
|
289
302
|
}, {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getOrigin } from "../../utils/url.mjs";
|
|
1
|
+
import { getOrigin, trimTrailingSlashes } from "../../utils/url.mjs";
|
|
2
2
|
import { env } from "@better-auth/core/env";
|
|
3
3
|
//#region src/plugins/oauth-proxy/utils.ts
|
|
4
4
|
/**
|
|
@@ -6,7 +6,7 @@ import { env } from "@better-auth/core/env";
|
|
|
6
6
|
*/
|
|
7
7
|
function stripTrailingSlash(url) {
|
|
8
8
|
if (!url) return "";
|
|
9
|
-
return url
|
|
9
|
+
return trimTrailingSlashes(url);
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
12
|
* Get base URL from vendor-specific environment variables
|
|
@@ -37,12 +37,5 @@ function checkSkipProxy(ctx, opts) {
|
|
|
37
37
|
if (!currentURL) return false;
|
|
38
38
|
return getOrigin(productionURL) === getOrigin(currentURL);
|
|
39
39
|
}
|
|
40
|
-
/**
|
|
41
|
-
* Redirect to error URL with error code
|
|
42
|
-
*/
|
|
43
|
-
function redirectOnError(ctx, errorURL, error) {
|
|
44
|
-
const sep = errorURL.includes("?") ? "&" : "?";
|
|
45
|
-
throw ctx.redirect(`${errorURL}${sep}error=${error}`);
|
|
46
|
-
}
|
|
47
40
|
//#endregion
|
|
48
|
-
export { checkSkipProxy,
|
|
41
|
+
export { checkSkipProxy, resolveCurrentURL, stripTrailingSlash };
|
|
@@ -499,10 +499,6 @@ const oidcProvider = (options) => {
|
|
|
499
499
|
error_description: "invalid code",
|
|
500
500
|
error: "invalid_grant"
|
|
501
501
|
});
|
|
502
|
-
if (verificationValue.expiresAt < /* @__PURE__ */ new Date()) throw new APIError("UNAUTHORIZED", {
|
|
503
|
-
error_description: "code expired",
|
|
504
|
-
error: "invalid_grant"
|
|
505
|
-
});
|
|
506
502
|
if (!client_id) throw new APIError("UNAUTHORIZED", {
|
|
507
503
|
error_description: "client_id is required",
|
|
508
504
|
error: "invalid_client"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { db_exports } from "../../db/index.mjs";
|
|
2
2
|
import { getEndpoints } from "../../api/index.mjs";
|
|
3
3
|
import * as z from "zod";
|
|
4
|
+
import { toPascalCase } from "@better-auth/core/utils/string";
|
|
4
5
|
//#region src/plugins/open-api/generator.ts
|
|
5
6
|
const allowedType = new Set([
|
|
6
7
|
"string",
|
|
@@ -156,7 +157,7 @@ function getResponse(responses) {
|
|
|
156
157
|
} } },
|
|
157
158
|
description: "Internal Server Error. This is a problem with the server that you cannot fix."
|
|
158
159
|
},
|
|
159
|
-
...responses
|
|
160
|
+
...responses ? structuredClone(responses) : {}
|
|
160
161
|
};
|
|
161
162
|
}
|
|
162
163
|
function toOpenApiPath(path) {
|
|
@@ -196,6 +197,16 @@ async function generator(ctx, options) {
|
|
|
196
197
|
return acc;
|
|
197
198
|
}, {}) } };
|
|
198
199
|
const paths = {};
|
|
200
|
+
const seenOperationIds = /* @__PURE__ */ new Set();
|
|
201
|
+
const uniqueOperationId = (operationId, method) => {
|
|
202
|
+
if (!operationId) return void 0;
|
|
203
|
+
const base = seenOperationIds.has(operationId) ? `${operationId}${toPascalCase(method)}` : operationId;
|
|
204
|
+
let result = base;
|
|
205
|
+
let n = 2;
|
|
206
|
+
while (seenOperationIds.has(result)) result = `${base}${n++}`;
|
|
207
|
+
seenOperationIds.add(result);
|
|
208
|
+
return result;
|
|
209
|
+
};
|
|
199
210
|
Object.entries(baseEndpoints.api).forEach(([_, value]) => {
|
|
200
211
|
if (!value.path || ctx.options.disabledPaths?.includes(value.path)) return;
|
|
201
212
|
const options = value.options;
|
|
@@ -207,7 +218,7 @@ async function generator(ctx, options) {
|
|
|
207
218
|
[method.toLowerCase()]: {
|
|
208
219
|
tags: ["Default", ...options.metadata?.openapi?.tags || []],
|
|
209
220
|
description: options.metadata?.openapi?.description,
|
|
210
|
-
operationId: options.metadata?.openapi?.operationId,
|
|
221
|
+
operationId: uniqueOperationId(options.metadata?.openapi?.operationId, method),
|
|
211
222
|
security: [{ bearerAuth: [] }],
|
|
212
223
|
parameters: getParameters(options),
|
|
213
224
|
responses: getResponse(options.metadata?.openapi?.responses)
|
|
@@ -220,7 +231,7 @@ async function generator(ctx, options) {
|
|
|
220
231
|
[method.toLowerCase()]: {
|
|
221
232
|
tags: ["Default", ...options.metadata?.openapi?.tags || []],
|
|
222
233
|
description: options.metadata?.openapi?.description,
|
|
223
|
-
operationId: options.metadata?.openapi?.operationId,
|
|
234
|
+
operationId: uniqueOperationId(options.metadata?.openapi?.operationId, method),
|
|
224
235
|
security: [{ bearerAuth: [] }],
|
|
225
236
|
parameters: getParameters(options),
|
|
226
237
|
...body ? { requestBody: body } : { requestBody: { content: { "application/json": { schema: {
|
|
@@ -253,7 +264,7 @@ async function generator(ctx, options) {
|
|
|
253
264
|
[method.toLowerCase()]: {
|
|
254
265
|
tags: options.metadata?.openapi?.tags || [plugin.id.charAt(0).toUpperCase() + plugin.id.slice(1)],
|
|
255
266
|
description: options.metadata?.openapi?.description,
|
|
256
|
-
operationId: options.metadata?.openapi?.operationId,
|
|
267
|
+
operationId: uniqueOperationId(options.metadata?.openapi?.operationId, method),
|
|
257
268
|
security: [{ bearerAuth: [] }],
|
|
258
269
|
parameters: getParameters(options),
|
|
259
270
|
responses: getResponse(options.metadata?.openapi?.responses)
|
|
@@ -264,7 +275,7 @@ async function generator(ctx, options) {
|
|
|
264
275
|
[method.toLowerCase()]: {
|
|
265
276
|
tags: options.metadata?.openapi?.tags || [plugin.id.charAt(0).toUpperCase() + plugin.id.slice(1)],
|
|
266
277
|
description: options.metadata?.openapi?.description,
|
|
267
|
-
operationId: options.metadata?.openapi?.operationId,
|
|
278
|
+
operationId: uniqueOperationId(options.metadata?.openapi?.operationId, method),
|
|
268
279
|
security: [{ bearerAuth: [] }],
|
|
269
280
|
parameters: getParameters(options),
|
|
270
281
|
requestBody: getRequestBody(options),
|