OAuth 2.0 and Social Login (Passport.js)
OAuth 2.0 is an authorisation framework that allows a third-party application to access a user's data on an external service (Google, GitHub, Facebook) without ever receiving the user's password. The user authenticates directly with the external provider, which then issues an access token to your application. Passport.js is the dominant Node.js middleware library for implementing OAuth 2.0 strategies, with over 500 community-maintained strategy packages.
The OAuth 2.0 Authorization Code Flow
The most secure OAuth flow for web apps is the Authorization Code flow. Step 1: your app redirects the user to the provider's login page with your client_id and a redirect_uri. Step 2: the user consents and the provider redirects back to your redirect_uri with a short-lived code. Step 3: your server exchanges the code for an access token in a server-to-server request (never exposed to the browser). Step 4: you use the access token to fetch the user's profile.
npm install passport passport-google-oauth20 passport-github2 express-session// config/passport.js
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: '/auth/google/callback'
}, async (accessToken, refreshToken, profile, done) => {
try {
// Find or create user based on Google profile ID
let user = await User.findOne({ googleId: profile.id });
if (!user) {
user = await User.create({
googleId: profile.id,
email: profile.emails[0].value,
displayName: profile.displayName,
avatar: profile.photos[0]?.value
});
}
return done(null, user);
} catch (err) {
return done(err);
}
}));
passport.serializeUser((user, done) => done(null, user.id));
passport.deserializeUser(async (id, done) => {
const user = await User.findById(id);
done(null, user);
});Wiring Routes and Session Serialisation
Passport integrates with Express sessions: after successful OAuth, it serialises the user to the session so they remain logged in across requests. Alternatively, you can skip sessions entirely, issue your own JWT on callback success, and use stateless authentication for API routes.
// app.js
const session = require('express-session');
const passport = require('passport');
require('./config/passport');
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false
}));
app.use(passport.initialize());
app.use(passport.session());
// routes/auth.js
// 1. Redirect to Google
router.get('/google', passport.authenticate('google', { scope: ['profile', 'email'] }));
// 2. Google redirects back here with the code
router.get('/google/callback',
passport.authenticate('google', { failureRedirect: '/login?error=oauth' }),
(req, res) => {
// Optionally issue JWT here instead of relying on session
const token = signAccess({ sub: req.user.id, role: req.user.role });
res.redirect('/dashboard?token=' + token);
}
);
// Protect a route with Passport session
function requireAuth(req, res, next) {
if (req.isAuthenticated()) return next();
res.status(401).json({ error: 'Unauthenticated' });
}
router.get('/profile', requireAuth, (req, res) => {
res.json(req.user);
});