DEV Community

Mohammad Abu Musa RABIUL
Mohammad Abu Musa RABIUL

Posted on

13 3

Keycloak Custom Rest Api (Search by user attribute - Keycloak)

In this project we are going to develop and integrate a custom rest api in keycloak server.

About Keycloak

Keycloak is an open source Identity and Access Management solution aimed at modern applications and services. It makes it easy to secure applications and services with little to no code. It provides with following features,

  • Single-Sign On
  • Identity Brokering and Social Login
  • User Federation
  • Client Adapters
  • Admin Console
  • Account Management Console
  • Standard Protocols: OpenID Connect, OAuth 2.0, and SAML.

Keycloak setup

We are going to use docker container to run Keycloak along with postgreSQL. We have integrated PostgreSQL with Keycloak inside docker-compose.yml file.

version: '3'
# from: https://github.com/keycloak/keycloak-containers/blob/master/docker-compose-examples/keycloak-postgres.yml
volumes:
postgres_data:
driver: local
services:
postgres:
image: postgres
container_name: postgres
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: password
pgadmin:
container_name: pgadmin
image: dpage/pgadmin4
environment:
PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-pgadmin4@pgadmin.org}
PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-admin}
ports:
- "5050:80"
restart: unless-stopped
keycloak:
image: jboss/keycloak:7.0.1
container_name: keycloak
environment:
DB_VENDOR: POSTGRES
DB_ADDR: postgres
DB_DATABASE: keycloak
DB_USER: keycloak
DB_SCHEMA: public
DB_PASSWORD: password
KEYCLOAK_USER: admin
KEYCLOAK_PASSWORD: Pa55w0rd
ports:
- 8090:8080
depends_on:
- postgres

To setup the docker containers, run the following command.


docker-compose -f ./docker-compose.yml up -d


Enter fullscreen mode Exit fullscreen mode

Now, your keycloak should run locally on 8090 port.

Keycloak setup and adding user

URL for keycloak http://localhost:8090/

Note: By default keycloak uses Master realm. As you can see we added user in Demo realm. In order to create new realm, do as follow
image

image
Now lets create a maven project. I will use VS Code as IDE for this project.However, you can use any IDE of your choice. You can check this link to configure VS Code for Java development.

Project Structure

image

pom.xml file contains all the required dependencies. You can copy the dependencies in your project.

<?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>org.example</groupId>
<artifactId>keycloak-api-extensions</artifactId>
<version>1.0-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
<packaging>jar</packaging>
<properties>
<keycloak.version>12.0.4</keycloak.version>
<maven.compiler.source>15</maven.compiler.source>
<maven.compiler.target>15</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.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>
<version>${keycloak.version}</version>
<scope>provided</scope>
</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-services</artifactId>
<scope>provided</scope>
<version>${keycloak.version}</version>
</dependency>
</dependencies>
</project>

