NextJS Authentication Flow — Store JWT In Cookie

Authentication flow using JWT and cookies.

Published on

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.

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.

Enjoyed this article?

Share it with your network to help others discover it

Notify: Just send the damn email. All with one API call.

Continue Learning

Discover more articles on similar topics