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-localfor username/password,passport-google-oauth20for 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 Passport | With Passport |
|---|---|
| Write auth logic from scratch for each method | Plug in a strategy; Passport handles the flow |
| Different patterns for local, Google, GitHub, etc. | Consistent API: passport.authenticate('strategy') |
| Manage serialization yourself | Built-in serialize/deserialize for sessions |
| Harder to add new login methods later | Add a new strategy in ~20 lines |
Core concepts:
| Concept | Description |
|---|---|
| Strategy | A pluggable authentication mechanism (local, OAuth, JWT, etc.) |
| Verify callback | Function you write to validate credentials and return a user |
| Serialization | Convert user object to session-storable ID |
| Deserialization | Convert 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:
| Call | Meaning |
|---|---|
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
- Go to Google Cloud Console
- Create a project (or select existing)
- Navigate to APIs & Services > Credentials
- Click Create Credentials > OAuth 2.0 Client ID
- Set Application type: Web application
- Add Authorized redirect URIs:
http://localhost:3000/api/auth/google/callback - 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:
| Scenario | Action |
|---|---|
| Google ID exists in DB | Return existing user |
| Email exists but no Google ID | Link Google ID to existing account |
| Neither exists | Create new user (no password) |
| User has password + Google ID | Can 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:
| Aspect | Session-based Passport | JWT Passport |
|---|---|---|
| Requires session store | Yes | No |
| serialize/deserialize | Required | Not needed |
passport.session() | Required | Not used |
| Logout | req.logout() destroys session | Client deletes token |
| Scalability | Shared session store needed | Stateless; any server works |
| Best for | Server-rendered apps | APIs, 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
- Passport uses the strategy pattern -- each auth method is a pluggable strategy.
- The verify callback receives credentials and returns
done(null, user)ordone(null, false). - Serialization stores user ID in session; deserialization retrieves the full user on each request.
- Google OAuth: user clicks "Login with Google" -> redirected to Google -> callback with profile data -> find or create user.
- Account linking lets a user log in with either password or Google if both are configured.
- For APIs/SPAs, use passport-jwt with
{ session: false }instead of session-based Passport.
Explain-It Challenge
Explain without notes:
- What is the difference between
passport.authenticate('local')andpassport.authenticate('google')? What does each strategy expect as input? - Why do you need both
serializeUseranddeserializeUserfor session-based Passport, but not for JWT-based Passport? - 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 ->