I need different authentication managers based on url matcher for different tenants on resource server. I would like use properties using OAuth2ResourceServerProperties.class to make configuration easier. For example yaml to use with map of this properties: ```spring: security: tenants: cat: jwt: issuer-uri: https://keycloak/auth/realms/cat jwk-set-uri: https://keycloak/auth/realms/cat/protocol/openid-connect/certs dog: jwt: issuer-uri: https://keycloak/auth/realms/dog jwk-set-uri: https://keycloak/auth/realms/dog/protocol/openid-connect/certs
OAuth2ResourceServerAutoConfiguration class uses this properties, but all classes are package protected and imports many other beans.
Overloaded method oauth2ResourceServer() of org.springframework.security.config.annotation.web.builders.HttpSecurity with parameter org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties should be nice enhancement making configuration a lot of easier.
**Comment From: wilkinsona**
> Overloaded method oauth2ResourceServer() of org.springframework.security.config.annotation.web.builders.HttpSecurity with parameter org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties
Unfortunately, we can't do that as it would require Spring Security to depend on Spring Boot and we already have a dependency in the other direction.
Can you please take a step back and describe your goal in more detail? It would be very useful to see the code that you currently have to write to achieve your goal. From that we can then see what, if anything, Spring Boot may be able to do to make it easier.
**Comment From: lowcasz**
I'm not sure it will works correctly, but I would like something like that:
properties.yaml
security: tenants: cat: jwt: issuer-uri: https://keycloak/auth/realms/cat jwk-set-uri: https://keycloak/auth/realms/cat/protocol/openid-connect/certs dog: jwt: issuer-uri: https://keycloak/auth/realms/dog jwk-set-uri: https://keycloak/auth/realms/dog/protocol/openid-connect/certs
Properties class
@Value @ConfigurationProperties("spring.security") public class SecurityProperties {
public SecurityProperties(
String[] permitAll,
String[] denyAll,
User basic,
Map<String, OAuth2ResourceServerProperties> tenants
) {
this.permitAll = Optional.ofNullable(permitAll).orElse(new String[0]);
this.denyAll = Optional.ofNullable(denyAll).orElse(new String[0]);
this.basic= Optional.ofNullable(tech).orElse(new User("user", "password", List.of()));
this.tenants = Optional.ofNullable(tenants).orElse(Map.of());
}
String[] permitAll;
String[] denyAll;
User basic;
Map<String, OAuth2ResourceServerProperties> tenants;
}
Configuration
@Configuration @RequiredArgsConstructor public class SecurityConfiguration {
private final SecurityProperties properties;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
authenticationManagerBuilder.inMemoryAuthentication().withUser(properties.getTech());
http
.cors(withDefaults())
.authorizeHttpRequests(config -> {
config
.requestMatchers(properties.getPermitAll()).permitAll()
.requestMatchers(properties.getDenyAll()).denyAll()
.anyRequest().authenticated();
}
)
.securityMatcher("/tech/**").authenticationManager(authenticationManagerBuilder.build())
.securityMatcher("**").oauth2ResourceServer(config -> config.jwt(withDefaults()));
properties.getTenants().forEach((tenant, config) -> http
.securityMatcher("/%s/**".formatted(tenant))
.oauth2ResourceServer(config)
);
return http.build();
}
}
**Comment From: wilkinsona**
As I said above, we can't have a Spring Security method (`oauth2ResourceServer` in this case) that depends on a Spring Boot type.
At this point, I'm afraid that a possible solution such as the one that you have sketched out above isn't what we're looking for. What I would like to see is the code that you have to write today to solve your problem. That will, hopefully, help us to understand what you're trying to do and we can then consider if it's something that we want to make easier and, if we do, start thinking about how we could do it.
**Comment From: spring-projects-issues**
If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.
**Comment From: lowcasz**
I did simplified workaround looking like this:
@Configuration @RequiredArgsConstructor public class SecurityConfiguration {
private final SecurityProperties properties;
private final GenericWebApplicationContext web;
@Bean
@Order(HIGHEST_PRECEDENCE)
public SecurityFilterChain publicSecurity(HttpSecurity http) throws Exception {
return http
.securityMatcher("/public/**")
.cors(withDefaults())
.authorizeHttpRequests(customizer -> customizer.anyRequest().permitAll())
.build();
}
@Bean
@Order(HIGHEST_PRECEDENCE)
public SecurityFilterChain basicSecurity(HttpSecurity http) throws Exception {
return buildWithStandardAuthorization(http.securityMatcher("/basic/**").httpBasic(withDefaults()));
}
@Bean
public SecurityFilterChain defaultSecurity(HttpSecurity http) throws Exception {
return buildWithStandardAuthorization(http.oauth2ResourceServer(oauth2 -> oauth2.jwt(withDefaults())));
//todo Authorities converter configurer
}
@Bean
public Void registerTenants(HttpSecurity ignore /* buildChain - uses HttpSecurity */) {
this.properties.getTenants().forEach((tenant, property) -> {
final SecurityFilterChain chain = buildChain(tenant, property.getJwt());
web.registerBean("%sTenant".formatted(tenant), SecurityFilterChainOrdered.class, () -> new SecurityFilterChainOrdered(chain));
});
return null;
}
@Bean
public UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager(properties.getUser());
}
@Bean
public PasswordEncoder encoder() {
return NoOpPasswordEncoder.getInstance();
}
@SneakyThrows
private SecurityFilterChain buildChain(String tenant, OAuth2ResourceServerProperties.Jwt property) {
return buildWithStandardAuthorization(web.getBean(HttpSecurity.class)
.securityMatcher("/%s/**".formatted(tenant))
.oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> jwt
.decoder(isNull(property.getJwkSetUri())
? fromIssuerLocation(property.getIssuerUri())
: fromOidcIssuerLocation(property.getJwkSetUri())
)
))
);
}
private SecurityFilterChain buildWithStandardAuthorization(HttpSecurity http) throws Exception {
return http
.cors(withDefaults())
.authorizeHttpRequests(configurer -> {
configurer
.requestMatchers(properties.getPermitAll()).permitAll()
.requestMatchers(properties.getDenyAll()).denyAll()
.anyRequest().authenticated();
}).build();
}
@RequiredArgsConstructor
private static class SecurityFilterChainOrdered implements SecurityFilterChain, Ordered {
private final int order;
private final SecurityFilterChain delegate;
public SecurityFilterChainOrdered(SecurityFilterChain securityFilterChain) {
this.order = HIGHEST_PRECEDENCE;
this.delegate = securityFilterChain;
}
@Override
public int getOrder() {
return order;
}
@Override
public boolean matches(HttpServletRequest request) {
return delegate.matches(request);
}
@Override
public List<Filter> getFilters() {
return delegate.getFilters();
}
}
}
@Value @ConfigurationProperties("spring.security") public class SecurityProperties {
public SecurityProperties(
String[] permitAll,
String[] denyAll,
User user,
Map<String, OAuth2ResourceServerProperties> tenants
) {
this.permitAll = Optional.ofNullable(permitAll).orElse(new String[0]);
this.denyAll = Optional.ofNullable(denyAll).orElse(new String[0]);
this.user = Optional.ofNullable(user).orElse(new User("user", "user", List.of()));
this.tenants = Optional.ofNullable(tenants).orElse(Map.of());
}
String[] permitAll;
String[] denyAll;
User user;
Map<String, OAuth2ResourceServerProperties> tenants;
} ```
Comment From: philwebb
I don't think we should attempt to support this in Spring Boot directly as it will add quite a bit of complexity for a fairly specialized use-case. Since you're only using issuer-uri and jwk-set-uri, I'd be tempted to create your own properties class rather than reusing OAuth2ResourceServerProperties. It could even implement Customizer<OAuth2ResourceServerConfigurer<HttpSecurity>> which would allow it to be used directly with http.authorizeHttpRequests.
Thanks anyway for the suggestion.
Comment From: lowcasz
It's simplified form of basic jwt only, but combination of jwt and opaque token should be useful. In other hand RequestMatcherDelegatingAuthenticationManagerResolver.class already exist and can be useful, but methods based on AuthenticationManagers that are complex to configure manually, and I haven't found any builders either.
Use properties is secondary future.
Main future is to resolve group of authentication managers/providers (single should be enough in most cases)* by url. They should be accesible in specified matcher only, instead of everywhere. It is easy to do by multiple SecurityFilterChain registered by @Bean annotation, but to implement additional logic and add the beans to context by loop on startup needs some workarounds like Void @Bean to start this logic due to HttpSecurity bean access problem. To do it dynamically there will be similar problems.
Use properties may be too much, but possibility to start logic uses HttpSecurity on startup and register isolated authentication managers is not so specialized imo.
Customizer is option, but not useful for multiple securityMatchers because of we cant move up in tree structure when we have already used one securityMatcher().
Comment From: wilkinsona
Those sound like suggestions that should be made to the Spring Security team.