From 052781a0e6be7b435089d39dda4fe557afd95619 Mon Sep 17 00:00:00 2001 From: jackiettran <41605212+jackiettran@users.noreply.github.com> Date: Wed, 8 Oct 2025 12:46:25 -0400 Subject: [PATCH] google sign in with oauth 2.0. no more console errors or warnings --- backend/middleware/validation.js | 8 +-- backend/routes/auth.js | 39 ++++++----- frontend/public/index.html | 1 - frontend/src/App.tsx | 2 + frontend/src/components/AuthModal.tsx | 83 +++++++---------------- frontend/src/contexts/AuthContext.tsx | 34 +++++++--- frontend/src/pages/GoogleCallback.tsx | 98 +++++++++++++++++++++++++++ frontend/src/services/api.ts | 14 +++- 8 files changed, 186 insertions(+), 93 deletions(-) create mode 100644 frontend/src/pages/GoogleCallback.tsx 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 */} + + ) : null} + + + + + + ); +}; + +export default GoogleCallback; diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 88d6398..8b906c1 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -58,10 +58,12 @@ api.interceptors.request.use(async (config) => { if (["POST", "PUT", "DELETE", "PATCH"].includes(method)) { // If we don't have a CSRF token yet, try to fetch it if (!csrfToken) { - // Skip fetching for auth endpoints to avoid circular dependency + // Skip fetching for most auth endpoints to avoid circular dependency + // Exception: /auth/google needs CSRF token and should auto-fetch as fallback const isAuthEndpoint = config.url?.includes("/auth/") && - !config.url?.includes("/auth/refresh"); + !config.url?.includes("/auth/refresh") && + !config.url?.includes("/auth/google"); if (!isAuthEndpoint) { await fetchCSRFToken(); } @@ -90,6 +92,12 @@ api.interceptors.response.use( errorData?.code === "CSRF_TOKEN_MISMATCH" && !originalRequest._csrfRetry ) { + // Don't retry OAuth endpoints - the authorization code is single-use + const isOAuthEndpoint = originalRequest.url?.includes("/auth/google"); + if (isOAuthEndpoint) { + return Promise.reject(error); + } + originalRequest._csrfRetry = true; // Try to fetch a new CSRF token and retry @@ -153,7 +161,7 @@ api.interceptors.response.use( export const authAPI = { register: (data: any) => api.post("/auth/register", data), login: (data: any) => api.post("/auth/login", data), - googleLogin: (data: any) => api.post("/auth/google", data), + googleLogin: (code: string) => api.post("/auth/google", { code }), logout: () => api.post("/auth/logout"), refresh: () => api.post("/auth/refresh"), getCSRFToken: () => api.get("/auth/csrf-token"),