I seem to be having trouble persisting my athntication status between django and reactjs ( when i refresh the page ) i loose authentication. upon sending requests to my endpoints in postman with my access_token everything is working great.
Here is how my drf is setup Settings.py
# CORS settings
CORS_ALLOW_CREDENTIALS = True
CORS_ORIGIN_WHITELIST = [
"http://localhost:3000",
]
# CSRF settings
CSRF_COOKIE_SECURE = True
CSRF_COOKIE_HTTPONLY = True
CSRF_COOKIE_SAMESITE = "None"
views.py
class CookieTokenObtainPairView(TokenObtainPairView):
def finalize_response(self, request, response, *args, **kwargs):
if response.data.get("access"):
response.set_cookie(
"access_token",
response.data["access"],
httponly=True,
secure=False,
samesite="Lax",
)
del response.data["access"]
return super().finalize_response(request, response, *args, **kwargs)
class CookieTokenRefreshView(TokenRefreshView):
def finalize_response(self, request, response, *args, **kwargs):
if response.data.get("access"):
response.set_cookie(
"access_token",
response.data["access"],
httponly=True,
secure=False,
samesite="Lax",
)
del response.data["access"]
return super().finalize_response(request, response, *args, **kwargs)
@api_view(["GET"])
@permission_classes([IsAuthenticated])
def check_auth_status(request):
# If the request reaches here, the user is authenticated
return Response({"status": "authenticated"})
urls.py
urlpatterns = [
path("admin/", admin.site.urls),
path("api/token/", CookieTokenObtainPairView.as_view(), name="token_obtain_pair"),
path("api/token/refresh/", CookieTokenRefreshView.as_view(), name="token_refresh"),
path("api/check_auth_status/", check_auth_status, name="check_auth_status"),
path("api/", include("vessels.urls")),
]
frontend side authslice.js
export const verifyAuth = createAsyncThunk(
"auth/verifyAuth",
async (_, { rejectWithValue }) => {
try {
await api.get("/api/check_auth_status/");
// If the request is successful, the user is authenticated
return true;
} catch (error) {
// If there is an error (like a 401), the user is not authenticated
return rejectWithValue(false);
}
}
);
export const authSlice = createSlice({
name: "auth",
initialState: {
isAuthenticated: false,
// other states...
},
reducers: {
// ... your other reducers ...
},
extraReducers: (builder) => {
builder
.addCase(verifyAuth.fulfilled, (state) => {
state.isAuthenticated = true;
})
.addCase(verifyAuth.rejected, (state) => {
state.isAuthenticated = false;
});
},
});
export const { loginSuccess, logout } = authSlice.actions;
export default authSlice.reducer;
loginpage.js
import React, { useState } from "react";
import api from "../../api/axios";
import { useNavigate } from "react-router-dom";
import { useDispatch } from "react-redux";
import { TextField, Button, Paper, Typography, Box } from "@mui/material";
import { verifyAuth } from "../../redux/slices/authSlice";
function LoginPage() {
const navigate = useNavigate();
const dispatch = useDispatch();
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const handleSubmit = async (e) => {
e.preventDefault();
try {
await api.post("api/token/", { username, password });
// Verify authentication status
dispatch(verifyAuth()).then(() => {
// Redirect after successful login
navigate("/");
});
} catch (error) {
setError("Invalid credentials");
console.error("Login error", error);
}
};
return (
<Box
display="flex"
justifyContent="center"
alignItems="center"
minHeight="100vh"
>
<Paper elevation={3} style={{ padding: "20px", width: "300px" }}>
<Typography variant="h5" style={{ textAlign: "center" }}>
Login
</Typography>
<form onSubmit={handleSubmit}>
<TextField
label="Username"
variant="outlined"
fullWidth
margin="normal"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<TextField
label="Password"
type="password"
variant="outlined"
fullWidth
margin="normal"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<Button
type="submit"
variant="contained"
color="primary"
fullWidth
style={{ marginTop: "20px" }}
>
Login
</Button>
{error && (
<Typography color="error" style={{ marginTop: "20px" }}>
{error}
</Typography>
)}
</form>
</Paper>
</Box>
);
}
export default LoginPage;
App.js
function App() {
const dispatch = useDispatch();
useEffect(() => {
dispatch(verifyAuth());
}, [dispatch]);
return (
<Router>
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route
path="/"
element={
<ProtectedRoute>
<HomePage />
</ProtectedRoute>
}
/>
</Routes>
</Router>
);
}
export default App;
and finally ProtectedRoute.js
const ProtectedRoute = ({ children }) => {
const isAuthenticated = useSelector((state) => state.auth.isAuthenticated);
const location = useLocation();
if (!isAuthenticated) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
return children;
};
export default ProtectedRoute;
I don't think it is the case that you are losing the authentication. I suspect that the isAuthenticated
state is reinitialized to false
when the page is reloaded and your ProtectedRoute
component selects that initial state value and renders the redirect back to the login page.
A trivial solution is to persist the auth state to localStorage and initialize the auth state from localStorage.
Example:
export const authSlice = createSlice({
name: "auth",
initialState: {
isAuthenticated: !!JSON.parse(localStorage.getItem("authenticated")),
// other states...
},
reducers: {
// ... your other reducers ...
},
extraReducers: (builder) => {
builder
.addCase(verifyAuth.fulfilled, (state) => {
state.isAuthenticated = true;
localStorage.setItem("authenticated", true);
})
.addCase(verifyAuth.rejected, (state) => {
state.isAuthenticated = false;
localStorage.setItem("authenticated", false);
});
},
});
Now when the page reloads, the persisted isAuthenticated
state will be reinitialized and the user should stay on the protected route they were on. The dispatched verifyAuth
action when the app mounts may invalidate their authentication later.