Implementing Federated Sign-Out with Auth.js in Next.js 14 App Router
Aryaman Sharma
Posted on September 28, 2024
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:
- The identity provider (IdP)
- 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. ## Approach:
Our solution involves:
- Custom Sign-out Button: Initiates the federated sign-out process.
- api/auth/federated-logout Route: Returns the URL for redirection for provider sign-out.
- federatedLogout Function: Handles the overall process.
- Sign out Page Component: Cleans up the client-side session upon user return.
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.
},
}),
];
- 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>
// ...
);
};
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 = "/";
}
}
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 });
}
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
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.
Posted on September 28, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.