Support for post_logout_redirect_uris in OIDC client registration (#12282)

Closes #10135
This commit is contained in:
Douglas Palmer 2022-07-25 01:57:52 -07:00 committed by GitHub
parent ee0c67c0c8
commit c00514d659
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 263 additions and 2 deletions

View file

@ -37,5 +37,9 @@
<artifactId>selenium-java</artifactId> <artifactId>selenium-java</artifactId>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-services</artifactId>
</dependency>
</dependencies> </dependencies>
</project> </project>

View file

@ -17,6 +17,7 @@
package org.keycloak.test.builders; package org.keycloak.test.builders;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import java.util.Collections; import java.util.Collections;
@ -88,6 +89,9 @@ public class ClientBuilder {
if (rep.getRedirectUris() == null && rep.getRootUrl() != null) if (rep.getRedirectUris() == null && rep.getRootUrl() != null)
rep.setRedirectUris(Collections.singletonList(rep.getRootUrl().concat("/*"))); rep.setRedirectUris(Collections.singletonList(rep.getRootUrl().concat("/*")));
if (OIDCAdvancedConfigWrapper.fromClientRepresentation(rep).getPostLogoutRedirectUris() == null) {
OIDCAdvancedConfigWrapper.fromClientRepresentation(rep).setPostLogoutRedirectUris(Collections.singletonList("+"));
}
return rep; return rep;
} }

View file

@ -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)";
}
}

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!--
~ * 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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<changeSet author="keycloak" id="19.0.0-10135">
<customChange class="org.keycloak.connections.jpa.updater.liquibase.custom.JpaUpdate19_0_0_DefaultPostLogoutRedirectUri"/>
</changeSet>
</databaseChangeLog>

View file

@ -73,5 +73,6 @@
<include file="META-INF/jpa-changelog-15.0.0.xml"/> <include file="META-INF/jpa-changelog-15.0.0.xml"/>
<include file="META-INF/jpa-changelog-17.0.0.xml"/> <include file="META-INF/jpa-changelog-17.0.0.xml"/>
<include file="META-INF/jpa-changelog-18.0.0.xml"/> <include file="META-INF/jpa-changelog-18.0.0.xml"/>
<include file="META-INF/jpa-changelog-19.0.0.xml"/>
</databaseChangeLog> </databaseChangeLog>

View file

