So i am trying to sign in my user(a mentor in this case) using oAuth. I am using supabase for auth with a local postgres database and zustand for state management. The user is supposed to be navigated to a protected route called "/dashboard" after a successful login, the routes are protected by a wrapper that checks whether the auth state has isAuthenticated set to true or not which happens if a user is passed to it.
This is my signIn.tsx:
import { useState } from 'react';
import { Link } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { useNavigate } from 'react-router-dom';
import axios from 'axios';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { Mail, Lock, Loader2 } from 'lucide-react';
import { supabase } from "@/lib/supabase";
import { useAuthStore } from '@/store/authStore';
import { FcGoogle } from "react-icons/fc";
import { FaLinkedin } from "react-icons/fa";
export default function SignIn() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [isLoading, setIsLoading] = useState(false);
const setUser = useAuthStore((state) => state.setUser);
const navigate = useNavigate();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
try {
const { data } = await axios.post("http://localhost:5000/auth/signin", {
email,
password,
});
setUser(data.mentor); // Store mentor data in Zustand
navigate("/dashboard");
} catch (error: any) {
alert(error.response?.data?.error || "Sign-in failed");
} finally {
setIsLoading(false);
}
};
const handleOAuthLogin = async (provider: "google" | "linkedin_oidc") => {
try {
setIsLoading(true);
const { data, error } = await supabase.auth.signInWithOAuth({ provider });
console.log("oAuth : ", data);
if (error) {
alert(error.message);
return;
}
// Wait for the OAuth process to complete
supabase.auth.onAuthStateChange(async (event, session) => {
if (event === "SIGNED_IN" && session?.user) {
try {
const { data: mentor } = await axios.get(`http://localhost:5000/auth/mentor/${session.user.id}`);
setUser(mentor); // Update Zustand store
navigate("/dashboard"); // Redirect to dashboard
} catch (err) {
console.error("Error fetching mentor profile:", err);
alert("Failed to fetch user profile");
}
}
});
} catch (err) {
console.error(err);
} finally {
setIsLoading(false);
}
};
return (
<div className="min-h-screen flex items-center justify-center p-4">
<div className="w-full max-w-md">
<Card className="border-none shadow-xl">
<CardHeader className="space-y-3">
<CardTitle className="text-3xl font-bold text-center">Welcome back</CardTitle>
<CardDescription className="text-center text-gray-600">
Enter your email and password to access your account
</CardDescription>
</CardHeader>
<form onSubmit={handleSubmit}>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<div className="relative">
<Mail className="absolute left-3 top-3 h-5 w-5 text-gray-400" />
<Input
id="email"
type="email"
value={email}
placeholder="[email protected]"
className="pl-10"
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<div className="relative">
<Lock className="absolute left-3 top-3 h-5 w-5 text-gray-400" />
<Input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Enter your password"
className="pl-10"
required
/>
</div>
</div>
<Button
type="submit"
className="w-full bg-blue-600 hover:bg-blue-700"
disabled={isLoading}
>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Signing in...
</>
) : (
'Sign In'
)}
</Button>
</CardContent>
</form>
<CardFooter className="flex flex-col space-y-4 text-center">
<Button
onClick={() => handleOAuthLogin("google")}
className="w-full bg-white border border-gray-300 hover:bg-gray-100 flex items-center justify-center space-x-2"
disabled={isLoading}
>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Logging in with Google...
</>
) : (
<>
<FcGoogle className="h-5 w-5" />
<span className="text-gray-700">Sign in with Google</span>
</>
)}
</Button>
<Button
onClick={() => handleOAuthLogin("linkedin_oidc")}
className="w-full bg-blue-500 hover:bg-blue-600 text-white flex items-center justify-center space-x-2"
disabled={isLoading}
>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Logging in with LinkedIn...
</>
) : (
<>
<FaLinkedin className="h-5 w-5" />
<span className="text-white">Sign in with LinkedIn</span>
</>
)}
</Button>
<div className="text-sm text-gray-600">
Don't have an account?{' '}
<Link to="/signup" className="font-semibold text-blue-600 hover:text-blue-700">
Sign up
</Link>
</div>
</CardFooter>
</Card>
</div>
</div>
);
}
In order to get the state updated in oAuth login i am waiting for the oAuthStateChange event to trigger and change to SIGNED_IN, then the api that is hit fetches the user's details from the local db based on the user id retrieved from the oAuth session:
router.get("/mentor/:id", async (req: any, res: any) => {
const { id } = req.params;
try {
const mentor = await prisma.mentorProfile.findUnique({
where: { mentor_id: id },
});
if (!mentor) return res.status(404).json({ error: "Mentor profile not found" });
return res.status(200).json(mentor);
} catch (err) {
console.error("Error fetching mentor profile:", err);
return res.status(500).json({ error: "Server error", details: err });
}
});
The issue is, this oAuthStateChange seems to be taking too much time because what happens is:
- i select the email and everything, i get the accessToken in Application section of devTools but the state is null in authState
- it redirects me to "/" for some reason
- after a while(2-3 minutes) the state also updates and user is navigated to "/dashboard"
My whole sign in process works just fine when user is logging in via email and password and not a provider.
So i am trying to sign in my user(a mentor in this case) using oAuth. I am using supabase for auth with a local postgres database and zustand for state management. The user is supposed to be navigated to a protected route called "/dashboard" after a successful login, the routes are protected by a wrapper that checks whether the auth state has isAuthenticated set to true or not which happens if a user is passed to it.
This is my signIn.tsx:
import { useState } from 'react';
import { Link } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { useNavigate } from 'react-router-dom';
import axios from 'axios';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { Mail, Lock, Loader2 } from 'lucide-react';
import { supabase } from "@/lib/supabase";
import { useAuthStore } from '@/store/authStore';
import { FcGoogle } from "react-icons/fc";
import { FaLinkedin } from "react-icons/fa";
export default function SignIn() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [isLoading, setIsLoading] = useState(false);
const setUser = useAuthStore((state) => state.setUser);
const navigate = useNavigate();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
try {
const { data } = await axios.post("http://localhost:5000/auth/signin", {
email,
password,
});
setUser(data.mentor); // Store mentor data in Zustand
navigate("/dashboard");
} catch (error: any) {
alert(error.response?.data?.error || "Sign-in failed");
} finally {
setIsLoading(false);
}
};
const handleOAuthLogin = async (provider: "google" | "linkedin_oidc") => {
try {
setIsLoading(true);
const { data, error } = await supabase.auth.signInWithOAuth({ provider });
console.log("oAuth : ", data);
if (error) {
alert(error.message);
return;
}
// Wait for the OAuth process to complete
supabase.auth.onAuthStateChange(async (event, session) => {
if (event === "SIGNED_IN" && session?.user) {
try {
const { data: mentor } = await axios.get(`http://localhost:5000/auth/mentor/${session.user.id}`);
setUser(mentor); // Update Zustand store
navigate("/dashboard"); // Redirect to dashboard
} catch (err) {
console.error("Error fetching mentor profile:", err);
alert("Failed to fetch user profile");
}
}
});
} catch (err) {
console.error(err);
} finally {
setIsLoading(false);
}
};
return (
<div className="min-h-screen flex items-center justify-center p-4">
<div className="w-full max-w-md">
<Card className="border-none shadow-xl">
<CardHeader className="space-y-3">
<CardTitle className="text-3xl font-bold text-center">Welcome back</CardTitle>
<CardDescription className="text-center text-gray-600">
Enter your email and password to access your account
</CardDescription>
</CardHeader>
<form onSubmit={handleSubmit}>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<div className="relative">
<Mail className="absolute left-3 top-3 h-5 w-5 text-gray-400" />
<Input
id="email"
type="email"
value={email}
placeholder="[email protected]"
className="pl-10"
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<div className="relative">
<Lock className="absolute left-3 top-3 h-5 w-5 text-gray-400" />
<Input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Enter your password"
className="pl-10"
required
/>
</div>
</div>
<Button
type="submit"
className="w-full bg-blue-600 hover:bg-blue-700"
disabled={isLoading}
>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Signing in...
</>
) : (
'Sign In'
)}
</Button>
</CardContent>
</form>
<CardFooter className="flex flex-col space-y-4 text-center">
<Button
onClick={() => handleOAuthLogin("google")}
className="w-full bg-white border border-gray-300 hover:bg-gray-100 flex items-center justify-center space-x-2"
disabled={isLoading}
>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Logging in with Google...
</>
) : (
<>
<FcGoogle className="h-5 w-5" />
<span className="text-gray-700">Sign in with Google</span>
</>
)}
</Button>
<Button
onClick={() => handleOAuthLogin("linkedin_oidc")}
className="w-full bg-blue-500 hover:bg-blue-600 text-white flex items-center justify-center space-x-2"
disabled={isLoading}
>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Logging in with LinkedIn...
</>
) : (
<>
<FaLinkedin className="h-5 w-5" />
<span className="text-white">Sign in with LinkedIn</span>
</>
)}
</Button>
<div className="text-sm text-gray-600">
Don't have an account?{' '}
<Link to="/signup" className="font-semibold text-blue-600 hover:text-blue-700">
Sign up
</Link>
</div>
</CardFooter>
</Card>
</div>
</div>
);
}
In order to get the state updated in oAuth login i am waiting for the oAuthStateChange event to trigger and change to SIGNED_IN, then the api that is hit fetches the user's details from the local db based on the user id retrieved from the oAuth session:
router.get("/mentor/:id", async (req: any, res: any) => {
const { id } = req.params;
try {
const mentor = await prisma.mentorProfile.findUnique({
where: { mentor_id: id },
});
if (!mentor) return res.status(404).json({ error: "Mentor profile not found" });
return res.status(200).json(mentor);
} catch (err) {
console.error("Error fetching mentor profile:", err);
return res.status(500).json({ error: "Server error", details: err });
}
});
The issue is, this oAuthStateChange seems to be taking too much time because what happens is:
- i select the email and everything, i get the accessToken in Application section of devTools but the state is null in authState
- it redirects me to "/" for some reason
- after a while(2-3 minutes) the state also updates and user is navigated to "/dashboard"
My whole sign in process works just fine when user is logging in via email and password and not a provider.
Share Improve this question asked Mar 18 at 8:12 kb456kb456 13 bronze badges1 Answer
Reset to default 0Fixing OAuth Authentication Flow in React with Supabase The main problem seems to be in how you're handling the OAuth flow and authentication state changes. Here's what I see:
In your handleOAuthLogin function, you're setting up the OAuth state change listener after initiating the sign-in process. This is problematic because:
The auth state change might happen before your listener is registered You're not cleaning up the listener, which can cause multiple listeners to accumulate
The redirect to "/" is likely happening because the OAuth flow completes, but your application hasn't yet processed the authenticated state.
Here's how I would fix this:
// Set up the auth state listener once, outside of the handleOAuthLogin function // Add this in a useEffect at the component level
supabase.auth.onAuthStateChange(async (event, session) => { if (event === "SIGNED_IN" && session?.user) { try { const { data: mentor } = await axios.get(`http://localhost:5000/auth/mentor/${session.user.id}`); setUser(mentor); navigate("/dashboard"); } catch (err) { console.error("Error fetching mentor profile:", err); alert("Failed to fetch user profile"); } } }); // Clean up the listener when component unmounts return () => { authListener.subscription.unsubscribe(); }; }, []);`
// Then modify your handleOAuthLogin function to be simpler
"linkedin_oidc") => { try { setIsLoading(true); const { error } = await supabase.auth.signInWithOAuth({ provider, options: { redirectTo: window.location.origin // Ensure it redirects back to your app } }); if (error) { alert(error.message); } } catch (err) { console.error(err); alert("Authentication failed"); } finally { setIsLoading(false); } };` Another issue might be that you're not handling the OAuth redirect properly. The OAuth flow typically redirects the user away from your application and then back after authentication. When the user returns, you need to check if they're already authenticated. Add this to your component: > ``` > > component mounts const checkUser = async () => { > const { data: { session } } = await supabase.auth.getSession(); > if (session?.user) { > try { > const { data: mentor } = await axios.get(`http://localhost:5000/auth/mentor/${session.user.id}`); > setUser(mentor); > navigate("/dashboard"); > } catch (err) { > console.error("Error fetching mentor profile:", err); > } > } }; > checkUser(); }, []); `useEffect(() => { // Check if user is already authenticated when This approach should: Set up the auth state listener once when the component mounts Check if the user is already authenticated when they land on the page (which handles the redirect back case) Properly clean up listeners to prevent memory leaks and duplicate event handlers Don't fet to import useEffect at the top of your file: ` import { useState, useEffect } from 'react'; `