Keycloak provides simple integration with Spring applications. As a result, we can easily configure our Spring Boot API security to delegate authentication and authorization to a Keycloak server.
keycloak-spring-boot
repository. If you want to run the app locally, visit the project repository on GitHub and follow the directions in the README.md file.I’m going to add maven dependencies and required properties for Keycloak integration. Then, before I actually restrict access to my API endpoints, I will define a basic security configuration and test that both tools work together properly.
First, I’m going to add the required dependencies to my project. In addition to the Keycloak and Spring Security starters for Spring Boot, I’ll add the Keycloak bom for adapters:
<!-- pom.xml -->
…
<properties>
…
<keycloak-adapter.version>16.1.1</keycloak-adapter.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.keycloak.bom</groupId>
<artifactId>keycloak-adapter-bom</artifactId>
<version>${keycloak-adapter.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
…
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
…
</dependencies>
…
I’m running my Keycloak instance in a Docker container. For my sample client configuration, see the screenshot from the Prerequisites section.
I’m going to copy some information from my Keycloak client setup to the application.properties
file:
# src/main/resources/application.properties
keycloak.realm=keep-growing
keycloak.resource=spring-boot-example-app
keycloak.auth-server-url=http://localhost:8024/auth
keycloak.credentials.secret=QjLCjk1I9sugcZSDFCsyAkoLOqAHDLKC
Below is a brief explanation of the properties:
realm
– my example realm name;resource
– my example client name;auth-server-url
– you can get the value from the Keycloak OIDC URI endpoint list by visiting the realm settings, clicking the OpenID Endpoint Configuration
and copying the auth
path:credentials.secret
– my example client has the confidential
access type. Therefore, I have to copy the secret
value from the Credentials
tab:If you want to access user roles at the client level and not user roles at the realm level, add the following property:
# src/main/resources/application.properties
…
keycloak.use-resource-role-mappings=true
However, in my example I’m using realm roles for users.
I’m going to create two configuration classes to handle my Keycloak setup.
The first one provides the ability to resolve my Keycloak config based on the application.properties
file. I’m going to enclose it in a separate file to avoid the circular dependency issue (described in Javadoc in the snippet below):
package in.keepgrowing.keycloakspringboot.security.config;
import org.keycloak.adapters.KeycloakConfigResolver;
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* The {@code KeycloakConfigResolver} bean is not defined in a configuration class that extends
* {@code KeycloakWebSecurityConfigurerAdapter} to avoid the {@code Circular References} problem in Spring Boot from version 2.6.0.
*
* @see <a href="https://github.com/keycloak/keycloak/issues/8857">Application don't start because of Circular Reference due to dependency injection</a>
*/
@Configuration
public class KeycloakConfig {
@Bean
public KeycloakConfigResolver keycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
}
Next, I’m going to add the following class containing a Keycloak-based Spring security configuration:
package in.keepgrowing.keycloakspringboot.security.config;
import org.keycloak.adapters.springsecurity.KeycloakConfiguration;
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.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
@KeycloakConfiguration //1
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter { //2
@Override
protected void configure(HttpSecurity http) throws Exception { //3
super.configure(http);
http
.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.and()
.authorizeRequests()
.anyRequest().permitAll();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) { //4
auth.authenticationProvider(getKeycloakAuthenticationProvider());
}
private KeycloakAuthenticationProvider getKeycloakAuthenticationProvider() { //5
KeycloakAuthenticationProvider authenticationProvider = keycloakAuthenticationProvider();
var mapper = new SimpleAuthorityMapper();
mapper.setConvertToUpperCase(true);
authenticationProvider.setGrantedAuthoritiesMapper(mapper);
return authenticationProvider;
}
@Bean
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() { //6
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
}
There are a few things to note:
@KeycloakConfiguration
– this metadata annotation provides all annotations that are needed to integrate Keycloak in Spring Security (e.g. for enabling Spring Security or configuring component scanning);KeycloakWebSecurityConfigurerAdapter
– by extending this class we gain a useful base class for creating a WebSecurityConfigurer instance secured by Keycloak;SecurityConfig::configure
– we’re going to overwrite the basic Spring Security behaviour, first by calling the parent implementation and then by adding our own csrf and endpoint protection config; SecurityConfig::configureGlobal
– furthermore, we have to register the KeycloakAuthenticationProvider with the authentication manager;SecurityConfig::getKeycloakAuthenticationProvider
– my auxiliary method for customising the authentication provider. Namely, I’m providing a simple one-to-one GrantedAuthoritiesMapper which adds the ROLE_
prefix and converts the authority value to upper case (e.g. a chief-operating-officer
role from Keycloak realm becomes ROLE_CHIEF-OPERATING-OFFICER
in my Spring Boot app);SecurityConfig::sessionAuthenticationStrategy
– the session authentication strategy bean has to be of type RegisterSessionAuthenticationStrategy
for public
or confidential
applications (NullAuthenticatedSessionStrategy
for bearer-only
applications).Although I haven’t restricted access to any endpoint in my API, I’m still going to test the current setup. I’m going to enable the DEBUG log lever for security by adding the following property to my application.properties
file:
logging.level.org.springframework.>
As a result, I can verify the configuration details and see that Keycloak filters are present in the filter chain:
…
DEBUG 12469 --- [main] edFilterInvocationSecurityMetadataSource : Adding web access control expression [permitAll] for any request
INFO 12469 --- [main] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with […, org.keycloak.adapters.springsecurity.filter.KeycloakPreAuthActionsFilter@439e3cb4, org.keycloak.adapters.springsecurity.filter.KeycloakAuthenticationProcessingFilter@31e76a8d, …]
…
Moreover, Spring Security doesn't create a user password which would be its default behaviour without the Keycloak configuration.
At this point, my Spring Boot application is delegating authentication and authorization processes to Keycloak. However, I am still free to call the endpoints as they are not explicitly secured.
Next, I'm going to restrict access to the API endpoints by replacing anyRequest().permitAll()
with the anyRequest().authenticated()
line:
…
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http
…
.authorizeRequests()
.anyRequest().authenticated();
}
After restarting the application, I can see in its logs that all endpoints are protected:
…
DEBUG 13361 --- [main] edFilterInvocationSecurityMetadataSource : Adding web access control expression [authenticated] for any request
…
As a result, my Postman setup and Swagger UI config require additional configuration to allow me to make API calls.
Configuring security in a project will break existing Spring MVC tests. Therefore, below you will find some details that require updating.
I don't want to clutter tests with the configuration of an external authorization service. Therefore, I'm going to configure my Spring MVC tests to use Spring Security instead of Keycloak. For full instructions on how to apply a different security configuration in tests, see the Keycloak with Spring Boot #2 – Spring Security instead of Keycloak in tests post.
Fortunately, Spring Security provides the @WithMockUser annotation. We can apply it to a specific test or an entire class. In the following example test method, we can see how to use this annotation in a single test while keeping the default user data:
@Test
@WithMockUser
void shouldReturnAllProducts() throws Exception {
ProductResponse productResponse = productResponseProvider.full();
String expected = objectMapper.writeValueAsString(List.of(productResponse));
when(apiFacade.findAll())
.thenReturn(List.of(productResponse));
mvc.perform(get("/api/products")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().json(expected));
}
Additionally, we can customise the username
, password
, roles
and authorities
(the latter two are exclusive) that our mocked user will receive:
@WithMockUser(username = "thomas", password = "test", roles = {"EDITOR"})
As you may have noticed, my security configuration uses csrf protection. Therefore, I have to add the SecurityMockMvcRequestPostProcessors::csrf
method that automatically populates a valid CSRF token in my POST request:
@Test
@WithMockUser
void shouldSaveNewProduct() throws Exception {
ProductRequest newProduct = productRequestProvider.full();
ProductResponse savedProduct = productResponseProvider.full();
String expected = objectMapper.writeValueAsString(savedProduct);
when(apiFacade.save(newProduct))
.thenReturn(savedProduct);
mvc.perform(post("/api/products")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(newProduct))
.with(csrf()))
.andExpect(status().isOk())
.andExpect(content().json(expected));
}
Spring Security allows us to use role-based control to restrict access to API resources. However,…
A custom annotation in Spring Boot tests is an easy and flexible way to provide…
Delegating user management to Keycloak allows us to better focus on meeting the business needs…
Swagger offers various methods to authorize requests to our Keycloak secured API. I'll show you…
Configuring our Spring Boot API to use Keycloak as an authentication and authorization server can…
Postman comes with a wide variety of OAuth 2.0 compliant configuration options that allow us…