Authentication in Next.js allows developers to verify the identity of users before granting access to certain pages or routes. It acts as a critical security layer that sits between the user request and the application, protecting sensitive data and resources.

Authentication is essential for modern web applications and can be implemented in various ways depending on your security requirements and use cases:

  • User Login & Registration: Verifying user identity with credentials
  • Role-Based Access Control (RBAC): Restricting features based on user roles
  • Session Management: Maintaining user state across requests
  • OAuth & Social Authentication: Third-party identity verification
  • Multi-Factor Authentication (MFA): Additional security via multiple verification methods
  • API Authentication: Securing API endpoints with tokens or keys
  • Single Sign-On (SSO): Unified authentication across multiple applications

How Authentication Works

Authentication in Next.js involves verifying the identity of a user before granting access to certain resources or pages. This is typically done by checking for a valid session or token.

When a user attempts to access a protected route, the middleware checks if they are authenticated. If not, they are redirected to the login page.

Authentication Flow


User submits login form
↓
Server verifies credentials
↓
Server creates session or token
↓
Token stored in HTTP-only cookie
↓
Middleware protects private routes

Authentication Methods in Next.js

There are several methods to implement authentication in Next.js applications. Each approach has different security profiles and use cases:

1. Cookie-Based Authentication

Cookies store session identifiers that are automatically sent with each request. Best for simple applications.

  • Server creates session on login
  • Session ID stored in HTTP-only cookie
  • Cookie automatically sent with requests
  • Good for CSRF protection with proper configuration

2. JWT (JSON Web Tokens)

JWTs are self-contained tokens that encode user information. Ideal for stateless authentication and APIs.

  • Token contains encoded user data
  • Client stores token (localStorage, sessionStorage, or cookie)
  • Token sent with each request in Authorization header
  • Server validates signature without database lookup

3. Session-Based Authentication

Server maintains session state in database or cache. Traditional and battle-tested approach.

  • Server creates session object on login
  • Session stored in database or Redis
  • Session ID in cookie or token
  • Server validates session on each request

4. Third-Party Authentication Libraries

Libraries like NextAuth.js, Auth0, and Firebase provide comprehensive solutions:

  • NextAuth.js: Open-source solution with OAuth, JWT, and session support
  • Auth0: Enterprise-grade authentication with MFA and advanced features
  • Firebase Auth: Google's platform with easy OAuth integration
  • Clerk: Modern authentication with built-in UI

Basic Authentication Example

Here's a simple example of how to implement authentication in Next.js using middleware:

Create Login Page

Lets create a simple login page with a form:

"use client"

export default function Login() {

  async function handleSubmit(
    e: React.FormEvent<HTMLFormElement>
  ) {

    e.preventDefault()

    const formData = new FormData(e.currentTarget)

    const email = formData.get("email")
    const password = formData.get("password")

    await fetch("/api/login", {
      method: "POST",
      body: JSON.stringify({ email, password }),
    })
  }

  return (
    <form onSubmit={handleSubmit}>

      <input name="email" placeholder="Email" />

      <input
        name="password"
        type="password"
        placeholder="Password"
      />

      <button type="submit">Login</button>

    </form>
  )
}

Create Login API

Now, let's create the API endpoint for handling the login request:


import { NextResponse } from "next/server"

export async function POST(request: Request) {

  const body = await request.json()

  const { email, password } = body

  if (email === "admin@test.com" && password === "1234") {

    const response = NextResponse.json({
      message: "Login successful",
    })

    response.cookies.set("token", "sample-token")

    return response
  }

  return NextResponse.json(
    { message: "Invalid credentials" },
    { status: 401 }
  )
}

This set a cookie name "token" with the value "sample-token"

Protect Route using Middleware

Finally, we can create a middleware to protect certain routes:

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

export function middleware(request: NextRequest) {

  const token = request.cookies.get("token")

  if (!token) {

    return NextResponse.redirect(
      new URL("/login", request.url)
    )
  }

  return NextResponse.next()
}

export const config = {
  matcher: "/dashboard/:path*",
}

This middleware will redirect users to the login page if they are not authenticated.

Create Protected Page

Now, let's create a simple protected page:

This will be our main protected page:

 // app/dashboard/page.tsx
export default function Dashboard() {
  return <h1>Welcome to Dashboard</h1>
}

Access Session Data

You can also access the session data in your protected pages:

 // app/dashboard/page.tsx
import { cookies } from "next/headers"

export default function Dashboard() {
  const token = cookies().get("session")?.value;
  return <h1>Welcome to Dashboard</h1>
}

Add Logout

To add a logout feature, we can create a simple API endpoint:

 // app/api/logout/route.ts
import { NextResponse } from "next/server"

export async function POST() {

  const response = NextResponse.json({
    message: "Logout successful",
  })

  response.cookies.delete("token")

  return response
}


JWT Based Authentication

JWT (JSON Web Tokens) are a popular method for implementing stateless authentication in Next.js applications. They allow you to securely transmit user information between the client and server without needing to maintain session state on the server.

How JWT Works

When a user logs in, the server generates a JWT containing user information and signs it with a secret key. The client stores this token (usually in localStorage or an HTTP-only cookie) and includes it in the Authorization header of subsequent requests. The server verifies the token's signature and grants access if it's valid.

Implementing JWT in Next.js

// API route for login
import { NextResponse } from "next/server";
import jwt from "jsonwebtoken";
export async function POST(request) {
  const body = await request.json();
  const { email, password } = body;

  // Validate credentials (example)
  if (email === "user@example.com" && password === "password") {
    const token = jwt.sign(
      { userId: 1, email },
      process.env.JWT_SECRET,
      { expiresIn: "1h" }
    );

    const response = NextResponse.json({ message: "Login successful" });
    response.cookies.set("token", token, {
      httpOnly: true,
      secure: true,
      sameSite: "strict",
      maxAge: 60 * 60, // 1 hour
    });

    return response;
  }
  return NextResponse.json({ message: "Invalid credentials" }, { status: 401 });
}

In the example above, we create a simple login endpoint that generates a JWT upon successful authentication and sets it as an HTTP-only cookie.

JWTs are particularly useful for stateless applications and microservices architectures where maintaining session state on the server is not feasible or desired.


JWT vs Session-Based Authentication

Feature JWT Session-Based
Stateless Yes - No server storage needed No - Requires server storage
Scalability Excellent for microservices Requires shared session store
Security Must use HTTPS, token refresh needed Very secure, can easily revoke
Performance Fast - No database lookup Slower - Database lookup per request
Revocation Difficult - Token valid until expiry Easy - Delete session immediately
Cross-Domain Works easily with CORS Requires careful cookie setup

When to use JWT: APIs, microservices, mobile apps, and stateless architectures

When to use Sessions: Traditional web apps, when immediate revocation is critical, and for enhanced security


OAuth & Social Authentication

OAuth2 Flow

OAuth2 allows users to authenticate using third-party providers like Google, GitHub, or Facebook.


// Using NextAuth.js with Google OAuth
import NextAuth from "next-auth";
import GoogleProvider from "next-auth/providers/google";

export const authOptions = {
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    }),
  ],
};

export const handler = NextAuth(authOptions);
  

Benefits of OAuth/Social Auth

  • No password management complexity
  • Reduced account creation friction
  • Leverages existing user accounts
  • Better user experience
  • Third-party security expertise

Password Security Best Practices

Never Store Plain Text Passwords

Always hash passwords using industry-standard algorithms like bcrypt, Argon2, or scrypt.


import bcrypt from "bcrypt";

// Hash password on registration
const hashedPassword = await bcrypt.hash(password, 10);

// Verify password on login
const isValidPassword = await bcrypt.compare(password, hashedPassword);
  

Password Requirements

  • Minimum 8-12 characters
  • Mix of uppercase, lowercase, numbers, and symbols
  • Avoid common patterns (123456, password, etc.)
  • Implement rate limiting on login attempts

Additional Security Measures

  • Use HTTPS everywhere
  • Implement password reset with token verification
  • Never log passwords
  • Enforce password expiration policies
  • Implement account lockout after failed attempts

Token Refresh Strategies

Managing token expiration and refresh is critical for security in JWT-based systems.

Access & Refresh Token Pattern


// API route for token refresh
import { NextResponse } from "next/server";
import jwt from "jsonwebtoken";

export async function POST(request) {
  const body = await request.json();
  const { refreshToken } = body;
  
  try {
    // Verify refresh token
    const decoded = jwt.verify(
      refreshToken,
      process.env.REFRESH_TOKEN_SECRET
    );
    
    // Generate new access token
    const newAccessToken = jwt.sign(
      { userId: decoded.userId },
      process.env.ACCESS_TOKEN_SECRET,
      { expiresIn: "15m" }
    );
    
    return NextResponse.json({ accessToken: newAccessToken });
  } catch (error) {
    return NextResponse.json(
      { error: "Invalid refresh token" },
      { status: 401 }
    );
  }
}
  

Token Expiration Best Practices

  • Access Token: Short-lived (15-30 minutes)
  • Refresh Token: Long-lived (7-30 days)
  • Store refresh token securely: HTTP-only cookie preferred
  • Implement token rotation: Refresh token can only be used once
  • Allow logout: Blacklist used refresh tokens

Authentication Security Best Practices

1. Use HTTPS Only

  • Always transmit authentication data over HTTPS
  • Never send tokens or credentials over HTTP
  • Enforce HTTPS on all authentication endpoints

2. Secure Cookie Configuration


// Set secure cookie with proper flags
response.cookies.set({
  name: "auth_token",
  value: token,
  httpOnly: true,      // Prevent XSS access
  secure: true,        // HTTPS only
  sameSite: "strict",  // Prevent CSRF
  maxAge: 60 * 60 * 24, // 24 hours
});
  

3. Implement CSRF Protection

  • Use SameSite cookie attribute
  • Validate CSRF tokens on state-changing requests
  • Use double-submit cookie pattern for APIs

4. Rate Limiting

  • Limit login attempts to prevent brute force
  • Implement exponential backoff after failed attempts
  • Use middleware to throttle authentication endpoints

5. Input Validation

  • Validate all user inputs
  • Sanitize inputs to prevent injection attacks
  • Use parameterized queries for database access

6. Error Handling

  • Never expose system details in error messages
  • Use generic error messages for security
  • Log security events for monitoring

Common Authentication Pitfalls to Avoid

1. Storing Passwords in Plain Text

❌ DON'T:


// WRONG - Never do this!
user.password = userPassword;
await user.save();
  

✅ DO:


// CORRECT - Hash before storing
const hashedPassword = await bcrypt.hash(password, 10);
user.password = hashedPassword;
await user.save();
  

2. Storing JWTs in LocalStorage

LocalStorage is vulnerable to XSS attacks. Use HTTP-only cookies instead.

3. Not Validating Tokens

Always validate token signatures and expiration on every request.

4. Exposing Sensitive Info in Error Messages

❌ DON'T: "User avi@example.com not found"

✅ DO: "Invalid email or password"

5. Missing Rate Limiting on Authentication Endpoints

Without rate limiting, attackers can brute force credentials. Always implement exponential backoff.

6. Not Handling Token Expiration

Implement proper token refresh mechanism instead of using long-lived tokens.