Skip to content

Commit dc79698

Browse files
dcl10claude
andcommitted
Fix sign-out not ending the Keycloak SSO session
next-auth's signOut only cleared the local JWT cookie, leaving Keycloak's SSO session intact. Signing back in would silently re-authenticate the same user without prompting for credentials. Store the ID token in the JWT and session on initial sign-in, then use it as id_token_hint when redirecting to Keycloak's end-session endpoint so the Keycloak session is properly terminated. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent f02e307 commit dc79698

2 files changed

Lines changed: 27 additions & 3 deletions

File tree

frontend/app/actions.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"use server";
22

3-
import { signIn, signOut } from "@/auth";
3+
import { redirect } from "next/navigation";
4+
5+
import { auth, signIn, signOut } from "@/auth";
46

57
/**
68
* Initiates the Keycloak OAuth flow via a server action.
@@ -21,7 +23,25 @@ export async function signUpWithKeycloak() {
2123
await signIn("keycloak", { redirectTo: "/dashboard" }, { prompt: "create" });
2224
}
2325

24-
/** Signs the user out and redirects to the landing page. */
26+
/**
27+
* Signs the user out locally and ends the Keycloak SSO session.
28+
* next-auth's signOut only clears the local cookie; without also calling
29+
* Keycloak's end-session endpoint the SSO session stays alive and the user
30+
* is silently re-authenticated on the next sign-in attempt.
31+
*/
2532
export async function signOutFromKeycloak() {
26-
await signOut({ redirectTo: "/" });
33+
const session = await auth();
34+
const idToken = session?.idToken;
35+
36+
await signOut({ redirect: false });
37+
38+
const params = new URLSearchParams({
39+
post_logout_redirect_uri: `${process.env.AUTH_URL ?? "http://localhost:3000"}/`,
40+
client_id: process.env.AUTH_KEYCLOAK_ID!,
41+
...(idToken ? { id_token_hint: idToken } : {}),
42+
});
43+
44+
redirect(
45+
`${process.env.AUTH_KEYCLOAK_ISSUER}/protocol/openid-connect/logout?${params}`,
46+
);
2747
}

frontend/auth.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ import Keycloak from "next-auth/providers/keycloak";
55
declare module "next-auth" {
66
interface Session {
77
accessToken?: string;
8+
idToken?: string;
89
error?: "RefreshAccessTokenError";
910
}
1011
}
1112

1213
declare module "@auth/core/jwt" {
1314
interface JWT {
1415
accessToken?: string;
16+
idToken?: string;
1517
refreshToken?: string;
1618
accessTokenExpires?: number;
1719
error?: "RefreshAccessTokenError";
@@ -57,6 +59,7 @@ export const { auth, handlers, signIn, signOut } = NextAuth({
5759
return {
5860
...token,
5961
accessToken: account.access_token,
62+
idToken: account.id_token,
6063
refreshToken: account.refresh_token,
6164
accessTokenExpires: account.expires_at
6265
? account.expires_at * 1000
@@ -79,6 +82,7 @@ export const { auth, handlers, signIn, signOut } = NextAuth({
7982

8083
async session({ session, token }) {
8184
session.accessToken = token.accessToken;
85+
session.idToken = token.idToken;
8286
session.error = token.error;
8387
return session;
8488
},

0 commit comments

Comments
 (0)