How to configure Spring-Security (Spring 6) for not having Filters executed on unsecured routes? - spring-boot

somewhat related to this other stackoverflow topic which doesn't give a proper solution nor is applicable to Spring 6 (Spring Boot 3).
I came up with a basic spring-boot app to make my case.
There is a controller with two end-points, where one must be secured and the other accessible.
#RestController
public class TestController {
#GetMapping("/secured-api")
public String securedApi() {
return "secured";
}
#GetMapping("/public/open-api")
public String openApi() {
return "open";
}
}
Security context as follow, imagine that MyFilter is doing something fancy, e.g: validating a JWT token and firing an exception if the token is invalid / expired.
#Configuration
public class ComponentSecurityContext {
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.addFilterAt(new MyFilter(), BasicAuthenticationFilter.class)
.authorizeHttpRequests(customizer -> customizer
.requestMatchers(new AntPathRequestMatcher("/public/**"))
.permitAll()
.anyRequest()
.authenticated())
.build();
}
public static class MyFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
System.out.println("Filter is called for uri: " + request.getRequestURI());
// performs some authentication
filterChain.doFilter(request, response);
}
}
}
Executing the following two curls on the server
curl http://localhost:9003/public/open-api
curl http://localhost:9003/secured-api
is triggering MyFilter
Filter is called for uri: /public/open-api
Filter is called for uri: /secured-api
I would expect MyFilter to be called only for secured end-points, I don't care if an expired token is used to access an unprotected end-point.
Any advise on how to properly wire spring-security to achieve just that?

Working solution where the filter is scoped by the securityMatcher:
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.securityMatcher(new NegatedRequestMatcher(new AntPathRequestMatcher("/public/**")))
.addFilterAt(new MyFilter(), BasicAuthenticationFilter.class)
.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated())
.build();
}

Related

Spring security permitall return 401

