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

You've built a sleek Next.js frontend and a robust backend API, but now you're staring at your screen, scratching your head about how to properly implement authentication between them. The documentation seems sparse, existing resources feel incomplete, and you're worried about making security mistakes that could compromise your users' data.

If you're feeling confused about handling authentication in your Next.js application, you're not alone. Many developers express frustration about finding comprehensive resources and implementing secure authentication practices, especially when working with separate frontend and backend services.

The good news is that there's a systematic approach to implementing authentication that will keep your application secure while maintaining a smooth user experience. This guide will walk you through the essential concepts, best practices, and practical implementation steps.

Understanding the Authentication Challenge

When building a Next.js application with a separate backend (like Express or Nest.js), several critical challenges emerge:

  • How to securely store and transmit authentication tokens

  • Where to implement authentication logic (frontend vs. backend)

  • How to maintain persistent login states across browser sessions

  • How to share authenticated user data across your application

  • How to handle token refreshing and session management

The complexity increases because you need to coordinate authentication between two separate systems while ensuring security isn't compromised.

Core Authentication Concepts

Before diving into implementation details, let's establish some fundamental concepts that will guide our approach:

1. Token-Based Authentication

Modern web applications typically use token-based authentication, where the server issues a token (usually a JWT) upon successful login. This token is then:

  • Stored securely in the client (preferably in httpOnly cookies)

  • Sent with subsequent requests to verify the user's identity

  • Used to maintain the user's session

2. Secure Storage

One of the most critical decisions is where to store authentication tokens. While local storage might seem convenient, it's vulnerable to XSS attacks. Instead, security experts recommend using httpOnly cookies because they:

  • Cannot be accessed by JavaScript

  • Are automatically sent with HTTP requests

  • Provide better protection against XSS attacks

3. Separation of Concerns

A clean authentication implementation should:

  • Centralize authentication logic on the backend

  • Use middleware for route protection

  • Implement proper error handling

  • Follow the principle of least privilege

Implementing Authentication in Next.js

Let's walk through a practical implementation that addresses these challenges.

1. Setting Up Backend Authentication

First, centralize your authentication logic on the backend. This typically involves:

// backend/auth/controller.ts
async function login(req, res) {
  try {
    const { email, password } = req.body;
    
    // Validate credentials
    const user = await validateUser(email, password);
    
    // Generate JWT token
    const token = jwt.sign(
      { userId: user.id },
      process.env.JWT_SECRET,
      { expiresIn: '24h' }
    );
    
    // Set httpOnly cookie
    res.cookie('auth-token', token, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'strict',
      maxAge: 24 * 60 * 60 * 1000 // 24 hours
    });
    
    return res.json({ success: true });
  } catch (error) {
    return res.status(401).json({ error: 'Authentication failed' });
  }
}

2. Frontend Authentication Middleware

Next.js 13+ provides powerful middleware capabilities for handling authentication. Here's how to implement it:

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const token = request.cookies.get('auth-token')
  
  // Protect routes that require authentication
  if (!token && !request.nextUrl.pathname.startsWith('/login')) {
    return NextResponse.redirect(new URL('/login', request.url))
  }
  
  // Redirect authenticated users away from auth pages
  if (token && request.nextUrl.pathname.startsWith('/login')) {
    return NextResponse.redirect(new URL('/dashboard', request.url))
  }
  
  return NextResponse.next()
}

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
}

3. Handling API Requests

When making API requests from your Next.js frontend to your backend, you'll need to include the authentication token:

// lib/api.ts
async function fetchWithAuth(url: string, options: RequestInit = {}) {
  const response = await fetch(url, {
    ...options,
    credentials: 'include', // Important for sending cookies
    headers: {
      ...options.headers,
      'Content-Type': 'application/json',
    },
  });

  if (!response.ok) {
    // Handle authentication errors
    if (response.status === 401) {
      // Redirect to login or refresh token
      window.location.href = '/login';
      return;
    }
    throw new Error('API request failed');
  }

  return response.json();
}

## Best Practices and Common Pitfalls

### Security Best Practices

