learning

Phase 3: Authentication & Security

2026 words11 min read
Phase 3: Authentication & Security
Authors

Welcome to Phase 3. Building a beautiful, functional, and lightning-fast application is an incredible achievement. But if that application is fundamentally insecure, it is nothing more than a liability. In the realm of backend engineering, security is paramount. A single misconfiguration or overlooked vulnerability can lead to data breaches, ruined reputations, and catastrophic financial losses.

In this massive, textbook-level guide, we will tear apart the concepts of Identity, Authentication, and Application Security. Most importantly, we will take a microscope to JSON Web Tokens (JWT)—exploring exactly how they work under the hood and how to implement them securely.


Chapter 1: The Identity Foundation

Before diving into implementations, we must clarify the terminology. Identity management fundamentally breaks down into two distinct phases.

Authentication (AuthN) vs. Authorization (AuthZ)

Authentication (AuthN): "Who are you?" Authentication is the process of verifying a user's identity.

  • Analogy: Showing your passport at border control. It proves you are who you claim to be.
  • Methods: Passwords, Biometrics, One-Time Passwords (OTPs), Magic Links.

Authorization (AuthZ): "What are you allowed to do?" Authorization happens after you are authenticated. It determines your permissions and access levels.

  • Analogy: You are inside the office building (Authenticated), but your keycard only lets you into the Engineering wing, not the CEO's office (Authorization).
  • Models:
    • RBAC (Role-Based Access Control): Permissions are assigned to roles (e.g., Admin, Editor, Viewer).
    • ABAC (Attribute-Based Access Control): Granular permissions based on context (e.g., "User can edit this document ONLY IF document.author_id === user.id AND time < 5pm).

Chapter 2: The Masterclass on JWT (JSON Web Tokens)

In Phase 2, we touched upon Session-based authentication, where the server stores session data in memory or Redis and sends the client a generic Session ID cookie.

As architectures evolved into distributed Microservices, managing a central Redis session store became a bottleneck. The industry needed a Stateless authentication mechanism. Enter JWT (JSON Web Tokens).

What is a JWT under the hood?

A JWT is not an encrypted, magical string. It is a standardized way to securely represent claims between two parties. The key concept is that the token itself contains all the user data needed by the server. The server does not need to look up a session in a database; it simply verifies the token.

If you look at a raw JWT, it looks like this: xxxxx.yyyyy.zzzzz

It consists of three parts, separated by dots:

  1. Header (xxxxx)
  2. Payload (yyyyy)
  3. Signature (zzzzz)

Let's break them down.

1. The Header

The header typically consists of two parts: the type of the token (JWT) and the signing algorithm being used (usually HMAC SHA256 or RSA).

{
  "alg": "HS256",
  "typ": "JWT"
}

This JSON is then Base64Url encoded to form the first part of the token. (Note: Base64 encoding is NOT encryption. Anyone can decode it easily).

2. The Payload (Claims)

The payload contains the "claims"—statements about an entity (typically, the user) and additional data.

{
  "sub": "1234567890",
  "name": "John Doe",
  "role": "admin",
  "iat": 1516239022,
  "exp": 1516242622
}
  • sub (Subject): The user ID.
  • iat (Issued At): When the token was created.
  • exp (Expiration Time): When the token dies.

This JSON is also Base64Url encoded to form the second part of the token. At this point, anyone who intercepts your token can decode it and read "John Doe" and "admin". Never put sensitive data like passwords or social security numbers in a JWT payload.

3. The Signature (The Magic)

If anyone can read the payload, what stops a malicious user from changing "role": "user" to "role": "admin", re-encoding it in Base64, and sending it to your server?

The Signature prevents this.

To create the signature part, you take the encoded header, the encoded payload, a secret key (which exists ONLY on your backend server), and the algorithm specified in the header.

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  YOUR_SUPER_SECRET_KEY
)

The resulting cryptographic hash forms the third part of the token.

How Verification Works:

  1. A client sends the JWT to the server.
  2. The server takes the Header and Payload from the token.
  3. The server runs them through the HMACSHA256 algorithm again, using its own YOUR_SUPER_SECRET_KEY.
  4. The server compares the resulting hash with the Signature attached to the token.
  5. If they match exactly, the token is valid, and the server trusts the payload. If a hacker changed "user" to "admin", the payloads wouldn't match the signature, and the server would instantly reject it.

