Expected Behavior I thought it can't be better if I can just list sets of information for each JWT format(issuer, jwk-set-uri, ...) in application.yaml like below(It's just an example. So it might not be compatible with another configurations of Spring Security OAuth2).
spring:
security:
oauth2:
resourceservers:
server1: # it'd be just a name developers designate
jwt:
jwk-set-uri: original.jwks.server:8080/.well-known/jwks.json
issuer-uri: https://s1.host.name
server2:
jwt:
jwk-set-uri: new.jwks.server:8080/.well-known/jwks.json
issuer-uri: https://s2.host.name
But, I found that these kind of configuration is not possible at the moment.
If support like above is not easy right now, it'd be really nice if I can configure different 'jwk-set-uri's for each issuer with using JwtIssuerReactiveAuthenticationManagerResolver. According to documentation of it, I can just set multiple issuers, but not able to set different jwt-set-uris.
JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerReactiveAuthenticationManagerResolver
("https://idp.example.org/issuerOne", "https://idp.example.org/issuerTwo");
http
.authorizeExchange(exchanges -> exchanges
.anyExchange().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.authenticationManagerResolver(authenticationManagerResolver)
);
Current Behavior Just support one set of jwk-set-uri and issuer like below.
spring:
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: original.server:8080/.well-known/jwks.json
issuer-uri: https://s1.host.name
And can't configure multiple jwk-set-uris associating with multiple issuers with using JwtIssuerReactiveAuthenticationManagerResolver, which is for OAuth2 resource server multi-tenancy.
Context Let me explain about my situation. Our service is using JWT in Resource Server issued by an Authorization Server(Let's call it S1). Also, we should have specific jwk-set-uri which is separated from issuer. application.yaml is like below.
spring:
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: original.server:8080/.well-known/jwks.json
issuer-uri: https://s1.host.name
Now we are replacing original Authorization Server(S1) to new one(S2) for issuing. And new issuer also has its own jwk-set-uri. In order for backward compatibility, we should permit original JWT format(issuer & jwk-set-uri) and, at the same time, new JWT format.
Comment From: ch4mpy
You might find this Spring Boot starter I wrote useful. Your use case is covered as so:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-addons-starter-oidc</artifactId>
<version>7.1.8</version>
</dependency>
@Configuration
@EnableReactiveMethodSecurity // @EnableMethodSecurity in a servlet
public class SecurityConfig {
}
com:
c4-soft:
springaddons:
oidc:
ops:
- iss: https://s1.host.name
jwk-set-uri: original.jwks.server:8080/.well-known/jwks.json
authorities:
# this is an array: you can define as many JSON path as you need
# you can also add basic transformation for each (prefix and force to upper / lower case)
- path: $.json-path.to.claim.to.use.as.authorities.source
- iss: https://s2.host.name
jwk-set-uri: new.jwks.server:8080/.well-known/jwks.json
authorities:
- path: $.json-path.to.claim.to.use.as.authorities.source
resourceserver:
permit-all:
- "/public/**"
In addition to the "static" multi-tenancy your are looking for, it provides with quite a few other features you might find useful - authorities mapping (source claims, prefix and case transformation), without having to provide authentication converter, user service or GrantedAuthoritiesMapper in each app - fine grained CORS configuration (per path matcher), which enables to override allowed origins as environment variable when switching from localhost to dev or prod environments - sessions & CSRF disabled by default on resource server (enabled on clients). If a cookie repo is chosen for CSRF (as required by Angular, React, Vue, etc.), then the right request handler is configured and a filter to actually set the cookie is added - basic access control: permitAll for a list of path matchers and authenticated as default (to be fine tuned with method security or a configuration post-processor bean)
It is compatible with both reactive and servlet applications.
It is also compatible with OAuth2 clients (even if there are some caveats with multi-tenancy on clients)
Comment From: sgc109
@ch4mpy Thank you for reply and your commitment to spring-addons which is such a great open source project. I'll check & try it out and ask you if I have any other questions! 🙏🏻
Comment From: bmd007
still think this functionality is really needed as native to the spring security resource server it self. I hope it gets attention. would be lovely.
Comment From: jzheaux
Thanks for reaching out, @sgc109.
You can configure JwtIssuerAuthenticationManagerResolver
with multiple jwk-set-uri
s in the following way:
@Bean
JwtIssuerAuthenticationManagerResolver authenticationManagerResolver() {
Map<String, JwtDecoder> decoders = Map.of(
"https://s1.host.name", decoder("original.jwks.server:8080/.well-known/jwks.json"),
"https://s2.host.name", decoder("new.jwks.server:8080/.well-known/jwks.json"));
return new JwtIssuerAuthenticationManagerResolver(decoders::get);
}
JwtDecoder decoder(String jwkSetUri) {
return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
}
As for enhancing the Spring Boot properties, please see https://github.com/spring-projects/spring-boot/issues/30108 for the latest discussion about that. If you feel you have more to add, please contribute it to that ticket so we can keep the conversation in one place.
Comment From: ntenherkel
Thanks for reaching out, @sgc109.
You can configure
JwtIssuerAuthenticationManagerResolver
with multiplejwk-set-uri
s in the following way:```java @Bean JwtIssuerAuthenticationManagerResolver authenticationManagerResolver() { Map
decoders = Map.of( "https://s1.host.name", decoder("original.jwks.server:8080/.well-known/jwks.json"), "https://s2.host.name", decoder("new.jwks.server:8080/.well-known/jwks.json")); return new JwtIssuerAuthenticationManagerResolver(decoders::get); } JwtDecoder decoder(String jwkSetUri) { return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build(); } ```
As for enhancing the Spring Boot properties, please see spring-projects/spring-boot#30108 for the latest discussion about that. If you feel you have more to add, please contribute it to that ticket so we can keep the conversation in one place.
Passing a Map
Comment From: ch4mpy
Just a note to point that multi-tenancy support for resource servers has recently improved in spring-addons-starter-oidc
:
- "static" multi-tenancy (when you know at configuration time all the issuers you want to trust) is still supported with just properties as I exposed above
- "dynamic" multi-tenancy (when some trusted issuers are added at runtime) can now be achieved by exposing a bean in charge of resolving the JWT decoder configuration for each new issuer to trust
As illustration, here is how you can accept tokens from any realm of a Keycloak instance (again, even if the realm is created after the resource server was started):
com:
c4-soft:
springaddons:
oidc:
ops:
- iss: https://oidc.c4-soft.com/auth/realms/
authorities:
- path: $.realm_access.roles
@Component
public class IssuerStartsWithOpenidProviderPropertiesResolver implements OpenidProviderPropertiesResolver {
private final SpringAddonsOidcProperties properties;
public IssuerStartsWithOpenidProviderPropertiesResolver(SpringAddonsOidcProperties properties) {
this.properties = properties;
}
@Override
public Optional<OpenidProviderProperties> resolve(Map<String, Object> claimSet) {
final var tokenIss = Optional
.ofNullable(claimSet.get(JwtClaimNames.ISS))
.map(Object::toString)
.orElseThrow(() -> new RuntimeException("Invalid token: missing issuer"));
return properties.getOps().stream().filter(opProps -> {
final var opBaseHref = Optional.ofNullable(opProps.getIss()).map(URI::toString).orElse(null);
if (!StringUtils.hasText(opBaseHref)) {
return false;
}
return tokenIss.startsWith(opBaseHref);
}).findAny();
}
}
You can easily write similar components for scenarios where you need to trust issuers from given (sub)domains, listed in any sort of datasource, or whatever.
Complete list of features in the module README.
Comment From: jzheaux
Passing a Map
is deprecated
My apologies, I had a typo in my sample. Here is what is supported (and not deprecated):
@Bean
JwtIssuerAuthenticationManagerResolver authenticationManagerResolver() {
Map<String, JwtDecoder> decoders = Map.of(
"https://s1.host.name", manager("original.jwks.server:8080/.well-known/jwks.json"),
"https://s2.host.name", manager("new.jwks.server:8080/.well-known/jwks.json"));
return new JwtIssuerAuthenticationManagerResolver(decoders::get);
}
AuthenticationManager authenticationManager(String jwkSetUri) {
JwtDecoder decoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
JwtAuthenticationProvider provider = new JwtAuthenticationProvider(decoder);
return new ProviderManager(provider);
}
That said, I'd encourage you to follow https://github.com/spring-projects/spring-security/issues/14677 as I believe that will reduce some of the boilerplate you are having to do.