Implementing Social Login

Building a complete social authentication system for your web applications

Introduction to Social Login

Social login (also known as social sign-in or OAuth authentication) allows users to authenticate with your application using their existing accounts from social platforms like Google, Facebook, GitHub, and others. This approach offers several benefits for both users and developers:

Benefits of Social Login

  • Improved User Experience: Users don't need to create and remember new credentials
  • Higher Conversion Rates: Removes friction from the registration process
  • Enhanced Security: Delegates authentication to trusted platforms with robust security measures
  • Access to User Data: Potentially retrieve profile information, contacts, and other data (with permission)
  • Reduced Development Effort: No need to build password recovery flows, email verification, etc.

In this lecture, we'll build a complete social login implementation using Node.js, Express, MongoDB, and Passport.js, with support for multiple providers and a unified user experience.

graph TD A[User] -->|Clicks "Login with Google"| B[Web App] B -->|Redirects to| C[Google] C -->|Authenticates User| C C -->|Returns with Auth Code| B B -->|Exchanges Code for Token| C B -->|Gets User Profile| C B -->|Creates/Updates User| D[(Database)] B -->|Sets Session/Cookie| A subgraph "Authentication Flow" B C D end

Project Setup and Configuration

Let's start by setting up our project structure and installing the necessary dependencies:

Project Structure

social-login-app/
├── config/
│   ├── passport.js     # Passport strategies configuration
│   └── database.js     # Database connection
├── controllers/
│   ├── authController.js   # Authentication logic
│   └── userController.js   # User-related logic
├── models/
│   └── User.js         # User database model
├── routes/
│   ├── authRoutes.js   # Authentication routes
│   └── userRoutes.js   # User-related routes
├── views/
│   ├── layouts/
│   │   └── main.handlebars  # Main layout template
│   ├── home.handlebars      # Home page
│   ├── login.handlebars     # Login page
│   └── profile.handlebars   # User profile page
├── public/
│   ├── css/
│   ├── js/
│   └── images/
├── .env               # Environment variables
├── app.js             # Main application file
└── package.json       # Project dependencies

Installing Dependencies

// Initialize npm project
npm init -y

// Install core dependencies
npm install express express-handlebars express-session connect-mongo mongoose dotenv

// Install passport and strategies
npm install passport passport-google-oauth20 passport-facebook passport-github2 passport-twitter

// Install utilities
npm install connect-flash bcryptjs helmet cors morgan

Environment Configuration

Create a .env file in the root directory to store your sensitive configuration:

# .env file
NODE_ENV=development
PORT=3000
MONGODB_URI=mongodb://localhost:27017/social-login
SESSION_SECRET=your_super_secure_session_secret

# Google OAuth
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret

# Facebook OAuth
FACEBOOK_APP_ID=your_facebook_app_id
FACEBOOK_APP_SECRET=your_facebook_app_secret

# GitHub OAuth
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret

# Twitter OAuth
TWITTER_CONSUMER_KEY=your_twitter_consumer_key
TWITTER_CONSUMER_SECRET=your_twitter_consumer_secret

Obtaining OAuth Credentials

You'll need to register your application with each provider to get the required credentials:

For each provider, you'll need to specify the appropriate callback URLs (e.g., http://localhost:3000/auth/google/callback for development).

Database Models and Configuration

Let's set up our MongoDB connection and define the User model that will store authentication information from various providers:

Database Connection

// config/database.js
const mongoose = require('mongoose');

const connectDB = async () => {
  try {
    const conn = await mongoose.connect(process.env.MONGODB_URI, {
      useNewUrlParser: true,
      useUnifiedTopology: true,
      useFindAndModify: false,
      useCreateIndex: true
    });
    
    console.log(`MongoDB Connected: ${conn.connection.host}`);
  } catch (err) {
    console.error(`Error connecting to MongoDB: ${err.message}`);
    process.exit(1);
  }
};

module.exports = connectDB;

User Model

Create a flexible User model that can store authentication information from different providers:

// models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');

const UserSchema = new mongoose.Schema({
  // Common user fields
  name: {
    type: String,
    required: true
  },
  email: {
    type: String,
    required: true,
    unique: true,
    lowercase: true,
    trim: true
  },
  password: {
    type: String,
    // Not required because social login users won't have a password
  },
  avatar: {
    type: String
  },
  role: {
    type: String,
    enum: ['user', 'admin'],
    default: 'user'
  },
  
  // Social provider fields
  googleId: String,
  facebookId: String,
  githubId: String,
  twitterId: String,
  
  // Provider data storage
  providerData: {
    type: Map,
    of: mongoose.Schema.Types.Mixed
  },
  
  // Account status
  emailVerified: {
    type: Boolean,
    default: false
  },
  active: {
    type: Boolean,
    default: true
  },
  
  // Timestamps
  createdAt: {
    type: Date,
    default: Date.now
  },
  updatedAt: {
    type: Date,
    default: Date.now
  }
});

// Pre-save middleware to hash password
UserSchema.pre('save', async function(next) {
  // Only hash the password if it has been modified (or is new)
  if (!this.isModified('password') || !this.password) {
    return next();
  }
  
  try {
    // Generate a salt
    const salt = await bcrypt.genSalt(10);
    
    // Hash the password with the salt
    this.password = await bcrypt.hash(this.password, salt);
    this.updatedAt = Date.now();
    next();
  } catch (err) {
    next(err);
  }
});

// Method to compare password
UserSchema.methods.comparePassword = async function(candidatePassword) {
  try {
    if (!this.password) return false;
    return await bcrypt.compare(candidatePassword, this.password);
  } catch (err) {
    throw err;
  }
};

// Method to check if user has a specific provider
UserSchema.methods.hasProvider = function(provider) {
  return Boolean(this[`${provider}Id`]);
};

// Method to list all connected providers
UserSchema.methods.getProviders = function() {
  const providers = [];
  
  if (this.googleId) providers.push('google');
  if (this.facebookId) providers.push('facebook');
  if (this.githubId) providers.push('github');
  if (this.twitterId) providers.push('twitter');
  if (this.password) providers.push('local');
  
  return providers;
};

const User = mongoose.model('User', UserSchema);

module.exports = User;

Model Design Considerations

This model design addresses several important aspects of social login:

  • Provider IDs: Separate fields for each provider's unique identifier
  • Dynamic Provider Data: Using a Map to store provider-specific data
  • Password Support: Optional password field for hybrid authentication
  • Helper Methods: Utility functions to check providers and manage authentication

This flexible approach allows users to connect multiple social accounts to a single user profile.

Passport.js Configuration

Now, let's configure Passport.js with strategies for multiple social providers:

// config/passport.js
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const FacebookStrategy = require('passport-facebook').Strategy;
const GitHubStrategy = require('passport-github2').Strategy;
const TwitterStrategy = require('passport-twitter').Strategy;
const LocalStrategy = require('passport-local').Strategy;
const User = require('../models/User');

// Serialize user into the session
passport.serializeUser((user, done) => {
  done(null, user.id);
});

// Deserialize user from the session
passport.deserializeUser(async (id, done) => {
  try {
    const user = await User.findById(id);
    done(null, user);
  } catch (err) {
    done(err);
  }
});

// Common OAuth callback function
async function oauthCallback(accessToken, refreshToken, profile, provider, done) {
  try {
    // Extract profile information based on provider
    const profileData = extractProfileData(profile, provider);
    
    // First, try to find by provider ID
    const providerField = `${provider}Id`;
    let user = await User.findOne({ [providerField]: profile.id });
    
    // If user exists, update their profile data
    if (user) {
      // Update user with latest provider data
      user.providerData.set(provider, {
        accessToken,
        refreshToken,
        profile: profile._json
      });
      
      user.updatedAt = Date.now();
      await user.save();
      
      return done(null, user);
    }
    
    // If no user with this provider ID, check by email
    if (profileData.email) {
      user = await User.findOne({ email: profileData.email });
      
      if (user) {
        // Connect this provider to existing account
        user[providerField] = profile.id;
        user.providerData.set(provider, {
          accessToken,
          refreshToken,
          profile: profile._json
        });
        
        user.updatedAt = Date.now();
        
        // If email wasn't verified before but provider verifies it
        if (!user.emailVerified && profileData.emailVerified) {
          user.emailVerified = true;
        }
        
        await user.save();
        return done(null, user);
      }
    }
    
    // Create new user if not found
    const newUser = new User({
      name: profileData.name,
      email: profileData.email || `${profile.id}@${provider}.user`,
      avatar: profileData.avatar,
      [providerField]: profile.id,
      emailVerified: profileData.emailVerified,
      providerData: new Map([[
        provider, {
          accessToken,
          refreshToken,
          profile: profile._json
        }
      ]])
    });
    
    await newUser.save();
    return done(null, newUser);
  } catch (err) {
    return done(err);
  }
}

