Method Level Authorization with Spring Boot and Keycloak
In this article we will go over how to implement role based, method level security in a Spring Boot project using a token generated by Keycloak. It’s a fairly simple and straightforward process, and shouldn’t take more than 30-45 minutes to setup in a new project.
Here’s a list of the steps we will cover in this article:
Setting Up Our Keycloak Server
- Creating a Realm and Client
- Creating a Client Role
- Creating a User
- Mapping the Client Role to our new User
Configuring our Spring Boot Project
- Setting up our POM file
- Setting up the Security Config
- Setting the Application Properties
Setting Up Our Controller and Finishing Up
- Controller set up
- Testing It Out
Let’s get started!
Setting Up Our Keycloak Server
First let’s start with setting up our Keycloak service. You can find the latest version here, but for our demo I will just be running it in Docker. Here is my docker-compose.yml file:
version: '3'
services:
keycloak:
image: quay.io/keycloak/keycloak:20.0.3
restart: always
environment:
- KEYCLOAK_ADMIN=admin
- KEYCLOAK_ADMIN_PASSWORD=password
ports:
- "8081:8080"
command:
- start-dev
Once that is running, simply navigate to localhost://8081
and click on Administration Console
, which will bring you to the login page.
Creating a Realm and Client
We already set up an admin and user, so simply enter admin
for the username and password
for the password and you’ll be in the console, in the initial realm that Keycloak sets for us called Master. Before we go any further, let’s go over some of the terminology we’ll be using in the article, namely Realm and Client. Let’s look at the Keycloak documentation to define each.
Realms: A realm manages a set of users, credentials, roles, and groups. A user belongs to and logs into a realm. Realms are isolated from one another and can only manage and authenticate the users that they control.
Clients: Clients are entities that can request Keycloak to authenticate a user. Most often, clients are applications and services that want to use Keycloak to secure themselves and provide a single sign-on solution. Clients can also be entities that just want to request identity information or an access token so that they can securely invoke other services on the network that are secured by Keycloak.
It is recommended to never use the master realm, as it’s meant to be used for super admins to create or manage realms, so let’s make a new one. We’ll call it demo-realm
.
Now that we’ve got our demo realm, let’s make a client.
For Client ID we’ll enter demo-client
, then hit next, then save.
Creating a Client Role
So now we have our client, the next steps are to make a user and a client role to attach to it. We’ll start with the role first. While still in the Clients
section, click on Roles
at the top and then click the button that says Create Role
. For the name, we are going to call it DEMO_ROLE
. We don’t need to input anything for the description. Click save when you’re done and we’ll move on to creating the user.
Creating a User
Let’s now add a new user. Click on Users
on the left, then click the Create new user
button. There are many properties a user can have, but for our demo all we need is a username. Let’s stick with the pattern we’ve been using and name it demo_user
and then click create
.
After saving we will be in the user details page. We’re going to set this user’s password and then map the role we created to it. So first lets’s click on Credentials
at the top. For this we will just use password
for the password. Make sure to turn Temporary
off and then click save
.
Mapping the Client Role to our new User
Next we’re going to click on Role Mappings
, and then Assign Role
. In the role assignment pop-up, click the filter dropdown menu, and choose Filter by clients
. In the list that is populated, you will find our DEMO_ROLE
role that we made earlier for the client. Select that and then click Assign
.
And that’s all we need to do for our Keycloak server. Next we’ll set up our Spring Boot project.
Setting up a Spring Boot project is outside of the scope of this article, but is super easy with spring initializr
Configuring our Spring Boot Project
Setting up our POM file
For our Spring Boot project, we need to add the spring-boot-starter-oauth2-resource-server
dependency to our dependencies. For Spring Boot 2 we had the keycloak-spring-boot-starter
and the keycloak-adapter-bom
, but are deprecated for Spring Boot 3 (and Keycloak has said in their documentation that they will not be updating their libraries for use with Spring Boot 3). I will provide an example of our depencies below. I am doing this in a maven project with a pom
file, but you can use a gradle project if you prefer:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
Setting up the Security Config
With Spring Boot 3 and the deprecation of Keycloak’s libraries, we’ve lost easy access to a few things that we had previously. Most of them are outside of the scope of this article, but we do need to map the client roles from the Keycloak token to the SecurityContext
in our application. Here is an example of the token we get from Keycloak:
"exp": 1672640935,
"nbf": 0,
"iat": 1672640635,
"iss": "http://localhost:8081/auth/realms/demo-realm",
"aud": "demo-client",
"sub": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"typ": "Bearer",
"azp": "demo-client",
"auth_time": 0,
"session_state": "2e710bd4-432c-4fef-9a4b-c256b7fe95e3",
"acr": "1",
"allowed-origins": [],
"realm_access": {
"roles": [
"offline_access",
"uma_authorization"
]
},
"resource_access": {
"demo-client": {
"roles": [
"DEMO_ROLE"
]
},
"account": {
"roles": [
"manage-account",
"manage-account-links",
"view-profile"
]
}
},
"scope": "email profile",
"email_verified": false,
"preferred_username": "demo_user"
}
The values we want to grab is in the array of strings under resource_access > demo-client
. Luckily we can get that done pretty simply, right in our SecurityConfig
class. So let’s do it:
import com.nimbusds.jose.shaded.gson.internal.LinkedTreeMap;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.web.SecurityFilterChain;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> {
auth.requestMatchers("/**").fullyAuthenticated();
})
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
return http.build();
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverterForKeycloak() {
Converter<Jwt, Collection<GrantedAuthority>> jwtGrantedAuthoritiesConverter = jwt -> {
Map<String, Object> resourceAccess = jwt.getClaim("resource_access");
Object client = resourceAccess.get("demo-client");
LinkedTreeMap<String, List<String>> clientRoleMap = (LinkedTreeMap<String, List<String>>) client;
List<String> clientRoles = new ArrayList<>(clientRoleMap.get("roles"));
return clientRoles.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
};
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
}
The second bean we see above (jwtAuthenticationConverterForKeycloak) is grabbing what we want, and adding it to the security context’s GrantedAuthority
.
Setting the Application Properties
Lastly we need to set up our app’s properties. I am using an application.properties
file, but if you prefer to use anapplication.yml
file that’s totally fine:
spring.security.oauth2.client.registration.keycloak.client-id=demo-client
spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.keycloak.scope=openid
spring.security.oauth2.client.provider.keycloak.issuer-uri=http://localhost:8081/auth/realms/demo-realm
spring.security.oauth2.client.provider.keycloak.user-name-attribute=preferred_username
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8081/auth/realms/demo-realm
And that’s it. Next up we will set up a controller and make sure our project works.
Setting Up Our Controller and Finishing Up
Controller set up
Ok. We are in the final stretch. Let’s set up a super simple controller. We’re going to be adding in @PreAuthorize
tag to our GET
method. This is what checks to make sure a user has the correct role to access the method. Here it is:
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/demo")
public class DemoController {
@GetMapping
@PreAuthorize("hasAnyAuthority('DEMO_ROLE')")
public ResponseEntity<String> demoController() {
return ResponseEntity.ok("Hi there!");
}
}
Inside our @PreAuthorize
tag, we have another call to hasAuthority
. This is where we give the role we want to check for. We can also use hasAnyAuthority
if we want to check for any of a list of roles.
Testing It Out
So let’s check this thing out! I’m using Postman
as an API client, but you can use whatever you want. As we can see in our controller, our endpoint is just http://localhost:8080/demo
so I’ll send a GET
request over to it.
Oh no! Our request came back with a 401. We’re not authorized!
But of course we knew that would happen. We didn’t generate a token for it. So let’s do that now. Here is the request for a token from Keycloak. It is a POST
request, with an x-www-form-urlencoded
body.
Let’s go ahead and copy that access_token
and paste it into the bearer token
input in our GET
request:
Success!
That’s really all there is to it. If you’d like further proof that it works as intended, simply navigate to the demo_user
, remove the DEMO_ROLE
, get a new token, and try again. You will see you are no longer authorized to access our endpoint.
Conclusion
There is a whole lot more we could do, like checking realm level roles, or mapping user attributes to a client, but that is outside of the scope of this article. For now I hope this was helpful. You can see the code we used in this article here.
Thanks for reading!