11

Hi, I've been searching the web with very little success and I'm hoping someone here at SO might be able to help.

Currently I have my Spring Boot API's (Version 2.5.4) accepting a JWT which is provided by Auth0. Now I've created a second tenant there and I'm struggling to understand how I can support two or more issuer-uri's.

Here is how I'm currently doing it:

@Configuration
@EnableWebSecurity(debug = false)
@EnableGlobalMethodSecurity(
        securedEnabled = true,
        jsr250Enabled = true,
        prePostEnabled = true
)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Value("${auth0.audience}")
    private String audience;

    @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
    private String issuer;

    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("http://localhost:3010"));
        configuration.setAllowedMethods(Arrays.asList("GET", "POST"));
        configuration.setAllowCredentials(true);
        configuration.addAllowedHeader("Authorization");
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http
                .cors()
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .headers().referrerPolicy(ReferrerPolicyHeaderWriter.ReferrerPolicy.SAME_ORIGIN)
                .and()
                .xssProtection()
                .and()
                .contentSecurityPolicy("script-src 'self'").and()
                .and()
                .csrf()
                .disable()
                .formLogin()
                .disable()
                .httpBasic()
                .disable()
                .exceptionHandling()
                .authenticationEntryPoint(new RestAuthenticationEntryPoint())
                .and()
                .authorizeRequests()
                .antMatchers("/api/v1/user/profile/**").permitAll()
                .antMatchers("/user/profile/**").permitAll()
                .antMatchers("/swagger-resources/**").permitAll()        
                .anyRequest()
                .authenticated()
                .and()
                .oauth2ResourceServer().jwt();
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/health", "/health/**");
    }

    @Bean
    JwtDecoder jwtDecoder() {
        /*
        By default, Spring Security does not validate the "aud" claim of the token, to ensure that this token is
        indeed intended for our app. Adding our own validator is easy to do:
        */

        NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder)
                JwtDecoders.fromOidcIssuerLocation(issuer);

        OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator(audience);
        OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuer);
        OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);

        jwtDecoder.setJwtValidator(withAudience);

        return jwtDecoder;
    }
}

Can anyone shed some light, with Node I can just set the issuer-uri to be a String array and it works out of the box. Hoping there is something similar in Spring?

EDIT: In case needed here is my build file for versions:

plugins {
    id "org.springframework.boot" version "2.5.4"
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
    id "com.github.davidmc24.gradle.plugin.avro" version "1.2.0"
    id "idea"
    id 'com.google.cloud.tools.jib' version '3.0.0'
}


apply plugin: 'idea'

group 'org.example'
version '1.0'


java {
    sourceCompatibility = JavaVersion.VERSION_14
    targetCompatibility = JavaVersion.VERSION_14
}


jib.from.image = 'openjdk:15-jdk-buster'
jib.to.image = 'gcr.io/thefullstack/tfs-project-service'

ext {
    avroVersion = "1.10.1"
}

repositories {
    mavenCentral()
    jcenter()
    maven {
        url "https://packages.confluent.io/maven/"
    }
}

avro {
    createSetters = true
    fieldVisibility = "PRIVATE"
}

dependencies {
    implementation('org.springframework.boot:spring-boot-starter-data-elasticsearch')
    implementation group: 'org.springframework.boot', name: 'spring-boot-starter-data-mongodb'
    implementation group: 'org.springframework.data', name: 'spring-data-elasticsearch'
    implementation group: 'org.springframework.boot', name: 'spring-boot-starter-security'
    implementation group: 'org.springframework.security', name: 'spring-security-oauth2-client'
    implementation group: 'org.springframework.boot', name: 'spring-boot-starter-validation'
    implementation group: 'org.springframework.boot', name: 'spring-boot-starter-oauth2-resource-server'
    implementation group: 'org.springframework.boot', name: 'spring-boot-starter-cache'
    implementation("org.springframework.boot:spring-boot-starter-actuator")
    implementation group: 'org.springframework.integration', name: 'spring-integration-core', version: '5.5.3'


//    implementation group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-logging'
    implementation group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-logging', version: '1.2.8.RELEASE'
    implementation("org.springframework.cloud:spring-cloud-gcp-starter-pubsub:1.2.5.RELEASE")
    implementation("org.springframework.integration:spring-integration-core")


    implementation group: 'com.amazonaws', name: 'aws-java-sdk', version: '1.11.860'
    implementation group: 'com.mashape.unirest', name: 'unirest-java', version: '1.4.9'

    implementation group: 'javax.validation', name: 'validation-api', version: '2.0.1.Final'
    implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: '2.12.3'
    implementation group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.12.3'
    implementation group: 'io.jsonwebtoken', name: 'jjwt', version: '0.9.1'
    implementation group: 'org.openapitools', name: 'jackson-databind-nullable', version: '0.2.1'

    implementation group: 'commons-io', name: 'commons-io', version: '2.6'
    implementation group: 'org.apache.commons', name: 'commons-collections4', version: '4.4'
    implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.11'

    implementation group: 'com.auth0', name: 'java-jwt', version: '3.12.0'
    implementation "org.apache.avro:avro:1.10.1"
    implementation "org.apache.avro:avro:${avroVersion}"

    implementation 'org.projectlombok:lombok:1.18.20'
    annotationProcessor 'org.projectlombok:lombok:1.18.20'

    implementation 'com.amazonaws:aws-java-sdk-s3'
    implementation 'org.springframework.boot:spring-boot-starter-web'

    testImplementation group: 'junit', name: 'junit', version: '4.12'
    testImplementation 'org.projectlombok:lombok:1.18.20'
    testAnnotationProcessor 'org.projectlombok:lombok:1.18.20'

    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
}
7
  • 1
    Does this help stackoverflow.com/questions/62249978/… Commented Sep 8, 2021 at 15:06
  • Thank you so much for the response. I'm not sure if you would know but how could i make my jwtDecoder() work with this approach? thank you again!
    – MetaCoder
    Commented Sep 8, 2021 at 16:14
  • You could use an AuthenticationManagerResolver that resolves to different JwtAuthenticationProviders, each with a different JwtDecoder. See this example. The approach would be very similar, except instead of a OpaqueTokenIntrospector, you would have 2 JwtDecoder. Commented Sep 9, 2021 at 8:11
  • Thank you for the response Eleftheria!! Sorry dog had an accident needing surgery so I'm only back at the computer now. I'm having some issues with parts of that code example (probably more on the understanding) would you mind if i updated my main question and see if you can still help? thanks again!!
    – MetaCoder
    Commented Sep 13, 2021 at 20:46
  • Feel free to update the question with what you have tried, or open a new question if it's not directly related. Commented Sep 14, 2021 at 7:50

