blog.broncotoxique.com

Juste another geek’s website

Spring Boot 3 – multitenant OIDC with Keycloak

Naturally Spring Security 3 allow you to do some kind of multi-tenant autentication, but you can’t use more than one OIDC tenant.

Let say you are building a Spring Boot App with Spring-Boot 3.x.x and you use Keycloak as OpenID Connect provider, ans you need to validate some tokens from more than one OIDC tenant. Here is an example of how to setup multi-tenant security config.

You’ll see this isn’t an obvious copy of the Spring Security manual. In this example you’ll see how to build a Jwt auth security configuration open to use more than one IDP tenant, and how to choose the name (‘preferred_username’) of the Principal.

First add those dependencies to your Spring project :

<dependency>
      <groupId>com.cacib.loanscape.security</groupId>
      <artifactId>loanscape-security-starter</artifactId>
</dependency>
<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

Add the security config class :

import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoders;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
import org.springframework.security.oauth2.server.resource.authentication.JwtIssuerAuthenticationManagerResolver;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity(debug = true)
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class OIDCSecurityConf {

  // The list of IDP you want to use. Obviously you can make it come from a parameter
  List<String> issuers = List.of("https://[keycloak URL]/auth/realms/[REALM]");

  // authenticationManagers is a Map of authentication manager, one per issuer
  Map<String, AuthenticationManager> authenticationManagers = new HashMap<>();

  // Provide the issuer resolver a reference on the method it's should use to get the authentication manager from authentication manages map
  JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver(authenticationManagers::get);

  // https://docs.spring.io/spring-security/reference/reactive/oauth2/resource-server/jwt.html
  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    // Build the differents Oauth issuers (tenant)
    issuers.stream().forEach(issuerConsumer);
    // An example of authorized request.
    http.authorizeHttpRequests().requestMatchers("/swagger-ui/**").permitAll();
    // Set authentication mandatory for all the remaining requests
    http.authorizeHttpRequests(auth -> auth.anyRequest().authenticated()).oauth2ResourceServer(oauth2ResourceServerCustomizer).cors(Customizer.withDefaults());
    return http.build();
  }

  Customizer<OAuth2ResourceServerConfigurer<HttpSecurity>> oauth2ResourceServerCustomizer = oauth2 -> oauth2.authenticationManagerResolver(authenticationManagerResolver);

  // Consumer that transform an URL String into an org.springframework.security.core.Authentication and fill the authenticationManagers
  Consumer<String> issuerConsumer = issuer -> {
    JwtAuthenticationProvider authenticationProvider = new JwtAuthenticationProvider(JwtDecoders.fromOidcIssuerLocation(issuer));
    authenticationProvider.setJwtAuthenticationConverter(grantedAuthoritiesExtractor());
    authenticationManagers.put(issuer, authenticationProvider::authenticate);
  };

  private Converter<Jwt, AbstractAuthenticationToken> grantedAuthoritiesExtractor() {
    JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
    jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new GrantedAuthoritiesExtractor());
    jwtAuthenticationConverter.setPrincipalClaimName("preferred_username");
    return jwtAuthenticationConverter;
  }

  private class GrantedAuthoritiesExtractor implements Converter<Jwt, Collection<GrantedAuthority>> {
    @Override
    public Collection<GrantedAuthority> convert(Jwt jwt) {
      Map<String, Object> claims = jwt.getClaims();
      Map<String, Object> realmAccess = (Map<String, Object>) claims.getOrDefault("realm_access", Map.of());
      List<String> roles = (List<String>) realmAccess.getOrDefault("roles", List.of());
      return roles.stream().map(SimpleGrantedAuthority::new).collect(toCollection(ArrayList::new));
    }
  }
}

Posted

in

by

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *