Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring MVC AccessDeniedException 500 error received instead of custom 401 error for @PreAuthorized unauth requests

I'm writing a Java Spring MVC 4 REST app that'll sit between front-end devices (websites, mobile apps, etc) and a database. I have code below that will create a new session for each request (since REST is stateless), look at the Authorization header of the request, and will confirm the token is valid and request authenticated.

When a user is requesting a secure method without a valid token, I'm looking to redirect unauthorized requests from a 500 Access Is Denied message to a 401 Unauthorized message.

This is what I have so far.

AccessDeniedHandler:

public class Unauthorized401AccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
                       AccessDeniedException accessDeniedException)
            throws IOException, ServletException {

        response.setStatus(401);
    }
}

WebSecurityConfigurerAdapter:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .exceptionHandling()
                .accessDeniedHandler(new Unauthorized401AccessDeniedHandler());

    }
}

Filter:

public class SecurityFilter implements Filter {
    final static Logger logger = Logger.getLogger(SecurityFilter.class);

    @Override
    public void doFilter(ServletRequest req, ServletResponse res,
                         FilterChain chain) throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
        HttpSession session = request.getSession();

        String requestUri = request.getRequestURI();

        session.invalidate();
        SecurityContextHolder.clearContext();

        session = request.getSession(true); // create a new session
        SecurityContext ctx = SecurityContextHolder.createEmptyContext();


        boolean isLoggedIn = false;

        String token = null;
        String authorizationHeader = request.getHeader("authorization");
        if(authorizationHeader != null && authorizationHeader.startsWith("bearer")) {
            String encryptedToken = authorizationHeader.split(" ")[1];
            token = StringUtils.newStringUtf8(Base64.decodeBase64(encryptedToken));

            // confirm user is logged in and authorized here TBD

            isLoggedIn = true;
        }

        PreAuthenticatedAuthenticationToken authentication = null;
        if(isLoggedIn) {
            SessionCredentialsModel authRequestModel = new SessionCredentialsModel();
            authRequestModel.employeeId = 323;
            authRequestModel.firstName = "Danny";
            authRequestModel.lastName = "Boy";
            authRequestModel.token = "this_is_a_test_token";

            authentication = new PreAuthenticatedAuthenticationToken(authRequestModel, token);
        } else {
            authentication = new PreAuthenticatedAuthenticationToken(new SessionCredentialsModel(), null);
        }

        authentication.setAuthenticated(true);
        ctx.setAuthentication(authentication);

        SecurityContextHolder.setContext(ctx);
        chain.doFilter(req, res);
    }

Security Model (aka Security Context Principal):

public class SessionCredentialsModel {
    public int employeeId;
    public String firstName;
    public String lastName;
    public String token;

    public boolean isAuthenticated() {
        if(employeeId > 0 && token != null) {
            return true;
        }

        return false;
    }
}

and finally the controller:

  @RequestMapping(value = "/", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
    @PreAuthorize("principal.isAuthenticated()")
    public ResponseEntity<LoginResponseModel> create() {
        LoginResponseModel responseModel = new LoginResponseModel();
        responseModel.statusCode = 55;
        responseModel.token = "authorized model worked!";

        return new ResponseEntity<LoginResponseModel>(responseModel, HttpStatus.OK);
    }

When I run the method without an Authorization header I get this error (instead of the error I'm looking to get):

HTTP Status 500 - Request processing failed; nested exception is org.springframework.security.access.AccessDeniedException: Access is denied

type Exception report

message Request processing failed; nested exception is org.springframework.security.access.AccessDeniedException: Access is denied

description The server encountered an internal error that prevented it from fulfilling this request.

exception

org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.springframework.security.access.AccessDeniedException: Access is denied
    org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:982)
    org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:872)
    javax.servlet.http.HttpServlet.service(HttpServlet.java:648)
    org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846)
    javax.servlet.http.HttpServlet.service(HttpServlet.java:729)
    org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
    net.pacificentertainment.middletier.app.security.SecurityFilter.doFilter(SecurityFilter.java:73)
root cause

org.springframework.security.access.AccessDeniedException: Access is denied
    org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:84)
    org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:233)
    org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:65)
    org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:656)
    net.pacificentertainment.middletier.app.controllers.EmployeeController$$EnhancerBySpringCGLIB$$b6765b64.create(<generated>)
    sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    java.lang.reflect.Method.invoke(Method.java:498)
    org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205)
    org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:133)
    org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:116)
    org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:827)
    org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:738)
    org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85)
    org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:963)
    org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:897)
    org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970)
    org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:872)
    javax.servlet.http.HttpServlet.service(HttpServlet.java:648)
    org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846)
    javax.servlet.http.HttpServlet.service(HttpServlet.java:729)
    org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
    net.pacificentertainment.middletier.app.security.SecurityFilter.doFilter(SecurityFilter.java:73)
note The full stack trace of the root cause is available in the Apache Tomcat/8.0.43 logs.

Apache Tomcat/8.0.43

I'm at a loss to figure out why I can't get the unauthorized request to return a 401 -- or any other status code other than a 500.

What do you think?

like image 317
Dan Avatar asked Apr 22 '17 02:04

Dan


1 Answers

OK guys, wasn't able to get any help from the community BUT I did find a solution -- although it's not a direct solution.

@ControllerAdvice
public class SecurityExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler({AccessDeniedException.class})
    public ResponseEntity<Object> handleAccessDeniedException(Exception ex, WebRequest request) {
        if(ex.getMessage().toLowerCase().indexOf("access is denied") > -1) {
            return new ResponseEntity<Object>("Unauthorized Access", new HttpHeaders(), HttpStatus.UNAUTHORIZED);
        }

        return new ResponseEntity<Object>(ex.getMessage(), new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

This new file in my app will allow me to control what happens during an exception. Now I can just manually inspect the problem to see if it was "access is denied" and then redirect to 401 which DOES WORK. The problem above was that the code to redirect to the 401 wasn't ever being hit. This code DOES get executed.

Again, this isn't a direct solution as we're manipulating a different piece of Spring MVC and kind of hacking default behavior to get it to work.

If anyone has a more elegant solution, please do post.

like image 149
Dan Avatar answered Oct 28 '22 15:10

Dan