We use cookies to enhance your experience on the site
CodeWorlds

Authentication basics in Next.js applications

Authentication is a fundamental aspect of modern web applications. It allows you to verify user identity, which is essential for personalizing experiences, protecting data, and providing features only to authorized users. In this module, we will learn the basics of implementing authentication in Next.js applications, which combines the benefits of server-side rendering (SSR) and client-side rendering.

What is authentication and why is it important?

Authentication is the process of verifying a user's identity. It is a key element of web application security because:

  1. Protects user data - ensures that personal information is accessible only to its owners
  2. Enables personalization - allows tailoring the experience to a specific user
  3. Controls access - determines which resources and features a user can access
  4. Ensures accountability - allows tracking who performed specific actions and when

Authentication vs. Authorization

It is important to distinguish between two related but separate concepts:

  • Authentication - verifying the user's identity ("Are you who you claim to be?")
  • Authorization - determining what an authenticated user can do ("Do you have permission to do this?")

In many cases, both processes are interconnected, but they serve different functions in the security architecture.

Authentication specifics in Next.js

Next.js offers a hybrid rendering model, which has a significant impact on authentication implementation:

  1. Server-Side Rendering (SSR) - enables authentication verification before sending the page to the client
  2. Static Site Generation (SSG) - requires additional client-side verification for protected content
  3. App Router - provides new, dedicated mechanisms for handling authentication

Basic authentication models

Several main authentication models can be applied in Next.js applications:

1. Session-based authentication

A traditional approach based on maintaining session state on the server:

1// Example API route for session-based login
2import { NextApiRequest, NextApiResponse } from 'next';
3import { getServerSession } from 'next-auth/next';
4import { authOptions } from '../auth/[...nextauth]';
5
6export default async function handler(req: NextApiRequest, res: NextApiResponse) {
7  const session = await getServerSession(req, res, authOptions);
8  
9  if (!session) {
10    return res.status(401).json({ message: 'Unauthorized' });
11  }
12  
13  // User authenticated, can proceed
14  return res.status(200).json({ user: session.user });
15}

2. Token-based authentication (JWT)

A popular approach using JWT (JSON Web Tokens):

1// Example API route for JWT token verification
2import { NextApiRequest, NextApiResponse } from 'next';
3import jwt from 'jsonwebtoken';
4
5export default function handler(req: NextApiRequest, res: NextApiResponse) {
6  const { authorization } = req.headers;
7  
8  if (!authorization || !authorization.startsWith('Bearer ')) {
9    return res.status(401).json({ message: 'Unauthorized' });
10  }
11  
12  const token = authorization.replace('Bearer ', '');
13  
14  try {
15    const decoded = jwt.verify(token, process.env.JWT_SECRET!);
16    // Token valid, can proceed
17    return res.status(200).json({ user: decoded });
18  } catch (error) {
19    return res.status(401).json({ message: 'Invalid token' });
20  }
21}

3. Authentication with external providers (OAuth)

Using external services like Google, Facebook, or GitHub:

1// Example NextAuth.js configuration with Google authentication
2import NextAuth from 'next-auth';
3import GoogleProvider from 'next-auth/providers/google';
4
5export default NextAuth({
6  providers: [
7    GoogleProvider({
8      clientId: process.env.GOOGLE_CLIENT_ID!,
9      clientSecret: process.env.GOOGLE_CLIENT_SECRET!
10    })
11  ],
12  // Additional configuration...
13});

Implementing authentication in the Next.js App Router

In newer versions of Next.js (13+) with App Router, the authentication approach has been improved. Below are the basic steps:

1. Configuring the authentication provider

First, we need to configure the authentication provider. In this example, we will use Auth.js (formerly NextAuth.js):

1// app/api/auth/[...nextauth]/route.ts
2import NextAuth from 'next-auth';
3import CredentialsProvider from 'next-auth/providers/credentials';
4import { compare } from 'bcrypt';
5import { prisma } from '@/lib/prisma';
6
7export const authOptions = {
8  providers: [
9    CredentialsProvider({
10      name: 'Credentials',
11      credentials: {
12        email: { label: "Email", type: "email" },
13        password: { label: "Password", type: "password" }
14      },
15      async authorize(credentials) {
16        if (!credentials?.email || !credentials?.password) {
17          return null;
18        }
19        
20        const user = await prisma.user.findUnique({
21          where: {
22            email: credentials.email
23          }
24        });
25        
26        if (!user) {
27          return null;
28        }
29        
30        const isPasswordValid = await compare(
31          credentials.password,
32          user.password
33        );
34        
35        if (!isPasswordValid) {
36          return null;
37        }
38        
39        return {
40          id: user.id,
41          email: user.email,
42          name: user.name
43        };
44      }
45    })
46  ],
47  pages: {
48    signIn: '/login',
49    signOut: '/logout',
50    error: '/login',
51  },
52  session: {
53    strategy: 'jwt'
54  }
55};
56
57const handler = NextAuth(authOptions);
58export { handler as GET, handler as POST };

