better-auth 1.6.11 → 1.6.13
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 +12 -48
- package/dist/api/routes/account.d.mts +2 -23
- package/dist/api/routes/account.mjs +94 -73
- package/dist/api/routes/callback.d.mts +1 -1
- package/dist/api/routes/callback.mjs +39 -42
- package/dist/api/routes/email-verification.d.mts +1 -0
- package/dist/api/routes/email-verification.mjs +4 -3
- package/dist/api/routes/password.mjs +1 -1
- package/dist/api/routes/session.mjs +15 -10
- package/dist/api/routes/sign-in.d.mts +1 -0
- package/dist/api/routes/sign-in.mjs +3 -2
- 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 +7 -7
- package/dist/client/fetch-plugins.mjs +2 -1
- 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/create-context.mjs +10 -14
- 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 +56 -50
- 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/index.d.mts +2 -2
- package/dist/oauth2/index.mjs +3 -3
- package/dist/oauth2/link-account.d.mts +27 -1
- package/dist/oauth2/link-account.mjs +27 -4
- 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/admin/routes.mjs +3 -3
- package/dist/plugins/anonymous/index.mjs +2 -2
- package/dist/plugins/bearer/index.mjs +4 -9
- package/dist/plugins/captcha/index.mjs +2 -2
- package/dist/plugins/email-otp/routes.mjs +1 -1
- 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 +37 -34
- 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 +2 -5
- package/dist/plugins/multi-session/index.mjs +2 -2
- package/dist/plugins/oauth-proxy/index.mjs +45 -32
- package/dist/plugins/oauth-proxy/utils.mjs +3 -10
- package/dist/plugins/oidc-provider/index.mjs +2 -5
- package/dist/plugins/one-tap/client.mjs +9 -2
- package/dist/plugins/one-tap/index.mjs +16 -39
- 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/organization/routes/crud-org.d.mts +4 -4
- package/dist/plugins/organization/routes/crud-org.mjs +2 -2
- package/dist/plugins/organization/types.d.mts +3 -3
- package/dist/plugins/phone-number/routes.mjs +1 -1
- package/dist/plugins/two-factor/backup-codes/index.d.mts +4 -3
- package/dist/plugins/two-factor/client.mjs +2 -1
- 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 +36 -144
- 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
|
@@ -102,7 +102,7 @@ const anonymous = (options) => {
|
|
|
102
102
|
if (options?.disableDeleteAnonymousUser) throw APIError.from("BAD_REQUEST", ANONYMOUS_ERROR_CODES.DELETE_ANONYMOUS_USER_DISABLED);
|
|
103
103
|
if (!session.user.isAnonymous) throw APIError.from("FORBIDDEN", ANONYMOUS_ERROR_CODES.USER_IS_NOT_ANONYMOUS);
|
|
104
104
|
try {
|
|
105
|
-
await ctx.context.internalAdapter.
|
|
105
|
+
await ctx.context.internalAdapter.deleteUserSessions(session.user.id);
|
|
106
106
|
} catch (error) {
|
|
107
107
|
ctx.context.logger.error("Failed to delete anonymous user sessions", error);
|
|
108
108
|
throw APIError.from("INTERNAL_SERVER_ERROR", ANONYMOUS_ERROR_CODES.FAILED_TO_DELETE_ANONYMOUS_USER_SESSIONS);
|
|
@@ -154,7 +154,7 @@ const anonymous = (options) => {
|
|
|
154
154
|
const newSessionIsAnonymous = Boolean(newSessionUser?.isAnonymous);
|
|
155
155
|
if (options?.disableDeleteAnonymousUser || isSameUser || newSessionIsAnonymous) return;
|
|
156
156
|
try {
|
|
157
|
-
await ctx.context.internalAdapter.
|
|
157
|
+
await ctx.context.internalAdapter.deleteUserSessions(session.user.id);
|
|
158
158
|
await ctx.context.internalAdapter.deleteUser(session.user.id);
|
|
159
159
|
} catch (error) {
|
|
160
160
|
ctx.context.logger.error("Failed to clean up anonymous user during post-link cleanup", {
|
|
@@ -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");
|
|
@@ -585,7 +585,7 @@ const resetPasswordEmailOTP = (opts) => createAuthEndpoint("/email-otp/reset-pas
|
|
|
585
585
|
else await ctx.context.internalAdapter.updatePassword(user.user.id, passwordHash);
|
|
586
586
|
if (ctx.context.options.emailAndPassword?.onPasswordReset) await ctx.context.options.emailAndPassword.onPasswordReset({ user: user.user }, ctx.request);
|
|
587
587
|
if (!user.user.emailVerified) await ctx.context.internalAdapter.updateUser(user.user.id, { emailVerified: true });
|
|
588
|
-
if (ctx.context.options.emailAndPassword?.revokeSessionsOnPasswordReset) await ctx.context.internalAdapter.
|
|
588
|
+
if (ctx.context.options.emailAndPassword?.revokeSessionsOnPasswordReset) await ctx.context.internalAdapter.deleteUserSessions(user.user.id);
|
|
589
589
|
return ctx.json({ success: true });
|
|
590
590
|
});
|
|
591
591
|
const requestEmailChangeEmailOTPBodySchema = z.object({
|
|
@@ -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,14 +1,15 @@
|
|
|
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 { generateState, parseState } from "../../oauth2/state.mjs";
|
|
3
|
+
import { missingEmailLogMessage, redirectOnError } from "../../oauth2/errors.mjs";
|
|
4
4
|
import { setTokenUtil } from "../../oauth2/utils.mjs";
|
|
5
|
+
import { applyUpdateUserInfoOnLink, handleOAuthUserInfo } from "../../oauth2/link-account.mjs";
|
|
6
|
+
import { generateState, parseState } from "../../oauth2/state.mjs";
|
|
5
7
|
import { sessionMiddleware } from "../../api/routes/session.mjs";
|
|
6
|
-
import { handleOAuthUserInfo } from "../../oauth2/link-account.mjs";
|
|
7
8
|
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,8 @@ 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");
|
|
251
|
+
await applyUpdateUserInfoOnLink(ctx, link.userId, userInfo);
|
|
255
252
|
let toRedirectTo;
|
|
256
253
|
try {
|
|
257
254
|
toRedirectTo = callbackURL.toString();
|
|
@@ -260,19 +257,25 @@ const oAuth2Callback = (options) => createAuthEndpoint("/oauth2/callback/:provid
|
|
|
260
257
|
}
|
|
261
258
|
throw ctx.redirect(toRedirectTo);
|
|
262
259
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
260
|
+
let result;
|
|
261
|
+
try {
|
|
262
|
+
result = await handleOAuthUserInfo(ctx, {
|
|
263
|
+
userInfo,
|
|
264
|
+
account: {
|
|
265
|
+
providerId: providerConfig.providerId,
|
|
266
|
+
accountId: userInfo.id,
|
|
267
|
+
...tokens,
|
|
268
|
+
scope: tokens.scopes?.join(",")
|
|
269
|
+
},
|
|
270
|
+
callbackURL,
|
|
271
|
+
disableSignUp: providerConfig.disableImplicitSignUp && !requestSignUp || providerConfig.disableSignUp,
|
|
272
|
+
overrideUserInfo: providerConfig.overrideUserInfo
|
|
273
|
+
});
|
|
274
|
+
} catch (e) {
|
|
275
|
+
if (isAPIError(e) && e.body?.code) redirectOnError(ctx, resolvedErrorURL, e.body.code, e.body.message);
|
|
276
|
+
throw e;
|
|
277
|
+
}
|
|
278
|
+
if (result.error) redirectOnError(ctx, resolvedErrorURL, result.error.split(" ").join("_"));
|
|
276
279
|
const { session, user } = result.data;
|
|
277
280
|
await setSessionCookie(ctx, {
|
|
278
281
|
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);
|
|
@@ -14,6 +14,7 @@ import { oidcProvider } from "../oidc-provider/index.mjs";
|
|
|
14
14
|
import { authorizeMCPOAuth } from "./authorize.mjs";
|
|
15
15
|
import { isProduction, logger } from "@better-auth/core/env";
|
|
16
16
|
import { safeJSONParse } from "@better-auth/core/utils/json";
|
|
17
|
+
import { isSafeUrlScheme } from "@better-auth/core/utils/url";
|
|
17
18
|
import { createAuthEndpoint, createAuthMiddleware } from "@better-auth/core/api";
|
|
18
19
|
import * as z from "zod";
|
|
19
20
|
import { base64 } from "@better-auth/utils/base64";
|
|
@@ -86,7 +87,7 @@ const getMCPProtectedResourceMetadata = (ctx, options) => {
|
|
|
86
87
|
};
|
|
87
88
|
};
|
|
88
89
|
const registerMcpClientBodySchema = z.object({
|
|
89
|
-
redirect_uris: z.array(z.string()),
|
|
90
|
+
redirect_uris: z.array(z.string().refine(isSafeUrlScheme, { message: "redirect_uri cannot use a javascript:, data:, or vbscript: scheme" })),
|
|
90
91
|
token_endpoint_auth_method: z.enum([
|
|
91
92
|
"none",
|
|
92
93
|
"client_secret_basic",
|
|
@@ -372,10 +373,6 @@ const mcp = (options) => {
|
|
|
372
373
|
error_description: "invalid code",
|
|
373
374
|
error: "invalid_grant"
|
|
374
375
|
});
|
|
375
|
-
if (verificationValue.expiresAt < /* @__PURE__ */ new Date()) throw new APIError("UNAUTHORIZED", {
|
|
376
|
-
error_description: "code expired",
|
|
377
|
-
error: "invalid_grant"
|
|
378
|
-
});
|
|
379
376
|
if (!client_id) throw new APIError("UNAUTHORIZED", {
|
|
380
377
|
error_description: "client_id is required",
|
|
381
378
|
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";
|
|
6
|
-
import {
|
|
7
|
+
import { redirectOnError } from "../../oauth2/errors.mjs";
|
|
7
8
|
import { handleOAuthUserInfo } from "../../oauth2/link-account.mjs";
|
|
9
|
+
import { parseGenericState } from "../../state.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 };
|
|
@@ -15,6 +15,7 @@ import { authorize } from "./authorize.mjs";
|
|
|
15
15
|
import { schema } from "./schema.mjs";
|
|
16
16
|
import { defaultClientSecretHasher } from "./utils.mjs";
|
|
17
17
|
import { getCurrentAuthContext } from "@better-auth/core/context";
|
|
18
|
+
import { isSafeUrlScheme } from "@better-auth/core/utils/url";
|
|
18
19
|
import { createAuthEndpoint, createAuthMiddleware } from "@better-auth/core/api";
|
|
19
20
|
import { deprecate } from "@better-auth/core/utils/deprecate";
|
|
20
21
|
import * as z from "zod";
|
|
@@ -101,7 +102,7 @@ const oAuthConsentBodySchema = z.object({
|
|
|
101
102
|
});
|
|
102
103
|
const oAuth2TokenBodySchema = z.record(z.any(), z.any());
|
|
103
104
|
const registerOAuthApplicationBodySchema = z.object({
|
|
104
|
-
redirect_uris: z.array(z.string()).meta({ description: "A list of redirect URIs. Eg: [\"https://client.example.com/callback\"]" }),
|
|
105
|
+
redirect_uris: z.array(z.string().refine(isSafeUrlScheme, { message: "redirect_uri cannot use a javascript:, data:, or vbscript: scheme" })).meta({ description: "A list of redirect URIs. Eg: [\"https://client.example.com/callback\"]" }),
|
|
105
106
|
token_endpoint_auth_method: z.enum([
|
|
106
107
|
"none",
|
|
107
108
|
"client_secret_basic",
|
|
@@ -499,10 +500,6 @@ const oidcProvider = (options) => {
|
|
|
499
500
|
error_description: "invalid code",
|
|
500
501
|
error: "invalid_grant"
|
|
501
502
|
});
|
|
502
|
-
if (verificationValue.expiresAt < /* @__PURE__ */ new Date()) throw new APIError("UNAUTHORIZED", {
|
|
503
|
-
error_description: "code expired",
|
|
504
|
-
error: "invalid_grant"
|
|
505
|
-
});
|
|
506
503
|
if (!client_id) throw new APIError("UNAUTHORIZED", {
|
|
507
504
|
error_description: "client_id is required",
|
|
508
505
|
error: "invalid_client"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PACKAGE_VERSION } from "../../version.mjs";
|
|
2
|
+
import { isSafeUrlScheme } from "@better-auth/core/utils/url";
|
|
2
3
|
//#region src/plugins/one-tap/client.ts
|
|
3
4
|
let isRequestInProgress = false;
|
|
4
5
|
function isFedCMSupported() {
|
|
@@ -49,7 +50,10 @@ const oneTapClient = (options) => {
|
|
|
49
50
|
...opts?.fetchOptions,
|
|
50
51
|
...fetchOptions
|
|
51
52
|
});
|
|
52
|
-
if (!opts?.fetchOptions && !fetchOptions || opts?.callbackURL)
|
|
53
|
+
if (!opts?.fetchOptions && !fetchOptions || opts?.callbackURL) {
|
|
54
|
+
const target = opts?.callbackURL ?? "/";
|
|
55
|
+
if (isSafeUrlScheme(target)) window.location.href = target;
|
|
56
|
+
}
|
|
53
57
|
}
|
|
54
58
|
const { autoSelect, cancelOnTapOutside, context } = opts ?? {};
|
|
55
59
|
const contextValue = context ?? options.context ?? "signin";
|
|
@@ -82,7 +86,10 @@ const oneTapClient = (options) => {
|
|
|
82
86
|
...opts?.fetchOptions,
|
|
83
87
|
...fetchOptions
|
|
84
88
|
});
|
|
85
|
-
if (!opts?.fetchOptions && !fetchOptions || opts?.callbackURL)
|
|
89
|
+
if (!opts?.fetchOptions && !fetchOptions || opts?.callbackURL) {
|
|
90
|
+
const target = opts?.callbackURL ?? "/";
|
|
91
|
+
if (isSafeUrlScheme(target)) window.location.href = target;
|
|
92
|
+
}
|
|
86
93
|
}
|
|
87
94
|
const { autoSelect, cancelOnTapOutside, context } = opts ?? {};
|
|
88
95
|
const contextValue = context ?? options.context ?? "signin";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parseUserOutput } from "../../db/schema.mjs";
|
|
2
2
|
import { setSessionCookie } from "../../cookies/index.mjs";
|
|
3
|
+
import { handleOAuthUserInfo } from "../../oauth2/link-account.mjs";
|
|
3
4
|
import { APIError } from "../../api/index.mjs";
|
|
4
5
|
import { PACKAGE_VERSION } from "../../version.mjs";
|
|
5
6
|
import { toBoolean } from "../../utils/boolean.mjs";
|
|
@@ -47,51 +48,27 @@ const oneTap = (options) => ({
|
|
|
47
48
|
}
|
|
48
49
|
const { email: rawEmail, email_verified, name, picture, sub } = payload;
|
|
49
50
|
if (!rawEmail) return ctx.json({ error: "Email not available in token" });
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const newUser = await ctx.context.internalAdapter.createOAuthUser({
|
|
55
|
-
email,
|
|
51
|
+
const result = await handleOAuthUserInfo(ctx, {
|
|
52
|
+
userInfo: {
|
|
53
|
+
id: sub,
|
|
54
|
+
email: rawEmail.toLowerCase(),
|
|
56
55
|
emailVerified: typeof email_verified === "boolean" ? email_verified : toBoolean(email_verified),
|
|
57
|
-
name,
|
|
56
|
+
name: name ?? "",
|
|
58
57
|
image: picture
|
|
59
|
-
},
|
|
60
|
-
|
|
61
|
-
accountId: sub
|
|
62
|
-
});
|
|
63
|
-
if (!newUser) throw new APIError("INTERNAL_SERVER_ERROR", { message: "Could not create user" });
|
|
64
|
-
const session = await ctx.context.internalAdapter.createSession(newUser.user.id);
|
|
65
|
-
await setSessionCookie(ctx, {
|
|
66
|
-
user: newUser.user,
|
|
67
|
-
session
|
|
68
|
-
});
|
|
69
|
-
return ctx.json({
|
|
70
|
-
token: session.token,
|
|
71
|
-
user: parseUserOutput(ctx.context.options, newUser.user)
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
if (!await ctx.context.internalAdapter.findAccount(sub)) {
|
|
75
|
-
const accountLinking = ctx.context.options.account?.accountLinking;
|
|
76
|
-
const providerEmailVerified = typeof email_verified === "boolean" ? email_verified : toBoolean(email_verified);
|
|
77
|
-
const requireLocalEmailVerified = accountLinking?.requireLocalEmailVerified ?? true;
|
|
78
|
-
if (accountLinking?.enabled !== false && accountLinking?.disableImplicitLinking !== true && (!requireLocalEmailVerified || user.user.emailVerified) && (ctx.context.trustedProviders.includes("google") || providerEmailVerified)) await ctx.context.internalAdapter.linkAccount({
|
|
79
|
-
userId: user.user.id,
|
|
58
|
+
},
|
|
59
|
+
account: {
|
|
80
60
|
providerId: "google",
|
|
81
61
|
accountId: sub,
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
const session = await ctx.context.internalAdapter.createSession(user.user.id);
|
|
88
|
-
await setSessionCookie(ctx, {
|
|
89
|
-
user: user.user,
|
|
90
|
-
session
|
|
62
|
+
idToken,
|
|
63
|
+
scope: "openid,profile,email"
|
|
64
|
+
},
|
|
65
|
+
disableSignUp: options?.disableSignup
|
|
91
66
|
});
|
|
67
|
+
if (result.error) throw new APIError("UNAUTHORIZED", { message: result.error });
|
|
68
|
+
await setSessionCookie(ctx, result.data);
|
|
92
69
|
return ctx.json({
|
|
93
|
-
token: session.token,
|
|
94
|
-
user: parseUserOutput(ctx.context.options,
|
|
70
|
+
token: result.data.session.token,
|
|
71
|
+
user: parseUserOutput(ctx.context.options, result.data.user)
|
|
95
72
|
});
|
|
96
73
|
}) },
|
|
97
74
|
options
|