Back to Course
Security

Authentication & Authorization in Node.js

Authentication and Authorization are the cornerstones of application security. Authentication verifies who a user is, while authorization determines what they can do. In Node.js applications, implementing these correctly is critical — a single vulnerability can expose your entire system.

Production Reality: Authentication and authorization are the most common attack vectors in web applications. Implementing them correctly is not optional — it's a requirement for any production application that handles user data.

1. Authentication vs Authorization — The Difference

AspectAuthenticationAuthorization
Question"Who are you?""What can you do?"
PurposeVerify user identityGrant or deny access
WhenDuring loginAfter authentication
MethodsPassword, JWT, OAuth, BiometricsRoles, Permissions, Policies
AnalogyShowing your ID at the doorGetting access to different rooms
// Authentication checks who you are
// Authorization checks what you can do

// ❌ Without authentication — anyone can access
app.get('/api/admin/users', (req, res) => {
    // No auth check — UNSAFE!
});

// ✅ With authentication and authorization
app.get('/api/admin/users', authenticateUser, authorizeRole('admin'), (req, res) => {
    // User is authenticated AND has admin role
});

2. Authentication Strategies

2.1 Session-Based Authentication (Traditional)

Uses cookies and server-side sessions. Simple and works well for traditional web applications.

Installation

npm install express-session
npm install connect-mongo  # For MongoDB session store
npm install bcrypt         # For password hashing
npm install cookie-parser

Session-Based Auth Implementation

const express = require('express');
const session = require('express-session');
const MongoStore = require('connect-mongo');
const bcrypt = require('bcrypt');
const cookieParser = require('cookie-parser');

const app = express();
app.use(express.json());
app.use(cookieParser());

// ✅ Session configuration
app.use(session({
    secret: process.env.SESSION_SECRET || 'your-secret-key',
    store: MongoStore.create({
        mongoUrl: process.env.MONGODB_URI,
        ttl: 14 * 24 * 60 * 60 // 14 days
    }),
    resave: false,
    saveUninitialized: false,
    cookie: {
        httpOnly: true,      // ✅ Can't be accessed by JavaScript
        secure: process.env.NODE_ENV === 'production', // ✅ HTTPS only
        sameSite: 'lax',      // ✅ CSRF protection
        maxAge: 24 * 60 * 60 * 1000 // 24 hours
    }
}));

// ✅ Register user
app.post('/api/auth/register', async (req, res) => {
    try {
        const { email, password, name } = req.body;

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

        // Hash password
        const saltRounds = 12;
        const hashedPassword = await bcrypt.hash(password, saltRounds);

        // Create user
        const user = new User({
            email,
            password: hashedPassword,
            name,
            role: 'user'
        });
        await user.save();

        // Create session
        req.session.userId = user._id;
        req.session.user = {
            id: user._id,
            email: user.email,
            name: user.name,
            role: user.role
        };

        res.status(201).json({
            message: 'Registration successful',
            user: req.session.user
        });
    } catch (err) {
        console.error('Registration error:', err);
        res.status(500).json({ error: 'Registration failed' });
    }
});

// ✅ Login
app.post('/api/auth/login', async (req, res) => {
    try {
        const { email, password } = req.body;

        // Find user
        const user = await User.findOne({ email });
        if (!user) {
            return res.status(401).json({ error: 'Invalid credentials' });
        }

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

        // Create session
        req.session.userId = user._id;
        req.session.user = {
            id: user._id,
            email: user.email,
            name: user.name,
            role: user.role
        };

        res.json({
            message: 'Login successful',
            user: req.session.user
        });
    } catch (err) {
        console.error('Login error:', err);
        res.status(500).json({ error: 'Login failed' });
    }
});

// ✅ Logout
app.post('/api/auth/logout', (req, res) => {
    req.session.destroy((err) => {
        if (err) {
            return res.status(500).json({ error: 'Logout failed' });
        }
        res.clearCookie('connect.sid');
        res.json({ message: 'Logged out successfully' });
    });
});

