Mastering Authentication in Modern Next.js Apps
Tags: webdev, beginners, react, nextjs
Published: 2024-06-24 02:55:42

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.

```jsx
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>
  );
}
```

### 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.

```typescript
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 };
};
```

### 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.

```typescript
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 };
};
```

### 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.

```typescript
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,
};
```

## 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.

```typescript
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();
}
```

### 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.

```typescript
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;
};
```

### Minimizing Data Exposure

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

```typescript
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;
};
```

## 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!