1. **Use HttpOnly Cookies**
   ```javascript
   // Always set httpOnly flag when setting authentication cookies
   res.cookie('auth-token', token, {
     httpOnly: true,
     secure: process.env.NODE_ENV === 'production',
     sameSite: 'strict'
   });
  1. Implement CSRF Protection

    • Use CSRF tokens for mutation operations

    • Implement proper CORS policies

    • Set appropriate SameSite cookie attributes

  2. Token Management

    • Implement token refresh mechanisms

    • Set appropriate token expiration times

    • Handle token revocation

Common Pitfalls to Avoid

  1. Storing Sensitive Data in Local Storage Many developers make the mistake of storing JWT tokens in localStorage, which is vulnerable to XSS attacks. Instead:

    // ❌ Don't do this
    localStorage.setItem('token', authToken);
    
    // ✅ Use httpOnly cookies instead
    // Let the backend handle cookie setting
    
  2. Insufficient Error Handling

    // ✅ Proper error handling
    async function handleLogin() {
      try {
        const response = await login(credentials);
        router.push('/dashboard');
      } catch (error) {
        if (error.status === 401) {
          setError('Invalid credentials');
        } else {
          setError('An unexpected error occurred');
        }
      }
    }
    
  3. Missing Token Refresh Logic Implement proper token refresh mechanisms to maintain user sessions:

    async function refreshToken() {
      try {
        const response = await fetch('/api/auth/refresh', {
          credentials: 'include'
        });
        
        if (!response.ok) {
          throw new Error('Token refresh failed');
        }
        
        // Backend will set new cookie
        return true;
      } catch (error) {
        // Handle refresh failure
        return false;
      }
    }
    

Advanced Authentication Patterns

1. Role-Based Access Control (RBAC)

Implement role-based protection for routes and components:

// components/RoleGuard.tsx
interface RoleGuardProps {
  children: React.ReactNode;
  allowedRoles: string[];
}

export function RoleGuard({ children, allowedRoles }: RoleGuardProps) {
  const { user } = useAuth();
  
  if (!user || !allowedRoles.includes(user.role)) {
    return <AccessDenied />;
  }
  
  return <>{children}</>;
}

2. Authentication State Management

Consider using a global state management solution for authentication state:

// hooks/useAuth.ts
import { create } from 'zustand';

interface AuthState {
  user: User | null;
  isLoading: boolean;
  login: (credentials: Credentials) => Promise<void>;
  logout: () => Promise<void>;
}

const useAuth = create<AuthState>((set) => ({
  user: null,
  isLoading: true,
  login: async (credentials) => {
    // Implementation
  },
  logout: async () => {
    // Implementation
  },
}));

Choosing Authentication Solutions

While implementing custom authentication is educational, for production applications, consider using established authentication solutions:

1. NextAuth.js

NextAuth.js is a popular choice that provides:

  • Built-in support for multiple authentication providers

  • Secure session management

  • TypeScript support

  • Easy integration with Next.js

However, some developers report that it can be opinionated and restrictive in certain use cases.

2. Lucia Auth

A lightweight alternative that offers:

  • More flexibility in implementation

  • Better performance

  • Less opinionated approach

3. Custom Implementation

If you need complete control, implementing your own solution using the patterns described in this article is viable, but ensure you:

  • Follow security best practices

  • Implement proper error handling

  • Test thoroughly

  • Consider compliance requirements

Conclusion

Implementing authentication between a Next.js frontend and separate backend requires careful consideration of security, user experience, and maintainability. By following the best practices outlined in this guide and choosing the right authentication solution for your needs, you can create a robust and secure authentication system.

Remember to:

  • Use httpOnly cookies for token storage

  • Implement proper error handling

  • Consider using established authentication libraries

  • Regularly update dependencies and security measures

  • Test your authentication system thoroughly

For more detailed information, consult the Next.js authentication documentation and stay updated with security best practices.

Raymond Yeh

Raymond Yeh

Published on 18 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
Best Practices in Implementing JWT in Next.js 15

Best Practices in Implementing JWT in Next.js 15

Comprehensive guide to JWT implementation in Next.js 15: Learn secure token storage, middleware protection, and Auth.js integration. Master authentication best practices today.

Read Full Story
Choosing the Right Authentication for Your Next.js App: A Practical Guide

Choosing the Right Authentication for Your Next.js App: A Practical Guide

Choosing the right authentication for your Next.js app can be overwhelming. Explore insights on NextAuth, Clerk, and BetterAuth to find your best fit!

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
Loading...