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.
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.
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
More related topics: