oauth-2.0react-routersupabasesupabase-js

Supabase Auth UI + Google Sign in not reaching AuthProvider


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?


Solution

  • Issue

    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.

    Solution

    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,
              },
            ],
          },
        ],
      },
    ]);