Can't catch success authorization even on Spring Security

Viewed 1448

Problem:
I have implemented the following application event listener and it can catch authentication (both cases success and failure) and authorization ( fail). However, while authorization is successful, the even does not be triggered. I traced the code and figured out publishAuthorizationSuccess in AbstractSecurityInterceptor class is always false so it doesn’t publish AuthorizedEvent.

Environment:
Run it on JUnit

The execution sequence of my program:
Run MySampleApp -> SomeService -> ResourcePatternBaseVoter -> AbstractSecurityInterceptor -> SecurityAuditor (not triggered when authorized successfully)

My code and config are shown as follows:

MySampleApp.class

public class MySampleApp{
@Test
public void test2() {
    Authentication authentication = providerManager
            .authenticate(new UsernamePasswordAuthenticationToken("admin", "admin"));
    SecurityContextHolder.getContext().setAuthentication(authentication);
    logger.debug(someService1.someMethod6());
}

SomeService.java

@Service
public class SomeService1 {
@Secured("rpb:reports:a.b.c:create")
public String someMethod6() {
    return String.valueOf(Math.random());
}

ResourcePatternBaseVoter.java

@Component
public class ResourcePatternBaseVoter implements org.springframework.security.access.AccessDecisionVoter<Object> {

private static final Log logger = LogFactory.getLog(ResourcePatternBaseVoter.class);

@Autowired
private ResourcePatternBaseAuthorizer resourcePatternBaseAuthorizer;

@Override
public boolean supports(ConfigAttribute attribute) {
    if ((attribute.getAttribute() != null) && attribute.getAttribute().startsWith("rpb:")) {
        logger.debug("support attribute: " + attribute.getAttribute());
        return true;
    } else {
        logger.debug("not support attribute: " + attribute.getAttribute());
        return false;
    }
}

@Override
public boolean supports(Class<?> clazz) {
    return true;
}

@Override
public int vote(Authentication authentication, Object secureObject, Collection<ConfigAttribute> attributes) {
    /*   doSomething        */

    return ACCESS_GRANTED;
}

}

SecurityAuditor.java

@Component
public class SecurityAuditor implements ApplicationListener<AuthorizedEvent> {

@Override
public void onApplicationEvent(AuthorizedEvent event) {
    logger.info("Here");
}

myAcl.xml

<bean id="methodAccessDecisionManager"
    class="org.springframework.security.access.vote.AffirmativeBased">
    <constructor-arg name="decisionVoters">
        <list>
            <bean class="org.springframework.security.access.vote.AuthenticatedVoter" />
            <bean class="com.ibm.gbsc.ty.acl.rpb.ResourcePatternBaseVoter" />
        </list>
    </constructor-arg>
</bean>

AbstractSecurityInterceptor.class

    if (publishAuthorizationSuccess) {
        publishEvent(new AuthorizedEvent(object, attributes, authenticated));
    }
2 Answers

This article got me started, but that bean does not exist in Spring Security 4.1.3 anymore. However, I found it hidden inside FilterChainProxy.

Not sure how ugly this hack is, but works:

@Configuration
@EnableWebSecurity
@EnableJpaAuditing
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private ApplicationContext applicationContext;

    @EventListener
    public void handle(ContextRefreshedEvent event) {
        FilterChainProxy proxy = applicationContext.getBean(FilterChainProxy.class);
        for (Filter f : proxy.getFilters("/")) {
            if (f instanceof FilterSecurityInterceptor) {
                ((FilterSecurityInterceptor)f).setPublishAuthorizationSuccess(true);
            }
        }
    }
    ...
}

Then my listener finally receives the AuthorizedEvent:

@Component
public class AppEventListener implements ApplicationListener {
    private static final Logger logger = LoggerFactory.getLogger(AppEventListener.class);

