mirror of
https://github.com/keycloak/keycloak.git
synced 2026-05-28 04:13:22 -04:00
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:
parent
3c4e67cb45
commit
0c6f898e11
3 changed files with 55 additions and 23 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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); }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in a new issue