Home > Back-end >  Return custom error message to login entry point with eventListeners
Return custom error message to login entry point with eventListeners

Time:10-13

In a rest API, i implemented 2 event listners to handle Authentication success and failure. It works fine and I do have a 403 error but i want to return a JSON Message.

For my login I implemented, the following :

@PostMapping("/login")
public ResponseEntity<UserResponse> loadUserByUsername(@RequestBody UserDetailsRequestModel userDetails) {
    if(userDetails.getEmail().isEmpty() || userDetails.getPassword().isEmpty()) {
        throw new UserServiceException(ErrorMessages.MISSING_REQUIRED_FIELD.getErrorMessage());
    }
    authenticate(userDetails.getEmail(), userDetails.getPassword());
    UserResponse userRestResponseModel = new UserResponse();

    ModelMapper modelMapper = new CustomMapper();
    modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STANDARD);

    UserDto loggedInUser = userService.getUser(userDetails.getEmail());

    userRestResponseModel = modelMapper.map(loggedInUser, UserResponse.class);
    // retrieve authorities manually
    for(RoleDto roleDto: loggedInUser.getRoles()) {
        Collection<AuthorityDto> authorityDtos = authorityService.getRoleAuthorities(roleDto);
        roleDto.setAuthorities(authorityDtos);
    }
    UserPrincipalManager userPrincipal = new UserPrincipalManager(modelMapper.map(loggedInUser, UserEntity.class));

    // authorities are not fetched ... so we'll fetch them manually
    HttpHeaders jwtHeader = getJwtHeader(userPrincipal);

    ResponseEntity<UserResponse> returnValue =
            new ResponseEntity<>(userRestResponseModel, jwtHeader, HttpStatus.OK);

    return returnValue;
}


private void authenticate(String userName, String password) {
    AuthenticationManager authenticationManager =
            (AuthenticationManager) SpringApplicationContext.getBean("authenticationManager");
    authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(userName, password));
}
private HttpHeaders getJwtHeader(UserPrincipalManager userPrincipal) {
    HttpHeaders headers = new HttpHeaders();
    String token = jwtTokenProvider.generateJwtToken(userPrincipal);
    headers.add(SecurityConstants.TOKEN_PREFIX, token);
    return headers;
}


@Component
public class AuthenticationFailureListener {
    private final LoginAttemptService loginAttemptService;


    @Autowired
    public AuthenticationFailureListener(LoginAttemptService loginAttemptService) {
        this.loginAttemptService = loginAttemptService;
    }

    @EventListener
    public void onAuthenticationFailure(AuthenticationFailureBadCredentialsEvent event) throws ExecutionException {
        Object principal = event.getAuthentication().getPrincipal();
        if (principal instanceof String) {
            String username = (String) event.getAuthentication().getPrincipal();
            loginAttemptService.addUserToLoginAttemptCache(username);
        }
    }
}

In my loginAttemptService I try to prepare a return to a rest response.

@Override
public void addUserToLoginAttemptCache(String username) {
    int attempts = 0;
    try {
        attempts = SecurityConstants.AUTH_ATTEMPT_INCREMENT   loginAttemptCache.get(username);
        loginAttemptCache.put(username, attempts);
        String message = "";
        if(!errorContext.isHasExceededMaxAttempts()) {
            message = "Invalid email or password. You tried : "   attempts   "/"   SecurityConstants.MAX_AUTH_ATTEMPTS;
        } else {
            message = "You reached "   attempts   " attempts. Account is now locked for "   SecurityConstants.LOCK_DURATION   " min";
        }
        throw new SecurityServiceException(message);
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
}

My issue is the following: using ControllerAdvice won't work because the error is handled before it could reach it. How can I then return a JSON response to the client ?

CodePudding user response:

I did find a trick for this issue. I created a ManagedBean class

@Data
@Builder
@AllArgsConstructor @NoArgsConstructor
@ManagedBean @ApplicationScope
public class ServletContext {
    private HttpServletRequest request;
    private HttpServletResponse response;
}

I inject it in my AuthenticationFilter custom class. Here in my attemptAuthentication method I can get access to HttpServletRequest and HttpServletResponse objects. I just have to set my ServletContext object with the request and the response.

@Override
public Authentication attemptAuthentication(HttpServletRequest request,
                                            HttpServletResponse response) throws AuthenticationException {
    // we may need to pass request and response object if we fail authentication,
    servletContext.setRequest(request);
    servletContext.setResponse(response);

    // spring tries to authenticate user
    try {
        UserLoginRequestModel creds = new ObjectMapper()
                .readValue(request.getInputStream(), UserLoginRequestModel.class);

        // we return authentication with email and password
        return authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                        creds.getEmail(),
                        creds.getPassword(),
                        new ArrayList<>()
                )
        );
    } catch (IOException e) {
        e.printStackTrace();
        throw new RuntimeException(e);
    }
}

Now in my AuthenticationFailureListener, I also inject my ServletContext class and retrieve the values in the method that handle onAuthenticationFailure:

@EventListener
public void onAuthenticationFailure(AuthenticationFailureBadCredentialsEvent event) throws ExecutionException, IOException {
    System.out.println(event);
    Object principal = event.getAuthentication().getPrincipal();
    if (principal instanceof String) {
        String username = (String) event.getAuthentication().getPrincipal();
        loginAttemptService.addUserToLoginAttemptCache(username);
        int attempts = loginAttemptService.getLoginAttempts(username);
        String message;
        if(!loginAttemptService.hasExceededMaxAttempts(username)) {
            message = "Invalid email or password. You tried : "   attempts   "/"   SecurityConstants.MAX_AUTH_ATTEMPTS;
        } else {
            message = "You reached "   attempts   " attempts. Account is now locked for "   SecurityConstants.LOCK_DURATION   " min";
        }
        ErrorMessageResponse errorMessageResponse = new ErrorMessageResponse(new Date(), message);
        HttpServletResponse response = servletContext.getResponse();
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        new ObjectMapper().writeValue(response.getOutputStream(), errorMessageResponse);
    }
}

At this stage, I do have HttpServletResponse object and I can use it to write value. I do believe there may be more elegant ways to handle this, but it works fine.

CodePudding user response:

You can use .accessDeniedHandler at your HttpSecurity in you Security Config.

Below Simple way de return JSON For 403 error :

  1. Define a private method in Config Security like this :
@Configuration
@EnableWebSecurity
public class SecurityConfig  extends WebSecurityConfigurerAdapter {

.....

private void writeResponse(HttpServletResponse httpResponse, int code, String message) throws IOException {
        httpResponse.setContentType("application/json");
        httpResponse.setStatus(code);
        httpResponse.getOutputStream().write(("{\"code\":"   code   ",").getBytes());
        httpResponse.getOutputStream().write(("\"message\":\""   message   "\"}").getBytes());
        httpResponse.getOutputStream().flush();
    }

}
  1. Add exceptionHandling at your HttpSecurity Config :
@Override
protected void configure(HttpSecurity http) throws Exception {
....
http  = http.exceptionHandling()
                .accessDeniedHandler((request, response, accessDeniedException) -> {
                            this.writeResponse(response,HttpServletResponse.SC_FORBIDDEN,accessDeniedException.getMessage());
                        }
                        )
                .and();

....

}
  • Related