-
Notifications
You must be signed in to change notification settings - Fork 134
feat: implement Google & GitHub OAuth and persist session stores via … #343
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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= |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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,78 @@ passport.use( | |
| ) | ||
| ); | ||
|
|
||
| passport.use( | ||
| new GoogleStrategy( | ||
| { | ||
| 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 { | ||
| // 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, | ||
|
Comment on lines
+50
to
+51
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handle OAuth username collisions against unique index. Line 49 and Line 85 can generate usernames that collide with existing local/OAuth users, causing duplicate-key failures on first OAuth login. Derive a deterministic unique base (provider + provider id suffix) or retry on duplicate key. Also applies to: 85-87 🤖 Prompt for AI Agents |
||
| // 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", | ||
| state: true | ||
| }, | ||
| 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); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,11 +14,23 @@ 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, | ||
| }, | ||
| }); | ||
|
|
||
| UserSchema.pre('save', async function () { | ||
| if (this.password === "OAUTH_USER_EXTERNAL_PROVIDER") { | ||
| return; | ||
| } | ||
|
Comment on lines
+31
to
+33
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replace magic-password provider detection with explicit auth provider fields. Line 22 and Line 34 key OAuth behavior off a literal password value. That makes auth mode user-input-dependent (a local user choosing this exact password gets a broken login path and plaintext persistence). Use explicit fields like Suggested direction const UserSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true },
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
+ authProvider: { type: String, enum: ["local", "google", "github"], default: "local" },
+ providerId: { type: String, default: null },
});
UserSchema.pre('save', async function () {
- if (this.password === "OAUTH_USER_EXTERNAL_PROVIDER") return;
+ if (this.authProvider !== "local") return;
if (!this.isModified('password')) return;
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
});
UserSchema.methods.comparePassword = async function (enteredPassword) {
- if (this.password === "OAUTH_USER_EXTERNAL_PROVIDER") return false;
+ if (this.authProvider !== "local") return false;
return await bcrypt.compare(enteredPassword, this.password);
};Also applies to: 34-36 🤖 Prompt for AI Agents |
||
| if (!this.isModified('password')) | ||
| return; | ||
|
|
||
|
|
@@ -28,6 +40,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); | ||
| }; | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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,16 @@ 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, | ||
| httpOnly: true, | ||
| sameSite: 'lax', | ||
| secure: process.env.NODE_ENV === 'production' | ||
| } | ||
|
Comment on lines
+23
to
+32
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, let's locate and examine backend/server.js
find . -name "server.js" -type f | head -20Repository: GitMetricsLab/github_tracker Length of output: 92 🏁 Script executed: # Let's check the directory structure to understand the project layout
git ls-files | grep -E "(backend|server)" | head -30Repository: GitMetricsLab/github_tracker Length of output: 355 🏁 Script executed: # Search for session configuration in the codebase
rg "session\(" -A 10 --max-count 5Repository: GitMetricsLab/github_tracker Length of output: 1827 🏁 Script executed: # Search for trust proxy configuration
rg "trust proxy" -iRepository: GitMetricsLab/github_tracker Length of output: 54 🏁 Script executed: # Search for cookie configuration patterns
rg "cookie\s*:" -B 2 -A 5Repository: GitMetricsLab/github_tracker Length of output: 381 🏁 Script executed: # Read the full backend/server.js file
cat -n backend/server.jsRepository: GitMetricsLab/github_tracker Length of output: 1614 🏁 Script executed: # Check Dockerfile configurations to understand deployment context
cat -n backend/Dockerfile.prodRepository: GitMetricsLab/github_tracker Length of output: 659 🏁 Script executed: # Check environment setup in package.json
cat -n backend/package.json | head -30Repository: GitMetricsLab/github_tracker Length of output: 996 🏁 Script executed: # Search for any proxy-related configuration in the codebase
rg "proxy" -i -B 2 -A 2Repository: GitMetricsLab/github_tracker Length of output: 54 🏁 Script executed: # Check if NODE_ENV is used anywhere in the backend
rg "NODE_ENV" backend/Repository: GitMetricsLab/github_tracker Length of output: 54 Harden session cookie settings for production. The session cookie configuration at lines 27-29 sets only Proposed fix+app.set("trust proxy", 1);
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
+ maxAge: 1000 * 60 * 60 * 24,
+ httpOnly: true,
+ sameSite: "lax",
+ secure: process.env.NODE_ENV === "production",
}
}));🤖 Prompt for AI Agents |
||
| })); | ||
| app.use(passport.initialize()); | ||
| app.use(passport.session()); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: GitMetricsLab/github_tracker
Length of output: 105
🏁 Script executed:
# Get the line count first wc -l backend/config/passportConfig.jsRepository: GitMetricsLab/github_tracker
Length of output: 109
🏁 Script executed:
Repository: GitMetricsLab/github_tracker
Length of output: 2593
🏁 Script executed:
Repository: GitMetricsLab/github_tracker
Length of output: 54
🏁 Script executed:
Repository: GitMetricsLab/github_tracker
Length of output: 2157
🏁 Script executed:
Repository: GitMetricsLab/github_tracker
Length of output: 208
🏁 Script executed:
Repository: GitMetricsLab/github_tracker
Length of output: 54
🏁 Script executed:
Repository: GitMetricsLab/github_tracker
Length of output: 164
🏁 Script executed:
# Read the User model file cat -n backend/models/User.jsRepository: GitMetricsLab/github_tracker
Length of output: 1309
🏁 Script executed:
Repository: GitMetricsLab/github_tracker
Length of output: 54
Guard email extraction in Google OAuth strategy before dereferencing array.
Line 44 and line 50 directly access
profile.emails[0].valuewithout checking if the array exists. If Google returns a profile without the emails field, this throws a TypeError and crashes the callback. The GitHub strategy already handles this correctly with optional chaining. Add null-safe extraction and reject auth when email is missing.Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents