I'm using Vite + React Router v6 + Supabase Auth components with a Google OAuth provider. I double checked that the Google credentials are in Supabase configuration, https://{my_supabase_id}.supabase.co/auth/v1/callback
is configured in Google's client configuration, and both the Google Authorized origin and Supabase Site URL are configured to my Vite url of http://localhost:5173
.
I can see the User authenticates in Supabase Authentication tables, but on my frontend, the AuthProvider never hits the debuggers I set for when a session gets created, thus it routes back to the login screen with no session.
Here is my AuthProvider:
const AuthProvider = (props: AuthProviderProps) => {
const [user, setUser] = useState<User | null>(null);
const [session, setSession] = useState<Session | null>(null);
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
const { data: listener } = supabase.auth.onAuthStateChange(
(_event, session) => {
console.log('session onAuthStateChange: ', session);
if (session) {
debugger;
}
setSession(session);
setUser(session?.user || null);
setLoading(false);
}
);
const setData = async () => {
const {
data: { session },
error,
} = await supabase.auth.getSession();
console.log('session at setData:', session);
if (session) {
debugger;
}
if (error) {
throw error;
}
setSession(session);
setUser(session?.user || null);
setLoading(false);
};
setData();
return () => {
listener?.subscription.unsubscribe();
};
}, [supabase.auth]);
const value = {
session,
user,
};
return (
<AuthContext.Provider value={value}>{props.children}</AuthContext.Provider>
);
};
export const useAuth = () => {
return useContext(AuthContext);
};
export default AuthProvider;
I'm a little uncertain as to where exactly this Provider should go, but here are my routes:
const router = createBrowserRouter([
{
path: '/',
element: (
<AuthProvider>
<Root />
</AuthProvider>
),
errorElement: <ErrorPage />,
children: [
{
path: 'login',
element: <Login />,
},
{
path: '/',
element: <ProtectedPage />,
children: [
{
path: 'picks',
element: <Picks />,
loader: picksLoader,
},
],
},
],
},
]);
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);
And here is my ProtectedPage
component:
const ProtectedPage = () => {
const { user, session } = useAuth();
if (!session) {
return <Navigate to='/login' replace />;
}
return <Layout />;
};
So basically, at /
, it attempts to load my <ProtectedPage>
and then it routes to login
if the session is null.
For my Login, I'm using the <Auth />
component from @supabase/auth-ui-react'
:
export const Login = () => {
return (
<div className='md:flex md:justify-center mb-6'>
<div className='flex-col prose'>
<h1>bp2</h1>
<div className='items-center justify-center mx-12 h-screen'>
<Auth
supabaseClient={supabase}
appearance={{ theme: ThemeSupa }}
providers={['google']}
/>
</div>
</div>
</div>
);
};
What am I missing?
If I am understanding correctly, you have unauthenticated users that access "/"
and get redirected to "/login"
and bounced out to your auth service. Once authenticated there they get redirected back to your app to "/"
. The supabase.auth.onAuthStateChange
and supabase.auth.getSession
handlers haven't "reacted" to this authentication change yet, and so the user
and session
state is not updated. The ProtectedPage
rendered on "/"
then redirects them back to "/login"
. This is the loop you are stuck in.
Use the loading
state and/or start with initially undefined session
state to help make the UI wait until at least the initial session check completes.
const AuthProvider = (props: AuthProviderProps) => {
const [user, setUser] = useState<User | null>(null);
const [session, setSession] = useState<Session | null | undefined>(); // <-- undefined
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
const { data: listener } = supabase.auth.onAuthStateChange(
(_event, session) => {
console.log('session onAuthStateChange: ', session);
setSession(session || null);
setUser(session?.user || null);
}
);
return () => {
listener?.subscription.unsubscribe();
};
}, [supabase.auth]);
const getSession = async () => {
try {
setLoading(true);
const {
data: { session },
error,
} = await supabase.auth.getSession();
console.log('session at setData:', session);
if (session) {
debugger;
}
if (error) {
throw error;
}
setSession(session || null);
setUser(session?.user || null);
} catch(error) {
setSession(null);
setUser(null);
} finally {
setLoading(false);
}
};
const value = {
loading,
session,
user,
getSession,
};
return (
<AuthContext.Provider value={value}>
{props.children}
</AuthContext.Provider>
);
};
const ProtectedPage = () => {
const { loading, session, getSession } = useAuth();
useEffect(() => {
if (!session) {
getSession();
}
}, [session, getSession]);
if (loading || session === undefined) {
return null; // <-- or loading indicator/spinner/etc
}
return session ? <Outlet /> : <Navigate to='/login' replace />;
};
const router = createBrowserRouter([
{
path: '/',
element: (
<AuthProvider>
<Root />
</AuthProvider>
),
errorElement: <ErrorPage />,
children: [
{
path: 'login',
element: <Login />,
},
{
element: <ProtectedPage />,
children: [
{
path: 'picks',
element: <Picks />,
loader: picksLoader,
},
],
},
],
},
]);