Keycloak + Multi-tenancy + Spring Security configuration

Hi all,

I’m attempting to use Keycloak to support multi-tenancy within a SpringBoot 2 application using Spring Security.

I’m using

org.keycloak
keycloak-spring-boot-starter
9.0.0


org.springframework.boot
spring-boot-starter-security
2.2.5.RELEASE

To enable multitenancy I have a MultitenantConfigResolver class which extends KeycloakSpringBootConfigResolver. MultitenantConfigResolver is loaded via my SecurityConfig which extends KeycloakWebSEcurityConfigurerAdapter.

This config resolver largely works as I expect. The target tenant is inferred from the issuer of the provided JWT on the Request. Then an AdapterConfig is built and passed into the KeycloakDeploymentBuilder.build(adapterConfig);

A key point I’m unsure is correct – when an unauthenticated request is made (no Authorization header), the resolve method of the MultitenantConfigResolver returns null.

I have certain endpoints which I want to be publicly accessible. Those are all prefixed by /public.

In SecurityConfig this is my HttpSecurity configuration:

@Override
protected void configure(HttpSecurity http) throws Exception
{
    super.configure(http);
    http
            .csrf().disable()
            .authorizeRequests()
            .antMatchers("/public/**").permitAll()
            .anyRequest().authenticated();
}

I’m finding that even when calling an endpoint starting with /public/ my MultitenantConfigResolver is still being called, and the endpoint is not called.

Of a lower priority, in this case instead of returning a 401, I get an empty 200, I’m not sure where to coerce the appropriate status code.

Complete code for both MultitenantConfigResolver and HttpSecurity are here: https://gist.github.com/joshdcollins/8392f9dc5e147efcf7f50abaeac7c69b

Not sure if this is an appropriate fix, but I toyed with returning an empty KeycloakDeployment when no token was provided.

        if (authHeader == null) {
            logger.warn("No Auth header provided");
            return new KeycloakDeployment();
        }

This kept the filter chain from short circuiting but was getting a bad response in Postman. After enabling debug logging, it became clear that Keycloak was attempting to redirect to a /sso/login URL. To avoid this, I set bearerOnly = true on the empty deployment.

        if (authHeader == null) {
            logger.warn("No Auth header provided");
            KeycloakDeployment unConfiguredDeployment = new KeycloakDeployment();
            unConfiguredDeployment.setBearerOnly(true);
            return unConfiguredDeployment;
        }

This appears to meet my 3 use cases:

  1. Anything in /public/** should be accessible without a token
  2. Anything not in /public should return a 401 without a token
  3. If I have a token, I should be able to process an authenticated endpoint as expected

could you please share the whole code ? Please