alirezasaremi.com logo

Alireza Saremi

Building Secure Auth in Next.js Without Third-Party Libraries

2025-05-18

Next.js

Authentication is a critical part of any web application. While there are many libraries and SaaS providers that offer plug‑and‑play solutions, building your own auth system gives you full control and avoids vendor lock‑in. In this post we show how to implement a simple yet secure authentication flow in Next.js using modern Web APIs and cryptographic best practices.

Table of Contents

1. Understanding Authentication Basics

Authentication answers the question “Who are you?”. A typical flow involves registering a new account, storing a hashed password in a database and logging in with those credentials. The server verifies the credentials and creates a session that the client uses for subsequent requests. To stay secure, you must protect passwords, prevent session hijacking and implement proper session expiry.

2. Storing Passwords Securely

Never store plain text passwords. Instead, use a slow hashing function such as bcrypt, scrypt or Argon2. These algorithms deliberately take time to compute, making brute force attacks impractical. Add a unique random salt to each password so that identical passwords produce different hashes. When the user logs in, hash the provided password with the same salt and compare it to the stored hash.

// api/register.ts
import bcrypt from 'bcryptjs';
import { saveUser } from '@/lib/db';

export default async function handler(req, res) {
  const { email, password } = req.body;
  const salt = await bcrypt.genSalt(10);
  const hash = await bcrypt.hash(password, salt);
  await saveUser({ email, passwordHash: hash });
  res.status(201).end();
}

This example uses bcryptjs, but you could also use Web Crypto in modern browsers. Always validate input to prevent SQL injection and store only the hash, never the original password.

3. Sessions vs JSON Web Tokens

Once a user is authenticated, you need a way to remember them across requests. Sessions store a secret on the server (often in memory or a database) and set a cookie with a session identifier. JSON Web Tokens (JWTs) store claims in the token itself, which the server verifies using a signature. Sessions are easier to revoke and generally more secure because the token never leaves the server. JWTs scale better in stateless environments but must be handled carefully: avoid storing sensitive data in a JWT and implement short expirations.

4. Implementing Signup and Login in Next.js

In Next.js you can create API routes for signup and login. Usefetch on the client to call these endpoints. After verifying the password, create a session cookie using theSet‑Cookie header. Mark the cookie as HttpOnlyso JavaScript cannot access it and set SameSite=Lax to protect against CSRF attacks. Here’s a simplified login route:

// api/login.ts
import bcrypt from 'bcryptjs';
import { getUserByEmail, createSession } from '@/lib/db';

export default async function handler(req, res) {
  const { email, password } = req.body;
  const user = await getUserByEmail(email);
  const isValid = user && (await bcrypt.compare(password, user.passwordHash));
  if (!isValid) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }
  const sessionId = await createSession(user.id);
  res.setHeader('Set-Cookie', `sid=${sessionId}; HttpOnly; Path=/; SameSite=Lax`);
  res.status(200).json({ message: 'Logged in' });
}

When the client receives the cookie, it will be sent automatically on subsequent requests to protected routes. On each request you look up the session ID in your database and retrieve the user. Be sure to set an expiry time and rotate session IDs on login to prevent fixation attacks.

5. Conclusion

Rolling your own authentication in Next.js is entirely possible and gives you flexibility over the user experience. The keys to security are hashing passwords properly, choosing an appropriate session mechanism and protecting cookies. By understanding these basics and leveraging Next.js API routes, you can build a robust auth system without relying on third‑party libraries.