Spring-boot: OAuth2AutoConfiguration uses OAuth2MethodSecurityExpressionHandler without a BeanResolver set

Created on 17 Jul 2015  路  3Comments  路  Source: spring-projects/spring-boot

NOTE This was originally reported on StackOverflow

There is a bug in the newly added OAuth2AutoConfiguration. Specifically it brings in OAuth2MethodSecurityConfiguration which overrides the DefaultMethodSecurityExpressionHandler with a OAuth2MethodSecurityExpressionHandler that does not have a BeanResolver set. This means that if you run the following:

@PreAuthorize("@security.denyRob()")
public void doStuff()

You will get the following stacktrace:

java.lang.Exception: Unexpected exception, expected<org.springframework.security.access.AccessDeniedException> but was<java.lang.IllegalArgumentException>
    at org.junit.internal.runners.statements.ExpectException.evaluate(ExpectException.java:28)
    at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:74)
    at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:85)
    at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:86)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:243)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:88)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
    at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:182)
    at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:675)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)
Caused by: java.lang.IllegalArgumentException: Failed to evaluate expression '#oauth2.throwOnError(@security.denyRob())'
    at org.springframework.security.access.expression.ExpressionUtils.evaluateAsBoolean(ExpressionUtils.java:14)
    at org.springframework.security.access.expression.method.ExpressionBasedPreInvocationAdvice.before(ExpressionBasedPreInvocationAdvice.java:44)
    at org.springframework.security.access.prepost.PreInvocationAuthorizationAdviceVoter.vote(PreInvocationAuthorizationAdviceVoter.java:57)
    at org.springframework.security.access.prepost.PreInvocationAuthorizationAdviceVoter.vote(PreInvocationAuthorizationAdviceVoter.java:25)
    at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:62)
    at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:232)
    at org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:64)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:653)
    at demo.ServiceImpl$$EnhancerBySpringCGLIB$$3a0d677c.doStuff(<generated>)
    at demo.So31473171NoBeanResolverApplicationTests.denied(So31473171NoBeanResolverApplicationTests.java:26)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:483)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.internal.runners.statements.ExpectException.evaluate(ExpectException.java:19)
    ... 21 more
Caused by: org.springframework.expression.spel.SpelEvaluationException: EL1057E:(pos 8): No bean resolver registered in the context to resolve access to bean 'security'
    at org.springframework.expression.spel.ast.BeanReference.getValueInternal(BeanReference.java:48)
    at org.springframework.expression.spel.ast.CompoundExpression.getValueRef(CompoundExpression.java:51)
    at org.springframework.expression.spel.ast.CompoundExpression.getValueInternal(CompoundExpression.java:87)
    at org.springframework.expression.spel.ast.MethodReference.getArguments(MethodReference.java:154)
    at org.springframework.expression.spel.ast.MethodReference.getValueRef(MethodReference.java:71)
    at org.springframework.expression.spel.ast.CompoundExpression.getValueRef(CompoundExpression.java:66)
    at org.springframework.expression.spel.ast.CompoundExpression.getValueInternal(CompoundExpression.java:87)
    at org.springframework.expression.spel.ast.SpelNodeImpl.getTypedValue(SpelNodeImpl.java:131)
    at org.springframework.expression.spel.standard.SpelExpression.getValue(SpelExpression.java:299)
    at org.springframework.security.access.expression.ExpressionUtils.evaluateAsBoolean(ExpressionUtils.java:11)
    ... 40 more

Workarounds

Remove OAuth2

If you are not using OAuth2, then the easiest solution is to remove Spring Security OAuth from your classpath.

Exclude OAuth2 AutoConfiguration

Alternatively, you can exclude the OAuth2AutoConfiguration using the following if you use @SpringBootApplication:

@SpringBootApplication(exclude=OAuth2AutoConfiguration.class)

alternatively you can use the following if you leverage @AutoConfiguration directly:

@AutoConfiguration(exclude=OAuth2AutoConfiguration.class)

Provide Custom MethodSecurityExpressionHandler

You can also use something like this:

import org.aopalliance.intercept.MethodInvocation;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.core.Authentication;

