Build a Web Authentication System
Introduction
Authentication is the cornerstone of application security. A well-designed authentication system protects user accounts, manages access control, and provides secure session handling.
In this tutorial, we'll build "AuthSystem" - a complete web authentication system with multiple authentication methods, OAuth integration, and security features.
What You'll Build
- User registration and login
- JWT-based authentication
- Session management
- OAuth 2.0 integration
- Password reset flow
What You'll Learn
- Password security best practices
- Token-based authentication
- OAuth 2.0 and OpenID Connect
- Session hijacking prevention
- Security headers
Core Concepts
Let's understand authentication fundamentals.
Authentication Methods
- Password-based - Username and password
- Token-based - JWT, OAuth tokens
- Biometric - Fingerprint, face recognition
- Multi-factor - Multiple verification methods
Token vs Session
- JWT Tokens - Stateless, scalable, good for APIs
- Session-based - Server-side control, traditional web apps
Project Setup
Bash
# Create project directory
mkdir auth-system
cd auth-system
# Initialize Node.js
npm init -y
# Install dependencies
npm install express mongoose bcryptjs jsonwebtoken passport passport-google-oauth20 cors helmet express-rate-limit
Project Structure
File Structure
auth-system/
├── config/
│ ├── db.js
│ └── passport.js
├── middleware/
│ ├── auth.js
│ └── rateLimiter.js
├── models/
│ ├── User.js
│ └── Session.js
├── routes/
│ ├── auth.js
│ └── oauth.js
├── utils/
│ ├── jwt.js
│ └── crypto.js
├── server.js
└── package.json
User Model
Let's create the user model with security features.
JavaScript
// models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const crypto = require('crypto');
const userSchema = mongoose.Schema({
email: {
type: String,
required: true,
unique: true,
lowercase: true,
trim: true
},
password: {
type: String,
required: true,
minlength: 8,
select: false
},
name: {
type: String,
required: true,
trim: true
},
avatar: String,
role: {
type: String,
enum: ['user', 'admin'],
default: 'user'
},
isVerified: {
type: Boolean,
default: false
},
verificationToken: String,
passwordResetToken: String,
passwordResetExpires: Date,
loginAttempts: {
type: Number,
default: 0
},
lockUntil: Date,
provider: {
type: String,
enum: ['local', 'google', 'github'],
default: 'local'
},
providerId: String,
lastLogin: Date
}, {
timestamps: true
});
// Hash password before saving
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) {
return next();
}
const salt = await bcrypt.genSalt(12);
this.password = await bcrypt.hash(this.password, salt);
next();
});
// Check if account is locked
userSchema.methods.isLocked = function() {
return this.lockUntil && this.lockUntil > Date.now();
};
// Increment login attempts
userSchema.methods.incrementLoginAttempts = async function() {
if this.lockUntil && this.lockUntil < Date.now()) {
return this.updateOne({
$set: { loginAttempts: 1 },
$unset: { lockUntil: 1 }
});
}
const updates = { $inc: { loginAttempts: 1 } };
if this.loginAttempts + 1 >= 5) {
updates.$set = { lockUntil: Date.now() + 15 * 60 * 1000 };
}
return this.updateOne(updates);
};
// Compare password
userSchema.methods.comparePassword = async function(candidatePassword) {
return await bcrypt.compare(candidatePassword, this.password);
};
// Generate verification token
userSchema.methods.generateVerificationToken = function() {
return crypto.randomBytes(32).toString('hex');
};
// Generate password reset token
userSchema.methods.generatePasswordResetToken = function() {
const resetToken = crypto.randomBytes(32).toString('hex');
this.passwordResetToken = crypto
.createHash('sha256')
.update(resetToken)
.digest('hex');
this.passwordResetExpires = Date.now() + 30 * 60 * 1000;
return resetToken;
};
module.exports = mongoose.model('User', userSchema);
Password Hashing
Let's implement secure password handling utilities.
JavaScript
// utils/password.js
const crypto = require('crypto');
class PasswordManager {
constructor(options = {}) {
this.saltLength = options.saltLength || 16;
this.iterations = options.iterations || 100000;
this.keyLength = options.keyLength || 64;
this.algorithm = options.algorithm || 'sha512';
}
hash(password) {
const salt = crypto.randomBytes(this.saltLength);
const hash = crypto.pbkdf2Sync(
password,
salt,
this.iterations,
this.keyLength,
this.algorithm
);
return {
salt: salt.toString('hex'),
hash: hash.toString('hex'),
iterations: this.iterations
};
}
verify(password, salt, hash, iterations) {
const saltBuffer = Buffer.from(salt, 'hex');
const verifyHash = crypto.pbkdf2Sync(
password,
saltBuffer,
iterations,
this.keyLength,
this.algorithm
);
return hash === verifyHash.toString('hex');
}
generatePassword(length = 16) {
const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=';
let password = '';
const randomBytes = crypto.randomBytes(length);
for (let i = 0; i < length; i++) {
password += charset[randomBytes[i] % charset.length];
}
return password;
}
checkStrength(password) {
let score = 0;
if (password.length >= 8) score++;
if (password.length >= 12) score++;
if (/[a-z]/.test(password)) score++;
if (/[A-Z]/.test(password)) score++;
if (/[0-9]/.test(password)) score++;
if (/[^a-zA-Z0-9]/.test(password)) score++;
const levels = ['Very Weak', 'Weak', 'Fair', 'Strong', 'Very Strong', 'Excellent'];
return {
score,
level: levels[Math.min(score, levels.length - 1)]
};
}
}
module.exports = PasswordManager;
JWT Tokens
Let's implement JWT token management.
JavaScript
// utils/jwt.js
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
class TokenManager {
constructor(options = {}) {
this.accessSecret = options.accessSecret || process.env.JWT_ACCESS_SECRET;
this.refreshSecret = options.refreshSecret || process.env.JWT_REFRESH_SECRET;
this.accessExpiry = options.accessExpiry || '15m';
this.refreshExpiry = options.refreshExpiry || '7d';
}
generateAccessToken(payload) {
return jwt.sign(payload, this.accessSecret, {
expiresIn: this.accessExpiry,
issuer: 'auth-system'
});
}
generateRefreshToken(payload) {
return jwt.sign(payload, this.refreshSecret, {
expiresIn: this.refreshExpiry,
issuer: 'auth-system',
jwtid: crypto.randomBytes(16).toString('hex')
});
}
generateTokenPair(user) {
const payload = {
sub: user._id,
email: user.email,
role: user.role
};
return {
accessToken: this.generateAccessToken(payload),
refreshToken: this.generateRefreshToken(payload),
expiresIn: 900
};
}
verifyAccessToken(token) {
try {
return {
valid: true,
decoded: jwt.verify(token, this.accessSecret)
};
} catch (error) {
return {
valid: false,
error: error.message
};
}
}
verifyRefreshToken(token) {
try {
return {
valid: true,
decoded: jwt.verify(token, this.refreshSecret)
};
} catch (error) {
return {
valid: false,
error: error.message
};
}
}
decodeToken(token) {
return jwt.decode(token);
}
getTokenExpiry(token) {
const decoded = this.decodeToken(token);
if (decoded && decoded.exp) {
return new Date(decoded.exp * 1000);
}
return null;
}
blacklistToken(token, redisClient = null) {
const decoded = this.decodeToken(token);
if (decoded && decoded.exp) {
const ttl = decoded.exp - Math.floor(Date.now() / 1000);
if redisClient) {
return redisClient.setex(`blacklist:${token}`, ttl, '1');
}
}
return Promise.resolve();
}
}
module.exports = TokenManager;
OAuth Integration
Let's add OAuth 2.0 support with Google.
JavaScript
// config/passport.js
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const passport = require('passport');
const User = require('../models/User');
const initializeGoogleStrategy = () => {
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: '/auth/google/callback'
}, async (accessToken, refreshToken, profile, done) => {
try {
let user = await User.findOne({ providerId: profile.id });
if (user) {
return done(null, user);
}
const email = profile.emails[0].value;
user = await User.findOne({ email });
if (user) {
user.provider = 'google';
user.providerId = profile.id;
user.isVerified = true;
await user.save();
return done(null, user);
}
user = new User({
name: profile.displayName,
email,
provider: 'google',
providerId: profile.id,
isVerified: true,
avatar: profile.photos[0]?.value
});
await user.save();
done(null, user);
} catch (error) {
done(error, null);
}
}));
};
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser(async (id, done) => {
try {
const user = await User.findById(id);
done(null, user);
} catch (error) {
done(error, null);
}
});
module.exports = { initializeGoogleStrategy, passport };
Session Management
Let's implement secure session management.
JavaScript
// models/Session.js
const mongoose = require('mongoose');
const crypto = require('crypto');
const sessionSchema = mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
token: {
type: String,
required: true,
unique: true
},
fingerprint: String,
ip: String,
userAgent: String,
expiresAt: {
type: Date,
required: true
},
lastActivity: {
type: Date,
default: Date.now
},
isRevoked: {
type: Boolean,
default: false
}
}, {
timestamps: true
});
// Index for faster queries
sessionSchema.index({ user: 1, expiresAt: 1 });
sessionSchema.index({ token: 1 });
// Check if session is valid
sessionSchema.methods.isValid = function() {
return !this.isRevoked && this.expiresAt > Date.now();
};
// Update last activity
sessionSchema.methods.updateActivity = async function() {
this.lastActivity = Date.now();
return this.save();
};
// Revoke session
sessionSchema.methods.revoke = async function() {
this.isRevoked = true;
return this.save();
};
// Static method to create session
sessionSchema.statics.createSession = async function(user, info = {}) {
const token = crypto.randomBytes(64).toString('hex');
const session = new this({
user: user._id,
token,
fingerprint: info.fingerprint,
ip: info.ip,
userAgent: info.userAgent,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
});
await session.save();
return session;
};
// Clean up expired sessions
sessionSchema.statics.cleanup = async function() {
return this.deleteMany({
expiresAt: { $lt: Date.now() }
});
};
module.exports = mongoose.model('Session', sessionSchema);
Security Best Practices
Let's add security middleware.
JavaScript
// middleware/security.js
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const cors = require('cors');
// Security headers
const securityHeaders = helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:']
}
},
hsts: {
maxAge: 31536000,
includeSubDomains: true
}
});
// CORS configuration
const corsOptions = {
origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
credentials: true,
optionsSuccessStatus: 200,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization']
};
// General rate limiter
const generalLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
message: {
success: false,
error: 'Too many requests, please try again later.'
}
});
// Strict rate limiter for auth endpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
skipSuccessfulRequests: true,
message: {
success: false,
error: 'Too many login attempts, please try again later.'
}
});
// Login rate limiter
const loginLimiter = rateLimit({
windowMs: 60 * 60 * 1000,
max: 5,
message: {
success: false,
error: 'Too many login attempts from this IP, please try again after an hour.'
}
});
module.exports = {
securityHeaders,
corsOptions,
generalLimiter,
authLimiter,
loginLimiter
};
Testing
Bash
# Test authentication endpoints
# Register
curl -X POST http://localhost:5000/api/auth/register \
-H "Content-Type: application/json" \
-d '{
"name": "John Doe",
"email": "john@example.com",
"password": "SecurePassword123!"
}'
# Login
curl -X POST http://localhost:5000/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "john@example.com",
"password": "SecurePassword123!"
}'
# Access protected route
curl http://localhost:5000/api/users/me \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
# Refresh token
curl -X POST http://localhost:5000/api/auth/refresh \
-H "Content-Type: application/json" \
-d '{
"refreshToken": "YOUR_REFRESH_TOKEN"
}'
# Logout
curl -X POST http://localhost:5000/api/auth/logout \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Testing Checklist
- User can register with valid credentials
- Login works with correct password
- Invalid password is rejected
- JWT tokens are generated correctly
- Protected routes require valid token
- OAuth login works
Summary
Congratulations! You've built a complete web authentication system.
What You Built
- User Model - Secure user storage with locking
- Password Hashing - PBKDF2 with salt
- JWT Tokens - Access and refresh tokens
- OAuth - Google authentication
- Sessions - Session management
- Security - Headers and rate limiting
Next Steps
- Add email verification
- Implement 2FA
- Add password reset flow
- Implement account merging