Spring Security Config
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.antMatchers("/admin").hasRole("ADMIN")
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/test/**").permitAll()
.antMatchers("/").permitAll()
.antMatchers("/favicon.ico").permitAll()
.antMatchers("/static/**").permitAll()
.antMatchers("/manifest.json").permitAll()
.antMatchers("/logo192.png").permitAll()
.anyRequest().authenticated();
http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
}
I also tried this but did not produce any result
.antMatchers(HttpMethod.POST, "/api/auth/**").permitAll()
/api/auth/signup return
error: "Unauthorized"
message: "Full authentication is required to access this resource"
path: "/error"
status: 401
Request URL: https://mysuite.ru/api/auth/signup
How can I fix this problem?
UPDATE
#Configuration
public class MvcSecurityConfig implements WebMvcConfigurer {
#Value("${path.frontend}")
private String frontendPath;
#Value("${frontendStaticResourcesPathPatterns}")
private String[] frontendStaticResourcesPathPatterns;
private static final String BASE_API_PATH = "/";
public void addResourceHandlers(ResourceHandlerRegistry registry){
String pathToFrontend = "file:" + this.frontendPath;
String pathToIndexHTML = pathToFrontend + "/index.html";
registry
.addResourceHandler(frontendStaticResourcesPathPatterns)
.setCachePeriod(0)
.addResourceLocations(pathToFrontend);
registry.addResourceHandler("/", "/**")
.setCachePeriod(0)
.addResourceLocations(pathToIndexHTML)
.resourceChain(true)
.addResolver(new PathResourceResolver() {
#Override
protected Resource getResource(String resourcePath, Resource location) throws IOException {
if (resourcePath.startsWith(BASE_API_PATH) || resourcePath.startsWith(BASE_API_PATH.substring(1))) {
return null;
}
return location.exists() && location.isReadable() ? location : null;
}
});
}
}
This is my Spring MVC Config.
Could any of this cause the problem?
I also tried to do permitAll step by step along the path but it didn't work (api/, api/auth, api/autn/**)
By default, Spring Security comes with CSRF Protection enabled, so when you perform an unsafe request (POST, PUT, DELETE) you have to provide a CSRF Token.
In your configure method you can disable it to check if it will work.
http.csrf().disable()
I advise you that disabling CSRF protection can be harmful to your app and you should make sure if you need to use it or not.
Also, if you are using Spring Security's version 5.4 or higher, you can enable trace logs to help you debug it.
logging.level.org.springframework.security=TRACE
You can get more details in the reference docs.
In an Ant matcher, ** matches zero or more directories in a path. Given your request URL you just need to match zero or more characters. Having said that, try replacing your Ant matcher with the following:
.antMatchers(HttpMethod.POST, "/api/auth/*").permitAll()
By pass your filter because any API request throught Filter. Your API can not pass Filter so you get 401 response.
Try add this to your Spring Security Config:
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/api/auth/**");
}
Or add this to OncePerRequestFilter:
#Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
return new AntPathMatcher().match("/api/auth/**", request.getServletPath());
}

Spring Security: oauth2Login redirect only on certain paths

I have Spring Security configured to authenticate my website, such that all paths are automatically redirected to the OAuth2 authorization URL (using .oauth2Login()). However, I want unauthenticated requests to the API (i.e. /api/**) to return 401 Unauthorized instead of being redirected. I can't figure out how to do this. Any help would be much appreciated.
Here is my current configuration:
http
.authorizeRequests()
.antMatchers("/api/auth/oauth2/callback").permitAll()
.anyRequest().authenticated()
.oauth2Login()
.authorizationEndpoint()
.baseUri(this.oauth2AuthorizationRedirectBaseUri);
http.logout()
.logoutUrl("/auth/logout")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID");
You can define a custom authentication entry point for /API/** and add t to your configuration:
#Component
public class CustomAuthenticationEntryPoint extends BasicAuthenticationEntryPoint {
#Override
public void commence(
HttpServletRequest request, HttpServletResponse response, AuthenticationException authEx)
throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
}
#Override
public void afterPropertiesSet() throws Exception {
setRealmName("developers");
super.afterPropertiesSet();
}
}
in your Http security configs add:
http.
...
.exceptionHandling()
.defaultAuthenticationEntryPointFor(
new CustomAuthenticationEntryPoint(),
new AntPathRequestMatcher("/api/**"))

obtain request parameter in Spring security Filter

Can someone help in in obtaining request parameter
in WebsecurityConfig Httpsecurity configure method ? I need to extract the request parameter in the below case acr=loa3 that is coming from request
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement()
.maximumSessions(1)
.expiredUrl(this.uiUri + "/expired")
.maxSessionsPreventsLogin(true)
.and()
.invalidSessionUrl(this.uiUri + "/expired")
.and()
.csrf().disable().cors()
.and()
.authorizeRequests()
.antMatchers("/expired").permitAll()
.anyRequest().authenticated()
.and()
//Can some one help me here on how to extract request param coming in the url for example xyz.com/login?acr=loa3 ? I need to send that as acr value before the configureOIDCfilter executes
.addFilterBefore(configureOIDCfilter(http, acrValue),
AbstractPreAuthenticatedProcessingFilter.class)
.exceptionHandling().authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint(this.redirectUri));
}
}
#Bean
public OIDCAuthenticationFilter configureOIDCfilter(HttpSecurity http, String acrValue) throws Exception {
OIDCAuthenticationFilter filter = new OIDCAuthenticationFilter();
StaticSingleIssuerService issuerService = new StaticSingleIssuerService();
issuerService.setIssuer(issuerUrl);
filter.setServerConfigurationService(new DynamicServerConfigurationService());
StaticClientConfigurationService clientService = new StaticClientConfigurationService();
RegisteredClient client = new RegisteredClient();
client.setClientId(clientId);
client.setDefaultACRvalues(ImmutableSet.of(acrValue));
return filter;
}
What you showed in your code is configuration. This is done at startup time and cannot catch any request parameters at this time. However, if you want to need to do something by request, you may want to implement a filter as I wrote in my recent blog post.
You could extend from a filter like this:
public class MyAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
public MyAuthenticationFilter(AuthenticationManager authenticationManager) {
this.setAuthenticationManager(authenticationManager);
}
}
Then, try to find what methods you want to override. In example:
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
...
}
In the above method you can access the http request parameters.
This filter needs to be added to your configuration as well:
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilter(new MyAuthenticationFilter()).
}
A filter will be called for any request and is the only way to receive request parameters (to my knowledge).

Spring security: Apply filter to only an endpoint

Currently, I've two kind of endpoints into my service:
/portal/**: I need to add a filter PortalAuthorizationFilter
The other ones: I need to add a filter OthersAuthorizationFilter
It's important that OthersFilter has not to be applied on /portal/** calls.
I've created a WebSecurityConfigurerAdapter. My current code is:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled=true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(new JWTExceptionHandlerFilter(objectMapper, messageSource), BasicAuthenticationFilter.class)
.cors().and()
.csrf().disable()
.antMatcher("/portal/**")
.addFilter(new PortalAuthorizationFilter())
.authorizeRequests()
.anyRequest().authenticated()
.and()
.addFilter(new OthersAuthorizationFilter());
}
}
I've debugged this code when a call is made to /portal/**, PortalAuthorizationFilter is reached, but then OthersAuthorizationFilter is reached as well.
I don't quite figure out how to solve it.
Any ideas?
You need to create another configuration class which will also extend WebSecurityConfigurerAdapter, there you match your url with antMatchers and add your filter. Whichever pattern match will run that security configuration
Read this post
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled=true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/portal/**").... //the configuration only
//applies when sb hitting /portal/**
}
}
if you want another configuration for another url you need overwrite another WebSecurityConfigurerAdapter:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled=true)
#Order(101) //#Order(100) is default
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http... //without antMatcher(...) default will be used "/**",
//so it take all request. So the order of this class should be
// higher
}
}
If you want use filter approach (.addFilter(new OthersAuthorizationFilter()); )then in your doFilter method you should implement:
doFilter(...) {
if(match(request, "/portal/**")
....
}
Unfortunately AuthenticationProvider will not give you such possibility, it doesn't know about url, just credentials. If you want more read spring-security-architecture.
But I thing you want to delegate authorization
Another option is to use a delegate pattern. Imagine if you have a filter that looks like this
public class PickAndChooseFilter extends OncePerRequestFilter {
private AntPathRequestMatcher matcher = new AntPathRequestMatcher("/portal/**");
private final Filter portalAuthorizationFilter;
private final Filter othersAuthorizationFilter;
public PickAndChooseFilter(Filter portalAuthorizationFilter, Filter othersAuthorizationFilter) {
this.portalAuthorizationFilter = portalAuthorizationFilter;
this.othersAuthorizationFilter = othersAuthorizationFilter;
}
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (matcher.matches(request)) {
portalAuthorizationFilter.doFilter(request, response, filterChain);
} else {
othersAuthorizationFilter.doFilter(request, response, filterChain);
}
}
}
then instead of
.addFilter(new PortalAuthorizationFilter())
.addFilter(new OthersAuthorizationFilter())
you'd simply have
.addFilter(new PickAndChooseFilter(
new PortalAuthorizationFilter(),
new OthersAuthorizationFilter()
)

Spring Boot REST API/Spring Security: Return custom message when authentication fails

I have a Spring Boot app using Jersey as the JAX-RS implementation. This is my security configuration:
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true)
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired TokenAuthenticationProvider tokenAuthenticationProvider;
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(tokenAuthenticationProvider);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(new AuthenticationTokenFilter(), BasicAuthenticationFilter.class)
.csrf().disable()
.authorizeRequests()
.antMatchers("/dataHub/**")
.authenticated();
}
}
What I want to be able to do is to have a way to catch the Exceptions thrown by my TokenAuthenticationProvider and convert them into a standardized JSON format that we have agreed upon. Is there a way to do this? I tried messing around with adding a custom AuthenticationFailureHandler, but couldn't get that to work.
WebSecurityConfigurerAdapter appraoch
The HttpSecurity class has a method called exceptionHandling which can be used to override the default behavior. The following sample presents how the response message can be customized.
#Override
protected void configure(HttpSecurity http) throws Exception {
http
// your custom configuration goes here
.exceptionHandling()
.authenticationEntryPoint((request, response, e) -> {
String json = String.format("{\"message\": \"%s\"}", e.getMessage());
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.getWriter().write(json);
});
}
#ControllerAdvice appraoch - Why it doesn't work in this case
At first I thought about #ControllerAdvice that catches authentication exceptions for the entire application.
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
#ControllerAdvice
public class AuthExceptionHandler {
#ResponseStatus(HttpStatus.UNAUTHORIZED)
#ExceptionHandler(AuthenticationException.class)
#ResponseBody
public String handleAuthenticationException(AuthenticationException e) {
return String.format("{\"message\": \"%s\"}", e.getMessage());
}
}
In the example above, the JSON is built manually, but you can simply return a POJO which will be mapped into JSON just like from a regular REST controller. Since Spring 4.3 you can also use #RestControllerAdvice, which is a combination of #ControllerAdvice and #ResponseBody.
However, this approach doesn't work because the exception is thrown by the AbstractSecurityInterceptor and handled by ExceptionTranslationFilter before any controller is reached.

Resources