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

View file

@ -146,6 +146,12 @@ function Keycloak (config) {
kc.enableLogging = false; kc.enableLogging = false;
} }
if (initOptions.logoutMethod === 'POST') {
kc.logoutMethod = 'POST';
} else {
kc.logoutMethod = 'GET';
}
if (typeof initOptions.scope === 'string') { if (typeof initOptions.scope === 'string') {
kc.scope = initOptions.scope; kc.scope = initOptions.scope;
} }
@ -487,6 +493,12 @@ function Keycloak (config) {
} }
kc.createLogoutUrl = function(options) { kc.createLogoutUrl = function(options) {
const logoutMethod = options?.logoutMethod ?? kc.logoutMethod;
if (logoutMethod === 'POST') {
return kc.endpoints.logout();
}
var url = kc.endpoints.logout() var url = kc.endpoints.logout()
+ '?client_id=' + encodeURIComponent(kc.clientId) + '?client_id=' + encodeURIComponent(kc.clientId)
+ '&post_logout_redirect_uri=' + encodeURIComponent(adapter.redirectUri(options, false)); + '&post_logout_redirect_uri=' + encodeURIComponent(adapter.redirectUri(options, false));
@ -1317,9 +1329,38 @@ function Keycloak (config) {
return createPromise().promise; return createPromise().promise;
}, },
logout: function(options) { logout: async function(options) {
const logoutMethod = options?.logoutMethod ?? kc.logoutMethod;
if (logoutMethod === "GET") {
window.location.replace(kc.createLogoutUrl(options)); window.location.replace(kc.createLogoutUrl(options));
return createPromise().promise; 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) { register: function(options) {

View file

@ -133,7 +133,12 @@ public class JavascriptTestExecutor {
} }
public JavascriptTestExecutor logout(JavascriptStateValidator validator, LogoutConfirmPage logoutConfirmPage) { 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 { try {
// simple check if we are at the logout confirm page, if so just click 'Yes' // 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.Matchers.lessThan;
import static org.hamcrest.collection.IsMapContaining.hasEntry; import static org.hamcrest.collection.IsMapContaining.hasEntry;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.keycloak.testsuite.util.ServerURLs.AUTH_SERVER_HOST; import static org.keycloak.testsuite.util.ServerURLs.AUTH_SERVER_HOST;
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlDoesntStartWith; import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlDoesntStartWith;
@ -159,6 +160,39 @@ public class JavascriptAdapterTest extends AbstractJavascriptTest {
.init(pkceS256, this::assertInitNotAuth); .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 @Test
public void testSilentCheckSso() { public void testSilentCheckSso() {
JSObjectBuilder checkSSO = defaultArguments().checkSSOOnLoad() JSObjectBuilder checkSSO = defaultArguments().checkSSOOnLoad()