6

I'm using Keycloak as my OAuth2 Authorization Server and I configured an OAuth2 Resource Server for Multitenancy following this official example on GitHub. The current Tenant is resolved considering the Issuer field of the JWT token. Hence the token is verified against the JWKS exposed at the corresponding OpenID Connect well known endpoint.

This is my Security Configuration:

@EnableWebSecurity
@RequiredArgsConstructor
@EnableAutoConfiguration(exclude = UserDetailsServiceAutoConfiguration.class)
public class OrganizationSecurityConfiguration extends WebSecurityConfigurerAdapter {

    private final TenantService tenantService;
    private List<Tenant> tenants;

    @PostConstruct
    public void init() {
        this.tenants = this.tenantService.findAllWithRelationships();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests().anyRequest().authenticated()
                .and()
                .oauth2ResourceServer()
                .authenticationManagerResolver(new MultiTenantAuthenticationManagerResolver(this.tenants));
    }
}

and this is my custom AuthenticationManagerResolver:

public class MultiTenantAuthenticationManagerResolver implements AuthenticationManagerResolver<HttpServletRequest> {

    private final AuthenticationManagerResolver<HttpServletRequest> resolver;

    private List<Tenant> tenants;

    public MultiTenantAuthenticationManagerResolver(List<Tenant> tenants) {
        this.tenants = tenants;

        List<String> trustedIssuers = this.tenants.stream()
                .map(Tenant::getIssuers)
                .flatMap(urls -> urls.stream().map(URL::toString))
                .collect(Collectors.toList());

        this.resolver = new JwtIssuerAuthenticationManagerResolver(trustedIssuers);
    }

    @Override
    public AuthenticationManager resolve(HttpServletRequest context) {
        return this.resolver.resolve(context);
    }
}

Now, because of the design of org.springframework.security.oauth2.server.resource.authentication.JwtIssuerAuthenticationManagerResolver.TrustedIssuerJwtAuthenticationManagerResolver which is private, the only way I can think in order to extract a custom principal is to reimplement everything that follows:

  • TrustedIssuerJwtAuthenticationManagerResolver
  • the returned AuthenticationManager
  • the AuthenticationConverter
  • the CustomAuthenticationToken which extends JwtAuthenticationToken
  • the CustomPrincipal

To me it seems a lot of Reinventing the wheel, where my only need would be to have a custom Principal.

The examples that I found don't seem to suit my case since they refer to OAuth2Client or are not tought for Multitenancy.

Do I really need to reimplement all such classes/interfaes or is there a smarter approach?

1Z10
  • 2,801
  • 7
  • 33
  • 82

1 Answers1

1

This is how I did it, without reimplementing a huge amount of classes. This is without using a JwtAuthenticationToken however.

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

  ...

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    
    http
      ...
      .oauth2ResourceServer(oauth2 -> oauth2.authenticationManagerResolver(authenticationManagerResolver()));
  }

  @Bean
  JwtIssuerAuthenticationManagerResolver authenticationManagerResolver() {

    List<String> issuers = ... // get this from list of tennants or config, whatever
    Predicate<String> trustedIssuer = issuers::contains;
    Map<String, AuthenticationManager> authenticationManagers = new ConcurrentHashMap<>();

    AuthenticationManagerResolver<String> resolver = (String issuer) -> {
      if (trustedIssuer.test(issuer)) {
        return authenticationManagers.computeIfAbsent(issuer, k -> {
          var jwtDecoder = JwtDecoders.fromIssuerLocation(issuer);
          var provider = new JwtAuthenticationProvider(jwtDecoder);
          provider.setJwtAuthenticationConverter(jwtAuthenticationService::loadUserByJwt);
          return provider::authenticate;
        });
      }
      return null;
    };
    
    return new JwtIssuerAuthenticationManagerResolver(resolver);
  }
}
@Service
public class JwtAuthenticationService {

  public AbstractAuthenticationToken loadUserByJwt(Jwt jwt) {
    
    UserDetails userDetails = ... // or your choice of principal
    List<GrantedAuthority> authorities = ... // extract from jwt or db
    ...
    return new UsernamePasswordAuthenticationToken(userDetails, null, authorities);
  }
}
kvbx
  • 380
  • 2
  • 11