Next.js authentication patterns best practices

Full Stack Engineer
May 11, 2024
Updated on January 8, 2025
0 MIN READ
#ssr#design-patterns#javascript#next.js#authentication

Introduction

Authentication is a critical aspect of modern web applications, and Next.js provides several patterns to implement it effectively. Whether you're building a simple website or a complex enterprise application, choosing the right authentication strategy can impact security, performance, and user experience. In this post, we'll explore Next.js authentication best practices, covering server-side authentication, JWT handling, middleware protection, and third-party providers.


1. Server-Side Authentication with Next.js API Routes

Next.js API routes provide a secure way to handle authentication logic on the server. Unlike client-side authentication, this approach keeps sensitive credentials and tokens hidden from the client-side JavaScript.

Best Practices:

  • Never expose secrets: Store API keys and secrets in environment variables (.env.local).
  • Use secure cookies: Prefer HTTP-only cookies over localStorage for session tokens.
  • Validate inputs: Sanitize and validate all incoming requests.

Here's an example of a login API route:

// pages/api/auth/login.js import { compare } from 'bcryptjs'; import { sign } from 'jsonwebtoken'; import { serialize } from 'cookie'; export default async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ message: 'Method not allowed' }); } const { email, password } = req.body; // 1. Fetch user from database (simplified example) const user = await getUserByEmail(email); if (!user) { return res.status(401).json({ message: 'Invalid credentials' }); } // 2. Verify password const isValid = await compare(password, user.password); if (!isValid) { return res.status(401).json({ message: 'Invalid credentials' }); } // 3. Create JWT token const token = sign( { userId: user.id, email: user.email }, process.env.JWT_SECRET, { expiresIn: '1h' } ); // 4. Set HTTP-only cookie const cookie = serialize('authToken', token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', maxAge: 3600, // 1 hour path: '/', }); res.setHeader('Set-Cookie', cookie); return res.status(200).json({ message: 'Login successful' }); }

2. Protecting Routes with Middleware

Next.js 12+ introduced middleware, which executes before a request is completed. This is ideal for route protection.

Implementation Tips:

  • Use middleware for global authentication checks
  • Redirect unauthenticated users
  • Handle API route protection differently than page routes

Example middleware:

// middleware.js import { NextResponse } from 'next/server'; import { verify } from 'jsonwebtoken'; export async function middleware(req) { const { pathname } = req.nextUrl; // Skip middleware for auth-related pages if (pathname.startsWith('/auth')) { return NextResponse.next(); } const token = req.cookies.get('authToken')?.value; try { // Verify JWT token verify(token, process.env.JWT_SECRET); return NextResponse.next(); } catch (err) { // Redirect to login if invalid token const loginUrl = new URL('/auth/login', req.url); loginUrl.searchParams.set('from', req.nextUrl.pathname); return NextResponse.redirect(loginUrl); } } // Config to specify matching paths export const config = { matcher: ['/dashboard/:path*', '/profile/:path*', '/api/private/:path*'], };

3. JWT Handling Strategies

JSON Web Tokens (JWTs) are popular for authentication, but they require careful implementation.

Best Practices:

  1. Short expiration times: 15-60 minutes for access tokens
  2. Implement refresh tokens: For longer sessions without compromising security
  3. Store securely: Never in localStorage - use HTTP-only cookies
  4. Validate properly: Always verify signatures and check expiration

Here's how to handle token refresh:

// pages/api/auth/refresh.js import { verify, sign } from 'jsonwebtoken'; import { serialize } from 'cookie'; export default async function handler(req, res) { const refreshToken = req.cookies.get('refreshToken')?.value; if (!refreshToken) { return res.status(401).json({ message: 'No refresh token' }); } try { // Verify refresh token const payload = verify(refreshToken, process.env.REFRESH_SECRET); // Issue new access token const newToken = sign( { userId: payload.userId }, process.env.JWT_SECRET, { expiresIn: '15m' } ); // Set new cookie const cookie = serialize('authToken', newToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', maxAge: 900, // 15 minutes path: '/', }); res.setHeader('Set-Cookie', cookie); return res.status(200).json({ message: 'Token refreshed' }); } catch (err) { return res.status(401).json({ message: 'Invalid refresh token' }); } }

4. Third-Party Authentication Providers

For many applications, using established providers (Auth0, NextAuth.js, Firebase Auth) is more secure and maintainable than building your own solution.

NextAuth.js Example:

// pages/api/auth/[...nextauth].js
import NextAuth from 'next-auth';
import GitHubProvider from 'next-auth/providers/github';

export default NextAuth({
  providers: [
    GitHubProvider({
      clientId: process.env.GITHUB_ID,
      clientSecret: process.env.GITHUB_SECRET,
    }),
  ],
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.id = user.id;
      }
      return token;
    },
    async session({ session, token }) {
      session.user.id = token.id;
      return session;
    },
  },
  secret: process.env.NEXTAUTH_SECRET,
  pages: {
    signIn: '/auth/signin',
  },
});

When to Use Third-Party Providers:

  • When you need social login (Google, Facebook, etc.)
  • For teams without security expertise
  • When compliance requirements exist (SOC2, HIPAA)
  • For rapid development

Conclusion

Implementing authentication in Next.js requires careful consideration of security, user experience, and maintainability. The best approach depends on your specific requirements:

  1. For full control: Use API routes with JWT and cookies
  2. For simplicity: Leverage middleware for route protection
  3. For security: Consider established providers like NextAuth.js

Remember that authentication is just one layer of security. Always implement additional measures like rate limiting, input validation, and regular security audits.

By following these patterns, you can build secure, scalable authentication systems in your Next.js applications that protect both your users and your infrastructure.

Share this article