← Back to Tutorials
Node.js

Build a Web Authentication System

Difficulty: Intermediate Est. Time: ~4 hours

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

Continue Learning

Try these tutorials:

  • Build a REST API with JWT
  • Build a Session Management System
  • Build an API Gateway