Going ProfessionalPro· 55 min read

Authentication & Authorization (JWT + bcrypt)

Let users sign up and log in, store passwords safely, and lock routes so only the right people get in — the single most expected back-end skill.

What you will learn

  • Hash passwords with bcrypt (never store them as plain text)
  • Issue and verify JSON Web Tokens (JWT) for login
  • Protect routes with middleware and check roles (authorization)

Two words that sound the same but are not

Two ideas sit at the heart of securing an app, and beginners constantly mix them up:

  • Authentication = Who are you? Proving identity, usually with email + password at login.
  • Authorization = What are you allowed to do? Deciding, once we know who you are, whether you may delete that post or view that admin page.

You log in once (authentication), and then on every later request the server checks your permissions (authorization). We will build both.

Rule #1 — never store a real password

If you save passwords as plain text and your database leaks, every user’s password is exposed — and because people reuse passwords, you have handed away their other accounts too. The fix is hashing: running the password through a one-way function that turns "hunter2" into a long scrambled string. You can check a guess against the hash, but you can never turn the hash back into the password. The standard tool is bcrypt.

Terminal: install bcrypt and the JWT library
npm install bcrypt jsonwebtoken

Note: Output: added several packages in 2s bcrypt hashes and checks passwords; jsonwebtoken creates and verifies the login tokens we use below.

Hashing and checking a password with bcrypt
const bcrypt = require("bcrypt");

// When a user signs up — hash before saving
const hashed = await bcrypt.hash("hunter2", 10);
// hashed looks like: $2b$10$N9qo8uLOickgx2ZMRZo...  (store THIS, not the password)

// When a user logs in — compare their guess to the stored hash
const ok = await bcrypt.compare("hunter2", hashed);   // true
const bad = await bcrypt.compare("wrong", hashed);    // false

How it works: bcrypt.hash(password, 10) scrambles the password; the 10 is the salt rounds — how much work bcrypt does, making it slow enough that guessing millions of passwords is impractical. You store only the returned hash. At login, bcrypt.compare(guess, hash) returns true if the guess matches and false if not — without ever un-scrambling the stored hash.

Note: Output: (hashed) $2b$10$N9qo8uLOickgx2ZMRZo... (ok) true (bad) false Notice you can verify a password without ever knowing it. Even you, the developer, cannot read your users’ passwords — exactly as it should be.

Rule #2 — hand out a token at login (JWT)

HTTP is stateless: the server forgets you the instant a request ends, so the next request would not know you just logged in. The modern fix is a JWT (JSON Web Token, said "jot") — a signed string the server gives you when you log in. You send it back on every later request; the server verifies the signature to confirm it is genuine and learns who you are, without storing a session.

Issue a JWT when the user logs in
const jwt = require("jsonwebtoken");

// On successful login — create a token that says "this is user 42"
const token = jwt.sign(
  { userId: 42, role: "user" },     // the data baked into the token
  process.env.JWT_SECRET,           // a secret only the server knows
  { expiresIn: "1h" }               // the token stops working after 1 hour
);
// Send token back to the client; it stores it and sends it on each request.

Reading it: jwt.sign(payload, secret, options) builds the token. The first object is the payload — small, non-secret facts like the user’s id and role. The JWT_SECRET (kept in .env) is the private key the server signs with; anyone who tampers with the token breaks the signature and is rejected. expiresIn: "1h" makes the token self-destruct after an hour, so a stolen token is not useful forever.

Watch out: A JWT is signed, not encrypted — anyone can read its payload. Never put a password or other secret inside it. Put only safe identifiers like a user id and role.

Rule #3 — a middleware that guards routes

Now lock routes. A small auth middleware runs before protected handlers: it reads the token from the request, verifies it, and either attaches the user to req and calls next(), or rejects with 401 (Unauthorized).

Auth middleware protecting a route
function requireAuth(req, res, next) {
  const header = req.headers.authorization || "";
  const token = header.replace("Bearer ", "");     // "Bearer <token>"
  if (!token) return res.status(401).json({ error: "No token" });

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;        // { userId, role } now available to handlers
    next();                    // token is valid — let the request through
  } catch {
    return res.status(401).json({ error: "Invalid or expired token" });
  }
}

// Protected route — only runs if requireAuth called next()
app.get("/profile", requireAuth, (req, res) => {
  res.json({ message: "Secret profile", userId: req.user.userId });
});

Step by step: the client sends the token in an Authorization: Bearer <token> header, so we strip the "Bearer " prefix to get the raw token. jwt.verify(token, secret) checks the signature and expiry; if valid it returns the payload, which we stash on req.user for the handler to use, then call next(). If the token is missing, forged, or expired, jwt.verify throws, the catch fires, and the request is rejected with 401 — the protected handler never runs.

Note: Output (GET /profile with a valid token): {"message":"Secret profile","userId":42} Output (GET /profile without a token): {"error":"No token"} The same route behaves completely differently depending on whether a valid token is present. That is authentication enforced at the door.

Authorization: checking roles

Knowing who you are is not the same as being allowed. For admin-only actions, add a second tiny middleware that checks the role baked into the token:

Role-based authorization (403 Forbidden)
function requireAdmin(req, res, next) {
  if (req.user.role !== "admin") {
    return res.status(403).json({ error: "Admins only" });
  }
  next();
}

// Stack both: must be logged in AND an admin
app.delete("/users/:id", requireAuth, requireAdmin, (req, res) => {
  res.json({ message: "User deleted" });
});

Note: Output (a logged-in non-admin calling DELETE /users/5): {"error":"Admins only"} Note the status is 403 (Forbidden), not 401. 401 means "I do not know who you are"; 403 means "I know who you are, but you are not allowed". requireAuth runs first to identify the user, then requireAdmin checks their role — middleware stacking lets you compose security rules cleanly.

The whole login flow, end to end

  1. Signup: client sends email + password → server hashes the password with bcrypt → saves the user (with the hash, never the password).
  2. Login: client sends email + password → server looks up the user and runs bcrypt.compare → if it matches, jwt.sign issues a token → token goes back to the client.
  3. Authenticated request: client sends the token in the Authorization header → requireAuth verifies it → the protected handler runs.
  4. Authorization: for restricted actions, requireAdmin (or a role check) decides whether this authenticated user may proceed.

Tip: Storing the token on the client: a simple approach is the browser’s localStorage, but the more secure option is an httpOnly cookie — a cookie JavaScript cannot read, which protects the token from XSS attacks. Pair short-lived access tokens with a longer-lived refresh token to re-issue them, and you have the same login lifecycle real companies use.

Q. What is the difference between authentication and authorization?

Answer: Authentication = identity (who are you?), proven at login. Authorization = permissions (what may you do?), checked on each protected action. You authenticate once, then authorize per request.

✍️ Practice

  1. Build a /signup route that hashes the password with bcrypt before saving, and a /login route that compares with bcrypt.compare and returns a JWT on success.
  2. Write a requireAuth middleware and protect a /profile route, testing it with and without a valid token.

🏠 Homework

  1. Add role-based authorization: give some users a role: "admin", and protect a DELETE route so only admins (403 otherwise) can use it.
Want to learn this with a mentor?

CodingClave runs guided, project-based training (28-day, 45-day & 6-month batches).

Explore Training →