// Helper to extract profile data from different providers
function extractProfileData(profile, provider) {
  switch (provider) {
    case 'google':
      return {
        name: profile.displayName,
        email: profile.emails?.[0]?.value,
        avatar: profile.photos?.[0]?.value,
        emailVerified: true // Google verifies emails
      };
    
    case 'facebook':
      return {
        name: profile.displayName,
        email: profile.emails?.[0]?.value,
        avatar: profile.photos?.[0]?.value,
        emailVerified: true // Facebook verifies emails
      };
    
    case 'github':
      return {
        name: profile.displayName || profile.username,
        email: profile.emails?.[0]?.value,
        avatar: profile.photos?.[0]?.value,
        emailVerified: Boolean(profile.emails?.[0]?.verified)
      };
    
    case 'twitter':
      return {
        name: profile.displayName,
        // Twitter doesn't provide email by default
        email: profile.emails?.[0]?.value,
        avatar: profile.photos?.[0]?.value.replace('_normal', ''),
        emailVerified: false // Twitter doesn't verify emails by default
      };
    
    default:
      return {
        name: profile.displayName,
        email: profile.emails?.[0]?.value,
        avatar: profile.photos?.[0]?.value,
        emailVerified: false
      };
  }
}

// Configure Google Strategy
passport.use(new GoogleStrategy({
  clientID: process.env.GOOGLE_CLIENT_ID,
  clientSecret: process.env.GOOGLE_CLIENT_SECRET,
  callbackURL: '/auth/google/callback',
  scope: ['profile', 'email']
}, (accessToken, refreshToken, profile, done) => {
  oauthCallback(accessToken, refreshToken, profile, 'google', done);
}));

// Configure Facebook Strategy
passport.use(new FacebookStrategy({
  clientID: process.env.FACEBOOK_APP_ID,
  clientSecret: process.env.FACEBOOK_APP_SECRET,
  callbackURL: '/auth/facebook/callback',
  profileFields: ['id', 'displayName', 'photos', 'email']
}, (accessToken, refreshToken, profile, done) => {
  oauthCallback(accessToken, refreshToken, profile, 'facebook', done);
}));

// Configure GitHub Strategy
passport.use(new GitHubStrategy({
  clientID: process.env.GITHUB_CLIENT_ID,
  clientSecret: process.env.GITHUB_CLIENT_SECRET,
  callbackURL: '/auth/github/callback',
  scope: ['user:email']
}, (accessToken, refreshToken, profile, done) => {
  oauthCallback(accessToken, refreshToken, profile, 'github', done);
}));

// Configure Twitter Strategy
passport.use(new TwitterStrategy({
  consumerKey: process.env.TWITTER_CONSUMER_KEY,
  consumerSecret: process.env.TWITTER_CONSUMER_SECRET,
  callbackURL: '/auth/twitter/callback',
  includeEmail: true
}, (token, tokenSecret, profile, done) => {
  // For Twitter, token secret is used instead of refresh token
  oauthCallback(token, tokenSecret, profile, 'twitter', done);
}));

// Configure Local Strategy
passport.use(new LocalStrategy(
  { usernameField: 'email' },
  async (email, password, done) => {
    try {
      // Find user by email
      const user = await User.findOne({ email: email.toLowerCase() });
      
      // User not found
      if (!user) {
        return done(null, false, { message: 'Invalid email or password' });
      }
      
      // User found but no password (social auth only)
      if (!user.password) {
        return done(null, false, { message: 'This account does not have a password. Please log in with a social provider.' });
      }
      
      // Check password
      const isMatch = await user.comparePassword(password);
      
      if (!isMatch) {
        return done(null, false, { message: 'Invalid email or password' });
      }
      
      return done(null, user);
    } catch (err) {
      return done(err);
    }
  }
));

module.exports = passport;

Key Features of This Configuration

  • Unified OAuth Callback: A common function handles all social providers
  • Profile Data Extraction: Provider-specific logic to normalize user data
  • Account Linking: Automatically links accounts with matching emails
  • Provider Data Storage: Stores access tokens and profile data for future API calls
  • Email Verification: Tracks which providers verify email addresses
  • Local Strategy: Supports traditional email/password authentication alongside social login

This approach makes it easy to add more providers in the future while maintaining consistent behavior.

Authentication Routes

Now, let's set up the routes for various authentication methods:

// routes/authRoutes.js
const express = require('express');
const passport = require('passport');
const router = express.Router();
const User = require('../models/User');

// Helper function to handle auth redirect
function handleAuthRedirect(req, res) {
  // Redirect to previous page or default to profile
  const redirectUrl = req.session.returnTo || '/profile';
  delete req.session.returnTo;
  res.redirect(redirectUrl);
}

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

router.get('/google/callback', 
  passport.authenticate('google', { failureRedirect: '/login' }),
  handleAuthRedirect
);

// Facebook OAuth Routes
router.get('/facebook', passport.authenticate('facebook', { scope: ['email'] }));

router.get('/facebook/callback',
  passport.authenticate('facebook', { failureRedirect: '/login' }),
  handleAuthRedirect
);

// GitHub OAuth Routes
router.get('/github', passport.authenticate('github', { scope: ['user:email'] }));

router.get('/github/callback',
  passport.authenticate('github', { failureRedirect: '/login' }),
  handleAuthRedirect
);

// Twitter OAuth Routes
router.get('/twitter', passport.authenticate('twitter'));

router.get('/twitter/callback',
  passport.authenticate('twitter', { failureRedirect: '/login' }),
  handleAuthRedirect
);

// Local Authentication Routes
router.post('/login', (req, res, next) => {
  passport.authenticate('local', (err, user, info) => {
    if (err) return next(err);
    
    if (!user) {
      req.flash('error', info.message);
      return res.redirect('/login');
    }
    
    req.login(user, (err) => {
      if (err) return next(err);
      return handleAuthRedirect(req, res);
    });
  })(req, res, next);
});

// Registration route
router.post('/register', async (req, res, next) => {
  try {
    const { name, email, password, password2 } = req.body;
    
    // Basic validation
    const errors = [];
    
    if (!name || !email || !password || !password2) {
      errors.push({ msg: 'Please fill in all fields' });
    }
    
    if (password !== password2) {
      errors.push({ msg: 'Passwords do not match' });
    }
    
    if (password.length < 6) {
      errors.push({ msg: 'Password should be at least 6 characters' });
    }
    
    if (errors.length > 0) {
      return res.render('register', {
        errors,
        name,
        email
      });
    }
    
    // Check if user already exists
    const existingUser = await User.findOne({ email: email.toLowerCase() });
    
    if (existingUser) {
      if (existingUser.password) {
        // User exists with password
        errors.push({ msg: 'Email is already registered' });
        return res.render('register', {
          errors,
          name,
          email
        });
      } else {
        // User exists but only with social login, set password
        existingUser.name = name;
        existingUser.password = password;
        await existingUser.save();
        
        req.flash('success_msg', 'Password set successfully, you can now log in with email');
        return res.redirect('/login');
      }
    }
    
    // Create new user
    const newUser = new User({
      name,
      email: email.toLowerCase(),
      password
    });
    
    await newUser.save();
    
    req.flash('success_msg', 'You are now registered and can log in');
    res.redirect('/login');
  } catch (err) {
    next(err);
  }
});

// Logout route
router.get('/logout', (req, res) => {
  req.logout();
  req.flash('success_msg', 'You are logged out');
  res.redirect('/');
});

// Account linking routes
router.get('/connect/google',
  passport.authorize('google', { scope: ['profile', 'email'] })
);

router.get('/connect/facebook',
  passport.authorize('facebook', { scope: ['email'] })
);

router.get('/connect/github',
  passport.authorize('github', { scope: ['user:email'] })
);

router.get('/connect/twitter',
  passport.authorize('twitter')
);

// Account unlinking routes
router.get('/unlink/:provider', async (req, res, next) => {
  try {
    if (!req.user) {
      req.flash('error_msg', 'You must be logged in');
      return res.redirect('/login');
    }
    
    const provider = req.params.provider;
    const validProviders = ['google', 'facebook', 'github', 'twitter', 'local'];
    
    if (!validProviders.includes(provider)) {
      req.flash('error_msg', 'Invalid provider');
      return res.redirect('/profile');
    }
    
    // Get user and connected providers
    const user = await User.findById(req.user.id);
    const connectedProviders = user.getProviders();
    
    // Don't allow unlinking the only provider
    if (connectedProviders.length <= 1) {
      req.flash('error_msg', 'Cannot unlink your only authentication method');
      return res.redirect('/profile');
    }
    
    // Unlink provider
    if (provider === 'local') {
      user.password = undefined;
    } else {
      user[`${provider}Id`] = undefined;
      user.providerData.delete(provider);
    }
    
    await user.save();
    
    req.flash('success_msg', `${provider.charAt(0).toUpperCase() + provider.slice(1)} successfully unlinked`);
    res.redirect('/profile');
  } catch (err) {
    next(err);
  }
});

module.exports = router;

Route Design Patterns

