58

I have the following Spring Security configuration:

httpSecurity
        .csrf().disable()
        .exceptionHandling()
            .authenticationEntryPoint(unauthorizedHandler)
            .and()
        .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
        .authorizeRequests()
            .antMatchers("/api/**").fullyAuthenticated()
            .and()
        .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);

The authenticationTokenFilterBean() is applied even on endpoints that do not match /api/** expression. I also tried adding the following configuration code:

@Override
public void configure(WebSecurity webSecurity) {
    webSecurity.ignoring().antMatchers("/some_endpoint");
}

but this still did not solve my problem. How can I tell Spring Security to apply filters only on endpoints that match the secured URI expression?

dur
  • 15,689
  • 25
  • 79
  • 125
Bravo
  • 1,944
  • 4
  • 29
  • 53

6 Answers6

59

I have an application with the same requirement and to solve it I basically restricted Spring Security to a given ant match patter (using antMatcher) as follows:

http
    .antMatcher("/api/**")
    .authorizeRequests() //
        .anyRequest().authenticated() //
        .and()
    .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);

You can read it as follows: for http only invoke these configurations on requests matching the ant pattern /api/** authorizing any request to authenticated users and add filter authenticationTokenFilterBean() before UsernamePasswordAuthenticationFilter. For all others requests this configuration has no effect.

dur
  • 15,689
  • 25
  • 79
  • 125
Francisco Spaeth
  • 23,493
  • 7
  • 67
  • 106
  • 6
    What if i wanted to allow /api/login ie bypass /api/login completely. Even I do a permitAll(), filter still gets called. Please suggest. – rohitpal Aug 11 '17 at 09:53
  • 19
    For further readers of comments: the answer is **correct**. All people who say it doesn't work just do something wrong (e.g. they define `authenticationTokenFilterBean()` method as `@Bean`, in which case spring-boot will auto-scan it and add it as generic filter even without this security configuration, which is obviously wrong if you want to add this filter just to security filter chain). – Ruslan Stelmachenko Feb 28 '21 at 14:15
  • 3
    **@Everyone:** Make sure you are using `antMatcher(..)` instead of `antMatchers(..)` (as shown in the solution). Also, note that the order of the path matching call is important. – Priidu Neemre Apr 01 '21 at 15:46
  • 1
    @RuslanStelmachenko is right, I had to remove the `@Configuration` annotation from my AuthenticationTokenFilter class to make it work. Thanks – Priyatham May 26 '21 at 07:36
  • @Everyonne: How to do when I have two urls which will be use different filter: e.g: /api/** for JwtAuthFilter and /reader/** for ApiKeyAuthFilter ?? I want to know if I can do something like belong?: `http.antMatcher("/api/**").authorizeRequests() .anyRequest().authenticated().and().addFilterBefore(jwtAuthFilterBean(),UsernamePasswordAuthenticationFilter.class);` --- `http.antMatcher("/reader/**").authorizeRequests() .anyRequest().authenticated().and().addFilterBefore(apikeyAuthFilterBean(), UsernamePasswordAuthenticationFilter.class);` – kevin kemta Oct 14 '21 at 15:30
  • @GabrielDinant Your filter chain is not restricted, the default is `antMatcher(/**)`, so your code is not working. You have to restrict it. In the answer the filter chain is restricted by `.antMatcher("/api/**")`. – dur Jan 04 '23 at 11:52
  • @dur is right. @Gabriel Dinant, I suggest you to take time to understand how security configuration works, there are detailed docs in official reference. When you put `requestMatchers` inside `authorizeHttpRequests` then you just configure **authorization** rules for the target security filter chain. But that security filter chain (so, your custom filter too) is still applied to **any** request because you didn't restrict it in any way. You should use `http.antMatcher("/secured-api/**")` to apply that security filter chain (so, your filter too) only to some urls instead of all urls. – Ruslan Stelmachenko Jan 04 '23 at 22:02
  • Dude You are Genius, You should get Noble for this :) Thanks from the mountain ! – 10101101 Jun 30 '23 at 09:59
  • This answer is so brilliant, there are many posts related to this, but this will be my final solution for now, no need for Swagger whitelisting anymore, hooray! – eyesfree Aug 16 '23 at 14:49
8

GenericFilterBean has a following method :

/**
     * Can be overridden in subclasses for custom filtering control,
     * returning {@code true} to avoid filtering of the given request.
     * <p>The default implementation always returns {@code false}.
     * @param request current HTTP request
     * @return whether the given request should <i>not</i> be filtered
     * @throws ServletException in case of errors
     */
    protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
        return false;
    }

