diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java index 48a3cd5aaf..4fb5157618 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java @@ -568,6 +568,11 @@ public class RealmCacheSession implements CacheRealmProvider { return getClientDelegate().getAlwaysDisplayInConsoleClientsStream(realm); } + @Override + public Map> getAllRedirectUrisOfEnabledClients(RealmModel realm) { + return getClientDelegate().getAllRedirectUrisOfEnabledClients(realm); + } + @Override public void removeClients(RealmModel realm) { getClientDelegate().removeClients(realm); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java index 73e6e03d44..1c5b39afbd 100644 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java @@ -279,6 +279,20 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, ClientSc return session.roles().getRoleById(client.getRealm(), roles.get(0)); } + @Override + public Map> getAllRedirectUrisOfEnabledClients(RealmModel realm) { + TypedQuery query = em.createNamedQuery("getAllRedirectUrisOfEnabledClients", Map.class); + query.setParameter("realm", realm.getId()); + return query.getResultStream() + .filter(s -> s.get("client") != null) + .collect( + Collectors.groupingBy( + s -> new ClientAdapter(realm, em, session, (ClientEntity) s.get("client")), + Collectors.mapping(s -> (String) s.get("redirectUri"), Collectors.toSet()) + ) + ); + } + @Override public Stream getRealmRolesStream(RealmModel realm, Integer first, Integer max) { TypedQuery query = em.createNamedQuery("getRealmRoles", RoleEntity.class); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java index 91e9432d94..16d78da429 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java @@ -56,6 +56,7 @@ import java.util.Set; @NamedQuery(name="searchClientsByClientId", query="select client.id from ClientEntity client where lower(client.clientId) like lower(concat('%',:clientId,'%')) and client.realmId = :realm order by client.clientId"), @NamedQuery(name="getRealmClientsCount", query="select count(client) from ClientEntity client where client.realmId = :realm"), @NamedQuery(name="findClientByClientId", query="select client from ClientEntity client where client.clientId = :clientId and client.realmId = :realm"), + @NamedQuery(name="getAllRedirectUrisOfEnabledClients", query="select new map(client as client, r as redirectUri) from ClientEntity client join client.redirectUris r where client.realmId = :realm and client.enabled = true"), }) public class ClientEntity { diff --git a/model/map/src/main/java/org/keycloak/models/map/client/MapClientProvider.java b/model/map/src/main/java/org/keycloak/models/map/client/MapClientProvider.java index f74f261691..6f609a7f2d 100644 --- a/model/map/src/main/java/org/keycloak/models/map/client/MapClientProvider.java +++ b/model/map/src/main/java/org/keycloak/models/map/client/MapClientProvider.java @@ -46,6 +46,7 @@ import static org.keycloak.common.util.StackUtil.getShortStackTrace; import org.keycloak.models.ClientScopeModel; import static org.keycloak.models.map.common.MapStorageUtils.registerEntityForChanges; import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import java.util.HashSet; import static org.keycloak.utils.StreamsUtil.paginatedStream; public class MapClientProvider implements ClientProvider { @@ -336,6 +337,22 @@ public class MapClientProvider implements ClientProvider { .collect(Collectors.toMap(ClientScopeModel::getName, Function.identity())); } + @Override + public Map> getAllRedirectUrisOfEnabledClients(RealmModel realm) { + ModelCriteriaBuilder mcb = clientStore.createCriteriaBuilder() + .compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()) + .compare(SearchableFields.ENABLED, Operator.EQ, Boolean.TRUE); + + try (Stream> st = tx.getUpdatedNotRemoved(mcb)) { + return st + .filter(mce -> mce.getRedirectUris() != null && ! mce.getRedirectUris().isEmpty()) + .collect(Collectors.toMap( + mce -> entityToAdapterFunc(realm).apply(mce), + mce -> new HashSet<>(mce.getRedirectUris())) + ); + } + } + public void preRemove(RealmModel realm, RoleModel role) { ModelCriteriaBuilder mcb = clientStore.createCriteriaBuilder() .compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()) diff --git a/model/map/src/main/java/org/keycloak/models/map/realm/MapRealmProvider.java b/model/map/src/main/java/org/keycloak/models/map/realm/MapRealmProvider.java index dadf23bb3e..d416bdd22f 100644 --- a/model/map/src/main/java/org/keycloak/models/map/realm/MapRealmProvider.java +++ b/model/map/src/main/java/org/keycloak/models/map/realm/MapRealmProvider.java @@ -340,6 +340,12 @@ public class MapRealmProvider implements RealmProvider { session.clientScopes().removeClientScopes(realm); } + @Override + @Deprecated + public Map> getAllRedirectUrisOfEnabledClients(RealmModel realm) { + return session.clients().getAllRedirectUrisOfEnabledClients(realm); + } + @Override @Deprecated public void moveGroup(RealmModel realm, GroupModel group, GroupModel toParent) { diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/MapFieldPredicates.java b/model/map/src/main/java/org/keycloak/models/map/storage/MapFieldPredicates.java index e9d6b26884..b3d123dc89 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/MapFieldPredicates.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/MapFieldPredicates.java @@ -96,6 +96,7 @@ public class MapFieldPredicates { put(CLIENT_PREDICATES, ClientModel.SearchableFields.REALM_ID, MapClientEntity::getRealmId); put(CLIENT_PREDICATES, ClientModel.SearchableFields.CLIENT_ID, MapClientEntity::getClientId); put(CLIENT_PREDICATES, ClientModel.SearchableFields.SCOPE_MAPPING_ROLE, MapFieldPredicates::checkScopeMappingRole); + put(CLIENT_PREDICATES, ClientModel.SearchableFields.ENABLED, MapClientEntity::isEnabled); put(CLIENT_PREDICATES, ClientModel.SearchableFields.ATTRIBUTE, MapFieldPredicates::checkClientAttributes); put(CLIENT_SCOPE_PREDICATES, ClientScopeModel.SearchableFields.REALM_ID, MapClientScopeEntity::getRealmId); diff --git a/server-spi/src/main/java/org/keycloak/models/ClientModel.java b/server-spi/src/main/java/org/keycloak/models/ClientModel.java index 0c39b8ba99..97abce87e7 100755 --- a/server-spi/src/main/java/org/keycloak/models/ClientModel.java +++ b/server-spi/src/main/java/org/keycloak/models/ClientModel.java @@ -41,6 +41,7 @@ public interface ClientModel extends ClientScopeModel, RoleContainerModel, Prot public static final SearchableModelField ID = new SearchableModelField<>("id", String.class); public static final SearchableModelField REALM_ID = new SearchableModelField<>("realmId", String.class); public static final SearchableModelField CLIENT_ID = new SearchableModelField<>("clientId", String.class); + public static final SearchableModelField ENABLED = new SearchableModelField<>("enabled", Boolean.class); public static final SearchableModelField SCOPE_MAPPING_ROLE = new SearchableModelField<>("scopeMappingRole", String.class); /** diff --git a/server-spi/src/main/java/org/keycloak/models/ClientProvider.java b/server-spi/src/main/java/org/keycloak/models/ClientProvider.java index ee72cfdafe..440bf2dd07 100644 --- a/server-spi/src/main/java/org/keycloak/models/ClientProvider.java +++ b/server-spi/src/main/java/org/keycloak/models/ClientProvider.java @@ -20,6 +20,7 @@ import org.keycloak.provider.Provider; import org.keycloak.storage.client.ClientLookupProvider; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -167,4 +168,13 @@ public interface ClientProvider extends ClientLookupProvider, Provider { * @param clientScope to be unassigned */ void removeClientScope(RealmModel realm, ClientModel client, ClientScopeModel clientScope); + + /** + * Returns a map of (rootUrl, {validRedirectUris}) for all enabled clients. + * @param realm + * @return + * @deprecated Do not use, this is only to support a deprecated logout endpoint and will vanish with it's removal + */ + @Deprecated + Map> getAllRedirectUrisOfEnabledClients(RealmModel realm); } 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 c81bdd5fca..feaec4e645 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 @@ -128,9 +128,26 @@ public class LogoutEndpoint { @QueryParam("state") String state, @QueryParam("initiating_idp") String initiatingIdp) { String redirect = postLogoutRedirectUri != null ? postLogoutRedirectUri : redirectUri; + IDToken idToken = null; + if (encodedIdToken != null) { + try { + idToken = tokenManager.verifyIDTokenSignature(session, encodedIdToken); + TokenVerifier.createWithoutSignature(idToken).tokenType(TokenUtil.TOKEN_TYPE_ID).verify(); + } catch (OAuthErrorException | VerificationException e) { + event.event(EventType.LOGOUT); + event.error(Errors.INVALID_TOKEN); + return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.SESSION_NOT_ACTIVE); + } + } if (redirect != null) { - String validatedUri = RedirectUtils.verifyRealmRedirectUri(session, redirect); + String validatedUri; + ClientModel client = (idToken == null || idToken.getIssuedFor() == null) ? null : realm.getClientById(idToken.getIssuedFor()); + if (client != null) { + validatedUri = RedirectUtils.verifyRedirectUri(session, redirect, client); + } else { + validatedUri = RedirectUtils.verifyRealmRedirectUri(session, redirect); + } if (validatedUri == null) { event.event(EventType.LOGOUT); event.detail(Details.REDIRECT_URI, redirect); @@ -141,17 +158,14 @@ public class LogoutEndpoint { } UserSessionModel userSession = null; - IDToken idToken = null; - if (encodedIdToken != null) { + if (idToken != null) { try { - idToken = tokenManager.verifyIDTokenSignature(session, encodedIdToken); - TokenVerifier.createWithoutSignature(idToken).tokenType(TokenUtil.TOKEN_TYPE_ID).verify(); userSession = session.sessions().getUserSession(realm, idToken.getSessionState()); if (userSession != null) { checkTokenIssuedAt(idToken, userSession); } - } catch (OAuthErrorException | VerificationException e) { + } catch (OAuthErrorException e) { event.event(EventType.LOGOUT); event.error(Errors.INVALID_TOKEN); return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.SESSION_NOT_ACTIVE); 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 9071a70d2c..da0f11af76 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 @@ -72,11 +72,12 @@ public class RedirectUtils { } private static Set getValidateRedirectUris(KeycloakSession session) { - return session.getContext().getRealm().getClientsStream() - .filter(client -> client.isEnabled() && OIDCLoginProtocol.LOGIN_PROTOCOL.equals(client.getProtocol()) && !client.isBearerOnly() && (client.isStandardFlowEnabled() || client.isImplicitFlowEnabled())) - .map(c -> resolveValidRedirects(session, c.getRootUrl(), c.getRedirectUris())) - .flatMap(Collection::stream) - .collect(Collectors.toSet()); + RealmModel realm = session.getContext().getRealm(); + return session.clientStorageManager().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) { diff --git a/services/src/main/java/org/keycloak/storage/ClientStorageManager.java b/services/src/main/java/org/keycloak/storage/ClientStorageManager.java index 94a4b79d1b..60fbe40525 100644 --- a/services/src/main/java/org/keycloak/storage/ClientStorageManager.java +++ b/services/src/main/java/org/keycloak/storage/ClientStorageManager.java @@ -34,7 +34,6 @@ import org.keycloak.utils.ServicesUtils; import java.util.Objects; import java.util.function.Function; import java.util.Set; -import java.util.stream.Collectors; import java.util.stream.Stream; import org.keycloak.models.ClientScopeModel; @@ -264,6 +263,11 @@ public class ClientStorageManager implements ClientProvider { session.clientLocalStorage().removeClientScope(realm, client, clientScope); } + @Override + public Map> getAllRedirectUrisOfEnabledClients(RealmModel realm) { + return session.clientLocalStorage().getAllRedirectUrisOfEnabledClients(realm); + } + @Override public void close() {