FastAPI logout works but user gets logged in again after page refresh

4 hours ago 4
ARTICLE AD BOX

I am implementing authentication in FastAPI using cookies for session management. I have a logout endpoint that deletes the authentication cookie. The logout request returns 200 OK and the frontend redirects the user to the login page.

However, when I refresh the page after logging out, the user gets logged in again automatically.

This suggests that either the cookie is not being properly removed or the browser is still sending some authentication information.

Backend (FastAPI)

manager = LoginManager(SECRET_KEY, token_url=TOKEN_URL, use_cookie=True) router = APIRouter(prefix="/auth", tags=["AUTHENTICATION"]) def _clear_auth_cookie(response: Response): response.delete_cookie( key=manager.cookie_name, path="/", httponly=True, samesite="none", secure=True, ) def _set_auth_cookie(response: Response, subject_id: str, role: str, is_active: bool): access_token = manager.create_access_token( data={"sub": subject_id, "role": role, "is_active": is_active}, expires=expire_duration, ) response.set_cookie( key=manager.cookie_name, value=access_token, httponly=True, samesite="none", secure=True, path="/", expires=seconds, ) # _append_partitioned_cookie_flag(response) def _clear_google_oauth_cookies(response: Response): response.delete_cookie( key=GOOGLE_OAUTH_STATE_COOKIE, path="/", httponly=True, samesite="none", secure=True, ) response.delete_cookie( key=GOOGLE_OAUTH_MODE_COOKIE, path="/", httponly=True, samesite="none", secure=True, ) @router.post("/tokenVerification", status_code=status.HTTP_200_OK) async def token(request: Request, response: Response, db: Session = Depends(get_db)): user = await manager.optional(request) if user: if user.role in {"member", "trainer"} and getattr(user, "email_verified", True) is False: _clear_auth_cookie(response) return { "valid": False, "role": "user", "is_active": False, "user_id": None, "is_super_admin": False, "email_verified": False, } user.last_login = func.now() db.commit() if user.role == "member": return { "valid": True, "role": user.role, "is_active": user.is_active, "user_id": user.user_id, "is_super_admin": False, } if user.role == "trainer": return { "valid": True, "role": user.role, "is_active": user.is_active, "user_id": user.trainer_id, "is_super_admin": False, } if user.role == "admin": return { "valid": True, "role": user.role, "is_active": user.is_active, "user_id": user.admin_id, "is_super_admin": bool(user.is_super_admin), } _clear_auth_cookie(response) return _clear_auth_cookie(response) return {"valid": False, "role": "user", "is_active": False, "user_id": None, "is_super_admin": False} @router.post("/logout") def logout(response: Response): _clear_auth_cookie(response) _clear_google_oauth_cookies(response) return {"message": "Logged out successfully"}

Frontend (React + Axios)

authSlice

import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"; import axios from "axios"; const BASE_URL = import.meta.env.VITE_BACKEND_URL; // Async backend verification export const verifyAuth = createAsyncThunk( "auth/verifyAuth", async (_, thunkAPI) => { try { const res = await axios.post(`${BASE_URL}/auth/tokenVerification`, null, { withCredentials: true, }); return res.data ?? { valid: false, role: 'user', is_active: false, user_id: null, is_super_admin: false }; } catch { return thunkAPI.rejectWithValue(false); } } ); const authSlice = createSlice({ name: "auth", initialState: { loggedIn: false, role:'user', is_active:false, user_id:null, is_super_admin: false, }, reducers: { logout(state) { state.loggedIn = false; state.role = 'user'; state.is_active = false; state.user_id = null; state.is_super_admin = false; }, }, extraReducers: (builder) => { builder .addCase(verifyAuth.fulfilled, (state, action) => { const {valid, role, is_active, user_id, is_super_admin} = action.payload; state.loggedIn = !!valid; state.role = role; state.is_active = is_active; state.user_id = user_id; state.is_super_admin = !!is_super_admin; }) .addCase(verifyAuth.rejected, (state) => { state.loggedIn = false; state.role = 'user'; state.is_active = false; state.user_id = null; state.is_super_admin = false; }); }, }); export const { logout } = authSlice.actions; export default authSlice.reducer;

Logout Function

const handleLogout = async () => { try { await axios.post(`${BASE_URL}/auth/logout`, null, { withCredentials: true }) dispatch(logout()) setTimeout(() => { navigate('/login') }, 500); } catch { return; } }

Steps to reproduce

Login successfully

Click logout

User is redirected to login page

Refresh the page

User appears logged in again

Expected behavior

After logout:

authentication cookie should be removed

refreshing the page should not restore the session

user should remain logged out

Actual behavior

Refreshing the page restores the logged-in state.

Response Headers After Logout:

set-cookie : access-token=""; expires=Sun, 15 Mar 2026 05:05:25 GMT; HttpOnly; Max-Age=0; Path=/; SameSite=none; Secure

set-cookie : google_oauth_state=""; expires=Sun, 15 Mar 2026 05:05:25 GMT; HttpOnly; Max-Age=0; Path=/; SameSite=none; Secure

set-cookie : google_oauth_mode=""; expires=Sun, 15 Mar 2026 05:05:25 GMT; HttpOnly; Max-Age=0; Path=/; SameSite=none; Secure

Read Entire Article