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
- User Authentication: The user provides their credentials (e.g., username and password).
- Token Generation: Upon successful authentication, the server generates a JWT and signs it with a secret key.
- Token Transmission: The server sends the JWT back to the client, usually in the response body.
- Token Storage: The client stores the token, typically in localStorage or a cookie.
- Authenticated Requests: For subsequent requests, the client includes the JWT in the
Authorizationheader. - 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
-
Initialize the Project
mkdir jwt-auth-typescript cd jwt-auth-typescript npm init -y -
Install Dependencies
npm install express jsonwebtoken bcryptjs cors dotenv -
Install TypeScript and Types
npm install --save-dev typescript @types/express @types/jsonwebtoken @types/bcryptjs @types/cors -
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
-
Compile TypeScript
npx tsc -
Run the Server
node dist/index.js -
Register a User
POST http://localhost:5000/api/auth/register Content-Type: application/json { "username": "testuser", "password": "password123" } -
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.
-
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.