56

Is there any way to authorize a POST http-request to a specific URL using org.springframework.security.config.annotation.web.builders.HttpSecurity ?

I'm using HttpSecurity as:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .addFilterAfter(new CsrfCookieGeneratorFilter(), CsrfFilter.class)
            .exceptionHandling()
            .authenticationEntryPoint(authenticationEntryPoint)
        .and()
            .rememberMe()
            .rememberMeServices(rememberMeServices)
            .key(env.getProperty("jhipster.security.rememberme.key"))
        .and()
            .formLogin()
            .loginProcessingUrl("/api/authentication")
            .successHandler(ajaxAuthenticationSuccessHandler)
            .failureHandler(ajaxAuthenticationFailureHandler)
            .usernameParameter("j_username")
            .passwordParameter("j_password")
            .permitAll()
        .and()
            .logout()
            .logoutUrl("/api/logout")
            .logoutSuccessHandler(ajaxLogoutSuccessHandler)
            .deleteCookies("JSESSIONID")
            .permitAll()
        .and()
            .headers()
            .frameOptions()
            .disable()
            .authorizeRequests()
                .antMatchers("/api/register").permitAll()
                .antMatchers("/api/activate").permitAll()
                .antMatchers("/api/authenticate").permitAll()
                .antMatchers("/api/logs/**").hasAuthority(AuthoritiesConstants.ADMIN)
                .antMatchers("/api/subscriptions").permitAll()
                .antMatchers("/api/**").authenticated();
}

I would like to allow only POST requests to the path "/api/subscription".

3 Answers 3

111

Take a look at Spring Data REST + Spring Security which has

http
  .httpBasic().and()
  .authorizeRequests()
    .antMatchers(HttpMethod.POST, "/employees").hasRole("ADMIN")
    .antMatchers(HttpMethod.PUT, "/employees/**").hasRole("ADMIN")
    .antMatchers(HttpMethod.PATCH, "/employees/**").hasRole("ADMIN");
2
  • Updated to exclude the csrf disable which was copied in from the referenced code. This was also discussed in an another answer
    – Matt C
    Commented Feb 29, 2016 at 17:27
  • Why you exclude? Commented Feb 12, 2021 at 0:10
46

TL;DR

Since Spring 6.0 release configuration methods antMatchers(), mvcMathcers() and regexMatchers() have been removed from the API.

And several flavors of requestMatchers() method has been provided as a replacement.

gh-11939 - Remove deprecated antMatchers, mvcMatchers, regexMatchers helper methods from Java Configuration. Instead, use requestMatchers or HttpSecurity#securityMatchers.

Also, an overloaded method authorizeHttpRequests() was introduced to replace Deprecated authorizeRequests().

Even if you're using an earlier Spring version in your project and not going to update to Spring 6 very soon, antMatchers() isn't the best tool you can choose for securing requests to your application.

While applying security rules using antMatchers() you need to be very careful because if you secure let's say path "/foo" these restrictions wouldn't be applied to other aliases of this path like "/foo/", "/foo.thml". As a consequence, it's very easy to misconfigure security rules and introduce a vulnerability (for instance, a path that is supposed to be accessible only for Admins becomes available for any authenticated user, it's surprising that none of the answers above mentions this).

Spring 6.0 - requestMatchers()

According to the documentation, the recommended way to restrict access to certain URL since 5.8 is to use HttpSecurity.authorizeHttpRequests(), which as well as its predecessor comes in two flavors:

  • A parameterless version, which returns an object responsible for configuring the requests (to be precise, an instance of this lengthy-name class). So we can chain requestMatchers() calls directly on it.

  • And the second one expecting an instance of the Customizer interface which allow to apply enhanced DSL (domain-specific language), by using lambda expressions. See this post, it uses outdated matching methods but nicely illustrates the key idea of Lambda DSL, which makes the configuration more intuitive to read by visually grouping configuration options and eliminates the need to use method and().

Which version of authorizeHttpRequests() to use is a stylistic choice (both are valid and supported in 6.0).

Now requestMatchers() are coming into play, this method has four overloaded versions which can replace any of the removed matching methods:

  • requestMatchers( String ... ) - expects a varargs of String patterns. This matcher uses the same rules for matching as Spring MVC. I.e. it would act in the same way as old mvcMatchers(), so that pattern /foo would match all existing aliases of that path like "/foo", "/foo/", "/foo.html". All other versions of requestMatchers() have the same matching behavior, it eliminates the possibility of misconfiguration, which was an Achilles' heel of antMatchers(). Note that the corresponding restriction (hasRole(), access(), etc.) would be applied to any matching request regardless of its HttpMethod.

Example:

