Search code examples
springspring-security

Allowing multiple authentication methods on same endpoint with security filter chains


I need my /api/** endpoints to be accessible using either a JWT token or a login/password.

I took inspiration from Spring documentation: https://docs.spring.io/spring-security/reference/servlet/configuration/java.html#_multiple_httpsecurity

But I may have misunderstood the example, and my API endpoints are only accessible using basic authent with this configuration :

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration {

    static final int BCRYPT_PASSWORD_STRENGTH = 10;
    
    @Bean
    @Order(1)
    public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http, @Qualifier("apiUserDetailsService") UserDetailsService userDetailsService) throws Exception {

        http.antMatcher("/api/**")
                .userDetailsService(userDetailsService)
                .csrf().disable()
                .cors(Customizer.withDefaults())
                .exceptionHandling(configurer -> configurer.authenticationEntryPoint(new AuthenticationFallbackEntryPoint()))
                .sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
                .httpBasic(Customizer.withDefaults());

        return http.build();
    }

    @Bean
    @Order(2)
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http, GrantedPortalRoleConverter grantedPortalRoleConverter) throws Exception {
        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedPortalRoleConverter);

        http
                .csrf().disable()
                .cors(Customizer.withDefaults())
                .exceptionHandling(configurer -> configurer.authenticationEntryPoint(new AuthenticationFallbackEntryPoint()))
                .sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(authorize -> authorize
                        .antMatchers(
                                "/swagger-ui/**",
                                "/swagger-ui.html",
                                "/swagger-resources/**",
                                "/v3/api-docs/**")
                        .permitAll()
                        .anyRequest().authenticated())
                .oauth2ResourceServer(configurer -> configurer.jwt().jwtAuthenticationConverter(jwtAuthenticationConverter));
        return http.build();
    }

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

How should I tweak this conf (still using SecurityFilterChain) to achieve my goal?


Solution

  • Here it is, the working config :

        @Bean
        @Order(1)
        public SecurityFilterChain basicAuthSecurityFilterChain(HttpSecurity http, @Qualifier("apiUserDetailsService") UserDetailsService userDetailsService) throws Exception {
    
            http.requestMatcher(new BasicAuthRequestMatcher())
                    .userDetailsService(userDetailsService)
                    .csrf().disable()
                    .cors(Customizer.withDefaults())
                    .exceptionHandling(configurer -> configurer.authenticationEntryPoint(new AuthenticationFallbackEntryPoint()))
                    .sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                    .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
                    .httpBasic(Customizer.withDefaults());
    
            return http.build();
        }
    
        @Bean
        @Order(2)
        public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http, GrantedPortalRoleConverter grantedPortalRoleConverter) throws Exception {
            JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
            jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedPortalRoleConverter);
    
            http
                    .csrf().disable()
                    .cors(Customizer.withDefaults())
                    .exceptionHandling(configurer -> configurer.authenticationEntryPoint(new AuthenticationFallbackEntryPoint()))
                    .sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                    .authorizeHttpRequests(authorize -> authorize
                            .antMatchers(
                                    "/swagger-ui/**",
                                    "/swagger-ui.html",
                                    "/swagger-resources/**",
                                    "/v3/api-docs/**")                              
                            .permitAll()
                            .anyRequest().authenticated())
                    .oauth2ResourceServer(configurer -> configurer.jwt().jwtAuthenticationConverter(jwtAuthenticationConverter));
            return http.build();
        }
    

    And the RequestMatcher :

    public class BasicAuthRequestMatcher implements RequestMatcher {
    
        @Override
        public boolean matches(HttpServletRequest request) {
            String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
            return (authorizationHeader != null && authorizationHeader.startsWith("Basic");
        }
    }