Next.js Authentication - Complete Implementation Guide

Mahesh Mahesh Waghmare
7 min read

Implementing authentication in Next.js requires understanding both client-side and server-side authentication patterns. This guide covers multiple authentication approaches, from simple JWT to comprehensive solutions like NextAuth.js.

Authentication Overview

Next.js supports multiple authentication patterns:

  • Server-side: API routes, Server Components, Middleware
  • Client-side: React components, hooks
  • Hybrid: Combination of both

Common Approaches:

  1. NextAuth.js - Complete authentication solution
  2. JWT Tokens - Custom JWT implementation
  3. Session-based - Server-side sessions
  4. OAuth - Third-party authentication
  5. Custom - Build your own solution

Key Considerations:

  • Secure token storage
  • Server-side validation
  • Protected routes
  • API route protection
  • Session management

NextAuth.js Setup

NextAuth.js is the most popular authentication solution for Next.js.

Installation

npm install next-auth

Basic Setup

1. Create API Route (app/api/auth/[...nextauth]/route.ts):

import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';

const handler = NextAuth({
  providers: [
    CredentialsProvider({
      name: 'Credentials',
      credentials: {
        username: { label: 'Username', type: 'text' },
        password: { label: 'Password', type: 'password' }
      },
      async authorize(credentials) {
        // Validate credentials
        if (credentials?.username === 'admin' && credentials?.password === 'password') {
          return { id: '1', name: 'Admin', email: 'admin@example.com' };
        }
        return null;
      }
    })
  ],
  pages: {
    signIn: '/auth/signin',
  },
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.id = user.id;
      }
      return token;
    },
    async session({ session, token }) {
      if (session.user) {
        session.user.id = token.id as string;
      }
      return session;
    }
  }
});

export { handler as GET, handler as POST };

2. Environment Variables (.env.local):

NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=your-secret-key-here

3. Session Provider (app/layout.tsx):

import { SessionProvider } from 'next-auth/react';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <SessionProvider>
          {children}
        </SessionProvider>
      </body>
    </html>
  );
}

4. Use in Components:

'use client';
import { useSession, signIn, signOut } from 'next-auth/react';

export default function Component() {
  const { data: session, status } = useSession();

  if (status === 'loading') return <p>Loading...</p>;

  if (session) {
    return (
      <>
        <p>Signed in as {session.user?.email}</p>
        <button onClick={() => signOut()}>Sign out</button>
      </>
    );
  }

  return (
    <>
      <p>Not signed in</p>
      <button onClick={() => signIn()}>Sign in</button>
    </>
  );
}
Advertisement

JWT Authentication

Custom JWT Implementation

1. Install Dependencies:

npm install jsonwebtoken bcryptjs
npm install -D @types/jsonwebtoken @types/bcryptjs

2. Login API Route (app/api/auth/login/route.ts):

import { NextRequest, NextResponse } from 'next/server';
import jwt from 'jsonwebtoken';
import bcrypt from 'bcryptjs';

const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';

export async function POST(request: NextRequest) {
  const { email, password } = await request.json();

  // Validate user (check database)
  const user = await getUserByEmail(email);
  if (!user) {
    return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 });
  }

  // Verify password
  const isValid = await bcrypt.compare(password, user.password);
  if (!isValid) {
    return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 });
  }

  // Generate JWT
  const token = jwt.sign(
    { userId: user.id, email: user.email },
    JWT_SECRET,
    { expiresIn: '7d' }
  );

  const response = NextResponse.json({ success: true });
  response.cookies.set('token', token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'strict',
    maxAge: 60 * 60 * 24 * 7 // 7 days
  });

  return response;
}

3. Verify Token Middleware (middleware.ts):

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import jwt from 'jsonwebtoken';

const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';

export function middleware(request: NextRequest) {
  const token = request.cookies.get('token')?.value;

  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  try {
    jwt.verify(token, JWT_SECRET);
    return NextResponse.next();
  } catch (error) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
}

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

Session Management

Server-Side Sessions

1. Session Storage (using database):

// lib/session.ts
import { cookies } from 'next/headers';
import { SignJWT, jwtVerify } from 'jose';

const secret = new TextEncoder().encode(process.env.JWT_SECRET);

export async function createSession(userId: string) {
  const session = await new SignJWT({ userId })
    .setProtectedHeader({ alg: 'HS256' })
    .setIssuedAt()
    .setExpirationTime('7d')
    .sign(secret);

  cookies().set('session', session, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    maxAge: 60 * 60 * 24 * 7
  });
}

