Authentication is a fundamental aspect of backend development, ensuring that users can securely access their accounts. In this blog, I’ll share my experience building a scalable authentication system and the challenges I overcame. This journey is part of my adventure with the HNG Internship, a program that I am excited to be a part of.
The Problem
In a recent project, I needed to develop an authentication system that could handle a large number of users and provide secure, reliable access. The system had to support multiple authentication methods, including username/password, OAuth, and two-factor authentication (2FA).
Step-by-Step Solution
Step 1: Choosing the Right Technology Stack
The first step was to select the appropriate technology stack. I chose Node.js for its scalability and Express.js for building the RESTful API. For the database, I used MongoDB, which offered flexibility and scalability for storing user data.
Step 2: Implementing User Registration and Authentication
I started by implementing the user registration and authentication endpoints. I used bcrypt for hashing passwords and JWT (JSON Web Tokens) for generating and validating authentication tokens. This ensured secure handling of user credentials.
User Registration Code Example:
const express = require('express');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const User = require('./models/User'); // Mongoose User model
const app = express();
app.use(express.json());
app.post('/register', async (req, res) => {
try {
const { username, password } = req.body;
const hashedPassword = await bcrypt.hash(password, 10);
const newUser = new User({ username, password: hashedPassword });
await newUser.save();
res.status(201).send('User registered successfully');
} catch (error) {
res.status(500).send('Error registering user');
}
});
app.post('/login', async (req, res) => {
try {
const { username, password } = req.body;
const user = await User.findOne({ username });
if (!user) return res.status(400).send('User not found');
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) return res.status(400).send('Invalid password');
const token = jwt.sign({ userId: user._id }, 'your_jwt_secret', { expiresIn: '1h' });
res.json({ token });
} catch (error) {
res.status(500).send('Error logging in');
}
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
Step 3: Integrating OAuth
To support social logins, I integrated OAuth using Passport.js. This allowed users to log in using their Google, Facebook, or GitHub accounts. Passport.js provided a straightforward way to handle OAuth authentication flows.
OAuth Integration Code Example:
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
passport.use(new GoogleStrategy({
clientID: 'YOUR_GOOGLE_CLIENT_ID',
clientSecret: 'YOUR_GOOGLE_CLIENT_SECRET',
callbackURL: '/auth/google/callback'
}, (accessToken, refreshToken, profile, done) => {
User.findOrCreate({ googleId: profile.id }, (err, user) => {
return done(err, user);
});
}));
app.get('/auth/google', passport.authenticate('google', { scope: ['profile'] }));
app.get('/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/login' }),
(req, res) => {
res.redirect('/');
}
);
Step 4: Adding Two-Factor Authentication
To enhance security, I added two-factor authentication (2FA) using the TOTP (Time-Based One-Time Password) algorithm. I implemented this with the help of the speakeasy library, which generated and validated TOTP codes. This added an extra layer of security for users.
2FA Code Example:
const speakeasy = require('speakeasy');
const qrcode = require('qrcode');
app.post('/2fa/setup', (req, res) => {
const secret = speakeasy.generateSecret({ length: 20 });
qrcode.toDataURL(secret.otpauth_url, (err, data_url) => {
res.json({ secret: secret.base32, qrCode: data_url });
});
});
app.post('/2fa/verify', (req, res) => {
const { token, secret } = req.body;
const verified = speakeasy.totp.verify({
secret,
encoding: 'base32',
token
});
if (verified) {
res.send('2FA token is valid');
} else {
res.send('2FA token is invalid');
}
});
Step 5: Ensuring Scalability and Performance
To ensure the authentication system could handle high traffic, I implemented rate limiting and caching. I used Redis for caching authentication tokens and rate limiting to prevent brute-force attacks. This helped maintain performance and security under heavy load.
Rate Limiting and Caching Code Example:
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const redis = require('redis');
const client = redis.createClient();
const limiter = rateLimit({
store: new RedisStore({
client: client,
}),
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests, please try again later.'
});
app.use(limiter);
The Result
The authentication system was robust, scalable, and secure. It handled a large number of users efficiently and provided multiple authentication methods, enhancing the overall user experience.
Why HNG Internship?
The HNG Internship offers a fantastic opportunity to work on challenging projects, learn from industry experts, and collaborate with talented developers. This experience is invaluable for anyone looking to advance their career in backend development. Learn more about the HNG Internship on their official website and see how they hire talented developers.
Conclusion
Building a scalable authentication system is crucial for any application. My journey with the HNG Internship has just begun, and I am excited about the opportunities and challenges ahead. Thank you for reading, and I hope my experience helps you in your own authentication system projects.
Top comments (0)