5
votes

First I want to comment that I already checked the other questions in Stack Overflow and implemented my own approach based on the answers: https://stackoverflow.com/a/14425801/2487263 and https://stackoverflow.com/a/16101649/2487263

I am trying to secure a REST API using Spring security 3.1 in a Spring 3.2, Spring MVC application, I am using a basic authentication approach with a simple configuration:

<http create-session="stateless" entry-point-ref="authenticationFailedEntryPoint">
    <intercept-url pattern="/**" access="ROLE_USER"/>
    <http-basic />
</http>

As you can see I am using my custom entry point I have my own ErrorResponse object that I will add to the http response in json format, see the code below:

    @Component
public class AuthenticationFailedEntryPoint implements AuthenticationEntryPoint {
    static Logger log = Logger.getLogger(AuthenticationFailedEntryPoint.class);

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        log.error(ExceptionUtils.getStackTrace(authException));
        ErrorResponse errorResponse = new ErrorResponse();  

            ... here I fill my errorResponse object ...

        ObjectMapper jsonMapper = new ObjectMapper();

        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(status); 

        PrintWriter out = response.getWriter();
        out.print(jsonMapper.writeValueAsString(errorResponse));   
    }
}

I tried the approach with two testing cases:

  1. Try to consume one service without providing basic authentication headers:

This is the request/response:

GET http://localhost:8081/accounts/accounts?accountNumber=1013


 -- response --
401 Unauthorized
Server:  Apache-Coyote/1.1

Content-Type:  application/json;charset=UTF-8

Content-Length:  320

Date:  Fri, 25 Oct 2013 17:11:15 GMT

Proxy-Connection:  Keep-alive

{"status":401,"messages":[{"code":"000011","message":"You are not authorized to reach this endpoint"}]}

2.- Try to consume the same service but now sending basic authentication headers with a wrong password:

This is the request/response:

GET http://localhost:8081/accounts/accounts?accountNumber=1013
Authorization: Basic bXl1c2VyOmdvb2RieWU=


 -- response --
401 Unauthorized
Server:  Apache-Coyote/1.1

WWW-Authenticate:  Basic realm="Spring Security Application"

Content-Type:  text/html;charset=utf-8

Content-Length:  1053

Date:  Fri, 25 Oct 2013 17:03:09 GMT

Proxy-Connection:  Keep-alive

<html> ... ugly html generated by tc server ... </html>

As you can see in the first case the entry point was reached and the commence method was executed with the proper handling of the exception and the json response was returned. But that did not happen when the password was a wrong one.

In the logs I found that both cases are producing a different flow:

For case 1 (no auth headers):

...
2013-10-25 13:11:15,830 DEBUG tomcat-http--13 org.springframework.security.web.access.intercept.FilterSecurityInterceptor - Secure object: FilterInvocation: URL: /accounts?accountNumber=1013; Attributes: [ROLE_USER]
2013-10-25 13:11:15,830 DEBUG tomcat-http--13 org.springframework.security.web.access.intercept.FilterSecurityInterceptor - Previously Authenticated: org.springframework.security.authentication.AnonymousAuthenticationToken@9055e4a6: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@957e: RemoteIpAddress: 127.0.0.1; SessionId: null; Granted Authorities: ROLE_ANONYMOUS
2013-10-25 13:11:15,830 DEBUG tomcat-http--13 org.springframework.security.access.vote.AffirmativeBased - Voter: org.springframework.security.access.vote.RoleVoter@11da1f99, returned: -1
2013-10-25 13:11:15,831 DEBUG tomcat-http--13 org.springframework.security.access.vote.AffirmativeBased - Voter: org.springframework.security.access.vote.AuthenticatedVoter@7507ef7, returned: 0
2013-10-25 13:11:15,831 DEBUG tomcat-http--13 org.springframework.security.web.access.ExceptionTranslationFilter - Access is denied (user is anonymous); redirecting to authentication entry point
org.springframework.security.access.AccessDeniedException: Access is denied
...

For the case 2 (wrong password):

