Skip to content

Commit

Permalink
feat: Support KC Organization Feature (DRAFT)
Browse files Browse the repository at this point in the history
This is just a rough draft - needs testing and refactorings.
  • Loading branch information
sventorben committed Apr 20, 2024
1 parent 810d84a commit e10811a
Show file tree
Hide file tree
Showing 16 changed files with 355 additions and 16 deletions.
4 changes: 2 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ services:
keycloak:
container_name: keycloak
hostname: keycloak
image: quay.io/keycloak/keycloak:24.0.2
image: quay.io/keycloak/keycloak:nightly

environment:
KEYCLOAK_ADMIN: admin
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
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
<maven.compiler.release>17</maven.compiler.release>

<!-- For compilation -->
<version.keycloak>24.0.2</version.keycloak>
<version.keycloak>999.0.0-SNAPSHOT</version.keycloak>

<!-- For compatibility tests -->
<keycloak.version>${version.keycloak}</keycloak.version>
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 @@ -3,6 +3,7 @@
import jakarta.ws.rs.core.Response;
import org.jboss.logging.Logger;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.broker.provider.AuthenticationRequest;
import org.keycloak.broker.provider.IdentityProvider;
import org.keycloak.broker.provider.IdentityProviderFactory;
Expand All @@ -13,6 +14,7 @@
import org.keycloak.models.RealmModel;
import org.keycloak.services.Urls;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.resources.IdentityBrokerService;
import org.keycloak.sessions.AuthenticationSessionModel;

import static org.keycloak.services.resources.IdentityBrokerService.getIdentityProviderFactory;
Expand Down Expand Up @@ -45,26 +47,27 @@ void redirectTo(IdentityProviderModel idp) {
}
new HomeIdpAuthenticationFlowContext(context).loginHint().copyTo(clientSessionCode);
IdentityProviderFactory providerFactory = getIdentityProviderFactory(keycloakSession, idp);
IdentityProvider identityProvider = providerFactory.create(keycloakSession, idp);
IdentityProvider<?> identityProvider = providerFactory.create(keycloakSession, idp);

Response response = identityProvider.performLogin(createAuthenticationRequest(providerAlias, clientSessionCode));
Response response = identityProvider.performLogin(createAuthenticationRequest(providerAlias, identityProvider, clientSessionCode));
context.forceChallenge(response);
}

private AuthenticationRequest createAuthenticationRequest(String providerId, ClientSessionCode<AuthenticationSessionModel> clientSessionCode) {
private AuthenticationRequest createAuthenticationRequest(String providerAlias, IdentityProvider<?> identityProvider, ClientSessionCode<AuthenticationSessionModel> clientSessionCode) {
AuthenticationSessionModel authSession = null;
IdentityBrokerState encodedState = null;

if (clientSessionCode != null) {
authSession = clientSessionCode.getClientSession();
String relayState = clientSessionCode.getOrGenerateCode();
encodedState = IdentityBrokerState.decoded(relayState, authSession.getClient().getId(), authSession.getClient().getClientId(), authSession.getTabId());
String clientData = identityProvider.supportsLongStateParameter() ? AuthenticationProcessor.getClientData(context.getSession(), authSession) : null;
encodedState = IdentityBrokerState.decoded(relayState, authSession.getClient().getId(), authSession.getClient().getClientId(), authSession.getTabId(), clientData);
}

KeycloakSession keycloakSession = context.getSession();
KeycloakUriInfo keycloakUriInfo = keycloakSession.getContext().getUri();
RealmModel realm = context.getRealm();
String redirectUri = Urls.identityProviderAuthnResponse(keycloakUriInfo.getBaseUri(), providerId, realm.getName()).toString();
String redirectUri = Urls.identityProviderAuthnResponse(keycloakUriInfo.getBaseUri(), providerAlias, realm.getName()).toString();
return new AuthenticationRequest(keycloakSession, realm, authSession, context.getHttpRequest(), keycloakUriInfo, encodedState, redirectUri);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,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
Expand Up @@ -6,8 +6,11 @@
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 +38,9 @@ public void close() {
public String getId() {
return PROVIDER_ID;
}

@Override
public Map<String, String> getOperationalInfo() {
return ServInf;
}
}
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;

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) {
IdentityProviderModel idp = org.getIdentityProvider();
if (idp != null) {
return Collections.singletonList(idp);
}
}
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 {

private 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 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,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

0 comments on commit e10811a

Please sign in to comment.