fix: replace nullable BiPredicate with LoginFilter.Context in LoginFilter (#42410)

Replaces the BiPredicate<IdentityProviderModel, AuthenticationSessionModel>
(with its nullable second argument) with a proper LoginFilter.Context record
that carries the forced-reauth flag. Callers without an auth-session context
(Infinispan cache invalidation) pass Context.standard() via the zero-arg
getLoginPredicate() overload; IdentityProviderBean maps the auth-session note
to Context.reauth() using the proper AuthenticationManager constant, keeping
session-layer knowledge out of server-spi.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Vilmos Nagy <me@vnagy.eu>
This commit is contained in:
Vilmos Nagy 2026-05-10 23:22:35 +02:00 committed by Vilmos Nagy
parent 3c4e67cb45
commit 0c6f898e11
3 changed files with 55 additions and 23 deletions

View file

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

View file

@ -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<IdentityProviderModel, AuthenticationSessionModel> filter;
private final BiPredicate<IdentityProviderModel, Context> filter;
LoginFilter(String key, String value, BiPredicate<IdentityProviderModel, AuthenticationSessionModel> filter) {
LoginFilter(String key, String value, BiPredicate<IdentityProviderModel, Context> filter) {
this.key = key;
this.value = value;
this.filter = filter;
@ -265,7 +261,7 @@ public interface IdentityProviderStorageProvider extends Provider {
return value;
}
public BiPredicate<IdentityProviderModel, AuthenticationSessionModel> getFilter() {
public BiPredicate<IdentityProviderModel, Context> 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<IdentityProviderModel, AuthenticationSessionModel> getLoginPredicate() {
return ((BiPredicate<IdentityProviderModel, AuthenticationSessionModel>) (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<IdentityProviderModel> 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<IdentityProviderModel> 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); }
}
}

View file

@ -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<IdentityProviderModel> 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);
}
/**