Initial implementation

This commit is contained in:
Sven-Torben Janus 2021-05-07 23:50:44 +02:00
parent df09e3a8ba
commit 30c3e5756a
11 changed files with 511 additions and 0 deletions

11
.editorconfig Executable file
View file

@ -0,0 +1,11 @@
root = true
[*]
charset = utf-8
insert_final_newline = true
indent_style = space
indent_size = 4
continuation_indent_size = 4
ij_continuation_indent_size = 4
max_line_length = 120
trim_trailing_whitespace = true

2
.gitattributes vendored Executable file
View file

@ -0,0 +1,2 @@
# Set the default behavior, in case people don't have core.autocrlf set.
* text=auto

28
.github/workflows/buildAndRelease.yml vendored Executable file
View file

@ -0,0 +1,28 @@
name: Java CI with Maven
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v1
with:
java-version: 11
- name: Cache maven repository
uses: actions/cache@v2
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
- name: Build with Maven
run: mvn -B clean package --file pom.xml

96
.gitignore vendored Executable file
View file

@ -0,0 +1,96 @@
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
.mvn/wrapper/maven-wrapper.jar
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
.idea/
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
*.iml
*.ipr
# CMake
cmake-build-*/
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties

19
docker-compose.yml Executable file
View file

@ -0,0 +1,19 @@
version: '3'
services:
keycloak:
container_name: keycloak
image: jboss/keycloak:13.0.0
environment:
DB_VENDOR: h2
KEYCLOAK_USER: admin
KEYCLOAK_PASSWORD: admin
DEBUG_PORT: '*:8787'
DEBUG_MODE: 'true'
command: '--debug'
ports:
- 8080:8080
- 8443:8443
- 8787:8787
- 9990:9990
volumes:
- ./target/keycloak-restrict-client-auth.jar:/opt/jboss/keycloak/standalone/deployments/keycloak-restrict-client-auth.jar

172
pom.xml Executable file
View file

@ -0,0 +1,172 @@
<?xml version="1.0" encoding="UTF-8"?>
<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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>de.sventorben.keycloak</groupId>
<artifactId>keycloak-restrict-client-auth</artifactId>
<version>13.0.0</version>
<name>Keycloak: Authenticator - Restrict client authentication</name>
<description>A Keycloak authenticator to restrict authentication on clients</description>
<packaging>jar</packaging>
<inceptionYear>2021</inceptionYear>
<developers>
<developer>
<id>sventorben</id>
<name>Sven-Torben Janus</name>
<email>sven-torben@sven-torben.de</email>
<url>https://sventorben.de</url>
<timezone>Europe/Berlin</timezone>
</developer>
</developers>
<scm>
<connection>scm:git:ssh://git@github.com:sventorben/keycloak-restrict-client-auth.git</connection>
<developerConnection>scm:git:ssh://git@github.com:sventorben/keycloak-restrict-client-auth.git</developerConnection>
<url>https://github.com/sventorben/keycloak-restrict-client-auth.git</url>
<tag>HEAD</tag>
</scm>
<issueManagement>
<url>https://github.com/sventorben/keycloak-restrict-client-auth/issues</url>
<system>GitHub Issues</system>
</issueManagement>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>13</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<maven.compiler.release>11</maven.compiler.release>
<version.keycloak>13.0.0</version.keycloak>
<version.mockito>3.7.7</version.mockito>
</properties>
<build>
<finalName>${project.artifactId}</finalName>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.0.0-M5</version>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<manifest>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
</manifest>
<manifestEntries>
<Dependencies>org.keycloak.keycloak-services</Dependencies>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
<dependencyManagement>
<dependencies>
<!-- Keycloak -->
<dependency>
<groupId>org.keycloak.bom</groupId>
<artifactId>keycloak-spi-bom</artifactId>
<version>${version.keycloak}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Tests -->
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>5.7.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Keycloak Extension -->
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
<version>${version.keycloak}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-server-spi</artifactId>
<version>${version.keycloak}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-server-spi-private</artifactId>
<version>${version.keycloak}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-services</artifactId>
<version>${version.keycloak}</version>
<scope>provided</scope>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${version.mockito}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>${version.mockito}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.19.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View file

