Spring Boot + Keycloak - Securing REST APIs

Hello everyone, In this article, we will learn how to secure the Spring boot REST APIs with Keycloak.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

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:



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. It contains default values for most projects. Some of the configurations that can be designated in the POM is the project dependencies, the plugins or goals that can be executed, the build profiles, and so on. Other information such as the project version, description, developers, mailing lists, and such can withal be designated.
<?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 = 86996cf7-5030-4a37-948b-99223f8bdabf
 

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();
}
}


Github repository download link is provided at the end of this tutorial

Local Setup:

Step 1: Download or clone the source code to a local machine.

Step 2mvn clean install

Step 3: Run the Spring Boot application
mvn spring-boot:run


Testing REST APIs using postman tool

Create Employee:


Employee Login and retrieve access token:


Call the secure REST API with the access token:


Popular posts from this blog

Spring boot video streaming example-HTML5

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

Spring Boot + Mockito simple application with 100% code coverage

Spring Boot + OpenCSV Export Data to CSV Example

Custom Exception Handling in Quarkus REST API

DataTable-Pagination example with Spring boot, jQuery and ajax

Registration and Login with Spring Boot + Spring Security + Thymeleaf

Node JS mini projects with source code - free download

Spring boot web project free download:User Registration System

Java - Blowfish Encryption and decryption Example