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.