Champetaman
Champetaman

Reputation: 3

How to win race condition between the rendering and when the session becomes available?

I am implementing authentication for my web app using AWS Cognito. The issue I’m facing seems to be a race condition between the time my authentication state is resolved and when the page tries to render protected routes. When a user signs in, they are being redirected to the /dashboard page, however the app does not recognize the user session and it returns to the /auth/signin page.

If I comment the redirection to the /signin page the page doesn’t load correctly, it's blank. However, after refreshing the page, it loads perfectly fine with the user authenticated.

My Setup: Next.js version: 14.2.13 AWS Cognito for authentication React 18.x with server-side rendering

Relevant Log:

User authenticated successfully: {username: 'a9be14a8-c061-702e-46b4-7e25037896d7', ...}
cognitoActions.ts:115 going to: /dashboard
page.tsx:46 User not authenticated, redirecting to /auth/signin
page.tsx:23 Auth loading state: false
page.tsx:24 Auth user state: null
page.tsx:25 Current path: /dashboard

App screenshot

I already tried:

I expect the user be redirected to the /dashboard page after signing in, and the page should load properly without needing a refresh. The authentication state should be fully resolved before attempting to load protected routes.

Here is my current Layout code:

import "./globals.css";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import ConfigureAmplifyClientSide from "./amplify-cognito-config";
import ClientAuth from "./(components)/ClientAuth/page";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  //metadata code
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <ConfigureAmplifyClientSide />
        <ClientAuth>{children}</ClientAuth>
      </body>
    </html>
  );
}

Here is my current ClientAuth component:

"use client";

import { usePathname, redirect } from "next/navigation";
import useAuthUser from "@/app/hooks/use-auth-user";
import DashboardWrapper from "@/app/dashboardWrapper";
import React, { useEffect, useMemo } from "react";

export default function ClientAuth({
  children,
}: {
  children: React.ReactNode;
}) {
  const { user, loading } = useAuthUser(); // Fetch user and loading state
  const pathname = usePathname(); // Get the current route

  const authRoutes = useMemo(
    () => ["/auth/signin", "/auth/signup", "/auth/confirm-signup"],
    []
  );

  // Add logging for debugging purposes
  useEffect(() => {
    console.log("Auth loading state:", loading); // Check if still loading
    console.log("Auth user state:", user); // Check if the user is properly loaded
    console.log("Current path:", pathname); // Check the current route

    // Wait for loading to complete before redirecting
    if (!loading) {
      if (!user && !authRoutes.includes(pathname)) {
        console.log(
          "Redirecting to /auth/signin because user is not authenticated"
        );
        redirect("/auth/signin");
      }
    }
  }, [loading, user, pathname]);

  // While loading, return a loader
  if (loading) {
    console.log("Loading authentication state...");
    return <p>Loading...</p>; // Show a loading state while the session is being checked
  }

  // After loading, if no user and trying to access non-auth routes, redirect to signin
  if (!user && !authRoutes.includes(pathname)) {
    console.log("User not authenticated, redirecting to /auth/signin");
    return null; // Prevent rendering anything while redirecting
  }

  // If user is authenticated and not on auth routes, render the dashboard layout
  if (user && !authRoutes.includes(pathname)) {
    console.log("Rendering DashboardWrapper");
    return <DashboardWrapper>{children}</DashboardWrapper>;
  }

  // For authentication routes, render children without wrapping in DashboardWrapper
  return <>{children}</>;
}

Here is my current use-auth-user.ts hook:

"use client";

import {
  fetchAuthSession,
  fetchUserAttributes,
  getCurrentUser,
} from "aws-amplify/auth";
import { useEffect, useState } from "react";

export default function useAuthUser() {
  const [user, setUser] = useState<Record<string, any> | null>(null);
  const [loading, setLoading] = useState(true); // Loading until auth state is fully checked

  useEffect(() => {
    let isMounted = true;

    async function getUser() {
      try {
        // Fetch session first
        const session = await fetchAuthSession();

        if (!session?.tokens) {
          console.log("No session tokens found, setting user as null");
          if (isMounted) setUser(null); // Ensure we clear the user state if no session exists
          return;
        }

        // Fetch current user data and attributes
        const currentUser = await getCurrentUser();
        const userAttributes = await fetchUserAttributes();

        // Construct user object with attributes
        const user = {
          ...currentUser,
          ...userAttributes,
          isAdmin: false,
        };

        const groups = session.tokens.accessToken.payload["cognito:groups"];
        // @ts-ignore
        if (groups && groups.includes("Admins")) {
          user.isAdmin = true;
        }

        if (isMounted) {
          setUser(user);
        }
      } catch (error) {
        console.error("Error fetching user session:", error);
        if (isMounted) setUser(null); // Ensure we set user to null on error
      } finally {
        if (isMounted) setLoading(false); // Stop loading regardless of success or failure
      }
    }

    getUser();

    // Clean up to avoid memory leaks
    return () => {
      isMounted = false;
    };
  }, []);

  return { user, loading };
}

Upvotes: 0

Views: 59

Answers (0)

Related Questions