17

I have spent a few whole days already trying to figure out what I'm doing wrong, but have no clue why it's not working. First of all, I would like to say that the following configuration is mostly copied from another projects I'm working on, and those projects can work without any issues (however they are configured slightly differently and use older Spring/Spring Boot versions). I cannot provide less code because I believe these classes are misconfigured and I'm unable to see a typo or whatever else in the following configuration classes. I would love to rewrite from scratch, but not this time. (Components whose names start with I are mine, not parts of the Spring Framework).

So the exception here is:

org.springframework.security.authentication.AuthenticationCredentialsNotFoundException: An Authentication object was not found in the SecurityContext
    at org.springframework.security.access.intercept.AbstractSecurityInterceptor.credentialsNotFound(AbstractSecurityInterceptor.java:379) ~[spring-security-core-4.2.1.RELEASE.jar:4.2.1.RELEASE]
    at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:223) ~[spring-security-core-4.2.1.RELEASE.jar:4.2.1.RELEASE]
    at org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:65) ~[spring-security-core-4.2.1.RELEASE.jar:4.2.1.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:656) ~[spring-aop-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at FOO.BAR.AuthenticationController$$EnhancerBySpringCGLIB$$b4949cda.getSelf(<generated>) ~[classes/:na]
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_65]
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_65]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_65]
    at java.lang.reflect.Method.invoke(Method.java:497) ~[na:1.8.0_65]
    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:220) ~[spring-web-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:134) ~[spring-web-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:116) ~[spring-webmvc-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:827) ~[spring-webmvc-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:738) ~[spring-webmvc-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85) ~[spring-webmvc-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:963) ~[spring-webmvc-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:897) ~[spring-webmvc-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970) [spring-webmvc-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:861) [spring-webmvc-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:622) ~[tomcat-embed-core-8.5.6.jar:8.5.6]
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846) [spring-webmvc-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.test.web.servlet.TestDispatcherServlet.service(TestDispatcherServlet.java:65) [spring-test-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:729) ~[tomcat-embed-core-8.5.6.jar:8.5.6]
    at org.springframework.mock.web.MockFilterChain$ServletFilterProxy.doFilter(MockFilterChain.java:167) [spring-test-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:134) [spring-test-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.test.web.servlet.MockMvc.perform(MockMvc.java:155) [spring-test-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at FOO.BAR.AbstractControllerTest.get(AbstractControllerTest.java:55) [web-test-0-SNAPSHOT.jar:na]
    at FOO.BAR.AuthenticationControllerOkTest.testAuthenticate(AuthenticationControllerOkTest.java:31) [test-classes/:na]
    <...JUnit stuff...>

The only similar question I found in the Web is this one. But it seems to describe a slightly different case, if I'm not wrong. Worth nothing, of course: adding @WithMockUser to the tests does not cause the exception, but since I'm testing the authentication controller, I cannot use this annotation (and it's impossible in the production mode, of course).

AbstractCustomTypesGlobalMethodSecurityConfiguration

This is a boilerplate class I use to add some custom types support to @PreAuthorize. Pretty easy I think, and this one does not look suspicious:

public abstract class AbstractCustomTypesGlobalMethodSecurityConfiguration
        extends GlobalMethodSecurityConfiguration {

    @Nonnull
    protected abstract ApplicationContext applicationContext();

    @Nonnull
    protected abstract ConversionService conversionService();

    @Nonnull
    protected abstract PermissionEvaluator permissionEvaluator();

    @Nonnull
    @SuppressWarnings("DesignForExtension")
    protected Object filter(@Nonnull final MethodSecurityExpressionHandler handler, @Nonnull final Object filterTarget,
            @Nonnull final Expression filterExpression, @Nonnull final EvaluationContext context) {
        return handler.filter(filterTarget, filterExpression, context);
    }

    @Override
    protected final MethodSecurityExpressionHandler createExpressionHandler() {
        final ApplicationContext applicationContext = applicationContext();
        final TypeConverter typeConverter = new StandardTypeConverter(conversionService());
        final DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler() {
            @Override
            public StandardEvaluationContext createEvaluationContextInternal(final Authentication authentication, final MethodInvocation methodInvocation) {
                final StandardEvaluationContext decoratedStandardEvaluationContext = super.createEvaluationContextInternal(authentication, methodInvocation);
                return new ForwardingStandardEvaluationContext() {
                    @Override
                    protected StandardEvaluationContext standardEvaluationContext() {
                        return decoratedStandardEvaluationContext;
                    }

                    @Override
                    public TypeConverter getTypeConverter() {
                        return typeConverter;
                    }
                };
            }

            @Override
            public Object filter(final Object filterTarget, final Expression filterExpression, final EvaluationContext context) {
                return AbstractCustomTypesGlobalMethodSecurityConfiguration.this.filter(this, filterTarget, filterExpression, context);
            }
        };
        handler.setApplicationContext(applicationContext);
        handler.setPermissionEvaluator(permissionEvaluator());
        return handler;
    }

}

SecurityConfiguration

Basically the following configuration just extends the latter configuration providing required beans using the template method design pattern. Nothing suspicious, I guess except @EnableGlobalMethodSecurity, however the annotation seems to work and enabling/disabling its flags affect the overall behavior too. (Moving the annotation to another configuration does not work either as it might work for some cases.)

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
class SecurityConfiguration
        extends AbstractCustomTypesGlobalMethodSecurityConfiguration {

    @Autowired
    private ApplicationContext applicationContext;

    @Autowired
    private ConversionService conversionService;

    @Nonnull
    @Override
    protected ApplicationContext applicationContext() {
        return applicationContext;
    }

    @Nonnull
    @Override
    protected ConversionService conversionService() {
        return conversionService;
    }

    @Nonnull
    @Override
    protected final PermissionEvaluator permissionEvaluator() {
        return getAlwaysPermittedPermissionEvaluator();
    }

    @Nonnull
    @Override
    protected final Object filter(@Nonnull final MethodSecurityExpressionHandler handler, @Nonnull final Object filterTarget,
            @Nonnull final Expression filterExpression, @Nonnull final EvaluationContext context) {
        final MethodSecurityExpressionOperations operations = (MethodSecurityExpressionOperations) context.getRootObject().getValue();
        operations.setFilterObject(filterTarget);
        return filterExpression.getValue(context, Object.class);
    }

}

WebSecurityConfiguration

More or less trivial Web security configuration that defines some rules to access the service endpoints. Please note that the filter "beaned" with authenticationTokenProcessingFilter is not being invoked, because the exception occurs first.

@Configuration
@EnableWebSecurity
class WebSecurityConfiguration
        extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private ITokenAuthenticationService tokenAuthenticationService;

    @Override
    protected final void configure(final HttpSecurity httpSecurity)
            throws Exception {
        httpSecurity
                .authorizeRequests()
                .antMatchers(POST, "/api/v0/authentication").permitAll()
                .antMatchers("/api/v0/**").fullyAuthenticated()
                .antMatchers("/**").permitAll();
        httpSecurity
                .csrf().disable()
                .httpBasic()
                .authenticationEntryPoint(customAuthenticationEntryPoint());
        httpSecurity
                .sessionManagement()
                .sessionCreationPolicy(STATELESS);
        httpSecurity
                .addFilterBefore(authenticationTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class);
    }

    @Bean
    AuthenticationEntryPoint customAuthenticationEntryPoint() {
        return getCustomAuthenticationEntryPoint();
    }

    @Bean
    GenericFilterBean authenticationTokenProcessingFilter() {
        return getAuthenticationTokenProcessingFilter(tokenAuthenticationService);
    }

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Autowired
    void registerGlobalAuthentication(final AuthenticationManagerBuilder managerBuilder)
            throws Exception {
        managerBuilder
                .userDetailsService(userDetailsService)
                .and()
                .eraseCredentials(false);
    }

}

That is pretty much and hopefully complete code that might require diagnostics, I think. It does not look broken or so, but I still cannot figure out the reason of why I'm getting the exception. I'm starting to feel my hair is becoming gray.

Any help is greatly appreciated!

Dependencies:

  • org.springframework.boot:spring-boot-dependencies:1.4.3.RELEASE:pom
  • org.springframework.boot:spring-boot-starter-web:1.4.3.RELEASE
  • org.springframework.security:spring-security-config:4.2.1.RELEASE
  • org.springframework.security:spring-security-core:4.2.1.RELEASE
  • org.springframework.security:spring-security-web:4.2.1.RELEASE

Edit 1

@Test
@DatabaseSetup(DATASET)
// @WithMockUser is commented out -- we're authenticating as Alice ourselves to obtain the authentication token
public void testAuthenticate()
        throws Exception {
    final MockHttpServletResponse response = post("/authentication", asJson(), identityWithKeyGsonIncomingDto("Alice", "alice123"))
            // Here is where it fails: the exception causes HTTP 500 rather than HTTP 201
            .andExpect(status().isCreated())
            .andReturn()
            .getResponse();
    @SuppressWarnings("unchecked")
    final Map<String, Object> responseMap = gson.fromJson(response.getContentAsString(), Map.class);
    final String token = (String) responseMap.get("token");
    get("/users/self", headers("Authorization", token))
            .andExpect(status().isOk());
}

Edit 2

public final class AuthenticationTokenProcessingFilter
        extends GenericFilterBean {

...

    @Override
    public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain)
            throws IOException, ServletException {
        @Nullable
        final String authenticationToken = getAuthenticationToken(request);
        if ( authenticationToken != null ) {
            try {
                final HttpServletRequest httpServletRequest = (HttpServletRequest) request;
                final Authentication authentication = tokenAuthenticationService.authenticate(authenticationToken, httpServletRequest);
                setCurrentAuthentication(authentication);
            } catch ( final AuthenticationException ex ) {
                ...
            }
        }
        chain.doFilter(request, response);
    }

}

Unfortunately, the exception happens before the filter above can take control for some reason. Please note that this filter is intended to set current user authentication only under certain circumstances, but never -- anonymous. At least this is how it works in my other modules.

Community
  • 1
  • 1
Lyubomyr Shaydariv
  • 20,327
  • 12
  • 64
  • 105
  • What about the test code you are running? Is it a full Spring boot test with a webserver etc, or are you mocking the Http Layer? Since the application is stateless you have to include the basic authentication with every request your tests make, or you need a MockUser (or manually set the Authentication is the `SecurityContextHolder`) – Klaus Groenbaek Mar 24 '17 at 21:08
  • @KlausGroenbaek Hm, I'm a bit confused, but I'm running the tests with `MockMvc` -- as far as I understand how the Spring Boot Starter Test works, the test web server is set up during the test phase. Sorry, I feel ashamed I cannot answer your question. Regarding authentication tokens, the authentication token cannot be set first while testing an authentication controller -- if the user is authenticated, I'm returning a token that is supposed to be put in every subsequent request. At least this is how it worked with all my previous modules. – Lyubomyr Shaydariv Mar 24 '17 at 21:46
  • @KlausGroenbaek There is also the filter `authenticationTokenProcessingFilter` bean that checks every requests for authentication headers and sets the current authentication. At least it has to... However, the exception happens before the filter can take control over a request. But also, if I'm not wrong, the filter in those working modules does not really care for setting the current authentication if no valid header was provided -- it simply passes the control to a downstream filter, and probably that is why I have never seen such an exception before, back in those days. Test added. – Lyubomyr Shaydariv Mar 24 '17 at 21:51
  • MockMVC existed before spring boot, it was designed to allow you tests to run without a webserver. It configures a mock servlet environment, so controllers, HttpMessageHandlers (and optionally securityfilter chains) are wired together. WithMockUser configures a authenticated user (so you should not set it if you wish to test the login mechanism), and is a convenient way to not deal with login in every test. However I mostly code the login logic into my tests instead. – Klaus Groenbaek Mar 28 '17 at 08:13
  • @KlausGroenbaek Yes, it comes from Spring Web Test if I'm not mistaken. I prefer `@WithMockUser` for real brevity, however I cannot test the authentication process because of the issue. I could narrow down the cause: `prePostEnabled = true` causes the exception to happen for unauthenticated access to `@PreAuthorize`-d components. However, it looks a bit silly that adding this option works fine for other projects, but not for this one. Some folks say that it may be caused with missing `springSecurityFilterChain`, however if it's true, so why it's missing in this project only. :) – Lyubomyr Shaydariv Mar 28 '17 at 08:21
  • I have never used `WithMockUser` so I can't really help you. I always test the login mechanism, and once you have that code, it is pretty simple to make a utility method that will perform a succesfull login before each test. I like my test scenarios to be as real world as possible, so I only 'mock' the webServer, never my controllers, services, database, or security layer. This means that almost any test scenario can be debugged as the real code, except for the webserver IO. This is only because it takes too long to start the server in every test. – Klaus Groenbaek Mar 28 '17 at 15:56

1 Answers1

15

I'm sorry that tons of code I provided do not reveal the real cause of the issue. After some more experiments with it I've been suggested to run the use case in production mode (I've forgotten it totally because of tests first), and it works in production mode without any issues. Narrowing down to the tests, I checked the tests annotations first to make sure that it has all annotations including @WithSecurityContextTestExecutionListener like what other modules have. And then I figured out that I missed a totally crucial thing: the smallest scope listeners can affect is a single test, and probably the mocked MVC object (MockMvc that I didn't include to the original question because I did believe it's just a configuration issue) is not configured well. And yes, MockMvc instances were initialized in the following way (a @Before method in one of the test super classes):

mvc = webAppContextSetup(webApplicationContext)
        .build();

This is why it didn't work to me, because MockMvc instances must be configured as well.

mvc = webAppContextSetup(webApplicationContext)
        .apply(springSecurity()) // this is the key
        .build();

A good example of not trusting "annotations can do everything you need themselves". Unfortunately, I wasted a lot of time, but I'm glad I could finally find the very cause.

Lyubomyr Shaydariv
  • 20,327
  • 12
  • 64
  • 105
  • 2
    Thank you for saving me days and solving this topic in 5min with your hints – hecko84 Dec 12 '19 at 16:07
  • 1
    Thanks a lot for solution. You saved my day – Sukh Jan 16 '20 at 17:45
  • U'r giving too much of codes! Still have the error message, but not with Spring boot, but with Spring with basic simple security config activated. Any would be appriciated. – lyrio Jan 20 '21 at 02:40
  • For spring-boot-3 / spring-security-6 I had to explicitly specify my SecurityFilterChain to be used. I autowired my filter chain bean, and then specified it this way: apply(springSecurity(new FilterChainProxy(securityFilterChain))) – Dmytro Buryak May 30 '23 at 13:37