In Spring Security 6, (specifically Spring Boot 3.2, Spring MVC with Thymeleaf in my case) when securityMatcher is used, the default /logout POST or GET stops working. You need to provide a custom logout with a URL that is a child to the securityMatcher's URL.
Described the issue with example code at: stack overflow with workaround
Not sure if this issue is by design or not. Also, not sure if there is a better solution than I provided in the stack overflow. However, it would be good to indicate the need for the custom logout in the documentation so folks don't have to figure it out on their own.
Comment From: sjohnr
@tlarsonlgdor thanks for the report.
The docs describe using securityMatcher in conjunction with multiple filter chains, which is always the intended way to configure Spring Security when part of the application requires specific security. Otherwise, you should omit securityMatcher since it doesn't make sense to intentionally limit Spring Security's coverage of requests. Spring Security's defaults assume the entire application is being secured, and always requires customization if this is not the case (which I believe should be uncommon). Does your use case truly require securing only part of the application?
Does the suggestion to define multiple filter chains seem unclear based on the docs linked above? Do you feel like this part of the docs could be improved? Is there another part of the docs where you feel the information you mention should be added?
Comment From: tonybob-tl
@sjohnr thanks for the response. Yes. I am in need of multiple filter chains as I have two different user tables (actually mongo collections) and two different authorization processes in my app. However, I assumed the default logout process could be shared, which apparently it can't.
Regarding the documentation, in my case, I have a custom login in the first (Order(1)) chain instead of httpBasic as shown in the documentation, so apparently I needed a custom logout too. I can't comment on documentation cause I'm not sure whether both or one of the chains will logout properly in the example discussed there. However, it might be nice to make a note somewhere about what defaults stop working with the securityMatcher.
In general, further discussion of how to use multiple tables and multiple authorization processes with the securityMatcher would be good. Much of the stackoverflow/Baeldung documentation online is outdated. I've figured out how to use AuthenticationManagerResolver and AuthenticationFilter with my two user tables and two login processes from Baeldung, but it took some back engineering and trial & error and I'm not sure it is best practice with thesecurityMatcher. Using AuthenticationUserDetailsService with filters on the chain to accommodate different logins might be a better solution for my case, but couldn't find any documentation on it.
Comment From: sjohnr
@tlarsonlgdor thanks for the reply! There's a few things to discuss about what you mentioned.
I assumed the default logout process could be shared, which apparently it can't.
Whether a logout process can or can't be shared between two different authentication mechanisms and/or different sets of endpoints depends heavily on the authentication mechanism used. If (for example) both mechanisms end up using sessions, the logout mechanism should be reusable in most cases. If you're not using sessions, logout likely wouldn't apply for whichever isn't using sessions.
Regarding the documentation, in my case, I have a custom login in the first (Order(1)) chain instead of
httpBasicas shown in the documentation, so apparently I needed a custom logout too. I can't comment on documentation cause I'm not sure whether both or one of the chains will logout properly in the example discussed there.
The httpBasic authentication is stateless by default (in Spring Security 6) and doesn't use a session (though CSRF might if it is enabled). Because of this, you can't really log out. I don't know how your authentication works so I can't say, but you may have something off in the implementation if logout can't be shared. Can you share the details?
However, it might be nice to make a note somewhere about what defaults stop working with the
securityMatcher.
I feel this statement is based on something related to a misconfiguration on your end. While overriding certain defaults in the SecurityFilterChain do turn off some features, this is typically well documented. What I'm trying to get at here is whether you feel like the use of securityMatcher to scope the filter chain requires discussion of customizing endpoints like login or logout. It might be good to add something about this.
In general, further discussion of how to use multiple tables and multiple authorization processes with the
securityMatcherwould be good.
I think this would fall under the category of a "how-to guide" more than reference documentation. The reference doesn't typically pull together specific use-cases and show an all-inclusive example of how to achieve this, but a how-to guide might. Can you outline your requirements for multiple chains in more detail so we can have an example to work off of for this? I don't know yet where such a guide could live but it is a good start to get the details.
Comment From: tonybob-tl
Hi sjohnr,
Thanks for the reply. I think my situation is related to the use of two different securityMatchers and the fact I'm using sessions in both security chains. This might be the reason the logout default is removed in my case. I'm not savvy on the session handler/holder enough to know if or how it splits relative to different securityMatchers.
Here is my config. Based on current tests, it seems to be working correctly as is. I can submit the login and logout controllers for the two security types if you care to see those. They both get the proper authentication using the filter and use it to authenticate then create a session then redirect.
Note, while both security chains use DaoAuthenticationProvider and same password converters, I'm trying to keep the two separate as the User filter chain will have much different authentication requirements than the Baudit user (many haven't been implemented yet).
@EnableWebSecurity @Configuration public class SecurityConfig {
@Autowired
FallotUserBoxService fallotUserBoxService;
@Autowired
UserService userService;
@Bean
// @Order(2)
public SecurityFilterChain openBauditFilterChain(HttpSecurity http) throws Exception {
http.securityMatcher("/EParticipate/baudit/**")
.addFilterBefore(authenticationFilter(), BasicAuthenticationFilter.class)
//See https://www.baeldung.com/spring-security-extra-login-fields
.addFilterBefore(new BauditUsernamePasswordAuthenticationFilter(),
UsernamePasswordAuthenticationFilter.class)
.authorizeHttpRequests((requests) -> requests
.dispatcherTypeMatchers(DispatcherType.FORWARD, DispatcherType.ERROR).permitAll()
.requestMatchers("/EParticipate/baudit/**").hasRole("BAUDIT")
.requestMatchers("/security/**").permitAll())
.formLogin(form -> form
.loginPage("/security/baudit_login_request").permitAll()
.defaultSuccessUrl("/EParticipate/baudit"))
.logout((logout) -> logout
.addLogoutHandler(new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(Directive.CACHE,
Directive.COOKIES, Directive.EXECUTION_CONTEXTS, Directive.STORAGE)))
.logoutUrl("/EParticipate/baudit/logout")
.logoutSuccessUrl("/security/baudit_login_request").permitAll());
return http.build();
}
@Bean
// @Order(2)
public SecurityFilterChain openUserFilterChain(HttpSecurity http) throws Exception {
http.securityMatcher("/EParticipate/toter/**","/EParticipate/fallot/**")
.addFilterBefore(authenticationFilter(), BasicAuthenticationFilter.class)
.authorizeHttpRequests((requests) -> requests
.dispatcherTypeMatchers(DispatcherType.FORWARD, DispatcherType.ERROR).permitAll()
.requestMatchers("/EParticipate/toter/**","/EParticipate/fallot/**").hasRole("TOTER")
.requestMatchers("/security/**").permitAll()
)
.formLogin(form -> form
.loginPage("/security/user_login_request").permitAll()
.defaultSuccessUrl("/EParticipate/toter")
)
.logout((logout) -> logout
.addLogoutHandler(new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(Directive.CACHE,Directive.COOKIES,Directive.EXECUTION_CONTEXTS,Directive.STORAGE)))
.logoutUrl("/EParticipate/toter/logout")
.logoutSuccessUrl("/security/user_login_request").permitAll()
);
return http.build();
}
@Bean
public SecurityContextRepository securityContextRepository() {
return new DelegatingSecurityContextRepository(
new RequestAttributeSecurityContextRepository(),
new HttpSessionSecurityContextRepository());
}
private AuthenticationManager bauditAuthenticationManager() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(fallotUserBoxService);
authenticationProvider.setPasswordEncoder(this.passwordEncoder());
return new ProviderManager(authenticationProvider);
}
private AuthenticationManager userAuthenticationManager() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userService);
authenticationProvider.setPasswordEncoder(this.passwordEncoder());
return new ProviderManager(authenticationProvider);
}
private AuthenticationManagerResolver<HttpServletRequest> resolver() {
return request -> {
try {
String redirectUri = request.getAttribute("redirectUri").toString();
if (redirectUri != null) {
if (redirectUri.startsWith("/EParticipate/baudit")) {
return bauditAuthenticationManager();
}
else if (redirectUri.startsWith("/EParticipate/toter") || redirectUri.startsWith("/EParticipate/fallot")) {
return userAuthenticationManager();
} else {
return null;
}
}
String uri = request.getRequestURI();
if (uri != null) {
if (uri.startsWith("/EParticipate/baudit")) {
return bauditAuthenticationManager();
}
else if (uri.startsWith("/EParticipate/toter") || uri.startsWith("/EParticipate/fallot")) {
return userAuthenticationManager();
} else {
return null;
}
}
return null;
}catch (Exception e){
return null;
}
};
}
@Bean
public AuthenticationFilter authenticationFilter() {
AuthenticationFilter filter = new AuthenticationFilter(resolver(), new BasicAuthenticationConverter());
filter.setSuccessHandler((request, response, auth) -> {});
return filter;
}
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
Comment From: sjohnr
@tlarsonlgdor
I think my situation is related to the use of two different securityMatchers and the fact I'm using sessions in both security chains. This might be the reason the logout default is removed in my case.
The reason generated logout page (GET /logout is removed is because you have specified a loginUrl() which disables the generated login and logout pages. However, if you are referring to the logout processing endpoint (POST /logout) then yes it is related to your use of securityMatchers, but also the fact that you've specified a custom logoutUrl.
Please see my comment above. Because both filter chains specify a securityMatcher, you are not protecting your entire application, only the two patterns you specified in your filter chains. So Spring Security cannot listen for a POST /logout request since there is no filter chain that matches that request. You should have a filter chain (perhaps a 3rd?) that does not specify a securityMatcher. Therefore, I believe your original issue and reason for reporting this issue is due to a misconfiguration.
I'm going to close this issue now based on our discussion, and open a new focused issue on improving the documentation around the use securityMatcher with multiple filter chains. The docs already exist for this feature but could use some explanation. If you have other suggestions, let me know.
Comment From: tonybob-tl
Yes. To confirm, the default /logout will work when a third securityMatcher is used that locks down the entire site. However, it's url needs to be properly permitted for each securityMatcher chain along with the default /login because the default /logout redirects there automatically after logging out. Therefore, you'll probably end up customizing /logout anyway - at least to use the logoutSuccessUrl() to properly redirect to the appropriate /login if you have multiple as I did.
Comment From: DamienSAVNTEC
The last comment of sjohnr who says i quote "The reason generated logout page (GET /logout is removed is because you have specified a loginUrl() which disables the generated login and logout pages." is correct. The deletion of the line that declare a custom login Page made the default logout method work again. Thanks sjohnr.