commit b22d6fe2f7ded4965ca933fb2264361fa5988146 Author: Max Erenberg <> Date: Mon Dec 27 00:59:01 2021 -0500 add ConditionalUserAttributeValue authenticator diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..477bd39 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/ca +/target +/META-INF diff --git a/README.md b/README.md new file mode 100644 index 0000000..2872a24 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# keycloak-spi +This repository contains some custom [Keycloak SPIs](https://www.keycloak.org/docs/latest/server_development/#_providers) +used by CSC on our Keycloak instance. + +## Build +Requires OpenJDK 11+ and Maven 3.6+. +```sh +mvn clean package +``` + +## Install +Copy target/csc-keycloak-spi.jar to /opt/jboss/keycloak/standalone/deployments +in the container where Keycloak is running. diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..644f806 --- /dev/null +++ b/pom.xml @@ -0,0 +1,92 @@ + + + + 4.0.0 + + ca.uwaterloo.csclub + keycloak-spi + 0.1.0 + jar + + + UTF-8 + 11 + 11 + 16.1.0 + 3.4.1.Final + 3.2.0 + + + + + org.keycloak + keycloak-core + ${version.org.keycloak} + + + org.keycloak + keycloak-server-spi + ${version.org.keycloak} + + + org.keycloak + keycloak-server-spi-private + ${version.org.keycloak} + + + org.keycloak + keycloak-services + ${version.org.keycloak} + + + org.jboss.logging + jboss-logging + ${version.org.jboss.logging} + provided + + + + + csc-keycloak-spi + + + org.wildfly.plugins + wildfly-maven-plugin + ${version.org.keycloak} + + false + + + + + + org.apache.maven.plugins + maven-jar-plugin + ${version.org.apache.maven.plugins.maven-jar-plugin} + + + + org.keycloak.keycloak-services + + + + + + + diff --git a/src/main/java/ca/uwaterloo/csclub/keycloakspi/authenticator/ConditionalUserAttributeValue.java b/src/main/java/ca/uwaterloo/csclub/keycloakspi/authenticator/ConditionalUserAttributeValue.java new file mode 100644 index 0000000..0e162ec --- /dev/null +++ b/src/main/java/ca/uwaterloo/csclub/keycloakspi/authenticator/ConditionalUserAttributeValue.java @@ -0,0 +1,56 @@ +// Copied from https://github.com/keycloak/keycloak/blob/main/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/authentication/ConditionalUserAttributeValue.java +package ca.uwaterloo.csclub.keycloakspi.authenticator; + +import org.keycloak.authentication.AuthenticationFlowContext; +import org.keycloak.authentication.AuthenticationFlowError; +import org.keycloak.authentication.AuthenticationFlowException; +import org.keycloak.authentication.authenticators.conditional.ConditionalAuthenticator; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; + +import java.util.Map; +import java.util.Objects; + + +public class ConditionalUserAttributeValue implements ConditionalAuthenticator { + + static final ConditionalUserAttributeValue SINGLETON = new ConditionalUserAttributeValue(); + + @Override + public boolean matchCondition(AuthenticationFlowContext context) { + // Retrieve configuration + Map config = context.getAuthenticatorConfig().getConfig(); + String attributeName = config.get(ConditionalUserAttributeValueFactory.CONF_ATTRIBUTE_NAME); + String attributeValue = config.get(ConditionalUserAttributeValueFactory.CONF_ATTRIBUTE_EXPECTED_VALUE); + boolean negateOutput = Boolean.parseBoolean(config.get(ConditionalUserAttributeValueFactory.CONF_NOT)); + + UserModel user = context.getUser(); + if (user == null) { + throw new AuthenticationFlowException("authenticator: " + ConditionalUserAttributeValueFactory.PROVIDER_ID, AuthenticationFlowError.UNKNOWN_USER); + } + + boolean result = user.getAttributeStream(attributeName).anyMatch(attr -> Objects.equals(attr, attributeValue)); + return negateOutput != result; + } + + @Override + public void action(AuthenticationFlowContext context) { + // Not used + } + + @Override + public boolean requiresUser() { + return true; + } + + @Override + public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) { + // Not used + } + + @Override + public void close() { + // Does nothing + } +} diff --git a/src/main/java/ca/uwaterloo/csclub/keycloakspi/authenticator/ConditionalUserAttributeValueFactory.java b/src/main/java/ca/uwaterloo/csclub/keycloakspi/authenticator/ConditionalUserAttributeValueFactory.java new file mode 100644 index 0000000..a9e60a5 --- /dev/null +++ b/src/main/java/ca/uwaterloo/csclub/keycloakspi/authenticator/ConditionalUserAttributeValueFactory.java @@ -0,0 +1,103 @@ +// Copied from https://github.com/keycloak/keycloak/blob/main/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/authentication/ConditionalUserAttributeValueFactory.java +package ca.uwaterloo.csclub.keycloakspi.authenticator; + +import org.keycloak.Config; +import org.keycloak.authentication.authenticators.conditional.ConditionalAuthenticator; +import org.keycloak.authentication.authenticators.conditional.ConditionalAuthenticatorFactory; +import org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.provider.ProviderConfigProperty; + +import java.util.Arrays; +import java.util.List; + +public class ConditionalUserAttributeValueFactory implements ConditionalAuthenticatorFactory { + + public static final String PROVIDER_ID = "conditional-user-attribute"; + + public static final String CONF_ATTRIBUTE_NAME = "attribute_name"; + public static final String CONF_ATTRIBUTE_EXPECTED_VALUE = "attribute_expected_value"; + public static final String CONF_NOT = "not"; + + private static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = { + AuthenticationExecutionModel.Requirement.REQUIRED, AuthenticationExecutionModel.Requirement.DISABLED + }; + + @Override + public void init(Config.Scope config) { + // no-op + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + // no-op + } + + @Override + public void close() { + // no-op + } + + @Override + public String getId() { + return PROVIDER_ID; + } + + @Override + public String getDisplayType() { + return "Condition - user attribute"; + } + + @Override + public String getReferenceCategory() { + return "condition"; + } + + @Override + public boolean isConfigurable() { + return true; + } + + @Override + public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { + return REQUIREMENT_CHOICES; + } + + @Override + public boolean isUserSetupAllowed() { + return false; + } + + @Override + public String getHelpText() { + return "Flow is executed only if the user attribute exists and has the expected value"; + } + + @Override + public List getConfigProperties() { + ProviderConfigProperty authNoteName = new ProviderConfigProperty(); + authNoteName.setType(ProviderConfigProperty.STRING_TYPE); + authNoteName.setName(CONF_ATTRIBUTE_NAME); + authNoteName.setLabel("Attribute name"); + authNoteName.setHelpText("Name of the attribute to check"); + + ProviderConfigProperty authNoteExpectedValue = new ProviderConfigProperty(); + authNoteExpectedValue.setType(ProviderConfigProperty.STRING_TYPE); + authNoteExpectedValue.setName(CONF_ATTRIBUTE_EXPECTED_VALUE); + authNoteExpectedValue.setLabel("Expected attribute value"); + authNoteExpectedValue.setHelpText("Expected value in the attribute"); + + ProviderConfigProperty negateOutput = new ProviderConfigProperty(); + negateOutput.setType(ProviderConfigProperty.BOOLEAN_TYPE); + negateOutput.setName(CONF_NOT); + negateOutput.setLabel("Negate output"); + negateOutput.setHelpText("Apply a not to the check result"); + + return Arrays.asList(authNoteName, authNoteExpectedValue, negateOutput); + } + + @Override + public ConditionalAuthenticator getSingleton() { + return ConditionalUserAttributeValue.SINGLETON; + } +} diff --git a/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory b/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory new file mode 100644 index 0000000..b214495 --- /dev/null +++ b/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory @@ -0,0 +1 @@ +ca.uwaterloo.csclub.keycloakspi.authenticator.ConditionalUserAttributeValueFactory