Learning Node.js - Basic Authentication and Authorization
Episode 11 of 23

Learning Node.js - Basic Authentication and Authorization

This episode adds the first security layer to the API: storing passwords with hashes, issuing and verifying JWT tokens, authentication middleware, and role-based access control to distinguish users' access rights.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

An API that serves user data must not let just anyone access it. This is where authentication and authorization come in: authentication proves who you are, authorization determines what you're allowed to do.

Episode 11 builds both practically with packages common in the Node.js ecosystem: bcryptjs for password hashing, jsonwebtoken for tokens, and Express middleware for verification. You'll secure endpoints, then differentiate access based on user roles.

Storing Passwords Securely

Hash Is Not Encryption

Storing passwords in plain text is a disaster waiting to happen. The correct practice: store a hash — the result of a one-way function that can't be reversed into the original password. bcryptjs provides hashing with automatic salt and a cost factor:

Install authentication packages
npm install bcryptjs jsonwebtoken

npm install bcryptjs jsonwebtoken adds both packages at once. Bcrypt is chosen because it's deliberately slow — that's precisely its strength, slowing down brute-force attacks.

Hashing and Verifying Passwords

JSHash and verify passwords
import bcrypt from "bcryptjs";
 
const hash = await bcrypt.hash("rahasia123", 10);
console.log(hash);
 
const cocok = await bcrypt.compare("rahasia123", hash);
console.log(cocok);

bcrypt.hash("rahasia123", 10) produces a hash string with a cost factor of 10, and bcrypt.compare(password, hash) verifies without storing or reading the original password. Store the hash in the database — if the database leaks, users' passwords remain safe.

JWT Tokens

Token Structure

JWT (JSON Web Token) is a stateless way to carry identity: the token contains a header, payload, and signature, encoded in three parts separated by dots. Because it's signed, its contents can't be altered without the server knowing.

JSIssue and verify a JWT
import jwt from "jsonwebtoken";
 
const token = jwt.sign(
  { userId: 42, role: "admin" },
  process.env.JWT_SECRET,
  { expiresIn: "1h" },
);
 
const payload = jwt.verify(token, process.env.JWT_SECRET);
console.log(payload.userId, payload.role);

jwt.sign(payload, secret, { expiresIn: "1h" }) issues a token that expires in one hour, and jwt.verify checks its authenticity and validity. The value process.env.JWT_SECRET must be stored in an environment variable — never hardcode it in code, as we'll discuss in episode 12.

Authentication Middleware

Protecting Endpoints with a Bearer Token

The client sends the token in the Authorization header in the format Bearer <token>. Authentication middleware checks the header, verifies the token, then attaches the identity to req.user:

JSAuthentication middleware
function auth(req, res, next) {
  const header = req.headers.authorization;
  if (!header || !header.startsWith("Bearer ")) {
    return res.status(401).json({ error: "Token tidak ditemukan" });
  }
  try {
    req.user = jwt.verify(header.slice(7), process.env.JWT_SECRET);
    next();
  } catch {
    return res.status(401).json({ error: "Token tidak valid" });
  }
}

header.slice(7) takes the token after the word Bearer . If the token is missing, expired, or corrupted, the middleware replies with 401 and stops the chain. If valid, req.user holds the payload ready for the next handler.

Mounting the Middleware on Routes

Attach auth to the routes that need protection:

JSProtected route
app.get("/api/profil", auth, (req, res) => {
  res.json({ userId: req.user.userId });
});

app.get("/api/profil", auth, ...) inserts the auth middleware between the route definition and the handler — only requests with a valid token reach the handler. Public routes like login stay unprotected.

Role-Based Authorization

Checking Access Rights

Authentication only answers who you are. Authorization answers what you're allowed to do. When the JWT payload carries role, create a second middleware to check the role:

JSAuthorization based on role
function role(peran) {
  return (req, res, next) => {
    if (req.user.role !== peran) {
      return res.status(403).json({ error: "Akses ditolak" });
    }
    next();
  };
}
 
app.delete("/api/pengguna/:id", auth, role("admin"), (req, res) => {
  res.json({ terhapus: req.params.id });
});

The pattern role("admin") returns middleware that compares req.user.role with the allowed role. An important code distinction: 401 for not authenticated, 403 for logged in but not authorized. The delete-user endpoint above can only be run by an admin.

JWT and Other Strategies

JWT suits APIs and stateless applications. For traditional web applications, sessions based on cookies are often simpler — we'll compare the two in episode 16. The key in this episode: authentication and authorization are two separate layers, each with its own middleware, and both are easy to test independently.

Closing

Here's what to take away:

  • Store passwords as bcrypt hashes, not plain text or encryption.
  • JWT carries a signed, stateless identity.
  • JWT_SECRET is stored in an environment variable, not in code.
  • The auth middleware verifies the token and fills req.user.
  • 401 means not logged in; 403 means not authorized.
  • role("admin") restricts an endpoint to a specific role.

In the next episode, episode 12, we'll discuss logging, environment config, and runtime settings — loading configuration from a .env file, distinguishing NODE_ENV, structured logging with pino, and the right logging levels for production. Clean configuration is a requirement for an application that can be deployed.

Learning Node.js - Basic Authentication and Authorization | Learn Node.js