Search code examples
springspring-bootspring-securityazure-active-directoryazure-ad-msal

Spring security multiple token validators


I have a web application in React and backend in spring boot (2.7.0). Web application has two options for login, username/password and Azure AD popup. If username and password option is selected then authentication is done on my backend and JWT is generated and sent in response. If Azure AD option is selected then pop-up in shown (using MSAL library) and after successful login the JWT is also generated using MSAL library and used to fetch resources from my backend. On my backend I need to have an option to validate both JWT generated from that same backend and JWT generated from Azure AD. What is the best possible way to achieve that?

Here is my SecurityConfig so far:

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private String jwkIssuerUri = "https://login.microsoftonline.com/xxxxxxxxxxxxx/v2.0";

    @Autowired
    JwtTokenValidator jwtTokenValidator;

    private final CustomUserDetailsService userDetailsService;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http.cors(Customizer.withDefaults()).csrf().disable()
                .authorizeRequests()
                .antMatchers("/auth/authenticate")
                .permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .oauth2ResourceServer(oauth -> oauth.jwt())
                .exceptionHandling((ex) ->
                        ex.authenticationEntryPoint(
                                new BearerTokenAuthenticationEntryPoint())
                                .accessDeniedHandler(new BearerTokenAccessDeniedHandler())).build();
    }

    @Bean
    public AuthenticationManager authenticationManager(UserDetailsService userDetailsService) {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService);
        authProvider.setPasswordEncoder(passwordEncoder());
        return new ProviderManager(authProvider);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    JwtEncoder jwtEncoder() {
        String key = "secret key";
        return new NimbusJwtEncoder(new ImmutableSecret<>(key.getBytes()));
    }

    @Bean
    JwtDecoder jwtDecoder() {
        if(******* TO DO IF AZURE AD*******) {
            NimbusJwtDecoder jwtDecoder = JwtDecoders.fromIssuerLocation(this.jwkIssuerUri);
            OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(this.jwkIssuerUri);
            OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, this.jwtTokenValidator);
            jwtDecoder.setJwtValidator(withAudience);
            return jwtDecoder;
        } else {
            String key = "secret key"
            byte[] bytes = key.getBytes();
            SecretKeySpec originalKey = new SecretKeySpec(bytes, 0, bytes.length,"RSA");
            return NimbusJwtDecoder.withSecretKey(originalKey).macAlgorithm(MacAlgorithm.HS512).build();
        }
    }


}

As you can see in jwtDecoder, I'm not sure how to get different JwtDecoder depending on the JWT that I need to validate.


Solution

  • What your are trying to achieve is called multi tenancy

    The goal is not to create a new JwtDecoder for each tenant, but instead configure the JwtDecoder to select the right key to use from the given token, at runtime.

    This option will tell your how to configure your own jwtDecoder. You probably want to customize the example in this way

    @Component
    public class TenantJWSKeySelector
        ...
        @SneakyThrows
        private JWSKeySelector<SecurityContext> fromUri(String uri) {
            if(Objects.equals(uri, "Azure AD issuerUrl"){
                 return JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL(new URL("https://login.microsoftonline.com/common/discovery/v2.0/keys"));
            } else {
                 return JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL(new URL("Your internal JWKS uri"));
            }
        }
    }
    

    You can replace those hard coded URIs with spring configuration easily since TenantJWSKeySelector is a component.

    The standard way of getting the jwks url is from the open-id configuration. For example, for azure AD it is https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration, which you can deduct the jwks_uri. What your want is a response that looks like this.

    I am only familiar with JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL, but it seems that JWSAlgorithmFamilyJWSKeySelector.fromJWKSource let your create your JWKS programmatically. This anwser might help for this part