DEV Community

Rodolphe Dupuis
Rodolphe Dupuis

Posted on

Securing Your Fullstack App: Authentication & Authorization with JWT in Next.js and Node πŸ”’ πŸš€

Securing your fullstack application is critical to protect user data and maintain trust. One of the most common ways to implement authentication and authorization in modern applications is through JSON Web Tokens (JWT).

In this guide, we’ll explore how to secure your Next.js app by setting up JWT-based authentication and implementing role-based authorization.

What Is JWT and Why Use It?

JWT is a compact, self-contained way to securely transmit information between parties as a JSON object. It is often used for:

  • Authentication: Verifying the identity of a user.
  • Authorization: Controlling access to resources based on user roles or permissions.

How JWT works

  1. Login: The client sends login credentials (e.g., email and password) to the backend.
  2. Token generation: If valid, the server generates a JWT containing user details and sends it back.
  3. Storage: The client stores the JWT (e.g., in httpOnly cookies or local storage).
  4. Subsequent Requests: The client includes the JWT in the request headers to access protected resources.
  5. Verification: The server verifies the token's validity before granting access.

Step 1: Set Up Your Next.js and Node.js Environment

Before diving into the code, ensure you have the following:

  • Node.js installed.
  • A new Next.js project ready to run: run the command npx create-next-app@latest your-project-name to create a Next.js app.
  • The following dependencies installed on your backend folder: npm install jsonwebtoken bcryptjs express cors body-parser cookie-parser

Your Next.js environment is now ready to create some magic πŸš€


Step 2: Build the Backend API

We’ll use Node.js with Express to handle authentication.

Create the Express server

const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const bodyParser = require('body-parser');
const cors = require('cors');
const cookieParser = require('cookie-parser');

const app = express();
const PORT = 5001;

// Middleware
app.use(cors({ origin: 'http://localhost:3000', credentials: true }));
app.use(bodyParser.json());
app.use(cookieParser());

// Secret key for JWT
const SECRET_KEY = 'your-secret-key';

// Mock user database
const users = [
  { id: 1, email: 'user@user.com', password: bcrypt.hashSync('aw3$0m3AndHaRdPwD!', 10), role: 'user' },
  { id: 2, email: 'admin@admin.com', password: bcrypt.hashSync('aw3$0m3AndHaRdPwDButAdm1n!', 10), role: 'admin' },
];
Enter fullscreen mode Exit fullscreen mode

Create Login route

app.post('/api/login', (req, res) => {
  const { email, password } = req.body;
  const user = users.find(u => u.email === email);

  if (!user || !bcrypt.compareSync(password, user.password)) {
    return res.status(401).json({ message: 'Invalid email or password' });
  }

  // Generate JWT
  const token = jwt.sign({ id: user.id, role: user.role }, SECRET_KEY, { expiresIn: '1h' });

  // Set as httpOnly cookie
  res.cookie('token', token, { httpOnly: true }).json({ message: 'Logged in successfully' });
});
Enter fullscreen mode Exit fullscreen mode

Create Protected route

app.get('/api/protected', (req, res) => {
  const token = req.cookies.token;

  if (!token) {
    return res.status(401).json({ message: 'Unauthorized' });
  }

  try {
    const decoded = jwt.verify(token, SECRET_KEY);
    res.json({ message: 'Welcome to the protected route!', user: decoded });
  } catch (err) {
    res.status(401).json({ message: 'Invalid token' });
  }
});
Enter fullscreen mode Exit fullscreen mode

Create Logout route

app.post('/api/logout', (req, res) => {
  res.clearCookie('token').json({ message: 'Logged out successfully' });
});
Enter fullscreen mode Exit fullscreen mode

Don't forget to add at the end of your file:

// Start the server
app.listen(PORT, () => {
    console.log(`Server is running on http://localhost:${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

This is a very basic implementation of an Express app that will allow us to send HTTP request on the implemented routes 🎯

Start the server to test using the commande: node server.js where server.js is the name of your file.


Step 3: Implement the Next.js frontend

First of all, don't forget to add the axios dependency by using the command: npm install axios in your Next.js app

Login page

Create a new file pages/login.js:

import { useState } from 'react';
import axios from 'axios';

export default function Login() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleLogin = async (e) => {
    e.preventDefault();
    try {
      const response = await axios.post('http://localhost:5000/api/login', { email, password }, { withCredentials: true });
      alert(response.data.message);
    } catch (err) {
      alert(err.response?.data?.message || 'Login failed');
    }
  };

  return (
    <form onSubmit={handleLogin}>
      <input type="email" placeholder="Email" value={email} onChange={(e) => setEmail(e.target.value)} />
      <input type="password" placeholder="Password" value={password} onChange={(e) => setPassword(e.target.value)} />
      <button type="submit">Login</button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

This page renders a very simple login form where you can use the login information you added in the users array variable in your server implementation. This is obviously not a good thing to do and you should never do this, this is a simple implementation to demonstrate the power of JWT security πŸ’ͺ

Add a protected page

Create a new file pages/protected.js:

import axios from 'axios';
import { useEffect, useState } from 'react';

export default function Protected() {
  const [data, setData] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await axios.get('http://localhost:5000/api/protected', { withCredentials: true });
        setData(response.data);
      } catch (err) {
        alert('Access denied');
      }
    };
    fetchData();
  }, []);

async function handleLogout() {
        try {
            await axios.post('http://localhost:5001/api/logout', {}, { withCredentials: true });
            router.push('/login');
        } catch (error) {
            alert('Could not logout');
        }
    }

  if (!data) return <p>Loading...</p>;

  return <div>
        {JSON.stringify(data)}
        <button onClick={handleLogout}>Logout</button>
    </div>;
}
Enter fullscreen mode Exit fullscreen mode

This page renders the result of the server GET request on the route /api/protected βœ…

If you try to access this route without being logged in, an error alert will be displayed on the page and you will not be able to access its content ❌


Step 4: Add Role-Based Authorization

Enhance your backend to enforce roles:

function authorizeRoles(allowedRoles) {
  return (req, res, next) => {
    const token = req.cookies.token;
    if (!token) return res.status(401).json({ message: 'Unauthorized' });

    try {
      const decoded = jwt.verify(token, SECRET_KEY);
      if (!allowedRoles.includes(decoded.role)) {
        return res.status(403).json({ message: 'Forbidden' });
      }
      req.user = decoded;
      next();
    } catch (err) {
      res.status(401).json({ message: 'Invalid token' });
    }
  };
}

// Example: Admin-only route
app.get('/api/admin', authorizeRoles(['admin']), (req, res) => {
  res.json({ message: 'Welcome Admin!' });
});
Enter fullscreen mode Exit fullscreen mode

This route uses a validation middleware authorizeRoles that is the function we added right before. This function verifies there is a token and that the user trying to access the resource has the right authorization.


Conclusion

So here is how you can simply, and very fast (as always with my guides 🀩), secure your fullstack application with JWT technology in Next.js and Node.

This approach provides a reliable and scalable solution for managing user sessions while ensuring a seamless user experience.

Stay proactive about security by keeping dependencies updated, implementing HTTPS, and following best practices for managing tokens.

Happy coding!

Top comments (0)