Spring Cloud Gateway — Resource Server with Keycloak RBAC

In this article, we will be exploring how we can integrate a resource server with an API gateway that is integrated with Keycloak and enable role-based access control (RBAC).

Introduction

So let’s get started.

Adding a custom keycloak role to the user.

For this, we will go to our realm and under the roles section and create a role called “product_read”.

Once the role is created, we will then assign this role to our “test” user. To do that, go to the “Users” section and then select the user “test”. Once you are in the user's settings, go to the “Role Mappings” tab and add the role to the user as follows.

We can remove the defaults roles that are already present, but I would leave them for now.

With this, we are done with allowing the user “test” to access the product resource from the Keycloak side.

Now Let’s create the resource server.

Creating a Resource Server

To create the resource server, let's go to https://start.spring.io and create an application called “product-service” with the following dependencies.

  • OAuth2 resource server
  • Spring Web

Once you generate and download the project, we will create a simple RestController that provides access to product resources.

@RestController
public class Controller {

@GetMapping("/product")
@RolesAllowed({"product_read"})
public String getProduct(Principal principal) {
return "Response from Product Service, User Id:" + principal.getName();
}
}

Here I am protecting the GET call with the “product_read” role which we had created in Keycloak. This means if the user can access the resource only if it has the role “product_read”.

Next, We will add some properties to application.yaml

spring:
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: http://localhost:8080/auth/realms/My-Realm/protocol/openid-connect/certs

server:
port: 9191

We can get this JWK URI from the “OpenId Connect Configuration” on the realm settings page. This JWK URI is required to validate the JWT token that comes in with the request.

Next, let’s set up the security configuration.

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(jsr250Enabled = true)
public class ResourceServerConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2ResourceServer()
.jwt(jwt -> jwt.jwtAuthenticationConverter( jwtAuthenticationConverter()));
}

private Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter() {
JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter();
jwtConverter.setJwtGrantedAuthoritiesConverter(new RealmRoleConverter());
return jwtConverter;
}
}

Here, we have added the@EnableGlobalMethodSecurity annotation, to enable method-level security in our application.

We then create a custom authorities converter. This converter will take out the keycloak roles (that are set as claims) from the JWT token and set them as authorities in spring security for role-based access.

Let’s look at the converter code.

public class RealmRoleConverter implements Converter<Jwt, Collection<GrantedAuthority>> {
@Override
public Collection<GrantedAuthority> convert(Jwt jwt) {
final Map<String, List<String>> realmAccess = (Map<String, List<String>>) jwt.getClaims().get("realm_access");
return realmAccess.get("roles").stream()
.map(roleName -> "ROLE_" + roleName)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
}

In this converter, we extract the “realm_access” claims and then convert them to roles, using the ROLE_ as a prefix. Spring security requires this prefix to interpret them as roles.

Why do we need this converter?

To answer this question, let’s look at the decoded JWT token.

The JWT payload has two parts, the “realm_access” and the “scope”. By default, the OAuth2 resource server JWT converter uses the “scope” claims. But these claims are part of the client scope i.e the client that was used in the API Gateway. If you go to the “Client Scopes” section in the client’s setting in Keycloak, you would find these scopes.

So we use the converter to extract the realm roles and use them as authorities in our spring application.

With all of this, we are done with creating the resource server.

Now, to connect it to the API Gateway application, we would have to make some changes to the API Gateway. Let’s have a look at that.

Connecting Resource Server to API Gateway

spring:
cloud:
gateway:
default-filters:
- TokenRelay
routes:
- id: product-resource-service
uri: http://localhost:9191
predicates:
- Path=/product/**

Here we are setting a route for any path request matching /product will be directed to the resource server (product-service) that is running at localhost at port 9191.

In the default-filters section, we would have to add “TokenRelay”, so that the API Gateway passes the JWT access token to the resource server.

With these properties, we are set to now run both the applications, i.e the API gateway and the product service.

Running the Applications

java -jar target/spring-cloud-gateway-keycloak-oauth2-0.0.1-SNAPSHOT.jarjava -jar target/product-service-0.0.1-SNAPSHOT.jar

The API Gateway runs at 9090 and the product service runs at 9191. Now let's go to the browser and call the following URL localhost:9090/product. On accessing the product resource from the API Gateway, we are redirected to the keycloak login page which is running at 8080. Use the username and password “test” and log in.

Once you log in, you get the response from the resource server containing the User Id from Keycloak.

Conclusion

I have uploaded the entire code integrating Keycloak, API Gateway, and resource server to my Github repo

I keep exploring and learning new things. If you want to know the latest trends and improve your software development skills, then subscribe to my newsletter on https://refactorfirst.com and also follow me on Twitter.

Enjoy!!

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store