export async function getSession() {
  const session = cookies().get('session')?.value;
  if (!session) return null;

  try {
    const { payload } = await jwtVerify(session, secret);
    return payload;
  } catch {
    return null;
  }
}

export async function deleteSession() {
  cookies().delete('session');
}

2. Use in Server Components:

import { getSession } from '@/lib/session';
import { redirect } from 'next/navigation';

export default async function ProtectedPage() {
  const session = await getSession();
  
  if (!session) {
    redirect('/login');
  }

  return <div>Protected Content</div>;
}

OAuth Providers

NextAuth.js with OAuth

Setup Google OAuth:

import GoogleProvider from 'next-auth/providers/google';

export default NextAuth({
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    })
  ],
});

Setup GitHub OAuth:

import GitHubProvider from 'next-auth/providers/github';

export default NextAuth({
  providers: [
    GitHubProvider({
      clientId: process.env.GITHUB_ID,
      clientSecret: process.env.GITHUB_SECRET,
    })
  ],
});

Environment Variables:

GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
GITHUB_ID=your-github-id
GITHUB_SECRET=your-github-secret
Advertisement

Protected Routes

Using Middleware

Middleware Protection (middleware.ts):

import { withAuth } from 'next-auth/middleware';

export default withAuth({
  pages: {
    signIn: '/auth/signin',
  },
});

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

Server Component Protection

import { getServerSession } from 'next-auth/next';
import { redirect } from 'next/navigation';
import { authOptions } from '@/app/api/auth/[...nextauth]/route';

export default async function ProtectedPage() {
  const session = await getServerSession(authOptions);

  if (!session) {
    redirect('/auth/signin');
  }

  return <div>Protected Content</div>;
}

Client Component Protection

'use client';
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';

export default function ProtectedComponent() {
  const { data: session, status } = useSession();
  const router = useRouter();

  useEffect(() => {
    if (status === 'unauthenticated') {
      router.push('/auth/signin');
    }
  }, [status, router]);

  if (status === 'loading') {
    return <div>Loading...</div>;
  }

  if (!session) {
    return null;
  }

  return <div>Protected Content</div>;
}

API Route Protection

Protect API Routes

import { getServerSession } from 'next-auth/next';
import { NextResponse } from 'next/server';
import { authOptions } from '../auth/[...nextauth]/route';

export async function GET(request: Request) {
  const session = await getServerSession(authOptions);

  if (!session) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  // Protected API logic
  return NextResponse.json({ data: 'Protected data' });
}

JWT Verification in API Routes

import { NextRequest, NextResponse } from 'next/server';
import jwt from 'jsonwebtoken';

const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';

export async function GET(request: NextRequest) {
  const token = request.cookies.get('token')?.value;

  if (!token) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  try {
    const decoded = jwt.verify(token, JWT_SECRET);
    // Use decoded.userId for database queries
    return NextResponse.json({ data: 'Protected data' });
  } catch (error) {
    return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
  }
}

Best Practices

Security

  1. Use HTTPS in production
  2. HttpOnly Cookies for tokens
  3. Secure Cookies in production
  4. Strong Secrets for JWT signing
  5. Token Expiration - set reasonable expiry times
  6. Password Hashing - use bcrypt with salt rounds
  7. Rate Limiting - prevent brute force attacks

Performance

  1. Server Components for authentication checks
  2. Middleware for route protection
  3. Session Caching - cache session data
  4. Token Refresh - implement refresh tokens

User Experience

  1. Loading States - show loading during auth checks
  2. Error Handling - clear error messages
  3. Redirect After Login - return to intended page
  4. Remember Me - optional longer sessions

Conclusion

Next.js authentication can be implemented using:

  • NextAuth.js - Easiest, most feature-complete
  • JWT - Custom, flexible solution
  • Sessions - Server-side session management
  • OAuth - Third-party authentication

Key Points:

  • Use NextAuth.js for quick setup
  • Implement JWT for custom needs
  • Protect routes with middleware
  • Secure API routes
  • Follow security best practices

Choose the approach that fits your needs and security requirements.

Advertisement
Mahesh Waghmare

Written by Mahesh Waghmare

I bridge the gap between WordPress architecture and modern React frontends. Currently building tools for the AI era.

Follow on Twitter

Read Next