Part 11 – Secure Authentication

Implementing secure authentication is crucial for any production Node.js application. This guide covers JWT, sessions, OAuth, and security best practices.

1. Authentication Strategies

Session-Based

  • Server stores session data
  • Cookie with session ID
  • Good for traditional web apps
  • Uses express-session

Token-Based (JWT)

  • Stateless authentication
  • Self-contained tokens
  • Ideal for APIs and SPAs
  • Uses jsonwebtoken

OAuth/Third-Party

  • Login via Google/Facebook/etc.
  • Uses passport.js
  • Great for user convenience
  • Still needs your own session/JWT

2. JWT Authentication Implementation

Setup and Configuration

const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');

// Store in environment variables
const JWT_SECRET = process.env.JWT_SECRET || 'super-secret-key';
const JWT_EXPIRES = process.env.JWT_EXPIRES || '30d';

User Registration

// Hash password before saving
userSchema.pre('save', async function(next) {
    if (!this.isModified('password')) return next();

    this.password = await bcrypt.hash(this.password, 12);
    next();
});

// In your controller
exports.register = async (req, res, next) => {
    try {
        const { email, password } = req.body;

        // 1. Check if user exists
        const existingUser = await User.findOne({ email });
        if (existingUser) {
            return res.status(400).json({ error: 'Email already in use' });
        }

        // 2. Create new user
        const user = await User.create({ email, password });

        // 3. Generate token
        const token = jwt.sign({ id: user._id }, JWT_SECRET, {
            expiresIn: JWT_EXPIRES
        });

        res.status(201).json({ token });
    } catch (err) {
        next(err);
    }
};

3. Login and Token Generation

exports.login = async (req, res, next) => {
    try {
        const { email, password } = req.body;

        // 1. Check if user exists
        const user = await User.findOne({ email }).select('+password');
        if (!user) {
            return res.status(401).json({ error: 'Invalid credentials' });
        }

        // 2. Verify password
        const isMatch = await bcrypt.compare(password, user.password);
        if (!isMatch) {
            return res.status(401).json({ error: 'Invalid credentials' });
        }

        // 3. Generate token
        const token = jwt.sign({ id: user._id }, JWT_SECRET, {
            expiresIn: JWT_EXPIRES
        });

        // 4. Send response
        res.json({ 
            token,
            user: {
                id: user._id,
                email: user.email
            }
        });
    } catch (err) {
        next(err);
    }
};

4. Protecting Routes with Middleware

Auth Middleware

exports.protect = async (req, res, next) => {
    try {
        // 1. Get token from header
        let token;
        if (req.headers.authorization && 
            req.headers.authorization.startsWith('Bearer')) {
            token = req.headers.authorization.split(' ')[1];
        }

        if (!token) {
            return res.status(401).json({ 
                error: 'Not authorized to access this route' 
            });
        }

        // 2. Verify token
        const decoded = jwt.verify(token, JWT_SECRET);

        // 3. Get user from DB
        const user = await User.findById(decoded.id);
        if (!user) {
            return res.status(401).json({ 
                error: 'User no longer exists' 
            });
        }

        // 4. Attach user to request
        req.user = user;
        next();
    } catch (err) {
        next(err);
    }
};

Role-Based Access

exports.restrictTo = (...roles) => {
    return (req, res, next) => {
        if (!roles.includes(req.user.role)) {
            return res.status(403).json({
                error: 'You do not have permission'
            });
        }
        next();
    };
};

// Usage in routes:
router.get('/admin', 
    authController.protect, 
    authController.restrictTo('admin'),
    adminController.getDashboard
);

5. Session-Based Authentication

Setup with express-session

const session = require('express-session');
const MongoStore = require('connect-mongo');

app.use(session({
    secret: process.env.SESSION_SECRET,
    resave: false,
    saveUninitialized: false,
    store: MongoStore.create({ 
        mongoUrl: process.env.DB_URI 
    }),
    cookie: { 
        maxAge: 24 * 60 * 60 * 1000, // 1 day
        secure: process.env.NODE_ENV === 'production',
        httpOnly: true
    }
}));

Login with Sessions

exports.login = async (req, res, next) => {
    try {
        const { email, password } = req.body;
        const user = await User.findOne({ email }).select('+password');

        if (!user || !(await bcrypt.compare(password, user.password))) {
            return res.status(401).json({ error: 'Invalid credentials' });
        }

        // Store user in session
        req.session.user = {
            id: user._id,
            email: user.email,
            role: user.role
        };

        res.json({ message: 'Logged in successfully' });
    } catch (err) {
        next(err);
    }
};

6. OAuth with Passport.js

Google OAuth Setup

const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;

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) => {
    // 1. Check if user exists
    let user = await User.findOne({ googleId: profile.id });

    if (!user) {
        // 2. Create new user if not exists
        user = await User.create({
            googleId: profile.id,
            email: profile.emails[0].value,
            name: profile.displayName
        });
    }

    done(null, user);
}));

// Serialize/deserialize user
passport.serializeUser((user, done) => done(null, user.id));
passport.deserializeUser(async (id, done) => {
    const user = await User.findById(id);
    done(null, user);
});

OAuth Routes

// Initiate Google auth
router.get('/auth/google',
    passport.authenticate('google', { scope: ['profile', 'email'] })
);

// Google callback
router.get('/auth/google/callback',
    passport.authenticate('google', { 
        failureRedirect: '/login',
        session: true 
    }),
    (req, res) => {
        // Successful auth
        res.redirect('/dashboard');
    }
);

// Protected route example
router.get('/dashboard', 
    (req, res, next) => {
        if (!req.isAuthenticated()) {
            return res.redirect('/login');
        }
        next();
    },
    dashboardController
);

7. Security Best Practices

  • Password Hashing: Always use bcrypt or Argon2
  • HTTPS: Essential for production
  • Cookie Security: httpOnly, secure, and sameSite flags
  • Rate Limiting: Prevent brute force attacks
  • CSRF Protection: Use csurf middleware
  • Token Expiry: Short-lived access tokens with refresh tokens
  • Input Validation: Never trust user input

Next: Error Handling and Debugging →

Authentication Checklist

  • ✅ Secure password storage (hashing + salt)
  • ✅ Protection against brute force attacks
  • ✅ Session/token expiration
  • ✅ Secure transmission (HTTPS only)
  • ✅ Role-based access control
  • ✅ Logging and monitoring

Leave a Comment

Your email address will not be published. Required fields are marked *