Kotlin + Spring Boot + Keycloak - Securing REST APIs
Hello everyone, In this article, we will learn how to secure the Kotlin + Spring boot REST APIs with Keycloak.GitHub repository link is provided at the end of this tutorial.
The credentials tab will show the Client Secret which is required for the Spring Boot Application Keycloak configurations.
Technologies Used:
- Kotlin
- Spring Boot 2.5.5
- KeyCloak 15.0.2
- Gradle
Let's begin our journey,
Keycloak - Environment Setup
Let's download the Keycloak-15.0.2 Standalone server distribution from the official source.
Once we've downloaded the Standalone server distribution, we can unzip and start Keycloak from the terminal:
In Windows
unzip keycloak-12.0.1.zip
cd keycloak-12.0.1/bin/
standalone.bat
In Linux / Unix
$ sudo tar -xvzf keycloak-12.0.1.tar.gz
$ cd keycloak-12.0.1/bin/
$ ./standalone.sh
Create Keycloak Server Initial Admin
Go to http://localhost:8090/auth/ and fill the form in the Administrator Console part. For the purpose of this exercise, knowledgefactory/password will be enough.
Then you should be able to log in to Keycloak Admin Console http://localhost:8080/auth/admin.
And, enter your admin username and password
On successful login, you will be redirected to the Keycloak Admin console
Create New Realm
Create a new realm named knowledgefactory-realm
After creating a new realm, you will be taken to your newly created realm Admin console page, as shown in the image below:
Create Clients
Create a new client named knowledgefactory-client with confidential access type, set its Valid Redirect URIs to, and save your changes.
Create Realm Level Roles
Let’s create a role: knowledgefactory-admin by clicking Add Role button.
After Save, enabled Composite Roles
This is the basic setup of Keycloak for use with web applications.
Set up a Spring Boot application
Project Structure:
Project Dependency(build.gradle.kts)
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.springframework.boot") version "2.5.5"
id("io.spring.dependency-management") version "1.0.11.RELEASE"
kotlin("jvm") version "1.5.31"
kotlin("plugin.spring") version "1.5.31"
}
group = "com.knf.dev.demo"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_11
configurations {
compileOnly {
extendsFrom(configurations.annotationProcessor.get())
}
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("org.keycloak:keycloak-spring-boot-starter:15.0.2")
implementation("org.keycloak:keycloak-admin-client:15.0.2")
implementation(platform("org.keycloak.bom:keycloak-adapter-bom:15.0.2"))
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.security:spring-security-test")
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "11"
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
application.properties
server.port = 9080
keycloak.auth-server-url = http://localhost:8080/auth
keycloak.realm = knowledgefactory-realm
keycloak.resource = knowledgefactory-client
keycloak.public-client = true
keycloak.use-resource-role-mappings = true
keycloak.ssl-required = external
server.connection-timeout = 9000
keycloak.credentials.secret = 86996cf7-5030-4a37-948b-99223f8bdabf
Configure Keycloak(SecurityConfig.kt)
package com.knf.dev.demo.kotlinspringkeycloakdemo.config
import org.keycloak.adapters.KeycloakConfigResolver
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper
import org.springframework.security.core.session.SessionRegistryImpl
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(jsr250Enabled = true)
class SecurityConfig : KeycloakWebSecurityConfigurerAdapter() {
@Throws(Exception::class)
override fun configure(http: HttpSecurity) {
super.configure(http)
http.cors().and().csrf().disable().sessionManagement().
sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().authorizeRequests()
.antMatchers("/employees/unprotected").permitAll()
.antMatchers("/employees/create").permitAll()
.antMatchers("/employees/login").permitAll()
.anyRequest().authenticated()
}
@Autowired
@Throws(Exception::class)
fun configureGlobal(auth: AuthenticationManagerBuilder) {
val keycloakAuthenticationProvider = keycloakAuthenticationProvider()
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(SimpleAuthorityMapper())
auth.authenticationProvider(keycloakAuthenticationProvider)
}
@Bean
override fun sessionAuthenticationStrategy(): SessionAuthenticationStrategy {
return RegisterSessionAuthenticationStrategy(SessionRegistryImpl())
}
@Bean
fun KeycloakConfigResolver(): KeycloakConfigResolver {
return KeycloakSpringBootConfigResolver()
}
}
Service class(KeycloakService.kt)
package com.knf.dev.demo.kotlinspringkeycloakdemo.service
import com.knf.dev.demo.kotlinspringkeycloakdemo.Vo.EmployeeVO
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder
import org.keycloak.OAuth2Constants
import org.keycloak.admin.client.CreatedResponseUtil
import org.keycloak.admin.client.KeycloakBuilder
import org.keycloak.authorization.client.AuthzClient
import org.keycloak.authorization.client.Configuration
import org.keycloak.representations.idm.CredentialRepresentation
import org.keycloak.representations.idm.UserRepresentation
import org.springframework.beans.factory.annotation.Value
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Service
import java.util.*
@Service
class KeycloakService {
@Value("\${keycloak.auth-server-url}")
private val authServerUrl: String? = null
@Value("\${keycloak.realm}")
private val realm: String? = null
@Value("\${keycloak.resource}")
private val clientId: String? = null
private val role = "knowledgefactory-admin"
private val adminName = "knowledgefactory"
private val adminPassword = "password"
private val realmAdmin = "master"
private val adminClientId = "admin-cli"
@Value("\${keycloak.credentials.secret}")
private val clientSecret: String? = null
fun createEmployee(employeeVo: EmployeeVO): EmployeeVO {
val keycloak = KeycloakBuilder.builder().serverUrl(authServerUrl)
.grantType(OAuth2Constants.PASSWORD).realm(realmAdmin).clientId(adminClientId)
.username(adminName).password(adminPassword)
.resteasyClient(ResteasyClientBuilder().connectionPoolSize(10).build()).build()
keycloak.tokenManager().accessToken
val employee = UserRepresentation()
employee.isEnabled = true
employee.username = employeeVo.email
employee.firstName = employeeVo.firstname
employee.lastName = employeeVo.lastname
employee.email = employeeVo.email
val realmResource = keycloak.realm(realm)
val usersResource = realmResource.users()
val response = usersResource.create(employee)
employeeVo.statusCode =response.status
employeeVo.status = response.statusInfo.toString()
if (response.status == 201) {
val userId = CreatedResponseUtil.getCreatedId(response)
val passwordCred = CredentialRepresentation()
passwordCred.isTemporary = false
passwordCred.type = CredentialRepresentation.PASSWORD
passwordCred.value = employeeVo.password
val userResource = usersResource[userId]
userResource.resetPassword(passwordCred)
val realmRoleUser = realmResource.roles()[role].toRepresentation()
userResource.roles().realmLevel().add(Arrays.asList(realmRoleUser))
}
return employeeVo
}
fun login(employeeVo: EmployeeVO): Any {
val clientCredentials: MutableMap<String, Any?> = HashMap()
clientCredentials["secret"] = clientSecret
clientCredentials["grant_type"] = "password"
val configuration = Configuration(
authServerUrl, realm, clientId,
clientCredentials, null
)
val authzClient = AuthzClient.create(configuration)
val response = authzClient.obtainAccessToken(
employeeVo.email,
employeeVo.password
)
return ResponseEntity.ok(response)
}
}
VO class(EmployeeVO.kt)
package com.knf.dev.demo.kotlinspringkeycloakdemo.Vo
class EmployeeVO {
val email: String? = null
val password: String? = null
val firstname: String? = null
val lastname: String? = null
var statusCode = 0
var status: String? = null
}
Rest Controller(EmployeeController.kt)
package com.knf.dev.demo.kotlinspringkeycloakdemo.controller
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.beans.factory.annotation.Autowired
import com.knf.dev.demo.kotlinspringkeycloakdemo.service.KeycloakService
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import com.knf.dev.demo.kotlinspringkeycloakdemo.Vo.EmployeeVO
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
@RequestMapping(value = ["/employees"])
@RestController
class EmployeeController {
@Autowired
private val keycloakService: KeycloakService? = null
@PostMapping(path = ["/create"])
fun createEmployee(@RequestBody employeeVo: EmployeeVO?): ResponseEntity<*> {
return ResponseEntity.ok(keycloakService!!.createEmployee(employeeVo!!))
}
@PostMapping(path = ["/login"])
fun login(@RequestBody employeeVo: EmployeeVO?): ResponseEntity<*> {
return ResponseEntity.ok(keycloakService!!.login(employeeVo!!))
}
@get:GetMapping(value = ["/unprotected"])
val unProtectedData: String
get() = "This api is not protected."
@get:GetMapping(value = ["/protected"])
val protectedData: String
get() = "This api is protected."
}
Spring Boot Main Class(Application.kt)
package com.knf.dev.demo.kotlinspringkeycloakdemo
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.context.annotation.Bean
@SpringBootApplication
class Application {
@Bean
fun keycloakSpringBootConfigResolver(): KeycloakSpringBootConfigResolver {
return KeycloakSpringBootConfigResolver()
}
companion object {
@JvmStatic
fun main(args: Array<String>) {
SpringApplication.run(Application::class.java, *args)
}
}
}
Run
Start Spring Boot: gradle bootRun.