diff --git a/docs/documentation/upgrading/topics/changes/changes-26_0_0.adoc b/docs/documentation/upgrading/topics/changes/changes-26_0_0.adoc index b8df6ccebd..8048a9589d 100644 --- a/docs/documentation/upgrading/topics/changes/changes-26_0_0.adoc +++ b/docs/documentation/upgrading/topics/changes/changes-26_0_0.adoc @@ -224,3 +224,14 @@ Update your custom embedded Infinispan cache configuration file with configurati For more details proceed to the https://www.keycloak.org/server/caching[Configuring distributed caches] guide. += Support for legacy `redirect_uri` parameter and SPI options has been removed + +Previous versions of {project_name} had supported automatic logout of the user and redirecting to the application by opening logout endpoint URL such as +`http(s)://example-host/auth/realms/my-realm-name/protocol/openid-connect/logout?redirect_uri=encodedRedirectUri`. This functionality was deprecated in {project_name} 18 and has been removed in this version in favor of following the OpenID Connect specification. + +As part of this change the following related configuration options for the SPI have been removed: + +- `--spi-login-protocol-openid-connect-legacy-logout-redirect-uri` +- `--spi-login-protocol-openid-connect-suppress-logout-confirmation-screen` + +If you were still making use these options or the `redirect_uri` parameter for logout you should implement the link:https://openid.net/specs/openid-connect-rpinitiated-1_0.html[OpenID Connect RP-Initiated Logout specification] instead. diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolFactory.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolFactory.java index 689a5f089e..79e0b7d36e 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolFactory.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolFactory.java @@ -109,21 +109,9 @@ public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory { public static final String ROLES_SCOPE_CONSENT_TEXT = "${rolesScopeConsentText}"; public static final String ORGANIZATION_SCOPE_CONSENT_TEXT = "${organizationScopeConsentText}"; - public static final String CONFIG_LEGACY_LOGOUT_REDIRECT_URI = "legacy-logout-redirect-uri"; - public static final String SUPPRESS_LOGOUT_CONFIRMATION_SCREEN = "suppress-logout-confirmation-screen"; - - private OIDCProviderConfig providerConfig; - @Override public void init(Config.Scope config) { initBuiltIns(); - this.providerConfig = new OIDCProviderConfig(config); - if (providerConfig.isLegacyLogoutRedirectUri()) { - logger.warnf("Deprecated switch '%s' is enabled. Please try to disable it and update your clients to use OpenID Connect compliant way for RP-initiated logout.", CONFIG_LEGACY_LOGOUT_REDIRECT_URI); - } - if (providerConfig.suppressLogoutConfirmationScreen()) { - logger.warnf("Deprecated switch '%s' is enabled. Please try to disable it and update your clients to use OpenID Connect compliant way for RP-initiated logout.", SUPPRESS_LOGOUT_CONFIRMATION_SCREEN); - } } @Override @@ -444,7 +432,7 @@ public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory { @Override public Object createProtocolEndpoint(KeycloakSession session, EventBuilder event) { - return new OIDCLoginProtocolService(session, event, providerConfig); + return new OIDCLoginProtocolService(session, event); } @Override diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java index a21a706085..968e61a773 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java @@ -64,7 +64,6 @@ public class OIDCLoginProtocolService { private final RealmModel realm; private final TokenManager tokenManager; private final EventBuilder event; - private final OIDCProviderConfig providerConfig; private final KeycloakSession session; @@ -74,13 +73,12 @@ public class OIDCLoginProtocolService { private final ClientConnection clientConnection; - public OIDCLoginProtocolService(KeycloakSession session, EventBuilder event, OIDCProviderConfig providerConfig) { + public OIDCLoginProtocolService(KeycloakSession session, EventBuilder event) { this.session = session; this.clientConnection = session.getContext().getConnection(); this.realm = session.getContext().getRealm(); this.tokenManager = new TokenManager(); this.event = event; - this.providerConfig = providerConfig; this.request = session.getContext().getHttpRequest(); this.headers = session.getContext().getRequestHeaders(); } @@ -212,11 +210,9 @@ public class OIDCLoginProtocolService { return new UserInfoEndpoint(session, tokenManager); } - /* old deprecated logout endpoint needs to be removed in the future - * https://issues.redhat.com/browse/KEYCLOAK-2940 */ @Path("logout") public Object logout() { - return new LogoutEndpoint(session, tokenManager, event, providerConfig); + return new LogoutEndpoint(session, tokenManager, event); } @Path("revoke") diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCProviderConfig.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCProviderConfig.java deleted file mode 100644 index b15cf42a37..0000000000 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCProviderConfig.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2022 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package org.keycloak.protocol.oidc; - -import org.keycloak.Config; - -/** - * @author Marek Posolda - */ -public class OIDCProviderConfig { - - private final boolean legacyLogoutRedirectUri; - private final boolean suppressLogoutConfirmationScreen; - - public OIDCProviderConfig(Config.Scope config) { - this.legacyLogoutRedirectUri = config.getBoolean(OIDCLoginProtocolFactory.CONFIG_LEGACY_LOGOUT_REDIRECT_URI, false); - this.suppressLogoutConfirmationScreen = config.getBoolean(OIDCLoginProtocolFactory.SUPPRESS_LOGOUT_CONFIRMATION_SCREEN, false); - } - - public boolean isLegacyLogoutRedirectUri() { - return legacyLogoutRedirectUri; - } - - public boolean suppressLogoutConfirmationScreen() { - return suppressLogoutConfirmationScreen; - } -} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java index a6c1e32885..4512c2c5cd 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java @@ -48,8 +48,6 @@ import org.keycloak.protocol.oidc.BackchannelLogoutResponse; import org.keycloak.protocol.oidc.LogoutTokenValidationCode; import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; import org.keycloak.protocol.oidc.OIDCLoginProtocol; -import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory; -import org.keycloak.protocol.oidc.OIDCProviderConfig; import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil; import org.keycloak.protocol.oidc.utils.LogoutUtil; @@ -112,17 +110,15 @@ public class LogoutEndpoint { private final TokenManager tokenManager; private final RealmModel realm; private final EventBuilder event; - private final OIDCProviderConfig providerConfig; private Cors cors; - public LogoutEndpoint(KeycloakSession session, TokenManager tokenManager, EventBuilder event, OIDCProviderConfig providerConfig) { + public LogoutEndpoint(KeycloakSession session, TokenManager tokenManager, EventBuilder event) { this.session = session; this.clientConnection = session.getContext().getConnection(); this.tokenManager = tokenManager; this.realm = session.getContext().getRealm(); this.event = event; - this.providerConfig = providerConfig; this.request = session.getContext().getHttpRequest(); this.headers = session.getContext().getRequestHeaders(); } @@ -143,7 +139,6 @@ public class LogoutEndpoint { * * All parameters are optional. Some combinations of parameters are invalid as described in the specification * - * @param deprecatedRedirectUri Parameter "redirect_uri" is not supported by the specification. It is here just for the backwards compatibility * @param encodedIdToken Parameter "id_token_hint" as described in the specification. * @param clientId Parameter "client_id" as described in the specification. * @param postLogoutRedirectUri Parameter "post_logout_redirect_uri" as described in the specification with the URL to redirect after logout. @@ -154,39 +149,23 @@ public class LogoutEndpoint { */ @GET @NoCache - public Response logout(@QueryParam(OIDCLoginProtocol.REDIRECT_URI_PARAM) String deprecatedRedirectUri, // deprecated - @QueryParam(OIDCLoginProtocol.ID_TOKEN_HINT) String encodedIdToken, + public Response logout(@QueryParam(OIDCLoginProtocol.ID_TOKEN_HINT) String encodedIdToken, @QueryParam(OIDCLoginProtocol.CLIENT_ID_PARAM) String clientId, @QueryParam(OIDCLoginProtocol.POST_LOGOUT_REDIRECT_URI_PARAM) String postLogoutRedirectUri, @QueryParam(OIDCLoginProtocol.STATE_PARAM) String state, @QueryParam(OIDCLoginProtocol.UI_LOCALES_PARAM) String uiLocales, @QueryParam(AuthenticationManager.INITIATING_IDP_PARAM) String initiatingIdp) { - if (!providerConfig.isLegacyLogoutRedirectUri()) { - if (deprecatedRedirectUri != null) { - event.event(EventType.LOGOUT); - String errorMessage = "Parameter 'redirect_uri' no longer supported."; - event.detail(Details.REASON, errorMessage); - event.error(Errors.INVALID_REQUEST); - logger.warnf("%s 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.", - errorMessage, 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); - String errorMessage = "Either the parameter 'client_id' or the parameter 'id_token_hint' is required when 'post_logout_redirect_uri' is used."; - event.detail(Details.REASON, errorMessage); - event.error(Errors.INVALID_REQUEST); - logger.warnf(errorMessage); - return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.MISSING_PARAMETER, - OIDCLoginProtocol.ID_TOKEN_HINT); - } + if (postLogoutRedirectUri != null && encodedIdToken == null && clientId == null) { + event.event(EventType.LOGOUT); + String errorMessage = "Either the parameter 'client_id' or the parameter 'id_token_hint' is required when 'post_logout_redirect_uri' is used."; + event.detail(Details.REASON, errorMessage); + event.error(Errors.INVALID_REQUEST); + logger.warnf(errorMessage); + return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.MISSING_PARAMETER, + OIDCLoginProtocol.ID_TOKEN_HINT); } - deprecatedRedirectUri = providerConfig.isLegacyLogoutRedirectUri() ? deprecatedRedirectUri : null; - final String redirectUri = postLogoutRedirectUri != null ? postLogoutRedirectUri : deprecatedRedirectUri; - boolean confirmationNeeded = true; boolean forcedConfirmation = false; ClientModel client = clientId == null ? null : realm.getClientByClientId(clientId); @@ -236,21 +215,16 @@ public class LogoutEndpoint { } String validatedRedirectUri = null; - if (redirectUri != null) { + if (postLogoutRedirectUri != null) { if (client != null) { OIDCAdvancedConfigWrapper wrapper = OIDCAdvancedConfigWrapper.fromClientModel(client); Set postLogoutRedirectUris = wrapper.getPostLogoutRedirectUris() != null ? new HashSet(wrapper.getPostLogoutRedirectUris()) : new HashSet<>(); - validatedRedirectUri = RedirectUtils.verifyRedirectUri(session, client.getRootUrl(), redirectUri, postLogoutRedirectUris, true); - } else if (clientId == null && providerConfig.isLegacyLogoutRedirectUri()) { - /* - * Only call verifyRealmRedirectUri against all in the realm, in case when "Legacy" switch is enabled and when we don't have a client - usually due both clientId and client are null - */ - validatedRedirectUri = RedirectUtils.verifyRealmRedirectUri(session, redirectUri); + validatedRedirectUri = RedirectUtils.verifyRedirectUri(session, client.getRootUrl(), postLogoutRedirectUri, postLogoutRedirectUris, true); } if (validatedRedirectUri == null) { event.event(EventType.LOGOUT); - event.detail(Details.REDIRECT_URI, redirectUri); + event.detail(Details.REDIRECT_URI, postLogoutRedirectUri); event.error(Errors.INVALID_REDIRECT_URI); return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REDIRECT_URI); } @@ -307,7 +281,7 @@ public class LogoutEndpoint { } // Logout confirmation screen will be displayed to the user in this case - if ((confirmationNeeded || forcedConfirmation) && !providerConfig.suppressLogoutConfirmationScreen()) { + if (confirmationNeeded || forcedConfirmation) { return displayLogoutConfirmationScreen(loginForm, logoutSession); } else { return doBrowserLogout(logoutSession); @@ -338,13 +312,14 @@ public class LogoutEndpoint { if (form.containsKey(OAuth2Constants.REFRESH_TOKEN)) { return logoutToken(); } else { - return logout(form.getFirst(OIDCLoginProtocol.REDIRECT_URI_PARAM), + return logout( 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)); + form.getFirst(AuthenticationManager.INITIATING_IDP_PARAM) + ); } } @@ -701,7 +676,7 @@ public class LogoutEndpoint { return backchannelLogoutResponse; } - + private boolean oneOrMoreDownstreamLogoutsFailed(BackchannelLogoutResponse backchannelLogoutResponse) { BackchannelLogoutResponse filteredBackchannelLogoutResponse = new BackchannelLogoutResponse(); for (BackchannelLogoutResponse.DownStreamBackchannelLogoutResponse response : backchannelLogoutResponse diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java index de8fd84e0a..500cc2bb80 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java @@ -24,7 +24,6 @@ import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakUriInfo; import org.keycloak.models.RealmModel; -import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.services.Urls; import org.keycloak.services.util.ResolveRelative; @@ -33,7 +32,6 @@ import java.util.Collection; import java.util.Set; import java.util.TreeSet; import java.util.regex.Pattern; -import java.util.stream.Collectors; /** * @author Stian Thorgersen @@ -42,17 +40,6 @@ public class RedirectUtils { private static final Logger logger = Logger.getLogger(RedirectUtils.class); - /** - * This method is deprecated for performance and security reasons and it is available just for the - * backwards compatibility. It is recommended to use some other methods of this class where the client is given as an argument - * to the method, so we know the client, which redirect-uri we are trying to resolve. - */ - @Deprecated - public static String verifyRealmRedirectUri(KeycloakSession session, String redirectUri) { - Set validRedirects = getValidateRedirectUris(session); - return verifyRedirectUri(session, null, redirectUri, validRedirects, true); - } - public static String verifyRedirectUri(KeycloakSession session, String redirectUri, ClientModel client) { return verifyRedirectUri(session, redirectUri, client, true); } @@ -77,16 +64,6 @@ public class RedirectUtils { return resolveValidRedirects; } - @Deprecated - private static Set getValidateRedirectUris(KeycloakSession session) { - RealmModel realm = session.getContext().getRealm(); - return session.clients().getAllRedirectUrisOfEnabledClients(realm).entrySet().stream() - .filter(me -> me.getKey().isEnabled() && OIDCLoginProtocol.LOGIN_PROTOCOL.equals(me.getKey().getProtocol()) && !me.getKey().isBearerOnly() && (me.getKey().isStandardFlowEnabled() || me.getKey().isImplicitFlowEnabled())) - .map(me -> resolveValidRedirects(session, me.getKey().getRootUrl(), me.getValue())) - .flatMap(Collection::stream) - .collect(Collectors.toSet()); - } - public static String verifyRedirectUri(KeycloakSession session, String rootUrl, String redirectUri, Set validRedirects, boolean requireRedirectUri) { KeycloakUriInfo uriInfo = session.getContext().getUri(); RealmModel realm = session.getContext().getRealm(); diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java index 046bbdf3f5..a07a5e8293 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java @@ -232,14 +232,6 @@ public class OAuthClient { return this; } - @Deprecated // Use only in backwards compatibility tests - public LogoutUrlBuilder redirectUri(String redirectUri) { - if (redirectUri != null) { - b.queryParam(OAuth2Constants.REDIRECT_URI, redirectUri); - } - return this; - } - public LogoutUrlBuilder state(String state) { if (state != null) { b.queryParam(OIDCLoginProtocol.STATE_PARAM, state); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSignedBrokerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSignedBrokerTest.java index bf65998d30..1a53300d44 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSignedBrokerTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSignedBrokerTest.java @@ -1,5 +1,6 @@ package org.keycloak.testsuite.broker; +import org.keycloak.OAuth2Constants; import org.keycloak.broker.saml.SAMLIdentityProviderConfig; import org.keycloak.crypto.Algorithm; import org.keycloak.dom.saml.v2.protocol.AuthnRequestType; @@ -21,6 +22,7 @@ import org.keycloak.testsuite.updaters.ClientAttributeUpdater; import org.keycloak.testsuite.updaters.IdentityProviderAttributeUpdater; import org.keycloak.testsuite.updaters.RealmAttributeUpdater; import org.keycloak.testsuite.util.KeyUtils; +import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.SamlClient; import org.keycloak.testsuite.util.SamlClient.Binding; import org.keycloak.testsuite.util.SamlClientBuilder; @@ -148,8 +150,14 @@ public class KcSamlSignedBrokerTest extends AbstractBrokerTest { loginUser(); // Logout should fail because logout response is not signed. + final String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + final OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password"); + final String idTokenString = tokenResponse.getIdToken(); final String redirectUri = getAccountUrl(getProviderRoot(), bc.providerRealmName()); - final String logoutUri = oauth.realm(bc.providerRealmName()).getLogoutUrl().redirectUri(redirectUri).build(); + final String logoutUri = oauth.realm(bc.providerRealmName()).getLogoutUrl() + .idTokenHint(idTokenString) + .postLogoutRedirectUri(redirectUri).build(); + driver.navigate().to(logoutUri); errorPage.assertCurrent(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LegacyLogoutTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LegacyLogoutTest.java deleted file mode 100644 index 3b71751ee9..0000000000 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LegacyLogoutTest.java +++ /dev/null @@ -1,273 +0,0 @@ -/* - * Copyright 2022 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package org.keycloak.testsuite.oauth; - -import java.io.Closeable; -import java.util.Collections; - -import jakarta.ws.rs.NotFoundException; - -import org.hamcrest.MatcherAssert; -import org.jboss.arquillian.graphene.page.Page; -import org.junit.After; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.keycloak.OAuth2Constants; -import org.keycloak.admin.client.resource.ClientResource; -import org.keycloak.admin.client.resource.ClientsResource; -import org.keycloak.events.Details; -import org.keycloak.events.Errors; -import org.keycloak.protocol.LoginProtocol; -import org.keycloak.protocol.oidc.OIDCLoginProtocol; -import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory; -import org.keycloak.representations.idm.ClientRepresentation; -import org.keycloak.representations.idm.RealmRepresentation; -import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; -import org.keycloak.testsuite.Assert; -import org.keycloak.testsuite.AssertEvents; -import org.keycloak.testsuite.admin.ApiUtil; -import org.keycloak.testsuite.pages.AppPage; -import org.keycloak.testsuite.pages.ErrorPage; -import org.keycloak.testsuite.pages.InfoPage; -import org.keycloak.testsuite.pages.LoginPage; -import org.keycloak.testsuite.pages.LogoutConfirmPage; -import org.keycloak.testsuite.pages.OAuthGrantPage; -import org.keycloak.testsuite.updaters.ClientAttributeUpdater; -import org.keycloak.testsuite.util.InfinispanTestTimeServiceRule; -import org.keycloak.testsuite.util.OAuthClient; -import org.keycloak.testsuite.util.ServerURLs; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlDoesntStartWith; -import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlEquals; - -/** - * Test logout endpoint with deprecated "redirect_uri" parameter - * - * @author Marek Posolda - */ -public class LegacyLogoutTest extends AbstractTestRealmKeycloakTest { - - @Rule - public AssertEvents events = new AssertEvents(this); - - @Rule - public InfinispanTestTimeServiceRule ispnTestTimeService = new InfinispanTestTimeServiceRule(this); - - @Page - protected AppPage appPage; - - @Page - protected LoginPage loginPage; - - @Page - protected OAuthGrantPage grantPage; - - @Page - protected LogoutConfirmPage logoutConfirmPage; - - @Page - protected InfoPage infoPage; - - @Page - private ErrorPage errorPage; - - private String APP_REDIRECT_URI; - - @Override - public void configureTestRealm(RealmRepresentation testRealm) { - } - - @Before - public void configLegacyRedirectUriEnabled() { - getTestingClient().testing().setSystemPropertyOnServer("oidc." + OIDCLoginProtocolFactory.CONFIG_LEGACY_LOGOUT_REDIRECT_URI, "true"); - getTestingClient().testing().reinitializeProviderFactoryWithSystemPropertiesScope(LoginProtocol.class.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL, "oidc."); - - APP_REDIRECT_URI = oauth.APP_AUTH_ROOT; - } - - @After - public void revertConfiguration() { - getTestingClient().testing().setSystemPropertyOnServer("oidc." + OIDCLoginProtocolFactory.CONFIG_LEGACY_LOGOUT_REDIRECT_URI, "false"); - getTestingClient().testing().setSystemPropertyOnServer("oidc." + OIDCLoginProtocolFactory.SUPPRESS_LOGOUT_CONFIRMATION_SCREEN, "false"); - getTestingClient().testing().reinitializeProviderFactoryWithSystemPropertiesScope(LoginProtocol.class.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL, "oidc."); - } - - - // Test logout with deprecated "redirect_uri" and with "id_token_hint" . Should od automatic redirect - @Test - public void logoutWithLegacyRedirectUriAndIdTokenHint() throws Exception { - OAuthClient.AccessTokenResponse tokenResponse = loginUser(); - String idTokenString = tokenResponse.getIdToken(); - String sessionId = tokenResponse.getSessionState(); - - String logoutUrl = oauth.getLogoutUrl().redirectUri(APP_REDIRECT_URI).idTokenHint(idTokenString).build(); - driver.navigate().to(logoutUrl); - - events.expectLogout(sessionId).detail(Details.REDIRECT_URI, APP_REDIRECT_URI).assertEvent(); - assertThat(false, is(isSessionActive(sessionId))); - assertCurrentUrlEquals(APP_REDIRECT_URI); - } - - // Test logout with deprecated "redirect_uri" and without "id_token_hint" . User should confirm logout - @Test - public void logoutWithLegacyRedirectUriAndWithoutIdTokenHint() throws Exception { - OAuthClient.AccessTokenResponse tokenResponse = loginUser(); - String sessionId = tokenResponse.getSessionState(); - - String logoutUrl = oauth.getLogoutUrl().redirectUri(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).client("account").removeDetail(Details.REDIRECT_URI).assertEvent(); - assertThat(false, is(isSessionActive(sessionId))); - 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).client("account").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 - @Test - public void logoutRedirectWithStarRedirectUriForDirectGrantClient() { - // Set "*" as redirectUri for some directGrant client - ClientResource clientRes = ApiUtil.findClientByClientId(testRealm(), "direct-grant"); - ClientRepresentation clientRepOrig = clientRes.toRepresentation(); - ClientRepresentation clientRep = clientRes.toRepresentation(); - clientRep.setStandardFlowEnabled(false); - clientRep.setImplicitFlowEnabled(false); - clientRep.setRedirectUris(Collections.singletonList("*")); - clientRes.update(clientRep); - - try { - OAuthClient.AccessTokenResponse tokenResponse = loginUser(); - - String invalidRedirectUri = ServerURLs.getAuthServerContextRoot() + "/bar"; - - String idTokenString = tokenResponse.getIdToken(); - - String logoutUrl = oauth.getLogoutUrl().redirectUri(invalidRedirectUri).build(); - driver.navigate().to(logoutUrl); - - events.expectLogoutError(Errors.INVALID_REDIRECT_URI).assertEvent(); - - assertCurrentUrlDoesntStartWith(invalidRedirectUri); - errorPage.assertCurrent(); - Assert.assertEquals("Invalid redirect uri", errorPage.getError()); - - // Session still active - assertThat(true, is(isSessionActive(tokenResponse.getSessionState()))); - } finally { - // Revert - clientRes.update(clientRepOrig); - } - } - - // Test logout with deprecated "redirect_uri" and without "id_token_hint" and client disabled after login - @Test - public void logoutWithLegacyRedirectUriAndWithoutIdTokenHintClientDisabled() throws Exception { - OAuthClient.AccessTokenResponse tokenResponse = loginUser(); - String sessionId = tokenResponse.getSessionState(); - - try (Closeable testAppClient = ClientAttributeUpdater.forClient(adminClient, "test", oauth.getClientId()) - .setEnabled(false).update()) { - - ClientsResource clients = adminClient.realm(oauth.getRealm()).clients(); - ClientRepresentation rep = clients.findByClientId(oauth.getClientId()).get(0); - MatcherAssert.assertThat(false, is(rep.isEnabled())); - - String logoutUrl = oauth.getLogoutUrl().redirectUri(APP_REDIRECT_URI).build(); - driver.navigate().to(logoutUrl); - - // Assert logout confirmation page. Session still exists. Assert default language on logout page (English) - logoutConfirmPage.assertCurrent(); - MatcherAssert.assertThat(true, is(isSessionActive(sessionId))); - events.assertEmpty(); - logoutConfirmPage.confirmLogout(); - - // Redirected back to the application with expected state - events.expectLogout(sessionId).client("account").removeDetail(Details.REDIRECT_URI).assertEvent(); - MatcherAssert.assertThat(false, is(isSessionActive(sessionId))); - assertCurrentUrlEquals(APP_REDIRECT_URI); - } - } - - // Test with "post_logout_redirect_uri" without "id_token_hint" and "suppress-logout-confirmation-screen": User should logout non interactive. - @Test - public void logoutWithPostLogoutUriWithoutIdTokenHintAndSuppressedConfirmation() { - getTestingClient().testing().setSystemPropertyOnServer("oidc." + OIDCLoginProtocolFactory.SUPPRESS_LOGOUT_CONFIRMATION_SCREEN, "true"); - getTestingClient().testing().reinitializeProviderFactoryWithSystemPropertiesScope(LoginProtocol.class.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL, "oidc."); - - OAuthClient.AccessTokenResponse tokenResponse = loginUser(); - String sessionId = tokenResponse.getSessionState(); - - String logoutUrl = oauth.getLogoutUrl().postLogoutRedirectUri(APP_REDIRECT_URI).build(); - driver.navigate().to(logoutUrl); - - events.expectLogout(sessionId).client("account").detail(Details.REDIRECT_URI, APP_REDIRECT_URI).assertEvent(); - assertThat(false, is(isSessionActive(sessionId))); - assertCurrentUrlEquals(APP_REDIRECT_URI); - } - - private OAuthClient.AccessTokenResponse loginUser() { - oauth.doLogin("test-user@localhost", "password"); - String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); - - oauth.clientSessionState("client-session"); - OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password"); - events.clear(); - return tokenResponse; - } - - private boolean isSessionActive(String sessionId) { - try { - testingClient.testing().getClientSessionsCountInUserSession("test", sessionId); - return true; - } catch (NotFoundException nfe) { - return false; - } - } -} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RPInitiatedLogoutTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RPInitiatedLogoutTest.java index e5b9bfe16c..fdc54426a9 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RPInitiatedLogoutTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RPInitiatedLogoutTest.java @@ -403,29 +403,6 @@ public class RPInitiatedLogoutTest extends AbstractTestRealmKeycloakTest { } - // Parameter "redirect_uri" is not valid in logoutRequest (See LegacyLogoutTest for the scenario with "redirect_uri" allowed by backwards compatibility switch) - @Test - public void logoutWithRedirectUriParameterShouldFail() throws Exception { - OAuthClient.AccessTokenResponse tokenResponse = loginUser(); - String idTokenString = tokenResponse.getIdToken(); - - // Logout with "redirect_uri" parameter alone should fail - String logoutUrl = oauth.getLogoutUrl().redirectUri(APP_REDIRECT_URI).build(); - driver.navigate().to(logoutUrl); - errorPage.assertCurrent(); - events.expectLogoutError(OAuthErrorException.INVALID_REQUEST).assertEvent(); - - // Logout with "redirect_uri" parameter and with "id_token_hint" should fail - oauth.getLogoutUrl().idTokenHint(idTokenString).redirectUri(APP_REDIRECT_URI).build(); - driver.navigate().to(logoutUrl); - errorPage.assertCurrent(); - events.expectLogoutError(OAuthErrorException.INVALID_REQUEST).assertEvent(); - - // Assert user still authenticated - MatcherAssert.assertThat(true, is(isSessionActive(tokenResponse.getSessionState()))); - } - - // Test with "post_logout_redirect_uri" without "id_token_hint" should fail @Test public void logoutWithPostLogoutUriWithoutIdTokenHintShouldFail() throws Exception { diff --git a/testsuite/utils/src/main/resources/META-INF/keycloak-server.json b/testsuite/utils/src/main/resources/META-INF/keycloak-server.json index fd458cd227..04a94c75f5 100755 --- a/testsuite/utils/src/main/resources/META-INF/keycloak-server.json +++ b/testsuite/utils/src/main/resources/META-INF/keycloak-server.json @@ -148,9 +148,6 @@ }, "login-protocol": { - "openid-connect": { - "legacy-logout-redirect-uri": "${keycloak.oidc.legacyLogoutRedirectUri:false}" - }, "saml": { "knownProtocols": [ "http=${auth.server.http.port}",