Mastering Authentication in Modern Next.js Apps

Vishal Yadav - Jun 24 - - Dev Community

Authentication is a complex and nuanced topic, especially with the introduction of server components, server actions, and middleware in modern web development frameworks like Next.js. This blog will break down the principles of authentication in Next.js applications, walk through the code, and explain new features and APIs. We'll also highlight best practices and common pitfalls to be aware of. Let's get started!

Getting Started with Authentication

Authentication usually begins with a sign-up process. We need to create a form to capture the user's name, email, and password.

Step 1: Creating the Sign-Up Form

First, let's create a form to capture user details. When the form is submitted, it invokes a server action.

import { useServer } from 'next/server';
import { useActionState } from 'next/action';

function SignUpForm() {
  const action = useServer('signupAction');
  const { pending, error } = useActionState(action);

  return (
    <form onSubmit={action}>
      <input name="name" placeholder="Name" required />
      <input type="email" name="email" placeholder="Email" required />
      <input type="password" name="password" placeholder="Password" required />
      <button type="submit" disabled={pending}>
        {pending ? 'Submitting...' : 'Sign Up'}
      </button>
      {error && <p>{error.message}</p>}
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Server Action for Sign-Up

In a new file, we'll create the server-side function that handles form submission. We'll validate the incoming fields before performing any authentication logic.

import { z } from 'zod';
import { hash } from 'bcryptjs';
import { prisma } from '../lib/prisma';
import { createSession } from '../lib/session';

export const signupAction = async (formData) => {
  const schema = z.object({
    name: z.string().min(1),
    email: z.string().email(),
    password: z.string().min(6),
  });

  const { success, error } = schema.safeParse(formData);

  if (!success) {
    return { error: 'Invalid input' };
  }

  const { name, email, password } = formData;

  const hashedPassword = await hash(password, 10);
  const user = await prisma.user.create({
    data: { name, email, password: hashedPassword },
  });

  const session = await createSession(user.id);
  return { session };
};
Enter fullscreen mode Exit fullscreen mode

Step 3: Session Management

To persist user sessions across requests, we'll create a file for session management logic, including utility functions to create, verify, update, and delete sessions.

import { sign, verify } from 'jsonwebtoken';
import { serialize, parse } from 'cookie';

const secretKey = process.env.JWT_SECRET;

export const createSession = (userId) => {
  const token = sign({ userId }, secretKey, { expiresIn: '1h' });
  const cookie = serialize('session', token, { httpOnly: true, maxAge: 3600 });
  return { cookie, userId };
};

export const verifySession = (req) => {
  const { session } = parse(req.headers.cookie || '');
  if (!session) return null;

  try {
    const payload = verify(session, secretKey);
    return payload.userId;
  } catch {
    return null;
  }
};

export const deleteSession = () => {
  const cookie = serialize('session', '', { httpOnly: true, maxAge: -1 });
  return { cookie };
};
Enter fullscreen mode Exit fullscreen mode

Step 4: Integrating Session Creation in Sign-Up

In the sign-up action, we'll use the createSession function to create a session for the user upon successful registration.

const { name, email, password } = formData;

const hashedPassword = await hash(password, 10);
const user = await prisma.user.create({
  data: { name, email, password: hashedPassword },
});

const { cookie, userId } = await createSession(user.id);
return {
  headers: { 'Set-Cookie': cookie },
  userId,
};
Enter fullscreen mode Exit fullscreen mode

Authorization: Controlling Access

Next, we need to decide what routes and data a user can access based on their roles or permissions. This is known as authorization.

Middleware for Authorization Checks

We can handle some authorization logic in middleware, checking if the current route is protected.

import { NextResponse } from 'next/server';
import { verifySession } from '../lib/session';

export function middleware(req) {
  const userId = verifySession(req);

  if (!userId && req.url.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect('/login');
  }

  return NextResponse.next();
}
Enter fullscreen mode Exit fullscreen mode

Protecting Data with a Data Access Layer

It's best practice to keep authorization logic close to where data is fetched using a data access layer. This ensures security and consistency.

import { prisma } from '../lib/prisma';
import { verifySession } from '../lib/session';

export const getUser = async (req) => {
  const userId = verifySession(req);
  if (!userId) throw new Error('Unauthorized');

  const user = await prisma.user.findUnique({ where: { id: userId } });
  return user;
};
Enter fullscreen mode Exit fullscreen mode

Minimizing Data Exposure

To reduce the risk of data leaks, minimize the data returned from APIs.

export const getUser = async (req) => {
  const userId = verifySession(req);
  if (!userId) throw new Error('Unauthorized');

  const user = await prisma.user.findUnique({
    where: { id: userId },
    select: { name: true, email: true },
  });
  return user;
};
Enter fullscreen mode Exit fullscreen mode

Conclusion

We've covered the main topics of authentication in Next.js apps, including creating sign-up forms, handling sessions, and authorization. Here are the key takeaways:

  • Use middleware for optimistic non-blocking checks.
  • Perform data fetching and compute-intensive checks within server actions.
  • Keep authorization logic close to data fetching to ensure security.
  • Minimize the data returned from APIs to reduce the risk of accidental leaks.

For further learning, explore the Next.js documentation and try out a complete example on GitHub.
I hope this guide helps you understand the principles of authentication in modern Next.js applications. If you have any questions or need further assistance, feel free to reach out. Happy coding!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .