OIDC logout: In "legacy mode", support post_logout_redirect_uri param without requiring id_token_hint param

Closes #12680
This commit is contained in:
danielFesenmeyer 2022-06-23 17:48:25 +02:00 committed by Marek Posolda
parent ffc1265e9a
commit b6d8c27cac
2 changed files with 56 additions and 23 deletions

View file

@ -17,6 +17,10 @@
package org.keycloak.protocol.oidc.endpoints; package org.keycloak.protocol.oidc.endpoints;
import static org.keycloak.models.UserSessionModel.State.LOGGED_OUT;
import static org.keycloak.models.UserSessionModel.State.LOGGING_OUT;
import static org.keycloak.services.resources.LoginActionsService.SESSION_CODE;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpRequest;
@ -72,6 +76,10 @@ import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.sessions.RootAuthenticationSessionModel; import org.keycloak.sessions.RootAuthenticationSessionModel;
import org.keycloak.util.TokenUtil; import org.keycloak.util.TokenUtil;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.OPTIONS; import javax.ws.rs.OPTIONS;
@ -83,13 +91,6 @@ import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.keycloak.models.UserSessionModel.State.LOGGED_OUT;
import static org.keycloak.models.UserSessionModel.State.LOGGING_OUT;
import static org.keycloak.services.resources.LoginActionsService.SESSION_CODE;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -162,20 +163,27 @@ public class LogoutEndpoint {
@QueryParam(OIDCLoginProtocol.UI_LOCALES_PARAM) String uiLocales, @QueryParam(OIDCLoginProtocol.UI_LOCALES_PARAM) String uiLocales,
@QueryParam(AuthenticationManager.INITIATING_IDP_PARAM) String initiatingIdp) { @QueryParam(AuthenticationManager.INITIATING_IDP_PARAM) String initiatingIdp) {
if (deprecatedRedirectUri != null && !providerConfig.isLegacyLogoutRedirectUri()) { if (!providerConfig.isLegacyLogoutRedirectUri()) {
event.event(EventType.LOGOUT); if (deprecatedRedirectUri != null) {
event.error(Errors.INVALID_REQUEST); event.event(EventType.LOGOUT);
logger.warnf("Parameter 'redirect_uri' no longer supported. Please use 'post_logout_redirect_uri' with 'id_token_hint' for this endpoint. Alternatively you can enable backwards compatibility option '%s' of oidc login protocol in the server configuration.", event.error(Errors.INVALID_REQUEST);
OIDCLoginProtocolFactory.CONFIG_LEGACY_LOGOUT_REDIRECT_URI); logger.warnf("Parameter 'redirect_uri' no longer supported. Please use 'post_logout_redirect_uri' with 'id_token_hint' for this endpoint. Alternatively you can enable backwards compatibility option '%s' of oidc login protocol in the server configuration.",
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_PARAMETER, OIDCLoginProtocol.REDIRECT_URI_PARAM); OIDCLoginProtocolFactory.CONFIG_LEGACY_LOGOUT_REDIRECT_URI);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_PARAMETER, OIDCLoginProtocol.REDIRECT_URI_PARAM);
}
if (postLogoutRedirectUri != null && encodedIdToken == null && clientId == null) {
event.event(EventType.LOGOUT);
event.error(Errors.INVALID_REQUEST);
logger.warnf(
"Either the parameter 'client_id' or the parameter 'id_token_hint' is required when 'post_logout_redirect_uri' is used.");
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.MISSING_PARAMETER,
OIDCLoginProtocol.ID_TOKEN_HINT);
}
} }
if (postLogoutRedirectUri != null && encodedIdToken == null && clientId == null) { deprecatedRedirectUri = providerConfig.isLegacyLogoutRedirectUri() ? deprecatedRedirectUri : null;
event.event(EventType.LOGOUT); final String redirectUri = postLogoutRedirectUri != null ? postLogoutRedirectUri : deprecatedRedirectUri;
event.error(Errors.INVALID_REQUEST);
logger.warnf("Either the parameter 'client_id' or the parameter 'id_token_hint' is required when 'post_logout_redirect_uri' is used.");
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.MISSING_PARAMETER, OIDCLoginProtocol.ID_TOKEN_HINT);
}
boolean confirmationNeeded = true; boolean confirmationNeeded = true;
boolean forcedConfirmation = false; boolean forcedConfirmation = false;
@ -222,12 +230,15 @@ public class LogoutEndpoint {
} }
String validatedRedirectUri = null; String validatedRedirectUri = null;
if (postLogoutRedirectUri != null || deprecatedRedirectUri != null) { if (redirectUri != null) {
String redirectUri = postLogoutRedirectUri != null ? postLogoutRedirectUri : deprecatedRedirectUri;
if (client != null) { if (client != null) {
validatedRedirectUri = RedirectUtils.verifyRedirectUri(session, redirectUri, client); validatedRedirectUri = RedirectUtils.verifyRedirectUri(session, redirectUri, client);
} else if (providerConfig.isLegacyLogoutRedirectUri()) { } else if (clientId == null) {
validatedRedirectUri = RedirectUtils.verifyRealmRedirectUri(session, deprecatedRedirectUri); /*
* Only call verifyRealmRedirectUri, in case both clientId and client are null - otherwise
* the logout uri contains a non-existing client, and we should show an INVALID_REDIRECT_URI error
*/
validatedRedirectUri = RedirectUtils.verifyRealmRedirectUri(session, redirectUri);
} }
if (validatedRedirectUri == null) { if (validatedRedirectUri == null) {

View file

@ -52,6 +52,7 @@ import org.keycloak.testsuite.util.InfinispanTestTimeServiceRule;
import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.ServerURLs; import org.keycloak.testsuite.util.ServerURLs;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlDoesntStartWith; import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlDoesntStartWith;
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlEquals; import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlEquals;
@ -147,6 +148,27 @@ public class LegacyLogoutTest extends AbstractTestRealmKeycloakTest {
assertCurrentUrlEquals(APP_REDIRECT_URI); assertCurrentUrlEquals(APP_REDIRECT_URI);
} }
// Test with "post_logout_redirect_uri" without "id_token_hint": User should confirm logout.
@Test
public void logoutWithPostLogoutUriWithoutIdTokenHint() {
OAuthClient.AccessTokenResponse tokenResponse = loginUser();
String sessionId = tokenResponse.getSessionState();
String logoutUrl = oauth.getLogoutUrl().postLogoutRedirectUri(APP_REDIRECT_URI).build();
driver.navigate().to(logoutUrl);
// Assert logout confirmation page. Session still exists. Assert default language on logout page (English)
logoutConfirmPage.assertCurrent();
assertThat(true, is(isSessionActive(sessionId)));
events.assertEmpty();
logoutConfirmPage.confirmLogout();
// Redirected back to the application with expected state
events.expectLogout(sessionId).removeDetail(Details.REDIRECT_URI).assertEvent();
assertThat(false, is(isSessionActive(sessionId)));
assertCurrentUrlEquals(APP_REDIRECT_URI);
}
// KEYCLOAK-16517 Make sure that just real clients with standardFlow or implicitFlow enabled are considered for redirectUri // KEYCLOAK-16517 Make sure that just real clients with standardFlow or implicitFlow enabled are considered for redirectUri
@Test @Test