13

I have a requirement to use two different authorization servers (two Okta instances) to validate authentication tokens coming from two different web applications inside a single Spring Boot application which is a back-end REST API layer.

Currently I have one resource server working with the following configuration:

@Configuration
@EnableWebSecurity
public class ResourceServerSecurityConfig extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(HttpSecurity http) throws Exception{
    http
      .authorizeRequests().antMatchers("/public/**").permitAll()
      .anyRequest().authenticated()
      .and()
      .oauth2ResourceServer().jwt();
  }
}
spring.security.oauth2.resourceserver.jwt.issuer-uri=https://dev-X.okta.com/oauth2/default
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://dev-X.okta.com/oauth2/default/v1/keys

and with dependencies spring-security-oauth2-resource-server and spring-security-oauth2-jose in my Spring Boot app (version 2.2.4.RELEASE)

The end state I want to get into is, depending on a custom HTTP header set in the request, I want to pick which Okta instance my Spring Boot app uses to decode and validate the JWT token.

Ideally I would have two properties in my configuration file as follows:

jwkSetUri.X=https://dev-X.okta.com/oauth2/default/v1/keys
jwtIssuerUri.X=https://dev-X.okta.com/oauth2/default

jwkSetUri.Y=https://dev-Y.okta.com/oauth2/default/v1/keys
jwtIssuerUri.Y=https://dev-Y.okta.com/oauth2/default

I should be able to use a RequestHeaderRequestMatcher to match the header value in the security configuration. What I cannot workout is how to use two different oauth2ResourceServer instances that goes with the security configuration.

sanjayav
  • 4,896
  • 9
  • 32
  • 30

3 Answers3

12

With spring boot this is not possible to do out of the box right now. Spring Security 5.3 provides functionality to do this (spring boot 2.2.6 still doesn't support spring security 5.3). Please see following issues:

https://github.com/spring-projects/spring-security/issues/7857
https://github.com/spring-projects/spring-security/pull/7887

It is possible to do manual configuration of resource server to use multiple identity providers, by following links that i have provided. Provided links are mainly for spring boot webflux development. For basic spring boot web development please see this video:

https://www.youtube.com/watch?v=ke13w8nab-k

Norbert Dopjera
  • 741
  • 5
  • 18
  • Can you please help explain how this can be achieved by Spring-boot 2.3 that supports the above Spring Security 5.3? – Suresh T Dec 15 '20 at 20:26
  • https://docs.spring.io/spring-security/site/docs/5.3.0.RELEASE/reference/html5/#webflux-oauth2resourceserver-multitenancy just wait few secs when visiting url, it will jump directly to section with multiple identity providers. While it's easy to do, you may still want to use manual configuration to bring custom behavior (i am still manually configuring it in spring boot 2.4). – Norbert Dopjera Dec 16 '20 at 00:27
  • Tickets are already closed, so most likely it's possible now – Ievgen May 12 '23 at 05:41
8

This is possible as of Spring security 5.3+ using the JwtIssuerAuthenticationManagerResolver object

Override the configure(HttpSecurity http) inside your configuration class which extends WebSecurityConfigurerAdapter

JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver(
            "http://localhost:8080/auth/realms/SpringBootKeyClock",
            "https://accounts.google.com/o/oauth2/auth",
            "https://<subdomain>.okta.com/oauth2/default"
    );

http.cors()
            .and()
            .authorizeRequests()
            .antMatchers(HttpMethod.GET, "/user/info", "/api/foos/**")
            .hasAnyAuthority("SCOPE_email")
            .antMatchers(HttpMethod.POST, "/api/foos")
            .hasAuthority("SCOPE_profile")
            .anyRequest()
            .authenticated()
            .and()
            .oauth2ResourceServer(oauth2 -> oauth2.authenticationManagerResolver(authenticationManagerResolver));
emilpmp
  • 1,716
  • 17
  • 32
  • This looks useful. Can some one answer to this question too which is regarding change the spring security configuration at runtime and lazy loading security configuration at runtime https://stackoverflow.com/questions/68831257/lazy-initialise-spring-security-at-runtime-reload-spring-security-configuratio – ARods Aug 19 '21 at 07:31
  • 2
    I know this is a year old BUT I'm wondering how can I use the above way for multiple issuers and validate the audience with JwtDecoder jwtDecoder() when there is more than one issuer? thanks – MetaCoder Sep 13 '21 at 22:40
  • This answer saved my time, thanks. – Rasool Ghafari Nov 09 '21 at 18:21
0

Step 1: Create a custom AuthenticationManagerResolver

    @Component
    public class TenantAuthenticationManagerResolver implements
        AuthenticationManagerResolver<HttpServletRequest> {private final Map<String, String> tenants;
      private final Map<String, AuthenticationManager> authenticationManagers = new HashMap<>();
    
      private final BearerTokenResolver resolver = new DefaultBearerTokenResolver();
    
      public TenantAuthenticationManagerResolver() {
        this.tenants = new HashMap<>();
      }
    
      public TenantAuthenticationManagerResolver(String tenantIds, String jwkUris) {
        List<String> tenantList = Arrays.asList(tenantIds.split(","));
        List<String> issuerList = Arrays.asList(jwkUris.split(","));
    
        this.tenants = IntStream.range(0, Math.min(tenantList.size(), issuerList.size()))
            .boxed()
            .collect(Collectors.toMap(tenantList::get, issuerList::get));
      }
    
      @Override
      public AuthenticationManager resolve(HttpServletRequest request) {
        return this.authenticationManagers.computeIfAbsent(toTenant(request), this::fromTenant);
      }
    
      private String toTenant(HttpServletRequest request) {
        String token = this.resolver.resolve(request);
        try {
          return (String) JWTParser.parse(token).getJWTClaimsSet().getClaim(TENANT_ID);
        } catch (Exception e) {
          throw new IllegalArgumentException(e);
        }
      }
    
      private AuthenticationManager fromTenant(String tenant) {
        return Optional.ofNullable(this.tenants.get(tenant))
            .map(issuer -> NimbusJwtDecoder.withJwkSetUri(issuer).build())
            .map(JwtAuthenticationProvider::new)
            .orElseThrow(() -> new IllegalArgumentException("unknown tenant"))::authenticate;
      }
    
    }

Step 2: Do the following in your security configuration

  @Value("${issuer.jwkUris}")
  private String jwkUris;

  @Value("${tenantId.tenants}")
  private String tenantIds;

  @Bean
  @Qualifier("tenantIds")
  public String tenantIds() {
    return tenantIds;
  }

  @Bean
  @Qualifier("jwkUris")
  public String jwkUris() {
    return jwkUris;
  }

  @Bean
  public TenantAuthenticationManagerResolver authenticationManagerResolver() {
    return new TenantAuthenticationManagerResolver(tenantIds(), jwkUris());
  }

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
        .exceptionHandling(exceptionHandling -> exceptionHandling.authenticationEntryPoint(
            restAuthenticationEntryPoint()))
        .oauth2ResourceServer(
            oauth2ResourceServer -> oauth2ResourceServer.authenticationManagerResolver(authenticationManagerResolver()));
    return http.build();
  }