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
- When the user clicks the logout button, redirect the user to a custom
/auth/federated-sign-out
route. - 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 thepost_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.