...
2013-10-25 13:03:08,941 DEBUG tomcat-http--11 org.springframework.security.web.authentication.www.BasicAuthenticationFilter - Basic Authentication Authorization header found for user 'myuser'
2013-10-25 13:03:08,941 DEBUG tomcat-http--11 org.springframework.security.authentication.ProviderManager - Authentication attempt using org.springframework.security.authentication.dao.DaoAuthenticationProvider
2013-10-25 13:03:09,544 DEBUG tomcat-http--11 org.springframework.security.authentication.dao.DaoAuthenticationProvider - Authentication failed: password does not match stored value
2013-10-25 13:03:09,545 DEBUG tomcat-http--11 org.springframework.security.web.authentication.www.BasicAuthenticationFilter - Authentication request for failed: org.springframework.security.authentication.BadCredentialsException: Bad credentials
2013-10-25 13:00:30,136 DEBUG tomcat-http--9 org.springframework.security.web.context.SecurityContextPersistenceFilter - SecurityContextHolder now cleared, as request processing completed
...

The first case throws an AccessDeniedException which is being caught and sent to the commence method in my entry point, but the second case which throws a BadCredentialsException is not going to the entry point.

The weird thing here is that the commence method is supposed to receive an AuthenticationException, but AccessDeniedException is not an AuthenticationException but BadCredentialsException is, see the inheritance tree from the Spring security 3.1 API documentation:

java.lang.Object
  extended by java.lang.Throwable
      extended by java.lang.Exception
          extended by java.lang.RuntimeException
              extended by org.springframework.security.access.AccessDeniedException

java.lang.Object
  extended by java.lang.Throwable
      extended by java.lang.Exception
          extended by java.lang.RuntimeException
              extended by org.springframework.security.core.AuthenticationException
                  extended by org.springframework.security.authentication.BadCredentialsException

Why is the commence method called with an exception that is not the correct type and why is not called when having the BadCredentialsException that is of the correct type ?

Edited --- Implemented the answer by @Luke

The two described solutions use the custom AuthenticationEntryPoint shown in the question, the configuration needs to be modified choosing one of the two following options:

  1. Adding a custom BASIC_AUTH_FILTER:

    <http create-session="stateless" entry-point-ref="authenticationFailedEntryPoint">
        <intercept-url pattern="/**" access="ROLE_USER"/>
        <custom-filter position="BASIC_AUTH_FILTER" ref="authenticationFilter" /> 
    </http>
    
    <beans:bean id="authenticationFilter" class="org.springframework.security.web.authentication.www.BasicAuthenticationFilter">
        <beans:constructor-arg name="authenticationManager" ref="authenticationManager" />
        <beans:constructor-arg name="authenticationEntryPoint" ref="authenticationFailedEntryPoint" />
    </beans:bean>
    
  2. OR Adding the entry point to the http-basic element, IMO is the cleanest solution:

    <http create-session="stateless" entry-point-ref="authenticationFailedEntryPoint">
        <intercept-url pattern="/**" access="ROLE_USER"/>
        <http-basic entry-point-ref="authenticationFailedEntryPoint" />
    </http>
    
1
About the exception issue commented at the end of the question, it seems that the AccesDeniedException is being translated by the ExceptionTranlationFilter to an AuthenticationException. That is why the commence method in the custom entry point is being calledraspacorp
raspacorp - How to execute "commence" method although we have @ExceptionalHandler in place?Pra_A
@pashtika This is an issue that I had almost two years ago and was solved by using that custom entry point. I remember that exception handlers were not catching that exception as it happens in an early step. However I don't know if that is fixed in more recent versions of the framework, in which case just adding a proper exception handler will do the trick.raspacorp

1 Answers

4
votes

I think the problem is that, after a failed basic authentication, the call to the entry point is made directly from the BasicAuthenticationFilter, which by default will be the built-in implementation.

You need to also set the entry-point-ref attribute on the http-basic element to fix this.

Alternatively you can define the basic authentication filter as a bean and avoid the namespace completely.