Initial implementation
This commit is contained in:
parent
df09e3a8ba
commit
30c3e5756a
11 changed files with 511 additions and 0 deletions
11
.editorconfig
Executable file
11
.editorconfig
Executable 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
2
.gitattributes
vendored
Executable 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
28
.github/workflows/buildAndRelease.yml
vendored
Executable 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
96
.gitignore
vendored
Executable 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
19
docker-compose.yml
Executable 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
172
pom.xml
Executable 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>
|
|
@ -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() {
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
0
src/main/resources/META-INF/beans.xml
Executable file
0
src/main/resources/META-INF/beans.xml
Executable file
|
@ -0,0 +1 @@
|
|||
de.sventorben.keycloak.authentication.RestrictClientAuthAuthenticatorFactory
|
1
src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
Executable file
1
src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
Executable file
|
@ -0,0 +1 @@
|
|||
mock-maker-inline
|
Loading…
Reference in a new issue