next.jsamazon-cognitogoogle-signinlogout

AWS Cognito Google Sign-in with Next.js works, but Sign-out does not fully log the user out


I’m using AWS Cognito with Google as an Identity Provider in a Next.js app. I am using AWS managed login as well. Sign-in works correctly, tokens are received, and the user session persists. However, when I call the sign-out URL, the browser redirects but the user is not fully logged out. When I click "Sign in with Google" again, it appears Google still thinks I'm authenticated and was never logged out.

Adding the param prompt=select_account to the login url allows me to choose another account and the logout appears to work but when I remove that from the string I am automatically signed back into the previous account why? Here is my sign-in and sign-out helper code:

export const getCognitoAuthUrl = () => {
  const domain = process.env.NEXT_PUBLIC_COGNITO_DOMAIN!;
  const clientId = process.env.NEXT_PUBLIC_COGNITO_CLIENT_ID!;
  const redirectUri = process.env.NEXT_PUBLIC_REDIRECT_URI!;

  return `${domain}/login?client_id=${clientId}&response_type=code&scope=email+openid+profile&redirect_uri=${encodeURIComponent(
    redirectUri
  )}&prompt=select_account`;
};

export const signOut = () => {
  const domain = process.env.NEXT_PUBLIC_COGNITO_DOMAIN!;
  const clientId = process.env.NEXT_PUBLIC_COGNITO_CLIENT_ID!;
  const logoutUri = process.env.NEXT_PUBLIC_LOGOUT_URI!;
  
  localStorage.removeItem('accessToken');
  localStorage.removeItem('userId');
  localStorage.removeItem('userInfo');
  
  window.location.href = `${domain}/logout?client_id=${clientId}&logout_uri=${encodeURIComponent(logoutUri)}`;
};

"use client";

import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { useAuth } from "@/hooks/useAuth";
import HomeContent from "@/components/home/HomeContent";

export default function Home() {
  const { user, isLoaded } = useAuth();
  const router = useRouter();

  useEffect(() => {
    const handleAuthCallback = async () => {
      const urlParams = new URLSearchParams(window.location.search);
      const code = urlParams.get("code");

      if (code) {
        try {
          const response = await fetch("/api/auth/callback", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ code })
          });

          const data = await response.json();
          if (data.success) {
            localStorage.setItem("accessToken", data.accessToken);
            localStorage.setItem("userId", data.userId);
            localStorage.setItem("userInfo", JSON.stringify(data.userInfo));
            window.history.replaceState({}, document.title, "/");
            window.location.reload(); // Reload to update auth state
          } else {
            router.push("/login");
          }
        } catch (error) {
          console.error("Auth callback error:", error);
          router.push("/login");
        }
      }
    };

    handleAuthCallback();
  }, [router]);

  // Show loading while checking auth
  if (!isLoaded) {
    return (
      <div className="flex items-center justify-center min-h-screen">
        <div className="text-center">
          <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto mb-4"></div>
          <p className="text-gray-600">Loading...</p>
        </div>
      </div>
    );
  }

  return <HomeContent user={user} />;
}

import { NextRequest, NextResponse } from "next/server";

export async function POST(request: NextRequest) {
  try {
    const { code } = await request.json();
    console.log("Received code:", code);

    if (!code) {
      return NextResponse.json({ success: false, error: "No code provided" });
    }

    // Exchange authorization code for tokens
    const tokenResponse = await fetch(
      `${process.env.NEXT_PUBLIC_COGNITO_DOMAIN}/oauth2/token`,
      {
        method: "POST",
        headers: {
          "Content-Type": "application/x-www-form-urlencoded"
        },
        body: new URLSearchParams({
          grant_type: "authorization_code",
          client_id: process.env.NEXT_PUBLIC_COGNITO_CLIENT_ID!,
          client_secret: process.env.COGNITO_CLIENT_SECRET!,
          code,
          redirect_uri: process.env.NEXT_PUBLIC_REDIRECT_URI!
        })
      }
    );

    const tokens = await tokenResponse.json();
    console.log("Token response:", tokens);

    if (!tokens.access_token) {
      console.error("No access token received:", tokens);
      return NextResponse.json({
        success: false,
        error: "Failed to get tokens",
        details: tokens
      });
    }

    // Get user info
    const userResponse = await fetch(
      `${process.env.NEXT_PUBLIC_COGNITO_DOMAIN}/oauth2/userInfo`,
      {
        headers: {
          Authorization: `Bearer ${tokens.access_token}`
        }
      }
    );

    const userInfo = await userResponse.json();
    console.log("User info:", userInfo);

    return NextResponse.json({
      success: true,
      accessToken: tokens.access_token,
      userId: userInfo.sub,
      userInfo
    });
  } catch (error) {
    console.error("Auth callback error:", error);
    return NextResponse.json({
      success: false,
      error: "Authentication failed",
      details: error.message
    });
  }
}

The OAuth callback handling works fine, tokens are received, user info loads, etc. The issue is specifically logout not invalidating the federated session.

My redirect environment variables:

NEXT_PUBLIC_REDIRECT_URI=http://localhost:3000
NEXT_PUBLIC_LOGOUT_URI=http://localhost:3000

What is happening:


Question: How do I properly log the user out so the Google login dialog appears again, rather than Cognito silently re-authenticating with the existing Google session?

Do I need to change the logout_uri, or explicitly revoke the Google session as well?

Any guidance would be appreciated.


Solution

  • 
    
    export const getCognitoAuthUrl = () => {
      const domain = process.env.NEXT_PUBLIC_COGNITO_DOMAIN!;
      const clientId = process.env.NEXT_PUBLIC_COGNITO_CLIENT_ID!;
      const redirectUri = process.env.NEXT_PUBLIC_REDIRECT_URI!;
    
      return `${domain}/login?client_id=${clientId}&response_type=code&scope=email+openid+profile&redirect_uri=${encodeURIComponent(
        redirectUri
      )}&prompt=select_account`;
    };
    
    export const signOut = () => {
      const domain = process.env.NEXT_PUBLIC_COGNITO_DOMAIN!;
      const clientId = process.env.NEXT_PUBLIC_COGNITO_CLIENT_ID!;
      const logoutUri = process.env.NEXT_PUBLIC_LOGOUT_URI!;
      
      localStorage.removeItem('accessToken');
      localStorage.removeItem('userId');
      localStorage.removeItem('userInfo');
      
      window.location.href = `${domain}/logout?client_id=${clientId}&logout_uri=${encodeURIComponent(logoutUri)}&federated=true`;
    };
    
    

    After much struggle I was able to fix it with the following code. Adding the federated=true param to the query string of the logout uri.