OAuth 2.0 Integration
Checking access...
OAuth 2.0 lets users authenticate through third-party providers (Google, GitHub, Facebook) without giving your app their password. This is commonly called “social login” or “Sign in with Google/GitHub”.
How OAuth 2.0 Works
1. User clicks "Sign in with Google"2. App redirects to Google's authorization page3. User grants permission on Google's site4. Google redirects back to your app with an authorization code5. Your server exchanges the code for access/ID tokens6. Your server uses the tokens to fetch user info7. User is authenticatedRegistration
Before implementing, register your app with the provider:
- Go to Google Cloud Console
- Create a project → APIs & Services → Credentials
- Create OAuth 2.0 Client ID (Web application)
- Set Authorized redirect URI:
http://localhost:3000/api/auth/google/callback
GitHub
- Go to GitHub Developer Settings
- New OAuth App
- Set Authorization callback URL:
http://localhost:3000/api/auth/github/callback
Passport.js Strategy
npm install passport passport-google-oauth20 passport-github2const passport = require("passport");const GoogleStrategy = require("passport-google-oauth20").Strategy;const GitHubStrategy = require("passport-github2").Strategy;
// Serialize user to sessionpassport.serializeUser((user, done) => { done(null, user.id);});
// Deserialize user from sessionpassport.deserializeUser(async (id, done) => { try { const user = await db.users.findById(id); done(null, user); } catch (err) { done(err); }});
// Google Strategypassport.use(new GoogleStrategy({ clientID: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, callbackURL: "/api/auth/google/callback", scope: ["profile", "email"],}, async (accessToken, refreshToken, profile, done) => { try { // Find or create user let user = await db.users.findByOAuthId("google", profile.id);
if (!user) { user = await db.users.create({ oauthProvider: "google", oauthId: profile.id, email: profile.emails?.[0]?.value, name: profile.displayName, avatar: profile.photos?.[0]?.value, }); }
done(null, user); } catch (err) { done(err); }}));
// GitHub Strategypassport.use(new GitHubStrategy({ clientID: process.env.GITHUB_CLIENT_ID, clientSecret: process.env.GITHUB_CLIENT_SECRET, callbackURL: "/api/auth/github/callback", scope: ["user:email"],}, async (accessToken, refreshToken, profile, done) => { try { let user = await db.users.findByOAuthId("github", profile.id);
if (!user) { // GitHub might not expose email in profile const email = profile.emails?.[0]?.value || `${profile.username}@github.com`;
user = await db.users.create({ oauthProvider: "github", oauthId: profile.id, email, name: profile.displayName || profile.username, avatar: profile.photos?.[0]?.value, }); }
done(null, user); } catch (err) { done(err); }}));
module.exports = passport;Express Routes
const express = require("express");const passport = require("../config/passport");const router = express.Router();
// Initiate Google OAuthrouter.get("/google", passport.authenticate("google", { scope: ["profile", "email"], session: true, }));
// Google callbackrouter.get("/google/callback", passport.authenticate("google", { failureRedirect: "/login?error=google_auth_failed", session: true, }), (req, res) => { // Successful authentication res.redirect("/dashboard"); });
// Initiate GitHub OAuthrouter.get("/github", passport.authenticate("github", { scope: ["user:email"], session: true, }));
// GitHub callbackrouter.get("/github/callback", passport.authenticate("github", { failureRedirect: "/login?error=github_auth_failed", session: true, }), (req, res) => { res.redirect("/dashboard"); });
// Logoutrouter.post("/logout", (req, res) => { req.logout((err) => { if (err) return res.status(500).json({ error: "Logout failed" }); req.session.destroy(() => { res.clearCookie("connect.sid"); res.json({ message: "Logged out" }); }); });});
module.exports = router;Manual OAuth (Without Passport)
// GitHub OAuth without Passportconst CLIENT_ID = process.env.GITHUB_CLIENT_ID;const CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET;const REDIRECT_URI = "http://localhost:3000/api/auth/github/callback";
// Step 1: Redirect user to GitHubapp.get("/api/auth/github", (req, res) => { const url = `https://github.com/login/oauth/authorize?client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&scope=user:email`; res.redirect(url);});
// Step 2: Handle callbackapp.get("/api/auth/github/callback", async (req, res) => { const { code } = req.query;
if (!code) { return res.status(400).json({ error: "No authorization code" }); }
try { // Exchange code for access token const tokenResponse = await fetch("https://github.com/login/oauth/access_token", { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, body: JSON.stringify({ client_id: CLIENT_ID, client_secret: CLIENT_SECRET, code, redirect_uri: REDIRECT_URI, }), });
const tokenData = await tokenResponse.json();
if (tokenData.error) { throw new Error(tokenData.error_description); }
const accessToken = tokenData.access_token;
// Fetch user info const userResponse = await fetch("https://api.github.com/user", { headers: { Authorization: `Bearer ${accessToken}` }, }); const profile = await userResponse.json();
// Fetch email const emailResponse = await fetch("https://api.github.com/user/emails", { headers: { Authorization: `Bearer ${accessToken}` }, }); const emails = await emailResponse.json(); const primaryEmail = emails.find((e) => e.primary)?.email;
// Find or create user let user = await db.users.findByOAuthId("github", profile.id); if (!user) { user = await db.users.create({ oauthProvider: "github", oauthId: profile.id, email: primaryEmail, name: profile.name || profile.login, avatar: profile.avatar_url, }); }
// Log the user in (session or JWT) req.session.userId = user.id; res.redirect("/dashboard"); } catch (err) { console.error("OAuth error:", err); res.redirect("/login?error=oauth_failed"); }});OAuth with JWT (No Sessions)
For APIs, combine OAuth with JWT instead of sessions:
app.get("/api/auth/google/callback", async (req, res) => { const { code } = req.query;
// Exchange code, fetch profile, find/create user...
// Generate JWT const token = jwt.sign( { sub: user.id, role: user.role }, process.env.JWT_SECRET, { expiresIn: "24h" } );
// Redirect to frontend with token res.redirect(`http://localhost:5173/auth/callback?token=${token}`);});PKCE Flow (Mobile Apps)
For mobile/native apps that can’t keep a client secret, use PKCE (Proof Key for Code Exchange):
// Step 1: Generate code verifier and challengefunction generatePKCE() { const verifier = crypto.randomBytes(32).toString("base64url"); const challenge = crypto .createHash("sha256") .update(verifier) .digest("base64url");
return { verifier, challenge };}
// Step 2: Redirect with code_challengeapp.get("/api/auth/mobile", (req, res) => { const { verifier, challenge } = generatePKCE(); // Store verifier temporarily (in session, memory, or send back) req.session.codeVerifier = verifier;
const params = new URLSearchParams({ client_id: process.env.GOOGLE_CLIENT_ID, redirect_uri: `${req.protocol}://${req.get("host")}/api/auth/callback`, response_type: "code", scope: "openid profile email", code_challenge: challenge, code_challenge_method: "S256", });
res.redirect(`https://accounts.google.com/o/oauth2/v2/auth?${params}`);});Security Best Practices
- Validate redirect URIs — Only accept exact matches, not prefixes
- Use state parameter — Prevent CSRF on OAuth callback
- Validate the
id_tokensignature — For OpenID Connect - Never log tokens — They grant access to user accounts
- Use HTTPS — All OAuth traffic must be encrypted
- Check token expiry — Don’t accept expired tokens
// State parameter (CSRF protection)const state = crypto.randomBytes(16).toString("hex");req.session.oauthState = state;
redirectURL.searchParams.set("state", state);
// On callbackif (req.query.state !== req.session.oauthState) { return res.status(403).json({ error: "Invalid state parameter" });}delete req.session.oauthState;Quick Reference
// OAuth flow1. Redirect → provider's authorize URL2. Provider → redirect back with code3. Exchange code → access token4. Access token → fetch user info5. Find or create user in database6. Log user in (session or JWT)
// Key URLshttps://accounts.google.com/o/oauth2/v2/auth // Google authorizehttps://github.com/login/oauth/authorize // GitHub authorizePractice Exercises
GitHub OAuth without Passport: Implement the full GitHub OAuth flow manually (no Passport.js). Include the state parameter for CSRF protection.
Account linking: After implementing Google and GitHub OAuth, allow users to link multiple providers to the same account. Show which providers are linked on the profile page.
OAuth + JWT API: Create an OAuth login that generates a JWT instead of creating a session. The callback URL should return the JWT to the frontend, which stores it and uses it for API calls.