2. Creating login and registration components

Next, we need a user interface for login:

1// app/login/page.tsx
2'use client';
3
4import { useState } from 'react';
5import { signIn } from 'next-auth/react';
6import { useRouter } from 'next/navigation';
7
8export default function LoginPage() {
9  const router = useRouter();
10  const [email, setEmail] = useState('');
11  const [password, setPassword] = useState('');
12  const [error, setError] = useState('');
13  const [loading, setLoading] = useState(false);
14  
15  async function handleSubmit(e: React.FormEvent) {
16    e.preventDefault();
17    setLoading(true);
18    setError('');
19    
20    try {
21      const result = await signIn('credentials', {
22        redirect: false,
23        email,
24        password
25      });
26      
27      if (result?.error) {
28        setError('Invalid email or password');
29      } else {
30        router.push('/dashboard');
31        router.refresh();
32      }
33    } catch (error) {
34      setError('An error occurred during login');
35    } finally {
36      setLoading(false);
37    }
38  }
39
40  return (
41    <div className="max-w-md mx-auto mt-10 p-6 bg-white rounded-lg shadow-md">
42      <h1 className="text-2xl font-bold mb-6">Login</h1>
43      
44      {error && (
45        <div className="mb-4 p-3 bg-red-100 text-red-700 rounded">
46          {error}
47        </div>
48      )}
49      
50      <form onSubmit={handleSubmit} className="space-y-4">
51        <div>
52          <label htmlFor="email" className="block text-sm font-medium mb-1">
53            Email
54          </label>
55          <input
56            id="email"
57            type="email"
58            value={email}
59            onChange={(e) => setEmail(e.target.value)}
60            required
61            className="w-full px-3 py-2 border border-gray-300 rounded"
62          />
63        </div>
64        
65        <div>
66          <label htmlFor="password" className="block text-sm font-medium mb-1">
67            Password
68          </label>
69          <input
70            id="password"
71            type="password"
72            value={password}
73            onChange={(e) => setPassword(e.target.value)}
74            required
75            className="w-full px-3 py-2 border border-gray-300 rounded"
76          />
77        </div>
78        
79        <button
80          type="submit"
81          disabled={loading}
82          className="w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 disabled:opacity-50"
83        >
84          {loading ? 'Logging in...' : 'Log in'}
85        </button>
86      </form>
87    </div>
88  );
89}

And a registration page:

1// app/register/page.tsx
2'use client';
3
4import { useState } from 'react';
5import { useRouter } from 'next/navigation';
6
7export default function RegisterPage() {
8  const router = useRouter();
9  const [name, setName] = useState('');
10  const [email, setEmail] = useState('');
11  const [password, setPassword] = useState('');
12  const [confirmPassword, setConfirmPassword] = useState('');
13  const [error, setError] = useState('');
14  const [loading, setLoading] = useState(false);
15  
16  async function handleSubmit(e: React.FormEvent) {
17    e.preventDefault();
18    setLoading(true);
19    setError('');
20    
21    // Validate form
22    if (password !== confirmPassword) {
23      setError('Passwords do not match');
24      setLoading(false);
25      return;
26    }
27    
28    try {
29      const response = await fetch('/api/register', {
30        method: 'POST',
31        headers: { 'Content-Type': 'application/json' },
32        body: JSON.stringify({ name, email, password })
33      });
34      
35      const data = await response.json();
36      
37      if (!response.ok) {
38        throw new Error(data.message || 'An error occurred during registration');
39      }
40      
41      // Registration successful, redirect to login page
42      router.push('/login?registered=true');
43    } catch (error) {
44      setError(error instanceof Error ? error.message : 'An error occurred');
45    } finally {
46      setLoading(false);
47    }
48  }
49  
50  return (
51    <div className="max-w-md mx-auto mt-10 p-6 bg-white rounded-lg shadow-md">
52      <h1 className="text-2xl font-bold mb-6">Registration</h1>
53      
54      {error && (
55        <div className="mb-4 p-3 bg-red-100 text-red-700 rounded">
56          {error}
57        </div>
58      )}
59      
60      <form onSubmit={handleSubmit} className="space-y-4">
61        <div>
62          <label htmlFor="name" className="block text-sm font-medium mb-1">
63            Full name
64          </label>
65          <input
66            id="name"
67            type="text"
68            value={name}
69            onChange={(e) => setName(e.target.value)}
70            required
71            className="w-full px-3 py-2 border border-gray-300 rounded"
72          />
73        </div>
74        
75        <div>
76          <label htmlFor="email" className="block text-sm font-medium mb-1">
77            Email
78          </label>
79          <input
80            id="email"
81            type="email"
82            value={email}
83            onChange={(e) => setEmail(e.target.value)}
84            required
85            className="w-full px-3 py-2 border border-gray-300 rounded"
86          />
87        </div>
88        
89        <div>
90          <label htmlFor="password" className="block text-sm font-medium mb-1">
91            Password
92          </label>
93          <input
94            id="password"
95            type="password"
96            value={password}
97            onChange={(e) => setPassword(e.target.value)}
98            required
99            minLength={8}
100            className="w-full px-3 py-2 border border-gray-300 rounded"
101          />
102        </div>
103        
104        <div>
105          <label htmlFor="confirmPassword" className="block text-sm font-medium mb-1">
106            Confirm password
107          </label>
108          <input
109            id="confirmPassword"
110            type="password"
111            value={confirmPassword}
112            onChange={(e) => setConfirmPassword(e.target.value)}
113            required
114            className="w-full px-3 py-2 border border-gray-300 rounded"
115          />
116        </div>
117        
118        <button
119          type="submit"
120          disabled={loading}
121          className="w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 disabled:opacity-50"
122        >
123          {loading ? 'Registering...' : 'Sign up'}
124        </button>
125      </form>
126    </div>
127  );
128}

