Best Practices in Implementing JWT in Next.js 15

You've just started building a Next.js 15 application and need to implement authentication. As you research JWT implementation, you find yourself overwhelmed with questions: Should you store tokens in cookies or localStorage? How do you properly verify tokens? What about security vulnerabilities you might accidentally introduce?

If you're feeling anxious about whether your JWT implementation is secure, you're not alone. Many developers struggle with these same concerns, especially given the serious consequences of getting authentication wrong.

The good news is that there are well-established patterns and tools to help you implement JWT authentication securely in Next.js 15. This guide will walk you through the best practices, common pitfalls to avoid, and show you how to leverage battle-tested solutions like Auth.js.

Understanding JWT Authentication

Before diving into implementation details, let's understand what JWT authentication is and why it's commonly used in Next.js applications.

A JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way to securely transmit information between parties as a JSON object. In web applications, JWTs are typically used for:

  • Authentication: Verifying the identity of users

  • Authorization: Determining what resources a user can access

  • Information Exchange: Securely transmitting data between parties

When a user logs into your application, the server generates a JWT containing the user's identity and permissions. This token is then sent to the client and included in subsequent requests to authenticate the user.

The Security Imperative

One of the most critical aspects of JWT implementation is security. Common vulnerabilities include:

  • Storing tokens in unsafe locations (like localStorage)

  • Not verifying token signatures

  • Failing to check token expiration

  • Allowing token impersonation

As one developer on Reddit noted: "I get anxious about whether what I am doing is a security vulnerability." This concern is valid - authentication is a critical security component that requires careful implementation.

Best Practice #1: Secure Token Storage

The first and most crucial best practice is proper token storage. A common mistake is storing JWTs in localStorage, which makes them vulnerable to Cross-Site Scripting (XSS) attacks.

Instead, always store JWTs in HttpOnly cookies. These special cookies cannot be accessed by client-side JavaScript, providing protection against XSS attacks. Here's how to set a secure cookie in Next.js:

// In your API route or server action
cookies().set({
  name: 'token',
  value: jwt,
  httpOnly: true,
  secure: process.env.NODE_ENV === 'production',
  sameSite: 'strict',
  path: '/'
});

Best Practice #2: Proper Token Verification

Another critical concern voiced by developers is token verification. As one Reddit user pointed out: "You should never trust anything you did not issue and cannot verify."

When implementing token verification, ensure you:

  1. Verify the token signature

  2. Check the expiration time

  3. Validate the token issuer

  4. Confirm the token audience

Here's an example of proper token verification in Next.js:

import { jwtVerify } from 'jose';

async function verifyToken(token: string) {
  try {
    const { payload } = await jwtVerify(
      token,
      new TextEncoder().encode(process.env.JWT_SECRET)
    );
    
    // Check if token is expired
    if (payload.exp && Date.now() >= payload.exp * 1000) {
      throw new Error('Token expired');
    }
    
    // Verify issuer
    if (payload.iss !== 'your-trusted-issuer') {
      throw new Error('Invalid token issuer');
    }
    
    return payload;
  } catch (error) {
    throw new Error('Invalid token');
  }
}

Best Practice #3: Use Auth.js Instead of Rolling Your Own

While understanding JWT implementation is important, you don't need to build everything from scratch. Auth.js (formerly NextAuth.js) is a complete authentication solution for Next.js applications that handles many security concerns automatically.

Here's why you should consider Auth.js:

  1. Built-in security best practices

  2. Automatic JWT handling and rotation

  3. Support for multiple authentication providers

  4. TypeScript support

  5. Active maintenance and community support

Getting started with Auth.js is straightforward:

npm install next-auth@beta

Basic setup in your Next.js application:

// app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';

const handler = NextAuth({
  providers: [
    CredentialsProvider({
      // Your credentials configuration
    }),
  ],
  session: {
    strategy: "jwt",
  },
  callbacks: {
    async jwt({ token, user }) {
      // Customize JWT contents
      return token;
    },
  },
});

export { handler as GET, handler as POST };

Best Practice #4: Implement Token Rotation and Refresh Tokens

To enhance security while maintaining a good user experience, implement token rotation with refresh tokens. This approach uses:

  • Short-lived access tokens (15-30 minutes)

  • Longer-lived refresh tokens (days or weeks)

  • Automatic token rotation

Here's how to implement token rotation with Auth.js:

// pages/api/auth/[...nextauth].ts
import NextAuth from 'next-auth';

