diff --git a/packages/better-auth/src/api/routes/session.ts b/packages/better-auth/src/api/routes/session.ts index 03cd093c4a..f8be5fd43e 100644 --- a/packages/better-auth/src/api/routes/session.ts +++ b/packages/better-auth/src/api/routes/session.ts @@ -302,6 +302,21 @@ export const sessionMiddleware = createAuthMiddleware(async (ctx) => { }; }); +/** + * This middleware forces the endpoint to require a valid session and ignores cookie cache. + * This should be used for sensitive operations like password changes, account deletion, etc. + * to ensure that revoked sessions cannot be used even if they're still cached in cookies. + */ +export const sensitiveSessionMiddleware = createAuthMiddleware(async (ctx) => { + const session = await getSessionFromCtx(ctx, { disableCookieCache: true }); + if (!session?.session) { + throw new APIError("UNAUTHORIZED"); + } + return { + session, + }; +}); + /** * This middleware allows you to call the endpoint on the client if session is valid. * However, if called on the server, no session is required. @@ -408,7 +423,7 @@ export const revokeSession = createAuthEndpoint( description: "The token to revoke", }), }), - use: [sessionMiddleware], + use: [sensitiveSessionMiddleware], requireHeaders: true, metadata: { openapi: { @@ -486,7 +501,7 @@ export const revokeSessions = createAuthEndpoint( "/revoke-sessions", { method: "POST", - use: [sessionMiddleware], + use: [sensitiveSessionMiddleware], requireHeaders: true, metadata: { openapi: { @@ -539,7 +554,7 @@ export const revokeOtherSessions = createAuthEndpoint( { method: "POST", requireHeaders: true, - use: [sessionMiddleware], + use: [sensitiveSessionMiddleware], metadata: { openapi: { description: diff --git a/packages/better-auth/src/api/routes/update-user.test.ts b/packages/better-auth/src/api/routes/update-user.test.ts index 0cc6fd08bd..afe0c86eb0 100644 --- a/packages/better-auth/src/api/routes/update-user.test.ts +++ b/packages/better-auth/src/api/routes/update-user.test.ts @@ -475,4 +475,66 @@ describe("delete user", async () => { }); expect(nullSession.data).toBeNull(); }); + + it("should ignore cookie cache for sensitive operations like changePassword", async () => { + const { client: cacheClient, sessionSetter: cacheSessionSetter } = + await getTestInstance( + { + session: { + cookieCache: { + enabled: true, + maxAge: 60, + }, + }, + }, + { + disableTestUser: true, + }, + ); + + const uniqueEmail = `cache-test-${Date.now()}@test.com`; + const testPassword = "testPassword123"; + + await cacheClient.signUp.email({ + email: uniqueEmail, + password: testPassword, + name: "Cache Test User", + }); + + const cacheHeaders = new Headers(); + await cacheClient.signIn.email({ + email: uniqueEmail, + password: testPassword, + fetchOptions: { + onSuccess: cacheSessionSetter(cacheHeaders), + }, + }); + + const initialSession = await cacheClient.getSession({ + fetchOptions: { + headers: cacheHeaders, + throw: true, + }, + }); + expect(initialSession?.user).toBeDefined(); + + const changePasswordResult = await cacheClient.changePassword({ + newPassword: "newSecurePassword123", + currentPassword: testPassword, + revokeOtherSessions: true, + fetchOptions: { + headers: cacheHeaders, + }, + }); + + expect(changePasswordResult.data).toBeDefined(); + + const sessionAfterPasswordChange = await cacheClient.getSession({ + fetchOptions: { + headers: cacheHeaders, + }, + }); + + expect(sessionAfterPasswordChange.data).toBeNull(); + }); }); diff --git a/packages/better-auth/src/api/routes/update-user.ts b/packages/better-auth/src/api/routes/update-user.ts index 8f13d4f07e..abb090e74a 100644 --- a/packages/better-auth/src/api/routes/update-user.ts +++ b/packages/better-auth/src/api/routes/update-user.ts @@ -2,7 +2,11 @@ import * as z from "zod/v4"; import { createAuthEndpoint } from "../call"; import { deleteSessionCookie, setSessionCookie } from "../../cookies"; -import { getSessionFromCtx, sessionMiddleware } from "./session"; +import { + getSessionFromCtx, + sensitiveSessionMiddleware, + sessionMiddleware, +} from "./session"; import { APIError } from "better-call"; import { createEmailVerificationToken } from "./email-verification"; import type { AdditionalUserFieldsInput, BetterAuthOptions } from "../../types"; @@ -150,7 +154,7 @@ export const changePassword = createAuthEndpoint( }) .optional(), }), - use: [sessionMiddleware], + use: [sensitiveSessionMiddleware], metadata: { openapi: { description: "Change the password of the user", @@ -318,7 +322,7 @@ export const setPassword = createAuthEndpoint( metadata: { SERVER_ONLY: true, }, - use: [sessionMiddleware], + use: [sensitiveSessionMiddleware], }, async (ctx) => { const { newPassword } = ctx.body; @@ -371,7 +375,7 @@ export const deleteUser = createAuthEndpoint( "/delete-user", { method: "POST", - use: [sessionMiddleware], + use: [sensitiveSessionMiddleware], body: z.object({ /** * The callback URL to redirect to after the user is deleted @@ -662,7 +666,7 @@ export const changeEmail = createAuthEndpoint( }) .optional(), }), - use: [sessionMiddleware], + use: [sensitiveSessionMiddleware], metadata: { openapi: { responses: {