An example application that demonstrates using HTTP-only cookies for secure authentication with Next.js.
HTTP/1.1 200 OK
Content-Type: application/json
Set-Cookie:refreshToken=eyJhbGci; Max-Age=3600; Path=/; Expires=Sun, 12 May 2024 04:37:24 GMT; HttpOnly; SameSite=Lax
{
"message": "Login successful",
"data": {
"accessToken" : "eyJlbWFp",
}
}
- Make a POST request to the
Route Handler
with the user's credentials (email and password) and set access token, refresh token in thecookies
usingnext/headers
orNextResponse.json()
.
export async function POST(request: Request) {
const body = await request.json();
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/auth/login`, {
method: "POST",
body: JSON.stringify(body),
credentials: "include",
headers: {
"Content-Type": "application/json",
},
});
const json = await res.json();
if (!res.ok) {
return NextResponse.json(
{
message: json.message,
},
{
status: res.status,
},
);
}
const cookiesFromResponse = res.headers.getSetCookie();
const cookieStore = cookies();
cookieStore.set("accessToken", json.data.accessToken, {
httpOnly: true,
secure: true,
sameSite: "strict",
path: "/",
});
return NextResponse.json(json, {
headers: {
"Set-Cookie": cookiesFromResponse.join(", "),
credentials: "include",
},
});
}
Note:
- Why we don't make a POST request directly to the
External API
from the client component?- As the
accessToken
is returned in the response body while therefreshToken
is returned in theCookies
header, it is not possible to setaccessToken
in thecookies
usingnext/headers
orNextResponse.json()
.
- As the
- Only use
Server Action
. - Validate the user's credentials(email and password).
- Make a POST request(
External Server
orAPI Handler
likeClient component
) with the user's credentialsand set access token, refresh token in thecookies
usingnext/headers
const login = async (formData: FormData) => {
"use server";
const { email, password } = Object.fromEntries(formData.entries());
const body = JSON.stringify({
email,
password,
});
const headers = {
"Content-Type": "application/json",
};
const res = await fetch(`${process.env.NEXT_PUBLIC_URL}/api/auth/login`, {
credentials: "include",
method: "POST",
headers,
body,
});
const json = await res.json();
const refreshToken =
res.headers.get("set-cookie")?.split(";")[0].split("=")[1] || "";
const cookieStore = cookies();
cookieStore.set("accessToken", json.data.accessToken, {
httpOnly: true,
secure: true,
sameSite: "strict",
path: "/",
});
cookieStore.set("refreshToken", refreshToken, {
httpOnly: true,
secure: true,
sameSite: "strict",
path: "/",
});
return json;
};
Note:
- If you send a POST request to the API Handler and return
accessToken
in thecookies
by usingnext/headers
orNextResponse.json()
, and the response from the External API to Server Action, the cookies won't be set in the browser. To resolve this issue, you need to set theaccessToken
in thecookies
usingnext/headers
orNextResponse.json()
in the Server Action as you did before.
- Make a POST request to the
Route Handler
and remove theaccessToken
andrefreshToken
from thecookies
usingnext/headers
orNextResponse.json()
.
API (/api/auth/logout)
import { HttpError } from "@/lib/fetch";
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
export async function DELETE(req: Request) {
try {
const headers = req.headers;
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/auth/logout`, {
method: "DELETE",
headers,
});
const json = await res.json();
if (!res.ok)
throw new HttpError({
error: json.error,
message: json.message,
statusCode: res.status,
});
const cookieStore = cookies();
cookieStore.delete("accessToken");
cookieStore.delete("refreshToken");
return NextResponse.json(json);
} catch (error) {
return Promise.reject(error);
}
}
- Only use
Server Action
. - Make a POST request(
External Server
) and remove theaccessToken
andrefreshToken
from thecookies
usingnext/headers
.
const logoutAction = async () => {
"use server";
const headers = {
"Content-Type": "application/json",
};
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/auth/logout`, {
credentials: "include",
method: "DELETE",
headers,
});
const json = await res.json();
const cookieStore = cookies();
cookieStore.delete("accessToken");
cookieStore.delete("refreshToken");
return json;
};
- Create
RefreshTokenMachine
to refresh theaccessToken
using therefreshToken
. - Set timeout to refresh the
accessToken
before it expires. - Make a POST request to the
Route Handler Proxy
with therefreshToken
and set the newaccessToken
in thecookies
usingnext/headers
orNextResponse.json()
.
refresh-token-machine.tsx
"use client";
import dayjs from "dayjs";
import { jwtDecode } from "jwt-decode";
import { useRouter } from "next/navigation";
import { useEffect, useRef, useState } from "react";
import { useAuthContext } from "../auth/context";
export default function RefreshTokenMachine() {
const [diff, setDiff] = useState<number | null>(null);
const [accessToken, setAccessToken] = useState<string | null>(null);
const prevAccessToken = useRef<string[]>([]);
const router = useRouter();
const { isAuthenticated } = useAuthContext();
useEffect(() => {
if (accessToken) {
prevAccessToken.current.push(accessToken);
}
}, [accessToken]);
useEffect(() => {
if (!isAuthenticated) return;
const getRefresh = async () => {
{
const res = await fetch(
`${process.env.NEXT_PUBLIC_URL}/api/proxy/auth/refresh-token`,
{
method: "GET",
},
);
const { data } = await res.json();
const { accessToken } = data;
setAccessToken(accessToken);
const jwtPayload = jwtDecode(accessToken);
if (!jwtPayload.exp) {
router.push("/logout");
return;
}
const refreshTime = dayjs(jwtPayload.exp * 1000).subtract(
55,
"seconds",
);
const now = dayjs();
const diff = refreshTime.diff(now, "milliseconds");
setDiff(diff);
}
};
let timeoutId = null;
if (diff !== null && diff > 0) {
timeoutId = setTimeout(getRefresh, diff);
} else {
getRefresh();
}
return () => (timeoutId ? clearTimeout(timeoutId) : void 0);
}, [isAuthenticated, diff, router]);
return null;
Note:
- Why do we make a GET request to the
Route Handler Proxy
instead of theExternal API
?- As the
accessToken
is returned in the response body while therefreshToken
is returned in theCookies
header, it is not possible to setaccessToken
in thecookies
usingnext/headers
orNextResponse.json()
.
- As the