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. 

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.




The credentials tab will show the Client Secret which is required for the Spring Boot Application Keycloak configurations.



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.

Popular posts from this blog

Learn Java 8 streams with an example - print odd/even numbers from Array and List

Java Stream API - How to convert List of objects to another List of objects using Java streams?

Registration and Login with Spring Boot + Spring Security + Thymeleaf

Java, Spring Boot Mini Project - Library Management System - Download

ReactJS, Spring Boot JWT Authentication Example

Top 5 Java ORM tools - 2024

Java - Blowfish Encryption and decryption Example

Spring boot video streaming example-HTML5

Google Cloud Storage + Spring Boot - File Upload, Download, and Delete