1. Introduction
Hello and welcome to the second article in a series showing how to secure Spring Boot applications with Keycloak. In the previous tutorial, we’ve learned how to set up the Keycloak server and secure Spring Boot REST API endpoints with it. I highly encourage you to check it out before starting this guide.
In today’s article, I would like to show you how to set up the Keycloak admin client with Spring Boot and Kotlin. I will walk you step by step through the process of setting roles, groups, and users with the Keycloak Admin REST Client.
2. Run Keycloak Server
Just like in the previous article, we will bootstrap the Keycloak server using the docker run command:
docker run -p 8090:8080 -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin quay.io/keycloak/keycloak:11.0.3
After the server is ready, let’s head to the admin console under the http://localhost:8090/auth/admin address and create a new realm called example (I have described it thoroughly in the 3rd paragraph of the previous post).
2. Create and Configure Admin Client
Make a real progress thanks to practical examples, exercises, and quizzes.
- 64 written lessons
- 62 quizzes with a total of 269 questions
- Dedicated Facebook Support Group
- 30-DAYS Refund Guarantee
2.1. Create New Client
As the next step, let’s create the admin client within our realm. Let’s hit the Create button on the Clients page and name and set up a new client called admin-spring-boot:
2.2. Configure Access Type
On the next page, let’s configure our client as follows:
Please notice two things here. Confidential access-type indicates, that we will need a secret to authenticate this client against the Keycloak server. On the other hand, the service accounts enabled set to ON allows us to generate a token dedicated to this client.
2.3. Obtain Client Credentials
Nextly, let’s go to the Credentials tab and copy the secret:
The above value will be used later in our Spring Boot application to set up the connection.
2.4. Assign Roles to Service Account
The last step, we will need to do in Keycloak is to assign the appropriate roles to the service account associated with our client. Let’s switch to the Service Account Roles panel and add the following roles from the realm-management set:
Please keep in mind, that I have chosen these roles for demonstrational purposes. In real-life scenarios, we should always follow the Least Privilege rule.
3. Set Up Spring Boot Application
With all the above being done, we can start creating our Spring Boot project. In this paragraph, we will see how to set it up from scratch and interact with the Keycloak service using the admin client library.
3.1. Imports
As always, let’s start with the imports. Let’s head to the build.gradle.kts and add the following lines:
implementation("org.keycloak:keycloak-spring-boot-starter:11.0.3") implementation("org.keycloak:keycloak-admin-client:11.0.3") implementation("org.springframework.boot:spring-boot-starter-web")
Technically, we do not need the web starter to interact with Keycloak. However, in this guide, we will create a few endpoints to better illustrate the fully-working system.
3.2. Configure Application Properties
As the next step, let’s go to the application.yaml file and put the following configuration there:
keycloak: realm: example resource: admin-spring-boot auth-server-url: http://localhost:8090/auth credentials: secret: <KEYCLOAK-CLIENT-CREDENTIALS>
The secret should be set with the value from paragraph 2.3.
3.3. Create Keycloak Client Config
Nextly, let’s create the KeycloakClientConfig class:
@Configuration class KeycloakClientConfig( @Value("\${keycloak.credentials.secret}") private val secretKey: String, @Value("\${keycloak.resource}") private val clientId: String, @Value("\${keycloak.auth-server-url}") private val authUrl: String, @Value("\${keycloak.realm}") private val realm: String ) { @Bean fun keycloak(): Keycloak { return KeycloakBuilder.builder() .grantType(CLIENT_CREDENTIALS) .serverUrl(authUrl) .realm(realm) .clientId(clientId) .clientSecret(secretKey) .build() } }
In this configuration, we use the KeycloakBuilder class to customize the RESTEasy client used to communicate with the Keycloak server. Additionally, we annotate the keycloak() function with @Bean annotation, so that we will be able to inject this bean into our services later.
Thanks to the @Value annotation, the content of the application.yaml file is injected into our properties
3.4. Manage Roles
This time, instead of defining services first, and then implementing the controllers’ layer and testing, we will focus on Keycloak groups, roles, and users separately.
As the first step, let’s create the RoleService class with three functions:
@Service class RoleService( private val keycloak: Keycloak, @Value("\${keycloak.realm}") private val realm: String ) { fun create(name: String) { val role = RoleRepresentation() role.name = name keycloak .realm(realm) .roles() .create(role) } fun findAll(): List<RoleRepresentation> = keycloak .realm(realm) .roles() .list() fun findByName(roleName: String): RoleRepresentation = keycloak .realm(realm) .roles() .get(roleName) .toRepresentation() }
As you might have noticed- the first function will create a new role within the Keycloak instance. The second one will be used to obtain already created roles. On the other hand, the third one will be used by the UserController later, to obtain the RoleRepresentation.
Let’s create a controller class, which will utilize these functions:
@RestController @RequestMapping("/api/role") class RoleController( private val roleService: RoleService ) { @GetMapping fun findAll() = roleService.findAll() @PostMapping fun createRole(@RequestParam name: String) = roleService.create(name) }
Finally, let’s build and run the application. To test if everything is working as expected, let’s use the cURL command to create a new role:
curl --location --request POST 'localhost:8080/api/role?name=ROLE_EXAMPLE'
To verify, we can either utilize the second endpoint:
curl 'localhost:8080/api/role'
Or use the Keycloak Admin Console:
3.5. Manage Groups
Similarly, let’s implement the logic responsible for Keycloak group management. Just like in the previous paragraph, let’s start with the service layer:
@Service class GroupService( private val keycloak: Keycloak, @Value("\${keycloak.realm}") private val realm: String ) { fun create(name: String) { val group = GroupRepresentation() group.name = name keycloak .realm(realm) .groups() .add(group) } fun findAll(): List<GroupRepresentation> = keycloak .realm(realm) .groups() .groups() }
This service looks almost the same, as the RoleService. You might have also noticed that in the second function, we’ve used groups() method two times. It’s not a typo- the creators decided to use this name in GroupsResource class instead of the list() (unfortunately).
We might want to test the group’s functionality as well. Let’s implement the GroupController:
@RestController @RequestMapping("/api/group") class GroupController( private val groupService: GroupService ) { @GetMapping fun findAll() = groupService.findAll() @PostMapping fun createGroup(@RequestParam name: String) = groupService.create(name) }
Similarly, after the following command:
curl --location --request POST 'localhost:8080/api/group?name=example_group'
We should see the newly created group:
curl 'localhost:8080/api/group'
3.6. Manage Users
As the last example, let’s create the logic responsible for user management. Let’s start by adding the UserRequest class to the project:
class UserRequest( val username: String, val password: String )
This simple class will be used to create new users later.
With that being done, let’s create the UserService class:
@Service class UserService( private val keycloak: Keycloak, @Value("\${keycloak.realm}") private val realm: String ) { fun findAll(): List<UserRepresentation> = keycloak .realm(realm) .users() .list() fun findByUsername(username: String): List<UserRepresentation> = keycloak .realm(realm) .users() .search(username) fun findById(id: String): UserRepresentation = keycloak .realm(realm) .users() .get(id) .toRepresentation() fun assignToGroup(userId: String, groupId: String) { keycloak .realm(realm) .users() .get(userId) .joinGroup(groupId) } fun assignRole(userId: String, roleRepresentation: RoleRepresentation) { keycloak .realm(realm) .users() .get(userId) .roles() .realmLevel() .add(listOf(roleRepresentation)) } fun create(request: UserRequest): Response { val password = preparePasswordRepresentation(request.password) val user = prepareUserRepresentation(request, password) return keycloak .realm(realm) .users() .create(user) } private fun preparePasswordRepresentation( password: String ): CredentialRepresentation { val cR = CredentialRepresentation() cR.isTemporary = false cR.type = CredentialRepresentation.PASSWORD cR.value = password return cR } private fun prepareUserRepresentation( request: UserRequest, cR: CredentialRepresentation ): UserRepresentation { val newUser = UserRepresentation() newUser.username = request.username newUser.credentials = listOf(cR) newUser.isEnabled = true return newUser } }
The responsibility of the three first functions is pretty simple- they allow us to find all users, or alternatively to fetch specific ones based on the username or the identifier.
Let’s analyze the logic responsible for users creation:
fun create(request: UserRequest): Response { val password = preparePasswordRepresentation(request.password) val user = prepareUserRepresentation(request, password) return keycloak .realm(realm) .users() .create(user) } private fun preparePasswordRepresentation( password: String ): CredentialRepresentation { val cR = CredentialRepresentation() cR.isTemporary = false cR.type = CredentialRepresentation.PASSWORD cR.value = password return cR } private fun prepareUserRepresentation( request: UserRequest, cR: CredentialRepresentation ): UserRepresentation { val newUser = UserRepresentation() newUser.username = request.username newUser.credentials = listOf(cR) newUser.isEnabled = true return newUser }
The public function create() takes the userRequest as a parameter and prepares CredentialRepresentation first- setting the password value and making it non-temporary. Nextly, this password representation is passed to the prepareUserRepresentation() function and assigned to the UserRepresentation. In this function, the desired user is annotated as enabled as well.
The UserService class contains two additional functions as well: assignToGroup and assignRole, which might be used to add groups/roles to the user.
Finally, let’s create the UserController function:
@RestController @RequestMapping("/api/user") class UserController( private val userService: UserService, private val roleService: RoleService ) { @GetMapping fun findAll() = userService.findAll() @GetMapping("/{id}") fun findById(@PathVariable id: String) = userService.findById(id) @GetMapping("/username/{username}") fun findByUsername(@PathVariable username: String) = userService.findByUsername(username) @PostMapping fun create(@RequestBody userRequest: UserRequest): ResponseEntity<URI> { val response = userService.create(userRequest) if (response.status != 201) throw RuntimeException("User was not created") return ResponseEntity.created(response.location).build() } @PostMapping("/{userId}/group/{groupId}") fun assignToGroup( @PathVariable userId: String, @PathVariable groupId: String ) { userService.assignToGroup(userId, groupId) } @PostMapping("/{userId}/role/{roleName}") fun assignRole( @PathVariable userId: String, @PathVariable roleName: String ) { val role = roleService.findByName(roleName) userService.assignRole(userId, role) } }
In order to test if everything is good, let’s start by creating a new user:
curl -i --location --request POST 'localhost:8080/api/user' \ --header 'Content-Type: application/json' \ --data-raw '{ "username": "user", "password": "password" }'
Please notice one, very important thing. When the Keycloak user is created successfully, the response body will be empty. If we would like to obtain the user ID, for instance, to save it in our database, we would need to extract it from the Location header of the response (that’s why we’ve used the -i flag in our command):
Location: http://localhost:8090/auth/admin/realms/example/users/[CREATED-USER-ID]
Additionally, we can check the Users page in the Keycloak Admin Console:
From now on, we should be able to sign in using the provided username and password combination. Nevertheless, we would have to set up a new client for this purpose (I’ve described this process in this tutorial).
Following, let’s add our user to the group:
curl --location --request POST 'localhost:8080/api/user/35520db6-3df0-4693-b8b1-f9abc09d7b86/group/7eb8db7a-7693-4df3-b682-d955da67d453'
After the operation finishes, we should see the following in the user Groups tab:
As the last step, let’s assign the ROLE_EXAMPLE to our user:
curl --location --request POST 'localhost:8080/api/user/35520db6-3df0-4693-b8b1-f9abc09d7b86/role/ROLE_EXAMPLE'
Similarly, when we head to the Role Mappings tab, we should see that the role has been assigned successfully:
4. Summary
And that would be all for this article. We’ve learned today, how to set up the Keycloak Admin Client with Spring Boot and Kotlin. I hope you really liked the two most recent articles related to this topic. If you would like me to cover some special aspects of the Keycloak/Spring Boot integration, please let me know in the comments, or by the contact form.
Also, for the source code, please refer to this GitHub repository.
14 Responses
Hi Piotr,
Well written and explained. I got a success creating a group. However, the call to create a User or Role (returned 400 Bad Request) is failing for me. Trying to figureout the issues on those.
In addition, could you demo a use case where i could allow/deny a resource or restrict an operation (read/update) based on:
1. Group
2. Policy/Permission
Thanks!
I have encountered the error also, and I don’t how to solve it
Hello! 🙂
I’m afraid that it might be a small mistake in your configuration (sometimes happens, even though we try to do everything letter by letter). I’ve just cloned the repository and set it up and both role and user was created successfully. (please remember about creating realm called example- chapter 2). It’s really hard to help what might be the cause without seeing the code
When it comes to creating the continuation with groups and policy/permissions- I don’t want to promise anything, because I got plenty of ongoing work that needs to be finished (and also I’ve used a Keycloak over the year ago last time).
Nevertheless, policy should (not sure) work out of the box with ‘hasPermission’ (similarly, like I do in the second tutorial with securing endpoints by ‘hasRole’)
Awesome! Thanks for this post, just sharing my experience, i’ve a conflict with eureka cilent dependecy, to fix that just add an exclusion in eureka depencency, in my case my packing is maven, and i’m using pom.xml, see the snippet below:
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
javax.ws.rs
jsr311-api
You have defined Keycloak class as a Bean in Spring Configuration. How is token expiration taken cared of? Does the client re-authenticate if token has expired?
Hello Markus!
That’s a really good question.
Basically, we’ve set our client as a confidential, which is used: “For server-side clients that perform browser logins and require client secrets when making an Access Token Request.” (for reference, please check https://www.keycloak.org/docs/latest/server_admin/#assembly-managing-clients_server_administration_guide and https://www.keycloak.org/docs/latest/server_admin/#_client-credentials)
Additionally, “By default, client credentials are represented by the clientId and clientSecret of the client in the Authorization: Basic header”
(ref. https://www.keycloak.org/docs/latest/server_admin/#_service_accounts)
So, to put together these information- our client authenticates with Keycloak based on clientId and clientSecret (so there is no expiration concept here).
Please let me know if this helped you and thanks for your comment 🙂
HI Piotr,
Thanks for you article, it helps a lot. Here I have encounter a problem, I read users from keycloak successfully, but I can not update user’s atrributes succesfully. Following is my java code :
@Override
public void updateWecomUserId(String id, String wecomUserId) {
UserResource userResource = keycloak.realm(realm).users().get(id);
UserRepresentation userPresentation = userResource.toRepresentation();
Predicate<Map<String, List>> prdc = p -> p == null;
if (prdc.test(userPresentation.getAttributes())) {
userPresentation.setAttributes(new HashMap());
}
userPresentation.setLastName(“===test===”);
userPresentation.getAttributes().put(WECOM_USER_ID,
Collections.singletonList(wecomUserId));
// for the update , it throws exception:javax.ws.rs.BadRequestException: HTTP 400 Bad Request
userResource.update(userPresentation);
}
Do you know what mistake I have made ? Thanks a lot.
Hello!
Let me get back to you with the answer during the weekend 🙂
OK, hello once again 🙂
I’ve just set up Keycloak from scratch:
1. Cloned the repository
2. Set Up config (step 2 + creating a realm called example from the link I’ve provided)
3. Update application.yaml and put credentials.secret with appropriate value
4. Added a bit modified code (I used Kotlin)
And this seems to be working fine, because when I update and then get user details by ID I receive:
“attributes”: {
“WECOM_USER_ID”: [
“myTestId”
]
}
I’ve uploaded my example as this commit: https://github.com/codersee-blog/kotlin-spring-boot-keycloak-admin/commit/9c4d593652b39de3c2482f5d1707928be14e1903
Let me know if this is still the issue
For me it’s the same. I update my user but nothing changes on my keycloak server. However, if I send a second update request, the user appears as updated as my first request. I have tried several pieces of code, but nothing works. I’m using the version 17.0.1 for spring boot.
@Override
public void updateUser(User user) {
UsersResource instance = getInstance().realm(realm).users();
UserResource userResource = instance.get(user.getSecurityProviderId());
UserRepresentation userRepresentation = userResource.toRepresentation();
userRepresentation.setFirstName(user.getName());
userRepresentation.setLastName(user.getSurname());
userRepresentation.setEmail(user.getEmail());
userResource.update(userRepresentation);
}
Hi Pedro
Could you please upload the code to GitHub? This way I will be able to help
Hi Piotr, thank you very much for this tutorial! I need to allow users to change their passwords and emails directly from my application(SpringBoot) . Is this possible with keycloak-admin-client and if yes, could you please show how? Thanks in advance, Alex
Hi man!
I haven’t verified this approach, but looks that the API exposes an endpoint, which you might be interested in:
PUT /{realm}/users/{id}/reset-password (link to the docs: https://www.keycloak.org/docs-api/15.0/rest-api/)
The example flow would look, as follow:
On your side:
1. Create table, for example “reset_tokens” with “token”, “email”, “expires_at” columns.
2. Expose new the endpoint, which user will be using to trigger the flow (he will have to send e-mail in the request body)
3. When user hits this endpoint, you should generate a new entry in the table. Check if the email exists in the system. If yest, then for the “token”, generate some random value and send this to the email provided by the user. (in real-life, this is done by creating a link with this token appended in the end so that the front-end automatically capture that and send to the server)
4. Create second endpoint. This will take two values in the request body: token and new password as text. When user hits this endpoint, then you check the table by the given token value (and also expires_at < now() ). If such a value is found, then you query the Keycloak endpoint.
Hi Piotr,
first of all thanks for the clear guide, very helpful.
I made it work on my end with the keycloak version 22.0.3. I noticed a strange issue, when I did bulk user creation (loop through 50 users and call single user creation one by one), the first ~10 is created successfully (including extracting the newly created ids), but then it throws this exception:
Caused by: jakarta.ws.rs.WebApplicationException: Create method returned status Bad Request (Code: 400); expected status: Created (201)
. This is thrown when I try to extract the newly created user id from the header (after ~10 successfully created user and extracted ids) .
Do you maybe know why this issue appears?
Best,
Karo