Skip to content

Instantly share code, notes, and snippets.

@joshdcollins
Last active November 3, 2023 05:30
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save joshdcollins/8392f9dc5e147efcf7f50abaeac7c69b to your computer and use it in GitHub Desktop.
Save joshdcollins/8392f9dc5e147efcf7f50abaeac7c69b to your computer and use it in GitHub Desktop.
Full Keycloak Multitenant Configuration
package commons.keycloakauth.security;
import java.text.ParseException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import com.nimbusds.jwt.JWT;
import com.nimbusds.jwt.JWTParser;
import admin.auth.AdminAuthService;
import commons.exceptions.ApplicationException;
import commons.exceptions.ErrorType;
import commons.keycloakauth.config.KeycloakAuthConfiguration;
import admin.resources.ClientApplicationResource;
import admin.resources.TenantResource;
import org.keycloak.adapters.KeycloakConfigResolver;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.OIDCHttpFacade;
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.keycloak.representations.adapters.config.AdapterConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Component
public class MultitenantConfigResolver extends KeycloakSpringBootConfigResolver implements KeycloakConfigResolver {
Logger logger = LoggerFactory.getLogger(MultitenantConfigResolver.class);
private final Map<String, KeycloakDeployment> cache = new ConcurrentHashMap<>();
private final AdminAuthService adminAuthService;
private final KeycloakAuthConfiguration authConfiguration;
private static AdapterConfig adapterConfig;
public static final String AUTH_HEADER = "Authorization";
public static final String TOKEN_PREFIX = "bearer";
public static final String REALMS_PATH = "realms/";
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
public MultitenantConfigResolver(AdminAuthService adminAuthService, KeycloakAuthConfiguration authConfiguration) {
this.adminAuthService = adminAuthService;
this.authConfiguration = authConfiguration;
}
@Override
public KeycloakDeployment resolve(OIDCHttpFacade.Request request) {
// The realm is retrieved from the issuer URL in the JWT
String realm;
try {
// Retrieve auth header
String authHeader = request.getHeader(AUTH_HEADER);
if (authHeader == null) {
logger.warn("No Auth header provided");
return null;
}
// Retrieve bearer token value from header
String token = extractTokenFromHeader(authHeader);
// Parse token value to JWT
JWT jwt = JWTParser.parse(token);
// Retrieve tenant value from issuer value. Ex: http://localhost:8180/auth/realms/master, master would be the realm/tenant
String issuerString = jwt.getJWTClaimsSet().getIssuer();
// This resolver can be used in two modes:
// - Local -- directly within the Admin service
// - Remote -- from another service (bridge, etc.)
// For remote use cases, a Feign call will be made via the RemoteAdminAuthService, setting this token
// allows the user's provided token to be used to make that call
adminAuthService.setToken(token);
if (issuerString != null && issuerString.contains(REALMS_PATH)) {
realm = issuerString.substring(issuerString.lastIndexOf("/")+1);
} else {
throw new ParseException("Invalid issuer in token", 0);
}
} catch (ParseException ex) {
throw new IllegalStateException("Invalid JWT provided");
}
// In cases of unauthenticated
if (realm == null) return null;
KeycloakDeployment deployment = cache.get(realm + "_" + this.authConfiguration.getClientName());
if (null == deployment) {
try {
TenantResource tenant = adminAuthService.getTenantByName(realm);
if (tenant == null) {
throw new IllegalStateException("Unable to find tenant [" + realm + "]");
}
ClientApplicationResource clientApplication = adminAuthService.getClientApplicationByTenantIdAndName(tenant.getId(), authConfiguration.getClientName());
if (clientApplication == null) {
throw new IllegalStateException("Unable to find client application [" + authConfiguration.getClientName() + "] in tenant [" + realm + "]");
}
AdapterConfig ac = new AdapterConfig();
ac.setRealm(realm);
ac.setResource(authConfiguration.getClientName());
ac.setAuthServerUrl(authConfiguration.getBaseUrl());
ac.setSslRequired("external");
ac.setBearerOnly(true);
ac.setAllowAnyHostname(true);
Map<String, Object> credentialsMap = new HashMap<>();
credentialsMap.put("secret", clientApplication.getClientSecret());
ac.setCredentials(credentialsMap);
deployment = KeycloakDeploymentBuilder.build(ac);
cache.put(realm + "_" + authConfiguration.getClientName(), deployment);
}
catch (Exception ex) {
throw new ApplicationException(ErrorType.TOKEN_VALIDATION_ERROR, ex);
}
}
return deployment;
}
public static void setAdapterConfig(AdapterConfig adapterConfig) {
MultitenantConfigResolver.adapterConfig = adapterConfig;
}
private static String extractTokenFromHeader(String authHeader) throws ParseException {
if (authHeader.toLowerCase().startsWith(TOKEN_PREFIX)) {
return authHeader.substring(TOKEN_PREFIX.length()+1);
} else {
throw new ParseException("Invalid Auth Header. Missing Bearer prefix", 0);
}
}
}
package bridge.security;
import admin.auth.AdminAuthService;
import commons.keycloakauth.config.KeycloakAuthConfiguration;
import commons.keycloakauth.security.MultitenantConfigResolver;
import org.keycloak.adapters.KeycloakConfigResolver;
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.keycloak.adapters.springsecurity.KeycloakSecurityComponents;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.keycloak.adapters.springsecurity.filter.KeycloakPreAuthActionsFilter;
import org.keycloak.adapters.springsecurity.filter.KeycloakSecurityContextRequestFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
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.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter;
@Configuration
@EnableWebSecurity
@ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
private final AdminAuthService adminAuthService;
private final KeycloakAuthConfiguration authConfiguration;
@Autowired
public SecurityConfig(AdminAuthService adminAuthService, KeycloakAuthConfiguration authConfiguration) {
this.adminAuthService = adminAuthService;
this.authConfiguration = authConfiguration;
}
/**
* Registers the KeycloakAuthenticationProvider with the authentication manager.
*/
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(keycloakAuthenticationProvider);
}
/**
* Sets keycloaks config resolver to use springs application.properties instead of keycloak.json (which is standard)
* @return KeycloakConfigResolver
*/
@Bean
public KeycloakConfigResolver KeycloakConfigResolver() {
return new MultitenantConfigResolver(adminAuthService, authConfiguration);
}
/**
* Defines the session authentication strategy.
*/
@Bean
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new NullAuthenticatedSessionStrategy();
}
@Override
protected void configure(HttpSecurity http) throws Exception
{
super.configure(http);
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.anyRequest().authenticated();
}
@Bean
public FilterRegistrationBean keycloakPreAuthActionsFilterRegistrationBean(KeycloakPreAuthActionsFilter filter) {
FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
registrationBean.setEnabled(false);
return registrationBean;
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().mvcMatchers(HttpMethod.OPTIONS, "/**");
// ignore swagger
web.ignoring().mvcMatchers("/swagger-ui.html/**", "/configuration/**", "/swagger-resources/**", "/api-docs", "/webjars/**");
}
@Primary
@Bean
public KeycloakSpringBootConfigResolver getConfigResolver() {
return new MultitenantConfigResolver(adminAuthService, authConfiguration);
}
}
@vbiginner
Copy link