.requestMatchers("/foo/*").hasRole("ADMIN") // only authenticated user with role ADMIN can access path /foo/something
.requestMatchers("/bar/*", "/baz/*").hasRole("ADMIN") // only authenticated requests to paths /foo/something and /baz/something  are allowed
  • requestMatchers( HttpMethod ) - expects an HttpMethod as argument. The corresponding restriction (hasRole(), access(), etc.) would be applied to any request handled by the current SecurityFilterChain having specified HttpMethod. If null provided as an argument, any request would match.

Example:

.requestMatchers(HttpMethod.POST, "/bar/**").hasAnyRole("USER", "ADMIN") // any authenticated POST-requests should from an ADMIN or USER are allowed

Example:

.requestMatchers(HttpMethod.POST, "/bar/**").hasAnyRole("USER", "ADMIN") // any POST-request should be authenticatd
.requestMatchers(HttpMethod.DELETE, "/baz/**").hasRole( "ADMIN") // only ADMINs can issue DELETE-requests to these paths
  • requestMatchers( RequestMatcher ... ) - the last version is probably the most flexible one, it allows to provide an arbitrary number of combines RequestMatcher instances. We don't need to implement this interface ourself (unless there's special need), there are several implementations available out of the box including RegexRequestMatcher (which can be used to replace outdated regexMatchers()).

Example:

.requestMatchers(new RegexRequestMatcher("/foo/bar", "POST")).authenticated()

Example - Addressing the Original problem

I would like to allow POST requests only to the "/api/subscription" path.

For that we would need to use this flavor of requestMatchers(HttpMethod, String...) which allows to specify HTTP-method and pattern for path "/api/subscription".

Reminder: WebSecurityConfigureAdapter is deprecated since Spring Security 5.7 release, and now HttpSecurity is being configured through SecurityFilterChain which should be defined as Bean.

Here's how we can secure requests to paths defined in the question using Spring 6 and lambda DSL:

@Configuration
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) {
        
        return http
            // other configuration options
            .authorizeHttpRequests(authCustomizer -> authCustomizer
                .requestMatchers(HttpMethod.POST, "/api/subscriptions").permitAll()
                .requestMatchers(
                    "/api/register", "/api/register", "/api/authenticate"
                ).permitAll()
                .requestMatchers("/api/logs/**").hasAuthority(AuthoritiesConstants.ADMIN)
                .requestMatchers("/api/**").authenticated()
            )
            .build();
    }
}

Note

  • Security rules are being evaluated from top to bottom, the first matching rule would be applied. Make sure that rules are declared in the proper order.

  • Apply deny-by-default policy. While an application evolves, new paths get introduced, and there's a possibility that during these changes your colleagues might forget to update security restrictions and some end point would appear to be unprotected. To avoid that in each SecurityFilterChain (we can declare more than one filter chain, to configure different parts of the system and specify their order) as the last constraint you can introduce a matcher that encompasses all unspecified URLs governed by this SecurityFilterChain like "/foo/**" (i.e. all paths under /foo) applying either authenticated() or denyAll(). And SecurityFilterChain which would be considered as the last one to handle the request should cut out request to all unspecified URLs from the root with requestMatchers("/**").denyAll(). That configuration would prevent both unauthenticated access and disclosing what paths are valid in your system. If a newly introduced endpoint is not accessible (for instance it should be permitted for non-authorize request) would be obvious for your colleague immediately during development process. It's way safer to specify what should be open and keep everything else closed by default.

Spring 5.7 and Earlier

As I've said at beginning even with an Spring version antMatchers() because by using them you can end up with intoducing Broken access control valnarability.

Here's one more related link: Deprecate trailing slash match.

Instead consider applying either mvcMathcers() which as it name suggests uses Spring MVC matching rules, or regex-based regexMatchers(). Both mvcMathcers() and regexMatchers() have an overloaded version that allows specifing an HTTP-method.

Here how to secure the API from the question using mvcMathcers() and Spring Security 5.2 Lambda DSL:

@Configuration
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain securityFilterChain1(HttpSecurity http) {
        
        return http
            // other configuration options
            .authorizeRequests(authCustomizer -> authCustomizer
                .mvcMatchers(
                    "/api/register", "/api/register", "/api/authenticate"
                ).permitAll()
                .mvcMatchers("/api/logs/**").hasAuthority(AuthoritiesConstants.ADMIN)
                .mvcMatchers(HttpMethod.POST, "/api/subscriptions").permitAll()
                .mvcMatchers("/api/**").authenticated()
                .regexMatchers("/**").denyAll()
            )
            .build();
    }
}
0
7

I know this question is a bit old but I don't believe disabling csrf support is an acceptable answer. I had this same problem but don't feel good able using csrf.disable(). Instead I added the following line at the bottom of the page inside the form tags.

<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />
1

Not the answer you're looking for? Browse other questions tagged or ask your own question.