DEV Community

Cover image for Implementing Federated Sign-Out with Auth.js in Next.js 14 App Router
Aryaman Sharma
Aryaman Sharma

Posted on

Implementing Federated Sign-Out with Auth.js in Next.js 14 App Router

In this blog post, we'll explore how to implement federated sign-out using Auth.js (formerly NextAuth.js) in a Next.js 14 application.

We'll be using an OpenID Connect standardized Duende server as our authentication provider.

The Challenge

Auth.js doesn't provide built-in support for federated sign-out as defined by the OpenID Connect standard. This means we need to implement it manually to ensure our users are properly signed out from both the client application and the identity provider.

Understanding Federated Sign-Out

When a user is logged in via an OAuth client, a federated sign-out involves logging the user out from two places:

  1. The identity provider (IdP)
  2. The client application Federated sign-out aims to log the user out of both sessions simultaneously. Authjs's signOut() function only addresses the client-side session, leaving them logged in on the provider's side. ## Solution Approach:

Our solution involves:

  1. Custom Sign-out Button: Initiates the federated sign-out process.
  2. api/auth/federated-logout Route: Returns the URL for redirection for provider sign-out.
  3. federatedLogout Function: Handles the overall process.
  4. Sign out Page Component: Cleans up the client-side session upon user return.

Image description


Implementing solution

1. Enabling Access to id_token_hint:

Auth.js doesn't directly provide the original id_token from the provider, you need to expose the ID token received from the provider so that it can be used during the sign-out process. We can achieve this by modifying our Auth.js configuration:


//Changes to callbacks
callbacks: {
  session({ session, token }) {
    session.user.idToken = token.idToken;  // Attach the id_token from the token to the session object
    return session;
  },
  jwt: async ({ token, user, trigger, session }) => {
    if (user) {
      token.idToken = user.idToken; //extract and store the id_token
    }
    return token;
  },
},

// ...

const providers: Provider[] = [
  DuendeIDS6Provider({
    clientId: env.DUENDE_IDS6_ID,
    clientSecret: env.DUENDE_IDS6_SECRET,
    issuer: env.DUENDE_IDS6_ISSUER,
    profile(profile, tokens) {
      return { ...profile,  
       idToken: tokens.id_token };// Store the id_token in the session for future use.
    },
  }),
];
Enter fullscreen mode Exit fullscreen mode
  • Profile Callback: Adds the id_token from the OpenID provider to the user object during login.
  • JWT Callback: Ensures that the id_token is stored inside the token object.
  • Session Callback: Exposes the id_token in the user session, making it available client-side for the federated sign-out process.

These changes ensure that the original ID token is stored in the session and can be accessed when needed for the federated logout process.

2. Sign out button

Now let's create a custom sign-out button that triggers our federated logout process:

// components/signoutButton.tsx

import { federatedLogout } from "@/lib/utils";

const NavbarProfile = () => {

  return (
    // ... 
    <button onClick={federatedLogout}>Logout</button>
    // ...
  );
};
Enter fullscreen mode Exit fullscreen mode

Here, federatedLogout() is the function that triggers the federated sign-out process.

3. Implement the Federated Logout Function

Next, we'll create a federatedLogout function that initiates the sign-out process:

// lib/utils.ts

export async function federatedLogout() {
  try {
    const response = await fetch("/api/auth/federated-logout");
    const data = await response.json();
    if (response.ok) {
      window.location.href = data.url;
      return;
    }
  } catch (error) {
    console.log(error);
    window.location.href = "/";
  }
}
Enter fullscreen mode Exit fullscreen mode

The federatedLogout() function makes a request to our API to get the provider's end session URL and then redirects the user to that URL.

4. Create the Federated Logout API Route (/api/auth/federated-logout)

This API route builds the end session URL, which will log the user out of the OpenID provider. It uses the user's ID token and a post_logout_redirect_uri

// app/api/auth/federated-logout/route.ts

import { auth } from "@/auth";
import { env } from "@/env";
import { DEFAULT_LOGIN_REDIRECT_URL } from "@/routes";

export async function GET() {
  let redirectPath: string = DEFAULT_LOGIN_REDIRECT_URL;
  try {
    const session = await auth();
    if (session) {
      const endSessionURL = new URL(
        `${env.DUENDE_IDS6_ISSUER}connect/endsession`
      );
      const redirectURL = `https://${productionURL}/auth/signout`;
      const endSessionParams = new URLSearchParams({
        id_token_hint: session.user.idToken,
        post_logout_redirect_uri: redirectURL,
      });
      const redirectPath = `${endSessionURL}?${endSessionParams.toString()}`;
    }
  } catch (error) {
    console.error(error);
  }
  return Response.json({ url: redirectPath });
}
Enter fullscreen mode Exit fullscreen mode

This route constructs the IdP's end session URL with the necessary parameters:

  • id_token_hint: The original ID token received from the IdP at login
  • post_logout_redirect_uri: The URL to redirect to after successful logout from the IdP

5. Create a Sign-Out Page Component

Finally, we'll create a sign-out page component that handles the redirect from the IdP and completes the client-side logout by calling signOut() to clear the client-side session.

// app/auth/signout/page.tsx

"use client";

import { useRouter } from "next/navigation";
import { useEffect } from "react";

export default function Logout() {
  const router = useRouter();

  useEffect(() => {
    signOut({ redirect: false }).then(() => {
      router.push("/");
    });
  }, []);

  return (
    <div className="h-screen w-full flex flex-col items-center justify-center">
      <span>loading...</span>
    </div>
  );
}v
Enter fullscreen mode Exit fullscreen mode

Conclusion

By implementing this federated sign-out solution, we ensure that users are properly logged out from both our Next.js application and the identity provider. This approach adheres to the OpenID Connect standard and provides a secure and seamless logout experience for users.

Top comments (0)