Last active
November 3, 2023 05:30
-
-
Save joshdcollins/8392f9dc5e147efcf7f50abaeac7c69b to your computer and use it in GitHub Desktop.
Full Keycloak Multitenant Configuration
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
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
@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.