feat: enhance security by implementing HttpOnly refresh tokens and in-memory access token management
- Added isomorphic-dompurify for improved XSS protection - Refactored token storage to use in-memory management for access tokens - Implemented server-side route handlers for OAuth token management - Introduced security headers in next.config.js - Removed client-side exposure of client_secret and sensitive tokens - Updated API interceptors to handle token refresh logic - Cleaned up cookie management for refresh tokens
This commit is contained in:
@@ -4,6 +4,7 @@ import { Button } from "@mui/material";
|
||||
import ArrowLeftB from "@/components/icons/ArrowLeftB";
|
||||
import { request } from "@/services/response";
|
||||
import Cookies from "js-cookie";
|
||||
import { setAccessToken } from "@/lib/tokenStore";
|
||||
|
||||
function SubmitData({ setStep, data, prevData, setErrors, doctor, isForAnother, selectedSlot, selectedDate, setAppointmentId, setAppointmentExpiresAt }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -11,21 +12,12 @@ function SubmitData({ setStep, data, prevData, setErrors, doctor, isForAnother,
|
||||
|
||||
const refreshAccessToken = async () => {
|
||||
try {
|
||||
const refreshToken = Cookies.get("refresh_token");
|
||||
if (!refreshToken) return false;
|
||||
|
||||
const res = await fetch("/api/auth/refresh", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ refresh_token: refreshToken }),
|
||||
});
|
||||
const res = await fetch("/api/auth/refresh", { method: "POST" });
|
||||
if (!res.ok) return false;
|
||||
const response = await res.json();
|
||||
|
||||
if (response?.access_token) {
|
||||
Cookies.set("access_token", response.access_token, { expires: 7 });
|
||||
if (response.refresh_token) {
|
||||
Cookies.set("refresh_token", response.refresh_token, { expires: 7 });
|
||||
}
|
||||
setAccessToken(response.access_token);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -12,7 +12,6 @@ import Surgeries from "./surgeries";
|
||||
import FamilyHistory from "./familyHistory";
|
||||
import Relatives from "./relatives";
|
||||
import { removeAdditionalKeysDashboard } from "@/helper";
|
||||
import Cookies from "js-cookie";
|
||||
import { toast } from "react-toastify";
|
||||
import LoadingComponent from "@/app/component/LoadingComponent";
|
||||
|
||||
@@ -37,11 +36,7 @@ function DetailUser({ user, isTurnsDetails, setIsTurnsDetails }) {
|
||||
const usedKays = removeAdditionalKeysDashboard(information);
|
||||
setLoading(true);
|
||||
request
|
||||
.patchUserProfile(
|
||||
usedKays,
|
||||
information?.uuid,
|
||||
Cookies.get("access_token")
|
||||
)
|
||||
.patchUserProfile(usedKays, information?.uuid)
|
||||
.then(() => {
|
||||
setLoading(false);
|
||||
toast.success("اطلاعات با موفقیت ثبت شد");
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import ArrowLeftWhiteD from "@/components/icons/ArrowLeftWhiteD";
|
||||
import {
|
||||
changeDateType,
|
||||
handleTimeExpiresToken,
|
||||
isValidIranNationalCode,
|
||||
removeAdditionalKeysDashboard,
|
||||
} from "@/helper";
|
||||
import { request } from "@/services/response";
|
||||
import { Button, CircularProgress } from "@mui/material";
|
||||
import Cookies from "js-cookie";
|
||||
import { toast } from "react-toastify";
|
||||
import { setAccessToken } from "@/lib/tokenStore";
|
||||
|
||||
function ButtonSendData({
|
||||
loading,
|
||||
@@ -31,21 +30,11 @@ function ButtonSendData({
|
||||
.postUserProfile(changeDateType(information, true))
|
||||
.then((response) => {
|
||||
if (response.uuid) {
|
||||
fetch("/api/auth/refresh", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ refresh_token: Cookies.get("refresh_token") }),
|
||||
})
|
||||
.then((r) => r.json())
|
||||
fetch("/api/auth/refresh", { method: "POST" })
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.then((res) => {
|
||||
if (res?.access_token) {
|
||||
const expiresTime = handleTimeExpiresToken(res.expires_in);
|
||||
Cookies.set("access_token", res.access_token, {
|
||||
expires: expiresTime.accessTokenExpires,
|
||||
path: "/",
|
||||
secure: true,
|
||||
sameSite: "strict",
|
||||
});
|
||||
setAccessToken(res.access_token);
|
||||
}
|
||||
setInformation({
|
||||
...information,
|
||||
@@ -69,11 +58,7 @@ function ButtonSendData({
|
||||
const usedKays = removeAdditionalKeysDashboard(information);
|
||||
|
||||
request
|
||||
.patchUserProfile(
|
||||
changeDateType(usedKays, true),
|
||||
information?.uuid,
|
||||
Cookies.get("access_token")
|
||||
)
|
||||
.patchUserProfile(changeDateType(usedKays, true), information?.uuid)
|
||||
.then(() => {
|
||||
setLoading(false);
|
||||
toast.success("اطلاعات با موفقیت ثبت شد");
|
||||
|
||||
@@ -6,14 +6,14 @@ import { cookies } from "next/headers";
|
||||
async function StLayout({ children, name }) {
|
||||
const { matchedCity } = await getStateInfo();
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get("access_token");
|
||||
const isLogged = Boolean(cookieStore.get("userInfo"));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<main className="w-full min-h-screen">
|
||||
<header className="w-full sticky bg-[#FFF] !z-[70] shadow-[0px_1px_24.8px_0px_rgba(204,_204,_204,_0.18)] mt-[12px] sm:mt-[21px] md:mt-[30px] lg:mt-[40px] top-0 right-1/2">
|
||||
<div className="w-full padding-responsive">
|
||||
<Header matchedCity={matchedCity} name={name} logged={token} />
|
||||
<Header matchedCity={matchedCity} name={name} logged={isLogged} />
|
||||
</div>
|
||||
</header>
|
||||
<section className="opacity-page -mt-[calc(48px_+_12px)] sm:-mt-[calc(56px_+_21px)] md:-mt-[calc(64px_+_30px)] lg:-mt-[calc(80px_+_40px)]">
|
||||
|
||||
@@ -18,7 +18,7 @@ function Content({ matchedCity, name, logged }) {
|
||||
const pathname = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
setIsLogged(Boolean(Cookies.get("access_token")));
|
||||
setIsLogged(Boolean(Cookies.get("userInfo")));
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,6 +3,7 @@ import { request } from "@/services/response";
|
||||
import Cookies from "js-cookie";
|
||||
import { toast } from "react-toastify";
|
||||
import { handleTimeExpiresToken } from "@/helper";
|
||||
import { setAccessToken } from "@/lib/tokenStore";
|
||||
|
||||
function SendReq({
|
||||
loading,
|
||||
@@ -44,14 +45,13 @@ function SendReq({
|
||||
const isHttps = window.location.protocol === "https:";
|
||||
const isLocalhost = hostname.includes("localhost");
|
||||
|
||||
// تنظیمات پایه
|
||||
// فقط دادههای غیرحساس در کوکی JS؛ access_token در memory، refresh_token در کوکی HttpOnly سرور
|
||||
let cookieOptions = {
|
||||
path: "/",
|
||||
sameSite: "lax",
|
||||
expires: expiresTime.accessTokenExpires,
|
||||
expires: expiresTime.refreshTokenExpires,
|
||||
};
|
||||
|
||||
// اگر روی دامنه اصلی و https بود
|
||||
if (!isLocalhost && isHttps) {
|
||||
cookieOptions = {
|
||||
...cookieOptions,
|
||||
@@ -60,17 +60,8 @@ function SendReq({
|
||||
};
|
||||
}
|
||||
|
||||
Cookies.set("access_token", response.access_token, cookieOptions);
|
||||
|
||||
Cookies.set("refresh_token", response.refresh_token, {
|
||||
...cookieOptions,
|
||||
expires: expiresTime.refreshTokenExpires,
|
||||
});
|
||||
|
||||
Cookies.set("uuid", uuid, {
|
||||
...cookieOptions,
|
||||
expires: expiresTime.refreshTokenExpires,
|
||||
});
|
||||
setAccessToken(response.access_token);
|
||||
Cookies.set("uuid", uuid, cookieOptions);
|
||||
|
||||
getInfo(response.access_token, cookieOptions);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user