diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/idp/InfinispanIdentityProviderStorageProvider.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/idp/InfinispanIdentityProviderStorageProvider.java index e5f16134a86..6c32bf70e9a 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/idp/InfinispanIdentityProviderStorageProvider.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/idp/InfinispanIdentityProviderStorageProvider.java @@ -412,7 +412,7 @@ public class InfinispanIdentityProviderStorageProvider implements IdentityProvid private void registerIDPLoginInvalidation(IdentityProviderModel idp) { // only invalidate login caches if the IDP qualifies as a login IDP. - if (getLoginPredicate().test(idp, null)) { + if (getLoginPredicate().test(idp)) { for (FetchMode mode : FetchMode.values()) { realmCache.registerInvalidation(cacheKeyForLogin(getRealm(), mode)); } @@ -433,11 +433,11 @@ public class InfinispanIdentityProviderStorageProvider implements IdentityProvid */ private void registerIDPLoginInvalidationOnUpdate(IdentityProviderModel original, IdentityProviderModel updated) { // IDP isn't currently available for login and update preserves that - no need to invalidate. - if (!getLoginPredicate().test(original, null) && !getLoginPredicate().test(updated, null)) { + if (!getLoginPredicate().test(original) && !getLoginPredicate().test(updated)) { return; } // IDP is currently available for login and update preserves that, including organization link - no need to invalidate. - if (getLoginPredicate().test(original, null) && getLoginPredicate().test(updated, null) + if (getLoginPredicate().test(original) && getLoginPredicate().test(updated) && Objects.equals(original.getOrganizationId(), updated.getOrganizationId())) { return; } diff --git a/server-spi/src/main/java/org/keycloak/models/IdentityProviderStorageProvider.java b/server-spi/src/main/java/org/keycloak/models/IdentityProviderStorageProvider.java index 175aff62c8f..ded58c4eb80 100644 --- a/server-spi/src/main/java/org/keycloak/models/IdentityProviderStorageProvider.java +++ b/server-spi/src/main/java/org/keycloak/models/IdentityProviderStorageProvider.java @@ -20,11 +20,11 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; import java.util.function.BiPredicate; +import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; import org.keycloak.provider.Provider; -import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.util.Booleans; /** @@ -236,22 +236,18 @@ public interface IdentityProviderStorageProvider extends Provider { */ enum LoginFilter { - ENABLED(IdentityProviderModel.ENABLED, Boolean.TRUE.toString(), (m, as) -> m.isEnabled()), + ENABLED(IdentityProviderModel.ENABLED, Boolean.TRUE.toString(), (m, ctx) -> m.isEnabled()), - LINK_ONLY(IdentityProviderModel.LINK_ONLY, Boolean.FALSE.toString(), (m, as) -> Booleans.isFalse(m.isLinkOnly())), + LINK_ONLY(IdentityProviderModel.LINK_ONLY, Boolean.FALSE.toString(), (m, ctx) -> Booleans.isFalse(m.isLinkOnly())), - HIDE_ON_LOGIN(IdentityProviderModel.HIDE_ON_LOGIN, Boolean.FALSE.toString(), (m, as) -> { - if (as != null && Objects.equals(as.getAuthNote("FORCED_REAUTHENTICATION"), "true")) { - return true; - } - return Booleans.isFalse(m.isHideOnLogin()); - }); + HIDE_ON_LOGIN(IdentityProviderModel.HIDE_ON_LOGIN, Boolean.FALSE.toString(), (m, ctx) -> + ctx.forcedReauth() || Booleans.isFalse(m.isHideOnLogin())); private final String key; private final String value; - private final BiPredicate filter; + private final BiPredicate filter; - LoginFilter(String key, String value, BiPredicate filter) { + LoginFilter(String key, String value, BiPredicate filter) { this.key = key; this.value = value; this.filter = filter; @@ -265,7 +261,7 @@ public interface IdentityProviderStorageProvider extends Provider { return value; } - public BiPredicate getFilter() { + public BiPredicate getFilter() { return filter; } @@ -273,9 +269,40 @@ public interface IdentityProviderStorageProvider extends Provider { return Stream.of(values()).collect(Collectors.toMap(LoginFilter::getKey, LoginFilter::getValue, (v1, v2) -> v1, LinkedHashMap::new)); } - public static BiPredicate getLoginPredicate() { - return ((BiPredicate) (m, as) -> Objects.nonNull(m)) - .and(Stream.of(values()).map(LoginFilter::getFilter).reduce(BiPredicate::and).get()); + /** + * Returns a {@link Predicate} that accepts an {@link IdentityProviderModel} only when it passes all login + * filters using the standard (non-re-auth) context. Equivalent to calling + * {@link #getLoginPredicate(Context) getLoginPredicate(Context.standard())}. + * + * @return a {@link Predicate} applying all login filters with the standard context. + */ + public static Predicate getLoginPredicate() { + return getLoginPredicate(Context.standard()); + } + + /** + * Returns a {@link Predicate} that accepts an {@link IdentityProviderModel} only when it passes all login + * filters evaluated against the provided {@link Context}. The context controls filter behaviour that depends + * on the current authentication state — for example, the {@link #HIDE_ON_LOGIN} filter is bypassed when + * {@link Context#forcedReauth()} is {@code true}. + * + * @param ctx the {@link Context} describing the current authentication state; must not be {@code null}. + * @return a {@link Predicate} applying all login filters with the given context. + */ + public static Predicate getLoginPredicate(Context ctx) { + return m -> Objects.nonNull(m) && Stream.of(values()).allMatch(f -> f.getFilter().test(m, ctx)); + } + + /** + * Captures the authentication-state information that login filters may need to evaluate an + * {@link IdentityProviderModel}. Use {@link #standard()} for ordinary login flows and {@link #reauth()} when + * a forced re-authentication is in progress (e.g. an App-Initiated Action). + * + * @param forcedReauth {@code true} when the current flow is a forced re-authentication; {@code false} otherwise. + */ + public record Context(boolean forcedReauth) { + public static Context standard() { return new Context(false); } + public static Context reauth() { return new Context(true); } } } diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/model/IdentityProviderBean.java b/services/src/main/java/org/keycloak/forms/login/freemarker/model/IdentityProviderBean.java index 852620a89bb..191e3481c1c 100755 --- a/services/src/main/java/org/keycloak/forms/login/freemarker/model/IdentityProviderBean.java +++ b/services/src/main/java/org/keycloak/forms/login/freemarker/model/IdentityProviderBean.java @@ -36,6 +36,7 @@ import org.keycloak.common.Profile; import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.IdentityProviderStorageProvider; +import org.keycloak.models.IdentityProviderStorageProvider.LoginFilter; import org.keycloak.models.KeycloakSession; import org.keycloak.models.OrderedModel; import org.keycloak.models.RealmModel; @@ -235,14 +236,18 @@ public class IdentityProviderBean { } /** - * Returns a predicate that can filter out IDPs associated with the current user's federated identities before those - * are converted into {@link IdentityProvider}s. Subclasses may use this as a way to further refine the IDPs that are - * to be returned. + * Returns a predicate that applies the standard login filters (enabled, not link-only, not hidden on login page) + * to IDPs associated with the current user's federated identities before those are converted into + * {@link IdentityProvider}s. The {@code HIDE_ON_LOGIN} check is bypassed when a forced re-authentication is in + * progress, so that a user whose only IdP is hidden can still be redirected to it for re-auth. + * Subclasses may use this as a way to further refine the IDPs that are to be returned. * - * @return the custom {@link Predicate} used as a last filter before conversion into {@link IdentityProvider} + * @return the {@link Predicate} used as a last filter before conversion into {@link IdentityProvider} */ protected Predicate federatedProviderPredicate() { - return m -> IdentityProviderStorageProvider.LoginFilter.getLoginPredicate().test(m, context.getAuthenticationSession()); + final var isReAuth = isForcedReauthentication(); + final var ctx = isReAuth ? LoginFilter.Context.reauth() : LoginFilter.Context.standard(); + return LoginFilter.getLoginPredicate(ctx); } /**