Spring Security filter chain with custom user ID check [duplicate] - spring-boot

This question already has answers here:
How to fix role in Spring Security?
(2 answers)
Closed 6 months ago.
I am trying to use an expression-based check for an user ID path variable, so users can only access resources that belong to them. It is pretty clearly described in the Spring documentation. But I cannot access the bean, with the error that a String is provided.
This is my security filter chain and the bean:
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.cors()
.and()
.csrf().disable()
.authorizeHttpRequests()
.antMatchers(WHITELIST_URLS).permitAll()
.antMatchers("/api/**").authenticated()
.antMatchers("/api/**/users/{userId}/**").access("#userSecurity.checkUserId(authentication,#userId)")
.and()
.oauth2Login(oauth2login ->
oauth2login.loginPage("/oauth2/authorization/api-client-oidc"))
.oauth2Client(Customizer.withDefaults())
.build();
}
public static class UserSecurityConfig extends WebSecurityConfiguration {
#Bean("userSecurity")
private boolean checkUserId(Authentication authentication, String userId) {
return authentication.getPrincipal().equals(userId);
}
}
Error:
Required type: AuthorizationManager
<org.springframework.security.web.access.intercept.RequestAuthorizationContext>
Provided: String
I also have been trying to use an AuthorizationDecision (as lambda expression) but could not access the path variable.
Is the spring documentation wrong on this one? Been searching for quiet a while, but mostly found the same thing as in the Spring documentation.
Actually, I would like to manage this globally in the config and not on each mapping in the controllers by using the #PreAuthorize annotation.
Edit:
I have been unsuccessffuly trying to solve this using something like:
.access((authentication, object) ->
new AuthorizationDecision(object.getRequest().getServletPath().contains(
authentication.get().getName())))
or
.access((authentication, object) ->
new AuthorizationDecision(authentication.get().getPrincipal().equals(
object.getVariables().get("#userId"))))

I figured it out, the following example works. The more specific matcher has to be called first, otherwise it will not work.
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.cors()
.and()
.csrf()
.disable()
.authorizeHttpRequests()
.antMatchers("/api/v1/users/{userId}/**")
.access((authentication, object) -> new AuthorizationDecision(
object.getRequest().getServletPath().contains(
authentication.get().getName())
))
.antMatchers(WHITELIST_URLS)
.permitAll()
.antMatchers("/api/v1/**")
.authenticated()
.and()
.oauth2Login(oauth2login ->
oauth2login.loginPage("/oauth2/authorization/api-client-oidc"))
.oauth2Client(Customizer.withDefaults())
.build();
}

Related

Spring Security Context Authentication is null

i am trying to add couple of filters in my request processing in spring boot security config.
Below is my code
#EnableWebSecurity
#Configuration
public class JwtSecurityConfiguration {
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(this::configureEndpoints)
return http.build();
}
private void configureEndpoints(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry authorizationManagerRequestMatcherRegistry){
authorizationManagerRequestMatcherRegistry.mvcMatchers("/permit")
.permitAll()
.mvcMatchers("/block")
.denyAll()
.and()
.mvcMatcher("/api")
.addFilterBefore(new Filter1(), SecurityContextHolderAwareRequestFilter.class)
// register TenantFilter in the chain after the SecurityContext is made available by the respective filter
.mvcMatcher("/api")
.addFilterAfter(new Filter2(), SecurityContextHolderAwareRequestFilter.class)
.authorizeHttpRequests()
.mvcMatchers("/api")
.authenticated()
.and();
}
}
It seems the authentication does not happen and filters are never hit.
If i try to access the authentication in my runtime code i get SecurityContextHolder.getContext().getAuthentication() as null.
Seems to some problem in the security configuration only.

Spring Security 6.0 allows me to see the h2-console login page but doesn't allow me to go inside, how do I do?