export default NextAuth({
  // ... other config
  session: {
    strategy: 'jwt',
    maxAge: 15 * 60, // 15 minutes
  },
  jwt: {
    // Custom JWT encoding/decoding
    encode: async ({ secret, token }) => {
      // Your custom encoding logic
    },
    decode: async ({ secret, token }) => {
      // Your custom decoding logic
    },
  },
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        // First time jwt callback is run, user object is available
        token.accessToken = generateAccessToken(user);
        token.refreshToken = generateRefreshToken(user);
      }
      
      // Subsequent runs, check if access token needs refresh
      if (isAccessTokenExpired(token.accessToken)) {
        const newToken = await refreshAccessToken(token.refreshToken);
        token.accessToken = newToken;
      }
      
      return token;
    },
  },
});

Best Practice #5: Secure API Routes and Middleware

Protect your API routes and pages using middleware to verify authentication. Next.js 15 makes this straightforward with the middleware API:

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { getToken } from 'next-auth/jwt';

export async function middleware(request: NextRequest) {
  const token = await getToken({ req: request });
  
  if (!token) {
    return NextResponse.redirect(new URL('/auth/signin', request.url));
  }
  
  // Verify user permissions if needed
  if (request.nextUrl.pathname.startsWith('/admin') && !token.isAdmin) {
    return NextResponse.redirect(new URL('/unauthorized', request.url));
  }
  
  return NextResponse.next();
}

export const config = {
  matcher: ['/protected/:path*', '/api/protected/:path*'],
};

Best Practice #6: Error Handling and Security Headers

Proper error handling and security headers are crucial for a robust JWT implementation. Here's how to implement them:

// next.config.js
const securityHeaders = [
  {
    key: 'X-DNS-Prefetch-Control',
    value: 'on'
  },
  {
    key: 'Strict-Transport-Security',
    value: 'max-age=63072000; includeSubDomains; preload'
  },
  {
    key: 'X-Frame-Options',
    value: 'SAMEORIGIN'
  },
  {
    key: 'X-Content-Type-Options',
    value: 'nosniff'
  }
];

module.exports = {
  async headers() {
    return [
      {
        source: '/:path*',
        headers: securityHeaders,
      },
    ];
  },
};

Common Pitfalls to Avoid

  1. Never Store Sensitive Data in localStorage As mentioned earlier, localStorage is vulnerable to XSS attacks. Always use HttpOnly cookies for token storage.

  2. Don't Ignore Token Expiration Always verify token expiration times and implement proper token refresh mechanisms.

  3. Avoid Including Sensitive Information in JWTs JWTs are base64 encoded, not encrypted. Don't include sensitive information in the payload.

  4. Don't Skip Signature Verification Always verify JWT signatures to ensure tokens haven't been tampered with.

Conclusion

Implementing JWT authentication in Next.js 15 requires careful attention to security best practices. While you can implement everything from scratch, using Auth.js provides a battle-tested solution that handles many security concerns automatically.

Remember these key points:

  • Store tokens in HttpOnly cookies

  • Implement proper token verification

  • Use Auth.js for production applications

  • Implement token rotation

  • Set appropriate security headers

  • Handle errors gracefully

By following these best practices, you can build a secure authentication system that protects your users and your application.

Additional Resources

Remember, security is an ongoing process. Stay updated with the latest security best practices and regularly audit your authentication implementation to ensure it remains secure.

Raymond Yeh

Raymond Yeh

Published on 11 February 2025

Get engineers' time back from marketing!

Don't let managing a blog on your site get in the way of your core product.

Wisp empowers your marketing team to create and manage content on your website without consuming more engineering hours.

Get started in few lines of codes.

Choosing a CMS
Related Posts
How to Handle Authentication Across Separate Backend and Frontend for Next.js Website

How to Handle Authentication Across Separate Backend and Frontend for Next.js Website

Learn how to implement secure authentication in Next.js with Express backend using httpOnly cookies, JWT tokens, and middleware. Complete guide with code examples.

Read Full Story
How to Fetch Content with Strapi's API with Authentication?

How to Fetch Content with Strapi's API with Authentication?

Struggling with Strapi authentication? Our guide covers API tokens & JWTs, ensuring secure, efficient content fetching for both server-side and frontend apps. Dive in!

Read Full Story
Handling Common CORS Errors in Next.js 15

Handling Common CORS Errors in Next.js 15

Tired of seeing "Access to fetch at... has been blocked by CORS policy" errors? Dive into our guide to master CORS issues in Next.js and streamline your development process.

Read Full Story
Loading...