Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
col-panic committed Jun 21, 2024
1 parent f748316 commit a599f0c
Show file tree
Hide file tree
Showing 12 changed files with 500 additions and 2 deletions.
40 changes: 40 additions & 0 deletions .classpath
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="optional" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="test" value="true"/>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
<attributes>
<attribute name="test" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="optional" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/bin/
/target/
deployments/
23 changes: 23 additions & 0 deletions .project
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>keycloak-conditional-http-header-authenticator</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>
6 changes: 6 additions & 0 deletions .settings/org.eclipse.core.resources.prefs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
eclipse.preferences.version=1
encoding//src/main/java=UTF-8
encoding//src/main/resources=UTF-8
encoding//src/test/java=UTF-8
encoding//src/test/resources=UTF-8
encoding/<project>=UTF-8
8 changes: 8 additions & 0 deletions .settings/org.eclipse.jdt.core.prefs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.targetPlatform=17
org.eclipse.jdt.core.compiler.compliance=17
org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore
org.eclipse.jdt.core.compiler.release=disabled
org.eclipse.jdt.core.compiler.source=17
23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,21 @@
# keycloak-conditional-http-header-authenticator
Keycloak Authenticator matching HTTP header
# Keycloak Conditional HTTP Header Authenticator

Check the http request header to match on a regex string.

Initial code by Sebastian Preisner, copied from https://github.com/keycloak/keycloak/pull/14605.

Relevant Keycloak Documentation part: https://www.keycloak.org/docs/latest/server_admin/#conditions-in-conditional-flows

### Build

Use with Maven (>3) and JDK (>= 17).

```
mvn clean verify
```

Copy deployments/conditional-http-header-authenticator-jar-with-dependencies.jar to /providers/ directory in Keycloak.

### Example

See https://github.com/elexis/keycloak-auth-deny-user-nosecondfactor for a usage example.
88 changes: 88 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

<name>Conditional HTTP Header Check</name>
<description/>
<modelVersion>4.0.0</modelVersion>

<groupId>info.elexis</groupId>
<artifactId>keycloak-conditional-http-header-authenticator</artifactId>
<version>0.1.0</version>
<packaging>jar</packaging>

<properties>
<java.version>17</java.version>
<keycloak.version>24.0.5</keycloak.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<junit.version>4.13.2</junit.version>
<jboss.logging.version>3.4.1.Final</jboss.logging.version>
</properties>

<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
<version>${keycloak.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-server-spi</artifactId>
<version>${keycloak.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-server-spi-private</artifactId>
<version>${keycloak.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
<version>${jboss.logging.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-services</artifactId>
<version>${keycloak.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>


<build>
<finalName>conditional-http-header-authenticator</finalName>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<outputDirectory>deployments</outputDirectory>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package info.elexis.conditionalhttpheader.authentication;

import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

import org.jboss.logging.Logger;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.authenticators.conditional.ConditionalAuthenticator;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;

import jakarta.ws.rs.core.MultivaluedMap;

public class ConditionalHttpHeaderAuthenticator implements ConditionalAuthenticator {

private static final Logger logger = Logger.getLogger(ConditionalHttpHeaderAuthenticator.class);

public static final ConditionalHttpHeaderAuthenticator SINGLETON = new ConditionalHttpHeaderAuthenticator();

public boolean containsMatchingRequestHeader(MultivaluedMap<String, String> requestHeaders, String headerPattern) {
if (headerPattern == null) {
logger.debugv("The matching request header patterns are <null>!");
return false;
}

Pattern pattern = Pattern.compile(headerPattern, Pattern.DOTALL | Pattern.CASE_INSENSITIVE);

for (Map.Entry<String, List<String>> entry : requestHeaders.entrySet()) {

String key = entry.getKey();
for (String value : entry.getValue()) {
String headerEntry = key.trim() + ": " + value.trim();
if (pattern.matcher(headerEntry).matches()) {
logger.debugv("Pattern {0} matches header entry {1}", headerPattern, headerEntry);
return true;
}
}
}

return false;
}

@Override
public boolean matchCondition(AuthenticationFlowContext context) {
Map<String, String> config = context.getAuthenticatorConfig().getConfig();
if (config == null) {
return false;
}

MultivaluedMap<String, String> requestHeaders = context.getHttpRequest().getHttpHeaders().getRequestHeaders();
String headerPattern = config.get(ConditionalHttpHeaderAuthenticatorFactory.HTTP_HEADER_PATTERN);
boolean negateOutcome = Boolean
.parseBoolean(config.get(ConditionalHttpHeaderAuthenticatorFactory.NEGATE_OUTCOME));

return (negateOutcome != containsMatchingRequestHeader(requestHeaders, headerPattern));
}

@Override
public boolean requiresUser() {
return false;
}

@Override
public void action(AuthenticationFlowContext context) {
// Not used
}

@Override
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
// Not used
}

@Override
public void close() {
// Does nothing
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package info.elexis.conditionalhttpheader.authentication;

import org.keycloak.Config.Scope;
import java.util.Arrays;
import java.util.List;

import org.keycloak.authentication.authenticators.conditional.ConditionalAuthenticator;
import org.keycloak.authentication.authenticators.conditional.ConditionalAuthenticatorFactory;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationExecutionModel.Requirement;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderConfigProperty;

/**
* An {@link ConditionalAuthenticatorFactory} for
* {@link ConditionalHttpHeaderAuthenticator}s.
*
* @author <a href="mailto:preisner@puzzle-itc.de">Sebastian Preisner</a>
*/
public class ConditionalHttpHeaderAuthenticatorFactory implements ConditionalAuthenticatorFactory {
public static final String PROVIDER_ID = "conditional-http-header";

public static final String HTTP_HEADER_PATTERN = "search_pattern";
public static final String NEGATE_OUTCOME = "negate_outcome";

@Override
public void init(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 - request header";
}

@Override
public boolean isConfigurable() {
return true;
}

private static final Requirement[] REQUIREMENT_CHOICES = { AuthenticationExecutionModel.Requirement.REQUIRED,
AuthenticationExecutionModel.Requirement.DISABLED };

@Override
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
return REQUIREMENT_CHOICES;
}

@Override
public boolean isUserSetupAllowed() {
return false;
}

@Override
public String getHelpText() {
return "Flow is executed olny if HTTP request header matches supplied regular expression.";
}

@Override
public List<ProviderConfigProperty> getConfigProperties() {

ProviderConfigProperty HttpHeaderPattern = new ProviderConfigProperty();
HttpHeaderPattern.setType(ProviderConfigProperty.STRING_TYPE);
HttpHeaderPattern.setName(HTTP_HEADER_PATTERN);
HttpHeaderPattern.setLabel("HTTP Header Pattern");
HttpHeaderPattern.setHelpText("If a HTTP request header matches the given pattern the condition will be true."
+ "Can be used to specify trusted networks via: X-Forwarded-Host: (1.2.3.4|1.2.3.5)."
+ "In this case requests from 1.2.3.4 and 1.2.3.5 come from a trusted source.");
HttpHeaderPattern.setDefaultValue("");

ProviderConfigProperty negateOutcome = new ProviderConfigProperty();
negateOutcome.setType(ProviderConfigProperty.BOOLEAN_TYPE);
negateOutcome.setName(NEGATE_OUTCOME);
negateOutcome.setLabel("Negate");
negateOutcome.setHelpText("Send false if the given pattern matches.");

return Arrays.asList(HttpHeaderPattern, negateOutcome);
}

@Override
public ConditionalAuthenticator getSingleton() {
return ConditionalHttpHeaderAuthenticator.SINGLETON;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
info.elexis.conditionalhttpheader.authentication.ConditionalHttpHeaderAuthenticatorFactory
Loading

0 comments on commit a599f0c

Please sign in to comment.