Summary

The docs for authorizeRequests state:

There are multiple children to the http.authorizeRequests() method each matcher is considered in the order they were declared.

However, if two matchers are declared with the exact same pattern, the second one defined actually "wins" instead of the first one.

This appears to be caused by converting a List to a Map. In AbstractConfigAttributeRequestMatcherRegistry in the createRequestMap function:

for (UrlMapping mapping : getUrlMappings()) {
  RequestMatcher matcher = mapping.getRequestMatcher();
  Collection<ConfigAttribute> configAttrs = mapping.getConfigAttrs();
  requestMap.put(matcher, configAttrs);
}

Just changing put to putIfAbsent in the last line of the for loop should resolve this issue.

Actual Behavior

@Override
protected void configure(HttpSecurity http) throws Exception {
  http
    .authorizeRequests()
    .antMatchers("A").denyAll()
    .antMatchers("A").permitAll();

The "denyAll" is ignored and any URL matching "A" will be permitted.

Expected Behavior

I would expect URLs matching "A" to be denied since that was defined first.

Version

4.2.3-RELEASE but appears to be the same in the latest code

Comment From: chschu

I just stumbled over this issue as well.

Another way to fix this would be to keep the duplicate mappings and rely on DefaultFilterInvocationSecurityMetadataSource to pick the first one. This could be done by wrapping the RequestMatcher (using object identity equals()) and using the wrapper as key for the LinkedHashMap.

The fix proposed by @mrfusi0n also looks good to me, because DefaultFilterInvocationSecurityMetadataSource only ever considers the first RequestMatcher that matches the request.

Comment From: jzheaux

Thanks, @mrfusi0n, for the report. The reason that put is used is to allow for this kind of last-one-wins overriding, which is consistent with how Spring Security views configuration in general.

For example, if a third-party dependency pre-configures certain authorization rules, like so:

http
    .authorizeRequests((authz) -> authz
        .mvcMatchers("/admin/**").hasAuthority("admin")
    );

it's more flexible to allow my application to override that dependency's configurations:

http
    .authorizeRequests((authz) -> authz
        .mvcMatchers("/admin/**").hasRole("ADMIN")
    );

Given that the reference generated some confusion about this, I think it would be nice to update the reference. @mrfusi0n, @chschu would either of you be able to provide a PR that updates the reference?