Add support for POST logout in Keycloak JS (#25348)

Closes #25167

Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
Co-authored-by: Jon Koops <jonkoops@gmail.com>
This commit is contained in:
Thomas Darimont 2023-12-11 14:55:48 +01:00 committed by GitHub
parent c7be03b103
commit 0f5bbae75c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 94 additions and 4 deletions

View file

@ -208,6 +208,11 @@ export interface KeycloakInitOptions {
* of the OIDC 1.0 specification.
*/
locale?: string;
/**
* HTTP method for calling the end_session endpoint. Defaults to 'GET'.
*/
logoutMethod?: 'GET' | 'POST';
}
export interface KeycloakLoginOptions {
@ -289,6 +294,11 @@ export interface KeycloakLogoutOptions {
* Specifies the uri to redirect to after logout.
*/
redirectUri?: string;
/**
* HTTP method for calling the end_session endpoint. Defaults to 'GET'.
*/
logoutMethod?: 'GET' | 'POST';
}
export interface KeycloakRegisterOptions extends Omit<KeycloakLoginOptions, 'action'> { }

View file

@ -146,6 +146,12 @@ function Keycloak (config) {
kc.enableLogging = false;
}
if (initOptions.logoutMethod === 'POST') {
kc.logoutMethod = 'POST';
} else {
kc.logoutMethod = 'GET';
}
if (typeof initOptions.scope === 'string') {
kc.scope = initOptions.scope;
}
@ -487,6 +493,12 @@ function Keycloak (config) {
}
kc.createLogoutUrl = function(options) {
const logoutMethod = options?.logoutMethod ?? kc.logoutMethod;
if (logoutMethod === 'POST') {
return kc.endpoints.logout();
}
var url = kc.endpoints.logout()
+ '?client_id=' + encodeURIComponent(kc.clientId)
+ '&post_logout_redirect_uri=' + encodeURIComponent(adapter.redirectUri(options, false));
@ -1317,9 +1329,38 @@ function Keycloak (config) {
return createPromise().promise;
},
logout: function(options) {
window.location.replace(kc.createLogoutUrl(options));
return createPromise().promise;
logout: async function(options) {
const logoutMethod = options?.logoutMethod ?? kc.logoutMethod;
if (logoutMethod === "GET") {
window.location.replace(kc.createLogoutUrl(options));
return;
}
const logoutUrl = kc.createLogoutUrl(options);
const response = await fetch(logoutUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: new URLSearchParams({
id_token_hint: kc.idToken,
client_id: kc.clientId,
post_logout_redirect_uri: adapter.redirectUri(options, false)
})
});
if (response.redirected) {
window.location.href = response.url;
return;
}
if (response.ok) {
window.location.reload();
return;
}
throw new Error("Logout failed, request returned an error code.");
},
register: function(options) {

View file

@ -133,7 +133,12 @@ public class JavascriptTestExecutor {
}
public JavascriptTestExecutor logout(JavascriptStateValidator validator, LogoutConfirmPage logoutConfirmPage) {
jsExecutor.executeScript("keycloak.logout()");
return logout(validator, logoutConfirmPage, null);
}
public JavascriptTestExecutor logout(JavascriptStateValidator validator, LogoutConfirmPage logoutConfirmPage, JSObjectBuilder logoutOptions) {
String logoutOptionsString = logoutOptions == null ? "" : logoutOptions.toString();
jsExecutor.executeScript("keycloak.logout(" + logoutOptionsString + ")");
try {
// simple check if we are at the logout confirm page, if so just click 'Yes'

View file

@ -55,6 +55,7 @@ import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.lessThan;
import static org.hamcrest.collection.IsMapContaining.hasEntry;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.keycloak.testsuite.util.ServerURLs.AUTH_SERVER_HOST;
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlDoesntStartWith;
@ -159,6 +160,39 @@ public class JavascriptAdapterTest extends AbstractJavascriptTest {
.init(pkceS256, this::assertInitNotAuth);
}
@Test
public void testLogoutWithDefaults() {
boolean stillLoggedIn = testExecutor.init(defaultArguments(), this::assertInitNotAuth)
.login(this::assertOnLoginPage)
.loginForm(testUser, this::assertOnTestAppUrl)
.init(defaultArguments(), this::assertInitAuth)
.logout(this::assertOnTestAppUrl)
.isLoggedIn();
assertFalse("still logged in", stillLoggedIn);
}
@Test
public void testLogoutWithInitOptionsPostMethod() {
boolean stillLoggedIn = testExecutor.init(defaultArguments(), this::assertInitNotAuth)
.login(this::assertOnLoginPage)
.loginForm(testUser, this::assertOnTestAppUrl)
.init(defaultArguments().add("logoutMethod", "POST"), this::assertInitAuth)
.logout(this::assertOnTestAppUrl, null)
.isLoggedIn();
assertFalse("still logged in", stillLoggedIn);
}
@Test
public void testLogoutWithOptionsPostMethod() {
boolean stillLoggedIn = testExecutor.init(defaultArguments(), this::assertInitNotAuth)
.login(this::assertOnLoginPage)
.loginForm(testUser, this::assertOnTestAppUrl)
.init(defaultArguments(), this::assertInitAuth)
.logout(this::assertOnTestAppUrl, null, JSObjectBuilder.create().add("logoutMethod", "POST"))
.isLoggedIn();
assertFalse("still logged in", stillLoggedIn);
}
@Test
public void testSilentCheckSso() {
JSObjectBuilder checkSSO = defaultArguments().checkSSOOnLoad()