I hope this is helpful, I got stuck on the same problem but with the help of ChatGTP and Claude AI, I was able to come across one possible solution.
I am using localhost in this example and tailwind CSS in a MERN Stack project.
-------------------------------Passport Setup--------------------------------
import passport from "passport";
import { Strategy as GoogleStrategy } from "passport-google-oauth20";
import User from "../models/user.model.js";
import dotenv from "dotenv";
dotenv.config();
// Configure Passport with a Google strategy for authentication
passport.use(
"google",
new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: "/api/auth/google/callback",
},
/**
* Verify the user's credentials using Google.
*
* This function is called by Passport when a user attempts to log in with their Google account.
* It:
* 1. Searches for a user with the provided Google ID.
* 2. If no user is found, it creates a new user with information from the Google profile.
* 3. Returns the user object.
* 4. Passes any errors to the `done` callback.
*
* @param {string} accessToken - The access token provided by Google.
* @param {string} refreshToken - The refresh token provided by Google.
* @param {Object} profile - The user's profile information from Google.
* @param {Function} done - The callback to call with the authentication result.
*/
async (accessToken, refreshToken, profile, done) => {
try {
let user = await User.findOne({ googleId: profile.id });
// Additional check to prevent duplicate accounts if Google email changes
if (!user) {
user = await User.findOne({ email: profile._json.email });
}
if (!user) {
// Generate a random password
const randomPassword = User.prototype.generateRandomPassword();
// Create a new user
user = await User.create({
googleId: profile.id,
name: profile._json.name,
email: profile._json.email,
password: randomPassword, // Set the generated password
profilePicture: profile._json.picture,
});
}
return done(null, user);
} catch (error) {
return done(error, false);
}
}
)
);
/**
* Serialize the user for the session.
*
* This function is called when a user is authenticated. It:
* 1. Takes the user object and stores the user ID in the session.
* 2. This ID is used to identify the user in subsequent requests.
*
* @param {Object} user - The authenticated user object.
* @param {Function} done - The callback to call with the serialized user ID.
*/
passport.serializeUser((user, done) => {
done(null, user.id);
});
/**
* Deserialize the user from the session.
*
* This function is called on each request to retrieve the user object based on the user ID stored in the session. It:
* 1. Finds the user by their ID.
* 2. Passes the user object to the `done` callback.
* 3. Passes any errors to the `done` callback if the user cannot be found.
*
* @param {string} id - The user ID stored in the session.
* @param {Function} done - The callback to call with the user object or an error.
*/
passport.deserializeUser(async (id, done) => {
try {
const user = await User.findById(id);
done(null, user);
} catch (err) {
done(err);
}
});
export default passport;
------------------------------- Auth Controller --------------------------------
import passport from "../lib/PassportSetup.js";
import User from "../models/user.model.js";
/**
* Initiates Google authentication.
*
* This function handles initiating the Google OAuth2 authentication process by:
* 1. Redirecting the user to Google's OAuth2 login page.
*
* @param {Object} req - The request object for initiating Google authentication.
* @param {Object} res - The response object to redirect the user to Google.
* @param {Function} next - The next middleware function in the stack.
*/
export const googleAuth = passport.authenticate("google", {
scope: ["profile", "email"],
});
/**
* Handles the callback from Google OAuth2.
*
* This function handles the callback after the user has authenticated with Google. It:
* 1. Uses Passport's 'google' strategy to authenticate the user.
* 2. Redirects the user to the home page on successful authentication.
* 3. Handles authentication errors by redirecting to the login page with an error message.
*
* @param {Object} req - The request object containing Google OAuth2 callback data.
* @param {Object} res - The response object to redirect the user.
* @param {Function} next - The next middleware function in the stack.
*/
export const googleAuthCallback = (req, res, next) => {
passport.authenticate("google", {
successRedirect: `${process.env.CLIENT_URL}/oauth/callback`,
failureRedirect: `${process.env.CLIENT_URL}/login`,
failureFlash: true,
})(req, res, next);
};
/**
* Handles successful authentication callbacks from OAuth providers.
*
* This function is triggered when a user is successfully authenticated via an OAuth provider (e.g., Google, GitHub).
* It:
* 1. Checks if a user object is present on the request, which is set by Passport after successful authentication.
* 2. Responds with a 200 status and user information if authentication is successful.
* 3. Includes the user's ID, name, email, profile picture, and role in the response.
*
* @param {Object} req - The request object, containing authenticated user data.
* @param {Object} res - The response object used to send back the authentication result.
* @param {Function} next - The next middleware function in the stack (not used in this function).
* @returns {Object} JSON object with user data on success, or an error status if authentication fails.
*/
export const authCallbackSuccess = (req, res, next) => {
return res.status(200).json({
success: true,
status: 200,
user: {
id: req.user.id,
name: req.user.name,
email: req.user.email,
profilePicture: req.user.profilePicture,
role: req.user.role,
},
});
};
------------------------------- Auth Routes --------------------------------
import express from "express";
import {
googleAuth,
googleAuthCallback,
authCallbackSuccess,
} from "../controllers/auth.controller.js";
const router = express.Router();
// Passport Google OAuth2 login
router.get("/google", googleAuth);
// Handles Passport Google OAuth2 callback
router.get("/google/callback", googleAuthCallback);
// Returns the user object after Passport Google OAuth2, Github, or any other callback
router.get("/callback/success", isAuthenticated, authCallbackSuccess);
export default router;
------------------------ React OAuthButtons.jsx -------------------------
import React from 'react';
import { useSelector } from 'react-redux';
function OAuthButtons() {
const { loading } = useSelector((state) => state.user);
const handleOAuth = (provider) => {
window.location.href = `http://localhost:4000/api/auth/${provider}`;
}
return (
<div className='flex flex-col gap-3'>
<button
className="bg-red-700 text-white rounded-lg p-3 uppercase hover:bg-red-600 disabled:bg-red-400"
type="button"
onClick={() => handleOAuth("google")}
disabled={loading}
>
Continue with Google
</button>
<button
className="bg-blue-700 text-white rounded-lg p-3 uppercase hover:bg-blue-600 disabled:bg-blue-400"
type="button"
onClick={() => handleOAuth("github")}
disabled={loading}
>
Continue with Github
</button>
</div>
);
}
export default OAuthButtons;
------------------------ React OAuthCallback.jsx -------------------------
import React, { useEffect } from 'react';
import axios from 'axios';
import { useDispatch } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import { loginStart, loginSuccess, loginFailure } from '../../redux/user/userSlice.js';
function OAuthCallback() {
const dispatch = useDispatch();
const navigate = useNavigate();
useEffect(() => {
const handleCallback = async () => {
try {
dispatch(loginStart());
const response = await axios.get(
`http://localhost:4000/api/auth/callback/success`,
{ withCredentials: true }
);
dispatch(loginSuccess({ user: response.data.user }));
navigate('/');
} catch (error) {
dispatch(loginFailure({
error: error.response?.data?.message || "Login using Google failed! Please try using email and password!"
}));
navigate('/login');
}
};
handleCallback();
}, [dispatch, navigate]);
return (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-red-700"></div>
</div>
);
}
export default OAuthCallback;
------------------------ React App.jsx -------------------------
import React from 'react';
import { BrowserRouter, Routes, Route } from "react-router-dom";
import NavigationBar from './components/Navigation/NavigationBar.jsx';
import Home from './pages/Static/Home.jsx';
import About from './pages/Static/About.jsx';
import Register from './pages/Auth/Register.jsx';
import Login from './pages/Auth/Login.jsx';
import OAuthCallback from './components/Auth/OAuthCallback.jsx';
function App() {
return (
<BrowserRouter>
<NavigationBar />
<Routes>
<Route path='/' element={<Home />} />
<Route path='/about' element={<About />} />
<Route path='/register' element={<Register />} />
<Route path='/login' element={<Login />} />
<Route path="/oauth/callback" element={<OAuthCallback />} />
</Routes>
</BrowserRouter>
)
}
export default App;
------------------------ React (Sample Implementation) Login.jsx -------------------------
import React, { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import axios from "axios";
import { useDispatch, useSelector } from 'react-redux';
import { loginStart, loginSuccess, loginFailure } from '../../redux/user/userSlice.js';
import OAuthButtons from '../../components/Auth/OAuthButtons.jsx';
function Login() {
const [formDate, setFormData] = useState({
email: "",
password: "",
});
const navigate = useNavigate();
const dispatch = useDispatch();
const { loading, error } = useSelector((state) => state.user);
const handleChange = (e) => {
setFormData({ ...formDate, [e.target.id]: e.target.value });
}
const handleSubmit = async (e) => {
e.preventDefault();
try {
dispatch(loginStart());
const response = await axios.post("http://localhost:4000/api/auth/login", formDate, { withCredentials: true });
const data = await response.data;
dispatch(loginSuccess({ user: data.user }));
navigate("/profile");
} catch (error) {
dispatch(loginFailure({ error: error.response?.data?.message || "An unexpected error occurred. Please try again." }));
}
}
return (
<div className='p-3 max-w-lg mx-auto'>
<h1 className='text-3xl text-center font-semibold my-7'>Login</h1>
<form onSubmit={handleSubmit} className='flex flex-col gap-4'>
<input type="email" placeholder='Email' id='email' className='bg-slate-100 p-3 rounded-lg' onChange={handleChange} />
<input type="password" placeholder='Password' id='password' className='bg-slate-100 p-3 rounded-lg' onChange={handleChange} />
<button type='submit' disabled={loading} className='bg-slate-700 text-white p-3 rounded-lg uppercase hover:opacity-95 disabled:opacity-75 cursor-pointer'>
{loading ? "Loading..." : "Login"}
</button>
<div className='border-b'></div>
<OAuthButtons />
</form>
<div className='flex gap-2 mt-5'>
<p>Don't an account?</p>
<span className='text-blue-500'><Link to={"/register"}>Register</Link></span>
</div>
<div>
<p className='text-red-700'>{error}</p>
</div>
</div>
)
}
export default Login;