Samuel Fajreldines

I am a specialist in the entire JavaScript and TypeScript ecosystem.

I am expert in AI and in creating AI integrated solutions.

I am expert in DevOps and Serverless Architecture

I am expert in PHP and its frameworks.

+55 (51) 99226-5039 samuelfajreldines@gmail.com

Implementing JWT Authentication in TypeScript: A Comprehensive Guide

Introduction

In today's web development landscape, security is paramount. Implementing robust authentication mechanisms is essential to protect user data and ensure secure communication between clients and servers. One of the most popular methods for securing APIs is using JSON Web Tokens (JWT). In this comprehensive guide, we'll explore what JWT authentication is and how to implement it in TypeScript, making your web applications both secure and scalable.

What is JWT?

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed, typically using a secret or a public/private key pair.

Why Use JWT?

  • Compact: JWTs are compact in size, making them ideal for passing in HTTP headers or URL parameters.
  • Self-contained: JWTs carry all the necessary information about the user, reducing the need to query the database multiple times.
  • Stateless: Since JWTs are self-contained, they enable stateless authentication, which scales better for distributed systems.

How JWT Authentication Works

  1. User Authentication: The user provides their credentials (e.g., username and password).
  2. Token Generation: Upon successful authentication, the server generates a JWT and signs it with a secret key.
  3. Token Transmission: The server sends the JWT back to the client, usually in the response body.
  4. Token Storage: The client stores the token, typically in localStorage or a cookie.
  5. Authenticated Requests: For subsequent requests, the client includes the JWT in the Authorization header.
  6. Token Verification: The server verifies the token's signature and, if valid, processes the request.

Implementing JWT Authentication in TypeScript

Let's dive into the step-by-step process of implementing JWT authentication in a Node.js application using TypeScript.

Prerequisites

  • Node.js installed on your machine.
  • Basic knowledge of TypeScript and Express.js.

Setting Up the Project

  1. Initialize the Project

    mkdir jwt-auth-typescript
    cd jwt-auth-typescript
    npm init -y
  2. Install Dependencies

    npm install express jsonwebtoken bcryptjs cors dotenv
  3. Install TypeScript and Types

    npm install --save-dev typescript @types/express @types/jsonwebtoken @types/bcryptjs @types/cors
  4. Initialize TypeScript Configuration

    npx tsc --init

Project Structure

jwt-auth-typescript/
├── src/
│   ├── index.ts
│   ├── routes/
│   │   └── auth.ts
│   ├── controllers/
│   │   └── authController.ts
│   ├── models/
│       └── user.ts
├── package.json
├── tsconfig.json
└── .env

Configuring TypeScript (tsconfig.json)

Make sure your tsconfig.json includes:

{
  "compilerOptions": {
    "target": "ES6",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true
  }
}

Setting Up Environment Variables

Create a .env file in the root directory:

PORT=5000
JWT_SECRET=your_jwt_secret_key

Creating the Server (index.ts)

import express from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import authRoutes from './routes/auth';

dotenv.config();

const app = express();
const PORT = process.env.PORT || 5000;

app.use(cors());
app.use(express.json());

app.use('/api/auth', authRoutes);

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Creating the User Model (models/user.ts)

In a real application, you'd use a database. For simplicity, we'll use an in-memory array.

interface User {
  id: number;
  username: string;
  password: string;
}

const users: User[] = [];

export default users;
export type { User };

Creating Authentication Routes (routes/auth.ts)

import { Router } from 'express';
import { register, login, protectedRoute } from '../controllers/authController';

const router = Router();

router.post('/register', register);
router.post('/login', login);
router.get('/protected', protectedRoute);

export default router;

Implementing the Controller (controllers/authController.ts)

import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import bcrypt from 'bcryptjs';
import users, { User } from '../models/user';

const JWT_SECRET = process.env.JWT_SECRET || 'secret_key';

export const register = async (req: Request, res: Response) => {
  const { username, password } = req.body;

  // Check if user exists
  const userExists = users.find((user) => user.username === username);
  if (userExists) {
    return res.status(400).json({ message: 'User already exists' });
  }

  // Hash password
  const hashedPassword = await bcrypt.hash(password, 10);

  // Create user
  const newUser: User = {
    id: users.length + 1,
    username,
    password: hashedPassword,
  };
  users.push(newUser);

  res.status(201).json({ message: 'User registered successfully' });
};

export const login = async (req: Request, res: Response) => {
  const { username, password } = req.body;

  // Find user
  const user = users.find((user) => user.username === username);
  if (!user) {
    return res.status(400).json({ message: 'Invalid credentials' });
  }

  // Check password
  const isValid = await bcrypt.compare(password, user.password);
  if (!isValid) {
    return res.status(400).json({ message: 'Invalid credentials' });
  }

  // Sign token
  const token = jwt.sign({ id: user.id, username: user.username }, JWT_SECRET, { expiresIn: '1h' });

  res.json({ token });
};

export const protectedRoute = (req: Request, res: Response, next: NextFunction) => {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];

  if (!token) {
    return res.sendStatus(401);
  }

  jwt.verify(token, JWT_SECRET, (err, user) => {
    if (err) return res.sendStatus(403);
    (req as any).user = user;
    next();
  });
};

Protecting Routes

Modify the protectedRoute function to include a middleware that verifies the JWT before proceeding.

export const protectedRoute = [
  (req: Request, res: Response, next: NextFunction) => {
    // Middleware to verify JWT
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1];
  
    if (!token) {
      return res.sendStatus(401);
    }
  
    jwt.verify(token, JWT_SECRET, (err, user) => {
      if (err) return res.sendStatus(403);
      (req as any).user = user;
      next();
    });
  },
  (req: Request, res: Response) => {
    // Protected route logic
    res.json({ message: 'This is a protected route', user: (req as any).user });
  }
];

Testing the Application

  1. Compile TypeScript

    npx tsc
  2. Run the Server

    node dist/index.js
  3. Register a User

    POST http://localhost:5000/api/auth/register
    Content-Type: application/json
    
    {
      "username": "testuser",
      "password": "password123"
    }
  4. Login

    POST http://localhost:5000/api/auth/login
    Content-Type: application/json
    
    {
      "username": "testuser",
      "password": "password123"
    }

    You will receive a JWT token in the response.

  5. Access Protected Route

    GET http://localhost:5000/api/auth/protected
    Authorization: Bearer <your_jwt_token>

    You should receive a response indicating access is granted.

Security Considerations

  • Use HTTPS: Always use HTTPS in production to prevent token interception.
  • Token Expiration: Set appropriate token expiration times to mitigate risks if a token is compromised.
  • Refresh Tokens: Implement refresh tokens for enhanced security.
  • Store Tokens Securely: Avoid storing tokens in insecure places like localStorage. Consider using HTTP-only cookies.
  • Input Validation: Always validate user inputs to prevent injection attacks.

Conclusion

Implementing JWT authentication in TypeScript is a powerful way to secure your web applications. It offers a scalable and stateless approach to handle authentication, making it ideal for modern web architectures. By following this guide, you should have a solid foundation to integrate JWT authentication into your TypeScript projects.

Further Reading

Share Your Thoughts

If you found this guide helpful or have any questions, feel free to leave a comment below or reach out to me on Twitter.


By Samuel Fajreldines, specialist in JavaScript, TypeScript, DevOps, and Serverless Architecture.


Resume

Experience

  • SecurityScoreCard

    Nov. 2023 - Present

    New York, United States

    Senior Software Engineer

    I joined SecurityScorecard, a leading organization with over 400 employees, as a Senior Full Stack Software Engineer. My role spans across developing new systems, maintaining and refactoring legacy solutions, and ensuring they meet the company's high standards of performance, scalability, and reliability.

    I work across the entire stack, contributing to both frontend and backend development while also collaborating directly on infrastructure-related tasks, leveraging cloud computing technologies to optimize and scale our systems. This broad scope of responsibilities allows me to ensure seamless integration between user-facing applications and underlying systems architecture.

    Additionally, I collaborate closely with diverse teams across the organization, aligning technical implementation with strategic business objectives. Through my work, I aim to deliver innovative and robust solutions that enhance SecurityScorecard's offerings and support its mission to provide world-class cybersecurity insights.

    Technologies Used:

    Node.js Terraform React Typescript AWS Playwright and Cypress