Search code examples
spring-bootspring-security

Custom authentication provider with @Component


While using spring security, I have encountered a weird issue that I am not able to wrap my head around.
If I declare a custom authentication provider with the @Component annotation, Spring is no longer able to detect the authentication provider for BasicAuthenticationFilter. I am passing both the basic auth and the key for my custom filter in the headers using postman. This is the exception/error:

2024-02-26T01:56:59.981+05:30 TRACE 4304 --- [nio-8080-exec-4] o.s.security.web.FilterChainProxy        : Trying to match request against DefaultSecurityFilterChain [RequestMatcher=Mvc [pattern='/public/**'], Filters=[]] (1/2)
2024-02-26T01:56:59.981+05:30 TRACE 4304 --- [nio-8080-exec-4] o.s.security.web.FilterChainProxy        : Trying to match request against DefaultSecurityFilterChain [RequestMatcher=any request, Filters=[org.springframework.security.web.session.DisableEncodeUrlFilter@409df37d, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@2ab1c7a9, org.springframework.security.web.context.SecurityContextHolderFilter@22c14d10, org.springframework.security.web.header.HeaderWriterFilter@e594c46, org.springframework.security.web.csrf.CsrfFilter@27b7913, org.springframework.security.web.authentication.logout.LogoutFilter@411e567e, com.prajwal.security.config.filter.CustomFilter@7cfb4736, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@5257123d, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@38276668, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@3344c1d7, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@6e818345, org.springframework.security.web.access.ExceptionTranslationFilter@5dd5422f, org.springframework.security.web.access.intercept.AuthorizationFilter@339b45f8]] (2/2)
2024-02-26T01:56:59.981+05:30 DEBUG 4304 --- [nio-8080-exec-4] o.s.security.web.FilterChainProxy        : Securing GET /private/hello
2024-02-26T01:56:59.981+05:30 TRACE 4304 --- [nio-8080-exec-4] o.s.security.web.FilterChainProxy        : Invoking DisableEncodeUrlFilter (1/13)
2024-02-26T01:56:59.981+05:30 TRACE 4304 --- [nio-8080-exec-4] o.s.security.web.FilterChainProxy        : Invoking WebAsyncManagerIntegrationFilter (2/13)
2024-02-26T01:56:59.982+05:30 TRACE 4304 --- [nio-8080-exec-4] o.s.security.web.FilterChainProxy        : Invoking SecurityContextHolderFilter (3/13)
2024-02-26T01:56:59.982+05:30 TRACE 4304 --- [nio-8080-exec-4] o.s.security.web.FilterChainProxy        : Invoking HeaderWriterFilter (4/13)
2024-02-26T01:56:59.982+05:30 TRACE 4304 --- [nio-8080-exec-4] o.s.security.web.FilterChainProxy        : Invoking CsrfFilter (5/13)
2024-02-26T01:56:59.982+05:30 TRACE 4304 --- [nio-8080-exec-4] o.s.security.web.csrf.CsrfFilter         : Did not protect against CSRF since request did not match CsrfNotRequired [TRACE, HEAD, GET, OPTIONS]
2024-02-26T01:56:59.982+05:30 TRACE 4304 --- [nio-8080-exec-4] o.s.security.web.FilterChainProxy        : Invoking LogoutFilter (6/13)
2024-02-26T01:56:59.982+05:30 TRACE 4304 --- [nio-8080-exec-4] o.s.s.w.a.logout.LogoutFilter            : Did not match request to Ant [pattern='/logout', POST]
2024-02-26T01:56:59.982+05:30 TRACE 4304 --- [nio-8080-exec-4] o.s.security.web.FilterChainProxy        : Invoking CustomFilter (7/13)
2024-02-26T01:56:59.982+05:30 TRACE 4304 --- [nio-8080-exec-4] o.s.security.web.FilterChainProxy        : Invoking BasicAuthenticationFilter (8/13)
2024-02-26T01:56:59.982+05:30 TRACE 4304 --- [nio-8080-exec-4] o.s.s.w.a.www.BasicAuthenticationFilter  : Found username 'prajwal' in Basic Authorization header
2024-02-26T01:56:59.982+05:30 DEBUG 4304 --- [nio-8080-exec-4] o.s.s.w.a.www.BasicAuthenticationFilter  : Failed to process authentication request