Step-by-Step Implementation: Secure JWT in Node.js

Let's write a bulletproof JWT implementation using Express.js. We will use the Access Token + Refresh Token pattern.

Why two tokens? If an Access Token is stolen, the hacker has full access. Therefore, Access Tokens should have a very short lifespan (e.g., 15 minutes). But forcing users to log in every 15 minutes is terrible UX. The solution: We issue a long-lived Refresh Token (e.g., 7 days) stored securely as an HttpOnly cookie. When the short-lived Access Token expires, the client silently sends the Refresh Token to a special endpoint to get a new Access Token.

Step 1: Login and Token Generation

When the user logs in successfully, we generate both tokens.

const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const app = express();
app.use(express.json());

// In production, load these from environment variables!
const ACCESS_TOKEN_SECRET = "super_secret_access_key";
const REFRESH_TOKEN_SECRET = "super_secret_refresh_key";

app.post('/login', async (req, res) => {
  const { username, password } = req.body;
  
  // 1. Authenticate user (pseudo-code DB lookup)
  const user = await db.findUser(username);
  if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
    return res.status(401).json({ error: "Invalid credentials" });
  }

  // 2. Create the Payload
  const payload = { userId: user.id, role: user.role };

  // 3. Generate Access Token (Short-lived)
  const accessToken = jwt.sign(payload, ACCESS_TOKEN_SECRET, { expiresIn: '15m' });

  // 4. Generate Refresh Token (Long-lived)
  const refreshToken = jwt.sign(payload, REFRESH_TOKEN_SECRET, { expiresIn: '7d' });

  // 5. Save the refresh token in the Database! 
  // (Crucial so we can revoke it later if the user logs out or gets hacked)
  await db.saveRefreshToken(user.id, refreshToken);

  // 6. Send tokens to client. 
  // Send Access Token in JSON response.
  // Send Refresh Token as an HttpOnly, Secure Cookie.
  res.cookie('jwt_refresh', refreshToken, {
    httpOnly: true, // Javascript cannot access this cookie (Prevents XSS)
    secure: true,   // Only sent over HTTPS
    sameSite: 'Strict', // Prevents CSRF
    maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
  });

  res.json({ accessToken });
});

Step 2: Protecting Routes with Middleware

Now, we create a middleware function to protect our API endpoints.

function authenticateToken(req, res, next) {
  // Clients send the Access Token in the Authorization header
  // Format: "Bearer xxxxx.yyyyy.zzzzz"
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];

  if (!token) return res.status(401).json({ error: "Access token required" });

  jwt.verify(token, ACCESS_TOKEN_SECRET, (err, decodedPayload) => {
    if (err) {
      // Token is expired or invalid. Client needs to use Refresh Token.
      return res.status(403).json({ error: "Invalid or expired token" }); 
    }
    
    // Attach the user payload to the request object for the next functions to use
    req.user = decodedPayload;
    next();
  });
}

// A protected route
app.get('/api/dashboard', authenticateToken, (req, res) => {
  res.json({ message: `Welcome User ${req.user.userId}! Your role is ${req.user.role}.` });
});

Step 3: The Refresh Token Endpoint

When the Access Token expires (403 Forbidden), the client automatically calls this endpoint to get a new one.

const cookieParser = require('cookie-parser');
app.use(cookieParser());

app.post('/refresh', async (req, res) => {
  // Read the HttpOnly cookie
  const refreshToken = req.cookies.jwt_refresh;
  if (!refreshToken) return res.status(401).json({ error: "No refresh token found" });

  // Verify the refresh token hasn't been revoked in our DB
  const isValidInDb = await db.checkRefreshTokenValid(refreshToken);
  if (!isValidInDb) return res.status(403).json({ error: "Token revoked" });

  // Verify the signature
  jwt.verify(refreshToken, REFRESH_TOKEN_SECRET, (err, decodedPayload) => {
    if (err) return res.status(403).json({ error: "Invalid refresh token" });

    // Generate a new Access Token
    const newPayload = { userId: decodedPayload.userId, role: decodedPayload.role };
    const newAccessToken = jwt.sign(newPayload, ACCESS_TOKEN_SECRET, { expiresIn: '15m' });

    res.json({ accessToken: newAccessToken });
  });
});

Step 4: Logout

To log out, we delete the Refresh Token from the database and clear the cookie. (Note: Because Access Tokens are stateless, you cannot "delete" them from the server. They remain valid until they expire. This is why a short 15-minute lifespan is critical).

app.post('/logout', async (req, res) => {
  const refreshToken = req.cookies.jwt_refresh;
  
  if (refreshToken) {
    // Revoke it in the database so it can't be used again
    await db.deleteRefreshToken(refreshToken);
  }

  // Clear the cookie in the browser
  res.clearCookie('jwt_refresh');
  res.json({ message: "Logged out successfully" });
});

Chapter 3: OAuth2 and Third-Party Identity

Implementing username/password auth is hard. That's why many apps use OAuth2, allowing users to "Log in with Google/GitHub/Apple".

OAuth2 is not an authentication protocol; it is an Authorization Framework. It allows a user to grant a third-party application (your app) limited access to their resources on another site (Google), without giving your app their password.

The OAuth2 Flow (Authorization Code Grant):

  1. User clicks "Login with GitHub" on your app.
  2. Your app redirects the user to GitHub's authorization page.
  3. User logs into GitHub and clicks "Allow Access".
  4. GitHub redirects the user back to your app, appending a short-lived Authorization Code to the URL.
  5. Your backend server takes that Code, attaches its own Client Secret (a password known only to your server and GitHub), and sends a back-channel request to GitHub.
  6. GitHub verifies the code and secret, and responds to your server with an Access Token.
  7. Your server uses that Access Token to fetch the user's email and profile picture from GitHub's API.
  8. Your server creates an account for them in your own database, and issues your own JWT to the client.

Chapter 4: Defense in Depth (The OWASP Top 10)

Authentication is just one piece of the puzzle. You must actively defend against common attack vectors.

1. SQL Injection (SQLi)

  • The Attack: A hacker types '; DROP TABLE users; -- into your login field. If your backend concatenates strings to build SQL queries, the database will execute the malicious command.
  • The Defense: Never trust user input. Always use Parameterized Queries or an ORM (like Prisma or TypeORM). Parameterized queries send the SQL command and the user input as separate packages to the database engine, making injection mathematically impossible.

2. Cross-Site Scripting (XSS)

  • The Attack: An attacker injects malicious JavaScript into your website (e.g., leaving a comment that contains a <script> tag). When other users view the comment, the script executes in their browser, potentially stealing their JWT tokens from LocalStorage.
  • The Defense:
    • Never store sensitive tokens in LocalStorage.
    • Always sanitize and escape user-generated content before rendering it. Modern frontend frameworks like React and Vue automatically escape variables by default.

3. Cross-Site Request Forgery (CSRF)

  • The Attack: You are logged into your bank (via cookies). An attacker tricks you into clicking a link on an evil website: <img src="https://bank.com/transfer?amount=10000&to=hacker" />. Your browser automatically attaches your bank cookies, and the money is transferred.
  • The Defense:
    • Use the SameSite=Strict or Lax attribute on your cookies. This prevents the browser from sending cookies along with cross-site requests.
    • Implement Anti-CSRF tokens.

4. Password Security

If you are storing passwords, you are a prime target.

  • Rule 1: Never store plain text passwords.
  • Rule 2: Standard hashing algorithms like MD5 or SHA256 are too fast; hackers can brute-force them rapidly.
  • The Defense: Use bcrypt or Argon2. These algorithms are intentionally slow and computationally expensive. They also automatically generate a unique cryptographic "Salt" for every user, neutralizing rainbow table attacks.

Conclusion

Security is not a feature you bolt on at the end; it is a mindset embedded into the foundation of your architecture. By mastering stateless authentication with JWTs, understanding delegated access with OAuth2, and defending against injection and XSS attacks, you are now equipped to build fortresses, not sandcastles.

In Phase 4: Databases, we will explore where all this sensitive data actually lives, and how to query it efficiently.

Tags

#security#auth#jwt#oauth