Skip to content
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

Support Keycloak Organization Feature (PREVIEW) #351

Merged
merged 4 commits into from
Jun 10, 2024
Merged
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
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ services:
KEYCLOAK_ADMIN_PASSWORD: admin
DEBUG_PORT: '*:8787'
DEBUG: 'true'
command: ['start-dev', '--debug', '--import-realm']
command: ['start-dev', '--debug', '--import-realm', '--features=organization']
ports:
- 8080:8080
- 8443:8443
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,7 @@ public final void close() {

@Override
public final Map<String, String> getOperationalInfo() {
String version = getClass().getPackage().getImplementationVersion();
if (version == null) {
version = "dev-snapshot";
}
return Map.of("Version", version);
return OperationalInfo.get();
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package de.sventorben.keycloak.authentication.hidpd;

import java.util.Map;

public final class OperationalInfo {

public static Map<String, String> get() {
String version = OperationalInfo.class.getPackage().getImplementationVersion();
if (version == null) {
version = "dev-snapshot";
}
return Map.of("Version", version);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
final class DomainExtractor {

private static final Logger LOG = Logger.getLogger(DomainExtractor.class);
private static final String EMAIL_ATTRIBUTE = "email";

private final EmailHomeIdpDiscovererConfig config;

Expand All @@ -26,11 +25,6 @@ Optional<Domain> extractFrom(UserModel user) {
LOG.warnf("Could not find user attribute '%s' for user '%s'", config.userAttribute(), user.getId());
return Optional.empty();
}
if (EMAIL_ATTRIBUTE.equalsIgnoreCase(config.userAttribute()) && !user.isEmailVerified()
&& !config.forwardUserWithUnverifiedEmail()) {
sventorben marked this conversation as resolved.
Show resolved Hide resolved
LOG.warnf("Email address of user '%s' is not verified and forwarding not enabled", user.getId());
return Optional.empty();
}
return extractFrom(userAttribute);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
public final class EmailHomeIdpDiscoverer implements HomeIdpDiscoverer {

private static final Logger LOG = Logger.getLogger(EmailHomeIdpDiscoverer.class);
private static final String EMAIL_ATTRIBUTE = "email";
private final Users users;
private final IdentityProviders identityProviders;

Expand All @@ -29,7 +30,6 @@ public EmailHomeIdpDiscoverer(Users users, IdentityProviders identityProviders)

@Override
public List<IdentityProviderModel> discoverForUser(AuthenticationFlowContext context, String username) {

EmailHomeIdpDiscovererConfig config = new EmailHomeIdpDiscovererConfig(context.getAuthenticatorConfig());
DomainExtractor domainExtractor = new DomainExtractor(config);

Expand All @@ -48,7 +48,13 @@ public List<IdentityProviderModel> discoverForUser(AuthenticationFlowContext con
} else {
LOG.tracef("User found in AuthenticationFlowContext. Extracting domain from stored user '%s'.",
user.getId());
emailDomain = domainExtractor.extractFrom(user);
if (EMAIL_ATTRIBUTE.equalsIgnoreCase(config.userAttribute()) && !user.isEmailVerified()
&& !config.forwardUserWithUnverifiedEmail()) {
Comment on lines +51 to +52

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ New issue: Complex Conditional
discoverForUser has 1 complex conditionals with 2 branches, threshold = 2

Suppress

LOG.warnf("Email address of user '%s' is not verified and forwarding not enabled", user.getId());
emailDomain = Optional.empty();
} else {
emailDomain = domainExtractor.extractFrom(user);
}
Comment on lines +51 to +57

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ New issue: Bumpy Road Ahead
discoverForUser has 2 blocks with nested conditional logic. Any nesting of 2 or deeper is considered. Threshold is one single, nested block per function

Suppress

}

if (emailDomain.isPresent()) {
Expand Down Expand Up @@ -84,7 +90,7 @@ private List<IdentityProviderModel> discoverHomeIdps(AuthenticationFlowContext c
Collectors.toMap(FederatedIdentityModel::getIdentityProvider, FederatedIdentityModel::getUserName));
}

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

import de.sventorben.keycloak.authentication.hidpd.OperationalInfo;
import de.sventorben.keycloak.authentication.hidpd.Users;
import de.sventorben.keycloak.authentication.hidpd.discovery.spi.HomeIdpDiscoverer;
import de.sventorben.keycloak.authentication.hidpd.discovery.spi.HomeIdpDiscovererFactory;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ServerInfoAwareProviderFactory;

public final class EmailHomeIdpDiscovererFactory implements HomeIdpDiscovererFactory {
import java.util.Map;

public final class EmailHomeIdpDiscovererFactory implements HomeIdpDiscovererFactory, ServerInfoAwareProviderFactory {

static final String PROVIDER_ID = "email";

Expand Down Expand Up @@ -35,4 +39,9 @@ public void close() {
public String getId() {
return PROVIDER_ID;
}

@Override
public Map<String, String> getOperationalInfo() {
return OperationalInfo.get();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;

import java.util.List;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -50,7 +51,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) {
default List<IdentityProviderModel> candidatesForHomeIdp(AuthenticationFlowContext context, UserModel user) {
RealmModel realm = context.getRealm();
List<IdentityProviderModel> enabledIdps = realm.getIdentityProvidersStream()
.filter(IdentityProviderModel::isEnabled)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package de.sventorben.keycloak.authentication.hidpd.discovery.orgs.domainhint;

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.organization.OrganizationProvider;

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

final class OrgsDomainDiscoverer implements HomeIdpDiscoverer {

private final KeycloakSession keycloakSession;

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

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

if (!orgProvider.isEnabled()) {
return Collections.emptyList();
}

OrganizationModel org = orgProvider.getByDomainName(domain);
if (org != null) {
return org.getIdentityProviders()
.filter(IdentityProviderModel::isEnabled)
.collect(Collectors.toList());
}
return Collections.emptyList();
}

@Override
public void close() {

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package de.sventorben.keycloak.authentication.hidpd.discovery.orgs.domainhint;

import de.sventorben.keycloak.authentication.hidpd.OperationalInfo;
import de.sventorben.keycloak.authentication.hidpd.discovery.spi.HomeIdpDiscoverer;
import de.sventorben.keycloak.authentication.hidpd.discovery.spi.HomeIdpDiscovererFactory;
import org.keycloak.Config;
import org.keycloak.common.Profile;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.provider.ServerInfoAwareProviderFactory;

import java.util.Map;

public final class OrgsDomainDiscovererProviderFactory implements HomeIdpDiscovererFactory, EnvironmentDependentProviderFactory, ServerInfoAwareProviderFactory {

static final String PROVIDER_ID = "orgs-domain";

@Override
public boolean isSupported(Config.Scope config) {
return Profile.isFeatureEnabled(Profile.Feature.ORGANIZATION);
}

@Override
public HomeIdpDiscoverer create(KeycloakSession keycloakSession) {
return new OrgsDomainDiscoverer(keycloakSession);
}

@Override
public void init(Config.Scope scope) {

}

@Override
public void postInit(KeycloakSessionFactory keycloakSessionFactory) {

}

@Override
public void close() {

}

@Override
public String getId() {
return PROVIDER_ID;
}

@Override
public final Map<String, String> getOperationalInfo() {
return OperationalInfo.get();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package de.sventorben.keycloak.authentication.hidpd.discovery.orgs.domainhint;

import de.sventorben.keycloak.authentication.hidpd.AbstractHomeIdpDiscoveryAuthenticatorFactory;
import org.keycloak.Config;
import org.keycloak.common.Profile;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.provider.ProviderConfigProperty;

import java.util.Collections;
import java.util.List;

public final class OrgsDomainHomeIdpDiscoveryAuthenticatorFactory extends AbstractHomeIdpDiscoveryAuthenticatorFactory implements EnvironmentDependentProviderFactory {
private static final String PROVIDER_ID = "orgs-domain";

@Override
public boolean isSupported(Config.Scope config) {
return Profile.isFeatureEnabled(Profile.Feature.ORGANIZATION);
}

public OrgsDomainHomeIdpDiscoveryAuthenticatorFactory() {
super(new DiscovererConfig() {
@Override
public List<ProviderConfigProperty> getProperties() {
return Collections.emptyList();
}

@Override
public String getProviderId() {
return OrgsDomainDiscovererProviderFactory.PROVIDER_ID;
}
});
}

@Override
public String getId() {
return PROVIDER_ID;
}

@Override
public String getDisplayType() {
return "Home IdP Discovery - Organization via Domain Hint";
}

@Override
public String getReferenceCategory() {
return null;
}

@Override
public String getHelpText() {
return "Redirects users to their organization's identity provider which will be discovered based on a domain hint";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package de.sventorben.keycloak.authentication.hidpd.discovery.orgs.email;

import org.keycloak.models.AuthenticatorConfigModel;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;

import java.util.List;
import java.util.Optional;

import static org.keycloak.provider.ProviderConfigProperty.BOOLEAN_TYPE;
import static org.keycloak.provider.ProviderConfigProperty.STRING_TYPE;

final class OrgsEmailHomeIdpDiscovererConfig {

private static final String FORWARD_UNVERIFIED_ATTRIBUTE = "forwardUnverifiedEmail";

private static final ProviderConfigProperty FORWARD_UNVERIFIED_PROPERTY = new ProviderConfigProperty(
FORWARD_UNVERIFIED_ATTRIBUTE,
"Forward users with unverified email",
"If 'User attribute' is set to 'email', whether to forward existing user if user's email is not verified.",
BOOLEAN_TYPE,
false,
false);

static final List<ProviderConfigProperty> CONFIG_PROPERTIES = ProviderConfigurationBuilder.create()
.property(FORWARD_UNVERIFIED_PROPERTY)
.build();
private final AuthenticatorConfigModel authenticatorConfigModel;

public OrgsEmailHomeIdpDiscovererConfig(AuthenticatorConfigModel authenticatorConfigModel) {
this.authenticatorConfigModel = authenticatorConfigModel;
}

boolean forwardUserWithUnverifiedEmail() {
return Optional.ofNullable(authenticatorConfigModel)
.map(it -> Boolean.parseBoolean(it.getConfig().getOrDefault(FORWARD_UNVERIFIED_ATTRIBUTE, "false")))
.orElse(false);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package de.sventorben.keycloak.authentication.hidpd.discovery.orgs.email;

import de.sventorben.keycloak.authentication.hidpd.OperationalInfo;
import de.sventorben.keycloak.authentication.hidpd.Users;
import de.sventorben.keycloak.authentication.hidpd.discovery.email.EmailHomeIdpDiscoverer;
import de.sventorben.keycloak.authentication.hidpd.discovery.spi.HomeIdpDiscoverer;
import de.sventorben.keycloak.authentication.hidpd.discovery.spi.HomeIdpDiscovererFactory;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ServerInfoAwareProviderFactory;

import java.util.Map;

public final class OrgsEmailHomeIdpDiscovererFactory implements HomeIdpDiscovererFactory, ServerInfoAwareProviderFactory {

static final String PROVIDER_ID = "orgs-email";

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

@Override
public void init(Config.Scope scope) {

}

@Override
public void postInit(KeycloakSessionFactory keycloakSessionFactory) {

}

@Override
public void close() {

}

@Override
public String getId() {
return PROVIDER_ID;
}

@Override
public final Map<String, String> getOperationalInfo() {
return OperationalInfo.get();
}
}
Loading