fbpx

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

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.

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.

Share on facebook
Share on twitter
Share on linkedin

1 thought on “How to Set Up Keycloak Admin Client with Spring Boot and Kotlin?”

  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!

Leave a Comment

Your email address will not be published. Required fields are marked *

Subscribe to our Newsletter

Join the community and get 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

Find us also on...

Join the FREE weekly newsletter and get two free eBooks as well:

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.