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);
}
}
tl;dr When using Spring Security:
AuthenticationManager
unless you have very complex use-casesAuthenticationProvider
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.
CustomAuthenticationManager
. You should not need this.CustomAuthentication
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;
}
// ...
}
}
AuthenticatonManager
to your CustomFilter
. There is an example in the docsCustomAuthenticationProvider
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();
}
// ...
}
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();
}
@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.