org.springframework.security.authentication.ProviderNotFoundException: No AuthenticationProvider found for org.springframework.security.authentication.UsernamePasswordAuthenticationToken
    at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:234) ~[spring-security-core-6.1.5.jar:6.1.5]
    at org.springframework.security.web.authentication.www.BasicAuthenticationFilter.doFilterInternal(BasicAuthenticationFilter.java:174) ~[spring-security-web-6.1.5.jar:6.1.5]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.0.14.jar:6.0.14]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) ~[spring-security-web-6.1.5.jar:6.1.5]
    at com.prajwal.security.config.filter.CustomFilter.doFilterInternal(CustomFilter.java:34) ~[classes/:na]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.0.14.jar:6.0.14]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) ~[spring-security-web-6.1.5.jar:6.1.5]
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:107) ~[spring-security-web-6.1.5.jar:6.1.5]
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:93) ~[spring-security-web-6.1.5.jar:6.1.5]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) ~[spring-security-web-6.1.5.jar:6.1.5]
    at org.springframework.security.web.csrf.CsrfFilter.doFilterInternal(CsrfFilter.java:117) ~[spring-security-web-6.1.5.jar:6.1.5]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.0.14.jar:6.0.14]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) ~[spring-security-web-6.1.5.jar:6.1.5]
    at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:90) ~[spring-security-web-6.1.5.jar:6.1.5]
    at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:75) ~[spring-security-web-6.1.5.jar:6.1.5]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.0.14.jar:6.0.14]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) ~[spring-security-web-6.1.5.jar:6.1.5]
    at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:82) ~[spring-security-web-6.1.5.jar:6.1.5]
    at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:69) ~[spring-security-web-6.1.5.jar:6.1.5]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) ~[spring-security-web-6.1.5.jar:6.1.5]
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:62) ~[spring-security-web-6.1.5.jar:6.1.5]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.0.14.jar:6.0.14]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) ~[spring-security-web-6.1.5.jar:6.1.5]
    at org.springframework.security.web.session.DisableEncodeUrlFilter.doFilterInternal(DisableEncodeUrlFilter.java:42) ~[spring-security-web-6.1.5.jar:6.1.5]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.0.14.jar:6.0.14]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) ~[spring-security-web-6.1.5.jar:6.1.5]
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:233) ~[spring-security-web-6.1.5.jar:6.1.5]
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:191) ~[spring-security-web-6.1.5.jar:6.1.5]
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:352) ~[spring-web-6.0.14.jar:6.0.14]
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:268) ~[spring-web-6.0.14.jar:6.0.14]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-6.0.14.jar:6.0.14]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.0.14.jar:6.0.14]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
    at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-6.0.14.jar:6.0.14]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.0.14.jar:6.0.14]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-6.0.14.jar:6.0.14]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.0.14.jar:6.0.14]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:482) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:340) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:391) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:896) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1744) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
    at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
    at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-10.1.16.jar:10.1.16]
    at java.base/java.lang.Thread.run(Thread.java:1583) ~[na:na]

CustomAuthentication.java

@AllArgsConstructor
@Getter
@Setter
public class CustomAuthentication implements Authentication {

    private final boolean isAuthenticated;
    private final String key;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of(() -> "USER");
    }

    @Override
    public Object getCredentials() {
        return null;
    }

    @Override
    public Object getDetails() {
        return null;
    }

    @Override
    public Object getPrincipal() {
        return null;
    }

    @Override
    public boolean isAuthenticated() {
        return this.isAuthenticated;
    }

    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { }

    @Override
    public boolean equals(Object another) {
        return false;
    }

    @Override
    public String getName() {
        return "CustomAuthentication";
    }

    @Override
    public boolean implies(Subject subject) {
        return Authentication.super.implies(subject);
    }
}

CustomAuthenticationProvider.java

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {

    @Value("${our.key}")
    private String key;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        CustomAuthentication ca = (CustomAuthentication) authentication;

        String headerKey = ca.getKey();
        if(null != headerKey && headerKey.equals(key)) {
            return new CustomAuthentication(true, null);
        }
        return authentication;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return CustomAuthentication.class.isAssignableFrom(authentication);
    }
}

CustomAuthenticationManager.java

@Component
@AllArgsConstructor
public class CustomAuthenticationManager implements AuthenticationManager {

    private final CustomAuthenticationProvider cap;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        if(cap.supports(authentication.getClass())) {
            return cap.authenticate(authentication);
        }

        return authentication;
    }
}

CustomFilter.java