@ -55,6 +55,7 @@ import org.keycloak.models.utils.DefaultKeyProviders;
import org.keycloak.models.utils.DefaultRequiredActions; import org.keycloak.models.utils.DefaultRequiredActions;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.RepresentationToModel; import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.representations.idm.ApplicationRepresentation; import org.keycloak.representations.idm.ApplicationRepresentation;
import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation; import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation;
import org.keycloak.representations.idm.AuthenticationFlowRepresentation; import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
@ -464,6 +465,10 @@ public class LegacyExportImportManager implements ExportImportManager {
Map<String, ClientModel> appMap = new HashMap<String, ClientModel>(); Map<String, ClientModel> appMap = new HashMap<String, ClientModel>();
for (ClientRepresentation resourceRep : rep.getClients()) { for (ClientRepresentation resourceRep : rep.getClients()) {
ClientModel app = RepresentationToModel.createClient(session, realm, resourceRep, mappedFlows); 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); appMap.put(app.getClientId(), app);
ValidationUtil.validateClient(session, app, false, r -> { ValidationUtil.validateClient(session, app, false, r -> {

View file

@ -51,6 +51,7 @@ import org.keycloak.models.utils.DefaultKeyProviders;
import org.keycloak.models.utils.DefaultRequiredActions; import org.keycloak.models.utils.DefaultRequiredActions;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.RepresentationToModel; import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.representations.idm.ApplicationRepresentation; import org.keycloak.representations.idm.ApplicationRepresentation;
import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation; import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation;
import org.keycloak.representations.idm.AuthenticationFlowRepresentation; import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
@ -449,6 +450,10 @@ public class MapExportImportManager implements ExportImportManager {
Map<String, ClientModel> appMap = new HashMap<>(); Map<String, ClientModel> appMap = new HashMap<>();
for (ClientRepresentation resourceRep : rep.getClients()) { for (ClientRepresentation resourceRep : rep.getClients()) {
ClientModel app = RepresentationToModel.createClient(session, realm, resourceRep, mappedFlows); 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); appMap.put(app.getClientId(), app);
ValidationUtil.validateClient(session, app, false, r -> { ValidationUtil.validateClient(session, app, false, r -> {

View file

@ -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_URI = "frontchannel.logout.url";
public static final String FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED = "frontchannel.logout.session.required"; 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() { private OIDCConfigAttributes() {
} }

View file

@ -24,6 +24,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.RepresentationToModel; import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.PartialImportRepresentation; import org.keycloak.representations.idm.PartialImportRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.representations.idm.ProtocolMapperRepresentation;
@ -118,6 +119,9 @@ public class ClientsPartialImport extends AbstractPartialImport<ClientRepresenta
} }
ClientModel client = RepresentationToModel.createClient(session, realm, clientRep); ClientModel client = RepresentationToModel.createClient(session, realm, clientRep);
if(OIDCAdvancedConfigWrapper.fromClientModel(client).getPostLogoutRedirectUris() == null) {
OIDCAdvancedConfigWrapper.fromClientModel(client).setPostLogoutRedirectUris(Collections.singletonList("+"));
}
RepresentationToModel.importAuthorizationSettings(clientRep, client, session); RepresentationToModel.importAuthorizationSettings(clientRep, client, session);
} }

View file

@ -24,6 +24,7 @@ import org.keycloak.models.ClientModel;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.utils.StringUtil; import org.keycloak.utils.StringUtil;
import java.util.ArrayList;
import java.util.List; import java.util.List;
/** /**
@ -355,4 +356,27 @@ public class OIDCAdvancedConfigWrapper extends AbstractClientConfigWrapper {
setAttribute(ClientModel.TOS_URI, tosUri); setAttribute(ClientModel.TOS_URI, tosUri);
} }
public List<String> getPostLogoutRedirectUris() {
List<String> 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<String> postLogoutRedirectUris) {
setAttributeMultivalued(OIDCConfigAttributes.POST_LOGOUT_REDIRECT_URIS, postLogoutRedirectUris);
}
} }

View file

@ -48,6 +48,7 @@ import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.SystemClientUtil; import org.keycloak.models.utils.SystemClientUtil;
import org.keycloak.protocol.oidc.BackchannelLogoutResponse; import org.keycloak.protocol.oidc.BackchannelLogoutResponse;
import org.keycloak.protocol.oidc.LogoutTokenValidationCode; import org.keycloak.protocol.oidc.LogoutTokenValidationCode;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory; import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
import org.keycloak.protocol.oidc.OIDCProviderConfig; 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.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.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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -233,7 +243,9 @@ public class LogoutEndpoint {
String validatedRedirectUri = null; String validatedRedirectUri = null;
if (redirectUri != null) { if (redirectUri != null) {
if (client != null) { if (client != null) {
validatedRedirectUri = RedirectUtils.verifyRedirectUri(session, redirectUri, client); OIDCAdvancedConfigWrapper wrapper = OIDCAdvancedConfigWrapper.fromClientModel(client);
Set<String> postLogoutRedirectUris = wrapper.getPostLogoutRedirectUris() != null ? new HashSet(wrapper.getPostLogoutRedirectUris()) : new HashSet<>();
validatedRedirectUri = RedirectUtils.verifyRedirectUri(session, client.getRootUrl(), redirectUri, postLogoutRedirectUris, true);
} else if (clientId == null) { } else if (clientId == null) {
/* /*
* Only call verifyRealmRedirectUri, in case both clientId and client are null - otherwise * Only call verifyRealmRedirectUri, in case both clientId and client are null - otherwise

View file

@ -208,6 +208,10 @@ public class DescriptionConverter {
configWrapper.setTosUri(clientOIDC.getTosUri()); configWrapper.setTosUri(clientOIDC.getTosUri());
} }
if (clientOIDC.getPostLogoutRedirectUris() != null) {
configWrapper.setPostLogoutRedirectUris(clientOIDC.getPostLogoutRedirectUris());
}
// CIBA // CIBA
String backchannelTokenDeliveryMode = clientOIDC.getBackchannelTokenDeliveryMode(); String backchannelTokenDeliveryMode = clientOIDC.getBackchannelTokenDeliveryMode();
if (backchannelTokenDeliveryMode != null) { if (backchannelTokenDeliveryMode != null) {
@ -403,6 +407,9 @@ public class DescriptionConverter {
if (config.getTokenEndpointAuthSigningAlg() != null) { if (config.getTokenEndpointAuthSigningAlg() != null) {
response.setTokenEndpointAuthSigningAlg(config.getTokenEndpointAuthSigningAlg()); response.setTokenEndpointAuthSigningAlg(config.getTokenEndpointAuthSigningAlg());
} }
if (config.getPostLogoutRedirectUris() != null) {
response.setPostLogoutRedirectUris(config.getPostLogoutRedirectUris());
}
response.setBackchannelLogoutUri(config.getBackchannelLogoutUrl()); response.setBackchannelLogoutUri(config.getBackchannelLogoutUrl());
response.setBackchannelLogoutSessionRequired(config.isBackchannelLogoutSessionRequired()); response.setBackchannelLogoutSessionRequired(config.isBackchannelLogoutSessionRequired());
response.setBackchannelLogoutSessionRequired(config.getBackchannelLogoutRevokeOfflineTokens()); response.setBackchannelLogoutSessionRequired(config.getBackchannelLogoutRevokeOfflineTokens());

View file

@ -173,6 +173,7 @@ public class RealmManager {
String baseUrl = "/admin/" + realm.getName() + "/console/"; String baseUrl = "/admin/" + realm.getName() + "/console/";
adminConsole.setBaseUrl(baseUrl); adminConsole.setBaseUrl(baseUrl);
adminConsole.addRedirectUri(baseUrl + "*"); adminConsole.addRedirectUri(baseUrl + "*");
adminConsole.setAttribute(OIDCConfigAttributes.POST_LOGOUT_REDIRECT_URIS, "+");
adminConsole.setWebOrigins(Collections.singleton("+")); adminConsole.setWebOrigins(Collections.singleton("+"));
adminConsole.setEnabled(true); adminConsole.setEnabled(true);
@ -417,6 +418,7 @@ public class RealmManager {
String baseUrl = "/realms/" + realm.getName() + "/account/"; String baseUrl = "/realms/" + realm.getName() + "/account/";
accountClient.setBaseUrl(baseUrl); accountClient.setBaseUrl(baseUrl);
accountClient.addRedirectUri(baseUrl + "*"); accountClient.addRedirectUri(baseUrl + "*");
accountClient.setAttribute(OIDCConfigAttributes.POST_LOGOUT_REDIRECT_URIS, "+");
accountClient.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); accountClient.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
@ -451,6 +453,7 @@ public class RealmManager {
accountConsoleClient.setRootUrl(Constants.AUTH_BASE_URL_PROP); accountConsoleClient.setRootUrl(Constants.AUTH_BASE_URL_PROP);
accountConsoleClient.setBaseUrl(baseUrl); accountConsoleClient.setBaseUrl(baseUrl);
accountConsoleClient.addRedirectUri(baseUrl + "*"); accountConsoleClient.addRedirectUri(baseUrl + "*");
accountConsoleClient.setAttribute(OIDCConfigAttributes.POST_LOGOUT_REDIRECT_URIS, "+");
accountConsoleClient.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); accountConsoleClient.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);

View file

@ -54,6 +54,7 @@ import org.keycloak.jose.jws.JWSInput;
import org.keycloak.models.UserSessionSpi; import org.keycloak.models.UserSessionSpi;
import org.keycloak.models.sessions.infinispan.InfinispanUserSessionProviderFactory; import org.keycloak.models.sessions.infinispan.InfinispanUserSessionProviderFactory;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.common.util.Retry; import org.keycloak.common.util.Retry;
@ -98,6 +99,7 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest {
.directAccessGrants() .directAccessGrants()
.redirectUris("*") .redirectUris("*")
.addWebOrigin("*") .addWebOrigin("*")
.attribute(OIDCConfigAttributes.POST_LOGOUT_REDIRECT_URIS, "+")
.secret("password") .secret("password")
.build(); .build();

View file

@ -4,6 +4,7 @@ import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.IdentityProviderSyncMode; import org.keycloak.models.IdentityProviderSyncMode;
import org.keycloak.protocol.ProtocolMapperUtils; import org.keycloak.protocol.ProtocolMapperUtils;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.mappers.HardcodedClaim; import org.keycloak.protocol.oidc.mappers.HardcodedClaim;
import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper; import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
@ -72,6 +73,8 @@ public class KcOidcBrokerConfiguration implements BrokerConfiguration {
client.setAdminUrl(getConsumerRoot() + client.setAdminUrl(getConsumerRoot() +
"/auth/realms/" + REALM_CONS_NAME + "/broker/" + IDP_OIDC_ALIAS + "/endpoint"); "/auth/realms/" + REALM_CONS_NAME + "/broker/" + IDP_OIDC_ALIAS + "/endpoint");
OIDCAdvancedConfigWrapper.fromClientRepresentation(client).setPostLogoutRedirectUris(Collections.singletonList("+"));
ProtocolMapperRepresentation emailMapper = new ProtocolMapperRepresentation(); ProtocolMapperRepresentation emailMapper = new ProtocolMapperRepresentation();
emailMapper.setName("email"); emailMapper.setName("email");
emailMapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); emailMapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
@ -170,6 +173,8 @@ public class KcOidcBrokerConfiguration implements BrokerConfiguration {
client.setBaseUrl(getConsumerRoot() + client.setBaseUrl(getConsumerRoot() +
"/auth/realms/" + REALM_CONS_NAME + "/app"); "/auth/realms/" + REALM_CONS_NAME + "/app");
OIDCAdvancedConfigWrapper.fromClientRepresentation(client).setPostLogoutRedirectUris(Collections.singletonList("+"));
return Collections.singletonList(client); return Collections.singletonList(client);
} }

View file

@ -633,6 +633,7 @@ public abstract class AbstractClientPoliciesTest extends AbstractKeycloakTest {
clientRep.setPublicClient(Boolean.FALSE); clientRep.setPublicClient(Boolean.FALSE);
clientRep.setServiceAccountsEnabled(Boolean.TRUE); clientRep.setServiceAccountsEnabled(Boolean.TRUE);
clientRep.setRedirectUris(Collections.singletonList(ServerURLs.getAuthServerContextRoot() + "/auth/realms/master/app/auth")); clientRep.setRedirectUris(Collections.singletonList(ServerURLs.getAuthServerContextRoot() + "/auth/realms/master/app/auth"));
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setPostLogoutRedirectUris(Collections.singletonList("+"));
op.accept(clientRep); op.accept(clientRep);
Response resp = adminClient.realm(REALM_NAME).clients().create(clientRep); Response resp = adminClient.realm(REALM_NAME).clients().create(clientRep);
if (resp.getStatus() == Response.Status.BAD_REQUEST.getStatusCode()) { if (resp.getStatus() == Response.Status.BAD_REQUEST.getStatusCode()) {

View file

@ -240,11 +240,14 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest {
.clientId("test-device") .clientId("test-device")
.secret("secret") .secret("secret")
.attribute(OAuth2DeviceConfig.OAUTH2_DEVICE_AUTHORIZATION_GRANT_ENABLED, "true") .attribute(OAuth2DeviceConfig.OAUTH2_DEVICE_AUTHORIZATION_GRANT_ENABLED, "true")
.attribute(OIDCConfigAttributes.POST_LOGOUT_REDIRECT_URIS, "+")
.build(); .build();
clients.add(app); clients.add(app);
ClientRepresentation appPublic = ClientBuilder.create().id(KeycloakModelUtils.generateId()).publicClient() 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(); .build();
clients.add(appPublic); clients.add(appPublic);

View file

@ -52,6 +52,7 @@ import java.util.stream.Collectors;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import static org.keycloak.testsuite.auth.page.AuthRealm.TEST; 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); realmRep.getAttributes().remove(Constants.ACR_LOA_MAP);
adminClient.realm("test").update(realmRep); 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());
}
} }

View file

@ -32,6 +32,7 @@ import org.keycloak.models.LDAPConstants;
import org.keycloak.models.credential.PasswordCredentialModel; import org.keycloak.models.credential.PasswordCredentialModel;
import org.keycloak.models.credential.dto.PasswordCredentialData; import org.keycloak.models.credential.dto.PasswordCredentialData;
import org.keycloak.models.utils.DefaultAuthenticationFlows; import org.keycloak.models.utils.DefaultAuthenticationFlows;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory; import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper; import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
@ -754,4 +755,14 @@ public class ExportImportUtil {
OIDCLoginProtocolFactory.MICROPROFILE_JWT_SCOPE OIDCLoginProtocolFactory.MICROPROFILE_JWT_SCOPE
)); ));
} }
public static void testDefaultPostLogoutRedirectUris(RealmResource realm) {
for (ClientRepresentation client : realm.clients().findAll()) {
List<String> redirectUris = client.getRedirectUris();
if(redirectUris != null && !redirectUris.isEmpty()) {
String postLogoutRedirectUris = client.getAttributes().get(OIDCConfigAttributes.POST_LOGOUT_REDIRECT_URIS);
Assert.assertEquals("+", postLogoutRedirectUris);
}
}
}
} }

View file

@ -315,6 +315,10 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
testRealmDefaultClientScopes(migrationRealm); testRealmDefaultClientScopes(migrationRealm);
} }
protected void testMigrationTo19_0_0() {
testPostLogoutRedirectUrisSet(migrationRealm);
}
protected void testDeleteAccount(RealmResource realm) { protected void testDeleteAccount(RealmResource realm) {
ClientRepresentation accountClient = realm.clients().findByClientId(ACCOUNT_MANAGEMENT_CLIENT_ID).get(0); ClientRepresentation accountClient = realm.clients().findByClientId(ACCOUNT_MANAGEMENT_CLIENT_ID).get(0);
ClientResource accountResource = realm.clients().get(accountClient.getId()); ClientResource accountResource = realm.clients().get(accountClient.getId());
@ -728,6 +732,11 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
ExportImportUtil.testClientDefaultClientScopes(realm); 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() { private void testOfflineScopeAddedToClient() {
log.infof("Testing offline_access optional scope present in realm %s for client migration-test-client", migrationRealm.toRepresentation().getRealm()); 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(); testMigrationTo18_0_0();
} }
protected void testMigrationTo19_x() {
testMigrationTo19_0_0();
}
protected void testMigrationTo7_x(boolean supportedAuthzServices) { protected void testMigrationTo7_x(boolean supportedAuthzServices) {
if (supportedAuthzServices) { if (supportedAuthzServices) {
testDecisionStrategySetOnResourceServer(); testDecisionStrategySetOnResourceServer();

View file

@ -77,6 +77,7 @@ public class MigrationTest extends AbstractMigrationTest {
testMigratedData(false); testMigratedData(false);
testMigrationTo12_x(true); testMigrationTo12_x(true);
testMigrationTo18_x(); testMigrationTo18_x();
testMigrationTo19_x();
// Always test offline-token login during migration test // Always test offline-token login during migration test
testOfflineTokenLogin(); testOfflineTokenLogin();
@ -95,6 +96,7 @@ public class MigrationTest extends AbstractMigrationTest {
testMigrationTo9_x(); testMigrationTo9_x();
testMigrationTo12_x(true); testMigrationTo12_x(true);
testMigrationTo18_x(); testMigrationTo18_x();
testMigrationTo19_x();
// Always test offline-token login during migration test // Always test offline-token login during migration test
testOfflineTokenLogin(); testOfflineTokenLogin();
@ -114,6 +116,7 @@ public class MigrationTest extends AbstractMigrationTest {
testMigrationTo9_x(); testMigrationTo9_x();
testMigrationTo12_x(true); testMigrationTo12_x(true);
testMigrationTo18_x(); testMigrationTo18_x();
testMigrationTo19_x();
// Always test offline-token login during migration test // Always test offline-token login during migration test
testOfflineTokenLogin(); testOfflineTokenLogin();
@ -141,6 +144,7 @@ public class MigrationTest extends AbstractMigrationTest {
testMigrationTo9_x(); testMigrationTo9_x();
testMigrationTo12_x(false); testMigrationTo12_x(false);
testMigrationTo18_x(); testMigrationTo18_x();
testMigrationTo19_x();
// Always test offline-token login during migration test // Always test offline-token login during migration test
testOfflineTokenLogin(); testOfflineTokenLogin();
@ -161,6 +165,7 @@ public class MigrationTest extends AbstractMigrationTest {
testMigrationTo9_x(); testMigrationTo9_x();
testMigrationTo12_x(false); testMigrationTo12_x(false);
testMigrationTo18_x(); testMigrationTo18_x();
testMigrationTo19_x();
// Always test offline-token login during migration test // Always test offline-token login during migration test
testOfflineTokenLogin(); testOfflineTokenLogin();

View file

@ -189,6 +189,7 @@ public class ClientTokenExchangeTest extends AbstractKeycloakTest {
directUntrustedPublic.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); directUntrustedPublic.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
directUntrustedPublic.setFullScopeAllowed(false); directUntrustedPublic.setFullScopeAllowed(false);
directUntrustedPublic.addRedirectUri("*"); directUntrustedPublic.addRedirectUri("*");
directUntrustedPublic.setAttribute(OIDCConfigAttributes.POST_LOGOUT_REDIRECT_URIS, "+");
directUntrustedPublic.addProtocolMapper(AudienceProtocolMapper.createClaimMapper("client-exchanger-audience", clientExchanger.getClientId(), null, true, false)); directUntrustedPublic.addProtocolMapper(AudienceProtocolMapper.createClaimMapper("client-exchanger-audience", clientExchanger.getClientId(), null, true, false));
ClientModel directNoSecret = realm.addClient("direct-no-secret"); ClientModel directNoSecret = realm.addClient("direct-no-secret");

View file

@ -55,7 +55,10 @@ import org.keycloak.testsuite.pages.LoginPage;
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map; import java.util.Map;
import javax.ws.rs.NotFoundException; 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.ClientAttributeUpdater;
import org.keycloak.testsuite.updaters.RealmAttributeUpdater; import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
import org.keycloak.testsuite.updaters.UserAttributeUpdater; import org.keycloak.testsuite.updaters.UserAttributeUpdater;
import org.keycloak.testsuite.util.ClientManager;
import org.keycloak.testsuite.util.InfinispanTestTimeServiceRule; import org.keycloak.testsuite.util.InfinispanTestTimeServiceRule;
import org.keycloak.testsuite.util.Matchers; import org.keycloak.testsuite.util.Matchers;
import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.OAuthClient;
@ -165,6 +169,43 @@ public class RPInitiatedLogoutTest extends AbstractTestRealmKeycloakTest {
assertCurrentUrlEquals(redirectUri + "&state=something"); assertCurrentUrlEquals(redirectUri + "&state=something");
} }
@Test
public void postLogoutRedirect() {
OAuthClient.AccessTokenResponse tokenResponse = loginUser();
String sessionId = tokenResponse.getSessionState();
String redirectUri = APP_REDIRECT_URI + "?post_logout";
List<String> 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 @Test
public void logoutRedirectWithIdTokenHintPointToDifferentSession() { public void logoutRedirectWithIdTokenHintPointToDifferentSession() {

View file

@ -13,6 +13,7 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List;
import static org.keycloak.testsuite.admin.ApiUtil.findClientByClientId; import static org.keycloak.testsuite.admin.ApiUtil.findClientByClientId;
import static org.keycloak.testsuite.admin.ApiUtil.findProtocolMapperByName; import static org.keycloak.testsuite.admin.ApiUtil.findProtocolMapperByName;
@ -162,6 +163,12 @@ public class ClientManager {
clientResource.update(app); clientResource.update(app);
} }
public void setPostLogoutRedirectUri(List<String> postLogoutRedirectUris) {
ClientRepresentation app = clientResource.toRepresentation();
OIDCAdvancedConfigWrapper.fromClientRepresentation(app).setPostLogoutRedirectUris(postLogoutRedirectUris);
clientResource.update(app);
}
public ClientManagerBuilder addWebOrigins(String... webOrigins) { public ClientManagerBuilder addWebOrigins(String... webOrigins) {
ClientRepresentation app = clientResource.toRepresentation(); ClientRepresentation app = clientResource.toRepresentation();
if (app.getWebOrigins() == null) { if (app.getWebOrigins() == null) {