Federated logout (OpenID Connect) #3938
Replies: 61 comments 45 replies
-
#1024 solved this by passing the Then you can save the token in the JWT, and reuse it when logging the user out. For this I created a custom api endpoint: // /api/auth/federated-logout
import jwt from "next-auth/jwt"
import log from "utils/server-logger"
export default async function federatedLogout(req, res) {
try {
const token = await jwt.getToken({req, secret: process.env.SECRET, encryption: true })
if (!token) {
log.warn("No JWT token found when calling /federated-logout endpoint")
return res.redirect(process.env.NEXTAUTH_URL)
}
if (!token.idToken)
log.warn("Without an id_token the user won't be redirected back from the IdP after logout.")
const endsessionURL = `https://${process.env.PROVIDER_DOMAIN}/connect/endsession`
const endsessionParams = new URLSearchParams({
id_token_hint: token.idToken,
post_logout_redirect_uri: process.env.NEXTAUTH_URL,
})
return res.redirect(`${endsessionURL}?${endsessionParams}`)
} catch (error) {
log.error(error)
res.redirect(process.env.NEXTAUTH_URL)
}
} And when logging out on the client: <button onClick={() => window.location.href = "/api/auth/federated-logout"}>
Sign out
</button> |
Beta Was this translation helpful? Give feedback.
-
Reopening, as this has not been fully resolved. We could try to support federated logout for most OIDC compliant providers out-of-the-box. |
Beta Was this translation helpful? Give feedback.
-
Hey @balazsorban44 - thanks for opening this issue. I was looking into this as well because I was working on implementing
Basically that is the Or would it be possible to resolve this in a different way? Cheers, |
Beta Was this translation helpful? Give feedback.
-
So my current solution for this is using the |
Beta Was this translation helpful? Give feedback.
-
Thanks @balazsorban44 - how do you save the ID token from the I went on to implementing the above using the
|
Beta Was this translation helpful? Give feedback.
-
Have a look at the |
Beta Was this translation helpful? Give feedback.
-
Cheers @balazsorban44 - forgot to look in the |
Beta Was this translation helpful? Give feedback.
-
Hi all, is an implication of this issue that people are not able to, say, switch from one Google account to another? I'm seeing that behavior on my site and wondering if this is the cause. |
Beta Was this translation helpful? Give feedback.
-
You can always force a new user to login with their own credentials using https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest To make it work in next-auth, you could do: https://next-auth.js.org/getting-started/client#additional-params |
Beta Was this translation helpful? Give feedback.
-
Thanks! The user is being prompted, but the issue I'm seeing is that next auth doesn't use the new account. E.g.
This still occurs with prompt: login or prompt: select_account |
Beta Was this translation helpful? Give feedback.
-
It sounds like they are not being signed out from the first session in NextAuth.js (i.e. that the existing session cookie is not being deleted). NoteIf you are using a database and sign a user with a second OAuth account while they are already signed in it will securely link the accounts; the user information returned in the session will be whatever the email addresses associated with the User ID is (e.g. the email address associated with the first account they signed in with). If this is the case it is easy to view by looking at the User and Accounts tables, you will see the Accounts are both referenced to the same User ID. To unlink the OAuth accounts, simply delete the entry for the account in the Accounts table. NB: Build in pages to allow users to link/unlink accounts and to edit their profile settings are planned, but for now you would need to build your own interface to handle this. If you are not using a database then it will swap the two instead of linking (as linking accounts without a database doesn't make sense without a database to track which accounts are linked). |
Beta Was this translation helpful? Give feedback.
-
@balazsorban44 Is it possible without |
Beta Was this translation helpful? Give feedback.
-
Not really sure what you mean. the secret is used to retrieve the token with See the docs: https://next-auth.js.org/configuration/options#jwt |
Beta Was this translation helpful? Give feedback.
-
Hi @balazsorban44, I'm trying this snippet, and it signs the user out from the auth provider successfully (if I go to the auth provider's panel it prompts me to lo in again, I'm using FusionAuth. But when the federated-logout endpoint redirects me back to the app, it still contains a valid session somehow (i.e the app calls |
Beta Was this translation helpful? Give feedback.
-
you will need to call |
Beta Was this translation helpful? Give feedback.
-
For some reason, I still had the cookie after using signout with redirect false, after returning from federation logout. I've solved it with useEffect import { signOut, useSession } from 'next-auth/react';
import { useRouter } from 'next/router';
import { useEffect } from 'react';
export default function Logout() {
const router = useRouter();
const { data, status } = useSession();
useEffect(() => {
if (data && status === 'authenticated') {
signOut();
} else if (status === 'unauthenticated') {
router.push('/');
}
}, [data, status]);
return <p>Logging out...</p>;
} |
Beta Was this translation helpful? Give feedback.
-
I specifically had an issue with other solutions when Amazon Cognito was configured for sign in with Looking for some feedback on federated logout for Amazon Cognito provider to clean up everything after each sign out. Sign-out on the frontend is basically:
Backend:
Anyone spot any issue with this approach? Thanks. |
Beta Was this translation helpful? Give feedback.
-
Performing a federated logout with keycloak normally does not require the user to press the logout button manually. For me federated logout with keycloak only works at the first time after a new account was created in the db, all following logouts require the user to manually click the logout button. I am assuming this happens because the Does anyone have a solution to this issue? |
Beta Was this translation helpful? Give feedback.
-
Okay I got it to work with Azure AD for Sign in + Sign Out Issue is that I can't sign in or sign out too quickly. Looks like its there to prevent constant logins/logouts to their api to handle lots of users. //log out button that initiates signOut and callbacks to Azure AD's log out flow to update both client + server side login status
//we redirect after to auth/signout which is a client side page to update the session and redirect if needed (this was to cover both cases if the session didnt update - probably a better way to do this second part, unsure if necessary need to test)
<a
onClick={async (e) => {
await signOut({
callbackUrl: `https://login.microsoftonline.com/${process.env.NEXT_PUBLIC_AZURE_AD_TENANT_ID}/oauth2/v2.0/logout?post_logout_redirect_uri=${process.env.NEXT_PUBLIC_NEXTAUTH_URL}/auth/signout`,
});
}}
>
Log Out
</a> // pages/auth/signout.js
import { signOut } from "next-auth/react";
import { useEffect } from "react";
import { useSession } from "next-auth/react";
import { useRouter } from "next/router";
export default function index() {
const router = useRouter();
const { data: session, status } = useSession();
useEffect(() => {
if (status === "authenticated") {
signOut({ callbackUrl: "/" });
} else if (status === "unauthenticated") {
router.push("/");
}
}, [status]);
return <div></div>;
}
|
Beta Was this translation helpful? Give feedback.
-
Did anyone get this working with the Next 13 app directory? As I stated earlier in this thread I was able to get it working with pages. I was able to migrate NextAuth to Next 13 app directory but I had to keep the federated logout solution in the old pages API area. |
Beta Was this translation helpful? Give feedback.
-
The problem occurs if more than three tabs are open at the same time. The user will log out of the system on one of them, and in this case the token remains in the cookies and a constant re-rendering of the page begins |
Beta Was this translation helpful? Give feedback.
-
In our sample app, we've implemented federated sign out the following way: First, fetch the Sign out button (.tsx): https://github.com/descope/nextjs-hackathon-template/blob/main/app/dashboard/_components/Header.tsx#L16 How the federated sign out process works (for Descope specifically, but would be similar to other providers as well), in the API: |
Beta Was this translation helpful? Give feedback.
-
A simple solution for a federated logout is to add the logout uri to redirects: async () => {
return [
{
source: "/api/sso/logout",
destination: env.COGNITO_LOGOUT_URL, // the logout url from the sso provider
permanent: false,
},
];
}, and then call the signOut({ callbackUrl: '/api/sso/logout' }) |
Beta Was this translation helpful? Give feedback.
-
Working example with Keycloak, App Router, and NextAuth v5. "next": "^14.1.0",
"next-auth": "beta", Keycloak
src/components/SignOut.tsx
import { signOut } from "../../auth";
export function SignOut({ refreshToken }: { refreshToken: string }) {
return (
<form
action={async () => {
"use server";
try {
await fetch("http://localhost:3000/api/auth/federated-sign-out", {
headers: {
refresh_token: refreshToken,
},
});
} catch (error) {
console.error("Error during sign out:", error);
}
await signOut();
}}
>
<button>Sign Out</button>
</form>
);
} app/api/auth/federated-sign-out/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function GET(req: NextRequest, res: NextResponse) {
try {
const refreshToken = req.headers.get("refresh_token");
if (
!refreshToken ||
!process.env.AUTH_KEYCLOAK_END_SESSION_ENDPOINT ||
!process.env.AUTH_KEYCLOAK_ID
) {
throw Error;
}
const body = `client_id=${process.env.AUTH_KEYCLOAK_ID}&refresh_token=${refreshToken}`;
const endSession = await fetch(
process.env.AUTH_KEYCLOAK_END_SESSION_ENDPOINT,
{
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
},
body,
}
);
if (endSession && endSession.status && endSession.status >= 300) {
console.warn("END_SESSION ERROR", endSession.status);
throw Error;
}
return NextResponse.json({ success: true });
} catch (error) {
return NextResponse.json({
success: false,
message: "Error Sign out",
});
}
} |
Beta Was this translation helpful? Give feedback.
-
Are there any plans to actually add federated logout to the library? |
Beta Was this translation helpful? Give feedback.
-
Is there an issue calling signOut from inside an API route handler? I want to call the end_session_endpoint first passing my API route handler as a callback. |
Beta Was this translation helpful? Give feedback.
-
For those using FusionAuth with nextauth 5: // app/api/auth/federated-logout/route.ts
import { auth } from "@/auth";
import { serverEnvs } from "@/config/constants.server";
import { getLogger } from "@/utils/logger";
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
const clearCookieOptions = "Max-Age=-1; Path=/; Secure; HttpOnly; SameSite=None;";
// https://github.com/nextauthjs/next-auth/discussions/3938#discussioncomment-8799241
// https://github.com/nextauthjs/next-auth/discussions/3938#discussioncomment-7791235
/**
* Signs the user out from both FusionAuth and the application
*/
export async function GET() {
const session = await auth();
const log = getLogger().withPrefix("[auth]");
try {
log.info("Signing out from FusionAuth...");
const endSessionURL = `${serverEnvs.FUSIONAUTH_URL}/oauth2/logout`;
await fetch(endSessionURL, {
method: "POST",
headers: {
"content-type": "application/json;charset=UTF-8",
},
body: JSON.stringify({
client_id: serverEnvs.FUSIONAUTH_CLIENT_ID,
client_secret: serverEnvs.FUSIONAUTH_CLIENT_SECRET,
id_token_hint: session.token.idToken,
tenantId: serverEnvs.FUSIONAUTH_TENANT_ID,
}),
});
// Clear all cookies
log.info("Clearing application cookies...");
let setCookies = "";
for (const cookie of cookies().getAll()) {
// Need to keep this cookie for passkey usage
if (!cookie.name.startsWith("fusionauth.webauthn-reauth")) {
setCookies += `${cookie.name}=; ${clearCookieOptions}`;
cookies().set(cookie.name, "", {
path: "/",
maxAge: -1,
secure: true,
sameSite: "none",
});
cookies().delete(cookie.name);
}
}
cookies().set("next-auth.session-token", "", {
path: "/",
maxAge: -1,
secure: true,
sameSite: "none",
});
cookies().set("__Secure-next-auth.session-token", "", {
path: "/",
maxAge: -1,
secure: true,
sameSite: "none",
});
setCookies += "next-auth.session-token=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Secure; SameSite=None;";
setCookies += "__Secure-next-auth.session-token=deleted; path=/; Secure; expires=Thu, 01 Jan 1970 00:00:00 GMT";
return new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: {
"Set-Cookie": setCookies,
"Content-Type": "application/json",
},
});
} catch (error) {
return NextResponse.json({
success: false,
message: "Error Sign out",
});
}
} Then apply in your component: "use client";
import { useSession } from "next-auth/react";
import { useCallback } from "react";
export default function UserMenu() {
const { data } = useSession();
const handleSignOut = useCallback(() => {
fetch("/api/auth/federated-logout")
.then((res) => {
return res.json();
})
.then(() => {
window.location.href = "/";
})
.catch((e) => {
getLogger().withError(e).error("Failed to sign out");
});
}, []);
...
} |
Beta Was this translation helpful? Give feedback.
-
Using an oauth2 compliant server I managed to do it using a custom route and a logout page which calls nextauths signout like this: /api/auth/fedSignOut
/[lang]/(blank-layout)/logout
additionally a button which calls the fedsignout route via window.location.href = url |
Beta Was this translation helpful? Give feedback.
-
I managed to signOut the user from my provider (cognito) using the new authjs.dev and next js 15, here is my code
|
Beta Was this translation helpful? Give feedback.
-
Summary of proposed feature
Acces
id_token
injwt
callback, whenidToken: true
in a provider's option.Purpose of proposed feature
Make it possible to start a logout process from a
next
app usingnext-auth
that will log out from the Identity Provider entirely, if it is OIDC compliant.Detail about proposed feature
OpenID Connect compliant IdPs (like IdentiyServer4, which is also supported by
next-auth
) have a federated logout.To initiate such a logout process, when
signOut()
is called,next-auth
should redirect tohttps://idp.com/endsession
, and forward at least the two following query params:id_token_hint
: The originalid_token
received from the IdP at login.post_logout_redirect_uri
: A preconfigured URL in the IdP client, that the IdP should redirect to after a successful logout. This could default toNEXTAUTH_URL
, but could be overridden in a provider setting. (Note that the same url must be registered in the IdP client)Additional optional query params exist, see the relevant spec section
Obtaining the
id_token
at the initialjwt()
callback call would make it possible to preserve it in a session database, or in the jwt token itself.Potential problems
According to the JWT callback docs
jwt
callback has already many parameters, and adding a newidToken
would just make it worse. In my opinion should/could be provided as a single object to basically be able to use named params, but that would be a breaking change.Setting the
id_token
in the JWT cookie is kind of an overhead, as the cookie most possibly contains some kind of a mapping of the id_token (name, email, etc.) already. My solution for this would be to not store it in the cookie, but create an in-memory database (just a JS class with key value pairs, where keys are user ids, and each user contains a list of sessions, containing sessions'id_token
s. Hint: The id_token usually contains asid
or Session ID, and timestamps if needed). Adding such an in-memory database should not be required from most of the users though. (Although this could very well be an optional part ofnext-auth
, that could also complement RFC: Limit logins (by concurrency, location) #583 nicely, which I am already working on at work, and some day be able to share it as a PR tonext-auth
) I am open for suggestions here!There is the 4096 bytes cookie size limit in browser, and because of that every extra bytes count, if we cannot somehow divide the data into multiple cookies somehow. (I am no expert on cookies though, so I do not know hard it would be to split/merge cookies)
Describe any alternatives you've considered
I haven't considered alternatives to
next-auth
as it is generally a high quality full auth solution for Next.js apps, and since it already supportsidToken
for OIDC compliant IdPs, it would only make sense to leverage that even further.Additional context
OIDC spec: https://openid.net/specs/openid-connect-rpinitiated-1_0.html
Relevant IDS4 documentation: https://identityserver4.readthedocs.io/en/latest/endpoints/endsession.html
id_token content: https://docs.microsoft.com/en-us/azure/active-directory/develop/id-tokens#payload-claims
Please indicate if you are willing and able to help implement the proposed feature.
If this is not a problem that can be solved in user land right now, I may be willing to try to implement this and open a PR for it, but before that, I need feedback on how to store the
id_token
in thejwt
callback, with minimal overhead.So just a TLDR:
If the user provides the
idToken: true
option as a Provider option, it would be nice to send it as a param to thejwt
callback as well. The rest can strictly speaking be implemented by the user for now.Beta Was this translation helpful? Give feedback.
All reactions