Codersee

How to Set Up Keycloak Admin Client with Spring Boot and Kotlin?

A featured image for category: Spring

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

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:

Image is a screenshot showing how to create an admin client inside the Keycloak Admin Console.

2.2. Configure Access Type

On the next page, let’s configure our client as follows:

The image shows a screenshot from Keycloak Admin Console showing how to set Access Type for new client.

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:

Image contains a screenshot from Keycloak client app showing how to get credentials for Keycloak client.
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:

The image is a screenshot from Keycloak panel showing how to assign roles to the service account

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.

 

Image shows two ebooks people can get for free after joining newsletter

 

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:
The image shows the view of all roles in Keycloak from 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'

Image shows the screenshot from Keycloak Admin Console showing created groups.

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:

The image shows the Users page inside 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:
the image shows the view of user groups.

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:
The image shows user assigned roles in Keycloak Admin Console.

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.

13 Responses

  1. 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!

    1. 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’)

  2. 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

  3. 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?

    1. 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 🙂

  4. 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.

    1. 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

    2. 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);
      }

  5. 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

    1. 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.

Leave a Reply

Your email address will not be published.

Categories

Author

Hi there! 👋

Hi there! 👋

My name is Piotr and I've created Codersee to share my knowledge about Kotlin, Spring Framework, and other related topics through practical, step-by-step guides. Always eager to chat and exchange knowledge.

Join the FREE weekly newsletter and get two free eBooks:

Image shows the covers of free ebooks accessible for newsletter subscribers.

You may opt out any time. Terms of Use and Privacy Policy.

To make Codersee work, we log user data. By using our site, you agree to our Privacy Policy and Terms of Use.