OIDC RP-Initiated Logout POST method support

Closes #11958
This commit is contained in:
mposolda 2022-05-27 16:38:35 +02:00 committed by Marek Posolda
parent c0fd3b89ea
commit 4222de8f41
4 changed files with 98 additions and 4 deletions

View file

@ -289,6 +289,30 @@ public class LogoutEndpoint {
.createLogoutConfirmPage();
}
/**
* This endpoint can be used either as:
* - OpenID Connect RP-Initiated Logout POST endpoint according to the specification https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout
* - Legacy Logout endpoint with refresh_token as an argument and client authentication needed. See {@link #logoutToken} for more details
*
* @return response
*/
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response logout() {
MultivaluedMap<String, String> form = request.getDecodedFormParameters();
if (form.containsKey(OAuth2Constants.REFRESH_TOKEN)) {
return logoutToken();
} else {
return logout(form.getFirst(OIDCLoginProtocol.REDIRECT_URI_PARAM),
form.getFirst(OIDCLoginProtocol.ID_TOKEN_HINT),
form.getFirst(OIDCLoginProtocol.CLIENT_ID_PARAM),
form.getFirst(OIDCLoginProtocol.POST_LOGOUT_REDIRECT_URI_PARAM),
form.getFirst(OIDCLoginProtocol.STATE_PARAM),
form.getFirst(OIDCLoginProtocol.UI_LOCALES_PARAM),
form.getFirst(AuthenticationManager.INITIATING_IDP_PARAM));
}
}
@Path("/logout-confirm")
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@ -389,9 +413,7 @@ public class LogoutEndpoint {
*
* @return
*/
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response logoutToken() {
private Response logoutToken() {
cors = Cors.add(request).auth().allowedMethods("POST").auth().exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS);
MultivaluedMap<String, String> form = request.getDecodedFormParameters();

View file

@ -1000,7 +1000,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
* See URLUtils.sendPOSTWithWebDriver for more details
*
* @param postRequestUrl Absolute URL. It can include query parameters etc. The POST request will be send to this URL
* @param encodedFormParameters Encoded parameters in the form of "param1=value1:param2=value2"
* @param encodedFormParameters Encoded parameters in the form of "param1=value1&param2=value2"
* @return
*/
@GET

View file

@ -11,6 +11,7 @@ import org.openqa.selenium.support.ui.WebDriverWait;
import java.net.URI;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import static org.keycloak.testsuite.util.DroneUtils.getCurrentDriver;
@ -96,6 +97,28 @@ public final class URLUtils {
return true;
}
/**
* @see #sendPOSTRequestWithWebDriver(String, String)
*
* @param postRequestUrl
* @param formParams form params in key/value form
*/
public static void sendPOSTRequestWithWebDriver(String postRequestUrl, Map<String, String> formParams) {
StringBuilder sb = new StringBuilder();
boolean first = true;
for (Map.Entry<String, String> entry : formParams.entrySet()) {
if (first) {
first = false;
} else {
sb.append("&");
}
sb.append(entry.getKey())
.append("=")
.append(entry.getValue());
}
sendPOSTRequestWithWebDriver(postRequestUrl, sb.toString());
}
/**
* This will send POST request to specified URL with specified form parameters. It's not easily possible to "trick" web driver to send POST
* request with custom parameters, which are not directly available in the form.

View file

@ -36,6 +36,7 @@ import org.keycloak.events.Errors;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.models.Constants;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.IDToken;
import org.keycloak.representations.LogoutToken;
import org.keycloak.representations.idm.ClientRepresentation;
@ -52,6 +53,8 @@ import org.keycloak.testsuite.pages.LoginPage;
import java.io.Closeable;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.core.HttpHeaders;
@ -76,6 +79,7 @@ import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
import org.keycloak.testsuite.util.InfinispanTestTimeServiceRule;
import org.keycloak.testsuite.util.Matchers;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.URLUtils;
import org.keycloak.testsuite.util.WaitUtils;
import org.openqa.selenium.NoSuchElementException;
@ -721,6 +725,51 @@ public class RPInitiatedLogoutTest extends AbstractTestRealmKeycloakTest {
}
// Calling RP-Initiated Logout endpoint with POST request. This must be supported according to specification
@Test
public void logoutWithPostRequest() throws IOException {
try (RealmAttributeUpdater updater = new RealmAttributeUpdater(testRealm()).addSupportedLocale("cs").update()) {
OAuthClient.AccessTokenResponse tokenResponse = loginUser();
// Logout with POST request and automatic redirect after logout
String redirectUri = APP_REDIRECT_URI + "?logout";
String idTokenString = tokenResponse.getIdToken();
String sessionId = tokenResponse.getSessionState();
Map<String, String> postParams = new HashMap<>();
postParams.put(OIDCLoginProtocol.POST_LOGOUT_REDIRECT_URI_PARAM, redirectUri);
postParams.put(OIDCLoginProtocol.ID_TOKEN_HINT, idTokenString);
postParams.put(OAuth2Constants.STATE, "my-state");
URLUtils.sendPOSTRequestWithWebDriver(oauth.getLogoutUrl().build(), postParams);
events.expectLogout(tokenResponse.getSessionState()).detail(Details.REDIRECT_URI, redirectUri).assertEvent();
Assert.assertThat(false, is(isSessionActive(sessionId)));
assertCurrentUrlEquals(redirectUri + "&state=my-state");
// Logout with showing confirmation screen
tokenResponse = loginUser();
sessionId = tokenResponse.getSessionState();
postParams.clear();
postParams.put(OIDCLoginProtocol.POST_LOGOUT_REDIRECT_URI_PARAM, redirectUri);
postParams.put(OAuth2Constants.CLIENT_ID, "test-app");
postParams.put(OAuth2Constants.STATE, "my-state-2");
postParams.put(OIDCLoginProtocol.UI_LOCALES_PARAM, "cs");
URLUtils.sendPOSTRequestWithWebDriver(oauth.getLogoutUrl().build(), postParams);
Assert.assertEquals("Odhlašování", PageUtils.getPageTitle(driver)); // Logging out
Assert.assertEquals("Čeština", logoutConfirmPage.getLanguageDropdownText());
logoutConfirmPage.confirmLogout();
WaitUtils.waitForPageToLoad();
events.expectLogout(tokenResponse.getSessionState()).detail(Details.REDIRECT_URI, redirectUri).assertEvent();
Assert.assertThat(false, is(isSessionActive(sessionId)));
assertCurrentUrlEquals(redirectUri + "&state=my-state-2");
}
}
@Test
public void testFrontChannelLogoutWithPostLogoutRedirectUri() throws Exception {
ClientsResource clients = adminClient.realm(oauth.getRealm()).clients();