Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Security Custom Authentication Filter and Authorization

I've implemented a custom authentication filter, and it works great. I use an external identity provider and redirect to my originally requested URL after setting my session and adding my authentication object to my security context.

Security Config

@EnableWebSecurity(debug = true)
@Configuration
class SecurityConfig extends WebSecurityConfigurerAdapter {

    // this is needed to pass the authentication manager into our custom security filter
    @Bean
    @Override
    AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean()
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeRequests()
                //.antMatchers("/admin/test").hasRole("METADATA_CURATORZ")
                .antMatchers("/**").permitAll()
            .anyRequest().authenticated()
            .and()
            .addFilterBefore(new CustomSecurityFilter(authenticationManagerBean()), UsernamePasswordAuthenticationFilter.class)
    }
}

Filter logic

For now, my custom filter (once identity is confirmed) simply hard codes a role:

SimpleGrantedAuthority myrole = new SimpleGrantedAuthority("METADATA_CURATORZ")
                return new PreAuthenticatedAuthenticationToken(securityUser, null, [myrole])

That authentication object (returned above) is then added to my SecurityContext before redirecting to the desired endpoint:

SecurityContextHolder.getContext().setAuthentication(authentication)

Controller Endpoint

  @RequestMapping(path = '/admin/test', method = GET, produces = 'text/plain')
  String test(HttpServletRequest request) {
    Authentication auth = SecurityContextHolder.getContext().getAuthentication()

    String roles = auth.getAuthorities()
    return "roles: ${roles}"
  }

This endpoint then yields a response in the browser of:

"roles: [METADATA_CURATORZ]"

Great. So my authentication and applying a role to my user is working great.

Now, if I uncomment this line from the security config:

//.antMatchers("/admin/test").hasRole("METADATA_CURATORZ")

I can no longer access that resource and get a 403 -- even though we've already proven the role was set.

This seems totally nonsensical and broken to me, but I'm no Spring Security expert.

I'm probably missing something very simple. Any ideas?

Some questions I have:

  • Does my custom filter need to be placed before a specific built-in filter to ensure the authorization step occurs after that filter is executed?
  • When in the request cycle is the antMatcher/hasRole check taking place?
  • Do I need to change the order of what I am calling in my security configure chain, and how should I understand the config as I've currently written it? It's obviously not doing what I think it should be.
like image 341
forgo Avatar asked Jul 02 '18 22:07

forgo


People also ask

What is authentication filter in Spring Security?

Class AuthenticationFilter A Filter that performs authentication of a particular request. An outline of the logic: A request comes in and if it does not match setRequestMatcher(RequestMatcher) , then this filter does nothing and the FilterChain is continued.

How do you make a Spring Security filter?

There are a couple of possible methods: addFilterBefore(filter, class) adds a filter before the position of the specified filter class. addFilterAfter(filter, class) adds a filter after the position of the specified filter class. addFilterAt(filter, class) adds a filter at the location of the specified filter class.

What is authentication and authorization in Spring Security?

Authentication is the process of knowing and identifying the user that wants to access. ADVERTISEMENT. ADVERTISEMENT. Authorization is the process to allow authority to perform actions in the application. We can apply authorization to authorize web request, methods and access to individual domain.


1 Answers

Does my custom filter need to be placed before a specific built-in filter to ensure the authorization step occurs after that filter is executed?

Your filter MUST come before FilterSecurityInterceptor, because that is where authorization and authentication take place. This filter is one of the last to be invoked.

Now as to where the best place for your filter might be, that really depends. For example, you really want your filter to come before AnonymousAuthenticationFilter because if not, unauthenticated users will always be "authenticated" with an AnonymousAuthenticationToken by the time your filter is invoked.

You can check out the default order of filters in FilterComparator. The AbstractPreAuthenticatedProcessingFilter pretty much corresponds to what it is you're doing - and its placement in the order of filters gives you an idea of where you could put yours. In any case, there should be no issue with your filter's order.

When in the request cycle is the antMatcher/hasRole check taking place?

All of this happens in FilterSecurityInterceptor, and more precisely, in its parent AbstractSecurityInterceptor:

protected InterceptorStatusToken beforeInvocation(Object object) {

    Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
            .getAttributes(object);

    if (attributes == null || attributes.isEmpty()) {
        ...
    }

    ...

    Authentication authenticated = authenticateIfRequired();

    // Attempt authorization
    try {
        this.accessDecisionManager.decide(authenticated, object, attributes);
    }
    catch (AccessDeniedException accessDeniedException) {

        ...

        throw accessDeniedException;
    }

Extra information: In essence, the FilterSecurityInterceptor has a ExpressionBasedFilterInvocationSecurityMetadataSource that contains a Map<RequestMatcher, Collection<ConfigAttribute>>. At runtime, your request is checked against the Map to see if any RequestMatcher key is a match. If it is, a Collection<ConfigAttribute> is passed to the AccessDecisionManager, which ultimately either grants or denies access. The default AccessDecisionManager is AffirmativeBased and contains objects (usually a WebExpressionVoter) that process the collection of ConfigAttribute and via reflection invokes the SpelExpression that corresponds to your "hasRole('METADATA_CURATORZ')" against a SecurityExpressionRoot object that was initialized with your Authentication.

Do I need to change the order of what I am calling in my security configure chain, and how should I understand the config as I've currently written it? It's obviously not doing what I think it should be.

No, there shouldn't be any issue with your filters. Just as a side note, in addition to what you have in your configure(HttpSecurity http) methods, the WebSecurityConfigurerAdapter you extend from has some defaults:

http
    .csrf().and()
    .addFilter(new WebAsyncManagerIntegrationFilter())
    .exceptionHandling().and()
    .headers().and()
    .sessionManagement().and()
    .securityContext().and()
    .requestCache().and()
    .anonymous().and()
    .servletApi().and()
    .apply(new DefaultLoginPageConfigurer<>()).and()
    .logout();

You can take a look at HttpSecurity if you want to see exactly what these do and what filters they add.

THE PROBLEM

When you do the following:

.authorizeRequests()
    .antMatchers("/admin/test").hasRole("METADATA_CURATORZ")

... the role that is searched for is "ROLE_METADATA_CURATORZ". Why? ExpressionUrlAuthorizationConfigurer's static hasRole(String role) method ends up processing "METADATA_CURATORZ":

if (role.startsWith("ROLE_")) {
    throw new IllegalArgumentException(
                "role should not start with 'ROLE_' since it is automatically inserted. Got '"
                        + role + "'");
    }
    return "hasRole('ROLE_" + role + "')";
}

So your authorization expression becomes "hasRole('ROLE_METADATA_CURATORZ'" and this ends up calling the method hasRole('ROLE_METADATA_CURATORZ') on SecurityExpressionRoot, which in turn searches for the role ROLE_METADATA_CURATORZ in the Authentication's authorities.

THE SOLUTION

Change

SimpleGrantedAuthority myrole = new SimpleGrantedAuthority("METADATA_CURATORZ");

to:

SimpleGrantedAuthority myrole = new SimpleGrantedAuthority("ROLE_METADATA_CURATORZ");
like image 96
NatFar Avatar answered Nov 10 '22 13:11

NatFar