@Component
@AllArgsConstructor
public class CustomFilter extends OncePerRequestFilter {

    private final CustomAuthenticationManager cam;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        CustomAuthentication ca = new CustomAuthentication(false, request.getHeader("key"));
        Authentication authentication = cam.authenticate(ca);

        SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
        securityContext.setAuthentication(authentication);

        SecurityContextHolder.setContext(securityContext);

        filterChain.doFilter(request, response);
        return;
    }
}

WebSecurityConfig.java

@Configuration
@AllArgsConstructor
@EnableWebSecurity
public class WebSecurityConfig {

    private final CustomFilter customFilter;

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return web -> web.ignoring().requestMatchers("/public/**");
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(authorizeConfig -> {
                    authorizeConfig.anyRequest().authenticated();
                })
                .httpBasic(Customizer.withDefaults())
                .addFilterBefore(customFilter, BasicAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
}

JpaUserDetailsService.java

@AllArgsConstructor
@Service
public class JpaUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) {
        System.out.println("UserDetailsService is called");
        User user = userRepository.findByName(username);
        System.out.println("User: " + user.getUsername() + " accessed the pages");
        return new SecurityUser(user);
    }
}

Solution

  • tl;dr When using Spring Security:

    • In general, do not implement your own AuthenticationManager unless you have very complex use-cases
    • Do not expose exactly one AuthenticationProvider as a @Bean or @Component. It gets precedence over auto-configuration, and prevents any UserDetailsService from being configured into an authentication provider.

    Explanations are below, as well as a suggestion for your particular use-case.


    Suggestions for your use-case

    1. Drop CustomAuthenticationManager. You should not need this.
    2. Keep the CustomAuthentication
    3. The CustomFilter may need little more logic around what happens if the key is not present. Do you really want to try and authenticate the request, or should you delegate to other authentication mechanisms? For example:
    @Component
    @AllArgsConstructor
    public class CustomFilter extends OncePerRequestFilter {
    
        private final AuthenticationManager cam;
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            if (request.getHeader("key") == null) {
                filterChain.doFilter(request, response);
                return;
            }
    
            // ...
        }
    }
    
    1. If the above seems correct, you probably need to build use an AbstractHttpConfigurer to pass the AuthenticatonManager to your CustomFilter. There is an example in the docs
    2. [🚨 MOST IMPORTANTLY] Your CustomAuthenticationProvider is fine, but DO NOT make it a @Component. Put it as a simple object your security configuration instead:
    @Configuration
    @AllArgsConstructor
    @EnableWebSecurity
    public class WebSecurityConfig {
    
        private final CustomFilter customFilter;
    
        @Bean
        public WebSecurityCustomizer webSecurityCustomizer() {
            return web -> web.ignoring().requestMatchers("/public/**");
        }
    
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
            http
                    // ...
                    .authenticationProvider(new CustomAuthenticationProvider());
    
            return http.build();
        }
    
        // ...
    }
    
    

    Explanations

    Custom AuthenticationManager

    Usually it is not a good idea to implement your own instead of relying on the defaults. You will be missing a lot of the features of the ProviderManager and potentially ObservationAuthenticationManager. What you want to do is to create your own AuthenticationProvider and make sure it is used.

    Additionally, creating the AuthenticationManager as a @Bean / @Component does nothing. It must be configured explicitly in the HttpSecurity config, like so:

        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
            return http
                    .authenticationManager(new MyAuthenticationManager())
                    // ...
                    .build();
        }
    

    AuthenticationProvider as @Bean / @Component

    In Spring Security, when you use @EnableWebSecurity, a "global" AuthenticationManager is created for you through the HttpSecurityConfiguration.

    By default, it will use any UserDetailsService you have provided as a bean, as hinted in the docs (emphasis mine):

    Normally, Spring Security builds an AuthenticationManager internally composed of a DaoAuthenticationProvider for username/password authentication. In certain cases, it may still be desired to customize the instance of AuthenticationManager used by Spring Security. For example, you may need to simply disable credential erasure for cached users.

    This happens through InitializeUserDetailsBeanManagerConfigurer.

    HOWEVER, if you provide exactly one @Bean / @Component of type AuthenticationProvider, like in your example, then another configuration kicks in, InitializeAuthenticationProviderBeanManagerConfigurer, which takes precedence over the user-details config mentioned above. Note that if you have zero or two or more components of type AuthenticationProvider, the UserDetails configuration does work as expected. This is not documented AFAIK.