Reputation: 3
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
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