I trying to implement spring security + jwt.I done log-in and log-out methods, jwt filter,provider and web config is configured. So the main problem is my controller and how to return error messages to user, for example if user typed wrong password/username or user account is banned etc.
I got a structure built on exception handling, looks terible.
controller
#PostMapping("/log-in")
public ResponseEntity logIn(#RequestBody UserDto userDto) {
log.info("[LOG-IN] user with username " + userDto.getUsername());
try {
HashMap<String, String> response = userService.logIn(userDto);
return ResponseEntity.ok(response);
} catch (UserStatusException ex) {
return ResponseEntity.badRequest().body("Account is Pending");
} catch (UsernameNotFoundException ex) {
return ResponseEntity.badRequest().body("Could not find account!");
} catch (AuthenticationException ex) {
log.error("Wrong username or password!");
return ResponseEntity.badRequest().body("Wrong username or password!");
}
}
service
#Override
public HashMap<String, String> logIn(UserDto userDto)throws AuthenticationException, UserStatusException{
User user = findByUsername(userDto.getUsername());
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(userDto.getUsername(), userDto.getPassword())); //login
checkUserStatus(user); //check if user pending or banned
user.setUserStatus(UserStatus.ACTIVE);
String token = jwtTokenProvider.createToken(user.getUsername(), user.getUserRoles());
HashMap<String, String> response = new HashMap<>();
response.put("token", token);
response.put("username", user.getUsername());
userRepository.save(user);
return response;
}
#Override
public User findByUsername(String username)throws UsernameNotFoundException {
log.info("[UserService, findByUsername]");
User user = userRepository.findByUsername(username);
if(user == null){
log.error("User not found with {} username: ", username);
throw new UsernameNotFoundException("User not found!");
}
log.info("User {} successfully loaded ",username);
return user;
}
#Override
public void checkUserStatus(User user)throws UserStatusException {
if (user.getUserStatus().equals(UserStatus.BANNED)
|| user.getUserStatus().equals(UserStatus.PENDING)) {
throw new UserStatusException("Not confirmed");
}
}
Is there any other way to replace this structure?
You should use a ControllerAdvice (see a tutorial here).
It's a special class that look like this
#ControllerAdvice
public class ControllerAdvice {
#ExceptionHandler(PersonNotFoundException.class)
public ResponseEntity <VndErrors > notFoundException(final PersonNotFoundException e) {
return error(e, HttpStatus.NOT_FOUND, e.getId().toString());
}
}
It will allow you to bind a specific return code and response to each exception you need to handle, and it will automatically catch all exception returned by your controller. It's also a good way to handle all exceptions at the same place instead of over each exceptions...
I'm not sure about it, but I think you even can bind it to a specific mapping of your API for more granularity.
Hope this help! Have fun!
You could add the repose status directly to your custom exception class:
#ResponseStatus(HttpStatus.BAD_REQUEST)
public class UsernameNotFoundException extends RuntimeException {
public UsernameNotFoundException(String message) {
super(message);
}
public UsernameNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}
In this way you don't need anymore to catch them in the controller and add the message and the status in ResponseEntity.
Related
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 ?
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.
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();
....
}
I'm very confused with this issue. When I print username I get the correct data I sent, however, credentials for some reasons returns null, unless I hardcode the data (this is not the case if when I do this same call in the controller.
But not only that, even when I hardcode the data with .findByPhoneNumber("123456789"), and successfully find a Credentials object in my database, the controller is still throwing an exception.
#Service
public class MyUserDetailsService implements UserDetailsService {
#Autowired
CredentialsRepository credentialsRepository;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Credentials credentials = credentialsRepository.findByPhoneNumber(username);
System.out.println(username);
System.out.println(credentials);
if (credentials == null) {
throw new UsernameNotFoundException("Incorrect credentials");
}
User userDetails = new User(credentials.getPhoneNumber(), credentials.getOtp(),
new ArrayList<>());
return userDetails;
}
}
try {
System.out.println("Here1");
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(phoneNumber, otp)
);
}
catch (BadCredentialsException e) {
System.out.println("Here2");
throw new Exception("Incorrect username or password", e);
}
In other words, I am always getting a BadCredentialsException, regardless of the successful return or not of loadUserByUsername.
Spring 5 has introduced ResponseStatusException, is it a good practice to throw this exception directly from the service layer.
Case 1:
#Service
public class UserService {
public User findUserByName(String username) {
User user = userRepository.findByUsernName(username);
if(null == user) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "user not found");
}
}
}
Case 2:
Or do we need to use custom exception and handle it in controller level ? We are catching the CustomException and throwing ResponseStatusException in this case, why do we have to catch the custom exception again instead of going with Case 1
#Service
public class UserService {
public User findUserByName(String username) {
User user = userRepository.findByUsernName(username);
if(null == user) {
throw new UserNotFoundException("user not found");
}
}
}
#RestController
public class UserController {
#GetMapping(path="/get-user")
public ResponseEntity<User> getUser(String username) {
try {
userService.findUserByName(username);
} catch (UserNotFoundException ex) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "user not found");
}
}
}
As it was mentioned in comments, you can create mapping in your error. Then you do not need to use try block in controller.
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
#ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "user not found")
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
}
I am implementing a REST api webservice which is fetching data from a MySql database. It is written here that we dont need to handle database exception explicitly. I have catch blocks in the Service layer. I have the following questions.
1- How do i send the appropriate error message to the respective model view from the catch block?
2- Is Service the right layer to catch the exception?
I have the following code
Controller
#RequestMapping(value = "/saveUser", method = RequestMethod.POST)
public ModelAndView saveUser(#ModelAttribute User user, BindingResult result)
{
ModelAndView mv = new ModelAndView();
validator.validate(user, result);
if(result.hasErrors()) {
mv.setViewName("addUser");
}
else {
service.saveUser(user);
mv.setViewName("redirect:/users/listAllUsers");
}
return mv;
}
Service
public void saveUser(User user) {
try {
userDao.saveUser(user);
} catch(DuplicateKeyException e) {
//Here i want to send "User already exist"
} catch(DataAccessException e) {
//Here i want to send "Databae unreachable"
}
}
UserDAO
public void saveUser(User user) {
String sql = "INSERT INTO User (fname, lname, address, phone)"
+ " VALUES (?, ?, ?, ?)";
jdbcTemplate.update(sql, user.getFname(), user.getLname(),
user.getAddress(), user.getPhone());
}
}
#dbreaux 's answer is correct.You should customize an exception.
public class UserException extends RuntimeException {
// You can add some custom variables
// such as error codes, error types, etc.
}
Then,you should define a ControllerAdvice to handle this Exception:
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
#ControllerAdvice
public class UserControllerAdvice{
#ExceptionHandler(value = UserException.class)
public ModelAndView handleUserException(UserException ex){
// Generate corresponding results(ModelAndView) based on exception.
// example: Put the error message to model.
return new ModelAndView("prompt_page",ex.getMessage());
}
}
Last,you can throws UserException in your Service.
public void saveUser(User user) {
try {
userDao.saveUser(user);
} catch(DuplicateKeyException e) {
throw new UserException("User already exist");
} catch(DataAccessException e) {
throw new UserException("Databae unreachable");
}
}
You want to isolate the web-specific behaviors from the service layer, and from the data layer, and I think the best way to do that is to throw a new, checked, domain-specific Exception that matches the meaning of each case you want to handle differently in the Controller.
For example, DuplicateUserException, SystemUnavailableException. Then the Controller catches those and adds the correct case to the Model.
Here's the scenario :
I created the following custom response exception, to fire the 401 Http Status :
#ResponseStatus(value = HttpStatus.UNAUTHORIZED)
public class HttpUnauthorizedException extends RuntimeException {
}
The controller that uses the exception :
#Controller
public UserController {
#RequestMapping(value = "api/user")
#ResponseBody
public String doLogin(
#RequestParam(value = "username", required = false) String username, #RequestParam(value = "password", required = false) String password) {
if(userLoggedIn(String username, String password)) {
return "OK";
}
else {
throw new HttpUnauthorizedException();
}
}
...
}
Now when I try to access the controller to see the 401 exception, the server fires the Http error code 500 instead. But interestingly enough, when I try with the HttpStatus.NOT_FOUND it actually works, the server fires 404. Is there something I'm missing on here?
Thanks in advance :-)
First throw new HttpUnauthorizedException();
then you can catch it at a normal controller that have #ControllerAdvice annotation
#ControllerAdvice // To Handle Exceptions
public class ExceptionController {
//// ...........
#ExceptionHandler({HttpUnauthorizedException.class})
#ResponseBody
#ResponseStatus(value = HttpStatus.UNAUTHORIZED)
Map<String, String> unauthorizedAccess(Exception e) {
Map<String, String> exception = new HashMap<String, String>();
log.error("unauthorized Access to the API: " + e.getMessage(), e);
exception.put("code", "401");
exception.put("reason", e.getMessage());
return exception;
}
}
I think code should be much simpler, maybe the answer was written with old Spring version.
In this example I've implemented method to handle exception - HttpClientErrorException.Unauthorized to cover authentication issue (401):
#ControllerAdvice
public class MyErrorsHandler extends ResponseEntityExceptionHandler
{
#ExceptionHandler(HttpClientErrorException.Unauthorized.class)
protected ResponseEntity<Object> handleAuthenticationError(RuntimeException ex, WebRequest request)
{
return handleExceptionInternal(ex,
"Cannot login, please check your inputs",
new HttpHeaders(), HttpStatus.UNAUTHORIZED, request);
}
}
Finally I get correct error to GUI