3. Implementing the registration API endpoint

To enable registration, we need an API endpoint:

1// app/api/register/route.ts
2import { NextResponse } from 'next/server';
3import { hash } from 'bcrypt';
4import { prisma } from '@/lib/prisma';
5import { z } from 'zod';
6
7// Validation schema
8const UserSchema = z.object({
9  name: z.string().min(2, 'First name must be at least 2 characters'),
10  email: z.string().email('Invalid email address'),
11  password: z.string().min(8, 'Password must be at least 8 characters')
12});
13
14export async function POST(request: Request) {
15  try {
16    const body = await request.json();
17    
18    // Validate data
19    const result = UserSchema.safeParse(body);
20    
21    if (!result.success) {
22      return NextResponse.json(
23        { message: result.error.errors[0].message },
24        { status: 400 }
25      );
26    }
27    
28    const { name, email, password } = result.data;
29    
30    // Check if user already exists
31    const existingUser = await prisma.user.findUnique({
32      where: { email }
33    });
34    
35    if (existingUser) {
36      return NextResponse.json(
37        { message: 'A user with this email address already exists' },
38        { status: 409 }
39      );
40    }
41    
42    // Hash the password
43    const hashedPassword = await hash(password, 10);
44    
45    // Create new user
46    const user = await prisma.user.create({
47      data: {
48        name,
49        email,
50        password: hashedPassword
51      }
52    });
53    
54    // Return response without password
55    const { password: _, ...userWithoutPassword } = user;
56    
57    return NextResponse.json(
58      { message: 'Registration completed successfully', user: userWithoutPassword },
59      { status: 201 }
60    );
61  } catch (error) {
62    console.error('Registration error:', error);
63    return NextResponse.json(
64      { message: 'A server error occurred' },
65      { status: 500 }
66    );
67  }
68}

4. Securing protected pages

To protect pages that should only be accessible to logged-in users, we can use middleware:

1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4import { getToken } from 'next-auth/jwt';
5
6export async function middleware(request: NextRequest) {
7  const token = await getToken({ req: request });
8  
9  // Check if user is logged in
10  if (!token) {
11    const url = new URL('/login', request.url);
12    url.searchParams.set('callbackUrl', request.nextUrl.pathname);
13    return NextResponse.redirect(url);
14  }
15  
16  return NextResponse.next();
17}
18
19// Define paths that should be protected
20export const config = {
21  matcher: ['/dashboard/:path*', '/profile/:path*', '/settings/:path*']
22};

5. Displaying user data and managing sessions

To use information about the logged-in user in a component, we can use the hook

useSession
:

1// components/ProfileSection.tsx
2'use client';
3
4import { useSession } from 'next-auth/react';
5import { signOut } from 'next-auth/react';
6
7export default function ProfileSection() {
8  const { data: session, status } = useSession();
9  
10  if (status === 'loading') {
11    return <div>Loading...</div>;
12  }
13  
14  if (status === 'unauthenticated' || !session) {
15    return <div>You are not logged in</div>;
16  }
17  
18  return (
19    <div className="p-4 bg-white rounded shadow">
20      <h2 className="text-xl font-bold mb-4">Your profile</h2>
21      <p>Hello, {session.user?.name || 'user'}!</p>
22      <p>Email: {session.user?.email}</p>
23      
24      <button
25        onClick={() => signOut({ callbackUrl: '/' })}
26        className="mt-4 px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
27      >
28        Log out
29      </button>
30    </div>
31  );
32}

6. Initializing SessionProvider for the entire application

For authentication to work properly, we need to add

SessionProvider
at the layout level:

1// app/providers.tsx
2'use client';
3
4import { SessionProvider } from 'next-auth/react';
5import { ReactNode } from 'react';
6
7export function Providers({ children }: { children: ReactNode }) {
8  return <SessionProvider>{children}</SessionProvider>;
9}
10
11// app/layout.tsx
12import { Providers } from './providers';
13
14export default function RootLayout({
15  children,
16}: {
17  children: React.ReactNode;
18}) {
19  return (
20    <html lang="en">
21      <body>
22        <Providers>{children}</Providers>
23      </body>
24    </html>
25  );
26}

Security mechanisms

When implementing authentication, it is important to keep in mind several key security mechanisms:

1. Secure password storage

Never store passwords in plain text! Always use hashing functions such as

bcrypt
:

1import { hash, compare } from 'bcrypt';
2
3// Hashing password during registration
4const hashedPassword = await hash(password, 10);
5
6// Verifying password during login
7const isValid = await compare(password, hashedPassword);

2. Protection against CSRF attacks

Cross-Site Request Forgery (CSRF) is a type of attack that exploits an authenticated user's session to perform unauthorized actions. Next.js and NextAuth.js provide some protections by default, but it is worth ensuring additional protection:

1// Adding a CSRF token to the form
2export default function LoginForm() {
3  const csrfToken = await getCsrfToken();
4  
5  return (
6    <form method="post" action="/api/auth/callback/credentials">
7      <input name="csrfToken" type="hidden" defaultValue={csrfToken} />
8      {/* Rest of the form */}
9    </form>
10  );
11}

3. HTTPS implementation

Using encrypted HTTPS connections is essential for protecting authentication credentials and session tokens. In a production environment, make sure your application is accessible only via HTTPS.

4. JWT token expiration

If you use JWT tokens, set an appropriate expiration time to limit risk in case of token theft:

1// JWT token expiration configuration in NextAuth.js
2export const authOptions = {
3  // ... other options
4  session: {
5    strategy: 'jwt',
6    maxAge: 30 * 24 * 60 * 60, // 30 days
7  },
8  jwt: {
9    secret: process.env.JWT_SECRET,
10    maxAge: 30 * 24 * 60 * 60, // 30 days
11  }
12};

Common challenges and solutions

Problem: Enforcing authentication on static pages (SSG)

A challenge can be securing pages that are statically generated during build:

Solution: Add an additional client-side verification layer:

1'use client';
2
3import { useSession } from 'next-auth/react';
4import { useRouter } from 'next/navigation';
5import { useEffect } from 'react';
6
7export default function ProtectedPage() {
8  const { status } = useSession();
9  const router = useRouter();
10  
11  useEffect(() => {
12    if (status === 'unauthenticated') {
13      router.push('/login');
14    }
15  }, [status, router]);
16  
17  if (status === 'loading') {
18    return <div>Loading...</div>;
19  }
20  
21  if (status === 'unauthenticated') {
22    return null; // Instead of rendering content, redirect to login page
23  }
24  
25  // Actual content of the protected page
26  return (
27    <div>
28      <h1>Protected page</h1>
29      {/* Rest of content */}
30    </div>
31  );
32}

Problem: Different user roles

Sometimes we need different access levels for users:

Solution: Extend the token/session with role information and implement role checking:

1// Extending NextAuth types
2declare module 'next-auth' {
3  interface Session {
4    user: {
5      id: string;
6      name?: string | null;
7      email?: string | null;
8      image?: string | null;
9      role: string;
10    };
11  }
12  
13  interface User {
14    id: string;
15    name?: string | null;
16    email?: string | null;
17    image?: string | null;
18    role: string;
19  }
20}
21
22// Using role information
23function canAccessAdmin(session: Session | null) {
24  return session?.user?.role === 'admin';
25}
26
27// In the component
28const { data: session } = useSession();
29const isAdmin = session?.user?.role === 'admin';
30
31if (!isAdmin) {
32  return <div>Access denied. This section is for administrators only.</div>;
33}

Summary

Authentication in Next.js can be implemented in many ways, but the most commonly used approaches include:

  1. NextAuth.js / Auth.js - a comprehensive solution offering integration with many authentication providers
  2. Custom JWT solution - for non-standard requirements
  3. External services - e.g., Firebase Auth, Auth0, Clerk

Choosing the right solution depends on the specific requirements of your application, such as:

  • Need for integration with external providers (Google, Facebook, etc.)
  • Data ownership requirements
  • Complexity of the permissions and roles model
  • Scalability

In the following modules, we will look at more advanced aspects of authentication, including detailed NextAuth.js implementation, session and token management, route protection using middleware, and security topics.

Go to CodeWorlds