From d482433c452d0e1a7cdb6fe89bc4fb6f5545fe27 Mon Sep 17 00:00:00 2001 From: Soumadip Sen Date: Wed, 20 May 2026 19:56:29 +0530 Subject: [PATCH 1/2] feat: implement Google & GitHub OAuth and persist session stores via Mongo --- backend/.env.sample | 6 ++- backend/config/passportConfig.js | 72 ++++++++++++++++++++++++++++++++ backend/models/User.js | 6 +++ backend/package.json | 8 ++-- backend/routes/auth.js | 20 +++++++++ backend/server.js | 9 +++- src/pages/Login/Login.tsx | 14 +++++++ src/pages/Signup/Signup.tsx | 14 +++++++ 8 files changed, 144 insertions(+), 5 deletions(-) diff --git a/backend/.env.sample b/backend/.env.sample index 98f96881..424da19e 100644 --- a/backend/.env.sample +++ b/backend/.env.sample @@ -1,3 +1,7 @@ PORT=5000 -MONGO_URI=mongodb://localhost:27017/githubTracker +MONGO_URI=mongodb://mongo:27017/githubTracker SESSION_SECRET=your-secret-key +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= \ No newline at end of file diff --git a/backend/config/passportConfig.js b/backend/config/passportConfig.js index 842f50ca..4382db8a 100644 --- a/backend/config/passportConfig.js +++ b/backend/config/passportConfig.js @@ -1,4 +1,6 @@ const passport = require("passport"); +const GoogleStrategy = require('passport-google-oauth20').Strategy; +const GitHubStrategy = require('passport-github2').Strategy; const LocalStrategy = require('passport-local').Strategy; const User = require("../models/User"); @@ -29,6 +31,76 @@ passport.use( ) ); +passport.use( + new GoogleStrategy( + { + clientID: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + callbackURL: "/api/auth/google/callback", + }, + async (accessToken, refreshToken, profile, done) => { + try { + // Check if user already exists by their email + let user = await User.findOne({ email: profile.emails[0].value }); + + if (!user) { + // Create a user placeholder if they are logging in for the first time + user = new User({ + username: profile.displayName.replace(/\s+/g, "_").toLowerCase() + "_" + profile.id.substring(0, 5), + email: profile.emails[0].value, + // Create a dummy password since the schema requires it + password: "OAUTH_USER_EXTERNAL_PROVIDER" + }); + await user.save(); + } + + return done(null, { + id: user._id.toString(), + username: user.username, + email: user.email + }); + } catch (err) { + return done(err); + } + } + ) +); + +// 2. GitHub OAuth Strategy +passport.use( + new GitHubStrategy( + { + clientID: process.env.GITHUB_CLIENT_ID, + clientSecret: process.env.GITHUB_CLIENT_SECRET, + callbackURL: "/api/auth/github/callback", + }, + async (accessToken, refreshToken, profile, done) => { + try { + // If GitHub doesn't return a public email, fall back to a dummy identifier string + const userEmail = profile.emails?.[0]?.value || `${profile.username}@github.oauth`; + let user = await User.findOne({ email: userEmail }); + + if (!user) { + user = new User({ + username: profile.username, + email: userEmail, + password: "OAUTH_USER_EXTERNAL_PROVIDER" + }); + await user.save(); + } + + return done(null, { + id: user._id.toString(), + username: user.username, + email: user.email + }); + } catch (err) { + return done(err); + } + } + ) +); + // Serialize user (store user info in session) passport.serializeUser((user, done) => { done(null, user.id); diff --git a/backend/models/User.js b/backend/models/User.js index aeb09514..44772cbd 100644 --- a/backend/models/User.js +++ b/backend/models/User.js @@ -19,6 +19,9 @@ const UserSchema = new mongoose.Schema({ }); UserSchema.pre('save', async function () { + if (this.password === "OAUTH_USER_EXTERNAL_PROVIDER") { + return; + } if (!this.isModified('password')) return; @@ -28,6 +31,9 @@ UserSchema.pre('save', async function () { // Compare passwords during login UserSchema.methods.comparePassword = async function (enteredPassword) { + if (this.password === "OAUTH_USER_EXTERNAL_PROVIDER") { + return false; + } return await bcrypt.compare(enteredPassword, this.password); }; diff --git a/backend/package.json b/backend/package.json index 38e15b8b..4e980c12 100644 --- a/backend/package.json +++ b/backend/package.json @@ -6,7 +6,6 @@ "dev": "nodemon server.js", "start": "node server.js", "test": "jasmine spec/**/*.spec.cjs" - }, "keywords": [], "author": "", @@ -15,12 +14,15 @@ "dependencies": { "bcryptjs": "^2.4.3", "body-parser": "^1.20.3", + "connect-mongo": "^6.0.0", "cors": "^2.8.5", "dotenv": "^16.4.5", - "express": "^4.21.1", + "express": "^4.22.2", "express-session": "^1.18.1", - "mongoose": "^8.8.2", + "mongoose": "^8.24.0", "passport": "^0.7.0", + "passport-github2": "^0.1.12", + "passport-google-oauth20": "^2.0.0", "passport-local": "^1.0.0", "zod": "^4.4.3" }, diff --git a/backend/routes/auth.js b/backend/routes/auth.js index 7c2cda78..ee7651c4 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -47,4 +47,24 @@ router.get("/logout", (req, res) => { }); }); +router.get("/google", passport.authenticate("google", { scope: ["profile", "email"] })); + +router.get( + "/google/callback", + passport.authenticate("google", { failureRedirect: "/login" }), + (req, res) => { + res.redirect(process.env.FRONTEND_URL || "http://localhost:5173"); + } +); + +router.get("/github", passport.authenticate("github", { scope: ["user:email"] })); + +router.get( + "/github/callback", + passport.authenticate("github", { failureRedirect: "/login" }), + (req, res) => { + res.redirect(process.env.FRONTEND_URL || "http://localhost:5173"); + } +); + module.exports = router; diff --git a/backend/server.js b/backend/server.js index 3f19f00b..d2db6958 100644 --- a/backend/server.js +++ b/backend/server.js @@ -5,7 +5,7 @@ const passport = require('passport'); const bodyParser = require('body-parser'); require('dotenv').config(); const cors = require('cors'); - +const MongoStore = require('connect-mongo'); // Passport configuration require('./config/passportConfig'); @@ -20,6 +20,13 @@ app.use(session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, + store: MongoStore.create({ + mongoUrl: process.env.MONGO_URI, + collectionName: 'sessions' + }), + cookie: { + maxAge: 1000 * 60 * 60 * 24 + } })); app.use(passport.initialize()); app.use(passport.session()); diff --git a/src/pages/Login/Login.tsx b/src/pages/Login/Login.tsx index e77ee3b5..8a8f2a73 100644 --- a/src/pages/Login/Login.tsx +++ b/src/pages/Login/Login.tsx @@ -126,6 +126,20 @@ const Login: React.FC = () => { > {isLoading ? "Signing in..." : "Sign In"} +
+ + Continue with Google + + + Continue with GitHub + +
{/* Message */} diff --git a/src/pages/Signup/Signup.tsx b/src/pages/Signup/Signup.tsx index b55df05d..20f3327c 100644 --- a/src/pages/Signup/Signup.tsx +++ b/src/pages/Signup/Signup.tsx @@ -209,6 +209,20 @@ const SignUp: React.FC = () => { > {isLoading ? "Creating account..." : "Create Account"} +
+ + Continue with Google + + + Continue with GitHub + +
{message && ( From 5b1ded9d5db4e64dfceca0d4ce367d966b80dd3e Mon Sep 17 00:00:00 2001 From: Soumadip Sen Date: Wed, 20 May 2026 20:29:06 +0530 Subject: [PATCH 2/2] chore: address CodeRabbit security review recommendations and harden session cookies --- backend/config/passportConfig.js | 2 ++ backend/models/User.js | 11 ++++++++++- backend/routes/auth.js | 8 ++++++-- backend/server.js | 5 ++++- 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/backend/config/passportConfig.js b/backend/config/passportConfig.js index 4382db8a..97f629ae 100644 --- a/backend/config/passportConfig.js +++ b/backend/config/passportConfig.js @@ -37,6 +37,7 @@ passport.use( clientID: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, callbackURL: "/api/auth/google/callback", + state: true }, async (accessToken, refreshToken, profile, done) => { try { @@ -73,6 +74,7 @@ passport.use( clientID: process.env.GITHUB_CLIENT_ID, clientSecret: process.env.GITHUB_CLIENT_SECRET, callbackURL: "/api/auth/github/callback", + state: true }, async (accessToken, refreshToken, profile, done) => { try { diff --git a/backend/models/User.js b/backend/models/User.js index 44772cbd..6b4eb037 100644 --- a/backend/models/User.js +++ b/backend/models/User.js @@ -14,7 +14,16 @@ const UserSchema = new mongoose.Schema({ }, password: { type: String, - required: true, + required: false, + }, + authProvider: { + type: String, + enum: ["local", "google", "github"], + default: "local", + }, + providerId: { + type: String, + default: null, }, }); diff --git a/backend/routes/auth.js b/backend/routes/auth.js index ee7651c4..b463789f 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -51,7 +51,9 @@ router.get("/google", passport.authenticate("google", { scope: ["profile", "emai router.get( "/google/callback", - passport.authenticate("google", { failureRedirect: "/login" }), + passport.authenticate("google", { + failureRedirect: `${process.env.FRONTEND_URL || "http://localhost:5173"}/login` + }), (req, res) => { res.redirect(process.env.FRONTEND_URL || "http://localhost:5173"); } @@ -61,7 +63,9 @@ router.get("/github", passport.authenticate("github", { scope: ["user:email"] }) router.get( "/github/callback", - passport.authenticate("github", { failureRedirect: "/login" }), + passport.authenticate("github", { + failureRedirect: `${process.env.FRONTEND_URL || "http://localhost:5173"}/login` + }), (req, res) => { res.redirect(process.env.FRONTEND_URL || "http://localhost:5173"); } diff --git a/backend/server.js b/backend/server.js index d2db6958..0c1769f7 100644 --- a/backend/server.js +++ b/backend/server.js @@ -25,7 +25,10 @@ app.use(session({ collectionName: 'sessions' }), cookie: { - maxAge: 1000 * 60 * 60 * 24 + maxAge: 1000 * 60 * 60 * 24, + httpOnly: true, + sameSite: 'lax', + secure: process.env.NODE_ENV === 'production' } })); app.use(passport.initialize());