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:
parent
c7be03b103
commit
0f5bbae75c
4 changed files with 94 additions and 4 deletions
10
js/libs/keycloak-js/dist/keycloak.d.ts
vendored
10
js/libs/keycloak-js/dist/keycloak.d.ts
vendored
|
@ -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'> { }
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in a new issue