Authentication Authorization Keycloak

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!

Author

Ben Blinebury