diff --git a/spring-boot-modules/spring-boot-keycloak-2/pom.xml b/spring-boot-modules/spring-boot-keycloak-2/pom.xml
index 8b1eec2e4e..572986e8c4 100644
--- a/spring-boot-modules/spring-boot-keycloak-2/pom.xml
+++ b/spring-boot-modules/spring-boot-keycloak-2/pom.xml
@@ -16,6 +16,10 @@
0.0.1-SNAPSHOT
../../parent-boot-2
+
+
+ 21.0.1
+
@@ -39,6 +43,41 @@
spring-boot-starter-test
test
+
+
+
+ org.keycloak
+ keycloak-admin-client
+ ${keycloak.version}
+
+
+
+ org.keycloak
+ keycloak-core
+ provided
+ ${keycloak.version}
+
+
+ org.keycloak
+ keycloak-server-spi
+ ${keycloak.version}
+ provided
+
+
+
+ org.keycloak
+ keycloak-server-spi-private
+ provided
+ ${keycloak.version}
+
+
+ org.keycloak
+ keycloak-services
+ provided
+ ${keycloak.version}
+
+
+
diff --git a/spring-boot-modules/spring-boot-keycloak-2/src/main/java/com/baeldung/keycloak/adminclient/AdminClientService.java b/spring-boot-modules/spring-boot-keycloak-2/src/main/java/com/baeldung/keycloak/adminclient/AdminClientService.java
new file mode 100644
index 0000000000..bacca9bdea
--- /dev/null
+++ b/spring-boot-modules/spring-boot-keycloak-2/src/main/java/com/baeldung/keycloak/adminclient/AdminClientService.java
@@ -0,0 +1,90 @@
+package com.baeldung.keycloak.adminclient;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import javax.annotation.PostConstruct;
+
+import org.keycloak.admin.client.Keycloak;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+public class AdminClientService {
+
+ private static final Logger logger = LoggerFactory.getLogger(AdminClientService.class);
+ private static final String REALM_NAME = "master";
+
+ @Autowired
+ private Keycloak keycloak;
+
+ @PostConstruct
+ public void searchUsers() {
+ logger.info("Searching users in Keycloak {}", keycloak.serverInfo()
+ .getInfo()
+ .getSystemInfo()
+ .getVersion());
+ searchByUsername("user1", true);
+ searchByUsername("user", false);
+ searchByUsername("1", false);
+ searchByEmail("user2@test.com", true);
+ searchByAttributes("DOB:2000-01-05");
+ searchByGroup("c67643fb-514e-488a-a4b4-5c0bdf2e7477");
+ searchByRole("user");
+ }
+
+ private void searchByUsername(String username, boolean exact) {
+ logger.info("Searching by username: {} (exact {})", username, exact);
+ List users = keycloak.realm(REALM_NAME)
+ .users()
+ .searchByUsername(username, exact);
+ logger.info("Users found by username {}", users.stream()
+ .map(user -> user.getUsername())
+ .collect(Collectors.toList()));
+ }
+
+ private void searchByEmail(String email, boolean exact) {
+ logger.info("Searching by email: {} (exact {})", email, exact);
+ List users = keycloak.realm(REALM_NAME)
+ .users()
+ .searchByEmail(email, exact);
+ logger.info("Users found by email {}", users.stream()
+ .map(user -> user.getEmail())
+ .collect(Collectors.toList()));
+ }
+
+ private void searchByAttributes(String query) {
+ logger.info("Searching by attributes: {}", query);
+ List 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()));
+ }
+
+ private void searchByGroup(String groupId) {
+ logger.info("Searching by group: {}", groupId);
+ List users = keycloak.realm(REALM_NAME)
+ .groups()
+ .group(groupId)
+ .members();
+ logger.info("Users found by group {}", users.stream()
+ .map(user -> user.getUsername())
+ .collect(Collectors.toList()));
+ }
+
+ private void searchByRole(String roleName) {
+ logger.info("Searching by role: {}", roleName);
+ List users = keycloak.realm(REALM_NAME)
+ .roles()
+ .get(roleName)
+ .getUserMembers();
+ logger.info("Users found by role {}", users.stream()
+ .map(user -> user.getUsername())
+ .collect(Collectors.toList()));
+ }
+}
\ No newline at end of file
diff --git a/spring-boot-modules/spring-boot-keycloak-2/src/main/java/com/baeldung/keycloak/adminclient/App.java b/spring-boot-modules/spring-boot-keycloak-2/src/main/java/com/baeldung/keycloak/adminclient/App.java
new file mode 100644
index 0000000000..a863bde2e3
--- /dev/null
+++ b/spring-boot-modules/spring-boot-keycloak-2/src/main/java/com/baeldung/keycloak/adminclient/App.java
@@ -0,0 +1,30 @@
+package com.baeldung.keycloak.adminclient;
+
+import org.keycloak.OAuth2Constants;
+import org.keycloak.admin.client.Keycloak;
+import org.keycloak.admin.client.KeycloakBuilder;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.PropertySource;
+
+@SpringBootApplication(scanBasePackages = { "com.baeldung.keycloak.adminclient" })
+@PropertySource("classpath:application-adminclient.properties")
+public class App {
+
+ public static void main(String[] args) {
+ SpringApplication.run(App.class, args);
+ }
+
+ @Bean
+ Keycloak keycloak() {
+ return KeycloakBuilder.builder()
+ .serverUrl("http://localhost:8080")
+ .realm("master")
+ .clientId("admin-cli")
+ .grantType(OAuth2Constants.PASSWORD)
+ .username("admin")
+ .password("password")
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/spring-boot-modules/spring-boot-keycloak-2/src/main/java/com/baeldung/keycloak/customendpoint/KeycloakUserApiProvider.java b/spring-boot-modules/spring-boot-keycloak-2/src/main/java/com/baeldung/keycloak/customendpoint/KeycloakUserApiProvider.java
new file mode 100644
index 0000000000..c34514104b
--- /dev/null
+++ b/spring-boot-modules/spring-boot-keycloak-2/src/main/java/com/baeldung/keycloak/customendpoint/KeycloakUserApiProvider.java
@@ -0,0 +1,52 @@
+package com.baeldung.keycloak.customendpoint;
+
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.ws.rs.GET;
+import javax.ws.rs.NotFoundException;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
+
+import org.keycloak.models.GroupModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.utils.ModelToRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.services.resource.RealmResourceProvider;
+
+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 searchUsersByGroupAndRoleName(@QueryParam("groupName") @NotNull String groupName, @QueryParam("roleName") @NotBlank String roleName) {
+ RealmModel realm = session.getContext().getRealm();
+
+ Optional 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));
+ }
+}
\ No newline at end of file
diff --git a/spring-boot-modules/spring-boot-keycloak-2/src/main/java/com/baeldung/keycloak/customendpoint/KeycloakUserApiProviderFactory.java b/spring-boot-modules/spring-boot-keycloak-2/src/main/java/com/baeldung/keycloak/customendpoint/KeycloakUserApiProviderFactory.java
new file mode 100644
index 0000000000..34eda94d6a
--- /dev/null
+++ b/spring-boot-modules/spring-boot-keycloak-2/src/main/java/com/baeldung/keycloak/customendpoint/KeycloakUserApiProviderFactory.java
@@ -0,0 +1,33 @@
+package com.baeldung.keycloak.customendpoint;
+
+import org.keycloak.Config.Scope;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.services.resource.RealmResourceProvider;
+import org.keycloak.services.resource.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;
+ }
+}
\ No newline at end of file
diff --git a/spring-boot-modules/spring-boot-keycloak-2/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory b/spring-boot-modules/spring-boot-keycloak-2/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory
new file mode 100644
index 0000000000..f11a60fd2b
--- /dev/null
+++ b/spring-boot-modules/spring-boot-keycloak-2/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory
@@ -0,0 +1 @@
+com.baeldung.keycloak.customendpoint.KeycloakUserApiProviderFactory
\ No newline at end of file
diff --git a/spring-boot-modules/spring-boot-keycloak-2/src/main/resources/application-adminclient.properties b/spring-boot-modules/spring-boot-keycloak-2/src/main/resources/application-adminclient.properties
new file mode 100644
index 0000000000..bafddced85
--- /dev/null
+++ b/spring-boot-modules/spring-boot-keycloak-2/src/main/resources/application-adminclient.properties
@@ -0,0 +1 @@
+server.port=8081
\ No newline at end of file