3 Answers 3

16

I have struggled with this issue for a bit a well.

The code below works with several issues (user pools). It's an alternative to one spring.security.oauth2.resourceserver.jwt.issuer-uri property

Please, look at the code for SpringSecurity configuration:


@Component
@ConfigurationProperties(prefix = "config")
class JWTIssuersProps {
    private List<String> issuers;

    // getter and setter
    public List<String> getIssuers() {
        return issuers;
    }

    public void setIssuers(List<String> issuers) {
        this.issuers = issuers;
    }
}

@Configuration
public class JWTCustomSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private JWTIssuersProps props;

    Map<String, AuthenticationManager> authenticationManagers = new HashMap<>();

    JwtIssuerAuthenticationManagerResolver authenticationManagerResolver =
            new JwtIssuerAuthenticationManagerResolver(authenticationManagers::get);

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        List<String> propsIssuers = props.getIssuers();
        propsIssuers.forEach(issuer -> addManager(authenticationManagers, issuer));

        http.
                // CORS configuration
                cors().configurationSource(request -> {
                    var cors = new CorsConfiguration();
                    cors.setAllowedOrigins(List.of("http://localhost:3000"));
                    cors.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
                    cors.setAllowedHeaders(List.of("*"));
                    return cors;
                })

                .and()
                .authorizeRequests()
                .antMatchers("/actuator/**").permitAll()
                .and()
                .oauth2ResourceServer(oauth2ResourceServer -> {
                    oauth2ResourceServer.authenticationManagerResolver(this.authenticationManagerResolver);
                });
    }

    public void addManager(Map<String, AuthenticationManager> authenticationManagers, String issuer) {
        JwtDecoder jwtDecoder = JwtDecoders.fromOidcIssuerLocation(issuer);

        JwtAuthenticationProvider authenticationProvider = new JwtAuthenticationProvider(jwtDecoder);
        authenticationProvider.setJwtAuthenticationConverter(new MyJwtAuthenticationConverter());
        authenticationManagers.put(issuer, authenticationProvider::authenticate);
    }

    static class MyJwtAuthenticationConverter extends JwtAuthenticationConverter {
        @Override
        protected Collection<GrantedAuthority> extractAuthorities(final Jwt jwt) {
            List<String> groups = jwt.getClaim("cognito:groups");

            System.out.println("User groups list.size =" + groups.size());
            System.out.println("User groups:");
            groups.forEach(System.out::println);

            List<GrantedAuthority> authorities = new ArrayList<>();
            for (String role : groups) {
                authorities.add(new SimpleGrantedAuthority(role));
            }
            return authorities;
        }
    }
}

It expects application.yaml file:

config:
  issuers:
  - https://cognito-idp.ca-central-1.amazonaws.com/<pool-ID-1>
  - https://cognito-idp.ca-central-1.amazonaws.com/<pool-ID-2>

I also have CORS configuration there. And read Amazon Cognito groups.

You can find the full project source code here: https://github.com/grenader/spring-security-cognito-oauth2-jwt/tree/java

2
  • 1
    II have tried this approach but i receive an error Error creating bean with name 'springSecurityFilterChain' ... AuthenticationManager is required ... any clue on this
    – simonC
    Commented Mar 11, 2022 at 9:17
  • @simonC It's not enough information to give you a direct answer. Your code is probably missing creating of AuthenticationManagerResolver that defined AuthenticationManagers. Please, look at my project from GitHub, I suggest downloading it and compare the Spring configuring with yours. Commented Mar 25, 2022 at 16:15
