I am working on a side project leveraging NextJS App router as the frontend and NestJS as the backend. I have been working on the authentication flow for a while, using JWT and Cookies. Here, I’m going to share the authentication flow that I am using.
Why using Cookie instead of LocalStorage or SessionStorage?
Items stored in LocalStorage/SessionStorage can be read from JavaScript, making them vulnerable to XSS attacks. Therefore, I have decided to use HttpOnly
cookies to store the access token because they are inaccessible to JavaScript. This means that attackers cannot read the access token from the cookie.
Authentication Flow
Login Flow:
Logout Flow:
Login
NextJS
Trigger the login route handler.
LoginButton.tsx
const loginRes = await fetch("/api/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(YOUR_LOGIN_CREDENTIAL),
});
NextJS Route Handler
Here’s where you want to set the access token.
app/api/login/route.ts
...
export async function POST(request: NextRequest) {
const loginRes = await axios.post(
"MY_BACKEND_URL/api/login",
{...}
);
cookies().set("accessToken", loginRes.data.accessToken, {
httpOnly: true,
maxAge: 24 * 60 * 60,
sameSite: "strict"
});
return NextResponse.json(loginRes.data, { status: 201 });
}
Axios
When cookies are set toSameSite=Strict
, they will not be sent automatically to your server if a request originates from a different domain. You may want to manually set jwt as cookie to send to your backend. Otherwise the backend have no idea where your cookie is. Therefore, I use Axios interceptors to attach the cookie in the request header.
import { cookies } from "next/headers";
export const getCookie = async (name: string) => {
return cookies().get(name)?.value ?? "";
};
instance.interceptors.request.use(async function (config) {
const accessToken = await getCookie("accessToken");
// You can set the cookie in the Authentication instead
// Depends on how you want to backend to read the accessToken
config.headers.Cookie = `accessToken=${accessToken}`;
return config;
});
NestJS
Create JWT and return it as part of the api response.
auth.service.ts
...
@Injectable()
export class AuthService {
constructor( private jwtService: JwtService, ) {}
async login(token: string) {
try {
...
const userPayload = {
sub: sub,
username: name,
email: email,
_id: user._id,
};
const accessToken = await this.jwtService.signAsync(userPayload);
return { accessToken };
} catch (e) {
console.log(e);
}
}
}
Logout
NextJS
Trigger Logout Route Handler
const logout = async () => {
await fetch("/api/logout", {
method: "POST",
});
};
NextJS Route Handler
Delete the cookie.
...
import { cookies } from "next/headers";
export async function POST(request: NextRequest) {
cookies().delete("accessToken");
return NextResponse.json({ status: 201 });
}
Conclusion
This is probably not my finalized authentication flow for the project. I am still looking for a better implementation for the flow, e.g. implementing Refresh Token flow, etc.
Thanks for reading.