Skip to content

Instantly share code, notes, and snippets.

@joshdcollins
Last active November 3, 2023 05:30
Show Gist options
  • 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);
}
}
@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