How to apply Spring Security filter only on secured endpoints?
Asked Answered
A

6

63

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?

Alloy answered 22/4, 2016 at 14:1 Comment(0)
G
62

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.

Gunman answered 26/4, 2016 at 21:4 Comment(9)
What if i wanted to allow /api/login ie bypass /api/login completely. Even I do a permitAll(), filter still gets called. Please suggest.Bucella
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).Romeu
@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.Swallow
@RuslanStelmachenko is right, I had to remove the @Configuration annotation from my AuthenticationTokenFilter class to make it work. ThanksCabral
@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);Elizbeth
@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/**").Adlib
@Adlib 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.Romeu
Dude You are Genius, You should get Noble for this :) Thanks from the mountain !Margetts
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!Bookkeeper
R
10

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.

Reichsmark answered 2/11, 2019 at 11:32 Comment(1)
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 directionRatiocination
G
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();
}
Gavial answered 26/8, 2020 at 16:52 Comment(2)
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.Moderation
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.Anemochore
E
6

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.

Elane answered 24/1, 2023 at 9:29 Comment(0)
H
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.

Hanan answered 22/8, 2018 at 15:28 Comment(0)
A
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.

Arciform answered 27/2, 2019 at 5:57 Comment(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.Arciform

© 2022 - 2024 — McMap. All rights reserved.