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>
);
}
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 };
};
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 };
};
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,
};
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();
}
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;
};
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;
};
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)
Good article! Thank you for sharing!
My Pleasure!
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
Actually I didn't created GitHub repo for this!
Thanks!
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