Skip to content

Decouple discovery process from AuthenticationFlowContext #525

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package de.sventorben.keycloak.authentication.hidpd;

import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.UserModel;
import org.keycloak.models.*;
import org.keycloak.models.utils.KeycloakModelUtils;

import java.util.stream.Stream;

public final class Users {

private static final Logger LOG = Logger.getLogger(Users.class);
Expand All @@ -26,4 +26,7 @@ public UserModel lookupBy(String username) {
return user;
}

public Stream<FederatedIdentityModel> getFederatedIdentitiesStream(RealmModel realm, UserModel user) {
return keycloakSession.users().getFederatedIdentitiesStream(realm, user);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package de.sventorben.keycloak.authentication.hidpd.discovery;

public interface HomeIdpDiscovererConfig {
String userAttribute();
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package de.sventorben.keycloak.authentication.hidpd.discovery.email;

import de.sventorben.keycloak.authentication.hidpd.discovery.HomeIdpDiscovererConfig;
import org.jboss.logging.Logger;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.models.IdentityProviderModel;

import java.util.List;
Expand All @@ -11,16 +11,15 @@ final class DefaultIdentityProviders implements IdentityProviders {

private static final Logger LOG = Logger.getLogger(DefaultIdentityProviders.class);


@Override
public List<IdentityProviderModel> withMatchingDomain(AuthenticationFlowContext context, List<IdentityProviderModel> candidates, Domain domain) {
EmailHomeIdpDiscovererConfig config = new EmailHomeIdpDiscovererConfig(context.getAuthenticatorConfig());
String userAttributeName = config.userAttribute();
public List<IdentityProviderModel> withMatchingDomain(HomeIdpDiscovererConfig discovererConfig, List<IdentityProviderModel> candidates, Domain domain) {
String userAttributeName = discovererConfig.userAttribute();
List<IdentityProviderModel> idpsWithMatchingDomain = candidates.stream()
.filter(it -> new IdentityProviderModelConfig(it).supportsDomain(userAttributeName, domain))
.collect(Collectors.toList());
LOG.tracef("IdPs with matching domain '%s' for attribute '%s': %s", domain, userAttributeName,
idpsWithMatchingDomain.stream().map(IdentityProviderModel::getAlias).collect(Collectors.joining(",")));
return idpsWithMatchingDomain;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;

import java.util.*;
Expand All @@ -15,7 +16,7 @@
import static java.util.Collections.emptyList;

@PublicAPI(unstable = true)
public final class EmailHomeIdpDiscoverer implements HomeIdpDiscoverer {
public final class EmailHomeIdpDiscoverer implements HomeIdpDiscoverer<EmailHomeIdpDiscovererConfig> {

private static final Logger LOG = Logger.getLogger(EmailHomeIdpDiscoverer.class);
private static final String EMAIL_ATTRIBUTE = "email";
Expand All @@ -29,11 +30,15 @@ public EmailHomeIdpDiscoverer(Users users, IdentityProviders identityProviders)
}

@Override
public List<IdentityProviderModel> discoverForUser(AuthenticationFlowContext context, String username) {
EmailHomeIdpDiscovererConfig config = new EmailHomeIdpDiscovererConfig(context.getAuthenticatorConfig());
public EmailHomeIdpDiscovererConfig getConfigFrom(AuthenticationFlowContext context) {
return new EmailHomeIdpDiscovererConfig(context.getAuthenticatorConfig());
}

@Override
public List<IdentityProviderModel> discoverForUser(EmailHomeIdpDiscovererConfig config, RealmModel realm, String username) {
DomainExtractor domainExtractor = new DomainExtractor(config);

String realmName = context.getRealm().getName();
String realmName = realm.getName();
LOG.tracef("Trying to discover home IdP for username '%s' in realm '%s' with authenticator config '%s'",
username, realmName, config.getAlias());

Expand All @@ -59,7 +64,7 @@ public List<IdentityProviderModel> discoverForUser(AuthenticationFlowContext con

if (emailDomain.isPresent()) {
Domain domain = emailDomain.get();
homeIdps = discoverHomeIdps(context, domain, user, username);
homeIdps = discoverHomeIdps(config, domain, realm, user, username);
if (homeIdps.isEmpty()) {
LOG.infof("Could not find home IdP for domain '%s' and user '%s' in realm '%s'",
domain, username, realmName);
Expand All @@ -71,10 +76,9 @@ public List<IdentityProviderModel> discoverForUser(AuthenticationFlowContext con
return homeIdps;
}

private List<IdentityProviderModel> discoverHomeIdps(AuthenticationFlowContext context, Domain domain, UserModel user, String username) {
private List<IdentityProviderModel> discoverHomeIdps(EmailHomeIdpDiscovererConfig config, Domain domain, RealmModel realm, UserModel user, String username) {
final Map<String, String> linkedIdps;

EmailHomeIdpDiscovererConfig config = new EmailHomeIdpDiscovererConfig(context.getAuthenticatorConfig());
if (user == null || !config.forwardToLinkedIdp()) {
linkedIdps = Collections.emptyMap();
LOG.tracef(
Expand All @@ -84,17 +88,17 @@ private List<IdentityProviderModel> discoverHomeIdps(AuthenticationFlowContext c
LOG.tracef(
"Found local user '%s' and forwarding to linked IdP is enabled. Discovering linked IdPs.",
username);
linkedIdps = context.getSession().users()
.getFederatedIdentitiesStream(context.getRealm(), user)
linkedIdps = users
.getFederatedIdentitiesStream(realm, user)
.collect(
Collectors.toMap(FederatedIdentityModel::getIdentityProvider, FederatedIdentityModel::getUserName));
}

List<IdentityProviderModel> candidateIdps = identityProviders.candidatesForHomeIdp(context, user);
List<IdentityProviderModel> candidateIdps = identityProviders.candidatesForHomeIdp(realm, user);
if (candidateIdps == null) {
candidateIdps = emptyList();
}
List<IdentityProviderModel> idpsWithMatchingDomain = identityProviders.withMatchingDomain(context, candidateIdps, domain);
List<IdentityProviderModel> idpsWithMatchingDomain = identityProviders.withMatchingDomain(config, candidateIdps, domain);
if (idpsWithMatchingDomain == null) {
idpsWithMatchingDomain = emptyList();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package de.sventorben.keycloak.authentication.hidpd.discovery.email;

import de.sventorben.keycloak.authentication.hidpd.PublicAPI;
import de.sventorben.keycloak.authentication.hidpd.discovery.HomeIdpDiscovererConfig;
import org.keycloak.models.AuthenticatorConfigModel;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
Expand All @@ -10,11 +12,12 @@
import static org.keycloak.provider.ProviderConfigProperty.BOOLEAN_TYPE;
import static org.keycloak.provider.ProviderConfigProperty.STRING_TYPE;

final class EmailHomeIdpDiscovererConfig {
@PublicAPI(unstable = true)
public final class EmailHomeIdpDiscovererConfig implements HomeIdpDiscovererConfig {

private static final String FORWARD_TO_LINKED_IDP = "forwardToLinkedIdp";
private static final String USER_ATTRIBUTE = "userAttribute";
private static final String FORWARD_UNVERIFIED_ATTRIBUTE = "forwardUnverifiedEmail";
public static final String USER_ATTRIBUTE = "userAttribute";
public static final String FORWARD_UNVERIFIED_ATTRIBUTE = "forwardUnverifiedEmail";

private static final ProviderConfigProperty FORWARD_TO_LINKED_IDP_PROPERTY = new ProviderConfigProperty(
FORWARD_TO_LINKED_IDP,
Expand Down Expand Up @@ -45,6 +48,7 @@ final class EmailHomeIdpDiscovererConfig {
.property(FORWARD_UNVERIFIED_PROPERTY)
.property(FORWARD_TO_LINKED_IDP_PROPERTY)
.build();

private final AuthenticatorConfigModel authenticatorConfigModel;

public EmailHomeIdpDiscovererConfig(AuthenticatorConfigModel authenticatorConfigModel) {
Expand All @@ -57,7 +61,8 @@ boolean forwardToLinkedIdp() {
.orElse(false);
}

String userAttribute() {
@Override
public String userAttribute() {
return Optional.ofNullable(authenticatorConfigModel)
.map(it -> it.getConfig().getOrDefault(USER_ATTRIBUTE, "email").trim())
.orElse("email");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import de.sventorben.keycloak.authentication.hidpd.OperationalInfo;
import de.sventorben.keycloak.authentication.hidpd.Users;
import de.sventorben.keycloak.authentication.hidpd.discovery.HomeIdpDiscovererConfig;
import de.sventorben.keycloak.authentication.hidpd.discovery.spi.HomeIdpDiscoverer;
import de.sventorben.keycloak.authentication.hidpd.discovery.spi.HomeIdpDiscovererFactory;
import org.keycloak.Config;
Expand All @@ -16,7 +17,7 @@ public final class EmailHomeIdpDiscovererFactory implements HomeIdpDiscovererFac
static final String PROVIDER_ID = "email";

@Override
public HomeIdpDiscoverer create(KeycloakSession keycloakSession) {
public HomeIdpDiscoverer<? extends HomeIdpDiscovererConfig> create(KeycloakSession keycloakSession) {
return new EmailHomeIdpDiscoverer(new Users(keycloakSession), new DefaultIdentityProviders());
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package de.sventorben.keycloak.authentication.hidpd.discovery.email;

import de.sventorben.keycloak.authentication.hidpd.PublicAPI;
import de.sventorben.keycloak.authentication.hidpd.discovery.HomeIdpDiscovererConfig;
import org.jboss.logging.Logger;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.models.IdentityProviderModel;
Expand Down Expand Up @@ -32,15 +33,15 @@ public interface IdentityProviders {
* Filters the given list of identity provider candidates to return those that match a specified
* domain within the context of an authentication flow.
*
* @param context The authentication flow context providing runtime information about the
* @param discovererConfig The authentication flow context providing runtime information about the
* current authentication process.
* @param candidates A list of potentially eligible identity providers that may be suitable
* for the user based on initial criteria (see {@code #candidatesForHomeIdp}).
* @param domain The domain criteria used to match identity providers.
* @return A filtered list of {@link IdentityProviderModel} that match the specified domain criteria.
* May be empty but not {@code null}.
*/
List<IdentityProviderModel> withMatchingDomain(AuthenticationFlowContext context, List<IdentityProviderModel> candidates, Domain domain);
List<IdentityProviderModel> withMatchingDomain(HomeIdpDiscovererConfig discovererConfig, List<IdentityProviderModel> candidates, Domain domain);

/**
* Retrieves a list of identity providers that are candidates for being the user's home IdP.
Expand All @@ -51,8 +52,7 @@ public interface IdentityProviders {
* current authentication process.
* @return A list of {@link IdentityProviderModel} from the realm. May be empty but not {@code null}.
*/
default List<IdentityProviderModel> candidatesForHomeIdp(AuthenticationFlowContext context, UserModel user) {
RealmModel realm = context.getRealm();
default List<IdentityProviderModel> candidatesForHomeIdp(RealmModel realm, UserModel user) {
List<IdentityProviderModel> enabledIdps = realm.getIdentityProvidersStream()
.filter(IdentityProviderModel::isEnabled)
.collect(Collectors.toList());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
package de.sventorben.keycloak.authentication.hidpd.discovery.orgs.domainhint;

import de.sventorben.keycloak.authentication.hidpd.discovery.email.EmailHomeIdpDiscovererConfig;
import de.sventorben.keycloak.authentication.hidpd.discovery.spi.HomeIdpDiscoverer;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OrganizationModel;
import org.keycloak.models.RealmModel;
import org.keycloak.organization.OrganizationProvider;

import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

final class OrgsDomainDiscoverer implements HomeIdpDiscoverer {
final class OrgsDomainDiscoverer implements HomeIdpDiscoverer<EmailHomeIdpDiscovererConfig> {

private final KeycloakSession keycloakSession;

Expand All @@ -20,7 +22,7 @@ final class OrgsDomainDiscoverer implements HomeIdpDiscoverer {
}

@Override
public List<IdentityProviderModel> discoverForUser(AuthenticationFlowContext context, String username) {
public List<IdentityProviderModel> discoverForUser(EmailHomeIdpDiscovererConfig discovererConfig, RealmModel realm, String username) {
String domain = username;
OrganizationProvider orgProvider = keycloakSession.getProvider(OrganizationProvider.class);

Expand All @@ -37,6 +39,11 @@ public List<IdentityProviderModel> discoverForUser(AuthenticationFlowContext con
return Collections.emptyList();
}

@Override
public EmailHomeIdpDiscovererConfig getConfigFrom(AuthenticationFlowContext context) {
return new EmailHomeIdpDiscovererConfig(context.getAuthenticatorConfig());
}

@Override
public void close() {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public final class OrgsEmailHomeIdpDiscovererFactory implements HomeIdpDiscovere

@Override
public HomeIdpDiscoverer create(KeycloakSession keycloakSession) {
return new EmailHomeIdpDiscoverer(new Users(keycloakSession), new OrgsIdentityProviders());
return new EmailHomeIdpDiscoverer(new Users(keycloakSession), new OrgsIdentityProviders(keycloakSession));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
package de.sventorben.keycloak.authentication.hidpd.discovery.orgs.email;

import de.sventorben.keycloak.authentication.hidpd.discovery.HomeIdpDiscovererConfig;
import de.sventorben.keycloak.authentication.hidpd.discovery.email.Domain;
import de.sventorben.keycloak.authentication.hidpd.discovery.email.IdentityProviders;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.OrganizationDomainModel;
import org.keycloak.models.OrganizationModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.*;
import org.keycloak.organization.OrganizationProvider;

import java.util.ArrayList;
Expand All @@ -17,9 +15,15 @@

final class OrgsIdentityProviders implements IdentityProviders {

private final KeycloakSession keycloakSession;

OrgsIdentityProviders(KeycloakSession keycloakSession) {
this.keycloakSession = keycloakSession;
}

@Override
public List<IdentityProviderModel> candidatesForHomeIdp(AuthenticationFlowContext context, UserModel user) {
OrganizationProvider orgProvider = context.getSession().getProvider(OrganizationProvider.class);
public List<IdentityProviderModel> candidatesForHomeIdp(RealmModel realm, UserModel user) {
OrganizationProvider orgProvider = keycloakSession.getProvider(OrganizationProvider.class);
if (user == null) {
return Collections.emptyList();
}
Expand All @@ -39,8 +43,8 @@ public List<IdentityProviderModel> candidatesForHomeIdp(AuthenticationFlowContex
}

@Override
public List<IdentityProviderModel> withMatchingDomain(AuthenticationFlowContext context, List<IdentityProviderModel> candidates, Domain domain) {
OrganizationProvider orgProvider = context.getSession().getProvider(OrganizationProvider.class);
public List<IdentityProviderModel> withMatchingDomain(HomeIdpDiscovererConfig discovererConfig, List<IdentityProviderModel> candidates, Domain domain) {
OrganizationProvider orgProvider = keycloakSession.getProvider(OrganizationProvider.class);
if (orgProvider.isEnabled()) {
OrganizationModel org = orgProvider.getByDomainName(domain.getRawValue());
if (org != null && org.isEnabled()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package de.sventorben.keycloak.authentication.hidpd.discovery.spi;

import de.sventorben.keycloak.authentication.hidpd.PublicAPI;
import de.sventorben.keycloak.authentication.hidpd.discovery.HomeIdpDiscovererConfig;
import de.sventorben.keycloak.authentication.hidpd.discovery.email.EmailHomeIdpDiscovererConfig;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.Provider;

import java.util.List;
Expand All @@ -28,7 +31,7 @@
* @see AuthenticationFlowContext
*/
@PublicAPI(unstable = true)
public interface HomeIdpDiscoverer extends Provider {
public interface HomeIdpDiscoverer<DC extends HomeIdpDiscovererConfig> extends Provider {

/**
* Discovers and returns a list of {@link IdentityProviderModel} instances representing
Expand Down Expand Up @@ -58,5 +61,14 @@ public interface HomeIdpDiscoverer extends Provider {
* home IdP(s) for the user. The list may be empty if no home IdP is associated
* with the user. Do not return {@code null}.
*/
List<IdentityProviderModel> discoverForUser(AuthenticationFlowContext context, String username);
default List<IdentityProviderModel> discoverForUser(AuthenticationFlowContext context, String username) {
DC discovererConfig = getConfigFrom(context);
RealmModel realm = context.getRealm();
return discoverForUser(discovererConfig, realm, username);
}

List<IdentityProviderModel> discoverForUser(DC discovererConfig, RealmModel realm, String username);

DC getConfigFrom(AuthenticationFlowContext context);

}
Loading