Create a model user class UserDto.java under models folder.
package keycloak.apiextension.models;
public class UserDto {
private String userName;
private String firstName;
private String lastName;
private String id;
private String email;
private String merchantId;
public UserDto(String userName, String firstName, String lastName, String id, String email, String merchantId) {
this.userName = userName;
this.firstName = firstName;
this.lastName = lastName;
this.id = id;
this.email = email;
this.merchantId = merchantId;
}
public String getUserName() {
return this.userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getFirstName() {
return this.firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return this.lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getId() {
return this.id;
}
public void setId(String id) {
this.id = id;
}
public String getEmail() {
return this.email;
}
public void setEmail(String email) {
this.email = email;
}
public String getMarchentId() {
return this.merchantId;
}
public void setMarchentId(String marchentId) {
this.merchantId = marchentId;
}
}
view raw UserDto.java hosted with ❤ by GitHub

Create a mapper class that will map keycloak's UserModel class object to UserDto class object. Use import org.keycloak.models.UserModel; to import UserModel class.

public class UserMapper {
public UserDto mapToUserDto(UserModel um) {
return new UserDto(
um.getUsername(),
um.getFirstName(),
um.getLastName(),
um.getId(),
um.getEmail(),
um.getAttribute("merchant_id").get(0)
);
}
}
view raw UserMapper.java hosted with ❤ by GitHub

Keycloak provides RealmResourceProvider and RealmResourceProviderFactory interfaces that are used to implement custom rest api.
First we create KeyCloakUserApiProvider class that implements RealmResourceProvider interface. We will then define our custome api method named searchUsersByAttribute.


    @GET
    @Path("users/search-by-attr")
    @NoCache
    @Produces({ MediaType.APPLICATION_JSON })
    @Encoded
    public List<UserDto> searchUsersByAttribute(@DefaultValue(defaultAttr) @QueryParam("attr") String attr,
            @QueryParam("value") String value) {
        return session.users().searchForUserByUserAttribute(attr, value, session.getContext().getRealm())
                .stream().map(e -> userMapper.mapToUserDto(e)).collect(Collectors.toList());
    }


Enter fullscreen mode Exit fullscreen mode

The above method filters user list based on user attribute. Default filter attribute is merchant_id.

KeyCloakUserApiProvider class

Use import org.keycloak.services.resource.RealmResourceProvider; to import the interface.

public class KeyCloakUserApiProvider implements RealmResourceProvider {
private final KeycloakSession session;
private final String defaultAttr = "merchant_id";
private final UserMapper userMapper;
public KeyCloakUserApiProvider(KeycloakSession session) {
this.session = session;
this.userMapper = new UserMapper();
}
public void close() {
}
public Object getResource() {
return this;
}
@GET
@Path("users/search-by-attr")
@NoCache
@Produces({ MediaType.APPLICATION_JSON })
@Encoded
public List<UserDto> searchUsersByAttribute(@DefaultValue(defaultAttr) @QueryParam("attr") String attr,
@QueryParam("value") String value) {
return session.users().searchForUserByUserAttribute(attr, value, session.getContext().getRealm())
.stream().map(e -> userMapper.mapToUserDto(e)).collect(Collectors.toList());
}
}

Lets define KeyCloakUserApiProviderFactory class that implements RealmResourceProviderFactory.

KeyCloakUserApiProviderFactory class

public class KeyCloakUserApiProviderFactory implements RealmResourceProviderFactory {
public static final String ID = "userapi-rest";
public RealmResourceProvider create(KeycloakSession session) {
return new KeyCloakUserApiProvider(session);
}
public void init(Scope config) {
}
public void postInit(KeycloakSessionFactory factory) {
}
public void close() {
}
public String getId() {
return ID;
}
}

Note: Factory instance will remain through out the lifecycle of keycloak server but KeyCloakUserApiProvider instance will be created at run time.

Register the KeyCloakUserApiProviderFactory class to keycloak by creating org.keycloak.services.resource.RealmResourceProviderFactory file under src\main\resources\META-INF\services\ folder.
Next copy the KeyCloakUserApiProviderFactory class name including package information into that file. For an example,

keycloak.apiextension.KeyCloakUserApiProviderFactory

After that, build the maven project by running mvn clean install. This will generate a target folder. Under the target folder there will be {project artifact id}-*.jar file.

image

Copy that jar file to the Keycloak's standalone/deployments/ directory. For an example, If you run your Keycloak in docker container, you can use the following command:



docker cp <jar_file_path> keycloak:/opt/jboss/keycloak/standalone/deployments/


Enter fullscreen mode Exit fullscreen mode

Test our custom api

Get list of users with merchant_id 1



curl --location --request GET 'http://localhost:8090/auth/realms/demo/userapi-rest/users/search-by-attr?attr=merchant_id&value=1'


Enter fullscreen mode Exit fullscreen mode

image

You can find the project on this GitHub repository.

If you find this article useful, kindly give a start on GitHub.

Top comments (8)

Collapse
 
hendisantika profile image
Hendi Santika

I was modified the docker-compose file. But it didn't work. Do You have any suggestion? Thanks

version: '3.9'

from: github.com/keycloak/keycloak-conta...

volumes:
postgres_data:
driver: local

services:
postgres:
image: postgres
container_name: postgres
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: password
pgadmin:
container_name: pgadmin
image: dpage/pgadmin4
environment:
# PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-pgadmin4@pgadmin.org}
PGADMIN_DEFAULT_EMAIL: hendi@yopmail.com
# PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:password}
PGADMIN_DEFAULT_PASSWORD: password
ports:
- "5050:80"
restart: unless-stopped

keycloak:
image: jboss/keycloak:16.1.1
container_name: keycloak
environment:
DB_VENDOR: POSTGRES
DB_ADDR: postgres
DB_DATABASE: keycloak
DB_USER: keycloak
DB_SCHEMA: public
DB_PASSWORD: password
KEYCLOAK_USER: admin
KEYCLOAK_PASSWORD: Pa55w0rd
ports:
- 8090:8080
depends_on:
- postgres

Collapse
 
belfhi profile image
Johannes Reppin

Hi,
this sounds very interesting.
Does this only allow to search for users with the attribute "merchantId"
If I want to search for a different attribute I need to adjust the mapper, right?
Cheers.

Collapse
 
silentrobi profile image
Mohammad Abu Musa RABIUL

Yes, you have to adjust the mapper

Collapse
 
oaztech profile image
Oussama ANDALOUSSI

it is easy to use this featurs with spring boot ?

Collapse
 
arulrajnet profile image
arulraj.net

Hi

Thanks for this post.

How to add authentication for the custom REST API?

Collapse
 
arulrajnet profile image
arulraj.net
Collapse
 
ccamba profile image
ccamba • Edited

Hi Mohammad, excelent article!
It's possible to implement the search without exact values ? for example value = '12' returns '123', '12x', ....

Collapse
 
stoany09 profile image
Siyaphakama Sosibo

Is it possible to achieve this for Groups and Roles?

Search for Groups and Roles via custom attributes

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay