Delegating user management to Keycloak allows us to better focus on meeting the business needs of an application. However, we still need to provide the appropriate configuration to translate user roles and privileges between Keycloak and Spring Boot. Additionally, we’re going to need some handy techniques for debugging how roles are converted between the two services.
Prerequisities
- The main principles of managing roles in Keycloak are described in the Keycloak in Docker #4 – How to define user privileges and roles post.
- Furthermore, you can follow the Keycloak with Spring Boot #1 – Configure Spring Security with Keycloak post to recreate the example configuration I use in this article.
First, I will describe some debugging techniques. Next, I’ll show you some sample role mapping setups.
Debugging role mapping between Keycloak and Spring Boot
I’m going to show you some useful methods for verifying role support in a Spring Boot app and Keycloak server. By seeing what is really going on under the hood, you can save a lot of time when an apparently correct configuration does not bring the expected results.
How to debug roles in Spring Security Context
We can look into the Spring Security Context to verify the actual permissions that are being assessed in two ways:
- Set the security logging level to
DEBUG
– add thelogging.level.org.springframework.security=DEBUG
property to theapplication.properties
file. Thanks to this, after each API request you’ll see a list ofGranted Authorities
in the logs:
1 |
DEBUG --- w.c.HttpSessionSecurityContextRepository : Stored SecurityContextImpl [Authentication=KeycloakAuthenticationToken [Principal=…, Granted Authorities=[ROLE_chief-operating-officer, ROLE_user]]] to HttpSession |
- Log Authorities manually – if you only want to log authorities for a single request, you can obtain Authentication from Security Context Holder and log GrantedAuthorities directly inside a selected endpoint:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
package in.keepgrowing.keycloakspringboot.products.adapters.driving.api.http.controllers; import in.keepgrowing.keycloakspringboot.products.adapters.driving.api.http.model.responses.ProductResponse; import in.keepgrowing.keycloakspringboot.products.adapters.driving.api.http.services.ProductHttpApiFacade; import lombok.extern.log4j.Log4j2; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; import java.util.List; … @RestController @RequestMapping(value = ProductControllerPaths.PRODUCTS_PATH, produces = MediaType.APPLICATION_JSON_VALUE) @Log4j2 public class ProductController { … @GetMapping public ResponseEntity<List<ProductResponse>> findAll() { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); auth.getAuthorities().forEach(a->log.info(String.valueOf(a))); return new ResponseEntity<>(apiFacade.findAll(), HttpStatus.OK); } … |
As a result, when you call this endpoint, you’ll see log entries similar to these:
1 2 |
INFO --- i.k.s.p.p.controllers.ProductController : ROLE_CHIEF-OPERATING-OFFICER INFO --- i.k.s.p.p.controllers.ProductController : ROLE_USER |
How to debug roles sent from Keycloak
In my example realm, there is a default user christina
who has the roles of user
and chief-operation-officer
. We can verify her privileges by copying the token from Postman or from the developer tool in the browser as seen in the screenshots below:
Then, decode the token value in e.g. jwt.io tool to see the actual content:
Roles vs Authorities
Spring Security allows us to configure privileges in a very granular manner with Authorities (e.g. CAN_WRITE). However, we can also manage resource access in a more coarse fashion with Roles (e.g ROLE_EDITOR). In other words, the ROLE_ prefix is what differentiate these concepts in Spring.
Keycloak recognises this naming convention. Therefore, we can decide whether we want to map privileges defined in our Keycloak server as roles or authorities in Spring Boot.
Mapping Roles
To map my chief-operating-officer
Keycloak role to ROLE_CHIEF-OPERATING-OFFICER
in Spring Boot, I’m going to use SimpleAuthorityMapper:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
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.core.authority.mapping.SimpleAuthorityMapper; @KeycloakConfiguration public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter { … @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) { auth.authenticationProvider(getKeycloakAuthenticationProvider()); } private KeycloakAuthenticationProvider getKeycloakAuthenticationProvider() { KeycloakAuthenticationProvider authenticationProvider = keycloakAuthenticationProvider(); var mapper = new SimpleAuthorityMapper(); mapper.setConvertToUpperCase(true); authenticationProvider.setGrantedAuthoritiesMapper(mapper); return authenticationProvider; } … } |
As we can see in the SimpleAuthorityMapper
documentation, the default prefix is ROLE_
:
Therefore, we don’t need to set it manually.
As a result, I can impose access restrictions to selected resources. Below you can see the role used in the @PreAuthorize annotation:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
package in.keepgrowing.springbootswaggeruikeycloak.products.presentation.controllers; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; … public class ProductController { … @PostMapping() @PreAuthorize("hasRole('CHIEF-OPERATING-OFFICER')") public ResponseEntity<Product> save(@RequestBody Product productDetails) { … } } |
On the other hand, I can restrict access to all POST endpoints only to users with this role:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
package in.keepgrowing.keycloakspringboot.security.config; import org.keycloak.adapters.springsecurity.KeycloakConfiguration; import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.builders.HttpSecurity; … @KeycloakConfiguration public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter { … @Override protected void configure(HttpSecurity http) throws Exception { super.configure(http); http … .antMatchers(HttpMethod.POST).hasRole("CHIEF-OPERATING-OFFICER") … } … |
In both cases, the hasRole
expression will automatically add the ROLE_
prefix to the given CHIEF-OPERATING-OFFICER
value and compare the result with the ROLE_CHIEF-OPERATING-OFFICER
GrantedAuthority
value which we have mapped in the Spring SecurityContext
. This behaviour is described in the method docs:
Mapping Authorities
If you want to control access in more detail, you can map the permissions from Keycloak to Authorities
in Spring Security. Remove the ROLE_
prefix when configuring the authority mapper:
1 2 3 4 5 6 7 8 9 10 11 12 |
package in.keepgrowing.keycloakspringboot.security.config; … private KeycloakAuthenticationProvider getKeycloakAuthenticationProvider() { KeycloakAuthenticationProvider authenticationProvider = keycloakAuthenticationProvider(); var mapper = new SimpleAuthorityMapper(); mapper.setConvertToUpperCase(true); mapper.setPrefix(""); authenticationProvider.setGrantedAuthoritiesMapper(mapper); return authenticationProvider; } … |
This makes the sample can-write
permission from Keycloak become CAN-WRITE
authority in Spring SecurityContext.
Below you can see the authority used in the @PreAuthorize annotation:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
package in.keepgrowing.springbootswaggeruikeycloak.products.presentation.controllers; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; … public class ProductController { … @PostMapping() @PreAuthorize("hasAuthority('CAN-WRITE')") public ResponseEntity<Product> save(@RequestBody Product productDetails) { … } } |
Then again, I can restrict access to all POST endpoints only to users with this authority:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
package in.keepgrowing.keycloakspringboot.security.config; import org.keycloak.adapters.springsecurity.KeycloakConfiguration; import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.builders.HttpSecurity; … @KeycloakConfiguration public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter { … @Override protected void configure(HttpSecurity http) throws Exception { super.configure(http); http … .antMatchers(HttpMethod.POST).hasAuthority(("CAN-WRITE") … } … |
In both cases, the hasAuthority
expression will compare the given CAN-WRITE
value with the CAN-WRITE
GrantedAuthority
value which we have mapped in the Spring SecurityContext
.
If you configure the authority mapper in a way that removes the ROLE_
prefix from the authorities, don’t use hasRole
in security expressions and configuration.
Realm roles vs client roles
You can read about differences between the realm and client roles in the Keycloak in Docker #4 – How to define user privileges and roles article. Given that the Keycloak server contains properly defined user roles, we need to instruct our Spring Boot app which role set to evaluate. We’ll do so with the keycloak.use-resource-role-mappings
property.
Evaluate realm roles in Spring Boot
A Spring Boot app will evaluate the realm roles if the keycloak.use-resource-role-mappings
property is set to false
, which is the default value. You can verify that the values from the realm-access
field in the access token are present as GrantedAuthirities
in the SecurityContext
using the debug methods shown above.
Evaluate client roles in Spring Boot
In the keycloak.*
properties, resource
is the client
. Therefore, by setting keycloak.use-resource-role-mappings
to true
, we’re telling Spring Boot that the client roles should be considered. You can verify that the values from the resource-access
field in the access token are present as GrantedAuthirities
in the SecurityContext
using the debug methods shown above.
Handle user roles in Spring Boot tests
When we restrict endpoint access to a specific role, we need to update Spring MVC tests to keep them working properly.
Custom annotation for a mocked user
Fortunately, Spring provides us with the @WithMockUser annotation that allows us to test an endpoint for a given user role, e.g.:
1 2 3 4 5 6 7 8 9 10 11 12 |
package in.keepgrowing.keycloakspringboot.products.adapters.driving.api.http.controllers; import org.springframework.security.test.context.support.WithMockUser; … @Test @WithMockUser(roles="CHIEF-OPERATING-OFFICER") void shouldDeleteProduct() throws Exception { mvc.perform(delete(BASE_PATH + "/" + TEST_UUID) .contentType(MediaType.APPLICATION_JSON) .with(csrf())) .andExpect(status().isNoContent()); } |
In the same manner we can mock a test user with a given authority:
1 2 3 |
… @WithMockUser(authorities="CAN-WRITE") … |
However, having to manually enter a role name for each test case or test class where that role matters would be cumbersome. Instead, we can create a custom meta annotation:
1 2 3 4 5 6 7 8 9 10 11 |
package in.keepgrowing.keycloakspringboot.testing.annotations; import org.springframework.security.test.context.support.WithMockUser; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @Retention(RetentionPolicy.RUNTIME) @WithMockUser(roles="CHIEF-OPERATING-OFFICER") public @interface WithMockChiefOperationOfficer { } |
And then use it in test cases (or classes) like in the example snippet below:
1 2 3 4 5 6 7 8 |
package in.keepgrowing.keycloakspringboot.products.adapters.driving.api.http.controllers; import in.keepgrowing.keycloakspringboot.testing.annotations.WithMockChiefOperationOfficer; … @Test @WithMockChiefOperationOfficer void shouldDeleteProduct() throws Exception { … |
Enable method-level security
If we’re using annotations like @PreAuthorize
to restrict access to endpoints, we have to create an appropriate configuration class for our MVC tests:
1 2 3 4 5 6 7 8 9 |
package in.keepgrowing.keycloakspringboot.testing.config; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; @TestConfiguration @EnableMethodSecurity public class ControllerIntegrationTestConfig { } |
Then we have to load it when running the tests. We can achieve it by adding @Import or a custom annotation to the test classes.
Read more on handling Keycloak roles in Spring Boot
- Difference between Role and GrantedAuthority in Spring Security
- Spring security added prefix “ROLE_” to all roles name?
- SimpleAuthorityMapper.java in the GitHub repo
- Testing With Spring Security
Photo by Robert Nagy from Pexels