    @Override
    @EventListener(value = {AuthorizedEvent.class})
    public void onApplicationEvent(ApplicationEvent event)
    {
        if (event instanceof InteractiveAuthenticationSuccessEvent) {
            Authentication auth =  ((InteractiveAuthenticationSuccessEvent)event).getAuthentication();
            logger.info("Login success: " + auth.getName() + ", details: " + event.toString());
        } else if (event instanceof AbstractAuthenticationFailureEvent) {
            logger.error("Login failed: " + event.toString());
        } else if (event instanceof AuthorizedEvent) {
            Authentication auth =  ((AuthorizedEvent)event).getAuthentication();
            logger.debug("Authorized: " + auth.getName() + ", details: " + event.toString());
        } else if (event instanceof AuthorizationFailureEvent) {
            Authentication auth =  ((AuthorizationFailureEvent)event).getAuthentication();
            logger.error("Authorization failed: " + auth.getName() + ", details: " + event.toString());
        }
    }
}

I tried to use the solution proposed by Arthur, however it throws a UnsupportedOperationException at proxy.getFilters("/").

Caused by: java.lang.UnsupportedOperationException: public abstract javax.servlet.ServletContext javax.servlet.ServletRequest.getServletContext() is not supported
    at org.springframework.security.web.UnsupportedOperationExceptionInvocationHandler.invoke(FilterInvocation.java:235) ~[spring-security-web-5.3.8.RELEASE.jar:5.3.8.RELEASE]
    at com.sun.proxy.$Proxy269.getServletContext(Unknown Source) ~[na:na]
    at javax.servlet.ServletRequestWrapper.getServletContext(ServletRequestWrapper.java:369) ~[tomcat-embed-core-9.0.43.jar:4.0.FR]
    at javax.servlet.ServletRequestWrapper.getServletContext(ServletRequestWrapper.java:369) ~[tomcat-embed-core-9.0.43.jar:4.0.FR]
    at org.springframework.boot.security.servlet.ApplicationContextRequestMatcher.matches(ApplicationContextRequestMatcher.java:58) ~[spring-boot-2.3.9.RELEASE.jar:2.3.9.RELEASE]
    at org.springframework.security.web.util.matcher.OrRequestMatcher.matches(OrRequestMatcher.java:67) ~[spring-security-web-5.3.8.RELEASE.jar:5.3.8.RELEASE]
    at org.springframework.security.web.DefaultSecurityFilterChain.matches(DefaultSecurityFilterChain.java:57) ~[spring-security-web-5.3.8.RELEASE.jar:5.3.8.RELEASE]
    at org.springframework.security.web.FilterChainProxy.getFilters(FilterChainProxy.java:226) ~[spring-security-web-5.3.8.RELEASE.jar:5.3.8.RELEASE]
    at org.springframework.security.web.FilterChainProxy.getFilters(FilterChainProxy.java:241) ~[spring-security-web-5.3.8.RELEASE.jar:5.3.8.RELEASE]

In order to fix this I changed the implementation to

@EventListener
    public void handle ( ContextRefreshedEvent event ) {
        applicationContext.getBean ( FilterChainProxy.class )
            .getFilterChains ()
            .stream ()
            .map ( SecurityFilterChain::getFilters )
            .flatMap ( Collection::stream )
            .filter ( filter -> filter instanceof FilterSecurityInterceptor )
            .map ( filter -> (FilterSecurityInterceptor) filter)
            .forEach ( filterSecurityInterceptor -> filterSecurityInterceptor.setPublishAuthorizationSuccess ( true ) );
    }

While this works this will apply to all filter chains and all instances of the FilterSecurityInterceptor.

It would be possible to filter these further since the FilterSecurityInterceptor maintains a map of which the keys are RequestMatchers and these could be used to narrow it down further, for example to those instances of the FilterSecurityInterceptor that are applied to a certain route or those that require a certain authority. However since the map is private and there is no way to access the keys of the map, Reflection is required in order to do this.

Since I want to avoid using Reflection, I would rather suggest carefully configuring the Security Filter Chain so that it does not throw unnecessary AuthorizedEvents and to ensure that whatever is listening to these events is cheap and fast to execute.

I'm using Spring Boot 2.3.9.RELEASE which depends on Spring Security 5.3.8.RELEASE


It worth pointing that Spring Security is currently adding the so called AuthorizationManager, hopefully this will allow the configuration of this option in more natural way or make it obsolete.

Related