These routes implement several important patterns:

  • Return URL Handling: Redirects users back to their original destination after authentication
  • Flash Messages: Provides feedback to users about authentication actions
  • Account Linking/Unlinking: Allows users to connect and disconnect social accounts
  • Hybrid Account Management: Handles cases where users have both social and local authentication
  • Error Handling: Provides user-friendly error messages for authentication issues

This comprehensive approach gives users flexibility in how they authenticate while maintaining security.

Application Setup

Let's set up the main application file to tie everything together:

// app.js
const express = require('express');
const exphbs = require('express-handlebars');
const session = require('express-session');
const MongoStore = require('connect-mongo')(session);
const mongoose = require('mongoose');
const passport = require('./config/passport');
const flash = require('connect-flash');
const helmet = require('helmet');
const cors = require('cors');
const morgan = require('morgan');
const path = require('path');
const connectDB = require('./config/database');
require('dotenv').config();

// Import routes
const authRoutes = require('./routes/authRoutes');
const userRoutes = require('./routes/userRoutes');

// Initialize app
const app = express();

// Connect to database
connectDB();

// Middleware
if (process.env.NODE_ENV === 'development') {
  app.use(morgan('dev'));
}

// Body parser
app.use(express.urlencoded({ extended: false }));
app.use(express.json());

// Security headers
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "'unsafe-inline'", 'cdnjs.cloudflare.com'],
      styleSrc: ["'self'", "'unsafe-inline'", 'fonts.googleapis.com', 'cdnjs.cloudflare.com'],
      fontSrc: ["'self'", 'fonts.gstatic.com', 'cdnjs.cloudflare.com'],
      imgSrc: ["'self'", 'data:', '*.googleusercontent.com', '*.githubusercontent.com', '*.fbcdn.net', 'twimg.com'],
    },
  },
}));

// CORS
app.use(cors());

// Static folder
app.use(express.static(path.join(__dirname, 'public')));

// Handlebars
app.engine('handlebars', exphbs({
  defaultLayout: 'main',
  helpers: {
    // Custom helpers
    ifEquals: function(arg1, arg2, options) {
      return (arg1 === arg2) ? options.fn(this) : options.inverse(this);
    },
    includes: function(array, value, options) {
      return array.includes(value) ? options.fn(this) : options.inverse(this);
    }
  }
}));
app.set('view engine', 'handlebars');

// Sessions
app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  store: new MongoStore({ mongooseConnection: mongoose.connection }),
  cookie: {
    maxAge: 1000 * 60 * 60 * 24, // 1 day
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax'
  }
}));

// Passport middleware
app.use(passport.initialize());
app.use(passport.session());

// Flash messages
app.use(flash());

// Global variables
app.use((req, res, next) => {
  res.locals.success_msg = req.flash('success_msg');
  res.locals.error_msg = req.flash('error_msg');
  res.locals.error = req.flash('error');
  res.locals.user = req.user || null;
  next();
});

// Store return URL for redirects
app.use((req, res, next) => {
  if (!req.isAuthenticated() && req.method === 'GET' && !req.originalUrl.startsWith('/auth') && req.originalUrl !== '/login' && req.originalUrl !== '/register') {
    req.session.returnTo = req.originalUrl;
  }
  next();
});

// Routes
app.get('/', (req, res) => {
  res.render('home');
});

app.get('/login', (req, res) => {
  if (req.isAuthenticated()) {
    return res.redirect('/profile');
  }
  res.render('login');
});

app.get('/register', (req, res) => {
  if (req.isAuthenticated()) {
    return res.redirect('/profile');
  }
  res.render('register');
});

// Authentication middleware
function ensureAuthenticated(req, res, next) {
  if (req.isAuthenticated()) {
    return next();
  }
  req.flash('error_msg', 'Please log in to view this resource');
  res.redirect('/login');
}

// Protected route
app.get('/profile', ensureAuthenticated, (req, res) => {
  res.render('profile', {
    user: req.user,
    providers: req.user.getProviders()
  });
});

// Mount route files
app.use('/auth', authRoutes);
app.use('/user', ensureAuthenticated, userRoutes);

// 404 handler
app.use((req, res) => {
  res.status(404).render('404');
});

// Error handler
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).render('error', {
    message: process.env.NODE_ENV === 'production' ? 'Something went wrong' : err.message
  });
});

// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running in ${process.env.NODE_ENV} mode on port ${PORT}`);
});

Security Considerations

This implementation includes several important security measures:

  • Helmet.js: Sets secure HTTP headers including Content Security Policy
  • Secure Cookies: HttpOnly, Secure, and SameSite attributes protect session cookies
  • Session Store: Using MongoDB instead of the default MemoryStore for production readiness
  • CSRF Protection: Through SameSite cookies and proper session management
  • Input Validation: Validates user input for registration and profile updates
  • Error Handling: Production-appropriate error messages that don't leak implementation details

These measures help protect user data and prevent common security vulnerabilities.

View Templates

Let's create the Handlebars templates for the user interface. First, the main layout:

// views/layouts/main.handlebars
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Social Login Demo</title>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/4.6.0/css/bootstrap.min.css">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
  <link rel="stylesheet" href="/css/style.css">
