Spring Boot + Keycloak - Securing REST APIs

Hello everyone, Hope you are doing well. In this article, we will learn how to secure the Spring boot REST APIs with Keycloak. The GitHub repository link is provided at the end of this tutorial. 

Technologies Used:

  • Java 11
  • Spring Boot 2.5.5
  • KeyCloak 15.0.2
  • Maven 3

Step 1: 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 would 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, enable Composite Roles


This is the basic setup of Keycloak for use with web applications.

                                     

Step 2: Set up a Spring Boot application

Project Structure:



Dependency Management -Maven - pom.xml

A Project Object Model or POM is the fundamental unit of work in Maven. It is an XML file that contains information about the project and configuration details utilized by Maven to build the project. 
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.5</version>
<relativePath/>
</parent>
<groupId>org.knf.dev.demo</groupId>
<artifactId>keycloakspringbootexample</artifactId>
<version>1.0</version>
<name>keycloakspringbootexample</name>
<description>Spring Boot + Keycloak</description>

<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-starter</artifactId>
<version>15.0.2</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-admin-client</artifactId>
<version>15.0.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
<scope>provided</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.keycloak.bom</groupId>
<artifactId>keycloak-adapter-bom</artifactId>
<version>15.0.2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>


application.properties
server.port = 9080
keycloak.auth-server-url = http://localhost:8080/auth
keycloak.realm = knowledgefactory-realm
keycloak.resource = knowledgefactory-clientManagement
keycloak.public-client = true
keycloak.use-resource-role-mappings = true
keycloak.ssl-required = external
server.connection-timeout = 6000
keycloak.credentials.secret = <Secret>
 

Configure Keycloak(SecurityConfig.java)

Starting from Spring Boot Keycloak Adapter, we are required to explicitly define a KeycloakSpringBootConfigResolver bean to make Spring Boot resolve the Keycloak configuration from application.properties. It must be defined in a @Configuration class.
package com.knf.springboot.keycloak.config;

import org.keycloak.adapters.KeycloakConfigResolver;
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.keycloak.adapters.springsecurity.authentication.
KeycloakAuthenticationProvider;
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)
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
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
public void configureGlobal(AuthenticationManagerBuilder auth)
throws Exception {
KeycloakAuthenticationProvider keycloakAuthenticationProvider
= keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper
(new SimpleAuthorityMapper());
auth.authenticationProvider(keycloakAuthenticationProvider);
}

@Bean
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy
(new SessionRegistryImpl());
}

@Bean
public KeycloakConfigResolver KeycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
}


Service class(KeycloakService.java)
package com.knf.springboot.keycloak.keycloakservice;

import com.knf.springboot.keycloak.vo.EmployeeVO;
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.CreatedResponseUtil;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.KeycloakBuilder;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.admin.client.resource.UsersResource;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.Configuration;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import javax.ws.rs.core.Response;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

