Securing Express APIs using OAuth2 and JSON Web Tokens
Rowland Adimoha / September 02, 2024
12 min read
Rowland Adimoha / September 02, 2024
12 min read

How secure are your users? In today's digital landscape, where data breaches and cyber attacks are increasingly common, implementing robust authentication in your web applications is no longer optional—it's essential. As developers, we are responsible for protecting our users' data and maintaining our applications' integrity.
In a world where cybersecurity is more critical than ever, implementing robust authentication in your Express.js application isn't just a technical necessity; it's a safeguard for your entire platform. This guide will walk you through building a secure authentication system using TypeScript, ensuring your application is resilient and scalable.
We'll explore everything from basic username/password authentication to advanced strategies like JSON Web Tokens (JWT) and OAuth 2.0, equipping you with the tools to protect your users and your application from unauthorized access.
Before we proceed with the implementation, it's crucial to set up your development environment properly. This ensures a smooth development process and helps maintain consistency across your team. Here's what you'll need:
Prerequisites:
Start by installing TypeScript globally on your system. This allows you to use TypeScript's compiler anywhere on your machine. Additionally, install ts-node, which enables you to run TypeScript files directly without a separate compilation step.
npm install -g typescript
npm install -g ts-nodeWith these tools in place, you're ready to start building secure Express.js applications with TypeScript. The combination of Express.js and TypeScript provides a robust foundation for creating scalable and maintainable web applications.
Authentication is the process of verifying the identity of a user, device, or system. In the context of web applications, it typically involves validating user credentials and creating a session or token to maintain the user's authenticated state.
Why is authentication crucial for your Express.js applications?
In Express.js, authentication is usually implemented as middleware. This middleware intercepts requests, verifies the user's identity, and either allows the request to proceed or denies access if the authentication fails.
There are several authentication strategies you can implement in Express.js:
Each strategy has its pros and cons, and the choice depends on your specific application requirements. In this guide, we'll focus on implementing token-based authentication using JSON Web Tokens (JWT), as it's widely used in modern web applications.
Setting up an Express.js project with TypeScript involves a few more steps than a standard JavaScript project, but the benefits in terms of code quality and maintainability are significant.
First, initialize your project and install the necessary dependencies:
mkdir express-auth-demo
cd express-auth-demo
npm init -y
npm install express body-parser
npm install --save-dev typescript @types/node @types/express @types/body-parserNext, create a tsconfig.json file in your project root. This file configures the TypeScript compiler options for your project:
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true
},
"include": ["src/**/*"]
}With the configuration in place, create your main application file (e.g., src/app.ts) using TypeScript syntax:
import express, { Request, Response } from 'express';
import bodyParser from 'body-parser';
const app = express();
const port = 3000;
app.use(bodyParser.json());
app.get('/', (req: Request, res: Response) => {
res.send('Welcome to our secure Express.js API with TypeScript!');
});
app.listen(port, () => {
console.log(`Server running on http://localhost:${port}`);
});
export default app;To run your TypeScript application during development, you can use ts-node:
ts-node src/app.tsLet's start with a simple username/password authentication system. While this method isn't the most secure for production environments, it serves as a good starting point to understand the basics of authentication in Express.js.
In this implementation, we'll create two routes:
/register: Allows users to create a new account./login: Authenticates users based on their credentials.For simplicity, we'll store user information in memory. In a real-world application, you'd use a database to persist this data.
import express, { Request, Response } from 'express';
import bodyParser from 'body-parser';
interface User {
username: string;
password: string;
}
const app = express();
const port = 3000;
app.use(bodyParser.json());
const users: User[] = [];
app.post('/register', (req: Request, res: Response) => {
const { username, password } = req.body;
if (users.find(user => user.username === username)) {
return res.status(400).json({ message: 'Username already exists' });
}
users.push({ username, password });
res.status(201).json({ message: 'User registered successfully' });
});
app.post('/login', (req: Request, res: Response) => {
const { username, password } = req.body;
const user = users.find(user => user.username === username && user.password === password);
if (user) {
res.json({ message: 'Login successful' });
} else {
res.status(401).json({ message: 'Invalid credentials' });
}
});
app.listen(port, () => {
console.log(`Server running on http://localhost:${port}`);
});
export default app;Key points to consider in this implementation:
While this basic implementation works, it's not secure enough for production use. In the next section, we'll enhance it with password hashing.
Storing passwords in plain text is a major security risk. If an attacker gains access to your user database, they will have immediate access to all user accounts. To mitigate this risk, we'll use the bcrypt library to hash passwords before storing them.
Bcrypt is a password hashing function designed by Niels Provos and David Mazières. It's based on the Blowfish cipher and incorporates a salt to protect against rainbow table attacks. Here's why bcrypt is a good choice:
Let's update our code to use bcrypt:
import express, { Request, Response } from 'express';
import bodyParser from 'body-parser';
import bcrypt from 'bcrypt';
interface User {
username: string;
password: string;
}
const app = express();
const port = 3000;
app.use(bodyParser.json());
const users: User[] = [];
app.post('/register', async (req: Request, res: Response) => {
const { username, password } = req.body;
if (users.find(user => user.username === username)) {
return res.status(400).json({ message: 'Username already exists' });
}
const hashedPassword = await bcrypt.hash(password, 10);
users.push({ username, password: hashedPassword });
res.status(201).json({ message: 'User registered successfully' });
});
app.post('/login', async (req: Request, res: Response) => {
const { username, password } = req.body;
const user = users.find(user => user.username === username);
if (user && await bcrypt.compare(password, user.password)) {
res.json({ message: 'Login successful' });
} else {
res.status(401).json({ message: 'Invalid credentials' });
}
});
app.listen(port, () => {
console.log(`Server running on http://localhost:${port}`);
});
export default app;When implementing password hashing:
Token-based authentication is a stateless authentication method that works particularly well with modern web architectures. JSON Web Tokens (JWTs) have become a popular choice for implementing token-based authentication.
A JWT consists of three parts:
Here's how JWT authentication
typically works:
Let's implement JWT authentication in our Express.js application:
import express, { Request, Response, NextFunction } from 'express';
import bodyParser from 'body-parser';
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
interface User {
username: string;
password: string;
}
const app = express();
const port = 3000;
const SECRET_KEY = 'your-secret-key'; // In production, use an environment variable
app.use(bodyParser.json());
const users: User[] = [];
app.post('/register', async (req: Request, res: Response) => {
const { username, password } = req.body;
if (users.find(user => user.username === username)) {
return res.status(400).json({ message: 'Username already exists' });
}
const hashedPassword = await bcrypt.hash(password, 10);
users.push({ username, password: hashedPassword });
res.status(201).json({ message: 'User registered successfully' });
});
app.post('/login', async (req: Request, res: Response) => {
const { username, password } = req.body;
const user = users.find(user => user.username === username);
if (user && await bcrypt.compare(password, user.password)) {
const token = jwt.sign({ username: user.username }, SECRET_KEY, { expiresIn: '1h' });
res.json({ message: 'Login successful', token });
} else {
res.status(401).json({ message: 'Invalid credentials' });
}
});
function authenticateToken(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (token == null) return res.sendStatus(401);
jwt.verify(token, SECRET_KEY, (err: any, user: any) => {
if (err) return res.sendStatus(403);
req.user = user;
next();
});
}
app.get('/protected', authenticateToken, (req: Request, res: Response) => {
res.json({ message: 'This is a protected route', user: req.user });
});
app.listen(port, () => {
console.log(`Server running on http://localhost:${port}`);
});
export default app;When implementing JWT authentication:
OAuth 2.0 is an authorization framework that enables applications to obtain limited access to user accounts on an HTTP service. It's widely used for implementing "Sign in with Google/Facebook/GitHub" functionality.
Implementing OAuth 2.0 in your Express.js application involves several steps:
Here's an example of implementing Google OAuth:
import express from 'express';
import passport from 'passport';
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
const app = express();
passport.use(new GoogleStrategy({
clientID: GOOGLE_CLIENT_ID,
clientSecret: GOOGLE_CLIENT_SECRET,
callbackURL: "http://localhost:3000/auth/google/callback"
},
function(accessToken, refreshToken, profile, cb) {
// Here you would find or create a user in your database
return cb(null, profile);
}
));
app.get('/auth/google',
passport.authenticate('google', { scope: ['profile', 'email'] }));
app.get('/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/login' }),
function(req, res) {
// Successful authentication, redirect home.
res.redirect('/');
});When implementing OAuth:
Implementing authentication is just the first step. To ensure your system remains secure, consider these best practices:
Testing is crucial to ensure your authentication system is working as expected. Write unit tests and integration tests for the following:
Consider using tools like Postman for manual testing and frameworks like Jest for automated tests.
Securing your Express.js API with robust authentication is a critical step in safeguarding your application and user data. By implementing the strategies outlined in this guide—ranging from basic username/password authentication to advanced token-based and OAuth 2.0 authentication—you can create a secure and scalable authentication system for your Express.js applications.
As you continue to develop and maintain your application, stay informed about the latest security practices and continuously evaluate and improve your authentication mechanisms. Your commitment to security will protect your users and enhance your platform's overall integrity and trustworthiness.