// ✅ Authentication middleware
const authenticateUser = (req, res, next) => {
    if (!req.session || !req.session.userId) {
        return res.status(401).json({ error: 'Authentication required' });
    }
    next();
};

// ✅ Protected route
app.get('/api/profile', authenticateUser, (req, res) => {
    res.json({ user: req.session.user });
});

2.2 JWT (JSON Web Token) Authentication — Modern API Standard

Stateless authentication suitable for APIs, mobile apps, and microservices.

Installation

npm install jsonwebtoken
npm install bcrypt
npm install dotenv

JWT Implementation

const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const { v4: uuidv4 } = require('uuid');

const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
const JWT_EXPIRY = process.env.JWT_EXPIRY || '7d';
const REFRESH_TOKEN_EXPIRY = process.env.REFRESH_TOKEN_EXPIRY || '30d';

// ✅ Generate JWT tokens
function generateTokens(user) {
    const accessToken = jwt.sign(
        { userId: user._id, email: user.email, role: user.role },
        JWT_SECRET,
        { expiresIn: JWT_EXPIRY }
    );

    const refreshToken = jwt.sign(
        { userId: user._id },
        JWT_SECRET,
        { expiresIn: REFRESH_TOKEN_EXPIRY }
    );

    return { accessToken, refreshToken };
}

// ✅ Register
app.post('/api/auth/register', async (req, res) => {
    try {
        const { email, password, name } = req.body;

        // Validate input
        if (!email || !password || !name) {
            return res.status(400).json({ error: 'All fields are required' });
        }

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

        // Hash password
        const saltRounds = 12;
        const hashedPassword = await bcrypt.hash(password, saltRounds);

        // Create user
        const user = new User({
            email,
            password: hashedPassword,
            name,
            role: 'user'
        });
        await user.save();

        // Generate tokens
        const { accessToken, refreshToken } = generateTokens(user);

        // Save refresh token (optional)
        user.refreshToken = refreshToken;
        await user.save();

        res.status(201).json({
            message: 'Registration successful',
            user: { id: user._id, email: user.email, name: user.name, role: user.role },
            accessToken,
            refreshToken
        });
    } catch (err) {
        console.error('Registration error:', err);
        res.status(500).json({ error: 'Registration failed' });
    }
});

// ✅ Login — JWT
app.post('/api/auth/login', async (req, res) => {
    try {
        const { email, password } = req.body;

        // Find user
        const user = await User.findOne({ email });
        if (!user) {
            return res.status(401).json({ error: 'Invalid credentials' });
        }

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

        // Generate tokens
        const { accessToken, refreshToken } = generateTokens(user);

        // Save refresh token
        user.refreshToken = refreshToken;
        await user.save();

        res.json({
            message: 'Login successful',
            user: { id: user._id, email: user.email, name: user.name, role: user.role },
            accessToken,
            refreshToken
        });
    } catch (err) {
        console.error('Login error:', err);
        res.status(500).json({ error: 'Login failed' });
    }
});

// ✅ Refresh token
app.post('/api/auth/refresh', async (req, res) => {
    try {
        const { refreshToken } = req.body;

        if (!refreshToken) {
            return res.status(400).json({ error: 'Refresh token required' });
        }

        // Verify refresh token
        const decoded = jwt.verify(refreshToken, JWT_SECRET);
        const user = await User.findById(decoded.userId);

        if (!user || user.refreshToken !== refreshToken) {
            return res.status(401).json({ error: 'Invalid refresh token' });
        }

        // Generate new tokens
        const tokens = generateTokens(user);

        // Update refresh token
        user.refreshToken = tokens.refreshToken;
        await user.save();

        res.json(tokens);
    } catch (err) {
        console.error('Refresh error:', err);
        res.status(401).json({ error: 'Invalid refresh token' });
    }
});

// ✅ Logout
app.post('/api/auth/logout', async (req, res) => {
    try {
        const token = req.headers.authorization?.split(' ')[1];
        if (token) {
            // Invalidate refresh token
            const decoded = jwt.verify(token, JWT_SECRET);
            const user = await User.findById(decoded.userId);
            if (user) {
                user.refreshToken = null;
                await user.save();
            }
        }
        res.json({ message: 'Logged out successfully' });
    } catch (err) {
        res.json({ message: 'Logged out successfully' });
    }
});

// ✅ JWT Authentication Middleware
const authenticateJWT = (req, res, next) => {
    const token = req.headers.authorization?.split(' ')[1];

    if (!token) {
        return res.status(401).json({ error: 'Authentication required' });
    }

    try {
        const decoded = jwt.verify(token, JWT_SECRET);
        req.user = decoded;
        next();
    } catch (err) {
        if (err.name === 'TokenExpiredError') {
            return res.status(401).json({ error: 'Token expired' });
        }
        return res.status(401).json({ error: 'Invalid token' });
    }
};

// ✅ Protected route with JWT
app.get('/api/profile', authenticateJWT, async (req, res) => {
    try {
        const user = await User.findById(req.user.userId).select('-password -refreshToken');
        res.json({ user });
    } catch (err) {
        res.status(500).json({ error: 'Failed to fetch profile' });
    }
});

2.3 OAuth 2.0 / Social Login (Google, GitHub, Facebook)

Allows users to authenticate using their existing accounts from third-party providers.

Installation (Passport.js)

npm install passport
npm install passport-google-oauth20
npm install passport-github
npm install express-session

OAuth Implementation with Passport.js

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

// ✅ Google OAuth Strategy
passport.use(new GoogleStrategy({
    clientID: process.env.GOOGLE_CLIENT_ID,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    callbackURL: '/api/auth/google/callback'
}, async (accessToken, refreshToken, profile, done) => {
    try {
        // Find or create user
        let user = await User.findOne({ googleId: profile.id });
        if (!user) {
            user = await User.findOne({ email: profile.emails[0].value });
            if (user) {
                user.googleId = profile.id;
                await user.save();
            } else {
                user = new User({
                    googleId: profile.id,
                    email: profile.emails[0].value,
                    name: profile.displayName,
                    avatar: profile.photos[0]?.value,
                    role: 'user',
                    isVerified: true
                });
                await user.save();
            }
        }
        return done(null, user);
    } catch (err) {
        return done(err, null);
    }
}));

// ✅ Routes for Google OAuth
app.get('/api/auth/google',
    passport.authenticate('google', { scope: ['profile', 'email'] })
);

app.get('/api/auth/google/callback',
    passport.authenticate('google', { session: true, failureRedirect: '/login' }),
    (req, res) => {
        // Generate JWT or start session
        const token = jwt.sign(
            { userId: req.user._id, email: req.user.email, role: req.user.role },
            JWT_SECRET,
            { expiresIn: '7d' }
        );
        res.redirect(`${process.env.FRONTEND_URL}/auth/callback?token=${token}`);
    }
);

3. Authorization — Role-Based Access Control (RBAC)

RBAC restricts system access based on user roles and permissions.

Role & Permission Models

// models/Role.js
const mongoose = require('mongoose');

const roleSchema = new mongoose.Schema({
    name: { type: String, required: true, unique: true },
    permissions: [{ type: String }],
    createdAt: { type: Date, default: Date.now }
});

module.exports = mongoose.model('Role', roleSchema);

// models/User.js — Add role field
const userSchema = new mongoose.Schema({
    // ... other fields
    role: {
        type: String,
        enum: ['user', 'moderator', 'admin', 'superadmin'],
        default: 'user'
    },
    permissions: [{ type: String }] // For granular permissions
});

Authorization Middleware

// middleware/authorization.js

// ✅ Check if user has a specific role
const authorizeRole = (...allowedRoles) => {
    return (req, res, next) => {
        if (!req.user) {
            return res.status(401).json({ error: 'Authentication required' });
        }

        // For JWT: req.user is from token
        // For Session: req.user from session
        const userRole = req.user.role || req.session?.user?.role;

        if (!userRole || !allowedRoles.includes(userRole)) {
            return res.status(403).json({ error: 'Insufficient permissions' });
        }
        next();
    };
};

// ✅ Check if user has specific permissions (granular)
const authorizePermission = (requiredPermission) => {
    return (req, res, next) => {
        if (!req.user) {
            return res.status(401).json({ error: 'Authentication required' });
        }

        const userPermissions = req.user.permissions || req.session?.user?.permissions || [];

        if (!userPermissions.includes(requiredPermission)) {
            return res.status(403).json({ error: 'Insufficient permissions' });
        }
        next();
    };
};

// ✅ Check if user owns a resource (resource-based authorization)
const authorizeResourceOwner = (getResourceId, getUserId) => {
    return async (req, res, next) => {
        try {
            const resourceId = getResourceId(req);
            const userId = getUserId(req);

            // This example uses JWT, adapt for session
            const resource = await Resource.findById(resourceId);
            if (!resource) {
                return res.status(404).json({ error: 'Resource not found' });
            }

            if (resource.userId.toString() !== userId.toString()) {
                return res.status(403).json({ error: 'You can only access your own resources' });
            }
            next();
        } catch (err) {
            next(err);
        }
    };
};

// ✅ Combine authentication + role authorization
const authAndRole = (allowedRoles) => {
    return [authenticateJWT, authorizeRole(...allowedRoles)];
};

module.exports = {
    authorizeRole,
    authorizePermission,
    authorizeResourceOwner,
    authAndRole
};

Protected Routes with Authorization

const { authenticateJWT } = require('../middleware/auth');
const { authorizeRole, authorizePermission } = require('../middleware/authorization');

// ✅ Only admins can access
app.get('/api/admin/users',
    authenticateJWT,
    authorizeRole('admin', 'superadmin'),
    async (req, res) => {
        const users = await User.find().select('-password -refreshToken');
        res.json(users);
    }
);

// ✅ Only superadmins can delete users
app.delete('/api/admin/users/:id',
    authenticateJWT,
    authorizeRole('superadmin'),
    async (req, res) => {
        const user = await User.findByIdAndDelete(req.params.id);
        if (!user) {
            return res.status(404).json({ error: 'User not found' });
        }
        res.status(204).send();
    }
);

// ✅ Granular permission check
app.post('/api/posts',
    authenticateJWT,
    authorizePermission('create:posts'),
    async (req, res) => {
        // Create post
    }
);

// ✅ Resource ownership check
app.put('/api/posts/:id',
    authenticateJWT,
    authorizeResourceOwner(
        (req) => req.params.id,
        (req) => req.user.userId
    ),
    async (req, res) => {
        // Update post (user owns it)
    }
);

4. Security Best Practices — Production Checklist

These security practices are critical for production.

Password Security

  • ✅ Use bcrypt with salt rounds ≥ 12bcrypt.hash(password, 12)
  • ✅ Never store plaintext passwords — Always hash before saving.
  • ✅ Require strong passwords — Minimum 8 characters, mix of cases, numbers, symbols.
  • ✅ Implement rate limiting — Prevent brute force attacks.
  • ✅ Use account lockout — Lock account after multiple failed attempts.

JWT Security

  • ✅ Use strong secrets — 256-bit+ secrets from environment variables.
  • ✅ Set short expiration times — 15 minutes for access tokens, 7 days for refresh tokens.
  • ✅ Store tokens securely — Use HTTP-only cookies or secure storage.
  • ✅ Implement token rotation — Issue new refresh tokens on each refresh.
  • ✅ Blacklist tokens — Maintain a token blacklist for logout/revocation.

Session Security

  • ✅ Use secure cookies{ httpOnly: true, secure: true, sameSite: 'lax' }
  • ✅ Use a session store — Redis or MongoDB for persistent sessions.
  • ✅ Set session expiration — Automatically expire inactive sessions.
  • ✅ Regenerate session ID on login — Prevent session fixation attacks.

General Security

  • ✅ Use Helmet.jsapp.use(helmet()) for security headers.
  • ✅ Enable CORS properly — Only allow trusted origins.
  • ✅ Use HTTPS in production — Always use TLS/SSL.
  • ✅ Validate all input — Use express-validator or Joi.
  • ✅ Sanitize output — Prevent XSS attacks.
  • ✅ Implement rate limitingexpress-rate-limit.
  • ✅ Keep dependencies updated — Run npm audit regularly.
  • ✅ Use environment variables — Never hardcode secrets.

5. Common Security Pitfalls & Solutions

Pitfall 1: Storing JWT in localStorage

Problem: Vulnerable to XSS attacks.

Solution: Use HTTP-only cookies instead.

// ✅ HTTP-only cookie for JWT
res.cookie('accessToken', token, {
    httpOnly: true,
    secure: true,
    sameSite: 'lax',
    maxAge: 15 * 60 * 1000 // 15 minutes
});

Pitfall 2: Not implementing rate limiting

Problem: Brute force attacks on login endpoints.

Solution: Use express-rate-limit.

const rateLimit = require('express-rate-limit');

const loginLimiter = rateLimit({
    windowMs: 15 * 60 * 1000, // 15 minutes
    max: 5, // 5 attempts per window
    message: 'Too many login attempts. Please try again later.'
});

app.post('/api/auth/login', loginLimiter, handleLogin);

Pitfall 3: Returning too much information in errors

Problem: Exposes internal details to attackers.

Solution: Only return generic error messages.

// ❌ Bad — exposes internal info
res.status(401).json({ error: 'User with email john@example.com not found' });

// ✅ Good — generic message
res.status(401).json({ error: 'Invalid credentials' });

6. Authentication Comparison — Choosing the Right Method

FactorSession-BasedJWTOAuth 2.0
StateStateful (server stores session)StatelessStateful
ScalabilityRequires shared session storeHighly scalableRequires token management
Mobile AppsWorks with cookies✅ Best for mobile✅ Excellent
Microservices❌ Complex✅ Ideal✅ Good
Logout✅ Immediate❌ Requires blacklist✅ With revocation
ImplementationSimpleModerateComplex
Use CaseTraditional web appsAPIs, SPAs, mobileSocial login, SSO

7. Production-Ready Checklist

  • ✅ Use bcrypt — For password hashing with salt rounds ≥ 12.
  • ✅ Implement JWT — With short-lived access tokens and refresh tokens.
  • ✅ Use HTTP-only cookies — For JWT storage (instead of localStorage).
  • ✅ Implement RBAC — Role-based access control.
  • ✅ Add rate limiting — Protect login and registration endpoints.
  • ✅ Use Helmet.js — Security headers.
  • ✅ Validate input — Use express-validator.
  • ✅ Sanitize output — Prevent XSS.
  • ✅ Enable HTTPS — Always in production.
  • ✅ Use environment variables — For all secrets.
  • ✅ Implement token rotation — For refresh tokens.
  • ✅ Add session expiration — Auto-logout inactive users.
  • ✅ Log security events — Failed logins, permission denials.
  • ✅ Run security auditsnpm audit regularly.
Production Wisdom: Security is not a feature — it's a mindset. Implement authentication and authorization from day one, not as an afterthought. A security breach can destroy trust, reputation, and your business. The time invested in proper security practices is the best investment you can make in your application.

Ready to master Node.js?

Build scalable, high-performance backend applications with Node.js. Learn from industry experts with hands-on projects.

Explore Course