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 :
- 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(); } }
- 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(); .... }