1. Overview
Keycloak is a third-party Identity and Access Management solution that can help us integrate authentication and authorization into our application.
In this tutorial, we’ll see a few examples of searching users in Keycloak.
2. Keycloak Configuration
First of all, we need to configure Keycloak. Let’s create an initial admin user named baeldung with the password secretPassword. Secondly, we need a realm to work on. Let’s use the master realm that already exists when we start Keycloak.
Finally, we need a client that we can use from our Spring Boot application. For this example, let’s use the admin-cli client that is created by default.
We need some users in the realm so that we can search for them later on. Let’s create a user with username “user1“, email “*[email protected]“, and name “First UserI*“. Now, we can repeat this pattern a few more times to have 10 users total.
3. Keycloak Admin Client
Keycloak has a REST API that can be used to access all of the features that are available on the Admin Console UI. We can use this with any client we’d like, but Keycloak provides a Java client that makes it easier. In our first examples, we’ll use this Java client from a Spring Boot application.
First of all, let’s add the keycloak-admin-client dependency to our pom.xml:
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-admin-client</artifactId>
<version>21.0.1</version>
</dependency>
It’s important to ensure that the version of the client matches the version of the server. Mismatched versions might not work properly.
Now, let’s configure the client in our application. To do so, we need to create a Keycloak bean:
@Bean
Keycloak keycloak() {
return KeycloakBuilder.builder()
.serverUrl("http://localhost:8080")
.realm("master")
.clientId("admin-cli")
.grantType(OAuth2Constants.PASSWORD)
.username("baeldung")
.password("secretPassword")
.build();
}
In this builder, we configure the server URL, set the previously mentioned realm and client names, and provide the correct credentials.
Let’s create a service that’ll use this bean to access our Keycloak server:
@Service
public class AdminClientService {
@Autowired
Keycloak keycloak;
@PostConstruct
void searchUsers() {
// ...
}
}
After this, let’s start our application. Now we can start searching for users.
3.1. Search by Username
The Java client proves a very convenient way to search users by username. First, we need to reference the realm we’d like to use, then access the users and use the searchByUsername() method. Let’s create a method that searches for users and logs the result:
private static final String REALM_NAME = "master";
void searchByUsername(String username, boolean exact) {
logger.info("Searching by username: {} (exact {})", username, exact);
List<UserRepresentation> users = keycloak.realm(REALM_NAME)
.users()
.searchByUsername(username, exact);
logger.info("Users found by username {}", users.stream()
.map(user -> user.getUsername())
.collect(Collectors.toList()));
}
Now, we can use it from our searchUsers() method with different parameters:
void searchUsers() {
searchByUsername("user1", true);
searchByUsername("user", false);
searchByUsername("1", false);
}
The first search looks for an exact match of the “user1” username. The second one doesn’t require an exact match, so it returns all users with a username that contains the word “user“. The third one is similar and looks for usernames that contain the number “1”. Since we have 10 users with usernames from user1 to user10, the logs contain the following results:
12:20:22.295 [main] INFO c.b.k.adminclient.AdminClientService - Searching users in Keycloak 21.0.1
12:20:22.296 [main] INFO c.b.k.adminclient.AdminClientService - Searching by username: user1 (exact true)
12:20:22.341 [main] INFO c.b.k.adminclient.AdminClientService - Users found by username [user1]
12:20:22.341 [main] INFO c.b.k.adminclient.AdminClientService - Searching by username: user (exact false)
12:20:22.357 [main] INFO c.b.k.adminclient.AdminClientService - Users found by username [user1, user10, user2, user3, user4, user5, user6, user7, user8, user9]
12:20:22.357 [main] INFO c.b.k.adminclient.AdminClientService - Searching by username: 1 (exact false)
12:20:22.369 [main] INFO c.b.k.adminclient.AdminClientService - Users found by username [user1, user10]
3.2. Search by Email
As we saw previously, it can be very easy to filter based on username. Fortunately, using email addresses as a filter is very similar to this. Let’s use the searchByEmail() method that accepts an email address and a boolean parameter for exact matches:
void searchByEmail(String email, boolean exact) {
logger.info("Searching by email: {} (exact {})", email, exact);
List<UserRepresentation> users = keycloak.realm(REALM_NAME)
.users()
.searchByEmail(email, exact);
logger.info("Users found by email {}", users.stream()
.map(user -> user.getEmail())
.collect(Collectors.toList()));
}
Let’s test it with the “*[email protected]*” address and look for an exact match. There is only one result this time:
12:24:16.130 [main] INFO c.b.k.adminclient.AdminClientService - Searching by email: [email protected] (exact true)
12:24:16.141 [main] INFO c.b.k.adminclient.AdminClientService - Users found by email [[email protected]]
3.3. Search by Custom Attributes
In Keycloak, users can have custom attributes as well, not only the simple username and email. Let’s add a custom attribute called DOB for date of birth to user1 with the value “2000-01-05”.
Now, we can use the searchByAttributes() method from the admin client:
void searchByAttributes(String query) {
logger.info("Searching by attributes: {}", query);
List<UserRepresentation> users = keycloak.realm(REALM_NAME)
.users()
.searchByAttributes(query);
logger.info("Users found by attributes {}", users.stream()
.map(user -> user.getUsername() + " " + user.getAttributes())
.collect(Collectors.toList()));
}
Let’s use the “DOB:2000-01-05” query to list the users with matching attributes:
13:19:51.091 [main] INFO c.b.k.adminclient.AdminClientService - Searching by attributes: DOB:2000-01-05
13:19:51.103 [main] INFO c.b.k.adminclient.AdminClientService - Users found by attributes [user1 {DOB=[2000-01-05]}]
3.4. Search by Group
Users can also belong to a group, and we can filter by this too. Let’s create a group named “Test Group” and add some members to it. Now, we can get the members of this group with the admin client:
void searchByGroup(String groupId) {
logger.info("Searching by group: {}", groupId);
List<UserRepresentation> users = keycloak.realm(REALM_NAME)
.groups()
.group(groupId)
.members();
logger.info("Users found by group {}", users.stream()
.map(user -> user.getUsername())
.collect(Collectors.toList()));
}
However, it’s important to notice that we used the group ID here and not the name. This way, we can list the members of a Keycloak group:
14:35:09.275 [main] INFO c.b.k.adminclient.AdminClientService - Searching by group: c67643fb-514e-488a-a4b4-5c0bdf2e7477
14:35:09.290 [main] INFO c.b.k.adminclient.AdminClientService - Users found by group [user1, user2, user3, user4, user5]
3.5. Search by Role
Searching by role is very similar to the previous method because we can list the users who have a specific role like we listed the members of a group. To do this, we need to assign a role to some of our users. Let’s create a role named “user” and assign it to “user1“. Now, we can implement the search functionality:
void searchByRole(String roleName) {
logger.info("Searching by role: {}", roleName);
List<UserRepresentation> users = keycloak.realm(REALM_NAME)
.roles()
.get(roleName)
.getUserMembers();
logger.info("Users found by role {}", users.stream()
.map(user -> user.getUsername())
.collect(Collectors.toList()));
}
Let’s see which users have this role:
12:03:23.788 [main] INFO c.b.k.adminclient.AdminClientService - Searching by role: user
12:03:23.802 [main] INFO c.b.k.adminclient.AdminClientService - Users found by role [user1]
4. Custom REST Endpoint
As we saw, Keycloak provides useful search functionalities by default, but in some cases, we might need something different or more complex. But there’s a solution to this, too. We can implement our own custom functionalities by adding a new API endpoint to Keycloak.
Let’s assume that we’d like to find users who are in a specific group and have a specific role as well. For example, we could find all the users who have the “project manager” role in the “software development department” group.
Firstly, we need a new class that extends from Keycloak’s RealmResourceProvider. Let’s implement our custom functionality here:
public class KeycloakUserApiProvider implements RealmResourceProvider {
private final KeycloakSession session;
public KeycloakUserApiProvider(KeycloakSession session) {
this.session = session;
}
public void close() {
}
public Object getResource() {
return this;
}
@GET
@Produces({ MediaType.APPLICATION_JSON })
public Stream<UserRepresentation> searchUsersByGroupAndRoleName(@QueryParam("groupName") @NotNull String groupName, @QueryParam("roleName") @NotBlank String roleName) {
RealmModel realm = session.getContext().getRealm();
Optional<GroupModel> groupByName = session.groups()
.getGroupsStream(realm)
.filter(group -> group.getName().equals(groupName))
.findAny();
GroupModel group = groupByName.orElseThrow(() -> new NotFoundException("Group not found with name " + groupName));
return session.users()
.getGroupMembersStream(realm, group)
.filter(user -> user.getRealmRoleMappingsStream().anyMatch(role -> role.getName().equals(roleName)))
.map(user -> ModelToRepresentation.toBriefRepresentation(user));
}
}
After this, we need to tell Keycloak to use this class by configuring a RealmResourceProviderFactory:
public class KeycloakUserApiProviderFactory implements RealmResourceProviderFactory {
public static final String ID = "users-by-group-and-role-name";
@Override
public RealmResourceProvider create(KeycloakSession session) {
return new KeycloakUserApiProvider(session);
}
@Override
public void init(Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return ID;
}
}
Finally, we need to register this class by creating a file in the META-INF folder. There should be only one line in this file that contains the qualified name of our class. Let’s create the src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory file so it contains the name of our class:
com.baeldung.keycloak.customendpoint.KeycloakUserApiProviderFactory
Let’s build the project and copy the jar to the providers folder in Keycloak and run the build command to update the server’s provider registry:
kc build
We get the following output which means our custom provider was recognized and registered:
Updating the configuration and installing your custom providers, if any. Please wait.
Server configuration updated and persisted. Run the following command to review the configuration:
kc.bat show-config
Let’s start the Keycloak instance again and access our custom API endpoint at http://localhost:8080/realms/master/users-by-group-and-role-name?groupName=Test%20Group&roleName=user.
This API endpoint successfully returns the user which satisfies both criteria, meaning that it belongs to the “Test Group” and has the role “user“:
[
{
"id": "2c59a20f-df38-4d14-8ff9-067ea30f7937",
"createdTimestamp": 1678099525313,
"username": "user1",
"enabled": true,
"emailVerified": true,
"firstName": "First",
"lastName": "User",
"email": "[email protected]"
}
]
5. Conclusion
In this article, we integrated our Spring Boot application with Keycloak using the Keycloak Admin Client to manage users. This offers an easy way to access the existing functionalities. Then, we created a custom REST endpoint to extend Keycloak with our custom feature that lets us implement any custom logic necessary.
As always, the source code for these examples is available over on GitHub.