better-auth 1.7.15 → 1.7.17
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 +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 +33 -8
- package/dist/client/index.d.mts +2 -1
- package/dist/cookies/index.mjs +1 -0
- package/dist/db/internal-adapter.mjs +28 -10
- 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 +4 -3
- package/dist/plugins/admin/client.d.mts +5 -4
- package/dist/plugins/admin/error-codes.d.mts +1 -0
- package/dist/plugins/admin/error-codes.mjs +1 -0
- package/dist/plugins/admin/routes.mjs +8 -0
- package/dist/plugins/anonymous/index.mjs +2 -1
- package/dist/plugins/device-authorization/routes.mjs +4 -4
- package/dist/plugins/email-otp/routes.mjs +33 -50
- package/dist/plugins/generic-oauth/index.mjs +54 -30
- 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/backup-codes/index.d.mts +2 -2
- package/dist/plugins/two-factor/index.d.mts +15 -8
- package/dist/plugins/two-factor/index.mjs +74 -26
- package/dist/plugins/two-factor/otp/index.d.mts +2 -2
- package/dist/plugins/two-factor/totp/index.d.mts +2 -2
- package/dist/plugins/two-factor/verify-two-factor.mjs +4 -2
- package/dist/plugins/username/index.mjs +1 -1
- package/dist/state.d.mts +6 -0
- package/dist/state.mjs +5 -2
- package/dist/utils/index.d.mts +2 -2
- package/package.json +11 -11
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 };
|
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
import { wildcardMatch } from "../utils/wildcard.mjs";
|
|
2
2
|
import { getHost, getOrigin, getProtocol } from "../utils/url.mjs";
|
|
3
|
+
import * as z from "zod";
|
|
3
4
|
//#region src/auth/trusted-origins.ts
|
|
4
5
|
const CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f-\u009f]/;
|
|
5
6
|
const RELATIVE_URL_PARSER_ORIGIN = "https://better-auth.invalid";
|
|
6
7
|
const ENCODED_PATH_SEPARATOR_PATTERN = /%2[fF]|%5[cC]/;
|
|
8
|
+
const IP_WILDCARD_HOST_PATTERN = /^(?=.*\d)[\d*?.]+$/;
|
|
9
|
+
const ipv4Schema = z.ipv4();
|
|
10
|
+
/**
|
|
11
|
+
* A wildcard host made only of digits, dots and wildcards (`192.168.*.*`,
|
|
12
|
+
* `10.*`) names an address range, so it admits only an IPv4 address. A plain
|
|
13
|
+
* `*` would otherwise cross `.` and let `192.168.attacker.com` match. Other
|
|
14
|
+
* hosts keep the documented subdomain matching.
|
|
15
|
+
*/
|
|
16
|
+
const isAllowedIPv4Host = (patternAuthority, authority) => {
|
|
17
|
+
const [patternHost = ""] = patternAuthority.split(":");
|
|
18
|
+
if (!IP_WILDCARD_HOST_PATTERN.test(patternHost)) return true;
|
|
19
|
+
const [host = ""] = authority.split(":");
|
|
20
|
+
return z.validate(ipv4Schema, host);
|
|
21
|
+
};
|
|
7
22
|
/**
|
|
8
23
|
* Resolves `.` and `..` segments in a path after percent-decoding so a
|
|
9
24
|
* path-pinned pattern cannot be bypassed with traversal: e.g.
|
|
@@ -33,7 +48,7 @@ const normalizePath = (path) => {
|
|
|
33
48
|
* host); the path is percent-decoded and resolved so traversal cannot bypass
|
|
34
49
|
* a path-pinned pattern.
|
|
35
50
|
*/
|
|
36
|
-
const parseCustomSchemeOrigin = (value) => {
|
|
51
|
+
const parseCustomSchemeOrigin = (value, wildcardPattern = false) => {
|
|
37
52
|
if (CONTROL_CHARACTER_PATTERN.test(value)) return null;
|
|
38
53
|
const schemeEnd = value.indexOf(":");
|
|
39
54
|
if (schemeEnd <= 0) return null;
|
|
@@ -42,7 +57,7 @@ const parseCustomSchemeOrigin = (value) => {
|
|
|
42
57
|
let authority = "";
|
|
43
58
|
if (rest.startsWith("//")) {
|
|
44
59
|
rest = rest.slice(2);
|
|
45
|
-
const authorityEnd = rest.search(/[/?#]/);
|
|
60
|
+
const authorityEnd = rest.search(wildcardPattern ? /[/#]/ : /[/?#]/);
|
|
46
61
|
if (authorityEnd === -1) {
|
|
47
62
|
authority = rest;
|
|
48
63
|
rest = "";
|
|
@@ -51,7 +66,7 @@ const parseCustomSchemeOrigin = (value) => {
|
|
|
51
66
|
rest = rest.slice(authorityEnd);
|
|
52
67
|
}
|
|
53
68
|
}
|
|
54
|
-
const pathEnd = rest.search(/[?#]/);
|
|
69
|
+
const pathEnd = rest.search(wildcardPattern ? /#/ : /[?#]/);
|
|
55
70
|
const path = normalizePath(pathEnd === -1 ? rest : rest.slice(0, pathEnd));
|
|
56
71
|
return {
|
|
57
72
|
scheme,
|
|
@@ -87,17 +102,27 @@ const isSafeRelativeURL = (value) => {
|
|
|
87
102
|
*/
|
|
88
103
|
const matchesOriginPattern = (url, pattern, settings) => {
|
|
89
104
|
if (url.startsWith("/")) return settings?.allowRelativePaths === true && isSafeRelativeURL(url);
|
|
90
|
-
|
|
91
|
-
|
|
105
|
+
const hasWildcard = pattern.includes("*") || pattern.includes("?");
|
|
106
|
+
if (hasWildcard && !pattern.includes("://")) {
|
|
92
107
|
const host = getHost(url);
|
|
93
108
|
if (!host) return false;
|
|
94
|
-
return wildcardMatch(pattern)(host);
|
|
109
|
+
return wildcardMatch(pattern)(host) && isAllowedIPv4Host(pattern, host);
|
|
95
110
|
}
|
|
96
111
|
const protocol = getProtocol(url);
|
|
97
|
-
if (protocol === "http:" || protocol === "https:" || !protocol)
|
|
112
|
+
if (protocol === "http:" || protocol === "https:" || !protocol) {
|
|
113
|
+
if (!hasWildcard) return pattern === getOrigin(url);
|
|
114
|
+
const origin = getOrigin(url);
|
|
115
|
+
if (origin === null || !wildcardMatch(pattern)(origin)) return false;
|
|
116
|
+
return isAllowedIPv4Host(pattern.slice(pattern.indexOf("://") + 3), origin.slice(origin.indexOf("://") + 3));
|
|
117
|
+
}
|
|
98
118
|
const parsed = parseCustomSchemeOrigin(url);
|
|
99
|
-
const parsedPattern = parseCustomSchemeOrigin(pattern);
|
|
119
|
+
const parsedPattern = parseCustomSchemeOrigin(pattern, hasWildcard);
|
|
100
120
|
if (!parsed || !parsedPattern || parsed.scheme !== parsedPattern.scheme) return false;
|
|
121
|
+
if (hasWildcard) {
|
|
122
|
+
if (parsed.authority.includes("@")) return false;
|
|
123
|
+
if (parsedPattern.authority && !(wildcardMatch(parsedPattern.authority)(parsed.authority) && isAllowedIPv4Host(parsedPattern.authority, parsed.authority))) return false;
|
|
124
|
+
return !parsedPattern.path || wildcardMatch(parsedPattern.path)(parsed.path || "/");
|
|
125
|
+
}
|
|
101
126
|
if (parsedPattern.authority && parsed.authority !== parsedPattern.authority) return false;
|
|
102
127
|
if (!parsedPattern.path) return true;
|
|
103
128
|
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/cookies/index.mjs
CHANGED
|
@@ -177,6 +177,7 @@ async function setSessionCookie(ctx, session, dontRememberMe, overrides) {
|
|
|
177
177
|
...overrides
|
|
178
178
|
});
|
|
179
179
|
if (dontRememberMe) await ctx.setSignedCookie(ctx.context.authCookies.dontRememberToken.name, "true", ctx.context.secret, ctx.context.authCookies.dontRememberToken.attributes);
|
|
180
|
+
else if (dontRememberMeCookie) expireCookie(ctx, ctx.context.authCookies.dontRememberToken);
|
|
180
181
|
await setCookieCache(ctx, session, dontRememberMe);
|
|
181
182
|
ctx.context.setNewSession(session);
|
|
182
183
|
}
|
|
@@ -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}`;
|
|
@@ -94,10 +104,10 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
94
104
|
}
|
|
95
105
|
await secondaryStorage.delete(activeSessionsKey);
|
|
96
106
|
}
|
|
97
|
-
async function queueCachedUserSessionDeletion(userId, sessionReferences) {
|
|
107
|
+
async function queueCachedUserSessionDeletion(userId, sessionReferences, revokedAt = Date.now()) {
|
|
98
108
|
if (!secondaryStorage) return;
|
|
99
109
|
const references = sessionReferences ?? await getActiveSessionReferences(userId);
|
|
100
|
-
await queueAfterTransactionHook(() => deleteCachedUserSessions(userId, references), { onError(error) {
|
|
110
|
+
await queueAfterTransactionHook(() => deleteCachedUserSessions(userId, references, revokedAt), { onError(error) {
|
|
101
111
|
logger.error("Failed to delete committed user sessions from secondary storage", error);
|
|
102
112
|
} });
|
|
103
113
|
}
|
|
@@ -194,6 +204,7 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
194
204
|
const now = Date.now();
|
|
195
205
|
const seenTokens = /* @__PURE__ */ new Set();
|
|
196
206
|
const sessions = [];
|
|
207
|
+
const revokedBeforeByUser = /* @__PURE__ */ new Map();
|
|
197
208
|
for (const { token, expiresAt } of list) {
|
|
198
209
|
if (expiresAt <= now || seenTokens.has(token)) continue;
|
|
199
210
|
seenTokens.add(token);
|
|
@@ -201,7 +212,7 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
201
212
|
if (!data) continue;
|
|
202
213
|
try {
|
|
203
214
|
const parsed = typeof data === "string" ? JSON.parse(data) : data;
|
|
204
|
-
if (!parsed?.session) continue;
|
|
215
|
+
if (!parsed?.session || await isRevokedSession(parsed.session, revokedBeforeByUser)) continue;
|
|
205
216
|
sessions.push(parseSessionOutput(ctx.options, {
|
|
206
217
|
...parsed.session,
|
|
207
218
|
expiresAt: new Date(parsed.session.expiresAt)
|
|
@@ -312,7 +323,10 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
312
323
|
});
|
|
313
324
|
const sessionTTL = getTTLSeconds(data.expiresAt, now);
|
|
314
325
|
if (sessionTTL > 0) await secondaryStorage.set(data.token, JSON.stringify({
|
|
315
|
-
session:
|
|
326
|
+
session: {
|
|
327
|
+
...sessionData,
|
|
328
|
+
createdAt: data.createdAt
|
|
329
|
+
},
|
|
316
330
|
user
|
|
317
331
|
}), sessionTTL);
|
|
318
332
|
return sessionData;
|
|
@@ -336,7 +350,7 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
336
350
|
if (!sessionStringified && (!options.session?.storeSessionInDatabase || ctx.options.session?.preserveSessionInDatabase)) return null;
|
|
337
351
|
if (sessionStringified) {
|
|
338
352
|
const s = safeJSONParse(sessionStringified);
|
|
339
|
-
if (!s) return null;
|
|
353
|
+
if (!s || await isRevokedSession(s.session)) return null;
|
|
340
354
|
return {
|
|
341
355
|
session: parseSessionOutput(ctx.options, {
|
|
342
356
|
...s.session,
|
|
@@ -372,11 +386,12 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
372
386
|
const uniqueTokens = [...new Set(sessionTokens)];
|
|
373
387
|
const sessionsByToken = /* @__PURE__ */ new Map();
|
|
374
388
|
const missingTokens = [];
|
|
389
|
+
const revokedBeforeByUser = /* @__PURE__ */ new Map();
|
|
375
390
|
if (secondaryStorage) for (const sessionToken of uniqueTokens) {
|
|
376
391
|
const sessionStringified = await secondaryStorage.get(sessionToken);
|
|
377
392
|
if (sessionStringified) try {
|
|
378
393
|
const s = typeof sessionStringified === "string" ? JSON.parse(sessionStringified) : sessionStringified;
|
|
379
|
-
if (!s) continue;
|
|
394
|
+
if (!s || await isRevokedSession(s.session, revokedBeforeByUser)) continue;
|
|
380
395
|
const expiresAt = new Date(s.session.expiresAt);
|
|
381
396
|
if (options?.onlyActiveSessions && expiresAt <= /* @__PURE__ */ new Date()) continue;
|
|
382
397
|
const session = {
|
|
@@ -516,24 +531,25 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
516
531
|
}], "account", void 0);
|
|
517
532
|
},
|
|
518
533
|
deleteUserSessions: async (userId) => {
|
|
534
|
+
const revokedAt = Date.now();
|
|
519
535
|
const sessionReferences = await getActiveSessionReferences(userId);
|
|
520
536
|
if (secondaryStorage) {
|
|
521
537
|
if (!options.session?.storeSessionInDatabase) {
|
|
522
|
-
await queueCachedUserSessionDeletion(userId, sessionReferences);
|
|
538
|
+
await queueCachedUserSessionDeletion(userId, sessionReferences, revokedAt);
|
|
523
539
|
return;
|
|
524
540
|
}
|
|
525
541
|
if (ctx.options.session?.preserveSessionInDatabase) {
|
|
526
542
|
if (await endPreservedSessions([{
|
|
527
543
|
field: "userId",
|
|
528
544
|
value: userId
|
|
529
|
-
}]) !== null) await queueCachedUserSessionDeletion(userId, sessionReferences);
|
|
545
|
+
}]) !== null) await queueCachedUserSessionDeletion(userId, sessionReferences, revokedAt);
|
|
530
546
|
return;
|
|
531
547
|
}
|
|
532
548
|
}
|
|
533
549
|
if (await deleteManyWithHooks([{
|
|
534
550
|
field: "userId",
|
|
535
551
|
value: userId
|
|
536
|
-
}], "session", void 0) !== null) await queueCachedUserSessionDeletion(userId, sessionReferences);
|
|
552
|
+
}], "session", void 0) !== null) await queueCachedUserSessionDeletion(userId, sessionReferences, revokedAt);
|
|
537
553
|
},
|
|
538
554
|
deleteSessions: async (sessionTokens) => {
|
|
539
555
|
if (secondaryStorage) {
|
|
@@ -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,
|
package/dist/index.d.mts
CHANGED
|
@@ -7,8 +7,8 @@ import { InferOptionSchema, InferPluginContext, InferPluginErrorCodes, InferPlug
|
|
|
7
7
|
import { Auth } from "./types/auth.mjs";
|
|
8
8
|
import { BetterAuthAdvancedOptions, BetterAuthCookies, BetterAuthOptions, BetterAuthPlugin, BetterAuthRateLimitOptions, StoreIdentifierOption, UserProvisioningSource, ValidateUserInfoAction, ValidateUserInfoMethod, ValidateUserInfoOAuthInfo, ValidateUserInfoResult, ValidateUserInfoSSOInfo, ValidateUserInfoSource } from "./types/index.mjs";
|
|
9
9
|
import { betterAuth } from "./auth/full.mjs";
|
|
10
|
+
import { StateData, generateGenericState, getAuthStateVerificationIdentifier, parseGenericState } from "./state.mjs";
|
|
10
11
|
import { GenerateStateOptions, generateIdTokenNonce, generateState, parseState } from "./oauth2/state.mjs";
|
|
11
|
-
import { StateData, generateGenericState, parseGenericState } from "./state.mjs";
|
|
12
12
|
import { HIDE_METADATA } from "./utils/hide-metadata.mjs";
|
|
13
13
|
import { getBaseURL, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, resolveBaseURL, resolveDynamicBaseURL, trimTrailingSlashes } from "./utils/url.mjs";
|
|
14
14
|
import "./utils/index.mjs";
|
|
@@ -28,4 +28,4 @@ export * from "@better-auth/core/utils/json";
|
|
|
28
28
|
export * from "@better-auth/core/social-providers";
|
|
29
29
|
export type * from "better-call";
|
|
30
30
|
export type * from "zod";
|
|
31
|
-
export { APIError, type Account, type AdditionalSessionFieldsInput, type AdditionalUserFieldsInput, type Auth, type BetterAuthAdvancedOptions, type BetterAuthClientOptions, type BetterAuthClientPlugin, type BetterAuthCookies, type BetterAuthOptions, type BetterAuthPlugin, type BetterAuthRateLimitOptions, type ClientAtomListener, type ClientStore, type DBAdapter, type DBAdapterInstance, type DBAdapterSchemaCreation, type DBTransactionAdapter, type ExtractPluginField, FilteredAPI, GenerateStateOptions, HIDE_METADATA, type HasRequiredKeys, InferAPI, InferActions, InferAdditionalFromClient, InferClientAPI, InferErrorCodes, type InferOptionSchema, type InferPluginContext, type InferPluginErrorCodes, type InferPluginFieldFromTuple, type InferPluginIDs, type InferPluginTypes, InferSessionAPI, InferSessionFromClient, InferUserFromClient, type IsAny, IsSignal, type JSONWebKeySet, type JWTPayload, type JoinConfig, type JoinOption, type OverrideMerge, type Prettify, type PrettifyDeep, type RateLimit, type RequiredKeysOf, type Session, SessionQueryParams, type StandardSchemaV1, type StateData, type StoreIdentifierOption, type StripEmptyObjects, type TelemetryEvent, type UnionToIntersection, type User, type UserProvisioningSource, type ValidateUserInfoAction, type ValidateUserInfoMethod, type ValidateUserInfoOAuthInfo, type ValidateUserInfoResult, type ValidateUserInfoSSOInfo, type ValidateUserInfoSource, type Verification, type Where, betterAuth, createTelemetry, generateGenericState, generateIdTokenNonce, generateState, getBaseURL, getCurrentAdapter, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, getTelemetryAuthConfig, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, parseGenericState, parseState, resolveBaseURL, resolveDynamicBaseURL, trimTrailingSlashes };
|
|
31
|
+
export { APIError, type Account, type AdditionalSessionFieldsInput, type AdditionalUserFieldsInput, type Auth, type BetterAuthAdvancedOptions, type BetterAuthClientOptions, type BetterAuthClientPlugin, type BetterAuthCookies, type BetterAuthOptions, type BetterAuthPlugin, type BetterAuthRateLimitOptions, type ClientAtomListener, type ClientStore, type DBAdapter, type DBAdapterInstance, type DBAdapterSchemaCreation, type DBTransactionAdapter, type ExtractPluginField, FilteredAPI, GenerateStateOptions, HIDE_METADATA, type HasRequiredKeys, InferAPI, InferActions, InferAdditionalFromClient, InferClientAPI, InferErrorCodes, type InferOptionSchema, type InferPluginContext, type InferPluginErrorCodes, type InferPluginFieldFromTuple, type InferPluginIDs, type InferPluginTypes, InferSessionAPI, InferSessionFromClient, InferUserFromClient, type IsAny, IsSignal, type JSONWebKeySet, type JWTPayload, type JoinConfig, type JoinOption, type OverrideMerge, type Prettify, type PrettifyDeep, type RateLimit, type RequiredKeysOf, type Session, SessionQueryParams, type StandardSchemaV1, type StateData, type StoreIdentifierOption, type StripEmptyObjects, type TelemetryEvent, type UnionToIntersection, type User, type UserProvisioningSource, type ValidateUserInfoAction, type ValidateUserInfoMethod, type ValidateUserInfoOAuthInfo, type ValidateUserInfoResult, type ValidateUserInfoSSOInfo, type ValidateUserInfoSource, type Verification, type Where, betterAuth, createTelemetry, generateGenericState, generateIdTokenNonce, generateState, getAuthStateVerificationIdentifier, getBaseURL, getCurrentAdapter, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, getTelemetryAuthConfig, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, parseGenericState, parseState, resolveBaseURL, resolveDynamicBaseURL, trimTrailingSlashes };
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getBaseURL, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, resolveBaseURL, resolveDynamicBaseURL, trimTrailingSlashes } from "./utils/url.mjs";
|
|
2
|
-
import { generateGenericState, parseGenericState } from "./state.mjs";
|
|
2
|
+
import { generateGenericState, getAuthStateVerificationIdentifier, parseGenericState } from "./state.mjs";
|
|
3
3
|
import { generateIdTokenNonce, generateState, parseState } from "./oauth2/state.mjs";
|
|
4
4
|
import { HIDE_METADATA } from "./utils/hide-metadata.mjs";
|
|
5
5
|
import { APIError } from "./api/index.mjs";
|
|
@@ -14,4 +14,4 @@ export * from "@better-auth/core/oauth2";
|
|
|
14
14
|
export * from "@better-auth/core/utils/error-codes";
|
|
15
15
|
export * from "@better-auth/core/utils/id";
|
|
16
16
|
export * from "@better-auth/core/utils/json";
|
|
17
|
-
export { APIError, HIDE_METADATA, betterAuth, createTelemetry, generateGenericState, generateIdTokenNonce, generateState, getBaseURL, getCurrentAdapter, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, getTelemetryAuthConfig, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, parseGenericState, parseState, resolveBaseURL, resolveDynamicBaseURL, trimTrailingSlashes };
|
|
17
|
+
export { APIError, HIDE_METADATA, betterAuth, createTelemetry, generateGenericState, generateIdTokenNonce, generateState, getAuthStateVerificationIdentifier, getBaseURL, getCurrentAdapter, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, getTelemetryAuthConfig, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, parseGenericState, parseState, resolveBaseURL, resolveDynamicBaseURL, trimTrailingSlashes };
|