@ -0,0 +1,84 @@
package de.sventorben.keycloak.authentication;
import org.jboss.logging.Logger;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.Authenticator;
import org.keycloak.models.*;
import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
public final class RestrictClientAuthAuthenticator implements Authenticator {
private static final Logger LOG = Logger.getLogger(RestrictClientAuthAuthenticator.class);
private final String clientRoleName;
RestrictClientAuthAuthenticator(final String clientRoleName) {
this.clientRoleName = clientRoleName;
}
@Override
public void authenticate(final AuthenticationFlowContext context) {
final ClientModel client = context.getSession().getContext().getClient();
if (!isAuthRestricted(client)) {
context.success();
return;
}
final UserModel user = context.getUser();
if (userHasClientRole(client, user)) {
context.success();
} else {
LOG.warnf("Authentication for user '%s' failed. User does not have client role '%s' on client '%s'.",
user.getUsername(), clientRoleName, client.getId());
final Response response = errorResponse("access_denied", "Access to client is denied.");
context.failure(AuthenticationFlowError.ACCESS_DENIED, response);
}
}
private boolean isAuthRestricted(ClientModel client) {
return client.getRole(clientRoleName) != null;
}
private boolean userHasClientRole(ClientModel client, UserModel user) {
final RoleModel role = client.getRole(clientRoleName);
if (role == null) return false;
return user != null && user.hasRole(role);
}
private static Response errorResponse(String error, String errorDescription) {
return Response.status(Response.Status.UNAUTHORIZED.getStatusCode())
.entity(new OAuth2ErrorRepresentation(error, errorDescription))
.type(MediaType.APPLICATION_JSON_TYPE)
.build();
}
@Override
public void action(AuthenticationFlowContext context) {
LOG.warn("Action called!");
context.failure(AuthenticationFlowError.ACCESS_DENIED);
}
@Override
public boolean requiresUser() {
return true;
}
@Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return true;
}
@Override
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,97 @@
package de.sventorben.keycloak.authentication;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ServerInfoAwareProviderFactory;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static org.keycloak.models.AuthenticationExecutionModel.Requirement.DISABLED;
import static org.keycloak.models.AuthenticationExecutionModel.Requirement.REQUIRED;
public class RestrictClientAuthAuthenticatorFactory implements AuthenticatorFactory, ServerInfoAwareProviderFactory {
private static final Logger LOG = Logger.getLogger(RestrictClientAuthAuthenticatorFactory.class);
private static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = new AuthenticationExecutionModel.Requirement[]{REQUIRED, DISABLED};
private static final String CLIENT_ROLE_NAME = "clientRoleName";
private final String CLIENT_ROLE_NAME_DEFAULT = "restricted-access";
private static final String PROVIDER_ID = "restrict-client-auth-authenticator";
private Config.Scope config;
@Override
public String getDisplayType() {
return "Restrict Client Authentication";
}
@Override
public String getReferenceCategory() {
return "JWT";
}
@Override
public boolean isConfigurable() {
return false;
}
@Override
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
return REQUIREMENT_CHOICES;
}
@Override
public boolean isUserSetupAllowed() {
return false;
}
@Override
public String getHelpText() {
return "Restricts authorization for users on certain clients based on a client role";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return Collections.emptyList();
}
@Override
public Authenticator create(KeycloakSession session) {
String clientRoleName = config.get(CLIENT_ROLE_NAME, CLIENT_ROLE_NAME_DEFAULT);
return new RestrictClientAuthAuthenticator(clientRoleName);
}
@Override
public void init(Config.Scope config) {
this.config = config;
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public Map<String, String> getOperationalInfo() {
String version = getClass().getPackage().getImplementationVersion();
return Map.of("Version", version);
}
}

View file

View file

@ -0,0 +1 @@
de.sventorben.keycloak.authentication.RestrictClientAuthAuthenticatorFactory

View file

@ -0,0 +1 @@
mock-maker-inline