Skip to content

Commit

Permalink
Initial import
Browse files Browse the repository at this point in the history
  • Loading branch information
trickert76 committed Jan 23, 2021
1 parent 011a8c9 commit af6da95
Show file tree
Hide file tree
Showing 18 changed files with 1,964 additions and 1 deletion.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ buildNumber.properties
.mvn/timing.properties
# https://github.com/takari/maven-wrapper#usage-without-binary-jar
.mvn/wrapper/maven-wrapper.jar
/.classpath
/.project
/.settings
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,16 @@
# http-keycloak-userstorage-spi
An example for having a custom user storage for Keycloak users, groups and roles

An example for having a custom user storage for Keycloak users, groups and roles.

Keycloak allows to have your own UserStorage backend. So you don't need to have an AD or LDAP. A possible use-case is for example an existing legacy system with its own user storage and you want to build some new services around it. When you then start with OIDC, you can use Keycloak as a OIDC provider and that can use your legacy backend for user storage.

This project uses the Keycloak UserStorage service provider interface to allow an HTTP client to read users from such a backend.

The backend itself needs at least this REST endpoints.

- GET /user - returns a list of HTTPUserModel. It supports paging and filtering (offset, limit) with the query param search for random search or group.
- GET /user/{username} - returns a single HTTPUserModel that matches the given username. If the HTTP response is not 200, there is no match.
- GET /user/mail/{email} - returns a single HTTPUserModel that matches the given mail address. If the HTTP response is not 200, there is no match.
- POST /user/validate/{username} - the POST body contains the password. This is used for validating the users password.

The HTTPUserModel contains some basic informations about the user for Keycloak, like the username, first and last name, email and attributes. If you want to apply groups and roles to the user (which is useful, if your services depends on different roles) your backend needs to fill the HashMap<String,List<String>>. Where the key is the group name and the List<String> is the list of role names. Of course you can build complexer GroupModels and RoleModels, if you want.
90 changes: 90 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>http.keycloak</groupId>
<artifactId>userstorage-spi</artifactId>
<version>0.1.0</version>

<name>Keycloak HTTP UserStoreProvider</name>
<description />

<properties>
<java.version>11</java.version>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

<keycloak.version>9.0.3</keycloak.version>
<resteasy.version>4.5.8.Final</resteasy.version>
</properties>

<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
<scope>provided</scope>
<version>${keycloak.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-server-spi</artifactId>
<scope>provided</scope>
<version>${keycloak.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-server-spi-private</artifactId>
<scope>provided</scope>
<version>${keycloak.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-kerberos-federation</artifactId>
<scope>provided</scope>
<version>${keycloak.version}</version>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-client</artifactId>
<version>${resteasy.version}</version>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jackson2-provider</artifactId>
<scope>provided</scope>
<version>${resteasy.version}</version>
</dependency>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
<scope>provided</scope>
<version>3.4.1.Final</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
<version>4.13.1</version>
</dependency>
<dependency>
<groupId>org.jboss.spec.javax.transaction</groupId>
<artifactId>jboss-transaction-api_1.3_spec</artifactId>
<scope>provided</scope>
<version>2.0.0.Final</version>
</dependency>
</dependencies>


<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
</plugin>
</plugins>
</build>

</project>
79 changes: 79 additions & 0 deletions src/main/java/http/keycloak/userstorage/FreshlyCreatedUsers.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package http.keycloak.userstorage;


import org.keycloak.models.KeycloakSession;

import java.util.Optional;

/**
* All new users who are still not persisted to http storage shall remain in current KeycloakSession. This way,
* if infinispan calls getUserById for the user that hasn't been persisted yet, this class will return the user as
* it was already persisted.
*/
public class FreshlyCreatedUsers {

private final KeycloakSession session;

public FreshlyCreatedUsers(KeycloakSession session) {
this.session = session;
}

private static boolean isNotBlank(String str) {
return str != null && !str.trim().isEmpty();
}

private static String usernameKey(String username) {
return "username:" + username;
}

private static String emailKey(String email) {
return "email:" + email;
}

private static String idKey(String id) {
return "id:" + id;
}

public Optional<HTTPUserModelDelegate> getFreshlyCreatedUserByUsername(String username) {
return Optional.ofNullable(session.getAttribute(usernameKey(username), HTTPUserModelDelegate.class));
}

public Optional<HTTPUserModelDelegate> getFreshlyCreatedUserById(String id) {
return Optional.ofNullable(session.getAttribute(idKey(id), HTTPUserModelDelegate.class));
}

public Optional<HTTPUserModelDelegate> getFreshlyCreatedUserByEmail(String email) {
return Optional.ofNullable(session.getAttribute(emailKey(email), HTTPUserModelDelegate.class));
}

public void saveInSession(HTTPUserModelDelegate userModel) {
String username = userModel.getUsername();
if (isNotBlank(username)) {
session.setAttribute(usernameKey(username), userModel);
}
String email = userModel.getEmail();
if (isNotBlank(email)) {
session.setAttribute(emailKey(email), userModel);
}
String id = userModel.getId();
if (isNotBlank(id)) {
session.setAttribute(idKey(id), userModel);
}
}

public void removeFromSession(HTTPUserModelDelegate userModel) {
String username = userModel.getUsername();
if (isNotBlank(username)) {
session.removeAttribute(usernameKey(username));
}
String email = userModel.getEmail();
if (isNotBlank(email)) {
session.removeAttribute(emailKey(email));
}
String id = userModel.getId();
if (isNotBlank(id)) {
session.removeAttribute(idKey(id));
}
}

}
62 changes: 62 additions & 0 deletions src/main/java/http/keycloak/userstorage/HTTPConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package http.keycloak.userstorage;

import org.keycloak.common.util.MultivaluedHashMap;

/**
* A model for all MACHWeb specific configurations
*/
public class HTTPConfig {
private final MultivaluedHashMap<String, String> config;

public HTTPConfig(MultivaluedHashMap<String, String> config) {
this.config = config;
}

public String getUrl() {
return config.getFirst(HTTPConstants.CONFIG_URL);
}

public String getUsername() {
return config.getFirst(HTTPConstants.CONFIG_USERNAME);
}

public String getPassword() {
return config.getFirst(HTTPConstants.CONFIG_PASSWORD);
}

public boolean isPagination() {
// for later - can be configurable
return false;
}

public int getBatchSizeForSync() {
// for later - can be configurable, if isPagination is true
return 100;
}

@Override
public boolean equals(Object obj) {
if (obj == this)
return true;
if (!(obj instanceof HTTPConfig))
return false;

HTTPConfig that = (HTTPConfig) obj;

if (!config.equals(that.config))
return false;
return true;
}

@Override
public int hashCode() {
return config.hashCode() * 13;
}

@Override
public String toString() {
MultivaluedHashMap<String, String> copy = new MultivaluedHashMap<String, String>(config);
copy.remove(HTTPConstants.CONFIG_PASSWORD);
return new StringBuilder(copy.toString()).toString();
}
}
Loading

0 comments on commit af6da95

Please sign in to comment.