@Service
public class KeycloakService {

@Value("${keycloak.auth-server-url}")
private String authServerUrl;

@Value("${keycloak.realm}")
private String realm;

@Value("${keycloak.resource}")
private String clientId;

private final String role = "knowledgefactory-admin";
private final String adminName = "knowledgefactory";
private final String adminPassword = "password";
private final String realmAdmin = "master";
private final String adminClientId = "admin-cli";

@Value("${keycloak.credentials.secret}")
private String clientSecret;

public EmployeeVO createEmployee(EmployeeVO employeeVo) {
Keycloak keycloak = KeycloakBuilder.builder().
serverUrl(authServerUrl)
.grantType(OAuth2Constants.PASSWORD).
realm(realmAdmin).
clientId(adminClientId)
.username(adminName).password(adminPassword)
.resteasyClient(new ResteasyClientBuilder().
connectionPoolSize(10).build()).build();
keycloak.tokenManager().getAccessToken();
UserRepresentation employee = new UserRepresentation();
employee.setEnabled(true);
employee.setUsername(employeeVo.getEmail());
employee.setFirstName(employeeVo.getFirstname());
employee.setLastName(employeeVo.getLastname());
employee.setEmail(employeeVo.getEmail());
RealmResource realmResource = keycloak.realm(realm);
UsersResource usersResource = realmResource.users();
Response response = usersResource.create(employee);
employeeVo.setStatusCode(response.getStatus());
employeeVo.setStatus(response.getStatusInfo().toString());

if (response.getStatus() == 201) {
String userId = CreatedResponseUtil.getCreatedId(response);
CredentialRepresentation passwordCred = new CredentialRepresentation();
passwordCred.setTemporary(false);
passwordCred.setType(CredentialRepresentation.PASSWORD);
passwordCred.setValue(employeeVo.getPassword());
UserResource userResource = usersResource.get(userId);
userResource.resetPassword(passwordCred);
RoleRepresentation realmRoleUser = realmResource.roles().get(role).
toRepresentation();
userResource.roles().realmLevel().add(Arrays.
asList(realmRoleUser));
}
return employeeVo;
}

public Object login(EmployeeVO employeeVo) {
Map<String, Object> clientCredentials = new HashMap<>();
clientCredentials.put("secret", clientSecret);
clientCredentials.put("grant_type", "password");
Configuration configuration =
new Configuration(authServerUrl, realm, clientId,
clientCredentials, null);
AuthzClient authzClient = AuthzClient.create(configuration);
AccessTokenResponse response =
authzClient.obtainAccessToken(employeeVo.getEmail(),
employeeVo.getPassword());
return ResponseEntity.ok(response);
}
}


VO class(EmployeeVO.java)
package com.knf.springboot.keycloak.vo;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class EmployeeVO {
private String email;
private String password;
private String firstname;
private String lastname;
private int statusCode;
private String status;

}


Rest Controller(EmployeeController.java)
package com.knf.springboot.keycloak.controller;

import com.knf.springboot.keycloak.keycloakservice.KeycloakService;
import com.knf.springboot.keycloak.vo.EmployeeVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RequestMapping(value = "/employees")
@RestController
public class EmployeeController {

@Autowired
private KeycloakService keycloakService;

@PostMapping(path = "/create")
public ResponseEntity<?> createEmployee
(@RequestBody EmployeeVO employeeVo) {
return ResponseEntity.ok(keycloakService.
createEmployee(employeeVo));
}

@PostMapping(path = "/login")
public ResponseEntity<?> login
(@RequestBody EmployeeVO employeeVo) {
return ResponseEntity.ok(keycloakService.
login(employeeVo));
}

@GetMapping(value = "/unprotected")
public String getUnProtectedData() {
return "This api is not protected.";
}

@GetMapping(value = "/protected")
public String getProtectedData() {
return "This api is protected.";
}
}


Spring Boot Main Class(Application.java)
package com.knf.springboot.keycloak;

import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Bean
public KeycloakSpringBootConfigResolver keycloakSpringBootConfigResolver(){
return new KeycloakSpringBootConfigResolver();
}
}


Local Setup and Run the application

Step 1: Download or clone the source code from GitHub to the local machine - Click here


Step 2: mvn clean install


Step 3: Run the Spring Boot application - 

mvn spring-boot:run or Run as Spring Boot application.


Testing REST APIs using the Postman tool

Create Employee:


Employee Login and retrieve access token:


Call the secure REST API with the access token:


Comments

Popular posts from this blog

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

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

Java - Blowfish Encryption and decryption Example

Java - DES Encryption and Decryption example

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

ReactJS - Bootstrap - Buttons

Spring Boot 3 + Spring Security 6 + Thymeleaf - Registration and Login Example

File Upload, Download, And Delete - Azure Blob Storage + Spring Boot Example

Java - How to Count the Number of Occurrences of Substring in a String

Top 5 Java ORM tools - 2024