DEV Community

Vishal Yadav
Vishal Yadav

Posted on

Mastering Authentication in Modern Next.js Apps

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!

Top comments (6)

Collapse
 
leandro_nnz profile image
Leandro Nuñez

Good article! Thank you for sharing!

Collapse
 
vyan profile image
Vishal Yadav

My Pleasure!

Collapse
 
good_stud profile image
Brian Taylor

It was a nice read... also could you please provide github repo with the project code (if possible). It would help me learning more about what I read about in the article..

Thanks.. loved the article

Collapse
 
vyan profile image
Vishal Yadav

Actually I didn't created GitHub repo for this!

Collapse
 
vyan profile image
Vishal Yadav

Thanks!

Collapse
 
eshimischi profile image
Info Comment hidden by post author - thread only accessible via permalink
eshimischi

Using Prisma is definitely not for beginners and besides you said nothing in your article about what exactly is Prisma, why did you choose it. No source code.

Some comments may only be visible to logged-in visitors. Sign in to view all comments. Some comments have been hidden by the post's author - find out more