</head>
<body>
  <nav class="navbar navbar-expand-lg navbar-dark bg-primary mb-4">
    <div class="container">
      <a class="navbar-brand" href="/">Social Login Demo</a>
      <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav">
        <span class="navbar-toggler-icon"></span>
      </button>
      <div class="collapse navbar-collapse" id="navbarNav">
        <ul class="navbar-nav ml-auto">
          <li class="nav-item">
            <a class="nav-link" href="/">Home</a>
          </li>
          {{#if user}}
            <li class="nav-item">
              <a class="nav-link" href="/profile">Profile</a>
            </li>
            <li class="nav-item">
              <a class="nav-link" href="/auth/logout">Logout</a>
            </li>
          {{else}}
            <li class="nav-item">
              <a class="nav-link" href="/login">Login</a>
            </li>
            <li class="nav-item">
              <a class="nav-link" href="/register">Register</a>
            </li>
          {{/if}}
        </ul>
      </div>
    </div>
  </nav>

  <div class="container">
    {{#if success_msg}}
      <div class="alert alert-success alert-dismissible fade show">
        {{success_msg}}
        <button type="button" class="close" data-dismiss="alert"><span>&times;</span></button>
      </div>
    {{/if}}
    
    {{#if error_msg}}
      <div class="alert alert-danger alert-dismissible fade show">
        {{error_msg}}
        <button type="button" class="close" data-dismiss="alert"><span>&times;</span></button>
      </div>
    {{/if}}
    
    {{#if error}}
      <div class="alert alert-danger alert-dismissible fade show">
        {{error}}
        <button type="button" class="close" data-dismiss="alert"><span>&times;</span></button>
      </div>
    {{/if}}
    
    {{{body}}}
  </div>

  <footer class="bg-light mt-5 py-3">
    <div class="container text-center">
      <p>© 2025 Social Login Demo</p>
    </div>
  </footer>

  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/4.6.0/js/bootstrap.bundle.min.js"></script>
  <script src="/js/main.js"></script>
</body>
</html>

Login page with social login buttons:

// views/login.handlebars
<div class="row mt-5">
  <div class="col-md-6 m-auto">
    <div class="card card-body">
      <h1 class="text-center mb-3">Login</h1>
      
      <form action="/auth/login" method="POST">
        <div class="form-group">
          <label for="email">Email</label>
          <input type="email" id="email" name="email" class="form-control" placeholder="Enter Email" required>
        </div>
        <div class="form-group">
          <label for="password">Password</label>
          <input type="password" id="password" name="password" class="form-control" placeholder="Enter Password" required>
        </div>
        <button type="submit" class="btn btn-primary btn-block">Login</button>
      </form>
      
      <hr>
      
      <p class="text-center">Or login with:</p>
      
      <div class="social-auth-links text-center mb-3">
        <a href="/auth/google" class="btn btn-block btn-danger">
          <i class="fab fa-google mr-2"></i> Google
        </a>
        <a href="/auth/facebook" class="btn btn-block btn-primary">
          <i class="fab fa-facebook-f mr-2"></i> Facebook
        </a>
        <a href="/auth/github" class="btn btn-block btn-dark">
          <i class="fab fa-github mr-2"></i> GitHub
        </a>
        <a href="/auth/twitter" class="btn btn-block btn-info">
          <i class="fab fa-twitter mr-2"></i> Twitter
        </a>
      </div>
      
      <p class="lead mt-4">No Account? <a href="/register">Register</a></p>
    </div>
  </div>
</div>

User profile page with connected accounts:

// views/profile.handlebars
<div class="row">
  <div class="col-md-4">
    <div class="card">
      <div class="card-body text-center">
        {{#if user.avatar}}
          <img src="{{user.avatar}}" alt="{{user.name}}" class="rounded-circle img-fluid mb-3" style="max-width: 150px;">
        {{else}}
          <img src="/images/default-avatar.png" alt="{{user.name}}" class="rounded-circle img-fluid mb-3" style="max-width: 150px;">
        {{/if}}
        <h3>{{user.name}}</h3>
        <p>{{user.email}}</p>
        
        <hr>
        
        <h5>Connected Accounts</h5>
        <ul class="list-group list-group-flush">
          <li class="list-group-item d-flex justify-content-between align-items-center">
            <span><i class="fas fa-envelope mr-2"></i> Email/Password</span>
            {{#if (includes providers 'local')}}
              <span class="badge badge-success">Connected</span>
              {{#if (includes providers 'google')}}
                <a href="/auth/unlink/local" class="btn btn-sm btn-outline-danger">Unlink</a>
              {{else if (includes providers 'facebook')}}
                <a href="/auth/unlink/local" class="btn btn-sm btn-outline-danger">Unlink</a>
              {{else if (includes providers 'github')}}
                <a href="/auth/unlink/local" class="btn btn-sm btn-outline-danger">Unlink</a>
              {{else if (includes providers 'twitter')}}
                <a href="/auth/unlink/local" class="btn btn-sm btn-outline-danger">Unlink</a>
              {{/if}}
            {{else}}
              <span class="badge badge-secondary">Not Connected</span>
              <a href="#" data-toggle="modal" data-target="#setPasswordModal" class="btn btn-sm btn-outline-primary">Connect</a>
            {{/if}}
          </li>
          
          <li class="list-group-item d-flex justify-content-between align-items-center">
            <span><i class="fab fa-google mr-2"></i> Google</span>
            {{#if (includes providers 'google')}}
              <span class="badge badge-success">Connected</span>
              {{#if (includes providers 'local')}}
                <a href="/auth/unlink/google" class="btn btn-sm btn-outline-danger">Unlink</a>
              {{else if (includes providers 'facebook')}}
                <a href="/auth/unlink/google" class="btn btn-sm btn-outline-danger">Unlink</a>
              {{else if (includes providers 'github')}}
                <a href="/auth/unlink/google" class="btn btn-sm btn-outline-danger">Unlink</a>
              {{else if (includes providers 'twitter')}}
                <a href="/auth/unlink/google" class="btn btn-sm btn-outline-danger">Unlink</a>
              {{/if}}
            {{else}}
              <span class="badge badge-secondary">Not Connected</span>
              <a href="/auth/connect/google" class="btn btn-sm btn-outline-primary">Connect</a>
            {{/if}}
          </li>
          
          <li class="list-group-item d-flex justify-content-between align-items-center">
            <span><i class="fab fa-facebook-f mr-2"></i> Facebook</span>
            {{#if (includes providers 'facebook')}}
              <span class="badge badge-success">Connected</span>
              {{#if (includes providers 'local')}}
                <a href="/auth/unlink/facebook" class="btn btn-sm btn-outline-danger">Unlink</a>
              {{else if (includes providers 'google')}}
                <a href="/auth/unlink/facebook" class="btn btn-sm btn-outline-danger">Unlink</a>
              {{else if (includes providers 'github')}}
                <a href="/auth/unlink/facebook" class="btn btn-sm btn-outline-danger">Unlink</a>
              {{else if (includes providers 'twitter')}}
                <a href="/auth/unlink/facebook" class="btn btn-sm btn-outline-danger">Unlink</a>
              {{/if}}
            {{else}}
              <span class="badge badge-secondary">Not Connected</span>
              <a href="/auth/connect/facebook" class="btn btn-sm btn-outline-primary">Connect</a>
            {{/if}}
          </li>
          
          <li class="list-group-item d-flex justify-content-between align-items-center">
            <span><i class="fab fa-github mr-2"></i> GitHub</span>
            {{#if (includes providers 'github')}}
              <span class="badge badge-success">Connected</span>
              {{#if (includes providers 'local')}}
                <a href="/auth/unlink/github" class="btn btn-sm btn-outline-danger">Unlink</a>
              {{else if (includes providers 'google')}}
                <a href="/auth/unlink/github" class="btn btn-sm btn-outline-danger">Unlink</a>
              {{else if (includes providers 'facebook')}}
                <a href="/auth/unlink/github" class="btn btn-sm btn-outline-danger">Unlink</a>
              {{else if (includes providers 'twitter')}}
                <a href="/auth/unlink/github" class="btn btn-sm btn-outline-danger">Unlink</a>
              {{/if}}
            {{else}}
              <span class="badge badge-secondary">Not Connected</span>
              <a href="/auth/connect/github" class="btn btn-sm btn-outline-primary">Connect</a>
            {{/if}}
          </li>
          
          <li class="list-group-item d-flex justify-content-between align-items-center">
            <span><i class="fab fa-twitter mr-2"></i> Twitter</span>
            {{#if (includes providers 'twitter')}}
              <span class="badge badge-success">Connected</span>
              {{#if (includes providers 'local')}}
                <a href="/auth/unlink/twitter" class="btn btn-sm btn-outline-danger">Unlink</a>
              {{else if (includes providers 'google')}}
                <a href="/auth/unlink/twitter" class="btn btn-sm btn-outline-danger">Unlink</a>
              {{else if (includes providers 'facebook')}}
                <a href="/auth/unlink/twitter" class="btn btn-sm btn-outline-danger">Unlink</a>
              {{else if (includes providers 'github')}}
                <a href="/auth/unlink/twitter" class="btn btn-sm btn-outline-danger">Unlink</a>
              {{/if}}
            {{else}}
              <span class="badge badge-secondary">Not Connected</span>
              <a href="/auth/connect/twitter" class="btn btn-sm btn-outline-primary">Connect</a>
            {{/if}}
          </li>
        </ul>
      </div>
    </div>
  </div>
  
  <div class="col-md-8">
    <div class="card">
      <div class="card-header">
        <h4>Profile Information</h4>
      </div>
      <div class="card-body">
        <form action="/user/update-profile" method="POST">
          <div class="form-group">
            <label for="name">Name</label>
            <input type="text" id="name" name="name" class="form-control" value="{{user.name}}" required>
          </div>
          <div class="form-group">
            <label for="email">Email</label>
            <input type="email" id="email" name="email" class="form-control" value="{{user.email}}" readonly>
            <small class="form-text text-muted">Email cannot be changed.</small>
          </div>
          
          <button type="submit" class="btn btn-primary">Update Profile</button>
        </form>
      </div>
    </div>
    
    {{#unless (includes providers 'local')}}
      <div class="card mt-4">
        <div class="card-header">
          <h4>Add Password</h4>
        </div>
        <div class="card-body">
          <p>Setting a password allows you to log in using your email address.</p>
          <button class="btn btn-primary" data-toggle="modal" data-target="#setPasswordModal">
            Set Password
          </button>
        </div>
      </div>
    {{/unless}}
    
    {{#if (includes providers 'local')}}
      <div class="card mt-4">
        <div class="card-header">
          <h4>Change Password</h4>
        </div>
        <div class="card-body">
          <form action="/user/change-password" method="POST">
            <div class="form-group">
              <label for="currentPassword">Current Password</label>
              <input type="password" id="currentPassword" name="currentPassword" class="form-control" required>
            </div>
            <div class="form-group">
              <label for="newPassword">New Password</label>
              <input type="password" id="newPassword" name="newPassword" class="form-control" required>
            </div>
            <div class="form-group">
              <label for="confirmPassword">Confirm New Password</label>
              <input type="password" id="confirmPassword" name="confirmPassword" class="form-control" required>
            </div>
            
            <button type="submit" class="btn btn-primary">Change Password</button>
          </form>
        </div>
      </div>
    {{/if}}
  </div>
</div>

<!-- Set Password Modal -->
<div class="modal fade" id="setPasswordModal" tabindex="-1" role="dialog" aria-hidden="true">
  <div class="modal-dialog" role="document">
    <div class="modal-content">
      <div class="modal-header">
        <h5 class="modal-title">Set Password</h5>
        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
          <span aria-hidden="true">&times;</span>
        </button>
      </div>
      <form action="/user/set-password" method="POST">
        <div class="modal-body">
          <div class="form-group">
            <label for="newPassword">New Password</label>
            <input type="password" id="newPassword" name="newPassword" class="form-control" required>
          </div>
          <div class="form-group">
            <label for="confirmPassword">Confirm Password</label>
            <input type="password" id="confirmPassword" name="confirmPassword" class="form-control" required>
          </div>
        </div>
        <div class="modal-footer">
          <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
          <button type="submit" class="btn btn-primary">Save Password</button>
        </div>
      </form>
    </div>
  </div>
</div>

UI Design Considerations

These templates implement several important UI patterns:

  • Clear Authentication Options: Multiple login methods clearly presented
  • Seamless Account Linking: Easy connection of multiple providers
  • Safe Unlinking: Prevents removing the only authentication method
  • Password Management: Adding/changing password for social-only accounts
  • Feedback Messages: Clear status and error messages for user actions
  • Responsive Design: Works well on all device sizes

This user-friendly approach makes authentication easy while maintaining security.

User Routes

Let's implement the user routes for profile management:

// routes/userRoutes.js
const express = require('express');
const router = express.Router();
const User = require('../models/User');
const bcrypt = require('bcryptjs');

// Update profile
router.post('/update-profile', async (req, res, next) => {
  try {
    const { name } = req.body;
    
    // Simple validation
    if (!name) {
      req.flash('error_msg', 'Name is required');
      return res.redirect('/profile');
    }
    
    // Find and update user
    const user = await User.findById(req.user.id);
    user.name = name;
    user.updatedAt = Date.now();
    
    await user.save();
    
    req.flash('success_msg', 'Profile updated successfully');
    res.redirect('/profile');
  } catch (err) {
    next(err);
  }
});

// Set password for social-only accounts
router.post('/set-password', async (req, res, next) => {
  try {
    const { newPassword, confirmPassword } = req.body;
    
    // Validation
    if (newPassword !== confirmPassword) {
      req.flash('error_msg', 'Passwords do not match');
      return res.redirect('/profile');
    }
    
    if (newPassword.length < 6) {
      req.flash('error_msg', 'Password must be at least 6 characters');
      return res.redirect('/profile');
    }
    
    // Find and update user
    const user = await User.findById(req.user.id);
    user.password = newPassword; // Will be hashed by pre-save hook
    user.updatedAt = Date.now();
    
    await user.save();
    
    req.flash('success_msg', 'Password set successfully');
    res.redirect('/profile');
  } catch (err) {
    next(err);
  }
});

// Change existing password
router.post('/change-password', async (req, res, next) => {
  try {
    const { currentPassword, newPassword, confirmPassword } = req.body;
    
    // Validation
    if (newPassword !== confirmPassword) {
      req.flash('error_msg', 'New passwords do not match');
      return res.redirect('/profile');
    }
    
    if (newPassword.length < 6) {
      req.flash('error_msg', 'Password must be at least 6 characters');
      return res.redirect('/profile');
    }
    
    // Find user
    const user = await User.findById(req.user.id);
    
    // Verify current password
    const isMatch = await user.comparePassword(currentPassword);
    
    if (!isMatch) {
      req.flash('error_msg', 'Current password is incorrect');
      return res.redirect('/profile');
    }
    
    // Update password
    user.password = newPassword; // Will be hashed by pre-save hook
    user.updatedAt = Date.now();
    
    await user.save();
    
    req.flash('success_msg', 'Password changed successfully');
    res.redirect('/profile');
  } catch (err) {
    next(err);
  }
});

module.exports = router;

User Management Features

These routes implement key user management functionality:

  • Profile Updates: Allows users to update basic profile information
  • Password Setting: Enables social-only users to add password authentication
  • Password Changes: Secure password changing with verification
  • Input Validation: Validates all user inputs before processing
  • Security Checks: Verifies current password before changes

These features enhance security while providing users with control over their authentication methods.

Testing and Debugging

Testing a social login implementation can be challenging. Here are some strategies:

Testing Strategy

Common OAuth Debugging Issues

Issue Possible Causes Solutions
Redirect URI Mismatch URI in code doesn't match registered URI Ensure exact match, including protocol, path, and case sensitivity
Missing Scopes Insufficient scopes requested Add required scopes for needed user data (email, profile, etc.)
Unauthorized Redirect URI Callback URL not allowed by provider Add the callback URL to the provider's allowed redirect URIs list
Session Issues Session not properly configured Check cookie settings, session store connection
Missing Profile Data Provider not returning expected fields Check scopes, provider documentation for required permissions
CORS Issues Cross-origin restrictions Ensure proper CORS configuration, especially for SPA implementations

Testing with Mock OAuth

// Mock OAuth provider for testing
        // test/mockOAuth.js
        const express = require('express');
        const crypto = require('crypto');
        
        // Create a mock OAuth server
        const mockOAuthServer = express();
        
        // Store mock users and authorization codes
        const mockUsers = {
          'mock-google-user': {
            id: '123456789',
            displayName: 'Mock Google User',
            emails: [{ value: 'mockgoogle@example.com' }],
            photos: [{ value: 'https://example.com/mockgoogle.jpg' }],
            provider: 'google'
          },
          'mock-facebook-user': {
            id: '987654321',
            displayName: 'Mock Facebook User',
            emails: [{ value: 'mockfacebook@example.com' }],
            photos: [{ value: 'https://example.com/mockfacebook.jpg' }],
            provider: 'facebook'
          }
        };
        
        const authCodes = {};
        const accessTokens = {};
        
        // Authorization endpoint
        mockOAuthServer.get('/auth', (req, res) => {
          const { client_id, redirect_uri, state, response_type, scope } = req.query;
          
          // Validate parameters
          if (!client_id || !redirect_uri || !state || response_type !== 'code') {
            return res.status(400).send('Invalid request parameters');
          }
          
          // Create authorization code
          const code = crypto.randomBytes(16).toString('hex');
          
          // Store code with associated user (mock selection)
          const userId = req.query.mock_user || 'mock-google-user';
          authCodes[code] = {
            client_id,
            redirect_uri,
            userId
          };
          
          // Redirect back with code
          const redirectUrl = new URL(redirect_uri);
          redirectUrl.searchParams.append('code', code);
          redirectUrl.searchParams.append('state', state);
          
          res.redirect(redirectUrl.toString());
        });
        
        // Token endpoint
        mockOAuthServer.post('/token', express.urlencoded({ extended: true }), (req, res) => {
          const { code, client_id, client_secret, redirect_uri, grant_type } = req.body;
          
          // Validate parameters
          if (!code || !client_id || !redirect_uri || grant_type !== 'authorization_code') {
            return res.status(400).json({ error: 'invalid_request' });
          }
          
          // Check if code exists and matches
          const codeData = authCodes[code];
          if (!codeData || codeData.client_id !== client_id || codeData.redirect_uri !== redirect_uri) {
            return res.status(400).json({ error: 'invalid_grant' });
          }
          
          // Generate tokens
          const accessToken = crypto.randomBytes(32).toString('hex');
          const refreshToken = crypto.randomBytes(32).toString('hex');
          
          // Store tokens
          accessTokens[accessToken] = {
            userId: codeData.userId,
            client_id
          };
          
          // Delete used code
          delete authCodes[code];
          
          // Return tokens
          res.json({
            access_token: accessToken,
            token_type: 'Bearer',
            expires_in: 3600,
            refresh_token: refreshToken
          });
        });
        
        // User info endpoint
        mockOAuthServer.get('/userinfo', (req, res) => {
          const authHeader = req.headers.authorization;
          
          if (!authHeader || !authHeader.startsWith('Bearer ')) {
            return res.status(401).json({ error: 'invalid_token' });
          }
          
          const token = authHeader.substring(7);
          const tokenData = accessTokens[token];
          
          if (!tokenData) {
            return res.status(401).json({ error: 'invalid_token' });
          }
          
          const user = mockUsers[tokenData.userId];
          
          if (!user) {
            return res.status(404).json({ error: 'user_not_found' });
          }
          
          res.json({
            id: user.id,
            name: user.displayName,
            email: user.emails[0].value,
            picture: user.photos[0].value
          });
        });
        
        // Start server
        const PORT = 3001;
        mockOAuthServer.listen(PORT, () => {
          console.log(`Mock OAuth server running on port ${PORT}`);
        });
        
        // Export server for testing
        module.exports = mockOAuthServer;

Automated Testing

// test/auth.test.js
        const request = require('supertest');
        const mongoose = require('mongoose');
        const mockOAuthServer = require('./mockOAuth');
        const app = require('../app');
        const User = require('../models/User');
        
        // Original passport config
        const originalPassport = require('../config/passport');
        
        // Mock passport strategies for testing
        jest.mock('passport', () => {
          const originalPassport = jest.requireActual('passport');
          
          // Create a mocked version that overrides the authenticate method
          const passport = {
            ...originalPassport,
            authenticate: jest.fn().mockImplementation((strategy, options, callback) => {
              // Return middleware that simulates authentication
              return (req, res, next) => {
                // Simulate successful authentication
                if (strategy === 'google' && req.path === '/auth/google/callback') {
                  req.user = {
                    id: '123456789',
                    name: 'Mock Google User',
                    email: 'mockgoogle@example.com',
                    googleId: '123456789'
                  };
                  
                  if (callback) {
                    return callback(null, req.user, null)(req, res, next);
                  }
                  
                  req.login(req.user, (err) => {
                    if (err) return next(err);
                    return next();
                  });
                } else {
                  next();
                }
              };
            }),
            initialize: originalPassport.initialize,
            session: originalPassport.session,
            serializeUser: originalPassport.serializeUser,
            deserializeUser: originalPassport.deserializeUser
          };
          
          return passport;
        });
        
        // Setup/teardown
        beforeAll(async () => {
          await mongoose.connect(process.env.TEST_MONGODB_URI, {
            useNewUrlParser: true,
            useUnifiedTopology: true
          });
        });
        
        afterAll(async () => {
          await mongoose.connection.dropDatabase();
          await mongoose.connection.close();
        });
        
        beforeEach(async () => {
          await User.deleteMany({});
        });
        
        // Tests
        describe('Authentication Routes', () => {
          // Test for homepage access
          test('should access home page', async () => {
            const res = await request(app).get('/');
            expect(res.statusCode).toBe(200);
            expect(res.text).toContain('Social Login Demo');
          });
          
          // Test for login page
          test('should access login page', async () => {
            const res = await request(app).get('/login');
            expect(res.statusCode).toBe(200);
            expect(res.text).toContain('Login with');
          });
          
          // Test Google OAuth route
          test('should redirect to Google OAuth', async () => {
            const res = await request(app).get('/auth/google');
            expect(res.statusCode).toBe(302); // Redirect status
          });
          
          // Test Google OAuth callback
          test('should handle Google OAuth callback', async () => {
            const res = await request(app)
              .get('/auth/google/callback?code=mockcode&state=mockstate');
            
            expect(res.statusCode).toBe(302); // Redirect status
            expect(res.headers.location).toBe('/profile');
          });
          
          // Test protected route access
          test('should deny access to protected route when not authenticated', async () => {
            const res = await request(app).get('/profile');
            expect(res.statusCode).toBe(302); // Redirect status
            expect(res.headers.location).toBe('/login');
          });
          
          // Test local registration
          test('should register a new user', async () => {
            const res = await request(app)
              .post('/auth/register')
              .send({
                name: 'Test User',
                email: 'test@example.com',
                password: 'password123',
                password2: 'password123'
              });
            
            expect(res.statusCode).toBe(302); // Redirect status
            expect(res.headers.location).toBe('/login');
            
            // Verify user was created in database
            const user = await User.findOne({ email: 'test@example.com' });
            expect(user).toBeTruthy();
            expect(user.name).toBe('Test User');
          });
        });

Browser Testing Tips

For manual testing in the browser:

  • Use Browser DevTools: Inspect network requests, cookies, and localStorage
  • Try Incognito Mode: Test login flows without existing cookies or sessions
  • Test Different OAuth Scopes: See how different permissions affect available user data
  • Test Account Linking: Connect and disconnect various providers
  • Check Error Handling: Purposely trigger errors to test recovery mechanisms
  • Mobile Testing: Ensure the authentication flow works well on mobile devices

Advanced Social Login Features

Beyond the basic implementation, you can add several advanced features to enhance your social login system:

Role-Based Access Control

// Middleware for role-based access control
        // middleware/rbac.js
        const roles = {
          user: ['read'],
          editor: ['read', 'create', 'update'],
          admin: ['read', 'create', 'update', 'delete', 'manage']
        };
        
        // Check if user has required role
        function hasRole(role) {
          return (req, res, next) => {
            // User must be authenticated
            if (!req.isAuthenticated()) {
              req.flash('error_msg', 'Please log in to access this resource');
              return res.redirect('/login');
            }
            
            // Check user role
            if (req.user.role !== role) {
              req.flash('error_msg', 'You do not have permission to access this resource');
              return res.redirect('/');
            }
            
            next();
          };
        }
        
        // Check if user has required permission
        function hasPermission(permission) {
          return (req, res, next) => {
            // User must be authenticated
            if (!req.isAuthenticated()) {
              req.flash('error_msg', 'Please log in to access this resource');
              return res.redirect('/login');
            }
            
            // Get permissions for user's role
            const userRole = req.user.role || 'user';
            const permissions = roles[userRole] || [];
            
            // Check if user has required permission
            if (!permissions.includes(permission)) {
              req.flash('error_msg', 'You do not have permission to access this resource');
              return res.redirect('/');
            }
            
            next();
          };
        }
        
        module.exports = {
          hasRole,
          hasPermission
        };
        
        // Usage example
        // routes/adminRoutes.js
        const express = require('express');
        const router = express.Router();
        const { hasRole, hasPermission } = require('../middleware/rbac');
        
        // Admin dashboard - requires admin role
        router.get('/dashboard', hasRole('admin'), (req, res) => {
          res.render('admin/dashboard');
        });
        
        // Manage users - requires manage permission
        router.get('/users', hasPermission('manage'), (req, res) => {
          // Fetch and display users
        });
        
        module.exports = router;

Two-Factor Authentication

// Install required packages
        // npm install speakeasy qrcode
        
        // Add 2FA to User model
        // models/User.js (additional fields)
        const userSchema = new mongoose.Schema({
          // ... existing fields
          
          // 2FA fields
          twoFactorEnabled: {
            type: Boolean,
            default: false
          },
          twoFactorSecret: {
            type: String
          }
        });
        
        // 2FA setup route
        // routes/userRoutes.js (additional routes)
        const speakeasy = require('speakeasy');
        const QRCode = require('qrcode');
        
        // Generate 2FA setup
        router.get('/setup-2fa', async (req, res, next) => {
          try {
            // Generate a secret
            const secret = speakeasy.generateSecret({
              name: `SocialLoginApp:${req.user.email}`
            });
            
            // Generate QR code
            const qrCodeUrl = await QRCode.toDataURL(secret.otpauth_url);
            
            // Store secret temporarily in session
            req.session.twoFactorSecret = secret.base32;
            
            // Render setup page with QR code
            res.render('setup-2fa', {
              qrCodeUrl,
              secret: secret.base32
            });
          } catch (err) {
            next(err);
          }
        });
        
        // Verify and enable 2FA
        router.post('/verify-2fa', async (req, res, next) => {
          try {
            const { token } = req.body;
            const secret = req.session.twoFactorSecret;
            
            if (!secret) {
              req.flash('error_msg', '2FA setup expired. Please try again.');
              return res.redirect('/profile');
            }
            
            // Verify token
            const verified = speakeasy.totp.verify({
              secret,
              encoding: 'base32',
              token
            });
            
            if (!verified) {
              req.flash('error_msg', 'Invalid verification code');
              return res.redirect('/user/setup-2fa');
            }
            
            // Enable 2FA for user
            const user = await User.findById(req.user.id);
            user.twoFactorEnabled = true;
            user.twoFactorSecret = secret;
            await user.save();
            
            // Clear secret from session
            delete req.session.twoFactorSecret;
            
            req.flash('success_msg', '2FA enabled successfully');
            res.redirect('/profile');
          } catch (err) {
            next(err);
          }
        });
        
        // Modify login process for 2FA
        // routes/authRoutes.js (modified login)
        router.post('/login', (req, res, next) => {
          passport.authenticate('local', async (err, user, info) => {
            if (err) return next(err);
            
            if (!user) {
              req.flash('error_msg', info.message);
              return res.redirect('/login');
            }
            
            // Check if user has 2FA enabled
            if (user.twoFactorEnabled) {
              // Store user ID in session for 2FA verification
              req.session.twoFactorUserId = user.id;
              return res.redirect('/auth/verify-2fa');
            }
            
            // Standard login (no 2FA)
            req.login(user, (err) => {
              if (err) return next(err);
              return handleAuthRedirect(req, res);
            });
          })(req, res, next);
        });
        
        // 2FA verification page
        router.get('/verify-2fa', (req, res) => {
          if (!req.session.twoFactorUserId) {
            return res.redirect('/login');
          }
          
          res.render('verify-2fa');
        });
        
        // Verify 2FA code
        router.post('/verify-2fa', async (req, res, next) => {
          try {
            const { token } = req.body;
            const userId = req.session.twoFactorUserId;
            
            if (!userId) {
              return res.redirect('/login');
            }
            
            // Get user
            const user = await User.findById(userId);
            
            if (!user) {
              delete req.session.twoFactorUserId;
              req.flash('error_msg', 'User not found');
              return res.redirect('/login');
            }
            
            // Verify token
            const verified = speakeasy.totp.verify({
              secret: user.twoFactorSecret,
              encoding: 'base32',
              token
            });
            
            if (!verified) {
              req.flash('error_msg', 'Invalid verification code');
              return res.redirect('/auth/verify-2fa');
            }
            
            // Clear 2FA session and log in
            delete req.session.twoFactorUserId;
            
            req.login(user, (err) => {
              if (err) return next(err);
              return handleAuthRedirect(req, res);
            });
          } catch (err) {
            next(err);
          }
        });

Social API Integration

// Fetching additional data from social providers
        // services/socialApiService.js
        const axios = require('axios');
        
        // Google API service
        const googleApi = {
          async getUserProfile(accessToken) {
            try {
              const response = await axios.get('https://www.googleapis.com/oauth2/v3/userinfo', {
                headers: {
                  Authorization: `Bearer ${accessToken}`
                }
              });
              
              return response.data;
            } catch (err) {
              console.error('Error fetching Google profile:', err.message);
              throw err;
            }
          },
          
          async getUserContacts(accessToken) {
            try {
              const response = await axios.get('https://people.googleapis.com/v1/people/me/connections', {
                params: {
                  personFields: 'names,emailAddresses,phoneNumbers',
                  pageSize: 100
                },
                headers: {
                  Authorization: `Bearer ${accessToken}`
                }
              });
              
              return response.data.connections || [];
            } catch (err) {
              console.error('Error fetching Google contacts:', err.message);
              throw err;
            }
          }
        };
        
        // Facebook API service
        const facebookApi = {
          async getUserProfile(accessToken) {
            try {
              const response = await axios.get('https://graph.facebook.com/v12.0/me', {
                params: {
                  fields: 'id,name,email,picture',
                  access_token: accessToken
                }
              });
              
              return response.data;
            } catch (err) {
              console.error('Error fetching Facebook profile:', err.message);
              throw err;
            }
          },
          
          async getUserFriends(accessToken) {
            try {
              const response = await axios.get('https://graph.facebook.com/v12.0/me/friends', {
                params: {
                  access_token: accessToken
                }
              });
              
              return response.data.data || [];
            } catch (err) {
              console.error('Error fetching Facebook friends:', err.message);
              throw err;
            }
          }
        };
        
        // Route to fetch contacts from social provider
        // routes/userRoutes.js (additional routes)
        router.get('/social-contacts', async (req, res, next) => {
          try {
            if (!req.isAuthenticated()) {
              return res.status(401).json({ message: 'Unauthorized' });
            }
            
            const userId = req.user.id;
            const provider = req.query.provider;
            
            // Check if provider is connected
            if (!req.user.hasProvider(provider)) {
              return res.status(400).json({ message: `User not connected to ${provider}` });
            }
            
            // Get provider data from user
            const providerData = req.user.providerData.get(provider);
            
            if (!providerData || !providerData.accessToken) {
              return res.status(400).json({ message: 'No access token found' });
            }
            
            // Fetch contacts based on provider
            let contacts = [];
            
            switch (provider) {
              case 'google':
                contacts = await googleApi.getUserContacts(providerData.accessToken);
                break;
                
              case 'facebook':
                contacts = await facebookApi.getUserFriends(providerData.accessToken);
                break;
                
              default:
                return res.status(400).json({ message: 'Unsupported provider' });
            }
            
            res.json({ contacts });
          } catch (err) {
            // Check for token expiration
            if (err.response && err.response.status === 401) {
              return res.status(401).json({ message: 'Token expired. Please reconnect your account.' });
            }
            
            next(err);
          }
        });

Remember Me Functionality

// Add Remember Me functionality
        // Install required package
        // npm install crypto-random-string
        
        // Add RememberToken model
        // models/RememberToken.js
        const mongoose = require('mongoose');
        const crypto = require('crypto-random-string');
        
        const RememberTokenSchema = new mongoose.Schema({
          userId: {
            type: mongoose.Schema.Types.ObjectId,
            ref: 'User',
            required: true
          },
          token: {
            type: String,
            required: true,
            unique: true
          },
          userAgent: {
            type: String
          },
          ipAddress: {
            type: String
          },
          expires: {
            type: Date,
            required: true
          },
          createdAt: {
            type: Date,
            default: Date.now
          }
        });
        
        // Create a remember token
        RememberTokenSchema.statics.createToken = async function(userId, userAgent, ipAddress) {
          // Generate a unique token
          const token = crypto({ length: 64 });
          
          // Set expiration (30 days)
          const expires = new Date();
          expires.setDate(expires.getDate() + 30);
          
          // Create and return token
          const rememberToken = await this.create({
            userId,
            token,
            userAgent,
            ipAddress,
            expires
          });
          
          return rememberToken;
        };
        
        // Find token and check if valid
        RememberTokenSchema.statics.findValidToken = async function(token) {
          return await this.findOne({
            token,
            expires: { $gt: new Date() }
          });
        };
        
        const RememberToken = mongoose.model('RememberToken', RememberTokenSchema);
        
        module.exports = RememberToken;
        
        // Modify login route for Remember Me
        // routes/authRoutes.js (modified login)
        const RememberToken = require('../models/RememberToken');
        
        router.post('/login', (req, res, next) => {
          passport.authenticate('local', async (err, user, info) => {
            if (err) return next(err);
            
            if (!user) {
              req.flash('error_msg', info.message);
              return res.redirect('/login');
            }
            
            // Handle Remember Me
            const rememberMe = req.body.rememberMe === 'on';
            
            // Log in the user
            req.login(user, async (err) => {
              if (err) return next(err);
              
              // Create remember token if requested
              if (rememberMe) {
                try {
                  const rememberToken = await RememberToken.createToken(
                    user.id,
                    req.get('User-Agent'),
                    req.ip
                  );
                  
                  // Set remember me cookie (30 days)
                  res.cookie('remember_token', rememberToken.token, {
                    httpOnly: true,
                    path: '/',
                    maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
                    secure: process.env.NODE_ENV === 'production',
                    sameSite: 'lax'
                  });
                } catch (err) {
                  console.error('Error creating remember token:', err);
                }
              }
              
              return handleAuthRedirect(req, res);
            });
          })(req, res, next);
        });
        
        // Add middleware to check for remember token
        // app.js (after session middleware)
        const RememberToken = require('./models/RememberToken');
        
        // Remember Me middleware
        app.use(async (req, res, next) => {
          // Skip if user is already authenticated
          if (req.isAuthenticated()) {
            return next();
          }
          
          // Check for remember token cookie
          const token = req.cookies.remember_token;
          
          if (!token) {
            return next();
          }
          
          try {
            // Find valid token
            const rememberToken = await RememberToken.findValidToken(token);
            
            if (!rememberToken) {
              // Invalid or expired token, clear cookie
              res.clearCookie('remember_token');
              return next();
            }
            
            // Find user
            const user = await User.findById(rememberToken.userId);
            
            if (!user) {
              // User not found, clear cookie and token
              res.clearCookie('remember_token');
              await RememberToken.findByIdAndDelete(rememberToken._id);
              return next();
            }
            
            // Log in the user
            req.login(user, (err) => {
              if (err) {
                console.error('Error logging in with remember token:', err);
                return next();
              }
              
              // Extend token expiration
              rememberToken.expires = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
              rememberToken.save().catch(err => {
                console.error('Error extending remember token:', err);
              });
              
              next();
            });
          } catch (err) {
            console.error('Remember Me error:', err);
            next();
          }
        });

Security Considerations for Advanced Features

When implementing these advanced features, keep these security considerations in mind:

  • Token Expiration: Always set appropriate expiration times for tokens and sessions
  • Access Token Storage: Store provider access tokens securely, ideally encrypted
  • 2FA Backup Codes: Provide backup codes for users who lose their 2FA device
  • Remember Me Security: Use secure, random tokens and store them safely
  • Rate Limiting: Implement rate limiting for authentication attempts
  • Session Regeneration: Regenerate sessions on privilege changes

These measures help protect user accounts against various attack vectors.

Single Sign-On (SSO) Implementation

For enterprise applications, you might want to implement single sign-on with identity providers like Okta, Auth0, or Azure AD. Here's a simplified example using Passport.js with SAML:

// Install required packages
        // npm install passport-saml
        
        // SAML strategy configuration
        // config/passport.js (additional strategy)
        const SamlStrategy = require('passport-saml').Strategy;
        const fs = require('fs');
        const path = require('path');
        
        // SAML certificate files
        const privateCert = fs.readFileSync(path.join(__dirname, '../certs/private.key'), 'utf8');
        const publicCert = fs.readFileSync(path.join(__dirname, '../certs/public.cert'), 'utf8');
        
        // Configure SAML strategy
        passport.use(new SamlStrategy({
          path: '/auth/saml/callback',
          entryPoint: process.env.SAML_ENTRY_POINT,
          issuer: process.env.SAML_ISSUER,
          cert: process.env.SAML_CERT,
          privateCert: privateCert,
          decryptionPvk: privateCert,
          identifierFormat: null
        }, async (profile, done) => {
          try {
            // Extract profile information
            const email = profile.email || profile.nameID;
            const name = profile.displayName || `${profile.firstName} ${profile.lastName}`.trim();
            
            // Find user by email
            let user = await User.findOne({ email });
            
            if (user) {
              // Update user info
              user.samlId = profile.nameID;
              user.updatedAt = Date.now();
              await user.save();
            } else {
              // Create new user
              user = await User.create({
                samlId: profile.nameID,
                name,
                email,
                emailVerified: true
              });
            }
            
            return done(null, user);
          } catch (err) {
            return done(err);
          }
        }));
        
        // SAML routes
        // routes/authRoutes.js (additional routes)
        router.get('/saml', passport.authenticate('saml'));
        
        router.post('/saml/callback',
          passport.authenticate('saml', { failureRedirect: '/login' }),
          handleAuthRedirect
        );
        
        // SAML metadata endpoint
        router.get('/saml/metadata', (req, res) => {
          const samlStrategy = passport._strategies.saml;
          const metadata = samlStrategy.generateServiceProviderMetadata(publicCert);
          
          res.header('Content-Type', 'text/xml').send(metadata);
        });

Enterprise SSO Providers

For larger organizations, specialized identity providers offer comprehensive SSO solutions:

  • Okta: Supports SAML, OIDC, and WS-Federation with extensive enterprise features
  • Auth0: Provides a flexible identity platform with support for multiple protocols
  • Azure AD: Microsoft's identity solution with seamless integration with Office 365
  • OneLogin: Enterprise identity solution with strong compliance features
  • Keycloak: Open-source identity solution with comprehensive SSO capabilities

These platforms can be integrated using specialized Passport strategies or their own SDKs.

Performance and Scalability Considerations

As your authentication system grows, you'll need to consider performance and scalability:

Caching Strategies

// Install Redis for caching
        // npm install redis
        
        // Configure Redis cache
        // services/cacheService.js
        const redis = require('redis');
        const { promisify } = require('util');
        
        // Create Redis client
        const client = redis.createClient({
          host: process.env.REDIS_HOST || 'localhost',
          port: process.env.REDIS_PORT || 6379,
          password: process.env.REDIS_PASSWORD || '',
          db: process.env.REDIS_DB || 0
        });
        
        // Promisify Redis commands
        const getAsync = promisify(client.get).bind(client);
        const setAsync = promisify(client.set).bind(client);
        const delAsync = promisify(client.del).bind(client);
        const expireAsync = promisify(client.expire).bind(client);
        
        // Handle Redis errors
        client.on('error', (err) => {
          console.error('Redis error:', err);
        });
        
        // Cache utility functions
        const cache = {
          // Get data from cache
          async get(key) {
            try {
              const data = await getAsync(key);
              return data ? JSON.parse(data) : null;
            } catch (err) {
              console.error('Cache get error:', err);
              return null;
            }
          },
          
          // Set data in cache with optional TTL
          async set(key, data, ttl = 3600) {
            try {
              await setAsync(key, JSON.stringify(data));
              
              if (ttl) {
                await expireAsync(key, ttl);
              }
              
              return true;
            } catch (err) {
              console.error('Cache set error:', err);
              return false;
            }
          },
          
          // Delete data from cache
          async del(key) {
            try {
              await delAsync(key);
              return true;
            } catch (err) {
              console.error('Cache delete error:', err);
              return false;
            }
          }
        };
        
        module.exports = cache;
        
        // Use caching for user profile data
        // controllers/userController.js
        const cache = require('../services/cacheService');
        
        // Get user profile with caching
        async function getUserProfile(userId) {
          try {
            // Check cache first
            const cacheKey = `user_profile:${userId}`;
            const cachedProfile = await cache.get(cacheKey);
            
            if (cachedProfile) {
              return cachedProfile;
            }
            
            // Not in cache, fetch from database
            const user = await User.findById(userId);
            
            if (!user) {
              return null;
            }
            
            // Create profile object
            const profile = {
              id: user._id,
              name: user.name,
              email: user.email,
              avatar: user.avatar,
              providers: user.getProviders(),
              createdAt: user.createdAt
            };
            
            // Cache profile (1 hour TTL)
            await cache.set(cacheKey, profile, 3600);
            
            return profile;
          } catch (err) {
            console.error('Error getting user profile:', err);
            throw err;
          }
        }
        
        // Clear cache when user is updated
        async function updateUserProfile(userId, updateData) {
          try {
            // Update user in database
            const user = await User.findByIdAndUpdate(userId, {
              ...updateData,
              updatedAt: Date.now()
            }, { new: true });
            
            if (!user) {
              return null;
            }
            
            // Clear user cache
            const cacheKey = `user_profile:${userId}`;
            await cache.del(cacheKey);
            
            return user;
          } catch (err) {
            console.error('Error updating user profile:', err);
            throw err;
          }
        }

Database Optimization

// Database indexes for optimized queries
        // models/User.js (adding indexes)
        const UserSchema = new mongoose.Schema({
          // ... existing fields
        });
        
        // Create indexes for frequently queried fields
        UserSchema.index({ email: 1 });
        UserSchema.index({ googleId: 1 });
        UserSchema.index({ facebookId: 1 });
        UserSchema.index({ githubId: 1 });
        UserSchema.index({ twitterId: 1 });
        
        // Compound index for role-based queries
        UserSchema.index({ role: 1, createdAt: -1 });
        
        // Connection pooling configuration
        // config/database.js (optimized connection)
        const connectDB = async () => {
          try {
            const conn = await mongoose.connect(process.env.MONGODB_URI, {
              useNewUrlParser: true,
              useUnifiedTopology: true,
              useFindAndModify: false,
              useCreateIndex: true,
              // Connection pool settings
              poolSize: 10,
              socketTimeoutMS: 45000,
              keepAlive: true,
              keepAliveInitialDelay: 300000
            });
            
            console.log(`MongoDB Connected: ${conn.connection.host}`);
          } catch (err) {
            console.error(`Error connecting to MongoDB: ${err.message}`);
            process.exit(1);
          }
        };

Horizontal Scaling

// Load balancing and session configuration
        // app.js (modified for scaling)
        const express = require('express');
        const session = require('express-session');
        const RedisStore = require('connect-redis')(session);
        const redis = require('redis');
        const cluster = require('cluster');
        const numCPUs = require('os').cpus().length;
        require('dotenv').config();
        
        if (cluster.isMaster && process.env.NODE_ENV === 'production') {
          console.log(`Master ${process.pid} is running`);
          
          // Fork workers
          for (let i = 0; i < numCPUs; i++) {
            cluster.fork();
          }
          
          cluster.on('exit', (worker, code, signal) => {
            console.log(`Worker ${worker.process.pid} died`);
            // Replace the dead worker
            cluster.fork();
          });
        } else {
          // Worker process
          const app = express();
          
          // Create Redis client for session store
          const redisClient = redis.createClient({
            host: process.env.REDIS_HOST,
            port: process.env.REDIS_PORT,
            password: process.env.REDIS_PASSWORD
          });
          
          // Configure session with Redis store for scaling
          app.use(session({
            store: new RedisStore({ client: redisClient }),
            secret: process.env.SESSION_SECRET,
            resave: false,
            saveUninitialized: false,
            cookie: {
              maxAge: 24 * 60 * 60 * 1000,
              httpOnly: true,
              secure: process.env.NODE_ENV === 'production',
              sameSite: 'lax'
            }
          }));
          
          // ... rest of app configuration
          
          // Start server
          const PORT = process.env.PORT || 3000;
          app.listen(PORT, () => {
            console.log(`Worker ${process.pid} started on port ${PORT}`);
          });
        }

Cloud Deployment Optimization

For high-traffic applications, consider these cloud deployment optimizations:

  • Containerization: Use Docker to package the application for consistent deployment
  • Kubernetes: Orchestrate containers for automatic scaling and high availability
  • CDN Integration: Use content delivery networks for static assets
  • Managed Services: Use managed MongoDB (Atlas) and Redis (ElastiCache) services
  • Auto-scaling: Configure auto-scaling based on traffic patterns
  • Health Checks: Implement thorough health monitoring for all components

These optimizations ensure your authentication system remains responsive under varying loads.

Practice Activities

Activity 1: Multi-Provider Social Login

Implement a complete social login system with multiple providers:

  1. Set up the project structure with Express, MongoDB, and Passport.js
  2. Implement authentication with at least three providers (Google, GitHub, and one other)
  3. Create a user model that supports multiple authentication methods
  4. Implement account linking and unlinking functionality
  5. Add a profile page showing connected providers
  6. Implement proper error handling and user feedback

Bonus: Add email/password authentication alongside social login

Activity 2: Advanced Authentication Features

Enhance your authentication system with advanced features:

  1. Implement two-factor authentication using time-based one-time passwords (TOTP)
  2. Add "Remember Me" functionality for persistent sessions
  3. Implement role-based access control with at least three roles
  4. Create an admin dashboard for managing users
  5. Add session management showing all active sessions for a user
  6. Implement account recovery mechanisms

Bonus: Add email verification for new user registrations

Activity 3: Performance Optimization

Optimize your authentication system for performance and scalability:

  1. Implement Redis caching for user profiles and sessions
  2. Add database indexes for frequently queried fields
  3. Configure connection pooling for database connections
  4. Implement rate limiting for authentication requests
  5. Add horizontal scaling support with cluster mode
  6. Set up monitoring for authentication performance

Bonus: Create a load testing script to benchmark your optimizations

Activity 4: OAuth Provider Integration

Extend your social login system with OAuth API integration:

  1. Implement Google contacts import using OAuth scopes
  2. Add GitHub repository listing using the GitHub API
  3. Implement automatic profile picture sync from social providers
  4. Add friend finder functionality that matches users with shared connections
  5. Implement token refresh mechanism for expired OAuth tokens
  6. Create a dashboard showing connected services and available data

Bonus: Add an activity feed showing actions across connected services

Additional Resources

Summary

By implementing these concepts, you can create a secure, user-friendly authentication system that leverages social providers while maintaining flexibility for future enhancements.