diff --git a/misc/keycloak-test-helper/pom.xml b/misc/keycloak-test-helper/pom.xml index 61a22a603d..2524add6e8 100644 --- a/misc/keycloak-test-helper/pom.xml +++ b/misc/keycloak-test-helper/pom.xml @@ -37,5 +37,9 @@ selenium-java provided + + org.keycloak + keycloak-services + diff --git a/misc/keycloak-test-helper/src/main/java/org/keycloak/test/builders/ClientBuilder.java b/misc/keycloak-test-helper/src/main/java/org/keycloak/test/builders/ClientBuilder.java index b4c78bcd7a..49a1d6ae58 100644 --- a/misc/keycloak-test-helper/src/main/java/org/keycloak/test/builders/ClientBuilder.java +++ b/misc/keycloak-test-helper/src/main/java/org/keycloak/test/builders/ClientBuilder.java @@ -17,6 +17,7 @@ package org.keycloak.test.builders; +import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; import org.keycloak.representations.idm.ClientRepresentation; import java.util.Collections; @@ -88,6 +89,9 @@ public class ClientBuilder { if (rep.getRedirectUris() == null && rep.getRootUrl() != null) rep.setRedirectUris(Collections.singletonList(rep.getRootUrl().concat("/*"))); + if (OIDCAdvancedConfigWrapper.fromClientRepresentation(rep).getPostLogoutRedirectUris() == null) { + OIDCAdvancedConfigWrapper.fromClientRepresentation(rep).setPostLogoutRedirectUris(Collections.singletonList("+")); + } return rep; } diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/custom/JpaUpdate19_0_0_DefaultPostLogoutRedirectUri.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/custom/JpaUpdate19_0_0_DefaultPostLogoutRedirectUri.java new file mode 100644 index 0000000000..9499c46ed8 --- /dev/null +++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/custom/JpaUpdate19_0_0_DefaultPostLogoutRedirectUri.java @@ -0,0 +1,53 @@ +/* + * Copyright 2020 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.connections.jpa.updater.liquibase.custom; + +import liquibase.exception.CustomChangeException; +import liquibase.statement.core.InsertStatement; +import liquibase.structure.core.Table; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; + +public class JpaUpdate19_0_0_DefaultPostLogoutRedirectUri extends CustomKeycloakTask { + + private static final String POST_LOGOUT_REDIRECT_URIS = "post.logout.redirect.uris"; + + @Override + protected void generateStatementsImpl() throws CustomChangeException { + String sql = "SELECT DISTINCT CLIENT_ID FROM " + getTableName("REDIRECT_URIS"); + + try (PreparedStatement statement = jdbcConnection.prepareStatement(sql); ResultSet rs = statement.executeQuery()) { + while (rs.next()) { + statements.add( + new InsertStatement(null, null, database.correctObjectName("CLIENT_ATTRIBUTES", Table.class)) + .addColumnValue("CLIENT_ID", rs.getString(1)) + .addColumnValue("NAME", POST_LOGOUT_REDIRECT_URIS) + .addColumnValue("VALUE", "+") + ); + } + } catch (Exception e) { + throw new CustomChangeException(getTaskId() + ": Exception when extracting data from previous version", e); + } + } + + @Override + protected String getTaskId() { + return "Default post_logout_redirect_uris (19.0.0)"; + } + +} diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-19.0.0.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-19.0.0.xml new file mode 100644 index 0000000000..15adbed54d --- /dev/null +++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-19.0.0.xml @@ -0,0 +1,24 @@ + + + + + + + + + \ No newline at end of file diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml index 348d87c9aa..4dc8d77b2a 100755 --- a/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml +++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml @@ -73,5 +73,6 @@ + diff --git a/model/legacy-private/src/main/java/org/keycloak/storage/datastore/LegacyExportImportManager.java b/model/legacy-private/src/main/java/org/keycloak/storage/datastore/LegacyExportImportManager.java index 2caa8531ff..19245e82bd 100644 --- a/model/legacy-private/src/main/java/org/keycloak/storage/datastore/LegacyExportImportManager.java +++ b/model/legacy-private/src/main/java/org/keycloak/storage/datastore/LegacyExportImportManager.java @@ -55,6 +55,7 @@ import org.keycloak.models.utils.DefaultKeyProviders; import org.keycloak.models.utils.DefaultRequiredActions; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.RepresentationToModel; +import org.keycloak.protocol.oidc.OIDCConfigAttributes; import org.keycloak.representations.idm.ApplicationRepresentation; import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation; import org.keycloak.representations.idm.AuthenticationFlowRepresentation; @@ -464,6 +465,10 @@ public class LegacyExportImportManager implements ExportImportManager { Map appMap = new HashMap(); for (ClientRepresentation resourceRep : rep.getClients()) { ClientModel app = RepresentationToModel.createClient(session, realm, resourceRep, mappedFlows); + String postLogoutRedirectUris = app.getAttribute(OIDCConfigAttributes.POST_LOGOUT_REDIRECT_URIS); + if (postLogoutRedirectUris == null) { + app.setAttribute(OIDCConfigAttributes.POST_LOGOUT_REDIRECT_URIS, "+"); + } appMap.put(app.getClientId(), app); ValidationUtil.validateClient(session, app, false, r -> { diff --git a/model/map/src/main/java/org/keycloak/models/map/datastore/MapExportImportManager.java b/model/map/src/main/java/org/keycloak/models/map/datastore/MapExportImportManager.java index edb167726d..2c3756e78c 100644 --- a/model/map/src/main/java/org/keycloak/models/map/datastore/MapExportImportManager.java +++ b/model/map/src/main/java/org/keycloak/models/map/datastore/MapExportImportManager.java @@ -51,6 +51,7 @@ import org.keycloak.models.utils.DefaultKeyProviders; import org.keycloak.models.utils.DefaultRequiredActions; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.RepresentationToModel; +import org.keycloak.protocol.oidc.OIDCConfigAttributes; import org.keycloak.representations.idm.ApplicationRepresentation; import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation; import org.keycloak.representations.idm.AuthenticationFlowRepresentation; @@ -449,6 +450,10 @@ public class MapExportImportManager implements ExportImportManager { Map appMap = new HashMap<>(); for (ClientRepresentation resourceRep : rep.getClients()) { ClientModel app = RepresentationToModel.createClient(session, realm, resourceRep, mappedFlows); + String postLogoutRedirectUris = app.getAttribute(OIDCConfigAttributes.POST_LOGOUT_REDIRECT_URIS); + if (postLogoutRedirectUris == null) { + app.setAttribute(OIDCConfigAttributes.POST_LOGOUT_REDIRECT_URIS, "+"); + } appMap.put(app.getClientId(), app); ValidationUtil.validateClient(session, app, false, r -> { diff --git a/server-spi-private/src/main/java/org/keycloak/protocol/oidc/OIDCConfigAttributes.java b/server-spi-private/src/main/java/org/keycloak/protocol/oidc/OIDCConfigAttributes.java index 11cb6cbe8a..bf66de0982 100644 --- a/server-spi-private/src/main/java/org/keycloak/protocol/oidc/OIDCConfigAttributes.java +++ b/server-spi-private/src/main/java/org/keycloak/protocol/oidc/OIDCConfigAttributes.java @@ -82,6 +82,8 @@ public final class OIDCConfigAttributes { public static final String FRONT_CHANNEL_LOGOUT_URI = "frontchannel.logout.url"; public static final String FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED = "frontchannel.logout.session.required"; + public static final String POST_LOGOUT_REDIRECT_URIS = "post.logout.redirect.uris"; + private OIDCConfigAttributes() { } diff --git a/services/src/main/java/org/keycloak/partialimport/ClientsPartialImport.java b/services/src/main/java/org/keycloak/partialimport/ClientsPartialImport.java index 2382f1c8bb..546349452d 100755 --- a/services/src/main/java/org/keycloak/partialimport/ClientsPartialImport.java +++ b/services/src/main/java/org/keycloak/partialimport/ClientsPartialImport.java @@ -24,6 +24,7 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.RepresentationToModel; +import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.PartialImportRepresentation; import org.keycloak.representations.idm.ProtocolMapperRepresentation; @@ -118,6 +119,9 @@ public class ClientsPartialImport extends AbstractPartialImport getPostLogoutRedirectUris() { + List postLogoutRedirectUris = getAttributeMultivalued(OIDCConfigAttributes.POST_LOGOUT_REDIRECT_URIS); + if(postLogoutRedirectUris == null || postLogoutRedirectUris.isEmpty()) { + return null; + } + else if (postLogoutRedirectUris.get(0).equals("+")) { + if(clientModel != null) { + return new ArrayList(clientModel.getRedirectUris()); + } + else if(clientRep != null) { + return clientRep.getRedirectUris(); + } + return null; + } + else { + return postLogoutRedirectUris; + } + } + + public void setPostLogoutRedirectUris(List postLogoutRedirectUris) { + setAttributeMultivalued(OIDCConfigAttributes.POST_LOGOUT_REDIRECT_URIS, postLogoutRedirectUris); + } + } 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 edecd0d920..8c11866ff8 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,6 +48,7 @@ import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.SystemClientUtil; 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; @@ -92,6 +93,15 @@ import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; +import java.util.HashSet; +import java.util.Set; +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 Stian Thorgersen @@ -233,7 +243,9 @@ public class LogoutEndpoint { String validatedRedirectUri = null; if (redirectUri != null) { if (client != null) { - validatedRedirectUri = RedirectUtils.verifyRedirectUri(session, redirectUri, client); + 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) { /* * Only call verifyRealmRedirectUri, in case both clientId and client are null - otherwise diff --git a/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java b/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java index a756bd03c3..4be269f09c 100755 --- a/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java +++ b/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java @@ -208,6 +208,10 @@ public class DescriptionConverter { configWrapper.setTosUri(clientOIDC.getTosUri()); } + if (clientOIDC.getPostLogoutRedirectUris() != null) { + configWrapper.setPostLogoutRedirectUris(clientOIDC.getPostLogoutRedirectUris()); + } + // CIBA String backchannelTokenDeliveryMode = clientOIDC.getBackchannelTokenDeliveryMode(); if (backchannelTokenDeliveryMode != null) { @@ -403,6 +407,9 @@ public class DescriptionConverter { if (config.getTokenEndpointAuthSigningAlg() != null) { response.setTokenEndpointAuthSigningAlg(config.getTokenEndpointAuthSigningAlg()); } + if (config.getPostLogoutRedirectUris() != null) { + response.setPostLogoutRedirectUris(config.getPostLogoutRedirectUris()); + } response.setBackchannelLogoutUri(config.getBackchannelLogoutUrl()); response.setBackchannelLogoutSessionRequired(config.isBackchannelLogoutSessionRequired()); response.setBackchannelLogoutSessionRequired(config.getBackchannelLogoutRevokeOfflineTokens()); diff --git a/services/src/main/java/org/keycloak/services/managers/RealmManager.java b/services/src/main/java/org/keycloak/services/managers/RealmManager.java index 129ebb5dd8..28d4a6c8b4 100755 --- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java +++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java @@ -173,6 +173,7 @@ public class RealmManager { String baseUrl = "/admin/" + realm.getName() + "/console/"; adminConsole.setBaseUrl(baseUrl); adminConsole.addRedirectUri(baseUrl + "*"); + adminConsole.setAttribute(OIDCConfigAttributes.POST_LOGOUT_REDIRECT_URIS, "+"); adminConsole.setWebOrigins(Collections.singleton("+")); adminConsole.setEnabled(true); @@ -417,6 +418,7 @@ public class RealmManager { String baseUrl = "/realms/" + realm.getName() + "/account/"; accountClient.setBaseUrl(baseUrl); accountClient.addRedirectUri(baseUrl + "*"); + accountClient.setAttribute(OIDCConfigAttributes.POST_LOGOUT_REDIRECT_URIS, "+"); accountClient.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); @@ -451,6 +453,7 @@ public class RealmManager { accountConsoleClient.setRootUrl(Constants.AUTH_BASE_URL_PROP); accountConsoleClient.setBaseUrl(baseUrl); accountConsoleClient.addRedirectUri(baseUrl + "*"); + accountConsoleClient.setAttribute(OIDCConfigAttributes.POST_LOGOUT_REDIRECT_URIS, "+"); accountConsoleClient.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java index 3af96f76d5..e46a235822 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/concurrency/ConcurrentLoginTest.java @@ -54,6 +54,7 @@ import org.keycloak.jose.jws.JWSInput; import org.keycloak.models.UserSessionSpi; import org.keycloak.models.sessions.infinispan.InfinispanUserSessionProviderFactory; import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.protocol.oidc.OIDCConfigAttributes; import org.keycloak.representations.AccessToken; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.common.util.Retry; @@ -98,6 +99,7 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest { .directAccessGrants() .redirectUris("*") .addWebOrigin("*") + .attribute(OIDCConfigAttributes.POST_LOGOUT_REDIRECT_URIS, "+") .secret("password") .build(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerConfiguration.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerConfiguration.java index f21c4d6381..1faa992dae 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerConfiguration.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerConfiguration.java @@ -4,6 +4,7 @@ import org.keycloak.broker.oidc.OIDCIdentityProviderConfig; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.IdentityProviderSyncMode; import org.keycloak.protocol.ProtocolMapperUtils; +import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.mappers.HardcodedClaim; import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper; @@ -72,6 +73,8 @@ public class KcOidcBrokerConfiguration implements BrokerConfiguration { client.setAdminUrl(getConsumerRoot() + "/auth/realms/" + REALM_CONS_NAME + "/broker/" + IDP_OIDC_ALIAS + "/endpoint"); + OIDCAdvancedConfigWrapper.fromClientRepresentation(client).setPostLogoutRedirectUris(Collections.singletonList("+")); + ProtocolMapperRepresentation emailMapper = new ProtocolMapperRepresentation(); emailMapper.setName("email"); emailMapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); @@ -170,6 +173,8 @@ public class KcOidcBrokerConfiguration implements BrokerConfiguration { client.setBaseUrl(getConsumerRoot() + "/auth/realms/" + REALM_CONS_NAME + "/app"); + OIDCAdvancedConfigWrapper.fromClientRepresentation(client).setPostLogoutRedirectUris(Collections.singletonList("+")); + return Collections.singletonList(client); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AbstractClientPoliciesTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AbstractClientPoliciesTest.java index 16f2b14fe4..d5e784b86c 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AbstractClientPoliciesTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AbstractClientPoliciesTest.java @@ -633,6 +633,7 @@ public abstract class AbstractClientPoliciesTest extends AbstractKeycloakTest { clientRep.setPublicClient(Boolean.FALSE); clientRep.setServiceAccountsEnabled(Boolean.TRUE); clientRep.setRedirectUris(Collections.singletonList(ServerURLs.getAuthServerContextRoot() + "/auth/realms/master/app/auth")); + OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setPostLogoutRedirectUris(Collections.singletonList("+")); op.accept(clientRep); Response resp = adminClient.realm(REALM_NAME).clients().create(clientRep); if (resp.getStatus() == Response.Status.BAD_REQUEST.getStatusCode()) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPoliciesTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPoliciesTest.java index 06d66ec57a..dfbcd23960 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPoliciesTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPoliciesTest.java @@ -240,11 +240,14 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest { .clientId("test-device") .secret("secret") .attribute(OAuth2DeviceConfig.OAUTH2_DEVICE_AUTHORIZATION_GRANT_ENABLED, "true") + .attribute(OIDCConfigAttributes.POST_LOGOUT_REDIRECT_URIS, "+") .build(); clients.add(app); ClientRepresentation appPublic = ClientBuilder.create().id(KeycloakModelUtils.generateId()).publicClient() - .clientId(DEVICE_APP_PUBLIC).attribute(OAuth2DeviceConfig.OAUTH2_DEVICE_AUTHORIZATION_GRANT_ENABLED, "true") + .clientId(DEVICE_APP_PUBLIC) + .attribute(OAuth2DeviceConfig.OAUTH2_DEVICE_AUTHORIZATION_GRANT_ENABLED, "true") + .attribute(OIDCConfigAttributes.POST_LOGOUT_REDIRECT_URIS, "+") .build(); clients.add(appPublic); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java index 56a7b432ad..a87a68c086 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java @@ -52,6 +52,7 @@ import java.util.stream.Collectors; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.keycloak.testsuite.auth.page.AuthRealm.TEST; @@ -844,4 +845,27 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest { realmRep.getAttributes().remove(Constants.ACR_LOA_MAP); adminClient.realm("test").update(realmRep); } + + @Test + public void testPostLogoutRedirectUri() throws Exception { + OIDCClientRepresentation clientRep = createRep(); + clientRep.setPostLogoutRedirectUris(Collections.singletonList("http://redirect/logout")); + OIDCClientRepresentation response = reg.oidc().create(clientRep); + assertEquals("http://redirect/logout", response.getPostLogoutRedirectUris().get(0)); + } + + @Test + public void testPostLogoutRedirectUriPlus() throws Exception { + OIDCClientRepresentation clientRep = createRep(); + clientRep.setPostLogoutRedirectUris(Collections.singletonList("+")); + OIDCClientRepresentation response = reg.oidc().create(clientRep); + assertEquals("http://redirect", response.getPostLogoutRedirectUris().get(0)); + } + + @Test + public void testPostLogoutRedirectUriNull() throws Exception { + OIDCClientRepresentation clientRep = createRep(); + OIDCClientRepresentation response = reg.oidc().create(clientRep); + assertNull(response.getPostLogoutRedirectUris()); + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java index da609d03a4..2c151306da 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java @@ -32,6 +32,7 @@ import org.keycloak.models.LDAPConstants; import org.keycloak.models.credential.PasswordCredentialModel; import org.keycloak.models.credential.dto.PasswordCredentialData; import org.keycloak.models.utils.DefaultAuthenticationFlows; +import org.keycloak.protocol.oidc.OIDCConfigAttributes; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory; import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper; @@ -754,4 +755,14 @@ public class ExportImportUtil { OIDCLoginProtocolFactory.MICROPROFILE_JWT_SCOPE )); } + + public static void testDefaultPostLogoutRedirectUris(RealmResource realm) { + for (ClientRepresentation client : realm.clients().findAll()) { + List redirectUris = client.getRedirectUris(); + if(redirectUris != null && !redirectUris.isEmpty()) { + String postLogoutRedirectUris = client.getAttributes().get(OIDCConfigAttributes.POST_LOGOUT_REDIRECT_URIS); + Assert.assertEquals("+", postLogoutRedirectUris); + } + } + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java index 3124e185ba..aa2fcc4853 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java @@ -315,6 +315,10 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest { testRealmDefaultClientScopes(migrationRealm); } + protected void testMigrationTo19_0_0() { + testPostLogoutRedirectUrisSet(migrationRealm); + } + protected void testDeleteAccount(RealmResource realm) { ClientRepresentation accountClient = realm.clients().findByClientId(ACCOUNT_MANAGEMENT_CLIENT_ID).get(0); ClientResource accountResource = realm.clients().get(accountClient.getId()); @@ -728,6 +732,11 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest { ExportImportUtil.testClientDefaultClientScopes(realm); } + private void testPostLogoutRedirectUrisSet(RealmResource realm) { + log.info("Testing that POST_LOGOUT_REDIRECT_URI is set to '+' for all clients in " + realm.toRepresentation().getRealm()); + ExportImportUtil.testDefaultPostLogoutRedirectUris(realm); + } + private void testOfflineScopeAddedToClient() { log.infof("Testing offline_access optional scope present in realm %s for client migration-test-client", migrationRealm.toRepresentation().getRealm()); @@ -941,6 +950,10 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest { testMigrationTo18_0_0(); } + protected void testMigrationTo19_x() { + testMigrationTo19_0_0(); + } + protected void testMigrationTo7_x(boolean supportedAuthzServices) { if (supportedAuthzServices) { testDecisionStrategySetOnResourceServer(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java index 30056c22c7..1f2c043aa1 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java @@ -77,6 +77,7 @@ public class MigrationTest extends AbstractMigrationTest { testMigratedData(false); testMigrationTo12_x(true); testMigrationTo18_x(); + testMigrationTo19_x(); // Always test offline-token login during migration test testOfflineTokenLogin(); @@ -95,6 +96,7 @@ public class MigrationTest extends AbstractMigrationTest { testMigrationTo9_x(); testMigrationTo12_x(true); testMigrationTo18_x(); + testMigrationTo19_x(); // Always test offline-token login during migration test testOfflineTokenLogin(); @@ -114,6 +116,7 @@ public class MigrationTest extends AbstractMigrationTest { testMigrationTo9_x(); testMigrationTo12_x(true); testMigrationTo18_x(); + testMigrationTo19_x(); // Always test offline-token login during migration test testOfflineTokenLogin(); @@ -141,6 +144,7 @@ public class MigrationTest extends AbstractMigrationTest { testMigrationTo9_x(); testMigrationTo12_x(false); testMigrationTo18_x(); + testMigrationTo19_x(); // Always test offline-token login during migration test testOfflineTokenLogin(); @@ -161,6 +165,7 @@ public class MigrationTest extends AbstractMigrationTest { testMigrationTo9_x(); testMigrationTo12_x(false); testMigrationTo18_x(); + testMigrationTo19_x(); // Always test offline-token login during migration test testOfflineTokenLogin(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientTokenExchangeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientTokenExchangeTest.java index 25d343fd95..37a4504f88 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientTokenExchangeTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientTokenExchangeTest.java @@ -189,6 +189,7 @@ public class ClientTokenExchangeTest extends AbstractKeycloakTest { directUntrustedPublic.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); directUntrustedPublic.setFullScopeAllowed(false); directUntrustedPublic.addRedirectUri("*"); + directUntrustedPublic.setAttribute(OIDCConfigAttributes.POST_LOGOUT_REDIRECT_URIS, "+"); directUntrustedPublic.addProtocolMapper(AudienceProtocolMapper.createClaimMapper("client-exchanger-audience", clientExchanger.getClientId(), null, true, false)); ClientModel directNoSecret = realm.addClient("direct-no-secret"); 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 4737425a9e..6f2dc25b0a 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 @@ -55,7 +55,10 @@ import org.keycloak.testsuite.pages.LoginPage; import java.io.Closeable; import java.io.IOException; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; +import java.util.List; import java.util.Map; import javax.ws.rs.NotFoundException; @@ -80,6 +83,7 @@ import org.keycloak.testsuite.pages.PageUtils; import org.keycloak.testsuite.updaters.ClientAttributeUpdater; import org.keycloak.testsuite.updaters.RealmAttributeUpdater; import org.keycloak.testsuite.updaters.UserAttributeUpdater; +import org.keycloak.testsuite.util.ClientManager; import org.keycloak.testsuite.util.InfinispanTestTimeServiceRule; import org.keycloak.testsuite.util.Matchers; import org.keycloak.testsuite.util.OAuthClient; @@ -165,6 +169,43 @@ public class RPInitiatedLogoutTest extends AbstractTestRealmKeycloakTest { assertCurrentUrlEquals(redirectUri + "&state=something"); } + @Test + public void postLogoutRedirect() { + OAuthClient.AccessTokenResponse tokenResponse = loginUser(); + String sessionId = tokenResponse.getSessionState(); + + String redirectUri = APP_REDIRECT_URI + "?post_logout"; + + List postLogoutRedirectUris = Collections.singletonList(redirectUri); + ClientManager.realm(adminClient.realm("test")).clientId("test-app").setPostLogoutRedirectUri(postLogoutRedirectUris); + + String idTokenString = tokenResponse.getIdToken(); + + try { + String logoutUrl = oauth.getLogoutUrl().postLogoutRedirectUri(redirectUri).idTokenHint(idTokenString).build(); + driver.navigate().to(logoutUrl); + + events.expectLogout(sessionId).detail(Details.REDIRECT_URI, redirectUri).assertEvent(); + MatcherAssert.assertThat(false, is(isSessionActive(sessionId))); + + assertCurrentUrlEquals(redirectUri); + + tokenResponse = loginUser(); + String sessionId2 = tokenResponse.getSessionState(); + idTokenString = tokenResponse.getIdToken(); + assertNotEquals(sessionId, sessionId2); + + // Test also "state" parameter is included in the URL after logout. Make sure to use idTokenHint from the last login to match with current browser session + logoutUrl = oauth.getLogoutUrl().postLogoutRedirectUri(redirectUri).idTokenHint(idTokenString).state("something").build(); + driver.navigate().to(logoutUrl); + events.expectLogout(sessionId2).detail(Details.REDIRECT_URI, redirectUri).assertEvent(); + MatcherAssert.assertThat(false, is(isSessionActive(sessionId2))); + assertCurrentUrlEquals(redirectUri + "&state=something"); + } finally { + postLogoutRedirectUris = Collections.singletonList("+"); + ClientManager.realm(adminClient.realm("test")).clientId("test-app").setPostLogoutRedirectUri(postLogoutRedirectUris); + } + } @Test public void logoutRedirectWithIdTokenHintPointToDifferentSession() { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientManager.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientManager.java index 79cf4a5a80..e4ccd87bb4 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientManager.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientManager.java @@ -13,6 +13,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedList; +import java.util.List; import static org.keycloak.testsuite.admin.ApiUtil.findClientByClientId; import static org.keycloak.testsuite.admin.ApiUtil.findProtocolMapperByName; @@ -162,6 +163,12 @@ public class ClientManager { clientResource.update(app); } + public void setPostLogoutRedirectUri(List postLogoutRedirectUris) { + ClientRepresentation app = clientResource.toRepresentation(); + OIDCAdvancedConfigWrapper.fromClientRepresentation(app).setPostLogoutRedirectUris(postLogoutRedirectUris); + clientResource.update(app); + } + public ClientManagerBuilder addWebOrigins(String... webOrigins) { ClientRepresentation app = clientResource.toRepresentation(); if (app.getWebOrigins() == null) {