← Back to DevBytes

Mongoose Authentication: JWT, Sessions, and OAuth Integration

Understanding Mongoose Authentication

Authentication in Mongoose-based applications is the process of verifying user identity before granting access to protected resources. Mongoose, as the MongoDB object modeling tool for Node.js, handles data persistence, but authentication logic typically combines Mongoose schemas with dedicated authentication libraries. The three dominant patterns—JWT (JSON Web Tokens), session-based authentication, and OAuth integration—each solve different architectural needs.

Why authentication matters: Without robust authentication, your API exposes sensitive user data, allows unauthorized mutations, and violates basic security principles. A well-implemented authentication layer protects user privacy, complies with regulations like GDPR, and establishes trust. Mongoose's schema validation and middleware hooks make it an ideal partner for embedding authentication logic directly into your data models.

Setting Up the Mongoose User Model

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Every authentication system begins with a properly designed User schema. This schema stores credentials, handles password hashing, and provides methods for verification.

const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');

const userSchema = new mongoose.Schema({
  email: {
    type: String,
    required: true,
    unique: true,
    lowercase: true,
    trim: true,
    validate: {
      validator: function(v) {
        return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v);
      },
      message: 'Please enter a valid email address'
    }
  },
  password: {
    type: String,
    required: true,
    minlength: 8
  },
  name: {
    type: String,
    required: true,
    trim: true
  },
  role: {
    type: String,
    enum: ['user', 'admin', 'moderator'],
    default: 'user'
  },
  refreshToken: {
    type: String,
    default: null
  },
  oauthProvider: {
    type: String,
    enum: ['local', 'google', 'github', 'facebook'],
    default: 'local'
  },
  oauthId: {
    type: String,
    default: null
  },
  createdAt: {
    type: Date,
    default: Date.now
  }
});

// Hash password before saving
userSchema.pre('save', async function(next) {
  if (!this.isModified('password')) return next();
  try {
    const salt = await bcrypt.genSalt(12);
    this.password = await bcrypt.hash(this.password, salt);
    next();
  } catch (error) {
    next(error);
  }
});

// Compare password method
userSchema.methods.comparePassword = async function(candidatePassword) {
  return await bcrypt.compare(candidatePassword, this.password);
};

// Remove password from JSON output
userSchema.methods.toJSON = function() {
  const obj = this.toObject();
  delete obj.password;
  delete obj.refreshToken;
  delete obj.__v;
  return obj;
};

const User = mongoose.model('User', userSchema);
module.exports = User;

Key design decisions in this schema: The email field uses a custom validator with regex for format checking. The password is hashed using bcrypt with a salt rounds value of 12—balancing security and performance. The toJSON() method strips sensitive fields from API responses, preventing accidental password leaks. The refreshToken field supports token rotation strategies we'll explore later.

JWT Authentication with Mongoose

What JWT Authentication Is

JWT authentication issues cryptographically signed tokens that clients store and attach to subsequent requests. Each token contains encoded claims—typically a user ID, role, and expiration timestamp. The server verifies the signature without querying the database on every request, making JWT ideal for stateless, high-throughput APIs and microservices.

Why JWT Matters for Mongoose Applications

