Error encountered implementing OAuth 2.0 Resource Server multi-tenancy

Viewed 1177

I have implemented code as specified here to add the multi-tenancy by issuer feature to my Spring Security configuration. However, when my Spring Boot application starts, I encounter the following error:

2021-10-26 | 10:31:37.762 | main | WARN | ConfigServletWebServerApplicationContext | Trace: | Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'springSecurityFilterChain' defined in class path resource [org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [javax.servlet.Filter]: Factory method 'springSecurityFilterChain' threw exception; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.security.oauth2.jwt.JwtDecoder' available
2021-10-26 | 10:31:39.361 | main | ERROR | o.s.b.d.LoggingFailureAnalysisReporter | Trace: |
***************************
APPLICATION FAILED TO START
***************************
Description:
Method springSecurityFilterChain in org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration required a bean of type 'org.springframework.security.oauth2.jwt.JwtDecoder' that could not be found.
Action:
Consider defining a bean of type 'org.springframework.security.oauth2.jwt.JwtDecoder' in your configuration.

The documentation states:

This is nice because the issuer endpoints are loaded lazily. In fact, the corresponding JwtAuthenticationProvider is instantiated only when the first request with the corresponding issuer is sent.

I wouldn't think that at application startup a JwtDecoder would be expected to be already instantiated according to this documentation. What am I missing in my configuration?

Update

After Steve Riesenberg's help, I have the following code compiling now. You can see in my code snippet what I used to have working (i.e., before we had the multi-tenant requirement) is now commented out:

//.jwt().jwtAuthenticationConverter(jwtAccessTokenConverter);

String[] issuers = new String[] {"https://www.example.com/auth/realms/example"};
JwtIssuerAuthenticationManagerResolver jwtIssuerAuthenticationManagerResolver =
    new JwtIssuerAuthenticationManagerResolver(issuers);
...
        .anyRequest()
        .authenticated()
        .and()
        .oauth2ResourceServer(
            oauth2ResourceServerConfigurer ->
                oauth2ResourceServerConfigurer
                    .authenticationManagerResolver(jwtIssuerAuthenticationManagerResolver)
                    .authenticationEntryPoint(authenticationExceptionHandler));
    // .jwt().jwtAuthenticationConverter(jwtAccessTokenConverter);

However, without the ability now to supply my own token converter since I had to remove .jwt(), I'm still unclear on what the default converter provides me.

Also, I'm not clear why I need to use the third constructor of JwtIssuerAuthenticationManagerResolver and provide my own AuthenticationManagerResolver<String>? If my code above is compiling, why do I need to do this?

1 Answers

The JwtDecoder is required if you've configured the resource server with a JwtAuthenticationProvider (because it requires a specific JwtDecoder). This would happen if you do for example:

http
    ...
    .oauth2ResourceServer(oauth2 -> oauth2
        .authenticationManagerResolver(authenticationManagerResolver)
        .jwt(Customizer.withDefaults())
    )

Since the authenticationManagerResolver is an alternative that branches at the AuthenticationManager level, you don't want to use a JwtAuthenticationProvider. It will be used internally by the JwtIssuerAuthenticationManagerResolver.

Remove .jwt() in that case to prevent the configurer from wiring one up.

Update

The section in the docs on Dynamic Tenants gives some more info on various customization options.

In your case, without the use of .jwt() you cannot as easily wire in a JwtAuthenticationConverter that can customize the returned granted authorities.

The JwtIssuerAuthenticationManagerResolver is internally using a TrustedIssuerJwtAuthenticationManagerResolver. This is what performs the multi-tenancy capability, by extracting an issuer claim from the JWT, and creating a JwtDecoder + new JwtAuthenticationProvider(jwtDecoder) based on the matched issuer.

In order to customize the JwtAuthenticationProvider, you will have to re-implement this class so you can inject your JwtAuthenticationConverter into each created instance. You will implement AuthenticationManagerResolver<String> to do this. Call it CustomTrustedIssuerJwtAuthenticationManagerResolver (see this line).

You just need to provide that to the JwtIssuerAuthenticationManagerResolver, like this:

String[] issuers = new String[] {"https://www.example.com/auth/realms/example"};
AuthenticationManagerResolver<String> authenticationManagerResolver =
    new CustomTrustedIssuerJwtAuthenticationManagerResolver(issuers);
JwtIssuerAuthenticationManagerResolver jwtIssuerAuthenticationManagerResolver =
    new JwtIssuerAuthenticationManagerResolver(authenticationManagerResolver);
...
Related