'How to return HTTP 403 after successful authentication, but unsuccessful authorization?

I have a Spring Boot application which also uses Spring Security. I want to check if user has access to login to the application, but it must be after authentication. The point is that, at login, the user selects the project to which they must connect. A user can be allowed to connect to one project, but can not be allowed to connect to another project. However, if user enters invalid credentials, message about invalid credentials must be shown first even if user has no right to login to the selected project. For this reason, checking for the rights to the project must be after authentication.

This my SecurityConfig class:

package org.aze.accountingprogram.config;

import org.aze.accountingprogram.models.CurrentUser;
import org.aze.accountingprogram.models.PermissionAliasConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.encoding.Md5PasswordEncoder;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private static final Logger logger = LoggerFactory.getLogger(SecurityConfig.class);

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests().antMatchers("/lib/**").permitAll().anyRequest().fullyAuthenticated()
                .and()
                .formLogin().successHandler(successHandler()).loginPage("/login").permitAll()
                .and().exceptionHandling().accessDeniedHandler(accessDeniedHandler())
                .and()
                .logout().logoutUrl("/logout").logoutSuccessUrl("/login").permitAll();

        http.csrf().disable();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(new Md5PasswordEncoder());
    }

    private AccessDeniedHandler accessDeniedHandler() {
        return (request, response, e) -> {
            logger.debug("Returning HTTP 403 FORBIDDEN with message: \"{}\"", e.getMessage());
            response.sendError(HttpStatus.FORBIDDEN.value(), e.getMessage());
        };
    }

    private AuthenticationSuccessHandler successHandler() {
        return new AuthenticationSuccessHandler() {
            @Override
            public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                CurrentUser user = (CurrentUser) authentication.getPrincipal();
                if (!user.hasAccessRight(PermissionAliasConstants.LOGIN)) {
                    throw new AccessDeniedException(String.format("User \"%s\" is not authorized to login \"%s\" project", user.getUsername(), user.getProject().getName()));
                }
            }
        };
    }

}

implementation of UserDetailsService:

package org.aze.accountingprogram.serviceimpl;

import org.aze.accountingprogram.common.Constants;
import org.aze.accountingprogram.exceptions.DataNotFoundException;
import org.aze.accountingprogram.models.AccessRightsPermission;
import org.aze.accountingprogram.models.CurrentUser;
import org.aze.accountingprogram.models.Project;
import org.aze.accountingprogram.models.User;
import org.aze.accountingprogram.service.AccessRightsService;
import org.aze.accountingprogram.service.ProjectService;
import org.aze.accountingprogram.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserService userService;

    @Autowired
    private ProjectService projectService;

    @Autowired
    private AccessRightsService accessRightsService;
    
    @Autowired
    private HttpServletRequest request;

    private static final Logger logger = LoggerFactory.getLogger(UserDetailsServiceImpl.class);

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user;
        Project project;
        final String projectId = request.getParameter(Constants.SESSION_PROJECT_ID);
        logger.debug("Username: {}, projectId: {}", username, projectId);

        try {
            user = userService.getUserByUsername(username);
        } catch (DataNotFoundException e) {
            throw new UsernameNotFoundException(e.getMessage(), e);
        }

        // Value of projectId is from combo box which is filled from table of projects
        // That is why there is no probability that the project will not be found
        project = projectService.getProjectById(Integer.valueOf(projectId));

        // User can have different rights for different projects
        List<AccessRightsPermission> accessRights = accessRightsService.getAccessRightsByProject(user.getId(), project.getId());
        Set<GrantedAuthority> authorities = new HashSet<>(accessRights.size());
        authorities.addAll(accessRights.stream().map(right -> new SimpleGrantedAuthority(right.getAlias())).collect(Collectors.toList()));

        final CurrentUser currentUser = new CurrentUser(user, project, authorities);

        // If to check LOGIN access right to project here, and user entered right credentials
        // then user will see message about invalid credentials.
        // .and().exceptionHandling().accessDeniedHandler(accessDeniedHandler()) at SecurityConfig is not working in this case
//        if (!currentUser.hasAccessRight(PermissionAliasConstants.LOGIN)) {
//            throw new AccessDeniedException(String.format("User \"%s\" is not authorized to login \"%s\" project", user.getUsername(), project.getName()));
//        }

        logger.info("Logged in user: {}", currentUser);
        return currentUser;
    }
}

and LoginController

package org.aze.accountingprogram.controllers;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;

import java.util.Optional;

@Controller
public class LoginController {

    @RequestMapping(value = "/login", method = RequestMethod.GET)
    public ModelAndView getLoginPage(@RequestParam Optional<String> error) {
        return new ModelAndView("login", "error", error);
    }

}

successHandler() works and application throws AccessDeniedException if user has not access right to login to the project. But accessDeniedHandler() doesn't work and doesn't send HTTP 403. Instead I receive HTTP 500.

How to return response with HTTP 403 and message of exception (e.g. "User "tural" is not authorized to login "AZB" project") and to handle it in LoginController (using @ResponseStatus(HttpStatus.FORBIDDEN) or @ExceptionHandler)?



Solution 1:[1]

Not sure if this is what you are looking for, but you can inject the HttpServletResponse in the login POST method of your Controller.

Therefore, if your service notifies you that authorization is not OK, you can for example do

response.setStatus(403);

@RequestMapping(value = "/login", method = RequestMethod.POST)
public ModelAndView loginAction(@RequestParam String login, @RequestParam String pw, HttpServletResponse response) {
   doStuff(login,pw);
   if(service.isNotHappy(login,pw)){
      response.setStatus(403);
   }else{
      // happy flow
   }
   ...
}

// UPDATE :

since the

public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException 

method is the implementation of the org.springframework.security.core.userdetails.UserDetailsService interface, indeed you can't inject the HttpServletResponse like you would in a @RequestMapping annotated controller.

However, here is another (hopefully correct!) solution :

1) In the implementation of the loadUserByUsername(String x) method, if the user does not have the access rights, throw a CustomYourException

2) Create a new ExceptionHandlingController class annotated with @ControllerAdvice at class level (and make sure it gets scanned like your controllers) that contains a method like this :

@ControllerAdvice
public class ExceptionHandlingController {

   @ExceptionHandler({CustomYourException.class})
   public ModelAndView handleCustomExceptionError(HttpServletRequest request, HttpServletResponse response, CustomYourException cyee) {
       // this method will be triggered upon CustomYourException only
       // you can manipulate the response code here, 
       // return a custom view, use a special logger
       // or do whatever else you want here :)
   }
}

As you can see, this handler will be called based on what specific exceptions you define in the @ExceptionHandler annotation - very useful ! From there, you can manipulate the HttpServletResponse and set the response code you want, and/or send the user to a specific view.

Hope this solves your issue :)

Solution 2:[2]

maybe you are supposed to return; after response.sendError()

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Oktfolio