2

You can use this Implementation for multiple issuer support in Spring boot 2.7.10. auth-servers urls at this sample are equals to bellows urls.

http://localhost:9292/auth/realms/sso
http://www.auth-server.org:9292/auth/realms/sso
http://127.0.0.1:9292/auth/realms/sso

also this is my application.properties configuration

spring.security.oauth2.client.registration.keycloak.client-id=sample-client
spring.security.oauth2.client.provider.keycloak.authorization-uri=http://www.auth-server.org:9292/auth/realms/sso/protocol/openid-connect/auth
spring.security.oauth2.client.provider.keycloak.token-uri=http://www.auth-server.org:9292/auth/realms/sso/protocol/openid-connect/token
spring.security.oauth2.client.registration.keycloak.redirect-uri=http://localhost:8080/login/oauth2/code/sample-client
spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.keycloak.scope=openid

This class just is a simple configuration.

@Configuration
public class ClientSecurityConfig extends WebSecurityConfigurerAdapter {

   @Override
   public void configure(HttpSecurity http) throws Exception {
       // @formatter:off
       http.authorizeHttpRequests()
               .anyRequest()
               .authenticated();
       http.oauth2Login();
       http.oauth2ResourceServer(httpSecurity -> httpSecurity.jwt(
               jwtConfigurer -> jwtConfigurer.decoder(CustomJwtDecoder::getJwt)
       ));
       // @formatter:on
    }
}

and to support multiple issuers, write two class CustomJwtDecoder and CustomJwtDecoderFactory that implements in order two interfaces JwtDecoder and JwtDecoderFactory.

implementation of these two class:

@Component
public class CustomJwtDecoder implements JwtDecoder {

    @Override
    public Jwt decode(String token) throws JwtException {
        return getJwt(token);
    }

    public static Jwt getJwt(String token) {
        Jwt result = null;
        JwtDecoder jdLocal =     JwtDecoders.fromIssuerLocation("http://localhost:9292/auth/realms/sso");
        JwtDecoder jdDomain = JwtDecoders.fromIssuerLocation("http://www.auth-server.org:9292/auth/realms/sso");
        JwtDecoder jdIp = JwtDecoders.fromIssuerLocation("http://127.0.0.1:9292/auth/realms/sso");
        try { result = jdLocal.decode(token); } catch (Exception ex) { ex.printStackTrace(); }
        try { result = jdDomain.decode(token); } catch (Exception ex) { ex.printStackTrace(); }
        try { result = jdIp.decode(token); } catch (Exception ex) { ex.printStackTrace(); }
        return result;
    }
}

and this class

@Component
public class CustomJwtDecoderFactory implements JwtDecoderFactory<ClientRegistration> {
    @Override
    public JwtDecoder createDecoder(ClientRegistration context) {
        return CustomJwtDecoder::getJwt;
    }
}
0

The following is working for me.

Read Properties (with extra issuer uri)

@Value("${spring.security.oauth2.client.provider.oidc.issuer-uri}")
private String issuerUri;

@Value("${app-properties.security.oauth2Issuer2}")
private String oauthIssuer2; //this is my second issuer URI

Create JWT Decoder with a custom validation

@Bean
/*
referenced org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerJwtConfiguration.JwtDecoderConfiguration.jwtDecoderByIssuerUri
 */
JwtDecoder jwtDecoder() {
    NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuerUri).build();
    OAuth2TokenValidator<Jwt> jwtValidator = JwtValidators.createDefaultWithValidators(new CustomJwtIssuerValidator(issuerUri, oauthIssuer2));
    jwtDecoder.setJwtValidator(jwtValidator);
    return jwtDecoder;
}

static final class CustomJwtIssuerValidator implements OAuth2TokenValidator<Jwt> {
    private final JwtClaimValidator<String> validator;

    public CustomJwtIssuerValidator(String issuer1, String issuer2) {
        Predicate<String> predicate = claimValue ->
            //check if either of them matches     
            issuer1.equals(claimValue) || issuer2.equals(claimValue);

        this.validator = new JwtClaimValidator<>(JwtClaimNames.ISS, predicate);
    }

    @Override
    public OAuth2TokenValidatorResult validate(Jwt token) {
        return this.validator.validate(token);
    }

}

Optional(SecurityFilterChain)

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .headers(h -> h.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin))
        .authorizeHttpRequests(ah -> ah
            .anyRequest().authenticated())
        .oauth2Login(withDefaults())
        .oauth2ResourceServer(o2 -> o2.jwt(withDefaults()))
        .oauth2Client(withDefaults());
    return http.build();
}

Reference:

Spring source code: org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerJwtConfiguration.JwtDecoderConfiguration#jwtDecoderByIssuerUri

Note: if you use audience (spring.security.oauth2.resourceserver.audiences) parameter, then you need to create the validator and add it yourself. See

OAuth2TokenValidator<Jwt> jwtValidator = JwtValidators.createDefaultWithValidators(new CustomJwtIssuerValidator(issuerUri, oauthIssuer2), ADDITIONAL_VALIDATORS);

Not the answer you're looking for? Browse other questions tagged or ask your own question.