How to Implement Secure User Authentication Using JWT, Express, and TypeScript

How to Implement Secure User Authentication Using JWT, Express, and TypeScript

In modern web applications, handling user authentication securely is crucial. One of the most popular and effective ways to authenticate users is by using JSON Web Tokens (JWT). In this post, we'll walk through the steps to implement JWT-based authentication using Express and TypeScript, covering everything from sign-up to token validation and securing routes.

Prerequisites

Before you begin, ensure that you have the following installed:

  • Node.js and npm

  • TypeScript

  • Express

  • JWT (jsonwebtoken)

You can install Express and JWT by running the following commands:

bun add express jsonwebtoken

You'll also need TypeScript typings for Express:

npm add -d @types/express

Project Structure

Here’s how we’ll structure our project:

/src
  |-- /middleware
    |-- inputValidation.ts
    |-- authorization.ts
  |-- /types
    |-- AuthRequest.ts
  |-- enum.ts
  |-- index.ts

1. Setting Up Express

First, let's set up our Express server and define routes for sign-up, sign-in, and accessing a protected route. This will form the core of our authentication flow.

import express from "express";
import jwt from "jsonwebtoken";
import { Status } from "./enum";
import { inputValidation } from "./middleware/inputValidation";
import { authorization } from "./middleware/authorization";
import { AuthRequest } from "./types/AuthRequest";

const app = express();
app.use(express.json());

const JWT_SECRET = "randomMeSecret"; // Secret for signing JWT tokens
const users = new Map<string, string>(); // In-memory store for users

app.get("/", (_req, res) => {
  res.status(Status.Ok).json({ success: true, message: "server [ on ]" });
});

app.post("/signup", inputValidation, (req, res) => {
  const { username, password } = req.body;
  if (!users.has(username)) {
    users.set(username, password);
    res.status(Status.Ok).json({ success: true, message: "user created" });
  } else {
    res.status(Status.Conflict).json({ success: false, message: "username already exists" });
  }
});

app.post("/signin", inputValidation, (req, res) => {
  const { username, password } = req.body;
  if (!users.has(username)) {
    res.status(Status.NotFound).json({ success: false, message: "User not available" });
  } else {
    const userPassword = users.get(username);
    if (userPassword === password) {
      const token = jwt.sign({ username }, JWT_SECRET, { expiresIn: "30s" });
      res.status(Status.Ok).json({ success: true, token });
    } else {
      res.status(Status.Unauthorized).json({ success: false, message: "invalid password" });
    }
  }
});

app.get("/me", authorization, (req: AuthRequest, res) => {
  res.status(Status.Ok).json({ success: true, username: req.user });
});

const server = app.listen(3000, () => {
  console.log("server : [ ON ]");
});

process.on("SIGINT", () => {
  server.close(() => {
    console.log("server : [ OFF ]");
    process.exit(0);
  });
});

2. Validating User Input with Middleware

Here’s the code for inputValidation.ts:

import { type Request, type Response, type NextFunction } from "express";
import { Status } from "../enum";

export const inputValidation = (req: Request, res: Response, next: NextFunction): void => {
  const { username, password } = req.body;

  if (!username || !password) {
    res.status(Status.BadRequest).json({ success: false, message: "wrong input" });
    return;
  }
  next();
};

3. Authorization Middleware

Here’s the code for authorization.ts:

import { type Request, type Response, type NextFunction } from "express";
import { Status } from "../enum";
import jwt from "jsonwebtoken";
import { type AuthRequest } from "../types/AuthRequest";

const JWT_SECRET = "randomMeSecret";

export const authorization = (req: AuthRequest, res: Response, next: NextFunction): void => {
  const token = req.headers.token;

  if (!token || typeof token !== "string") {
    res.status(Status.Unauthorized).json({
      success: false,
      message: "Unauthorized - invalid or missing token",
    });
    return;
  }

  try {
    const decoded = jwt.verify(token, JWT_SECRET);
    if (typeof decoded !== "object" || !decoded || !("username" in decoded)) {
      res.status(Status.Unauthorized).json({ success: false, message: "Unauthorized - invalid token" });
      return;
    }

    req.user = (decoded as { username: string }).username;
    next();
  } catch (e) {
    res.status(Status.Unauthorized).json({ success: false, message: "Unauthorized - token not valid" });
  }
};

4. Creating Custom Request Types

Here’s the code for AuthRequest.ts:

import { type Request } from "express";

export interface AuthRequest extends Request {
  user?: string;
}

5. HTTP Status Codes Enum

Here’s the code for enum.ts:

export enum Status {
  Ok = 200,
  Created = 201,
  NoContent = 204,
  BadRequest = 400,
  Unauthorized = 401,
  Forbidden = 403,
  NotFound = 404,
  Conflict = 409,
  UnprocessableEntity = 422,
  ServerError = 500,
  ServiceUnavailable = 503,
}

Conclusion

With this setup, you have a basic yet secure authentication system using JWT with Express and TypeScript. We’ve covered:

  • Middleware for input validation, token verification, and authorization.

  • TypeScript’s type safety for request data.

  • An enum for clearer HTTP status codes.