So in your filter that extends GenericFilterBean you can override that method and implement logic to run the filter only on the routes that you would like.

Saša Šijak
  • 8,717
  • 5
  • 47
  • 82
  • 9
    I can't seem to find this in the javadoc. Are you sure this exists? edit: I found that it was moved to `OncePerRequestFilter` but thanks for pointing to the correct direction – Rey Libutan Nov 18 '19 at 15:11
6

My Requirement was to exclude the endpoint matching /api/auth/**, to achieve the same I have configured my WebSecurityConfig spring configuration component as follows:

/**
 * The purpose of this method is to exclude the URL's specific to Login, Swagger UI and static files.
 * Any URL that should be excluded from the Spring security chain should be added to the ignore list in this
 * method only
 */
@Override
public void configure(WebSecurity web) throws Exception {
    web.ignoring().antMatchers("/api/auth/**","/v2/api-docs", 
            "/configuration/ui", 
            "/swagger-resources", 
            "/configuration/security",
            "/swagger-ui.html", 
            "/webjars/**",
            "/favicon.ico",
            "/**/*.png",
            "/**/*.gif",
            "/**/*.svg",
            "/**/*.jpg",
            "/**/*.html",
            "/**/*.css",
            "/**/*.js");
}


   /**
     * The purpose of this method is to define the HTTP configuration that defines how an HTTP request is 
     * going to be treated by the Spring Security chain. All the request URL's (excluding the URL's added
     * in WebSecurity configuration ignore list) matching this configuration have to pass through the
     * custom Spring security filter defined in this method
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
        .cors().disable()
        .authorizeRequests()
        .anyRequest()
        .authenticated()
        .and()
        .exceptionHandling()
        .authenticationEntryPoint(unauthorizedHandler)
        .and()
        .sessionManagement()
        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
        .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
    }

/**
 * The purpose of this method is to create a new instance of JWTAuthenticationFilter
 * and return the same from the method body. It must be ensured that this filter should
 * not be configured as a Spring bean or registered into the Spring Application context
 * failing which the below filter shall be registered as a default web filter, and thus
 * all the URL's even the excluded ones shall be intercepted by the below filter
 */
public JWTAuthenticationFilter authenticationTokenFilterBean() {
    return new JWTAuthenticationFilter();
}
  • 1
    Thank you so much, this solved my problem! I could not use the `/api/**` approach mentioned in other places, so this suits my use-case. Can you explain why this works? Does `WebSecurity` get called first in the chain? I'm just wondering why it works to `.ignore` end points on `WebSecurity` that `HttpSecurity` honors. – mrClean Dec 02 '20 at 21:44
  • This works, so upvoted, but the latest versions of spring boot log a WARN and advise you to use the HttpSecurity API to do this. – Andy Brown Mar 22 '22 at 12:53
4

We recently updated to Spring Boot 3.0.0 which uses Spring Security 6.0.0 and ran into a similar issue when a filter was applied to all requests, although the authorizeHttpRequests() was used with specific paths defined.

Turned out, if you want the HttpSecurity to be configured for a specific path, you need to use securityMatcher() at the beginning.

So it will be something like this:

private SecurityFilterChain configureFilterChain(HttpSecurity http, String pattern, String... roles) throws Exception {
    return http
               .securityMatcher(pattern)
               .authorizeHttpRequests(auth -> auth.requestMatchers(AntPathRequestMatcher.antMatcher(pattern)).hasAnyRole(roles))
               .addFilterBefore(new TokenFilter(), UsernamePasswordAuthenticationFilter.class)
               .sessionManagement()
                   .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                   .and()
               .exceptionHandling()
                   .authenticationEntryPoint(new AuthenticationEntryPointImpl())
                   .accessDeniedHandler(new AccessDeniedHandlerImpl())
                   .and()
               .csrf().disable()
               .build();
}

So in this case, TokenFilter will be applied to only requests which have this pattern.

dur
  • 15,689
  • 25
  • 79
  • 125
Artem Malchenko
  • 2,320
  • 1
  • 18
  • 39
2

If you use the

.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

You can define in the constructor the specific path it will apply to:

public class JwtAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        super("/api/**");
        this.setAuthenticationManager(authenticationManager);
    }

    @Override
    protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
        return super.requiresAuthentication(request, response);
    }

The requiresAuthentication method will be used to know if that endpoint needs authentication.

Koray Tugay
  • 22,894
  • 45
  • 188
  • 319
Qualaelay
  • 713
  • 2
  • 10
  • 21
0

I think I've found a way to solve it. I have JwtTokenAuthenticationProcessingFilter which is an AbstractAuthenticationProcessingFilter. I want it to authenticate request if there is token in the head but do not block the request if failed. All you need is to rewrite the doFilter and invoke the chain.doFilter no matter what the authentication result is(invoking unsuccessfulAuthentication is optional). Here is part of my code.

public class JwtTokenAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {

    private final TokenExtractor tokenExtractor;

    @Autowired
    public JwtTokenAuthenticationProcessingFilter(TokenExtractor tokenExtractor, RequestMatcher matcher) {
        super(matcher);
        this.tokenExtractor = tokenExtractor;
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException,
            ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
        if (!this.requiresAuthentication(request, response)) {
            chain.doFilter(request, response);
        } else {
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Request is to process authentication");
            }

            boolean success = true;

            Authentication authResult = null;
            try {
                authResult = this.attemptAuthentication(request, response);
            } catch (InternalAuthenticationServiceException var8) {
                this.logger.error("An internal error occurred while trying to authenticate the user.", var8);
                success = false;
            } catch (AuthenticationException var9) {
                success = false;
            }


            if (success && null != authResult) {
                this.successfulAuthentication(request, response, chain, authResult);
            }

            // Please ensure that chain.doFilter(request, response) is invoked upon successful authentication. You want
            // processing of the request to advance to the next filter, because very last one filter
            // FilterSecurityInterceptor#doFilter is responsible to actually invoke method in your controller that is
            // handling requested API resource.
            chain.doFilter(request, response);
        }
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {
        String tokenPayload = request.getHeader(WebSecurityConfig.AUTHENTICATION_HEADER_NAME);
        RawAccessJwtToken token = new RawAccessJwtToken(tokenExtractor.extract(tokenPayload));
        return getAuthenticationManager().authenticate(new JwtAuthenticationToken(token));
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
                                            Authentication authResult) throws IOException, ServletException {
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        context.setAuthentication(authResult);
        SecurityContextHolder.setContext(context);
    }
}

Update at Apr 22

To register the filter, just add the following code to the WebSecurityConfig

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private final JwtAuthenticationProvider mJwtAuthenticationProvider;

    @Autowired
    public WebSecurityConfig(JwtAuthenticationProvider jwtAuthenticationProvider) {
        this.mJwtAuthenticationProvider = jwtAuthenticationProvider;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // When multiple authentication providers are defined, the providers will be queried in the order they’re
        // declared.
        auth.authenticationProvider(mJwtAuthenticationProvider);
    }
}

In the code, I only revealed the critical part about adding the filter. All this implementation was inspired by this site. Give credit to the author Vladimir Stankovic for his detail explanation.

Shengfeng Li
  • 606
  • 7
  • 11
  • 1
    @NeelamKapoor Hi, there. You can use the filter as you need, You can also use a new filter and then register it to the adapter. It depends on how you implement the code. – Shengfeng Li Jun 20 '19 at 14:27