public class DelegatingMethodSecurityExpressionHandler implements
        MethodSecurityExpressionHandler {

    private final MethodSecurityExpressionHandler delegate;

    public DelegatingMethodSecurityExpressionHandler(
            MethodSecurityExpressionHandler delegate) {
        super();
        this.delegate = delegate;
    }

    public Object filter(Object filterTarget, Expression filterExpression,
            EvaluationContext ctx) {
        return delegate.filter(filterTarget, filterExpression, ctx);
    }

    public ExpressionParser getExpressionParser() {
        return delegate.getExpressionParser();
    }

    public EvaluationContext createEvaluationContext(
            Authentication authentication, MethodInvocation invocation) {
        return delegate.createEvaluationContext(authentication, invocation);
    }

    public void setReturnObject(Object returnObject, EvaluationContext ctx) {
        delegate.setReturnObject(returnObject, ctx);
    }
}

Then in your configuration use:

@Autowired(required = false)
List<AuthenticationTrustResolver> trustResolvers = new ArrayList<>();

@Autowired(required = false)
List<PermissionEvaluator> permissionEvaluators = new ArrayList<>();

@Bean
public MethodSecurityExpressionHandler securityExpressionHandler(ApplicationContext context) {
    OAuth2MethodSecurityExpressionHandler delegate = new OAuth2MethodSecurityExpressionHandler();
    delegate.setApplicationContext(context);
    if(trustResolvers.size() == 1) {
        delegate.setTrustResolver(trustResolvers.get(0));
    }
    if(permissionEvaluators.size() == 1) {
        delegate.setPermissionEvaluator(permissionEvaluators.get(0));
    }
    return new DelegatingMethodSecurityExpressionHandler(delegate);
}

We have to wrap it in the DelegatingMethodSecurityExpressionHandler because Spring Boot's auto config will replace any subclass of DefaultMethodSecurityExpressionHandler with the broken configuration.

bug

All 3 comments

You can resolve this generating the OAuth2MethodSecurityExpressionHandler instance as a bean.

Instead do that:

    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class OAuth2ResourceServerConfig extends GlobalMethodSecurityConfiguration {

            @Override
            protected MethodSecurityExpressionHandler createExpressionHandler() {
                 return new OAuth2MethodSecurityExpressionHandler();
            }

            ....
    }

do this:

    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class OAuth2ResourceServerConfig extends GlobalMethodSecurityConfiguration {

            @Override
            protected MethodSecurityExpressionHandler createExpressionHandler() {
                 return getOAuth2MethodSecurityExpressionHandler();
            }

            @Bean
            public OAuth2MethodSecurityExpressionHandler getOAuth2MethodSecurityExpressionHandler() {
                 return new OAuth2MethodSecurityExpressionHandler();
            }

            ....
    }

Hope this'll others !

The issue persists for OAuth2WebSecurityExpressionHandler

public class AuthConfig extends ResourceServerConfigurerAdapter {
// skipped code

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests() // skipped
                        .anyRequest()
                        .access("@webSecurity.isSuperUser(authentication) "
                                        + "or (hasAnyAuthority('ROLE_THIS', 'ROLE_THAT') "
                                        + " and @webSecurity.check(authentication, request))")
                         // skipped 
                        ;
    }

}

As a workaround I added (to the same config)

    @Autowired
    private OAuth2WebSecurityExpressionHandler expressionHandler;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
       // more skipped code
        resources.expressionHandler(expressionHandler);
    }

    @Bean
    public OAuth2WebSecurityExpressionHandler oAuth2WebSecurityExpressionHandler(
                    ApplicationContext applicationContext) {
        OAuth2WebSecurityExpressionHandler expressionHandler = new OAuth2WebSecurityExpressionHandler();
        expressionHandler.setApplicationContext(applicationContext);

        return expressionHandler;
    }

The issue persists for OAuth2WebSecurityExpressionHandler

@karayv If you believe that to be the case, please open a new issue with a sample that reproduces the problem. Thanks.

Was this page helpful?
0 / 5 - 0 ratings