Spring Boot Keycloak Multi Tenant Configuration
Asked Answered
M

2

5

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,

Morez answered 26/10, 2021 at 10:35 Comment(0)
A
6

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

    [...]
}
Astronavigation answered 26/10, 2021 at 11:27 Comment(7)
thanks for your quickly response @GeerPt i have another question/doubt. let's assume I have 10 microservices to protect. Do I have to define all the same multiple keycloak.json in each one? thanks,Morez
Are all the 10 microservices user-facing? Then yes. Otherwise: it depends how you setup backend-to-backend security.Astronavigation
Imagine that scenario ... Frontend app (app1) --> Api (app2) --> Api (app3). But the app1 can do login (request access token's) from realm_1 and realm_2. For this example app1 should be public, app2 and app3 bearer only ?Morez
Well, if you use OAuth2 access tokens between app1 and app2, app2 will also need to be able to validate tokens from both realm1 and realm2. I haven't done this, but please try it out, and ask a new question if you stumble upon an issue.Astronavigation
@Astronavigation is it really needed to cache KeycloakDeployment in concurrentMap or similar threadsafe constructs? Keycloak doc seems to be suggesting a cache in one block but does not returning anything and in other block no cache.Impersonalize
@Astronavigation Have a question over here, To identify the realm i see everywhere suggesting to take this input as route parameter or a different http header but to avoid this extra parameter, we are looking at url decoding the token and then take the issuer to identify which realm we need to validate the token too, Is this approach recommended ?Faubourg
@Geertpt dear I have configured this in my application, #76687639. What I am facing now that i have configured also jwt converter to getting the roles and authenticate using them the problem that jwt ast for jwtdecoder been therefore I have add configs for the jwt issuer in application.yml and scince I have multiple issurer url regarding each realm I can authenticate the other user from other realmsErivan
T
0

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

}

Tilt answered 24/6, 2022 at 10:33 Comment(1)
Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.Thereinafter

© 2022 - 2024 — McMap. All rights reserved.