Spring Boot + Keycloak RBAC

208 Views Asked by At

I have a spring boot application and a keycloak instance.

The spring boot application runs in 8083 port and has some endpoints-resources and I want all these to be accessed upon authentication but also authorization having a certain role

Let's assume that we have some users and the role named myrole assigned to one or more of them.

I have the below configuration in my spring boot app in order to make all my endpoints to be accessible from every user that has the myrole role assigned.

SecurityConfig.java

package mypackage;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
    }

    @Bean
    public SecurityFilterChain resourceServerFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(authorize -> authorize.anyRequest().hasAuthority("myrole"));
        http.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
        return http.build();
    }

    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
        return http.getSharedObject(AuthenticationManagerBuilder.class)
                .build();
    }

}

application.properties

keycloak.uri=http://localhost:8080/realms/myrealm

spring.security.oauth2.client.registration.keycloak.client-id=myclient
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=${keycloak.uri}
spring.security.oauth2.client.provider.keycloak.use-resource-role-mappings=true
spring.security.oauth2.client.provider.keycloak.user-name-attribute=preferred_username

spring.security.oauth2.resourceserver.jwt.issuer-uri=${keycloak.uri}

I test it with postman and every time I get a 403 Forbidden response. The request has the bearer token in Authorization header.

What do I miss?

2

There are 2 best solutions below

6
ch4mp On

RTFM

Also, you shouldn't need OAuth2 client dependencies and configuration properties on a REST API authorized with access tokens.

If you want something simpler that what is exposed in the doc, you can have a look at this starter I wrote:

<!-- you should have this 2 already -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

<!-- this is an additional Spring Boot starter compatible with Boot 3.2.1 -->
<dependency>
    <groupId>com.c4-soft.springaddons</groupId>
    <artifactId>spring-addons-starter-oidc</artifactId>
    <version>7.3.1</version>
</dependency>
keycloak.uri: http://localhost:8080/realms/myrealm

# spring.security.oauth2.resourceserver.jwt.* properties are ignored by spring-addons, you can remove it.

# spring.security.oauth2.client.* properties are used by spring-boot-starter-oauth2-client 
# which is of no use to secure a REST API with access tokens. You should remove it.

com:
  c4-soft:
    springaddons:
      oidc:
        ops:
        - iss: ${keycloak.uri}
          username-claim: preferred_username
          authorities:
          - path: $.realm_access.roles
            # you can add here some basic transformation like adding a "ROLE_" prefix or force role names to uppercase
          # you can add more entries with other JSON path to ressource_access (client) roles
        # resource server with nothing accessible to anonymous and no CORS configuration
        resourceserver:
          permit-all:
          cors:

If you're using method security (decorate your @Controller classes or methods with @PreAuthorize("hasAuthority('myrole')")), then all you need is:

@Configuration
@EnableMethodSecurity
public class SecurityConfig {
}

If you prefer to use AuthorizationManagerRequestMatcherRegistry in your conf, then what you need is:

@Configuration
public class SecurityConfig {
    @Bean
    ResourceServerExpressionInterceptUrlRegistryPostProcessor expressionInterceptUrlRegistryPostProcessor() {
        return (AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) -> registry
            .anyRequest().hasAuthority("myrole");
    }
}
1
Panagiotis Bellias On

Issue fixed! It just needed to add the below method in SecurityConfig to retrieve properly the user's roles from access token so I changed the configuration to this:

package eu.seafarers.config;

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.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.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 -> {

            Object client = jwt.getClaim("realm_access");

            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;
    }
}