TypeScript Authentication System
Authentication is a critical component of most modern web applications. In this tutorial, we'll build a complete authentication system using TypeScript, Express, and various security libraries. By the end, you'll understand how to implement secure user registration, login functionality, and protect routes using JSON Web Tokens (JWT).
Introduction to Authentication
Authentication is the process of verifying that users are who they claim to be. A typical authentication flow includes:
- User registration
- Email verification (optional)
- User login
- Session management (using tokens)
- Protected routes
- Password reset functionality
Project Setup
Let's start by setting up our TypeScript project structure.
1. Initialize a new project
mkdir ts-auth-system
cd ts-auth-system
npm init -y
2. Install dependencies
npm install express jsonwebtoken bcrypt dotenv mongoose
npm install --save-dev typescript ts-node @types/express @types/jsonwebtoken @types/bcrypt @types/node nodemon
3. Create a TypeScript configuration
Create a tsconfig.json
file:
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
4. Project structure
Let's organize our project files:
src/
├── config/
│ └── db.ts
├── controllers/
│ └── authController.ts
├── interfaces/
│ └── User.ts
├── middleware/
│ └── auth.ts
├── models/
│ └── User.ts
├── routes/
│ └── authRoutes.ts
├── services/
│ └── authService.ts
├── utils/
│ └── validation.ts
└── app.ts
Creating User Models and Interfaces
Let's start by defining our user interface and model.
User Interface
In src/interfaces/User.ts
:
export interface IUser {
_id?: string;
email: string;
password: string;
name: string;
createdAt?: Date;
updatedAt?: Date;
}
export interface UserDocument extends IUser {
comparePassword(candidatePassword: string): Promise<boolean>;
}
User Model
In src/models/User.ts
:
import mongoose, { Schema, model } from 'mongoose';
import bcrypt from 'bcrypt';
import { UserDocument } from '../interfaces/User';
const userSchema = new Schema(
{
email: {
type: String,
required: true,
unique: true,
trim: true,
lowercase: true,
},
password: {
type: String,
required: true,
minlength: 8,
},
name: {
type: String,
required: true,
trim: true,
},
},
{ timestamps: true }
);
// Pre-save hook to hash password before saving
userSchema.pre('save', async function (next) {
const user = this as UserDocument;
// Only hash the password if it has been modified (or is new)
if (!user.isModified('password')) return next();
try {
// Generate salt
const salt = await bcrypt.genSalt(10);
// Hash the password along with the new salt
const hash = await bcrypt.hash(user.password, salt);
// Override the plain text password with the hashed one
user.password = hash;
next();
} catch (error: any) {
return next(error);
}
});
// Method to compare passwords
userSchema.methods.comparePassword = async function(candidatePassword: string): Promise<boolean> {
try {
return await bcrypt.compare(candidatePassword, this.password);
} catch (error) {
return false;
}
};
const UserModel = model<UserDocument>('User', userSchema);
export default UserModel;
Setting up Database Connection
In src/config/db.ts
:
import mongoose from 'mongoose';
const connectDB = async (): Promise<void> => {
try {
const conn = await mongoose.connect(process.env.MONGO_URI!);
console.log(`MongoDB Connected: ${conn.connection.host}`);
} catch (error) {
console.error(`Error: ${error}`);
process.exit(1);
}
};
export default connectDB;
Authentication Service Layer
In src/services/authService.ts
:
import jwt from 'jsonwebtoken';
import { IUser } from '../interfaces/User';
import UserModel from '../models/User';
export const createUser = async (userData: IUser) => {
try {
const user = new UserModel(userData);
await user.save();
return user;
} catch (error) {
throw error;
}
};
export const findUserByEmail = async (email: string) => {
return await UserModel.findOne({ email });
};
export const generateToken = (userId: string): string => {
return jwt.sign(
{ id: userId },
process.env.JWT_SECRET!,
{ expiresIn: '1d' }
);
};
export const verifyToken = (token: string) => {
try {
return jwt.verify(token, process.env.JWT_SECRET!);
} catch (error) {
throw new Error('Invalid token');
}
};
Auth Controllers
In src/controllers/authController.ts
:
import { Request, Response } from 'express';
import { createUser, findUserByEmail, generateToken } from '../services/authService';
import { IUser } from '../interfaces/User';
export const register = async (req: Request, res: Response) => {
try {
const { name, email, password } = req.body;
// Check if user already exists
const existingUser = await findUserByEmail(email);
if (existingUser) {
return res.status(400).json({ message: 'User already exists' });
}
// Create new user
const userData: IUser = {
name,
email,
password,
};
const user = await createUser(userData);
// Generate JWT token
const token = generateToken(user._id!.toString());
res.status(201).json({
message: 'User registered successfully',
token,
user: {
id: user._id,
name: user.name,
email: user.email,
},
});
} catch (error) {
console.error('Registration error:', error);
res.status(500).json({ message: 'Server error during registration' });
}
};
export const login = async (req: Request, res: Response) => {
try {
const { email, password } = req.body;
// Find user by email
const user = await findUserByEmail(email);
if (!user) {
return res.status(401).json({ message: 'Invalid credentials' });
}
// Verify password
const isMatch = await user.comparePassword(password);
if (!isMatch) {
return res.status(401).json({ message: 'Invalid credentials' });
}
// Generate JWT token
const token = generateToken(user._id.toString());
res.status(200).json({
message: 'Login successful',
token,
user: {
id: user._id,
name: user.name,
email: user.email,
},
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({ message: 'Server error during login' });
}
};
Auth Middleware for Route Protection
In src/middleware/auth.ts
:
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import UserModel from '../models/User';
// Extend Express Request interface to include user property
declare global {
namespace Express {
interface Request {
user?: any;
}
}
}
export const protect = async (
req: Request,
res: Response,
next: NextFunction
) => {
let token;
// Check if token exists in headers
if (
req.headers.authorization &&
req.headers.authorization.startsWith('Bearer')
) {
try {
// Get token from header
token = req.headers.authorization.split(' ')[1];
// Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as { id: string };
// Get user from database (excluding password)
req.user = await UserModel.findById(decoded.id).select('-password');
next();
} catch (error) {
console.error(error);
res.status(401).json({ message: 'Not authorized, token failed' });
}
}
if (!token) {
res.status(401).json({ message: 'Not authorized, no token provided' });
}
};
Auth Routes
In src/routes/authRoutes.ts
:
import { Router } from 'express';
import { register, login } from '../controllers/authController';
import { protect } from '../middleware/auth';
const router = Router();
/**
* @route POST /api/auth/register
* @desc Register a new user
* @access Public
*/
router.post('/register', register);
/**
* @route POST /api/auth/login
* @desc Login user & get token
* @access Public
*/
router.post('/login', login);
/**
* @route GET /api/auth/profile
* @desc Get user profile
* @access Private
*/
router.get('/profile', protect, (req, res) => {
res.json({
user: {
id: req.user._id,
name: req.user.name,
email: req.user.email,
},
});
});
export default router;
Main Application Setup
In src/app.ts
:
import express from 'express';
import dotenv from 'dotenv';
import connectDB from './config/db';
import authRoutes from './routes/authRoutes';
// Load environment variables
dotenv.config();
// Connect to MongoDB
connectDB();
// Initialize Express
const app = express();
// Parse JSON body
app.use(express.json());
// Routes
app.use('/api/auth', authRoutes);
// Default route
app.get('/', (req, res) => {
res.send('Authentication API is running...');
});
// Start server
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Environment Variables
Create a .env
file at the root of your project:
PORT=5000
MONGO_URI=mongodb://localhost:27017/ts-auth-system
JWT_SECRET=your_jwt_secret_here
Running the Application
Add these scripts to your package.json
:
"scripts": {
"start": "node dist/app.js",
"dev": "nodemon --exec ts-node src/app.ts",
"build": "tsc"
}
Now you can run the application:
npm run dev
Usage Examples
Registering a User
curl -X POST http://localhost:5000/api/auth/register \
-H "Content-Type: application/json" \
-d '{"name": "John Doe", "email": "[email protected]", "password": "password123"}'
Response:
{
"message": "User registered successfully",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": "60a1b2c3d4e5f6g7h8i9j0k",
"name": "John Doe",
"email": "[email protected]"
}
}
Logging In
curl -X POST http://localhost:5000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "password": "password123"}'
Response:
{
"message": "Login successful",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": "60a1b2c3d4e5f6g7h8i9j0k",
"name": "John Doe",
"email": "[email protected]"
}
}
Accessing a Protected Route
curl -X GET http://localhost:5000/api/auth/profile \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
Response:
{
"user": {
"id": "60a1b2c3d4e5f6g7h8i9j0k",
"name": "John Doe",
"email": "[email protected]"
}
}
Security Considerations
- Password Storage: We use bcrypt to hash passwords, never storing plaintext passwords.
- JWT Tokens: We sign tokens with a secret key and set expiration times.
- Authorization Headers: We check for proper Bearer token format.
- Error Handling: We provide minimal error information to clients to prevent information leakage.
- Input Validation: In a production environment, add comprehensive validation using libraries like Joi or Yup.
Let's add a simple validation function in src/utils/validation.ts
:
export const validateEmail = (email: string): boolean => {
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return re.test(email);
};
export const validatePasswordStrength = (password: string): boolean => {
// Password must be at least 8 characters long with at least one number, one uppercase, one lowercase
const re = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}$/;
return re.test(password);
};
You can use these functions in your controllers to validate inputs before processing them.
Enhancing the Authentication System
To make our system more robust, consider adding:
- Email verification: Send verification emails to new users to validate their email addresses.
- Password reset flow: Implement a "forgot password" functionality.
- Rate limiting: Prevent brute force attacks by limiting login attempts.
- Two-factor authentication (2FA): Add an extra layer of security.
- Refresh tokens: Implement token rotation for better security.
Summary
In this tutorial, we've built a complete TypeScript authentication system from scratch that includes:
- User registration with password hashing
- Secure login functionality
- JWT token generation and verification
- Protected routes using middleware
- MongoDB integration with Mongoose
- Type safety throughout using TypeScript interfaces
This structure provides a solid foundation for building secure applications. Remember that security is a continuous process, and you should regularly update dependencies and review security best practices.
Additional Resources
- TypeScript Documentation
- JWT.io - Debugger and information about JSON Web Tokens
- OWASP Authentication Cheat Sheet
- bcrypt Documentation
- Express.js Security Best Practices
Exercises
- Implement an email verification system using a temporary token.
- Add password reset functionality with time-limited tokens.
- Implement OAuth integration with Google or GitHub.
- Add refresh token logic to extend user sessions securely.
- Create a role-based access control system to restrict certain routes to specific user roles.
By completing this tutorial, you now have a solid foundation for implementing authentication in your TypeScript applications. Security is critical, so make sure to stay updated with the latest security practices and regularly audit your code for vulnerabilities.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)