Spring Boot Keycloak Multi Tenant Configuration

Viewed 2785

I have a Keycloak instance and created two realms and one user for each realm.

Realm1 (Tenant1) -> User 1
Realm2 (Tenant2) -> User 2

And i have my spring boot application.yml (resource server - API) for one specific realm and fixed in my code.

keycloak:
  realm: Realm1
  auth-server-url: https://localhost:8443/auth
  ssl-required: external
  resource: app
  bearer-only: true
  use-resource-role-mappings: true

It's working and validate for Realm1.

but now i can receive requests from user2 (tenant2) and the token will not be valid because the public key (realm1) is not valid for the signed request jwt token (realm2).

What is the best way to allow multi tenancy and dynamically configuration for multi realms?

thanks,

2 Answers

There's a whole chapter on it: 2.1.18: Multi-Tenanacy

Instead of defining the keycloak config in spring application.yaml, keep multiple keycloak.json config files, and use a custom KeycloakConfigResolver:

public class PathBasedKeycloakConfigResolver implements KeycloakConfigResolver {

    @Override
    public KeycloakDeployment resolve(OIDCHttpFacade.Request request) {
        if (request.getPath().startsWith("alternative")) { // or some other criteria 
            InputStream is = getClass().getResourceAsStream("/tenant1-keycloak.json");
            return KeycloakDeploymentBuilder.build(is); //TODO: cache result
        } else {
            InputStream is = getClass().getResourceAsStream("/default-keycloak.json");
            return KeycloakDeploymentBuilder.build(is); //TODO: cache result
        }
    }    
}

I'm not sure if this works well with the keycloak-spring-boot-starter, but I think it's enough to just wire your custom KeycloakConfigResolver in the KeycloakWebSecurityConfigurerAdapter:

@Configuration
@EnableWebSecurity
class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {

    @Bean
    public KeycloakConfigResolver keycloakConfigResolver() {
        return new PathBasedKeycloakConfigResolver();
    }

    [...]
}

import org.keycloak.adapters.springsecurity.KeycloakConfiguration;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.DependsOn;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;

@DependsOn("keycloakConfigResolver")
@KeycloakConfiguration
@EnableGlobalMethodSecurity(jsr250Enabled = true)
@ConditionalOnProperty(name = "keycloak.enabled", havingValue = "true", matchIfMissing = true)
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
    /**
     * Registers the KeycloakAuthenticationProvider with the authentication manager.
     */
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        KeycloakAuthenticationProvider authenticationProvider = new KeycloakAuthenticationProvider();
        authenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
        auth.authenticationProvider(authenticationProvider);
    }

    /**
     * Defines the session authentication strategy.
     */
    @Bean
    @Override
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
        http
                .cors()
                .and()
                .authorizeRequests().antMatchers(HttpMethod.OPTIONS)
                .permitAll()
                .antMatchers("/api-docs/**", "/configuration/ui",
                        "/swagger-resources/**", "/configuration/**", "/v2/api-docs",
                        "/swagger-ui.html/**", "/webjars/**", "/swagger-ui/**")
                .permitAll()
                .anyRequest().authenticated();
    }
}

import org.keycloak.adapters.KeycloakConfigResolver;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.OIDCHttpFacade;
import java.io.InputStream;
import java.util.concurrent.ConcurrentHashMap;


public class PathBasedConfigResolver implements KeycloakConfigResolver {

    private final ConcurrentHashMap<String, KeycloakDeployment> cache = new ConcurrentHashMap<>();

    @Override
    public KeycloakDeployment resolve(OIDCHttpFacade.Request request) {

        String path = request.getURI();
        String realm = "realmName";
        if (!cache.containsKey(realm)) {
            InputStream is = getClass().getResourceAsStream("/" + realm + "-keycloak.json");
            cache.put(realm, KeycloakDeploymentBuilder.build(is));
        }
        return cache.get(realm);
    }

}


import org.keycloak.adapters.KeycloakConfigResolver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.context.annotation.Bean;

@SpringBootApplication()
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(DIVMasterApplication.class, args);
    }

    @Bean
    @ConditionalOnMissingBean(PathBasedConfigResolver.class)
    public KeycloakConfigResolver keycloakConfigResolver() {
        return new PathBasedConfigResolver();
    }

}

Related