Peter Penzov
Peter Penzov

Reputation: 1680

Create custom messages for expired and locked user

I want to return custom message if user during authentication process is locked or expired. I tried to implement this:

@Service
public class UserDetailsHandler implements UserDetailsService {

    @Autowired
    private UsersService usersService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        final Optional<Users> user = usersService.findByLogin(username);

        if (!user.isPresent()) {
            throw new UsernameNotFoundException("User '" + username + "' not found");
        }

        return user
            .map(value -> {
                return new User(
                        value.getLogin(),
                        value.getEncryptedPassword(),
                        value.getEnabled(),
                        hasAccountExpired(value.getExpiredAt()),
                        hasPasswordExpired(value.getPasswordChangedAt()),
                        hasAccountLocked(value.getLockedAt()),
                        Collections.singleton(new SimpleGrantedAuthority(value.getRole().getAuthority()))
                );
            }).orElseThrow(() -> new UsernameNotFoundException("User with username " + username + " not found"));
    }

    private boolean hasAccountExpired(LocalDateTime account_expired_at) {

        return account_expired_at == null;
    }

Full code: GitHub

The question is how to create handlers which return some custom message if the validation returns true value for statuses user locked or user expired?

Upvotes: 2

Views: 2320

Answers (4)

Kunal Vohra
Kunal Vohra

Reputation: 2846

Its simple 2 steps approach. User expired means token expired

Step 1 Modify JWTTokenProvider Class to add a custom header to Http Servlet Request using setAttribute() method.

JwtTokenProvider.java

public boolean validateToken(String token,HttpServletRequest httpServletRequest){
        try {
            Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token);
            return true;
        }catch (SignatureException ex){
            System.out.println("Invalid JWT Signature");
        }catch (MalformedJwtException ex){
            System.out.println("Invalid JWT token");
        }catch (ExpiredJwtException ex){
            System.out.println("Expired JWT token");
            httpServletRequest.setAttribute("expired",ex.getMessage());
        }catch (UnsupportedJwtException ex){
            System.out.println("Unsupported JWT exception");
        }catch (IllegalArgumentException ex){
            System.out.println("Jwt claims string is empty");
        }
        return false;
}

Step 2

Modify commence method in JwtAuthenticationEntryPoint.class to check expired header in http servlet request header that we added in step 1.

JwtAuthenticationEntryPoint.java

@Override
    public void commence(HttpServletRequest httpServletRequest,
                         HttpServletResponse httpServletResponse,
                         AuthenticationException e) throws IOException, ServletException {

        final String expired = (String) httpServletRequest.getAttribute("expired");
        System.out.println(expired);
        if (expired!=null){
            httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED,expired);
        }else{
            httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED,"Invalid Login details");
        }
}

It is a best practice to return detailed error messages in any REST API. We used this to customize spring rest jwt token expired response to return more detailed error response. We can use this method not only for token expired but also for other jwt token exceptions like SignatureException, Malformed JwtException, UnsupportedJwtException and IllegalArgumentException.

Upvotes: 1

Romil Patel
Romil Patel

Reputation: 13777

Spring Security uses the messages.properties which consist of default messages, we can add our custom message with the same. Add messages.properties and add a message as shown below.

AccountStatusUserDetailsChecker.expired=User account has expired
AccountStatusUserDetailsChecker.locked=User account is locked
AbstractUserDetailsAuthenticationProvider.expired=User account has expired
AbstractUserDetailsAuthenticationProvider.locked=User account is locked

You may find the default messages here

Upvotes: 1

Ken Chan
Ken Chan

Reputation: 90497

Well I briefly look at your codes and you implement a JwtTokenFilter that will some how calls the UserDetailsHandler .

In JwtTokenFilter , you already catch and handle EngineException which contain the HTTP status codes and a message. An HTTP response will be sent out which the status and the body message that are the same as what defined in the caught EngineException

It seems that you already configure everything for it to work , so just simply throw EngineException with the suitable HTTP status code and message from the UserDetailsHandler . Something like :

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
         Users user = usersService.findByLogin(username)
                    .map(value -> {
                        return new User(
                            value.getLogin(),
                            value.getEncryptedPassword(),
                            value.getEnabled(),
                            hasAccountExpired(value.getExpiredAt()),
                            hasPasswordExpired(value.getPasswordChangedAt()),
                            hasAccountLocked(value.getLockedAt()),
                            Collections.singleton(new SimpleGrantedAuthority(value.getRole().getAuthority()))
                    ).orElseThrow(()-> throw new UsernameNotFoundException("User '" + username + "' not found"));

             if (user.isAccountLock()){
                throw new EngineException(HttpStatus.UNAUTHORIZED , "Custom message for account lock ......")
             }

             if(user.isAccountExpired()){
                throw new EngineException(HttpStatus.UNAUTHORIZED , "Custom message for account expired... ......")
             }

    }

Upvotes: 1

user731136
user731136

Reputation:

The best option for you is:

  1. Implement Spring UserDetails in your entity Users.
  2. Check in loadUserByUsername if the user has been locked, etc using Spring AccountStatusUserDetailsChecker class.
  3. Add into your EngineExceptionHandler the required methods to manage those exceptions: LockedException, AccountExpiredException, etc

You will see examples of above points in the following links:

Point 1

Point 2

Points 2-3

Upvotes: 2

Related Questions