Secure Spring Boot Application with OpenID Connect and Role Based Authorization
In this article, I hope to give a step by step approach on how you can secure your Spring Boot application with OpenID Connect Login and Role Based Authorization. I will here be using Keycloak as the Identity Provider however you are free to use another Identity and Access Management Solution in place. The solution given here will work with any other identity provider as well with few changes.
For the scope of this tutorial, I will omit the steps on setting up the Keycloak server and Spring Boot application. I will add links on official guidelines on how you can set up those under References section.
Use case
For simplicity let’s assume we have a Spring Boot application with a Rest Controller to view customers and their payment information. There will be 2 endpoints with GET mapping.
- /customers will list basic information about a customers available in the system. This endpoint allows a user to access customer information with a GET request. We need to ensure security for this endpoint such that only users who successfully authenticated can view the endpoint.
- /payments With this endpoint users can view customer specific payment information. Since this endpoint displays confidential information, we need to enforce an additional level of security. Hence, in addition to having user authentication, we also need to ensure that users with certain privileges (admin access) can view this endpoint data. Therefore, we will be enforcing role based authorization such that only an authenticated user with customer-service-admin role will be allowed to access the endpoint.
Configuring Keycloak for OpenID Connect Login
Create a new realm in your Keycloak server. Let’s use my-realm as the realm name.
Configure a new OpenID Connect client as shown in below image. Let’s use customer-service-client as the Client ID.
Make sure to enable the options ‘Standard flow’ and ‘Client authentication’
Configure a client role from the client role tab. Role name: customer-service-admin
Create 2 new users. Let’s use usernames peter and ben.
For the first user (peter) assign the client role that we created in a previous step.
Navigate to Client Scopes -> Roles -> Client Roles -> Scopes -> Enable the option ‘Add to ID token’
These are the steps required to configure Keycloak for securing your application. Now, if all the configurations are successful, you can try out authorization code flow and obtain an access token from authorization code grant type using a tool like postman or curl and make sure the standard flow is working as expected. If you decode the id_token using a tool like jwt.io, you should be able to find the client roles under the resource_access field in the token as shown below. (When you authenticate with user peter).
Configuring Spring Boot Application
We need to use spring-boot-starter-security and spring-boot-starter-oauth2-client libraries to configure OpenID Connect login for the Spring Boot application. Hence, add the following to your pom file.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
Next, we need to update application.properties file in order to communicate with the Keycloak server using OpenID Connect protocol. Add the below configs to the property file. Make sure to update the values with your application specific settings.
spring.security.oauth2.client.registration.keycloak.client-id=customer-service-client
spring.security.oauth2.client.registration.keycloak.client-secret=cUYL7M81Vm9CKYrxFTqUTtB2FIVqckzM
spring.security.oauth2.client.registration.keycloak.scope=openid,profile,roles
spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.keycloak.redirect-uri=http://localhost:8001/login/oauth2/code/keycloak
spring.security.oauth2.client.provider.keycloak.issuer-uri=http://localhost:8080/realms/my-realm
Now, we need to override the default HTTP Security configuration in Spring to enforce OpenID Connect Login for the users. This is possible with the registration of a SecurityFilterChain bean as shown below.
@Configuration
@EnableWebSecurity
public class ConfigSecurity {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.requestMatchers("/customers/**").authenticated()
.and().oauth2Login();
return http.build();
}
}
Now if you try to access the /customers endpoint, your browser will be redirected to Keycloak login page. With any of the above created users (peter or ben) you should be able to successfully authenticate.
Next, we need to configure role based authorization for the /payments endpoint as per our second requirement.
Spring internally uses GrantedAuthorities for access control. By default, Spring Framework will generate SimpleGrantedAuthories based on the values available in the ‘scope’ claim of a JWT by prepending them with SCOPE_ prefix. (e.g. SCOPE_openid, SCOPE_email). Those can be used for authorizing requests, such as in hasRole or hasAuthority methods.
In our case, we need a way to extract the client roles from the JWT, and generate GrantedAuthorities for those. For this, I will be implementing a GrantedAuthoritiesMapper as shown below.
public class GrantedAuthoritiesMapperImpl implements GrantedAuthoritiesMapper {
private static final String RESOURCE_ACCESS = "resource_access";
private static final String ROLES = "roles";
private static final String ROLE_PREFIX = "ROLE_";
@Value("${spring.security.oauth2.client.registration.keycloak.client-id}")
private String deliveryServiceClientId;
@Override
public Collection<? extends GrantedAuthority> mapAuthorities(Collection<? extends GrantedAuthority> authorities) {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
authorities.forEach(authority -> {
if (authority instanceof OidcUserAuthority oidcUserAuthority) {
// Map the claims found in idToken to one or more GrantedAuthority.
OidcIdToken idToken = oidcUserAuthority.getIdToken();
mappedAuthorities.addAll(extractClientRoles(idToken));
}
});
return mappedAuthorities;
}
private Collection<GrantedAuthority> extractClientRoles(OidcIdToken idToken) {
Map<String, Object> resourceAccess = idToken.getClaim(RESOURCE_ACCESS);
Map<String, Object> resource;
Collection<String> resourceRoles;
if (deliveryServiceClientId == null || resourceAccess == null
|| (resource = (Map<String, Object>) resourceAccess.get(deliveryServiceClientId)) == null
|| (resourceRoles = (Collection<String>) resource.get(ROLES)) == null) {
return Set.of();
}
return resourceRoles.stream()
.map(role -> new SimpleGrantedAuthority(ROLE_PREFIX + role))
.collect(Collectors.toSet());
}
}
Next, we need to register a bean for the GrantedAuthoritiesMapper class and update the securityFilterChain to enforce Role based Access Control for the /payments endpoint as shown below.
@Configuration
@EnableWebSecurity
public class ConfigSecurity {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.requestMatchers(HttpMethod.GET, "/payments/**").hasRole("customer-service-admin")
.requestMatchers("/customers/**").authenticated()
.and().oauth2Login().userInfoEndpoint()
.userAuthoritiesMapper(this.userAuthoritiesMapper());
return http.build();
}
@Bean
public GrantedAuthoritiesMapper userAuthoritiesMapper() {
return new GrantedAuthoritiesMapperImpl();
}
}
Now if you try to access the /payments endpoint, your browser will be redirected to Keycloak login page as before. But only the user with the admin privileges (peter) will be able to view the contents of this endpoint.