diff --git a/backend/middleware/validation.js b/backend/middleware/validation.js
index 1179197..447beeb 100644
--- a/backend/middleware/validation.js
+++ b/backend/middleware/validation.js
@@ -146,11 +146,11 @@ const validateLogin = [
// Google auth validation
const validateGoogleAuth = [
- body("idToken")
+ body("code")
.notEmpty()
- .withMessage("Google ID token is required")
- .isLength({ max: 2048 })
- .withMessage("Invalid token format"),
+ .withMessage("Authorization code is required")
+ .isLength({ max: 512 })
+ .withMessage("Invalid authorization code format"),
handleValidationErrors,
];
diff --git a/backend/routes/auth.js b/backend/routes/auth.js
index e831b35..7bb7142 100644
--- a/backend/routes/auth.js
+++ b/backend/routes/auth.js
@@ -13,7 +13,11 @@ const { csrfProtection, getCSRFToken } = require("../middleware/csrf");
const { loginLimiter, registerLimiter } = require("../middleware/rateLimiter");
const router = express.Router();
-const googleClient = new OAuth2Client(process.env.GOOGLE_CLIENT_ID);
+const googleClient = new OAuth2Client(
+ process.env.GOOGLE_CLIENT_ID,
+ process.env.GOOGLE_CLIENT_SECRET,
+ process.env.GOOGLE_REDIRECT_URI || "http://localhost:3000/auth/google/callback"
+);
// Get CSRF token endpoint
router.get("/csrf-token", (req, res) => {
@@ -214,15 +218,21 @@ router.post(
validateGoogleAuth,
async (req, res) => {
try {
- const { idToken } = req.body;
+ const { code } = req.body;
- if (!idToken) {
- return res.status(400).json({ error: "ID token is required" });
+ if (!code) {
+ return res.status(400).json({ error: "Authorization code is required" });
}
- // Verify the Google ID token
+ // Exchange authorization code for tokens
+ const { tokens } = await googleClient.getToken({
+ code,
+ redirect_uri: process.env.GOOGLE_REDIRECT_URI || "http://localhost:3000/auth/google/callback",
+ });
+
+ // Verify the ID token from the token response
const ticket = await googleClient.verifyIdToken({
- idToken,
+ idToken: tokens.id_token,
audience: process.env.GOOGLE_CLIENT_ID,
});
@@ -315,26 +325,21 @@ router.post(
// Don't send token in response body for security
});
} catch (error) {
- if (error.message && error.message.includes("Token used too late")) {
+ if (error.message && error.message.includes("invalid_grant")) {
return res
.status(401)
- .json({ error: "Google token has expired. Please try again." });
+ .json({ error: "Invalid or expired authorization code. Please try again." });
}
- if (error.message && error.message.includes("Invalid token")) {
- return res
- .status(401)
- .json({ error: "Invalid Google token. Please try again." });
- }
- if (error.message && error.message.includes("Wrong number of segments")) {
+ if (error.message && error.message.includes("redirect_uri_mismatch")) {
return res
.status(400)
- .json({ error: "Malformed Google token. Please try again." });
+ .json({ error: "Redirect URI mismatch. Please contact support." });
}
const reqLogger = logger.withRequestId(req.id);
- reqLogger.error("Google auth error", {
+ reqLogger.error("Google OAuth error", {
error: error.message,
stack: error.stack,
- tokenInfo: logger.sanitize({ idToken: req.body.idToken })
+ codePresent: !!req.body.code
});
res
.status(500)
diff --git a/frontend/public/index.html b/frontend/public/index.html
index 47f6d0e..0ba0097 100644
--- a/frontend/public/index.html
+++ b/frontend/public/index.html
@@ -19,7 +19,6 @@
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css"
/>
-
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 7f0989f..0c04ec0 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -6,6 +6,7 @@ import Footer from './components/Footer';
import Home from './pages/Home';
import Login from './pages/Login';
import Register from './pages/Register';
+import GoogleCallback from './pages/GoogleCallback';
import ItemList from './pages/ItemList';
import ItemDetail from './pages/ItemDetail';
import EditItem from './pages/EditItem';
@@ -36,6 +37,7 @@ function App() {
} />
} />
} />
+ } />
} />
} />
} />
diff --git a/frontend/src/components/AuthModal.tsx b/frontend/src/components/AuthModal.tsx
index e23dd16..e13de3b 100644
--- a/frontend/src/components/AuthModal.tsx
+++ b/frontend/src/components/AuthModal.tsx
@@ -1,4 +1,4 @@
-import React, { useState, useEffect, useRef } from "react";
+import React, { useState, useEffect, useRef, useCallback } from "react";
import { useAuth } from "../contexts/AuthContext";
import PasswordStrengthMeter from "./PasswordStrengthMeter";
@@ -20,9 +20,8 @@ const AuthModal: React.FC = ({
const [lastName, setLastName] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
- const googleButtonRef = useRef(null);
- const { login, register, googleLogin, updateUser } = useAuth();
+ const { login, register } = useAuth();
// Update mode when modal is opened with different initialMode
useEffect(() => {
@@ -31,43 +30,29 @@ const AuthModal: React.FC = ({
}
}, [show, initialMode]);
- // Initialize Google Sign-In
- useEffect(() => {
- if (show && window.google && process.env.REACT_APP_GOOGLE_CLIENT_ID) {
- try {
- window.google.accounts.id.initialize({
- client_id: process.env.REACT_APP_GOOGLE_CLIENT_ID,
- callback: handleGoogleResponse,
- auto_select: false,
- cancel_on_tap_outside: false,
- });
- } catch (error) {
- console.error("Error initializing Google Sign-In:", error);
- }
- }
- }, [show]);
+ const resetModal = () => {
+ setError("");
+ setEmail("");
+ setPassword("");
+ setFirstName("");
+ setLastName("");
+ };
- const handleGoogleResponse = async (response: any) => {
- try {
- setLoading(true);
- setError("");
+ const handleGoogleLogin = () => {
+ const clientId = process.env.REACT_APP_GOOGLE_CLIENT_ID;
+ const redirectUri = `${window.location.origin}/auth/google/callback`;
+ const scope = 'email profile';
+ const responseType = 'code';
- if (!response?.credential) {
- throw new Error("No credential received from Google");
- }
+ const googleAuthUrl = `https://accounts.google.com/o/oauth2/v2/auth?` +
+ `client_id=${encodeURIComponent(clientId || '')}` +
+ `&redirect_uri=${encodeURIComponent(redirectUri)}` +
+ `&response_type=${responseType}` +
+ `&scope=${encodeURIComponent(scope)}` +
+ `&access_type=offline` +
+ `&prompt=consent`;
- await googleLogin(response.credential);
- onHide();
- resetModal();
- } catch (err: any) {
- setError(
- err.response?.data?.error ||
- err.message ||
- "Failed to sign in with Google"
- );
- } finally {
- setLoading(false);
- }
+ window.location.href = googleAuthUrl;
};
const handleEmailSubmit = async (e: React.FormEvent) => {
@@ -96,27 +81,6 @@ const AuthModal: React.FC = ({
}
};
- const handleSocialLogin = (provider: string) => {
- if (provider === "google") {
- if (window.google) {
- try {
- window.google.accounts.id.prompt();
- } catch (error) {
- setError("Failed to open Google Sign-In. Please try again.");
- }
- } else {
- setError("Google Sign-In is not available. Please try again later.");
- }
- }
- };
-
- const resetModal = () => {
- setError("");
- setEmail("");
- setPassword("");
- setFirstName("");
- setLastName("");
- };
if (!show) return null;
@@ -226,8 +190,9 @@ const AuthModal: React.FC = ({
{/* Social Login Options */}