From b22d6fe2f7ded4965ca933fb2264361fa5988146 Mon Sep 17 00:00:00 2001
From: Max Erenberg <>
Date: Mon, 27 Dec 2021 00:59:01 -0500
Subject: [PATCH] add ConditionalUserAttributeValue authenticator
---
.gitignore | 3 +
README.md | 13 +++
pom.xml | 92 ++++++++++++++++
.../ConditionalUserAttributeValue.java | 56 ++++++++++
.../ConditionalUserAttributeValueFactory.java | 103 ++++++++++++++++++
...ycloak.authentication.AuthenticatorFactory | 1 +
6 files changed, 268 insertions(+)
create mode 100644 .gitignore
create mode 100644 README.md
create mode 100644 pom.xml
create mode 100644 src/main/java/ca/uwaterloo/csclub/keycloakspi/authenticator/ConditionalUserAttributeValue.java
create mode 100644 src/main/java/ca/uwaterloo/csclub/keycloakspi/authenticator/ConditionalUserAttributeValueFactory.java
create mode 100644 src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory
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