Jazzschmidt
Jazzschmidt

Reputation: 1105

Integrating Spring Security Global Method Security with Keycloak

I have issues with using the Pre/Post Authorization Annotations from Spring Security and the Servlet API with Keycloak integration. I investigated a lot of articles, tutorials and the following questions without further luck:

All I want is removing the ROLES_ prefix, use hierarchical roles and a comfortable way to retrieve the users' roles.

As of now, I am able to retrieve a hierarchical role like this in a Controller but cannot use the annotations:

@Controller
class HomeController {

    @Autowired
    AccessToken token

    @GetMapping('/')
    def home(Authentication auth, HttpServletRequest request) {
        // Role 'admin' is defined in Keycloak for this application
        assert token.getResourceAccess('my-app').roles == ['admin']
        // All effective roles are mapped
        assert auth.authorities.collect { it.authority }.containsAll(['admin', 'author', 'user'])

        // (!) But this won't work:
        assert request.isUserInRole('admin')
    }

    // (!) Leads to a 403: Forbidden
    @GetMapping('/sec')
    @PreAuthorize("hasRole('admin')") {
        return "Hello World"
    }

}

I am guessing that the @PreAuthorize annotation does not work, because that Servlet method is not successful.

There are only three roles - admin, author, user - defined in Keycloak and Spring:

enum Role {
    USER('user'),
    AUTHOR('author'),
    ADMIN('admin')

    final String id

    Role(String id) {
        this.id = id
    }

    @Override
    String toString() {
        id
    }
}

Keycloak Configuration

Upon removing the @EnableGlobalMethodSecurity annotation from this Web Security reveals an Error creating bean with name 'resourceHandlerMapping' caused by a No ServletContext set error - no clue, where that comes from!

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {

    /**
     * Registers the KeycloakAuthenticationProvider with the authentication manager.
     */
    @Autowired
    void configureGlobal(AuthenticationManagerBuilder auth) {
        auth.authenticationProvider(keycloakAuthenticationProvider().tap { provider ->
            // Assigns the Roles via Keycloaks role mapping
            provider.grantedAuthoritiesMapper = userAuthoritiesMapper
        })
    }

    @Bean
    RoleHierarchyImpl getRoleHierarchy() {
        new RoleHierarchyImpl().tap {
            hierarchy = "$Role.ADMIN > $Role.AUTHOR > $Role.USER"
        }
    }

    @Bean
    GrantedAuthoritiesMapper getUserAuthoritiesMapper() {
        new RoleHierarchyAuthoritiesMapper(roleHierarchy)
    }

    SecurityExpressionHandler<FilterInvocation> expressionHandler() {
        // Removes the prefix
        new DefaultWebSecurityExpressionHandler().tap {
            roleHierarchy = roleHierarchy
            defaultRolePrefix = null
        }
    }

    // ...

    @Bean
    @Scope(scopeName = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
    AccessToken accessToken() {
        def request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest()
        def authToken = (KeycloakAuthenticationToken) request.userPrincipal
        def securityContext = (KeycloakSecurityContext) authToken.credentials

        return securityContext.token
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http)
        http
            .authorizeRequests()
            .expressionHandler(expressionHandler())
            // ...
    }

}

Global Method Security Configuration

I needed to explicitly allow allow-bean-definition-overriding, because otherwise I got a bean with that name already defined error, which reveals that I completely lost control over this whole situation and don't know what's goin on.

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
class GlobalMethodSecurityConfig extends GlobalMethodSecurityConfiguration {

    @Autowired
    RoleHierarchy roleHierarchy

    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        ((DefaultMethodSecurityExpressionHandler)super.createExpressionHandler()).tap {
            roleHierarchy = roleHierarchy
            defaultRolePrefix = null
        }
    }
}

Any further configurations that could be important? Thanks a lot for your help!

Upvotes: 2

Views: 2768

Answers (2)

Arun Chaudhary
Arun Chaudhary

Reputation: 275

Apart from suggestions provided in (docs.spring.io) Disable ROLE_ Prefixing, and suggestion provided by M. Deinum, one more modification is needed while using KeycloakWebSecurityConfigurerAdapter.

In configureGlobal method, grantedAuthoritiesMapper bean is set in the bean keycloakAuthenticationProvider. And in grantedAuthoritiesMapper, prefix can be set to anything you want, where the default value is "ROLE_".

The code goes as follows:

    @Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();

    SimpleAuthorityMapper grantedAuthoritiesMapper = new SimpleAuthorityMapper();
    grantedAuthoritiesMapper.setPrefix("");
    keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(grantedAuthoritiesMapper);
    auth.authenticationProvider(keycloakAuthenticationProvider);
}

This solution works for me.

Upvotes: 1

Jazzschmidt
Jazzschmidt

Reputation: 1105

As M. Deinum pointed out, one must remove the defaultRolePrefix in multiple places with a BeanPostProcessor, which is explained in (docs.spring.io) Disable ROLE_ Prefixing.

This approach seemed not very clean to me and so I wrote a custom AuthoritiesMapper to achieve mapping hierarchical roles from Keycloak without the need to rename them to the ROLE_ Spring standard. First of all, the Roles enumeration was modified to conform that standard inside the application scope:

enum Role {
    USER('ROLE_USER'),
    AUTHOR('ROLE_AUTHOR'),
    ADMIN('ROLE_ADMIN')

    // ...
}

Secondly, I replaced the RoleHierarchyAuthoritiesMapper with a prefixing hierarchical implementation:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {

    // ..

    // Replaces the RoleHierarchyAuthoritiesMapper
    @Bean
    GrantedAuthoritiesMapper getUserAuthoritiesMapper() {
        new PrefixingRoleHierarchyAuthoritiesMapper(roleHierarchy)
    }

}
class PrefixingRoleHierarchyAuthoritiesMapper extends RoleHierarchyAuthoritiesMapper {

        String prefix = 'ROLE_'

        PrefixingRoleHierarchyAuthoritiesMapper(RoleHierarchy roleHierarchy) {
            super(roleHierarchy)
        }

        @Override
        Collection<? extends GrantedAuthority> mapAuthorities(Collection<? extends GrantedAuthority> authorities) {
            def prefixedAuthorities = authorities.collect { GrantedAuthority originalAuthority ->
                new GrantedAuthority() {
                    String authority = "${prefix}${originalAuthority.authority}".toUpperCase()
                }
            }

            super.mapAuthorities(prefixedAuthorities)
        }
    }

And lastly, I got rid of the GlobalMethodSecurityConfig.

Upvotes: 1

Related Questions