KINDERAS.COM

Sun Aug 28 2022

Implementing federated sign out when using next-auth in you Next.js app

When using federated auth, such a Google or a custom oAuth & OpenId Connect setup, a user will in some cases expect to be signed out from the original provider as well as your Next application. In this post I'll go into detail on how this can be achieved using next-auth v4.

Next-auth will as writing this (v4.10) not automatically handle federated sign out defined by the OpenId standard, so we have to manually implement this.

This post is based on this Github discussion over at the next-auth repo. See sources below.

The approach

  1. When the user clicks the logout button, redirect the user to a custom /auth/federated-sign-out route.
  2. After the thrid party provider has signed the user out, the federated auth server will redirect back to a route in the Next app (e.g. `/logout`). This page/route is passed to the provider auth server using the post_logout_redirect_uri parameter defined by the OpenID standard.

We'll need two new routes and a sign out button component. We will also need access to the original id token provided by the federated provider, let's take a look at this first.

How the get the provider id token

Since next-auth creates its own token, it doesn't automatically give you access to the original id token from the third party provider.

However, it's quite straight forward to make the original id token available in the next-auth created token. We'll need to update the pages/api/auth/[...nextauth].ts route to make this happen.

import NextAuth, { NextAuthOptions, User } from "next-auth";

export const authOptions: NextAuthOptions = {
	providers: [
		{
			// ...your provider config
			idToken: true,
			profile(profile, tokens) {
				return {
					id: profile.id,
					email: "",
					// Append the id token to the profile
					idToken: tokens.id_token,
				};
			},
		},
	],
	callbacks: {
		async session({ session, user, token }) {
			session.user = {
				// append the id token to the next-auth session
				idToken: token.idToken,
			} as any;
			return session;
		},
		async jwt({ token, user, account, profile, isNewUser }) {
			if (user) {
				// append the id token to the next-auth token
				token.idToken = user.idToken;
			}
			return token;
		},
	},
};

export default NextAuth(authOptions);

The /logout route/component

The logout route will sign the user out locally, meaning that the next-auth session will be removed and the user will be signed out from both the federated provider auth server and your Next.js application.

This route is a simple React component which will automatically call the next-auth signOut function (without a redirect), then redirect to the root page (or a page of your choosing).

PS: You can place this page/route where ever you want to, as long as the path matches the post_logout_redirect_uri defined in the /auth/federated-sign-out route.
import { signOut } from "next-auth/react";
import { useRouter } from "next/router";
import { useEffect } from "react";

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

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

	return <p>Logging out...</p>;
}

The sign out button

The sign out button will redirect the user to the /auth/federated-sign-out route (see below). You can e.g define this button as a React component like so.

export default function SignOutButton(): JSX.Element {
	const router = useRouter();

	function signOutHandler() {
		router.push("/auth/federated-sign-out");
	}

	return (
		<button 
		    type="button" 
		    onClick={signOutHandler}>
		    Sign out
	  </button>
	);

The `/auth/federated-sign-out` route

This route will assemble the provider /connect/endsession url, including query params (original id token and url to the logout page), and then redirect the user to the provider auth server.

PS: For this to work properly you'll probably need to configure valid redirect url's at the provider OpenID setup.
/* PS: This code has dummy urls and no env vars. You should probably not hardcode the baseUrl and provider auth endpoint, this is done for demo purposes in this code snippet.*/

import type { NextApiRequest, NextApiResponse } from "next";
import { unstable_getServerSession } from "next-auth";
import { authOptions } from "./[...nextauth]";

export default async function federatedSignOut(req: NextApiRequest, res: NextApiResponse) {
	// Get the site base url
	const baseUrl = "https://your.site";

	try {
		// We need to grab the session to get at the id token
		// PS: You can use the «getToken()» method here instead of «unstable_getServerSession».
		const session = await unstable_getServerSession(req, res, authOptions);
		if (!session) {
			// If the user isn't logged in, just redirect to root
			return res.redirect(baseUrl);
		}

		// Create the provider endsession url
		const endSessionURL = `https://auth-provider-endpoint.com/connect/endsession`;
		// And the redirect url
		// At this url (/logout) the local next-auth session will be removed
		const redirectURL = `${baseUrl}/logout`;
		// Construct the query params and redirect the browser to
		// the provider auth server
		const endSessionParams = new URLSearchParams({
		  // Pass the original id tok the to the provider
			id_token_hint: session.user.idToken,
			// Pass the redirect url
			post_logout_redirect_uri: redirectURL,
		});
		const fullUrl = `${endSessionURL}?${endSessionParams.toString()}`;
		return res.redirect(fullUrl);
	} catch (error) {
		res.redirect(baseUrl);
	}
}

And that's it.

Sources