Hi @joshdcollins.. We have a similar requirement of using different realm at runtime (Using Keycloak with Spring Boot). I am struggling to get it done and yours is the only appropriate answer available on internet. Can you please provide any help or the sample code where the multi tenancy is achieved with spring boot while using Keycloak config in application properties. Thanks in advance..

@joshdcollins
Copy link
Author

joshdcollins commented Jul 18, 2020

@vbiginner

Are you getting any specific errors with the above code that are throwing you off?

My requirement was I needed:

  1. Multi-tenancy (meaning multiple realms within KC)
  2. Dynamic tenants (I could not rely on having a config file in my classpath to represent each realms' configuration
  3. This needed to work in a microservices environment where one of the microservices was responsible for maintaining tenants and their references to realms in KC (along with references to clients managed in KC)

This gist is a partial solution which does not include the Auth service which is referenced at https://gist.github.com/joshdcollins/8392f9dc5e147efcf7f50abaeac7c69b#file-multitenantconfigresolver-java-L69, https://gist.github.com/joshdcollins/8392f9dc5e147efcf7f50abaeac7c69b#file-multitenantconfigresolver-java-L86, and https://gist.github.com/joshdcollins/8392f9dc5e147efcf7f50abaeac7c69b#file-multitenantconfigresolver-java-L90.

I cannot share that code, but you can generally infer what it is doing. It is reaching out to a service and returning the client id / client secret for the client that is representing the microservice using this code.

Once you have that information, you can trivially create an AdapterConfig and then a KeycloakDeployment from that.

@vbiginner
Copy link

@joshdcollins.. Thank you so much for the quick reply..

I was trying to refer your code and thinking around the Auth service.. Thanks for explanation about your use case and sharing the gist. It made me clear what to do.
My requirement is kind of similar - the Spring Boot app should be able to decide which realm to use in config based on the request it receives(extracting realm from the token and so on).
I will try to get this work.. thanks for your help :)

@Ristop
Copy link

Ristop commented Jan 5, 2022

@joshdcollins Did you find an answer for https://keycloak.discourse.group/t/keycloak-multi-tenancy-spring-security-configuration/2571. I'm running into a similar issue and had implemented a similar solution for unauthenticated requests. But creating an unconfigured deployment seems hackish.

@R3n3r0
Copy link

R3n3r0 commented Jun 5, 2023

is it possible to have a test springboot project ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment