-
-
Save joshdcollins/8392f9dc5e147efcf7f50abaeac7c69b to your computer and use it in GitHub Desktop.
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); | |
} | |
} |
Are you getting any specific errors with the above code that are throwing you off?
My requirement was I needed:
- Multi-tenancy (meaning multiple realms within KC)
- Dynamic tenants (I could not rely on having a config file in my classpath to represent each realms' configuration
- 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.
@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 :)
@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.
is it possible to have a test springboot project ?
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..