Skip to content

Support for setting different 'jwk-set-uri's for each JWT in OAuth 2.0 Resource Server Multi-tenancy #13808

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
sgc109 opened this issue Sep 13, 2023 · 7 comments
Assignees
Labels
in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) status: duplicate A duplicate of another issue type: enhancement A general enhancement

Comments

@sgc109
Copy link

sgc109 commented Sep 13, 2023

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.

@sgc109 sgc109 added status: waiting-for-triage An issue we've not yet triaged type: enhancement A general enhancement labels Sep 13, 2023
@ch4mpy
Copy link
Contributor

ch4mpy commented Sep 13, 2023

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)

@sgc109
Copy link
Author

sgc109 commented Sep 19, 2023

@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! 🙏🏻

@bmd007
Copy link

bmd007 commented Nov 7, 2023

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.

@jzheaux
Copy link
Contributor

jzheaux commented Dec 8, 2023

Thanks for reaching out, @sgc109.

You can configure JwtIssuerAuthenticationManagerResolver with multiple jwk-set-uris 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 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.

@jzheaux jzheaux closed this as completed Dec 8, 2023
@jzheaux jzheaux self-assigned this Dec 8, 2023
@jzheaux jzheaux added status: duplicate A duplicate of another issue in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) and removed status: waiting-for-triage An issue we've not yet triaged labels Dec 8, 2023
@ntenherkel
Copy link

Thanks for reaching out, @sgc109.

You can configure JwtIssuerAuthenticationManagerResolver with multiple jwk-set-uris 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 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<String, JwtDecoder> is deprecated in Spring Framework 6.2 so this solution will break in Spring Boot 3.3.x

Scherm­afbeelding 2024-02-10 om 10 55 34

@ch4mpy
Copy link
Contributor

ch4mpy commented Feb 10, 2024

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.

@jzheaux
Copy link
Contributor

jzheaux commented Mar 4, 2024

Passing a Map<String, JwtDecoder> 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 #14677 as I believe that will reduce some of the boilerplate you are having to do.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) status: duplicate A duplicate of another issue type: enhancement A general enhancement
Projects
None yet
Development

No branches or pull requests

5 participants