Next.js Auth System Using JWT, Roles, and Permissions: A Simple Guide

Author

Kritim Yantra

Aug 08, 2025

Next.js Auth System Using JWT, Roles, and Permissions: A Simple Guide

Ever built a Next.js app, only to realize you have no idea how to handle authentication securely? 😅 You're not alone!

I remember my first time trying to implement user roles—"Should I store permissions in localStorage? How do I protect admin routes?"—it was a mess. I ended up with spaghetti code, security holes, and a lot of frustration.

But here’s the good news: JWT (JSON Web Tokens) + role-based access control doesn’t have to be complicated. In this guide, I’ll walk you through a simple, secure, and scalable authentication system for Next.js using JWT, roles, and permissions—without overcomplicating things.

Let’s dive in!


What We’ll Cover

✅ JWT Authentication – Secure user login & token management
✅ Role-Based Access Control (RBAC) – Define user roles (e.g., Admin, User, Guest)
✅ Permission Checks – Restrict access to pages & API routes
✅ Protecting Routes – Server-side & client-side guards

By the end, you’ll have a production-ready auth system. 🚀


Step 1: Setting Up Next.js & JWT Auth

1. Install Dependencies

We’ll use:

  • jsonwebtoken (JWT generation & verification)
  • bcryptjs (password hashing)
  • next-iron-session (secure session management)
npm install jsonwebtoken bcryptjs next-iron-session

2. Create a Basic Login API

// pages/api/login.js
import jwt from 'jsonwebtoken';
import bcrypt from 'bcryptjs';

export default async function handler(req, res) {
  if (req.method !== 'POST') return res.status(405).end();

  const { email, password } = req.body;

  // 1. Find user in DB (mock example)
  const user = { id: 1, email: 'user@example.com', password: '$2a$10$hashedPassword', role: 'user' };

  // 2. Verify password
  const isValid = await bcrypt.compare(password, user.password);
  if (!isValid) return res.status(401).json({ error: 'Invalid credentials' });

  // 3. Generate JWT
  const token = jwt.sign(
    { userId: user.id, role: user.role },
    process.env.JWT_SECRET,
    { expiresIn: '1h' }
  );

  res.status(200).json({ token });
}

Step 2: Storing & Validating JWT Securely

Option 1: HTTP-Only Cookies (Recommended for Security)

// pages/api/login.js (updated)
res.setHeader('Set-Cookie', `token=${token}; HttpOnly; Secure; Path=/; SameSite=Strict`);
res.status(200).json({ success: true });

Option 2: localStorage (for Frontend Access)

// After successful login (client-side)
localStorage.setItem('token', token);

Step 3: Role-Based Access Control (RBAC)

1. Define User Roles

const ROLES = {
  ADMIN: 'admin',
  USER: 'user',
  GUEST: 'guest',
};

2. Protect API Routes (Middleware)

// pages/api/admin-route.js
import jwt from 'jsonwebtoken';

export default function handler(req, res) {
  const token = req.cookies.token || req.headers.authorization?.split(' ')[1];
  
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    
    // Check if user is admin
    if (decoded.role !== ROLES.ADMIN) {
      return res.status(403).json({ error: 'Forbidden' });
    }

    // Proceed if authorized
    res.status(200).json({ secretAdminData: "You're in!" });
  } catch (err) {
    res.status(401).json({ error: 'Unauthorized' });
  }
}

Step 4: Protecting Frontend Routes

1. Client-Side Guard (Next.js Middleware or useEffect Check)

// hooks/useAuth.js
import { useEffect } from 'react';
import { useRouter } from 'next/router';

export function useAuth(requiredRole) {
  const router = useRouter();

  useEffect(() => {
    const token = localStorage.getItem('token');
    if (!token) router.push('/login');

    const decoded = jwt.decode(token);
    if (decoded.role !== requiredRole) router.push('/unauthorized');
  }, []);
}

2. Usage in a Protected Page

// pages/admin/dashboard.js
import { useAuth } from '../../hooks/useAuth';

export default function AdminDashboard() {
  useAuth('admin'); // Redirects if not admin

  return <div>Welcome, Admin!</div>;
}

Step 5: Handling Permissions (Optional)

For fine-grained control:

// utils/permissions.js
const PERMISSIONS = {
  DELETE_POST: 'delete_post',
  EDIT_USER: 'edit_user',
};

export function hasPermission(user, permission) {
  return user.permissions?.includes(permission);
}

Final Thoughts & Best Practices

✔ Never store JWT in localStorage if you can avoid it (use HTTP-only cookies).
✔ Always validate tokens server-side (don’t trust client-side checks).
✔ Use short-lived tokens (e.g., 1h expiry) + refresh tokens for better security.


FAQs (Common Struggles)

Q: Should I use Next.js API routes or an external auth service like Auth0?
A: For small apps, Next.js API is fine. For production, consider Auth0/Firebase for scalability.

Q: How do I handle token expiration?
A: Implement a refresh token system or redirect to login when expired.

Q: Is JWT secure enough?
A: Yes, if used correctly (secure storage, short expiry, proper validation).


Your Turn!

What’s your biggest struggle with Next.js auth? JWT confusion? Role management? Let me know in the comments! 👇

Happy coding! 🚀🔥

Comments

No comments yet. Be the first to comment!

Please log in to post a comment:

Sign in with Google

Related Posts