JWT eliminates server-side session storage, reducing database load and enabling horizontal scaling without sticky sessions. For Mongoose-backed REST APIs and GraphQL servers, JWT provides a clean separation between authentication (verifying the token) and authorization (checking the user's role from the decoded payload). The stateless nature means your Mongoose models focus on business logic rather than session management.

Implementing JWT Authentication

Install required dependencies:

npm install jsonwebtoken bcryptjs cookie-parser

Create a dedicated JWT utility module for token generation and verification:

const jwt = require('jsonwebtoken');

const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET || 'your-refresh-secret-change-in-production';

const tokenUtils = {
  // Generate access token (short-lived)
  generateAccessToken(user) {
    const payload = {
      userId: user._id.toString(),
      email: user.email,
      role: user.role
    };
    
    return jwt.sign(payload, JWT_SECRET, { 
      expiresIn: '15m',
      issuer: 'your-app-name',
      audience: 'your-app-users'
    });
  },

  // Generate refresh token (long-lived)
  generateRefreshToken(user) {
    const payload = {
      userId: user._id.toString(),
      tokenVersion: user.tokenVersion || 0
    };
    
    return jwt.sign(payload, JWT_REFRESH_SECRET, { 
      expiresIn: '7d',
      issuer: 'your-app-name',
      audience: 'your-app-users'
    });
  },

  // Verify access token
  verifyAccessToken(token) {
    try {
      const decoded = jwt.verify(token, JWT_SECRET, {
        issuer: 'your-app-name',
        audience: 'your-app-users'
      });
      return { valid: true, expired: false, decoded };
    } catch (error) {
      if (error.name === 'TokenExpiredError') {
        return { valid: true, expired: true, decoded: jwt.decode(token) };
      }
      return { valid: false, expired: false, decoded: null };
    }
  },

  // Verify refresh token
  verifyRefreshToken(token) {
    try {
      const decoded = jwt.verify(token, JWT_REFRESH_SECRET, {
        issuer: 'your-app-name',
        audience: 'your-app-users'
      });
      return { valid: true, decoded };
    } catch (error) {
      return { valid: false, decoded: null };
    }
  },

  // Decode token without verification (for extracting userId)
  decodeToken(token) {
    return jwt.decode(token);
  }
};

module.exports = tokenUtils;

Now implement the authentication middleware that protects routes:

const tokenUtils = require('../utils/tokenUtils');
const User = require('../models/User');

const authMiddleware = {
  // Verify access token and attach user to request
  async authenticate(req, res, next) {
    try {
      const authHeader = req.headers.authorization;
      
      if (!authHeader || !authHeader.startsWith('Bearer ')) {
        return res.status(401).json({ 
          error: 'Authentication required',
          message: 'Please provide a valid access token in the Authorization header'
        });
      }

      const token = authHeader.split(' ')[1];
      
      if (!token) {
        return res.status(401).json({ 
          error: 'Authentication required',
          message: 'Token not found in Authorization header'
        });
      }

      const result = tokenUtils.verifyAccessToken(token);

      if (!result.valid) {
        return res.status(401).json({ 
          error: 'Invalid token',
          message: 'The provided token is invalid or has been tampered with'
        });
      }

      if (result.expired) {
        return res.status(401).json({ 
          error: 'Token expired',
          message: 'Your access token has expired, please refresh your token'
        });
      }

      // Fetch user from database to ensure they still exist and have proper permissions
      const user = await User.findById(result.decoded.userId).select('-password -refreshToken');
      
      if (!user) {
        return res.status(401).json({ 
          error: 'User not found',
          message: 'The user associated with this token no longer exists'
        });
      }

      // Attach user to request object for downstream handlers
      req.user = user;
      req.tokenPayload = result.decoded;
      next();
      
    } catch (error) {
      return res.status(500).json({ 
        error: 'Authentication error',
        message: 'An unexpected error occurred during authentication'
      });
    }
  },

  // Role-based authorization middleware
  authorize(...allowedRoles) {
    return (req, res, next) => {
      if (!req.user) {
        return res.status(401).json({ 
          error: 'Authentication required',
          message: 'You must be authenticated to access this resource'
        });
      }

      if (!allowedRoles.includes(req.user.role)) {
        return res.status(403).json({ 
          error: 'Insufficient permissions',
          message: `You need one of the following roles: ${allowedRoles.join(', ')}`
        });
      }

      next();
    };
  },

  // Optional authentication (doesn't fail if no token)
  async optionalAuth(req, res, next) {
    try {
      const authHeader = req.headers.authorization;
      
      if (authHeader && authHeader.startsWith('Bearer ')) {
        const token = authHeader.split(' ')[1];
        const result = tokenUtils.verifyAccessToken(token);
        
        if (result.valid && !result.expired) {
          const user = await User.findById(result.decoded.userId).select('-password -refreshToken');
          if (user) {
            req.user = user;
            req.tokenPayload = result.decoded;
          }
        }
      }
      
      // Always proceed, even without authentication
      next();
      
    } catch (error) {
      next(); // Proceed without authentication on error
    }
  }
};

module.exports = authMiddleware;

Create the authentication routes for login, registration, and token refresh:

const express = require('express');
const router = express.Router();
const User = require('../models/User');
const tokenUtils = require('../utils/tokenUtils');
const authMiddleware = require('../middleware/authMiddleware');

// Register a new user
router.post('/register', async (req, res) => {
  try {
    const { email, password, name } = req.body;

    // Check if user already exists
    const existingUser = await User.findOne({ email: email.toLowerCase() });
    if (existingUser) {
      return res.status(409).json({ 
        error: 'User already exists',
        message: 'An account with this email address already exists'
      });
    }

    // Create new user
    const user = new User({
      email: email.toLowerCase(),
      password, // Will be hashed by the pre-save hook
      name,
      oauthProvider: 'local'
    });

    await user.save();

    // Generate tokens
    const accessToken = tokenUtils.generateAccessToken(user);
    const refreshToken = tokenUtils.generateRefreshToken(user);

    // Store refresh token hash in database for tracking
    user.refreshToken = refreshToken;
    await user.save();

    res.status(201).json({
      message: 'User registered successfully',
      user: user.toJSON(),
      accessToken,
      refreshToken
    });

  } catch (error) {
    if (error.name === 'ValidationError') {
      const messages = Object.values(error.errors).map(e => e.message);
      return res.status(400).json({ 
        error: 'Validation failed',
        messages 
      });
    }
    res.status(500).json({ 
      error: 'Registration failed',
      message: 'An unexpected error occurred during registration'
    });
  }
});

// Login user
router.post('/login', async (req, res) => {
  try {
    const { email, password } = req.body;

    // Find user by email
    const user = await User.findOne({ email: email.toLowerCase() });
    
    if (!user) {
      return res.status(401).json({ 
        error: 'Invalid credentials',
        message: 'No account found with this email address'
      });
    }

    // Check if user uses local authentication
    if (user.oauthProvider !== 'local') {
      return res.status(401).json({ 
        error: 'OAuth account',
        message: `This account uses ${user.oauthProvider} authentication. Please sign in with ${user.oauthProvider}.`
      });
    }

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

    // Generate tokens
    const accessToken = tokenUtils.generateAccessToken(user);
    const refreshToken = tokenUtils.generateRefreshToken(user);

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

    res.json({
      message: 'Login successful',
      user: user.toJSON(),
      accessToken,
      refreshToken
    });

  } catch (error) {
    res.status(500).json({ 
      error: 'Login failed',
      message: 'An unexpected error occurred during login'
    });
  }
});

// Refresh access token
router.post('/refresh-token', async (req, res) => {
  try {
    const { refreshToken } = req.body;

    if (!refreshToken) {
      return res.status(400).json({ 
        error: 'Refresh token required',
        message: 'Please provide a refresh token'
      });
    }

    const result = tokenUtils.verifyRefreshToken(refreshToken);

    if (!result.valid) {
      return res.status(401).json({ 
        error: 'Invalid refresh token',
        message: 'The refresh token is invalid or expired, please login again'
      });
    }

    // Find user and verify they still exist
    const user = await User.findById(result.decoded.userId);
    
    if (!user) {
      return res.status(401).json({ 
        error: 'User not found',
        message: 'The user associated with this token no longer exists'
      });
    }

    // Check if the stored refresh token matches (optional but adds security)
    if (user.refreshToken !== refreshToken) {
      // Potential token reuse detected — invalidate all tokens
      user.refreshToken = null;
      await user.save();
      return res.status(401).json({ 
        error: 'Token reuse detected',
        message: 'This refresh token has already been used. Please login again for security.'
      });
    }

    // Generate new access token
    const newAccessToken = tokenUtils.generateAccessToken(user);
    
    // Generate new refresh token (rotation for security)
    const newRefreshToken = tokenUtils.generateRefreshToken(user);
    user.refreshToken = newRefreshToken;
    await user.save();

    res.json({
      accessToken: newAccessToken,
      refreshToken: newRefreshToken
    });

  } catch (error) {
    res.status(500).json({ 
      error: 'Token refresh failed',
      message: 'An unexpected error occurred during token refresh'
    });
  }
});

// Logout - invalidate refresh token
router.post('/logout', authMiddleware.authenticate, async (req, res) => {
  try {
    req.user.refreshToken = null;
    await req.user.save();
    
    res.json({ message: 'Logged out successfully' });
  } catch (error) {
    res.status(500).json({ 
      error: 'Logout failed',
      message: 'An unexpected error occurred during logout'
    });
  }
});

// Get current user profile
router.get('/me', authMiddleware.authenticate, async (req, res) => {
  res.json({ user: req.user });
});

// Admin-only route example
router.get('/admin/users', 
  authMiddleware.authenticate,
  authMiddleware.authorize('admin'),
  async (req, res) => {
    const users = await User.find().select('-password -refreshToken');
    res.json({ users });
  }
);

module.exports = router;

JWT Best Practices

Session-Based Authentication with Mongoose

What Session Authentication Is

Session-based authentication stores user identity on the server after successful login. A session identifier—typically a random, opaque string—is stored in a cookie on the client. On each request, the server looks up the session in the database or an in-memory store like Redis, retrieves the associated user data, and attaches it to the request context.

Why Session Authentication Matters

Sessions provide immediate revocation capability: deleting the session record instantly invalidates access. This contrasts with JWT, where tokens remain valid until expiration unless you implement a blacklist. Sessions excel in traditional server-rendered applications (Express + EJS/Pug), where the server controls the full request lifecycle. For Mongoose applications, sessions stored in MongoDB offer persistence across server restarts without external dependencies.

Implementing Session Authentication

Install the required packages:

npm install express-session connect-mongo

Create a session store backed by MongoDB using connect-mongo, which leverages Mongoose's connection:

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

const app = express();

// Configure session middleware
app.use(session({
  secret: process.env.SESSION_SECRET || 'a-strong-secret-for-sessions',
  resave: false,
  saveUninitialized: false,
  rolling: true,
  cookie: {
    maxAge: 24 * 60 * 60 * 1000, // 24 hours
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    domain: process.env.COOKIE_DOMAIN || undefined
  },
  store: MongoStore.create({
    mongoUrl: process.env.MONGODB_URI || 'mongodb://localhost:27017/yourapp',
    collectionName: 'sessions',
    ttl: 24 * 60 * 60, // 24 hours in seconds
    autoRemove: 'native',
    crypto: {
      secret: process.env.SESSION_STORE_SECRET || 'another-strong-secret'
    },
    touchAfter: 3 * 60 // Update session every 3 minutes max to reduce DB writes
  }),
  name: 'app.sid' // Custom session cookie name to avoid default fingerprinting
}));

Create the login and session management routes:

const router = require('express').Router();
const User = require('../models/User');

// Session-based login
router.post('/login', async (req, res) => {
  try {
    const { email, password } = req.body;
    
    const user = await User.findOne({ email: email.toLowerCase() });
    
    if (!user) {
      return res.status(401).json({ 
        error: 'Invalid credentials',
        message: 'No account found with this email'
      });
    }

    const isMatch = await user.comparePassword(password);
    
    if (!isMatch) {
      return res.status(401).json({ 
        error: 'Invalid credentials',
        message: 'Incorrect password'
      });
    }

    // Regenerate session to prevent session fixation
    req.session.regenerate(async (err) => {
      if (err) {
        return res.status(500).json({ 
          error: 'Session error',
          message: 'Failed to create session'
        });
      }

      // Store user data in session
      req.session.userId = user._id.toString();
      req.session.email = user.email;
      req.session.role = user.role;
      req.session.loginTime = new Date().toISOString();
      req.session.userAgent = req.headers['user-agent'] || 'unknown';
      req.session.ipAddress = req.ip;

      // Save session explicitly
      req.session.save((saveErr) => {
        if (saveErr) {
          return res.status(500).json({ 
            error: 'Session save error',
            message: 'Failed to persist session'
          });
        }
        
        res.json({
          message: 'Login successful',
          user: user.toJSON()
        });
      });
    });

  } catch (error) {
    res.status(500).json({ 
      error: 'Login failed',
      message: 'An unexpected error occurred'
    });
  }
});

// Session check middleware
function requireSession(req, res, next) {
  if (!req.session || !req.session.userId) {
    return res.status(401).json({ 
      error: 'Authentication required',
      message: 'Please login to access this resource'
    });
  }
  next();
}

// Attach user to request from session
async function attachUser(req, res, next) {
  if (req.session && req.session.userId) {
    try {
      const user = await User.findById(req.session.userId).select('-password -refreshToken');
      if (user) {
        req.user = user;
        req.isAuthenticated = true;
      } else {
        // User deleted — destroy session
        req.session.destroy();
        req.isAuthenticated = false;
      }
    } catch (error) {
      req.isAuthenticated = false;
    }
  } else {
    req.isAuthenticated = false;
  }
  next();
}

// Logout - destroy session
router.post('/logout', (req, res) => {
  req.session.destroy((err) => {
    if (err) {
      return res.status(500).json({ 
        error: 'Logout failed',
        message: 'Could not destroy session'
      });
    }
    
    res.clearCookie('app.sid', {
      path: '/',
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'lax'
    });
    
    res.json({ message: 'Logged out successfully' });
  });
});

// Get current session info
router.get('/session', requireSession, async (req, res) => {
  const user = await User.findById(req.session.userId).select('-password -refreshToken');
  
  res.json({
    session: {
      id: req.session.id,
      loginTime: req.session.loginTime,
      userAgent: req.session.userAgent
    },
    user
  });
});

module.exports = { router, requireSession, attachUser };

Apply the session middleware globally and protect routes:

const express = require('express');
const app = express();
const { attachUser, requireSession } = require('./routes/sessionAuth');

// Apply attachUser globally for optional authentication
app.use(attachUser);

// Public route
app.get('/api/public', (req, res) => {
  res.json({ message: 'Public data', authenticated: req.isAuthenticated });
});

// Protected route requiring active session
app.get('/api/protected', requireSession, (req, res) => {
  res.json({ 
    message: 'Protected data',
    user: req.user 
  });
});

// Admin route with role check
app.get('/api/admin', requireSession, (req, res) => {
  if (req.user.role !== 'admin') {
    return res.status(403).json({ 
      error: 'Forbidden',
      message: 'Admin access required'
    });
  }
  
  res.json({ message: 'Admin data', user: req.user });
});

Session Authentication Best Practices

OAuth Integration with Mongoose

What OAuth Integration Is

OAuth integration delegates authentication to trusted third-party providers like Google, GitHub, or Facebook. Instead of managing passwords, your application redirects users to the provider, receives an authorization code, exchanges it for an access token, and uses that token to fetch the user's profile information. This profile data is then used to find or create a corresponding Mongoose User document.

Why OAuth Matters

OAuth reduces friction in the onboarding process—users can sign up with one click using an existing account. It also shifts the burden of password management and security to specialized providers. For applications that integrate with third-party services (like accessing a user's Google Drive or GitHub repositories), OAuth provides both authentication and API authorization in a single flow.

Implementing Google OAuth Integration

Install the Google OAuth library and configure it:

npm install googleapis @google-oauth2 passport passport-google-oauth20

Set up Passport with the Google strategy and connect it to Mongoose:

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

passport.use(new GoogleStrategy({
  clientID: process.env.GOOGLE_CLIENT_ID,
  clientSecret: process.env.GOOGLE_CLIENT_SECRET,
  callbackURL: process.env.GOOGLE_CALLBACK_URL || 'http://localhost:3000/auth/google/callback',
  passReqToCallback: true,
  scope: ['profile', 'email']
}, async (req, accessToken, refreshToken, profile, done) => {
  try {
    // Check if user already exists with this OAuth ID
    let user = await User.findOne({ 
      oauthProvider: 'google',
      oauthId: profile.id 
    });

    if (user) {
      // Update tokens and profile info
      user.name = profile.displayName || user.name;
      user.email = profile.emails[0].value || user.email;
      user.refreshToken = refreshToken || user.refreshToken;
      await user.save();
      return done(null, user);
    }

    // Check if user exists with same email but different auth method
    const email = profile.emails[0]?.value;
    if (email) {
      const existingUser = await User.findOne({ email: email.toLowerCase() });
      
      if (existingUser) {
        // Link OAuth to existing account
        existingUser.oauthProvider = 'google';
        existingUser.oauthId = profile.id;
        existingUser.refreshToken = refreshToken;
        await existingUser.save();
        return done(null, existingUser);
      }
    }

    // Create new user from Google profile
    user = new User({
      email: email || `${profile.id}@google.oauth`,
      name: profile.displayName,
      password: await require('crypto').randomBytes(32).toString('hex'), // Random secure password
      oauthProvider: 'google',
      oauthId: profile.id,
      refreshToken: refreshToken
    });

    await user.save();
    return done(null, user);

  } catch (error) {
    return done(error, null);
  }
}));

// Serialize user for session (if using sessions alongside OAuth)
passport.serializeUser((user, done) => {
  done(null, user._id.toString());
});

passport.deserializeUser(async (id, done) => {
  try {
    const user = await User.findById(id).select('-password -refreshToken');
    done(null, user);
  } catch (error) {
    done(error, null);
  }
});

module.exports = passport;

Create the OAuth routes for initiating and handling the callback:

const express = require('express');
const router = express.Router();
const passport = require('../config/passport');
const tokenUtils = require('../utils/tokenUtils');

// Initiate Google OAuth flow
router.get('/google', (req, res, next) => {
  // Store the intended redirect URL in the state parameter
  const state = req.query.redirect || '/';
  const stateEncoded = Buffer.from(JSON.stringify({ redirect: state })).toString('base64');
  
  passport.authenticate('google', {
    scope: ['profile', 'email'],
    state: stateEncoded,
    prompt: 'select_account' // Allow user to choose account
  })(req, res, next);
});

// Google OAuth callback
router.get('/google/callback', 
  passport.authenticate('google', { 
    failureRedirect: '/login?error=oauth_failed',
    session: false // Disable session if using JWT
  }),
  async (req, res) => {
    try {
      // Generate JWT tokens for the authenticated user
      const accessToken = tokenUtils.generateAccessToken(req.user);
      const refreshToken = tokenUtils.generateRefreshToken(req.user);
      
      req.user.refreshToken = refreshToken;
      await req.user.save();

      // Parse the state parameter to get redirect URL
      let redirectUrl = process.env.CLIENT_URL || 'http://localhost:3000/dashboard';
      if (req.query.state) {
        try {
          const stateObj = JSON.parse(Buffer.from(req.query.state, 'base64').toString());
          if (stateObj.redirect) {
            redirectUrl = stateObj.redirect;
          }
        } catch (e) {
          // Invalid state, use default redirect
        }
      }

      // Redirect with tokens (in production, use a secure method to pass tokens)
      const params = new URLSearchParams({
        accessToken,
        refreshToken,
        user: JSON.stringify(req.user.toJSON())
      });
      
      res.redirect(`${redirectUrl}?${params.toString()}`);
      
    } catch (error) {
      res.redirect('/login?error=token_generation_failed');
    }
  }
);

// GitHub OAuth routes follow the same pattern
router.get('/github', passport.authenticate('github', {
  scope: ['user:email']
}));

router.get('/github/callback',
  passport.authenticate('github', { 
    failureRedirect: '/login?error=oauth_failed',
    session: false
  }),
  async (req, res) => {
    const accessToken = tokenUtils.generateAccessToken(req.user);
    const refreshToken = tokenUtils.generateRefreshToken(req.user);
    req.user.refreshToken = refreshToken;
    await req.user.save();
    
    res.redirect(`${process.env.CLIENT_URL}?accessToken=${accessToken}&refreshToken=${refreshToken}`);
  }
);

// Link additional OAuth providers to existing account
router.post('/link/google', tokenUtils.verifyAccessToken, async (req, res) => {
  // This would redirect to Google OAuth with a linking state parameter
  const linkState = Buffer.from(JSON.stringify({ 
    action: 'link',
    userId: req.user._id.toString() 
  })).toString('base64');
  
  res.redirect(`/auth/google?state=${linkState}`);
});

module.exports = router;

Handle account linking in the Passport callback by checking the state parameter:

// Inside the GoogleStrategy callback, add this logic before finding the user:

if (req.query.state) {
  try {
    const stateObj = JSON.parse(Buffer.from(req.query.state, 'base64').toString());
    
    if (stateObj.action === 'link' && stateObj.userId) {
      // User wants to link this Google account to an existing account
      const existingUser = await User.findById(stateObj.userId);
      
      if (existingUser) {
        // Check if this Google account is already linked to another user
        const googleUser = await User.findOne({ 
          oauthProvider: 'google', 
          oauthId: profile.id 
        });
        
        if (googleUser && googleUser._id.toString() !== stateObj.userId) {
          return done(new Error('This Google account is already linked to another user'), null);
        }
        
        // Link the Google account
        existingUser.oauthProvider = 'google';
        existingUser.oauthId = profile.id;
        existingUser.email = profile.emails[0]?.value || existingUser.email;
        await existingUser.save();
        
        return done(null, existingUser);
      }
    }
  } catch (e) {
    // Invalid state, proceed with normal flow
  }
}

OAuth Integration Best Practices

Combining Multiple Authentication Strategies

Real-world applications often support multiple authentication methods simultaneously. Users might log in via email/password, while others use Google OAuth, and admin

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles