Episode 3 — NodeJS MongoDB Backend Architecture / 3.14 — Authentication and Authorization

3.14.f — Passport.js

In one sentence: Passport.js is an authentication middleware for Node.js that uses a strategy pattern -- plug in passport-local for username/password, passport-google-oauth20 for Google login, or 500+ other strategies, all with a consistent API.

Navigation: <- 3.14.e Auth Middleware | 3.14 Exercise Questions ->


1. What is Passport.js?

Passport is the most popular authentication library for Node.js/Express with 500+ strategies (plugins) for every authentication method imaginable.

Why use Passport?

Without PassportWith Passport
Write auth logic from scratch for each methodPlug in a strategy; Passport handles the flow
Different patterns for local, Google, GitHub, etc.Consistent API: passport.authenticate('strategy')
Manage serialization yourselfBuilt-in serialize/deserialize for sessions
Harder to add new login methods laterAdd a new strategy in ~20 lines

Core concepts:

ConceptDescription
StrategyA pluggable authentication mechanism (local, OAuth, JWT, etc.)
Verify callbackFunction you write to validate credentials and return a user
SerializationConvert user object to session-storable ID
DeserializationConvert session-stored ID back to user object

2. Installing Passport

# Core + local strategy (username/password)
npm install passport passport-local

# Session support
npm install express-session

# Optional: Google OAuth
npm install passport-google-oauth20

# Optional: JWT strategy (for token-based Passport)
npm install passport-jwt

3. passport-local: username/password authentication

Step 1: Configure the strategy

// config/passport.js
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const User = require('../models/User');

passport.use(
  new LocalStrategy(
    {
      usernameField: 'email',    // default is 'username'; override for email
      passwordField: 'password',
    },
    async (email, password, done) => {
      try {
        // 1. Find user by email
        const user = await User.findOne({ email }).select('+password');
        if (!user) {
          return done(null, false, { message: 'Invalid email or password' });
        }

        // 2. Compare password
        const isMatch = await user.comparePassword(password);
        if (!isMatch) {
          return done(null, false, { message: 'Invalid email or password' });
        }

        // 3. Success -- return user
        return done(null, user);
      } catch (err) {
        return done(err);
      }
    }
  )
);

The done callback pattern:

CallMeaning
done(null, user)Success -- user authenticated
done(null, false, { message })Authentication failed (bad credentials)
done(err)Server error

Step 2: Serialization and deserialization

For session-based Passport, you must tell Passport how to store the user in the session and how to retrieve them.

// Store user ID in session
passport.serializeUser((user, done) => {
  done(null, user._id);
});

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

Flow:

Login success
  → serializeUser: user object → user._id stored in session
  
Subsequent request
  → deserializeUser: session._id → full user object on req.user

Step 3: Express integration

// app.js
const express = require('express');
const session = require('express-session');
const passport = require('passport');

const app = express();

// Body parsing
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Session (must come before passport.session())
app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    maxAge: 24 * 60 * 60 * 1000,
  },
}));

// Initialize Passport
app.use(passport.initialize());
app.use(passport.session());     // enable session-based auth

// Load strategies
require('./config/passport');

// Routes
app.use('/api/auth', require('./routes/auth'));

Step 4: Login route

// routes/auth.js
const express = require('express');
const passport = require('passport');
const router = express.Router();

router.post('/login', (req, res, next) => {
  passport.authenticate('local', (err, user, info) => {
    if (err) return next(err);
    if (!user) {
      return res.status(401).json({ error: info.message });
    }

    // Establish session
    req.logIn(user, (err) => {
      if (err) return next(err);
      res.json({
        message: 'Logged in successfully',
        user: { id: user._id, name: user.name, email: user.email },
      });
    });
  })(req, res, next);
});

// Logout
router.post('/logout', (req, res, next) => {
  req.logout((err) => {
    if (err) return next(err);
    req.session.destroy();
    res.json({ message: 'Logged out' });
  });
});

// Check if authenticated
router.get('/me', (req, res) => {
  if (!req.isAuthenticated()) {
    return res.status(401).json({ error: 'Not authenticated' });
  }
  res.json({ user: req.user });
});

module.exports = router;

Passport middleware to protect routes

function ensureAuthenticated(req, res, next) {
  if (req.isAuthenticated()) {
    return next();
  }
  res.status(401).json({ error: 'Authentication required' });
}

// Usage
router.get('/profile', ensureAuthenticated, (req, res) => {
  res.json({ user: req.user });
});

4. Google OAuth 2.0 authentication

Step 1: Create Google OAuth credentials

  1. Go to Google Cloud Console
  2. Create a project (or select existing)
  3. Navigate to APIs & Services > Credentials
  4. Click Create Credentials > OAuth 2.0 Client ID
  5. Set Application type: Web application
  6. Add Authorized redirect URIs: http://localhost:3000/api/auth/google/callback
  7. Copy the Client ID and Client Secret
# .env
GOOGLE_CLIENT_ID=123456789.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-abc123...
GOOGLE_CALLBACK_URL=http://localhost:3000/api/auth/google/callback

Step 2: Configure Google strategy

// config/passport.js (add to existing file)
const GoogleStrategy = require('passport-google-oauth20').Strategy;

passport.use(
  new GoogleStrategy(
    {
      clientID: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
      callbackURL: process.env.GOOGLE_CALLBACK_URL,
      scope: ['profile', 'email'],
    },
    async (accessToken, refreshToken, profile, done) => {
      try {
        // Check if user already exists with this Google ID
        let user = await User.findOne({ googleId: profile.id });

        if (user) {
          // Existing user -- return them
          return done(null, user);
        }

        // Check if user exists with same email (link accounts)
        user = await User.findOne({ email: profile.emails[0].value });

        if (user) {
          // Link Google ID to existing account
          user.googleId = profile.id;
          user.avatar = profile.photos[0]?.value;
          await user.save();
          return done(null, user);
        }

        // Create new user
        user = await User.create({
          name: profile.displayName,
          email: profile.emails[0].value,
          googleId: profile.id,
          avatar: profile.photos[0]?.value,
          // No password -- OAuth users don't need one
        });

        return done(null, user);
      } catch (err) {
        return done(err);
      }
    }
  )
);

Step 3: Update User model for OAuth

const userSchema = new mongoose.Schema({
  name:      { type: String, required: true },
  email:     { type: String, required: true, unique: true },
  password:  { type: String, select: false },      // optional for OAuth users
  role:      { type: String, enum: ['user', 'admin'], default: 'user' },
  googleId:  { type: String, unique: true, sparse: true }, // sparse allows null
  avatar:    { type: String },
  provider:  { type: String, enum: ['local', 'google'], default: 'local' },
}, { timestamps: true });

// Only hash password if it exists (OAuth users have no password)
userSchema.pre('save', async function (next) {
  if (!this.isModified('password') || !this.password) return next();
  this.password = await bcrypt.hash(this.password, 12);
  next();
});

Step 4: Google auth routes

// routes/auth.js (add to existing)
const passport = require('passport');

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

// Google callback (after user grants permission)
router.get('/google/callback',
  passport.authenticate('google', {
    failureRedirect: '/login?error=google_auth_failed',
  }),
  (req, res) => {
    // Successful authentication
    // Option A: redirect to frontend (session-based)
    res.redirect('/dashboard');

    // Option B: issue JWT and redirect with token
    // const token = jwt.sign({ userId: req.user._id }, SECRET, { expiresIn: '24h' });
    // res.redirect(`/auth/success?token=${token}`);
  }
);

5. Social login flow diagram

┌──────────┐    ┌──────────────┐    ┌───────────────┐    ┌────────────┐
│  Client   │    │  Your Server  │    │  Google Auth   │    │  Database   │
└─────┬────┘    └──────┬───────┘    └───────┬───────┘    └─────┬──────┘
      │                │                     │                  │
  1.  │── GET /auth/google ──►               │                  │
      │                │                     │                  │
  2.  │◄── 302 Redirect to Google ──         │                  │
      │                │                     │                  │
  3.  │────────── Visit Google consent ─────►│                  │
      │                │                     │                  │
  4.  │◄───── User approves, redirect ──────│                  │
      │         with auth code               │                  │
      │                │                     │                  │
  5.  │── GET /auth/google/callback?code= ──►│                  │
      │                │                     │                  │
  6.  │                │── Exchange code for tokens ────►       │
      │                │◄── access token + profile ────         │
      │                │                     │                  │
  7.  │                │── Find or create user ────────────────►│
      │                │◄── user document ─────────────────────│
      │                │                     │                  │
  8.  │◄── Set session/JWT + redirect ──     │                  │
      │                │                     │                  │

6. Combining local + OAuth strategies

A user might register with email/password, then later "Login with Google" using the same email. Handle this gracefully:

// In the Google strategy callback:
async (accessToken, refreshToken, profile, done) => {
  const email = profile.emails[0].value;

  // Look for existing user by Google ID first
  let user = await User.findOne({ googleId: profile.id });
  if (user) return done(null, user);

  // Look for existing user by email (account linking)
  user = await User.findOne({ email });
  if (user) {
    // User registered with password; now also linking Google
    user.googleId = profile.id;
    user.avatar = user.avatar || profile.photos[0]?.value;
    await user.save();
    return done(null, user);
  }

  // Brand new user via Google
  user = await User.create({
    name: profile.displayName,
    email,
    googleId: profile.id,
    avatar: profile.photos[0]?.value,
    provider: 'google',
  });

  return done(null, user);
};

Account linking rules:

ScenarioAction
Google ID exists in DBReturn existing user
Email exists but no Google IDLink Google ID to existing account
Neither existsCreate new user (no password)
User has password + Google IDCan log in with either method

7. Session-based Passport vs JWT Passport

Session-based (what we built above)

Login → passport.authenticate → req.logIn → serializeUser → session
Request → deserializeUser → req.user → req.isAuthenticated()

JWT-based (passport-jwt)

npm install passport-jwt
const JwtStrategy = require('passport-jwt').Strategy;
const ExtractJwt = require('passport-jwt').ExtractJwt;

passport.use(
  new JwtStrategy(
    {
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: process.env.JWT_SECRET,
    },
    async (payload, done) => {
      try {
        const user = await User.findById(payload.userId);
        if (!user) return done(null, false);
        return done(null, user);
      } catch (err) {
        return done(err, false);
      }
    }
  )
);

// Usage: protect routes with JWT (no sessions)
router.get('/profile',
  passport.authenticate('jwt', { session: false }),
  (req, res) => {
    res.json({ user: req.user });
  }
);

Comparison:

AspectSession-based PassportJWT Passport
Requires session storeYesNo
serialize/deserializeRequiredNot needed
passport.session()RequiredNot used
Logoutreq.logout() destroys sessionClient deletes token
ScalabilityShared session store neededStateless; any server works
Best forServer-rendered appsAPIs, SPAs, mobile

8. Full working example: local + Google with sessions

// --- config/passport.js (complete) ---
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const User = require('../models/User');

// Serialize: user → session
passport.serializeUser((user, done) => done(null, user._id));

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

// --- Local Strategy ---
passport.use(
  new LocalStrategy(
    { usernameField: 'email' },
    async (email, password, done) => {
      try {
        const user = await User.findOne({ email }).select('+password');
        if (!user || !user.password) {
          return done(null, false, { message: 'Invalid credentials' });
        }
        const isMatch = await user.comparePassword(password);
        if (!isMatch) {
          return done(null, false, { message: 'Invalid credentials' });
        }
        return done(null, user);
      } catch (err) {
        return done(err);
      }
    }
  )
);

// --- Google Strategy ---
passport.use(
  new GoogleStrategy(
    {
      clientID: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
      callbackURL: process.env.GOOGLE_CALLBACK_URL,
    },
    async (accessToken, refreshToken, profile, done) => {
      try {
        let user = await User.findOne({ googleId: profile.id });
        if (user) return done(null, user);

        user = await User.findOne({ email: profile.emails[0].value });
        if (user) {
          user.googleId = profile.id;
          await user.save();
          return done(null, user);
        }

        user = await User.create({
          name: profile.displayName,
          email: profile.emails[0].value,
          googleId: profile.id,
          avatar: profile.photos[0]?.value,
          provider: 'google',
        });
        return done(null, user);
      } catch (err) {
        return done(err);
      }
    }
  )
);

module.exports = passport;

9. Key takeaways

  1. Passport uses the strategy pattern -- each auth method is a pluggable strategy.
  2. The verify callback receives credentials and returns done(null, user) or done(null, false).
  3. Serialization stores user ID in session; deserialization retrieves the full user on each request.
  4. Google OAuth: user clicks "Login with Google" -> redirected to Google -> callback with profile data -> find or create user.
  5. Account linking lets a user log in with either password or Google if both are configured.
  6. For APIs/SPAs, use passport-jwt with { session: false } instead of session-based Passport.

Explain-It Challenge

Explain without notes:

  1. What is the difference between passport.authenticate('local') and passport.authenticate('google')? What does each strategy expect as input?
  2. Why do you need both serializeUser and deserializeUser for session-based Passport, but not for JWT-based Passport?
  3. A user registered with email/password. Later they click "Login with Google" (same email). Describe what happens in the Google strategy callback.

Navigation: <- 3.14.e Auth Middleware | 3.14 Exercise Questions ->