From 7c8c01b798333a687ef0e6cdb2cd15b817da81c3 Mon Sep 17 00:00:00 2001 From: Alexander Sander Date: Thu, 26 Mar 2026 15:32:41 +0100 Subject: [PATCH] Handle unauthenticated POST logout() scenarios, matching `createLogoutUrl()` behavior for GET requests. Closes #287 Signed-off-by: Alexander Sander --- lib/keycloak.js | 6 +++++- test/tests/login.spec.ts | 31 +++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/lib/keycloak.js b/lib/keycloak.js index 284242f..7cb1177 100755 --- a/lib/keycloak.js +++ b/lib/keycloak.js @@ -335,11 +335,15 @@ export default class Keycloak { form.style.display = 'none' // Add data to form as hidden input fields. + // Match behavior of createLogoutUrl() for GET requests. const data = { - id_token_hint: this.idToken, client_id: this.clientId, post_logout_redirect_uri: redirectUri(options) } + // Only add id token parameter when it is present to avoid error 'invalid id token hint' + if (this.idToken) { + data.id_token_hint = this.idToken + } for (const [name, value] of Object.entries(data)) { const input = document.createElement('input') diff --git a/test/tests/login.spec.ts b/test/tests/login.spec.ts index db8604f..92b1a01 100644 --- a/test/tests/login.spec.ts +++ b/test/tests/login.spec.ts @@ -19,6 +19,11 @@ test('logs in and out with default configuration', async ({ page, appUrl, authSe // After logging out, the user should no longer be authenticated. expect(await executor.initializeAdapter(initOptions)).toBe(false) expect(await executor.isAuthenticated()).toBe(false) + // Logout again to simulate a call with an unauthenticated user (id token is not present) + await executor.logout() + // After logging out again, the user should still not be authenticated. + expect(await executor.initializeAdapter(initOptions)).toBe(false) + expect(await executor.isAuthenticated()).toBe(false) }) test('logs in and out using a URL to the adapter config', async ({ page, appUrl, authServerUrl }) => { @@ -39,6 +44,11 @@ test('logs in and out using a URL to the adapter config', async ({ page, appUrl, // After logging out, the user should no longer be authenticated. await executor.instantiateAdapter(configUrl.toString()) expect(await executor.initializeAdapter(initOptions)).toBe(false) + // Logout again to simulate a call with an unauthenticated user (id token is not present) + await executor.logout() + // After logging out again, the user should still not be authenticated. + await executor.instantiateAdapter(configUrl.toString()) + expect(await executor.initializeAdapter(initOptions)).toBe(false) }) test('logs in and out using a generic OpenID provider', async ({ page, appUrl, authServerUrl }) => { @@ -61,6 +71,11 @@ test('logs in and out using a generic OpenID provider', async ({ page, appUrl, a // After logging out, the user should no longer be authenticated. await executor.instantiateAdapter(configOptions) expect(await executor.initializeAdapter(initOptions)).toBe(false) + // Logout again to simulate a call with an unauthenticated user (id token is not present) + await executor.logout() + // After logging out again, the user should still not be authenticated. + await executor.instantiateAdapter(configOptions) + expect(await executor.initializeAdapter(initOptions)).toBe(false) }) test('logs in and out without initialization options', async ({ page, appUrl, authServerUrl }) => { @@ -75,6 +90,10 @@ test('logs in and out without initialization options', async ({ page, appUrl, au await executor.logout() // After logging out, the user should no longer be authenticated. expect(await executor.initializeAdapter()).toBe(false) + // Logout again to simulate a call with an unauthenticated user (id token is not present) + await executor.logout() + // After logging out again, the user should still not be authenticated. + expect(await executor.initializeAdapter()).toBe(false) }) test('logs in and out without PKCE', async ({ page, appUrl, authServerUrl }) => { @@ -90,6 +109,10 @@ test('logs in and out without PKCE', async ({ page, appUrl, authServerUrl }) => await executor.logout() // After logging out, the user should no longer be authenticated. expect(await executor.initializeAdapter(initOptions)).toBe(false) + // Logout again to simulate a call with an unauthenticated user (id token is not present) + await executor.logout() + // After logging out again, the user should still not be authenticated. + expect(await executor.initializeAdapter(initOptions)).toBe(false) }) test("logs in and out with 'POST' logout configured at initialization", async ({ page, appUrl, authServerUrl }) => { @@ -105,6 +128,10 @@ test("logs in and out with 'POST' logout configured at initialization", async ({ await executor.logout() // After logging out, the user should no longer be authenticated. expect(await executor.initializeAdapter(initOptions)).toBe(false) + // Logout again to simulate a call with an unauthenticated user (id token is not present) + await executor.logout() + // After logging out again, the user should still not be authenticated. + expect(await executor.initializeAdapter(initOptions)).toBe(false) }) test("logs in and out with 'POST' logout configured at logout", async ({ page, appUrl, authServerUrl }) => { @@ -120,6 +147,10 @@ test("logs in and out with 'POST' logout configured at logout", async ({ page, a await executor.logout({ logoutMethod: 'POST' }) // After logging out, the user should no longer be authenticated. expect(await executor.initializeAdapter(initOptions)).toBe(false) + // Logout again to simulate a call with an unauthenticated user (id token is not present) + await executor.logout({ logoutMethod: 'POST' }) + // After logging out again, the user should still not be authenticated. + expect(await executor.initializeAdapter(initOptions)).toBe(false) }) test('logs in and checks session status', async ({ page, appUrl, authServerUrl, strictCookies }) => {