better-auth 1.7.14 → 1.7.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/dispatch.mjs +1 -1
- package/dist/api/index.d.mts +2 -1
- package/dist/api/index.mjs +3 -2
- package/dist/api/routes/account.mjs +14 -6
- package/dist/api/routes/email-verification.mjs +29 -37
- package/dist/api/routes/sign-in.mjs +2 -2
- package/dist/api/routes/sign-up.mjs +1 -1
- package/dist/api/routes/update-user.mjs +10 -4
- package/dist/api/state/session-source.d.mts +10 -0
- package/dist/api/state/session-source.mjs +28 -0
- package/dist/auth/trusted-origins.mjs +31 -8
- package/dist/client/index.d.mts +2 -1
- package/dist/client/parser.mjs +1 -11
- package/dist/client/vue/index.mjs +13 -7
- package/dist/context/secret-utils.mjs +3 -3
- package/dist/db/internal-adapter.mjs +24 -6
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/oauth2/link-account.d.mts +3 -5
- package/dist/oauth2/link-account.mjs +33 -3
- package/dist/oauth2/state.d.mts +6 -5
- package/dist/package.mjs +1 -1
- package/dist/plugins/admin/access/statement.d.mts +10 -10
- package/dist/plugins/admin/access/statement.mjs +2 -0
- package/dist/plugins/admin/admin.d.mts +6 -4
- package/dist/plugins/admin/admin.mjs +4 -4
- package/dist/plugins/admin/client.d.mts +6 -4
- package/dist/plugins/admin/error-codes.d.mts +2 -0
- package/dist/plugins/admin/error-codes.mjs +3 -1
- package/dist/plugins/admin/routes.mjs +9 -0
- package/dist/plugins/anonymous/index.mjs +2 -1
- package/dist/plugins/device-authorization/routes.mjs +3 -3
- package/dist/plugins/email-otp/routes.mjs +33 -50
- package/dist/plugins/generic-oauth/index.mjs +54 -30
- package/dist/plugins/generic-oauth/providers/hubspot.mjs +5 -2
- package/dist/plugins/index.d.mts +2 -1
- package/dist/plugins/index.mjs +2 -1
- package/dist/plugins/oauth-proxy/index.mjs +1 -1
- package/dist/plugins/one-tap/index.mjs +1 -1
- package/dist/plugins/organization/adapter.mjs +1 -0
- package/dist/plugins/organization/client.d.mts +1 -0
- package/dist/plugins/organization/error-codes.d.mts +1 -0
- package/dist/plugins/organization/error-codes.mjs +1 -0
- package/dist/plugins/organization/has-permission.d.mts +2 -3
- package/dist/plugins/organization/has-permission.mjs +62 -23
- package/dist/plugins/organization/index.d.mts +2 -1
- package/dist/plugins/organization/index.mjs +2 -1
- package/dist/plugins/organization/organization.d.mts +2 -2
- package/dist/plugins/organization/organization.mjs +1 -3
- package/dist/plugins/organization/permission.mjs +3 -3
- package/dist/plugins/organization/roles.d.mts +8 -0
- package/dist/plugins/organization/roles.mjs +26 -0
- package/dist/plugins/organization/routes/crud-access-control.mjs +2 -1
- package/dist/plugins/organization/routes/crud-invites.mjs +22 -11
- package/dist/plugins/organization/routes/crud-members.mjs +10 -14
- package/dist/plugins/organization/routes/crud-org.mjs +12 -3
- package/dist/plugins/phone-number/client.d.mts +2 -0
- package/dist/plugins/phone-number/error-codes.d.mts +2 -0
- package/dist/plugins/phone-number/error-codes.mjs +2 -0
- package/dist/plugins/phone-number/index.d.mts +25 -1
- package/dist/plugins/phone-number/index.mjs +22 -6
- package/dist/plugins/phone-number/routes.mjs +34 -35
- package/dist/plugins/two-factor/index.d.mts +9 -2
- package/dist/plugins/two-factor/index.mjs +73 -26
- package/dist/plugins/two-factor/verify-two-factor.mjs +2 -2
- package/dist/plugins/username/index.mjs +6 -19
- package/dist/state.d.mts +6 -0
- package/dist/state.mjs +5 -2
- package/dist/utils/index.d.mts +2 -2
- package/package.json +9 -9
package/dist/api/dispatch.mjs
CHANGED
|
@@ -243,7 +243,7 @@ async function dispatchAuthEndpoint(endpoint, input) {
|
|
|
243
243
|
});
|
|
244
244
|
if (result instanceof Response) return result;
|
|
245
245
|
internalContext.context.returned = result.response;
|
|
246
|
-
internalContext.context
|
|
246
|
+
mergeResponseHeaders(internalContext.context, result.headers);
|
|
247
247
|
const after = await runAfterHooks(internalContext, afterHooks, endpoint, operationId);
|
|
248
248
|
if (after.response !== void 0) result.response = after.response;
|
|
249
249
|
result.headers = after.headers ?? result.headers;
|
package/dist/api/index.d.mts
CHANGED
|
@@ -21,6 +21,7 @@ import { updateSession } from "./routes/update-session.mjs";
|
|
|
21
21
|
import { changeEmail, changePassword, deleteUser, deleteUserCallback, setPassword, updateUser } from "./routes/update-user.mjs";
|
|
22
22
|
import "./routes/index.mjs";
|
|
23
23
|
import { addOAuthServerContext, getOAuthState } from "./state/oauth.mjs";
|
|
24
|
+
import { markSessionOutsideStore } from "./state/session-source.mjs";
|
|
24
25
|
import { getShouldSkipSessionRefresh, setShouldSkipSessionRefresh } from "./state/should-session-refresh.mjs";
|
|
25
26
|
import { AuthContext, Awaitable, BetterAuthOptions, BetterAuthPlugin } from "@better-auth/core";
|
|
26
27
|
import { InternalLogger } from "@better-auth/core/env";
|
|
@@ -4048,4 +4049,4 @@ export declare const router: <Option extends BetterAuthOptions>(ctx: AuthContext
|
|
|
4048
4049
|
} extends (infer T_2) ? { [K in keyof T_2 as K extends keyof T_1 ? never : K]: T_2[K]; } : never) & T_1> : never : never : never;
|
|
4049
4050
|
};
|
|
4050
4051
|
//#endregion
|
|
4051
|
-
export { APIError, type AuthEndpoint, type AuthMiddleware, type DispatchContext, NO_STORE_HEADERS, accountInfo, addOAuthServerContext, callbackOAuth, changeEmail, changePassword, createAuthEndpoint, createAuthMiddleware, createEmailVerificationToken, deleteUser, deleteUserCallback, dispatchAuthEndpoint, error, formCsrfMiddleware, freshSessionMiddleware, getAccessToken, getAuthoritativeSessionFromCtx, getIP, getOAuthState, getSession, getSessionFromCtx, getShouldSkipSessionRefresh, isAPIError, isStateful, linkSocialAccount, listSessions, listUserAccounts, ok, optionsMiddleware, originCheck, originCheckMiddleware, refreshToken, requestOnlySessionMiddleware, requestPasswordReset, requestPasswordResetCallback, requireOrgRole, requireResourceOwnership, resetPassword, revokeOtherSessions, revokeSession, revokeSessions, sendVerificationEmail, sendVerificationEmailFn, sensitiveSessionMiddleware, sessionMiddleware, setPassword, setShouldSkipSessionRefresh, signInEmail, signInSocial, signOut, signUpEmail, unlinkAccount, updateSession, updateUser, verifyEmail, verifyPassword };
|
|
4052
|
+
export { APIError, type AuthEndpoint, type AuthMiddleware, type DispatchContext, NO_STORE_HEADERS, accountInfo, addOAuthServerContext, callbackOAuth, changeEmail, changePassword, createAuthEndpoint, createAuthMiddleware, createEmailVerificationToken, deleteUser, deleteUserCallback, dispatchAuthEndpoint, error, formCsrfMiddleware, freshSessionMiddleware, getAccessToken, getAuthoritativeSessionFromCtx, getIP, getOAuthState, getSession, getSessionFromCtx, getShouldSkipSessionRefresh, isAPIError, isStateful, linkSocialAccount, listSessions, listUserAccounts, markSessionOutsideStore, ok, optionsMiddleware, originCheck, originCheckMiddleware, refreshToken, requestOnlySessionMiddleware, requestPasswordReset, requestPasswordResetCallback, requireOrgRole, requireResourceOwnership, resetPassword, revokeOtherSessions, revokeSession, revokeSessions, sendVerificationEmail, sendVerificationEmailFn, sensitiveSessionMiddleware, sessionMiddleware, setPassword, setShouldSkipSessionRefresh, signInEmail, signInSocial, signOut, signUpEmail, unlinkAccount, updateSession, updateUser, verifyEmail, verifyPassword };
|
package/dist/api/index.mjs
CHANGED
|
@@ -2,9 +2,9 @@ import { isAPIError } from "../utils/is-api-error.mjs";
|
|
|
2
2
|
import { requireOrgRole, requireResourceOwnership } from "./middlewares/authorization.mjs";
|
|
3
3
|
import { formCsrfMiddleware, originCheck, originCheckMiddleware } from "./middlewares/origin-check.mjs";
|
|
4
4
|
import { onRequestRateLimit } from "./rate-limiter/index.mjs";
|
|
5
|
-
import { addOAuthServerContext, getOAuthState } from "./state/oauth.mjs";
|
|
6
5
|
import { getShouldSkipSessionRefresh, setShouldSkipSessionRefresh } from "./state/should-session-refresh.mjs";
|
|
7
6
|
import { freshSessionMiddleware, getAuthoritativeSessionFromCtx, getSession, getSessionFromCtx, isStateful, listSessions, requestOnlySessionMiddleware, revokeOtherSessions, revokeSession, revokeSessions, sensitiveSessionMiddleware, sessionMiddleware } from "./routes/session.mjs";
|
|
7
|
+
import { addOAuthServerContext, getOAuthState } from "./state/oauth.mjs";
|
|
8
8
|
import { accountInfo, getAccessToken, linkSocialAccount, listUserAccounts, refreshToken, unlinkAccount } from "./routes/account.mjs";
|
|
9
9
|
import { callbackOAuth } from "./routes/callback.mjs";
|
|
10
10
|
import { createEmailVerificationToken, sendVerificationEmail, sendVerificationEmailFn, verifyEmail } from "./routes/email-verification.mjs";
|
|
@@ -18,6 +18,7 @@ import { updateSession } from "./routes/update-session.mjs";
|
|
|
18
18
|
import { changeEmail, changePassword, deleteUser, deleteUserCallback, setPassword, updateUser } from "./routes/update-user.mjs";
|
|
19
19
|
import { dispatchAuthEndpoint } from "./dispatch.mjs";
|
|
20
20
|
import { toAuthEndpoints } from "./to-auth-endpoints.mjs";
|
|
21
|
+
import { markSessionOutsideStore } from "./state/session-source.mjs";
|
|
21
22
|
import { logger } from "@better-auth/core/env";
|
|
22
23
|
import { APIError } from "@better-auth/core/error";
|
|
23
24
|
import { getIP } from "@better-auth/core/utils/ip";
|
|
@@ -222,4 +223,4 @@ const router = (ctx, options) => {
|
|
|
222
223
|
});
|
|
223
224
|
};
|
|
224
225
|
//#endregion
|
|
225
|
-
export { APIError, NO_STORE_HEADERS, accountInfo, addOAuthServerContext, callbackOAuth, changeEmail, changePassword, checkEndpointConflicts, createAuthEndpoint, createAuthMiddleware, createEmailVerificationToken, deleteUser, deleteUserCallback, dispatchAuthEndpoint, error, formCsrfMiddleware, freshSessionMiddleware, getAccessToken, getAuthoritativeSessionFromCtx, getEndpoints, getIP, getOAuthState, getSession, getSessionFromCtx, getShouldSkipSessionRefresh, isAPIError, isStateful, linkSocialAccount, listSessions, listUserAccounts, ok, optionsMiddleware, originCheck, originCheckMiddleware, refreshToken, requestOnlySessionMiddleware, requestPasswordReset, requestPasswordResetCallback, requireOrgRole, requireResourceOwnership, resetPassword, revokeOtherSessions, revokeSession, revokeSessions, router, sendVerificationEmail, sendVerificationEmailFn, sensitiveSessionMiddleware, sessionMiddleware, setPassword, setShouldSkipSessionRefresh, signInEmail, signInSocial, signOut, signUpEmail, unlinkAccount, updateSession, updateUser, verifyEmail, verifyPassword };
|
|
226
|
+
export { APIError, NO_STORE_HEADERS, accountInfo, addOAuthServerContext, callbackOAuth, changeEmail, changePassword, checkEndpointConflicts, createAuthEndpoint, createAuthMiddleware, createEmailVerificationToken, deleteUser, deleteUserCallback, dispatchAuthEndpoint, error, formCsrfMiddleware, freshSessionMiddleware, getAccessToken, getAuthoritativeSessionFromCtx, getEndpoints, getIP, getOAuthState, getSession, getSessionFromCtx, getShouldSkipSessionRefresh, isAPIError, isStateful, linkSocialAccount, listSessions, listUserAccounts, markSessionOutsideStore, ok, optionsMiddleware, originCheck, originCheckMiddleware, refreshToken, requestOnlySessionMiddleware, requestPasswordReset, requestPasswordResetCallback, requireOrgRole, requireResourceOwnership, resetPassword, revokeOtherSessions, revokeSession, revokeSessions, router, sendVerificationEmail, sendVerificationEmailFn, sensitiveSessionMiddleware, sessionMiddleware, setPassword, setShouldSkipSessionRefresh, signInEmail, signInSocial, signOut, signUpEmail, unlinkAccount, updateSession, updateUser, verifyEmail, verifyPassword };
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { shouldBindAccountCookieToSessionUser } from "../../context/store-capabilities.mjs";
|
|
2
2
|
import { parseAccountOutput } from "../../db/schema.mjs";
|
|
3
|
+
import { symmetricEncrypt } from "../../crypto/index.mjs";
|
|
3
4
|
import { getAccountCookie, setAccountCookie } from "../../cookies/session-store.mjs";
|
|
4
5
|
import { getAwaitableValue } from "../../context/helpers.mjs";
|
|
6
|
+
import { derivePurposeKey } from "../../crypto/purpose.mjs";
|
|
5
7
|
import { resolveOAuthAccountKeyForAPI } from "../../oauth2/account-key.mjs";
|
|
6
8
|
import { missingEmailLogMessage } from "../../oauth2/errors.mjs";
|
|
9
|
+
import { freshSessionMiddleware, getSessionFromCtx, isStateful, sensitiveSessionMiddleware, sessionMiddleware } from "./session.mjs";
|
|
10
|
+
import { toBoolean } from "../../utils/boolean.mjs";
|
|
7
11
|
import { decryptOAuthToken, getOAuthCallbackPath, setTokenUtil } from "../../oauth2/utils.mjs";
|
|
8
12
|
import { applyUpdateUserInfoOnLink } from "../../oauth2/link-account.mjs";
|
|
9
13
|
import { generateIdTokenNonce, generateState } from "../../oauth2/state.mjs";
|
|
10
|
-
import { freshSessionMiddleware, getSessionFromCtx, isStateful, sessionMiddleware } from "./session.mjs";
|
|
11
14
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
12
15
|
import { additionalAuthorizationParamsSchema, supportsIdTokenSignIn, verifyProviderIdToken } from "@better-auth/core/oauth2";
|
|
13
16
|
import { SocialProviderListEnum } from "@better-auth/core/social-providers";
|
|
@@ -129,7 +132,7 @@ const linkSocialAccount = createAuthEndpoint("/link-social", {
|
|
|
129
132
|
*/
|
|
130
133
|
additionalData: z.record(z.string(), z.any()).optional()
|
|
131
134
|
}),
|
|
132
|
-
use: [
|
|
135
|
+
use: [sensitiveSessionMiddleware],
|
|
133
136
|
metadata: { openapi: {
|
|
134
137
|
description: "Link a social account to the user",
|
|
135
138
|
operationId: "linkSocialAccount",
|
|
@@ -169,11 +172,12 @@ const linkSocialAccount = createAuthEndpoint("/link-social", {
|
|
|
169
172
|
c.context.logger.warn("Invalid id token", { provider: c.body.provider });
|
|
170
173
|
throw APIError.from("UNAUTHORIZED", BASE_ERROR_CODES.INVALID_TOKEN);
|
|
171
174
|
}
|
|
172
|
-
const
|
|
175
|
+
const oauthTokens = {
|
|
173
176
|
idToken: token,
|
|
174
177
|
accessToken: c.body.idToken.accessToken,
|
|
175
178
|
refreshToken: c.body.idToken.refreshToken
|
|
176
|
-
}
|
|
179
|
+
};
|
|
180
|
+
const linkingUserInfo = provider.getUserInfoFromIdToken ? await provider.getUserInfoFromIdToken(token) : await provider.getUserInfo(oauthTokens);
|
|
177
181
|
if (!linkingUserInfo || !linkingUserInfo?.user) {
|
|
178
182
|
c.context.logger.error("Failed to get user info", { provider: c.body.provider });
|
|
179
183
|
throw APIError.from("UNAUTHORIZED", BASE_ERROR_CODES.FAILED_TO_GET_USER_INFO);
|
|
@@ -204,7 +208,7 @@ const linkSocialAccount = createAuthEndpoint("/link-social", {
|
|
|
204
208
|
});
|
|
205
209
|
}
|
|
206
210
|
if (linkedAccount) throw APIError.from("CONFLICT", BASE_ERROR_CODES.SOCIAL_ACCOUNT_ALREADY_LINKED);
|
|
207
|
-
if (!c.context.trustedProviders.includes(provider.id) && !linkingUserInfo.user.emailVerified || c.context.options.account?.accountLinking?.enabled === false) throw APIError.from("UNAUTHORIZED", {
|
|
211
|
+
if (!c.context.trustedProviders.includes(provider.id) && !toBoolean(linkingUserInfo.user.emailVerified) || c.context.options.account?.accountLinking?.enabled === false) throw APIError.from("UNAUTHORIZED", {
|
|
208
212
|
message: "Account not linked - linking not allowed",
|
|
209
213
|
code: "LINKING_NOT_ALLOWED"
|
|
210
214
|
});
|
|
@@ -237,7 +241,11 @@ const linkSocialAccount = createAuthEndpoint("/link-social", {
|
|
|
237
241
|
const state = await generateState(c, {
|
|
238
242
|
link: {
|
|
239
243
|
userId: session.user.id,
|
|
240
|
-
email: session.user.email
|
|
244
|
+
email: session.user.email,
|
|
245
|
+
encryptedSessionToken: await symmetricEncrypt({
|
|
246
|
+
key: derivePurposeKey(c.context.secretConfig, "oauth-link-session-token"),
|
|
247
|
+
data: session.session.token
|
|
248
|
+
})
|
|
241
249
|
},
|
|
242
250
|
additionalData: c.body.additionalData,
|
|
243
251
|
idTokenNonce
|
|
@@ -26,7 +26,7 @@ async function sendVerificationEmailFn(ctx, user) {
|
|
|
26
26
|
ctx.context.logger.error("Verification email isn't enabled.");
|
|
27
27
|
throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.VERIFICATION_EMAIL_NOT_ENABLED);
|
|
28
28
|
}
|
|
29
|
-
const token = await createEmailVerificationToken(ctx.context.secret, user.email, void 0, ctx.context.options.emailVerification?.expiresIn);
|
|
29
|
+
const token = await createEmailVerificationToken(ctx.context.secret, user.email, void 0, ctx.context.options.emailVerification?.expiresIn, { userId: user.id });
|
|
30
30
|
const callbackURL = ctx.body.callbackURL ? encodeURIComponent(ctx.body.callbackURL) : encodeURIComponent("/");
|
|
31
31
|
const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${callbackURL}`;
|
|
32
32
|
await ctx.context.options.emailVerification.sendVerificationEmail({
|
|
@@ -183,24 +183,30 @@ const verifyEmail = createAuthEndpoint("/verify-email", {
|
|
|
183
183
|
const parsed = z.object({
|
|
184
184
|
email: z.email(),
|
|
185
185
|
updateTo: z.string().optional(),
|
|
186
|
-
requestType: z.string().optional()
|
|
186
|
+
requestType: z.string().optional(),
|
|
187
|
+
userId: z.string().optional()
|
|
187
188
|
}).parse(jwt.payload);
|
|
188
|
-
const user = await ctx.context.internalAdapter.findUserByEmail(parsed.email);
|
|
189
|
-
if (!user) return redirectOnError(BASE_ERROR_CODES.USER_NOT_FOUND);
|
|
190
189
|
if (parsed.updateTo) {
|
|
190
|
+
if (!parsed.userId) return redirectOnError(BASE_ERROR_CODES.INVALID_TOKEN);
|
|
191
|
+
const boundUser = await ctx.context.internalAdapter.findUserById(parsed.userId);
|
|
192
|
+
if (!boundUser) return redirectOnError(BASE_ERROR_CODES.USER_NOT_FOUND);
|
|
193
|
+
if (boundUser.email.toLowerCase() !== parsed.email) return redirectOnError(BASE_ERROR_CODES.INVALID_TOKEN);
|
|
191
194
|
const session = await getSessionFromCtx(ctx);
|
|
192
|
-
if (session && session.user.
|
|
195
|
+
if (session && session.user.id !== boundUser.id) return redirectOnError(BASE_ERROR_CODES.INVALID_USER);
|
|
193
196
|
switch (parsed.requestType) {
|
|
194
197
|
/**
|
|
195
198
|
* User clicks confirmation -> sends verification to new email
|
|
196
199
|
*/
|
|
197
200
|
case "change-email-confirmation": {
|
|
198
|
-
const newToken = await createEmailVerificationToken(ctx.context.secret, parsed.email, parsed.updateTo, ctx.context.options.emailVerification?.expiresIn, {
|
|
201
|
+
const newToken = await createEmailVerificationToken(ctx.context.secret, parsed.email, parsed.updateTo, ctx.context.options.emailVerification?.expiresIn, {
|
|
202
|
+
requestType: "change-email-verification",
|
|
203
|
+
userId: boundUser.id
|
|
204
|
+
});
|
|
199
205
|
const updateCallbackURL = ctx.query.callbackURL ? encodeURIComponent(ctx.query.callbackURL) : encodeURIComponent("/");
|
|
200
206
|
const url = `${ctx.context.baseURL}/verify-email?token=${newToken}&callbackURL=${updateCallbackURL}`;
|
|
201
207
|
if (ctx.context.options.emailVerification?.sendVerificationEmail) await ctx.context.runInBackgroundOrAwait(ctx.context.options.emailVerification.sendVerificationEmail({
|
|
202
208
|
user: {
|
|
203
|
-
...
|
|
209
|
+
...boundUser,
|
|
204
210
|
email: parsed.updateTo
|
|
205
211
|
},
|
|
206
212
|
url,
|
|
@@ -213,26 +219,18 @@ const verifyEmail = createAuthEndpoint("/verify-email", {
|
|
|
213
219
|
* User clicks verification -> updates email
|
|
214
220
|
*/
|
|
215
221
|
case "change-email-verification": {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const newSession = await ctx.context.internalAdapter.createSession(user.user.id);
|
|
219
|
-
if (!newSession) throw APIError.from("INTERNAL_SERVER_ERROR", BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION);
|
|
220
|
-
activeSession = {
|
|
221
|
-
session: newSession,
|
|
222
|
-
user: user.user
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
const updatedUser = await ctx.context.internalAdapter.updateUserByEmail(parsed.email, {
|
|
222
|
+
const emailVerified = boundUser.emailVerified;
|
|
223
|
+
const updatedUser = await ctx.context.internalAdapter.updateUser(boundUser.id, {
|
|
226
224
|
email: parsed.updateTo,
|
|
227
|
-
emailVerified
|
|
225
|
+
emailVerified
|
|
228
226
|
});
|
|
229
|
-
if (ctx.context.options.emailVerification?.afterEmailVerification) await ctx.context.options.emailVerification.afterEmailVerification(updatedUser, ctx.request);
|
|
230
|
-
await setSessionCookie(ctx, {
|
|
231
|
-
session:
|
|
227
|
+
if (emailVerified && ctx.context.options.emailVerification?.afterEmailVerification) await ctx.context.options.emailVerification.afterEmailVerification(updatedUser, ctx.request);
|
|
228
|
+
if (session) await setSessionCookie(ctx, {
|
|
229
|
+
session: session.session,
|
|
232
230
|
user: {
|
|
233
|
-
...
|
|
231
|
+
...session.user,
|
|
234
232
|
email: parsed.updateTo,
|
|
235
|
-
emailVerified
|
|
233
|
+
emailVerified
|
|
236
234
|
}
|
|
237
235
|
});
|
|
238
236
|
if (ctx.query.callbackURL) throw ctx.redirect(ctx.query.callbackURL);
|
|
@@ -248,30 +246,21 @@ const verifyEmail = createAuthEndpoint("/verify-email", {
|
|
|
248
246
|
* - updates email immediately
|
|
249
247
|
*/
|
|
250
248
|
default: {
|
|
251
|
-
|
|
252
|
-
if (!activeSession) {
|
|
253
|
-
const newSession = await ctx.context.internalAdapter.createSession(user.user.id);
|
|
254
|
-
if (!newSession) throw APIError.from("INTERNAL_SERVER_ERROR", BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION);
|
|
255
|
-
activeSession = {
|
|
256
|
-
session: newSession,
|
|
257
|
-
user: user.user
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
const updatedUser = await ctx.context.internalAdapter.updateUserByEmail(parsed.email, {
|
|
249
|
+
const updatedUser = await ctx.context.internalAdapter.updateUser(boundUser.id, {
|
|
261
250
|
email: parsed.updateTo,
|
|
262
251
|
emailVerified: false
|
|
263
252
|
});
|
|
264
|
-
const newToken = await createEmailVerificationToken(ctx.context.secret, parsed.updateTo);
|
|
253
|
+
const newToken = await createEmailVerificationToken(ctx.context.secret, parsed.updateTo, void 0, void 0, { userId: boundUser.id });
|
|
265
254
|
const updateCallbackURL = ctx.query.callbackURL ? encodeURIComponent(ctx.query.callbackURL) : encodeURIComponent("/");
|
|
266
255
|
if (ctx.context.options.emailVerification?.sendVerificationEmail) await ctx.context.runInBackgroundOrAwait(ctx.context.options.emailVerification.sendVerificationEmail({
|
|
267
256
|
user: updatedUser,
|
|
268
257
|
url: `${ctx.context.baseURL}/verify-email?token=${newToken}&callbackURL=${updateCallbackURL}`,
|
|
269
258
|
token: newToken
|
|
270
259
|
}, safeCloneRequest(ctx.request)));
|
|
271
|
-
await setSessionCookie(ctx, {
|
|
272
|
-
session:
|
|
260
|
+
if (session) await setSessionCookie(ctx, {
|
|
261
|
+
session: session.session,
|
|
273
262
|
user: {
|
|
274
|
-
...
|
|
263
|
+
...session.user,
|
|
275
264
|
email: parsed.updateTo,
|
|
276
265
|
emailVerified: false
|
|
277
266
|
}
|
|
@@ -284,6 +273,9 @@ const verifyEmail = createAuthEndpoint("/verify-email", {
|
|
|
284
273
|
}
|
|
285
274
|
}
|
|
286
275
|
}
|
|
276
|
+
const user = await ctx.context.internalAdapter.findUserByEmail(parsed.email);
|
|
277
|
+
if (!user) return redirectOnError(BASE_ERROR_CODES.USER_NOT_FOUND);
|
|
278
|
+
if (parsed.userId && parsed.userId !== user.user.id) return redirectOnError(BASE_ERROR_CODES.INVALID_TOKEN);
|
|
287
279
|
if (user.user.emailVerified) {
|
|
288
280
|
if (ctx.query.callbackURL) throw ctx.redirect(ctx.query.callbackURL);
|
|
289
281
|
return ctx.json({
|
|
@@ -167,7 +167,7 @@ const signInSocial = () => createAuthEndpoint("/sign-in/social", {
|
|
|
167
167
|
refreshToken: c.body.idToken.refreshToken,
|
|
168
168
|
user: c.body.idToken.user
|
|
169
169
|
};
|
|
170
|
-
const userInfo = await provider.getUserInfo(oauthTokens);
|
|
170
|
+
const userInfo = provider.getUserInfoFromIdToken ? await provider.getUserInfoFromIdToken(token) : await provider.getUserInfo(oauthTokens);
|
|
171
171
|
if (!userInfo || !userInfo?.user) {
|
|
172
172
|
c.context.logger.error("Failed to get user info", { provider: c.body.provider });
|
|
173
173
|
throw APIError.from("UNAUTHORIZED", BASE_ERROR_CODES.FAILED_TO_GET_USER_INFO);
|
|
@@ -338,7 +338,7 @@ const signInEmail = () => createAuthEndpoint("/sign-in/email", {
|
|
|
338
338
|
if (ctx.context.options?.emailAndPassword?.requireEmailVerification && !user.emailVerified) {
|
|
339
339
|
if (!ctx.context.options?.emailVerification?.sendVerificationEmail) throw APIError.from("FORBIDDEN", BASE_ERROR_CODES.EMAIL_NOT_VERIFIED);
|
|
340
340
|
if (ctx.context.options?.emailVerification?.sendOnSignIn) {
|
|
341
|
-
const token = await createEmailVerificationToken(ctx.context.secret, user.email, void 0, ctx.context.options.emailVerification?.expiresIn);
|
|
341
|
+
const token = await createEmailVerificationToken(ctx.context.secret, user.email, void 0, ctx.context.options.emailVerification?.expiresIn, { userId: user.id });
|
|
342
342
|
const callbackURL = ctx.body.callbackURL ? encodeURIComponent(ctx.body.callbackURL) : encodeURIComponent("/");
|
|
343
343
|
const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${callbackURL}`;
|
|
344
344
|
await ctx.context.runInBackgroundOrAwait(ctx.context.options.emailVerification.sendVerificationEmail({
|
|
@@ -239,7 +239,7 @@ const signUpEmail = () => createAuthEndpoint("/sign-up/email", {
|
|
|
239
239
|
password: hash
|
|
240
240
|
});
|
|
241
241
|
if (ctx.context.options.emailVerification?.sendOnSignUp ?? ctx.context.options.emailAndPassword.requireEmailVerification) {
|
|
242
|
-
const token = await createEmailVerificationToken(ctx.context.secret, createdUser.email, void 0, ctx.context.options.emailVerification?.expiresIn);
|
|
242
|
+
const token = await createEmailVerificationToken(ctx.context.secret, createdUser.email, void 0, ctx.context.options.emailVerification?.expiresIn, { userId: createdUser.id });
|
|
243
243
|
const callbackURL = body.callbackURL ? encodeURIComponent(body.callbackURL) : encodeURIComponent("/");
|
|
244
244
|
const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${callbackURL}`;
|
|
245
245
|
if (ctx.context.options.emailVerification?.sendVerificationEmail) await ctx.context.runInBackgroundOrAwait(ctx.context.options.emailVerification.sendVerificationEmail({
|
|
@@ -450,7 +450,7 @@ const changeEmail = createAuthEndpoint("/change-email", {
|
|
|
450
450
|
throw APIError.fromStatus("BAD_REQUEST", { message: "Verification email isn't enabled" });
|
|
451
451
|
}
|
|
452
452
|
if (await ctx.context.internalAdapter.findUserByEmail(newEmail)) {
|
|
453
|
-
await createEmailVerificationToken(ctx.context.secret, ctx.context.session.user.email, newEmail, ctx.context.options.emailVerification?.expiresIn);
|
|
453
|
+
await createEmailVerificationToken(ctx.context.secret, ctx.context.session.user.email, newEmail, ctx.context.options.emailVerification?.expiresIn, { userId: ctx.context.session.user.id });
|
|
454
454
|
ctx.context.logger.info("Change email attempt for existing email");
|
|
455
455
|
return ctx.json({ status: true });
|
|
456
456
|
}
|
|
@@ -467,7 +467,7 @@ const changeEmail = createAuthEndpoint("/change-email", {
|
|
|
467
467
|
}
|
|
468
468
|
});
|
|
469
469
|
if (canSendVerification) {
|
|
470
|
-
const token = await createEmailVerificationToken(ctx.context.secret, newEmail, void 0, ctx.context.options.emailVerification?.expiresIn);
|
|
470
|
+
const token = await createEmailVerificationToken(ctx.context.secret, newEmail, void 0, ctx.context.options.emailVerification?.expiresIn, { userId: ctx.context.session.user.id });
|
|
471
471
|
const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${encodeURIComponent(ctx.body.callbackURL || "/")}`;
|
|
472
472
|
await ctx.context.runInBackgroundOrAwait(canSendVerification({
|
|
473
473
|
user: {
|
|
@@ -484,7 +484,10 @@ const changeEmail = createAuthEndpoint("/change-email", {
|
|
|
484
484
|
* If the email is verified, we need to send a verification email
|
|
485
485
|
*/
|
|
486
486
|
if (canSendConfirmation) {
|
|
487
|
-
const token = await createEmailVerificationToken(ctx.context.secret, ctx.context.session.user.email, newEmail, ctx.context.options.emailVerification?.expiresIn, {
|
|
487
|
+
const token = await createEmailVerificationToken(ctx.context.secret, ctx.context.session.user.email, newEmail, ctx.context.options.emailVerification?.expiresIn, {
|
|
488
|
+
requestType: "change-email-confirmation",
|
|
489
|
+
userId: ctx.context.session.user.id
|
|
490
|
+
});
|
|
488
491
|
const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${encodeURIComponent(ctx.body.callbackURL || "/")}`;
|
|
489
492
|
await ctx.context.runInBackgroundOrAwait(canSendConfirmation({
|
|
490
493
|
user: ctx.context.session.user,
|
|
@@ -498,7 +501,10 @@ const changeEmail = createAuthEndpoint("/change-email", {
|
|
|
498
501
|
ctx.context.logger.error("Verification email isn't enabled.");
|
|
499
502
|
throw APIError.fromStatus("BAD_REQUEST", { message: "Verification email isn't enabled" });
|
|
500
503
|
}
|
|
501
|
-
const token = await createEmailVerificationToken(ctx.context.secret, ctx.context.session.user.email, newEmail, ctx.context.options.emailVerification?.expiresIn, {
|
|
504
|
+
const token = await createEmailVerificationToken(ctx.context.secret, ctx.context.session.user.email, newEmail, ctx.context.options.emailVerification?.expiresIn, {
|
|
505
|
+
requestType: "change-email-verification",
|
|
506
|
+
userId: ctx.context.session.user.id
|
|
507
|
+
});
|
|
502
508
|
const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${encodeURIComponent(ctx.body.callbackURL || "/")}`;
|
|
503
509
|
await ctx.context.runInBackgroundOrAwait(canSendVerification({
|
|
504
510
|
user: {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/api/state/session-source.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Marks the request session with `token` as built without a session-store
|
|
4
|
+
* row. Checks that re-read the store to catch a concurrent revocation skip
|
|
5
|
+
* such sessions.
|
|
6
|
+
*
|
|
7
|
+
* @internal Shared by Better Auth packages; not a supported public API.
|
|
8
|
+
*/
|
|
9
|
+
export declare function markSessionOutsideStore(token: string): Promise<void>;
|
|
10
|
+
//#endregion
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { defineRequestState } from "@better-auth/core/context";
|
|
2
|
+
//#region src/api/state/session-source.ts
|
|
3
|
+
/**
|
|
4
|
+
* Tokens of request sessions that a plugin built without a session-store
|
|
5
|
+
* row, such as one authenticated by an API key on every request.
|
|
6
|
+
*/
|
|
7
|
+
const { get: getTokensOutsideStore } = defineRequestState(() => /* @__PURE__ */ new Set());
|
|
8
|
+
/**
|
|
9
|
+
* Marks the request session with `token` as built without a session-store
|
|
10
|
+
* row. Checks that re-read the store to catch a concurrent revocation skip
|
|
11
|
+
* such sessions.
|
|
12
|
+
*
|
|
13
|
+
* @internal Shared by Better Auth packages; not a supported public API.
|
|
14
|
+
*/
|
|
15
|
+
async function markSessionOutsideStore(token) {
|
|
16
|
+
(await getTokensOutsideStore()).add(token);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Whether the request session with `token` was marked by
|
|
20
|
+
* `markSessionOutsideStore`. Outside a request, nothing is marked.
|
|
21
|
+
*
|
|
22
|
+
* @internal Shared by Better Auth packages; not a supported public API.
|
|
23
|
+
*/
|
|
24
|
+
async function isSessionOutsideStore(token) {
|
|
25
|
+
return (await getTokensOutsideStore().catch(() => null))?.has(token) ?? false;
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
export { isSessionOutsideStore, markSessionOutsideStore };
|
|
@@ -4,6 +4,19 @@ import { getHost, getOrigin, getProtocol } from "../utils/url.mjs";
|
|
|
4
4
|
const CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f-\u009f]/;
|
|
5
5
|
const RELATIVE_URL_PARSER_ORIGIN = "https://better-auth.invalid";
|
|
6
6
|
const ENCODED_PATH_SEPARATOR_PATTERN = /%2[fF]|%5[cC]/;
|
|
7
|
+
const IP_WILDCARD_HOST_PATTERN = /^(?=.*\d)[\d*?.]+$/;
|
|
8
|
+
const IPV4_HOST_PATTERN = /^\d{1,3}(?:\.\d{1,3}){3}$/;
|
|
9
|
+
/**
|
|
10
|
+
* A wildcard host made only of digits, dots and wildcards (`192.168.*.*`,
|
|
11
|
+
* `10.*`) names an address range, so it admits only an IPv4 address. A plain
|
|
12
|
+
* `*` would otherwise cross `.` and let `192.168.attacker.com` match. Other
|
|
13
|
+
* hosts keep the documented subdomain matching.
|
|
14
|
+
*/
|
|
15
|
+
const isAllowedIPv4Host = (patternAuthority, authority) => {
|
|
16
|
+
const patternHost = patternAuthority.split(":")[0];
|
|
17
|
+
if (!IP_WILDCARD_HOST_PATTERN.test(patternHost)) return true;
|
|
18
|
+
return IPV4_HOST_PATTERN.test(authority.split(":")[0]);
|
|
19
|
+
};
|
|
7
20
|
/**
|
|
8
21
|
* Resolves `.` and `..` segments in a path after percent-decoding so a
|
|
9
22
|
* path-pinned pattern cannot be bypassed with traversal: e.g.
|
|
@@ -33,7 +46,7 @@ const normalizePath = (path) => {
|
|
|
33
46
|
* host); the path is percent-decoded and resolved so traversal cannot bypass
|
|
34
47
|
* a path-pinned pattern.
|
|
35
48
|
*/
|
|
36
|
-
const parseCustomSchemeOrigin = (value) => {
|
|
49
|
+
const parseCustomSchemeOrigin = (value, wildcardPattern = false) => {
|
|
37
50
|
if (CONTROL_CHARACTER_PATTERN.test(value)) return null;
|
|
38
51
|
const schemeEnd = value.indexOf(":");
|
|
39
52
|
if (schemeEnd <= 0) return null;
|
|
@@ -42,7 +55,7 @@ const parseCustomSchemeOrigin = (value) => {
|
|
|
42
55
|
let authority = "";
|
|
43
56
|
if (rest.startsWith("//")) {
|
|
44
57
|
rest = rest.slice(2);
|
|
45
|
-
const authorityEnd = rest.search(/[/?#]/);
|
|
58
|
+
const authorityEnd = rest.search(wildcardPattern ? /[/#]/ : /[/?#]/);
|
|
46
59
|
if (authorityEnd === -1) {
|
|
47
60
|
authority = rest;
|
|
48
61
|
rest = "";
|
|
@@ -51,7 +64,7 @@ const parseCustomSchemeOrigin = (value) => {
|
|
|
51
64
|
rest = rest.slice(authorityEnd);
|
|
52
65
|
}
|
|
53
66
|
}
|
|
54
|
-
const pathEnd = rest.search(/[?#]/);
|
|
67
|
+
const pathEnd = rest.search(wildcardPattern ? /#/ : /[?#]/);
|
|
55
68
|
const path = normalizePath(pathEnd === -1 ? rest : rest.slice(0, pathEnd));
|
|
56
69
|
return {
|
|
57
70
|
scheme,
|
|
@@ -87,17 +100,27 @@ const isSafeRelativeURL = (value) => {
|
|
|
87
100
|
*/
|
|
88
101
|
const matchesOriginPattern = (url, pattern, settings) => {
|
|
89
102
|
if (url.startsWith("/")) return settings?.allowRelativePaths === true && isSafeRelativeURL(url);
|
|
90
|
-
|
|
91
|
-
|
|
103
|
+
const hasWildcard = pattern.includes("*") || pattern.includes("?");
|
|
104
|
+
if (hasWildcard && !pattern.includes("://")) {
|
|
92
105
|
const host = getHost(url);
|
|
93
106
|
if (!host) return false;
|
|
94
|
-
return wildcardMatch(pattern)(host);
|
|
107
|
+
return wildcardMatch(pattern)(host) && isAllowedIPv4Host(pattern, host);
|
|
95
108
|
}
|
|
96
109
|
const protocol = getProtocol(url);
|
|
97
|
-
if (protocol === "http:" || protocol === "https:" || !protocol)
|
|
110
|
+
if (protocol === "http:" || protocol === "https:" || !protocol) {
|
|
111
|
+
if (!hasWildcard) return pattern === getOrigin(url);
|
|
112
|
+
const origin = getOrigin(url);
|
|
113
|
+
if (origin === null || !wildcardMatch(pattern)(origin)) return false;
|
|
114
|
+
return isAllowedIPv4Host(pattern.slice(pattern.indexOf("://") + 3), origin.slice(origin.indexOf("://") + 3));
|
|
115
|
+
}
|
|
98
116
|
const parsed = parseCustomSchemeOrigin(url);
|
|
99
|
-
const parsedPattern = parseCustomSchemeOrigin(pattern);
|
|
117
|
+
const parsedPattern = parseCustomSchemeOrigin(pattern, hasWildcard);
|
|
100
118
|
if (!parsed || !parsedPattern || parsed.scheme !== parsedPattern.scheme) return false;
|
|
119
|
+
if (hasWildcard) {
|
|
120
|
+
if (parsed.authority.includes("@")) return false;
|
|
121
|
+
if (parsedPattern.authority && !(wildcardMatch(parsedPattern.authority)(parsed.authority) && isAllowedIPv4Host(parsedPattern.authority, parsed.authority))) return false;
|
|
122
|
+
return !parsedPattern.path || wildcardMatch(parsedPattern.path)(parsed.path || "/");
|
|
123
|
+
}
|
|
101
124
|
if (parsedPattern.authority && parsed.authority !== parsedPattern.authority) return false;
|
|
102
125
|
if (!parsedPattern.path) return true;
|
|
103
126
|
return parsed.path === parsedPattern.path || parsed.path.startsWith(`${parsedPattern.path}/`);
|
package/dist/client/index.d.mts
CHANGED
|
@@ -16,7 +16,8 @@ import { OrganizationOptions } from "../plugins/organization/types.mjs";
|
|
|
16
16
|
import { InferInvitation, InferMember, InferOrganization, InferOrganizationRolesFromOption, InferOrganizationZodRolesFromOption, InferTeam, Invitation, InvitationInput, InvitationStatus, Member, MemberInput, Organization, OrganizationInput, OrganizationRole, OrganizationSchema, Team, TeamInput, TeamMember, TeamMemberInput, defaultRolesSchema, invitationSchema, invitationStatus, memberSchema, organizationRoleSchema, organizationSchema, roleSchema, teamMemberSchema, teamSchema } from "../plugins/organization/schema.mjs";
|
|
17
17
|
import { getOrgAdapter } from "../plugins/organization/adapter.mjs";
|
|
18
18
|
import { hasPermission } from "../plugins/organization/has-permission.mjs";
|
|
19
|
-
import {
|
|
19
|
+
import { parseRoles } from "../plugins/organization/roles.mjs";
|
|
20
|
+
import { DefaultOrganizationPlugin, DynamicAccessControlEndpoints, OrganizationCreator, OrganizationEndpoints, OrganizationPlugin, TeamEndpoints, organization } from "../plugins/organization/organization.mjs";
|
|
20
21
|
import "../plugins/organization/index.mjs";
|
|
21
22
|
import { BetterAuthOptions, BetterAuthPlugin } from "@better-auth/core";
|
|
22
23
|
import { DBPrimitive } from "@better-auth/core/db";
|
package/dist/client/parser.mjs
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
1
|
//#region src/client/parser.ts
|
|
2
|
-
const PROTO_POLLUTION_PATTERNS = {
|
|
3
|
-
proto: /"(?:_|\\u0{2}5[Ff]){2}(?:p|\\u0{2}70)(?:r|\\u0{2}72)(?:o|\\u0{2}6[Ff])(?:t|\\u0{2}74)(?:o|\\u0{2}6[Ff])(?:_|\\u0{2}5[Ff]){2}"\s*:/,
|
|
4
|
-
constructor: /"(?:c|\\u0063)(?:o|\\u006[Ff])(?:n|\\u006[Ee])(?:s|\\u0073)(?:t|\\u0074)(?:r|\\u0072)(?:u|\\u0075)(?:c|\\u0063)(?:t|\\u0074)(?:o|\\u006[Ff])(?:r|\\u0072)"\s*:/,
|
|
5
|
-
protoShort: /"__proto__"\s*:/,
|
|
6
|
-
constructorShort: /"constructor"\s*:/
|
|
7
|
-
};
|
|
8
2
|
const JSON_SIGNATURE = /^\s*["[{]|^\s*-?\d{1,16}(\.\d{1,17})?([Ee][+-]?\d+)?\s*$/;
|
|
9
3
|
const SPECIAL_VALUES = {
|
|
10
4
|
true: true,
|
|
@@ -40,14 +34,10 @@ function betterJSONParse(value, options = {}) {
|
|
|
40
34
|
if (strict) throw new SyntaxError("[better-json] Invalid JSON");
|
|
41
35
|
return value;
|
|
42
36
|
}
|
|
43
|
-
if (Object.entries(PROTO_POLLUTION_PATTERNS).some(([key, pattern]) => {
|
|
44
|
-
const matches = pattern.test(trimmed);
|
|
45
|
-
if (matches && warnings) console.warn(`[better-json] Detected potential prototype pollution attempt using ${key} pattern`);
|
|
46
|
-
return matches;
|
|
47
|
-
}) && strict) throw new Error("[better-json] Potential prototype pollution attempt detected");
|
|
48
37
|
try {
|
|
49
38
|
const secureReviver = (key, value) => {
|
|
50
39
|
if (key === "__proto__" || key === "constructor" && value && typeof value === "object" && "prototype" in value) {
|
|
40
|
+
if (strict) throw new Error("[better-json] Potential prototype pollution attempt detected");
|
|
51
41
|
if (warnings) console.warn(`[better-json] Dropping "${key}" key to prevent prototype pollution`);
|
|
52
42
|
return;
|
|
53
43
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { trimTrailingSlashes } from "../../utils/url.mjs";
|
|
1
2
|
import { getClientConfig } from "../config.mjs";
|
|
2
3
|
import { createDynamicPathProxy } from "../proxy.mjs";
|
|
3
4
|
import { useStore } from "./vue-store.mjs";
|
|
@@ -16,18 +17,23 @@ function toHeadersInit(headers) {
|
|
|
16
17
|
}
|
|
17
18
|
function createAuthClient(options) {
|
|
18
19
|
const { baseURL, pluginPathMethods, pluginsActions, pluginsAtoms, hydrateSession, $sessionSignal, $fetch, $store, atomListeners } = getClientConfig(options, false);
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
const baseURLKey = options?.baseURL || "inferred";
|
|
21
|
+
const basePath = options?.basePath ?? "/api/auth";
|
|
22
|
+
const sessionCacheKey = `better-auth:session:${baseURLKey}:${basePath}`;
|
|
23
|
+
const sessionPath = trimTrailingSlashes(basePath.startsWith("/") ? basePath : `/${basePath}`);
|
|
24
|
+
/**
|
|
25
|
+
* Stays relative without an explicit `baseURL` so the SSR session fetch
|
|
26
|
+
* forwards the incoming cookies.
|
|
27
|
+
*
|
|
28
|
+
* @see https://nuxt.com/docs/4.x/getting-started/data-fetching
|
|
29
|
+
*/
|
|
30
|
+
const sessionURL = `${options?.baseURL ? baseURL : sessionPath}/get-session`;
|
|
25
31
|
const resolvedHooks = {};
|
|
26
32
|
for (const [key, value] of Object.entries(pluginsAtoms)) resolvedHooks[getAtomKey(key)] = () => useStore(value);
|
|
27
33
|
function useSession(useFetch) {
|
|
28
34
|
if (useFetch) {
|
|
29
35
|
const sessionSignal = useStore($sessionSignal);
|
|
30
|
-
return useFetch(
|
|
36
|
+
return useFetch(sessionURL, {
|
|
31
37
|
headers: toHeadersInit(options?.fetchOptions?.headers),
|
|
32
38
|
key: sessionCacheKey,
|
|
33
39
|
watch: [sessionSignal]
|
|
@@ -12,12 +12,12 @@ function estimateEntropy(str) {
|
|
|
12
12
|
}
|
|
13
13
|
function parseSecretsEnv(envValue) {
|
|
14
14
|
if (!envValue) return null;
|
|
15
|
-
return envValue.split(",").map((entry) => {
|
|
15
|
+
return envValue.split(",").map((entry, index) => {
|
|
16
16
|
entry = entry.trim();
|
|
17
17
|
const colonIdx = entry.indexOf(":");
|
|
18
|
-
if (colonIdx === -1) throw new BetterAuthError(`Invalid BETTER_AUTH_SECRETS entry
|
|
18
|
+
if (colonIdx === -1) throw new BetterAuthError(`Invalid BETTER_AUTH_SECRETS entry at position ${index + 1}. Expected format: "<version>:<secret>"`);
|
|
19
19
|
const version = parseInt(entry.slice(0, colonIdx), 10);
|
|
20
|
-
if (!Number.isInteger(version) || version < 0) throw new BetterAuthError(`Invalid version in BETTER_AUTH_SECRETS
|
|
20
|
+
if (!Number.isInteger(version) || version < 0) throw new BetterAuthError(`Invalid version in BETTER_AUTH_SECRETS entry at position ${index + 1}. Version must be a non-negative integer.`);
|
|
21
21
|
const value = entry.slice(colonIdx + 1).trim();
|
|
22
22
|
if (!value) throw new BetterAuthError(`Empty secret value for version ${version} in BETTER_AUTH_SECRETS.`);
|
|
23
23
|
return {
|
|
@@ -78,8 +78,18 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
78
78
|
const activeSessions = await secondaryStorage.get(`active-sessions-${userId}`);
|
|
79
79
|
return activeSessions ? safeJSONParse(activeSessions) || [] : [];
|
|
80
80
|
}
|
|
81
|
-
|
|
81
|
+
const revokedBeforeKey = (userId) => `revoked-sessions-before-${userId}`;
|
|
82
|
+
const revokedBeforeTTL = Math.max(sessionExpiration, 86400, Number(options.plugins?.find((plugin) => plugin.id === "admin")?.options?.impersonationSessionDuration) || 0);
|
|
83
|
+
async function isRevokedSession(session, revokedBeforeByUser) {
|
|
84
|
+
if (!secondaryStorage) return false;
|
|
85
|
+
const revokedBefore = revokedBeforeByUser?.get(session.userId) ?? Number(await secondaryStorage.get(revokedBeforeKey(session.userId)));
|
|
86
|
+
revokedBeforeByUser?.set(session.userId, revokedBefore);
|
|
87
|
+
return new Date(session.createdAt).getTime() < revokedBefore;
|
|
88
|
+
}
|
|
89
|
+
async function deleteCachedUserSessions(userId, sessionReferences, revokedAt) {
|
|
82
90
|
if (!secondaryStorage) return;
|
|
91
|
+
const previousRevokedBefore = Number(await secondaryStorage.get(revokedBeforeKey(userId)));
|
|
92
|
+
await secondaryStorage.set(revokedBeforeKey(userId), String(Math.max(revokedAt, previousRevokedBefore || 0)), revokedBeforeTTL);
|
|
83
93
|
const deletedTokens = new Set(sessionReferences.map((session) => session.token));
|
|
84
94
|
for (const { token } of sessionReferences) await secondaryStorage.delete(token);
|
|
85
95
|
const activeSessionsKey = `active-sessions-${userId}`;
|
|
@@ -97,7 +107,8 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
97
107
|
async function queueCachedUserSessionDeletion(userId, sessionReferences) {
|
|
98
108
|
if (!secondaryStorage) return;
|
|
99
109
|
const references = sessionReferences ?? await getActiveSessionReferences(userId);
|
|
100
|
-
|
|
110
|
+
const revokedAt = Date.now();
|
|
111
|
+
await queueAfterTransactionHook(() => deleteCachedUserSessions(userId, references, revokedAt), { onError(error) {
|
|
101
112
|
logger.error("Failed to delete committed user sessions from secondary storage", error);
|
|
102
113
|
} });
|
|
103
114
|
}
|
|
@@ -194,6 +205,7 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
194
205
|
const now = Date.now();
|
|
195
206
|
const seenTokens = /* @__PURE__ */ new Set();
|
|
196
207
|
const sessions = [];
|
|
208
|
+
const revokedBeforeByUser = /* @__PURE__ */ new Map();
|
|
197
209
|
for (const { token, expiresAt } of list) {
|
|
198
210
|
if (expiresAt <= now || seenTokens.has(token)) continue;
|
|
199
211
|
seenTokens.add(token);
|
|
@@ -201,7 +213,7 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
201
213
|
if (!data) continue;
|
|
202
214
|
try {
|
|
203
215
|
const parsed = typeof data === "string" ? JSON.parse(data) : data;
|
|
204
|
-
if (!parsed?.session) continue;
|
|
216
|
+
if (!parsed?.session || await isRevokedSession(parsed.session, revokedBeforeByUser)) continue;
|
|
205
217
|
sessions.push(parseSessionOutput(ctx.options, {
|
|
206
218
|
...parsed.session,
|
|
207
219
|
expiresAt: new Date(parsed.session.expiresAt)
|
|
@@ -312,7 +324,10 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
312
324
|
});
|
|
313
325
|
const sessionTTL = getTTLSeconds(data.expiresAt, now);
|
|
314
326
|
if (sessionTTL > 0) await secondaryStorage.set(data.token, JSON.stringify({
|
|
315
|
-
session:
|
|
327
|
+
session: {
|
|
328
|
+
...sessionData,
|
|
329
|
+
createdAt: data.createdAt
|
|
330
|
+
},
|
|
316
331
|
user
|
|
317
332
|
}), sessionTTL);
|
|
318
333
|
return sessionData;
|
|
@@ -336,7 +351,7 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
336
351
|
if (!sessionStringified && (!options.session?.storeSessionInDatabase || ctx.options.session?.preserveSessionInDatabase)) return null;
|
|
337
352
|
if (sessionStringified) {
|
|
338
353
|
const s = safeJSONParse(sessionStringified);
|
|
339
|
-
if (!s) return null;
|
|
354
|
+
if (!s || await isRevokedSession(s.session)) return null;
|
|
340
355
|
return {
|
|
341
356
|
session: parseSessionOutput(ctx.options, {
|
|
342
357
|
...s.session,
|
|
@@ -372,11 +387,12 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
372
387
|
const uniqueTokens = [...new Set(sessionTokens)];
|
|
373
388
|
const sessionsByToken = /* @__PURE__ */ new Map();
|
|
374
389
|
const missingTokens = [];
|
|
390
|
+
const revokedBeforeByUser = /* @__PURE__ */ new Map();
|
|
375
391
|
if (secondaryStorage) for (const sessionToken of uniqueTokens) {
|
|
376
392
|
const sessionStringified = await secondaryStorage.get(sessionToken);
|
|
377
393
|
if (sessionStringified) try {
|
|
378
394
|
const s = typeof sessionStringified === "string" ? JSON.parse(sessionStringified) : sessionStringified;
|
|
379
|
-
if (!s) continue;
|
|
395
|
+
if (!s || await isRevokedSession(s.session, revokedBeforeByUser)) continue;
|
|
380
396
|
const expiresAt = new Date(s.session.expiresAt);
|
|
381
397
|
if (options?.onlyActiveSessions && expiresAt <= /* @__PURE__ */ new Date()) continue;
|
|
382
398
|
const session = {
|
|
@@ -590,6 +606,8 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
590
606
|
join: { ...options?.includeAccounts ? { account: true } : {} }
|
|
591
607
|
});
|
|
592
608
|
if (!result) return null;
|
|
609
|
+
const normalize = (value) => value.normalize("NFC").toLowerCase();
|
|
610
|
+
if (normalize(result.email) !== normalize(email)) return null;
|
|
593
611
|
const { account: accounts, ...user } = result;
|
|
594
612
|
return {
|
|
595
613
|
user,
|