I'm struggling to access my h2-console under the protection of Spring Security 6.0, here is the code
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorizeRequests -> authorizeRequests
.requestMatchers("/h2-console/**").authenticated()
.anyRequest().authenticated()
)
.formLogin(formLogin -> formLogin
.permitAll()
)
.csrf(csrf -> csrf
.ignoringRequestMatchers("/h2-console/**"))
.headers(headers -> headers
.frameOptions().sameOrigin());
return http.build();
}
I can see the h2-console login page though I'm not allowed to go inside.
similar code works well with Spring Security 5.7.5
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorizeRequests -> authorizeRequests
.antMatchers("/h2-console/**").authenticated()
.anyRequest().authenticated()
)
.formLogin(formLogin -> formLogin
.permitAll()
)
.csrf(csrf -> csrf
.ignoringAntMatchers("/h2-console/**"))
.headers(headers -> headers
.frameOptions().sameOrigin())
;
return http.build();
}
I also tried WebSecurityCustomizer, which doesn't work either.
#Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring()
.requestMatchers("/h2-console/**");
}
Here is the debug log
Securing POST
/h2-console/login.do?jsessionid=aa31e312f86f5a876457524984cad7e0
Invalid CSRF token found for
http://127.0.0.1:8080/h2-console/login.do?jsessionid=aa31e312f86f5a876457524984cad7e0
Responding with 403 status code
What am I missing?
All the merits go to this github issue.
I'm just copying the solution over here to make it more convenient to find, as I experienced myself various h2 console problems (401, 403, ...) when I migrated my app from spring boot 2.7.x to 3.0.x (e.g. spring core 5.x to 6.x, and spring security 5.x to 6.x), and it took a little while to fall on that actual github issue and find that solution, which works perfectly fine (more details on why the antMatcher is still being used can be found in the linked github issue...):
#Configuration
public class SecurityConfiguration {
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers(AntPathRequestMatcher.antMatcher("/h2-console/**")).permitAll()
)
.headers(headers -> headers.frameOptions().disable())
.csrf(csrf -> csrf
.ignoringRequestMatchers(AntPathRequestMatcher.antMatcher("/h2-console/**")));
return http.build();
}
}
I traced the source codes from CsrfFilter.java, checked the comparison logic that happens inside. Only to find that it is comparing the provided ignoring URL /h2-console/** with the requesting URL,let's say it is /h2-console/login.do, from which the servlet name counterpart /h2-console/ gets removed, therefore comparing /h2-console/** with login.do instead, end up being evaluated to false.
I dont know what the new purpose would be for doing this, but set CSRF disabled could fix this problem.
Or you could manually put a custom matching logic instead of telling Spring nothing but a URL pattern:
http.csrf().ignoringRequestMatchers(new RequestMatcher() {
#Override
public boolean matches(HttpServletRequest request) {
String contextPath = request.getServletContext().getContextPath();
return request.getRequestURI().startsWith(contextPath + "/h2-console/");
}
});
I dont like lambda at all. I will update this answer if I know something new.
I had to disable csrf, frameOptions, and allow path through requestMatcher to make it all work. I also used toH2Console as it automatically provides correct matcher.
import static org.springframework.boot.autoconfigure.security.servlet.PathRequest.toH2Console;
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.csrf()
.disable()
.headers().frameOptions().disable()
.and()
.authorizeHttpRequests()
.requestMatchers(toH2Console())
.permitAll()
...

Endpoint not accessible after Boot 2.6 upgrade

After upgrading the Boot version in my project to 2.6.0, my endpoint is no longer accessible, I'm automatically redirected to the login page even though I configured it with a permitAll() directive:
#Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.mvcMatchers("presentations")
.permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin();
}
// ...
}
It seems this is actually related to how Spring Boot processes the mvcMatchers values after the upgrade:
The default strategy for matching request paths against registered Spring MVC handler mappings has changed from AntPathMatcher to PathPatternParser.
https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.6-Release-Notes#deferred-openid-connect-discovery
And this new setup requires my presentations pattern to start with /:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.mvcMatchers("/presentations")
.permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin();
}
Alternatively, if I had several routes and I need a quick fix I could set up the following application property to revert this to its previous behavior:
spring.mvc.pathmatch.matching-strategy=ant-path-matcher
BTW, path-pattern-parser is the default value, as it seems to be more efficient... here is some additional information on this and on the differences between PathPatternParser and AntPathMatcher:
https://spring.io/blog/2020/06/30/url-matching-with-pathpattern-in-spring-mvc
https://docs.spring.io/spring-framework/docs/5.3.13/reference/html/web.html#mvc-ann-requestmapping-uri-templates
EDIT: I also realized that using antMatchers() made some of my MockMvc tests fail, this was a bug that got fixed in Boot 2.6.1.

Spring HttpSecurity: Custom web security expressions

I am trying to configure the security of a new spring web application to check requests done against some of my urls.
Since none of the built-in expressions were valid for my logic, I decided to write my own, but it is not working at all.
#Override
protected void configure(HttpSecurity http) throws Exception {
http.headers().cacheControl();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/**/actuator/**").permitAll()
.antMatchers("/**/instances/**").permitAll()
//Custom expresion to check against
.antMatchers("/(?!login|user-profiles)/**").access("#checkAccess.hasRoleSelected()")
.anyRequest().authenticated()
.and()
.httpBasic().disable()
.addFilterBefore(new JWTLoginFilter(jwtConfig.getUri(), authenticationManager(), tokenService), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new JwtTokenAuthenticationFilter(tokenService), UsernamePasswordAuthenticationFilter.class);
}
#Service
public class CheckAccess {
public boolean hasRoleSelected() {
return true;
}
}
As you can see in the documentation, to get this done you need a bean with a method returning a boolean value. While I do have both, the method is never called and no error is thrown.
What am I missing?
Btw, I am running 5.2.2 version of spring security.
Your antMatcher is invalid.
.antMatchers("/(?!login|user-profiles)/**").
Have a look at the allowed patterns in the AntPathMatcher doc.
It is basically, "?", "*" and "**".
You might want to give the regexMatcher a try, instead.

spring security: disable multiple sessions on the same user not working

HttpSecurity object configed like this:
http.authorizeRequests().antMatchers("/login","/loginPage","/static/login.html","/","/index","/static/authenticationErr.html","/static/duplicatedUserErr.html").permitAll()
.and()
.httpBasic()
.authenticationEntryPoint(customAuthenticationEntryPoint)
.and()
.addFilterAt(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.csrf().disable()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginProcessingUrl("/login")
.loginPage("/loginPage")
.permitAll()
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(customLogoutHandler)
.permitAll()
.and()
.sessionManagement() // not working??
.maximumSessions(1)
.maxSessionsPreventsLogin(true)
.expiredUrl("/static/duplicatedUserErr.html")
;
Here is what I tried: by following the spring security reference at https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#concurrent-sessions and source code trace, I found out the key to determine if this is a duplicate login session is this part of code written in method onAuthentication of class ConcurrentSessionControlAuthenticationStrategy:
final List<SessionInformation> sessions = sessionRegistry.getAllSessions(
authentication.getPrincipal(), false);
then base on the size of this list, compare with the maximumSessions limit deined in HttpSecurity config to check if this is an duplicated user. After debugging, I know every time a user try to login, this line of code will be called, however no matter how many times try to login in my browsers, sessions object always be null, it turns out the principals field defined in SessionRegistryImpl has been a empty map since it is created and never be filled with new elements.
Here is other detail of my config:
AuthenticationProvider: org.springframework.security.authentication.dao.DaoAuthenticationProvider
UserDetailService:
CustomUserDetailsService implements UserDetailsService
UserDetails:
org.springframework.security.core.userdetails.User
AuthenticationProcessingFilter:
CustomAuthenticationFilter extends AbstractAuthenticationProcessingFilter
Updated:
#Bean
public CustomAuthenticationFilter customAuthenticationFilter() throws Exception {
CustomAuthenticationFilter filter = new CustomAuthenticationFilter("/login");
filter.setAuthenticationManager(this.authenticationManagerBean());
filter.setAuthenticationFailureHandler(failureHandler);
filter.setAuthenticationSuccessHandler(successHandler);
filter.setSessionAuthenticationStrategy(sessionControlAuthenticationStrategy());
return filter;
}
Can someone give me a light of this?
Finally I have figured it out, you have to create beans by following this reference https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#concurrent-sessions, and you have to set CompositeSessionAuthenticationStrategy bean manually to CustomAuthenticationFilter , and then in your ConcurrentSessionControlAuthenticationStrategy bean, set ExceptionIfMaximumExceeded as true so it will throw a SessionAuthenticationException when a duplicate seesion of the same user created.
My code of above description is like this:
#Bean
public CustomAuthenticationFilter customAuthenticationFilter() throws Exception {
CustomAuthenticationFilter filter = new CustomAuthenticationFilter("/login");
filter.setAuthenticationManager(this.authenticationManagerBean());
filter.setAuthenticationFailureHandler(failureHandler);
filter.setAuthenticationSuccessHandler(successHandler);
//If don't set it here, spring will inject a compositeSessionAuthenticationStrategy bean automatically, but looks like it didn't work as expected for me
filter.setSessionAuthenticationStrategy(compositeSessionAuthenticationStrategy());
return filter;
}
#Bean
public ConcurrentSessionControlAuthenticationStrategy sessionControlAuthenticationStrategy() {
ConcurrentSessionControlAuthenticationStrategy csas = new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry());
csas.setExceptionIfMaximumExceeded(true);
return csas;
}

Resources