What actually happens when you are using Firebase or Supabase as an auth provider for your web app? Both use JSON web token (JWT). Supabase has some decent introductory material to understand JWT, but I prefer to learn by doing.

Note: Authentication is asking “Who are you?”. Authorization is asking “What are you allowed to do?”. Auth refers to both.

For this guide, we will be using Node.js and Express.

Step 1: initialize the project and install packages

npm init -y
npm install express jsonwebtoken bcryptjs

Step 2: create your server

Create a new file named server.js. This is the entry point for your server and where we will be putting all of our code. It will contain all of the necessary setup for the server.

const express = require("express");
const jwt = require("jsonwebtoken");
const bcrypt = require("bcryptjs");

const app = express();

app.use(express.json()); // Enable JSON request body parsing

const users = []; // This will act as a simple in-memory "database" for this tutorial

app.listen(3000, () => console.log("Server started on port 3000"));

Step 3: add a signup route

Our signup route will take a username and password, hash the password using the imported bcrypt library, and store the user in memory.

Why hash passwords? So hackers can’t use them easily.

More info on the bcrypt hashing library can be found here.

app.post("/signup", async (req, res) => {
  const { username, password } = req.body;

  const user = users.find((user) => user.username === username);
  if (user) {
    return res.status(400).send({ message: "User already exists" });
  }

  // 10 is the "cost factor", meaning we feed our password through the hashing algorithm 2^10 times
  const hashedPassword = await bcrypt.hash(password, 10);

  users.push({ username, password: hashedPassword });

  res.status(201).send();
});

Step 4: add a login route

Our login route will find the user in our mock database with the username provided. Then, it will check the hashed passwords. If they match, we return a signed JWT token. What’s that???

Here’s a great analogy I shamelessly used ChatGPT to help create: a JWT is like a VIP wristband at a festival. When you enter (login) to the festival (website), you get a wristband (JWT) after showing your ticket (user credentials). Now, every time you go to a different part of the festival, you don’t have to show your ticket again - you just show your wristband. The staff at the festival can trust that you’re allowed to be there because you have the wristband, and they know nobody else could have your wristband because it’s attached to your wrist and can’t be removed without tearing it.

This is similar to how a JWT works in an online application. When you first log in, you provide your username and password. The server checks these, and if they’re correct, it provides a JWT. Then, for future requests, you just include this JWT, and the server knows you’re the same person who logged in earlier without having to ask for your username and password again.

// In a real application, don't store secrets in code (use an environment variable instead)
const jwtSecret = "the-key-with-a-bent-wing";

app.post("/login", async (req, res) => {
  const { username, password } = req.body;

  const user = users.find((user) => user.username === username);
  if (!user || !(await bcrypt.compare(password, user.password))) {
    return res.status(400).send({ message: "Invalid username or password" });
  }

  const token = jwt.sign({ username: user.username }, jwtSecret);

  res.send({ token });
});

Step 5: add an authenticated route

We’ve made it into the music festival and now we’re trying to get in the VIP area. However, to prevent just anyone from creating a fake VIP wristband and getting in, the festival organizers have a special strategy in place - they use a secret ingredient in the ink used to print the VIP wristbands.

The night before the festival, the security team quietly decides on this secret ingredient. This year, they’ve decided on adding a rare glow-in-the-dark element to the ink. Now, under regular light, all wristbands might look the same. But when security shines a special UV flashlight on it, only the true VIP wristbands with the secret ingredient will glow. This is their way of ensuring that only legitimate VIP wristbands grant access to the VIP area.

This is similar to how a JWT secret works. The secret is used to “sign” the token, similar to how the secret ingredient is used in the ink. When a server receives a token, it uses the secret to verify it, just like the UV flashlight reveals the glowing ink. Unless someone else knows the exact secret, they can’t create a token that the server will accept, just as someone without the secret ingredient can’t create a wristband that will glow under the UV flashlight.

const authenticateJWT = (req, res, next) => {
  const authHeader = req.headers.authorization;

  if (authHeader) {
    const token = authHeader.split(" ")[1];

    jwt.verify(token, jwtSecret, (err, user) => {
      if (err) {
        return res.sendStatus(403);
      }

      req.user = user;
      next();
    });
  } else {
    res.sendStatus(401);
  }
};

app.get("/protected", authenticateJWT, (req, res) => {
  res.send({ message: "You are authenticated", user: req.user });
});

Step 6: testing the server

We will test with curl in the command line. First, we will run our server:

node server.js

Then, open a new terminal and let’s try to access our protected route without a JWT:

curl -X GET -H "Content-Type: application/json" http://localhost:3000/protected

We get an “Unauthorized” response as expected. Let’s go through the full auth flow, starting with signing up:

curl -X POST -H "Content-Type: application/json" -d '{"username": "testuser", "password": "testpassword"}' http://localhost:3000/signup

You should get a response in the form of {"token":"eyJhbGciOiJI..."}. Now, copy the token from the login response and use it to access the protected route:

curl -X GET -H "Content-Type: application/json" -H "Authorization: Bearer eyJhbGciOiJI..." http://localhost:3003/protected

We are in the VIP area! If you’d like to dig deeper, I highly recommend this extensive article on JWT alternatives.