Skip to main content

Skillber v1.0 is here!

Learn more

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 page
3. User grants permission on Google's site
4. Google redirects back to your app with an authorization code
5. Your server exchanges the code for access/ID tokens
6. Your server uses the tokens to fetch user info
7. User is authenticated

Registration

Before implementing, register your app with the provider:

Google

  1. Go to Google Cloud Console
  2. Create a project → APIs & Services → Credentials
  3. Create OAuth 2.0 Client ID (Web application)
  4. Set Authorized redirect URI: http://localhost:3000/api/auth/google/callback

GitHub

  1. Go to GitHub Developer Settings
  2. New OAuth App
  3. Set Authorization callback URL: http://localhost:3000/api/auth/github/callback

Passport.js Strategy

Terminal window
npm install passport passport-google-oauth20 passport-github2
src/config/passport.js
const passport = require("passport");
const GoogleStrategy = require("passport-google-oauth20").Strategy;
const GitHubStrategy = require("passport-github2").Strategy;
// Serialize user to session
passport.serializeUser((user, done) => {
done(null, user.id);
});
// Deserialize user from session
passport.deserializeUser(async (id, done) => {
try {
const user = await db.users.findById(id);
done(null, user);
} catch (err) {
done(err);
}
});
// Google Strategy
passport.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 Strategy
passport.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

src/routes/auth.js
const express = require("express");
const passport = require("../config/passport");
const router = express.Router();
// Initiate Google OAuth
router.get("/google",
passport.authenticate("google", {
scope: ["profile", "email"],
session: true,
})
);
// Google callback
router.get("/google/callback",
passport.authenticate("google", {
failureRedirect: "/login?error=google_auth_failed",
session: true,
}),
(req, res) => {
// Successful authentication
res.redirect("/dashboard");
}
);
// Initiate GitHub OAuth
router.get("/github",
passport.authenticate("github", {
scope: ["user:email"],
session: true,
})
);
// GitHub callback
router.get("/github/callback",
passport.authenticate("github", {
failureRedirect: "/login?error=github_auth_failed",
session: true,
}),
(req, res) => {
res.redirect("/dashboard");
}
);
// Logout
router.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 Passport
const 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 GitHub
app.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 callback
app.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 challenge
function 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_challenge
app.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

  1. Validate redirect URIs — Only accept exact matches, not prefixes
  2. Use state parameter — Prevent CSRF on OAuth callback
  3. Validate the id_token signature — For OpenID Connect
  4. Never log tokens — They grant access to user accounts
  5. Use HTTPS — All OAuth traffic must be encrypted
  6. 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 callback
if (req.query.state !== req.session.oauthState) {
return res.status(403).json({ error: "Invalid state parameter" });
}
delete req.session.oauthState;

Quick Reference

// OAuth flow
1. Redirect → provider's authorize URL
2. Provider → redirect back with code
3. Exchange code → access token
4. Access token → fetch user info
5. Find or create user in database
6. Log user in (session or JWT)
// Key URLs
https://accounts.google.com/o/oauth2/v2/auth // Google authorize
https://github.com/login/oauth/authorize // GitHub authorize

Practice Exercises

  1. GitHub OAuth without Passport: Implement the full GitHub OAuth flow manually (no Passport.js). Include the state parameter for CSRF protection.

  2. 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.

  3. 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.