diff --git a/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/controllers/AuthController.java b/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/controllers/AuthController.java index 93c1d07..1471038 100644 --- a/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/controllers/AuthController.java +++ b/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/controllers/AuthController.java @@ -10,8 +10,9 @@ import com.hakimfauzi23.boilerplatespringsecurity.data.payload.request.TokenRefr import com.hakimfauzi23.boilerplatespringsecurity.data.payload.response.JwtResponse; import com.hakimfauzi23.boilerplatespringsecurity.data.payload.response.MessageResponse; import com.hakimfauzi23.boilerplatespringsecurity.data.payload.response.TokenRefreshResponse; +import com.hakimfauzi23.boilerplatespringsecurity.exception.exception.SignInException; import com.hakimfauzi23.boilerplatespringsecurity.jwt.JwtUtils; -import com.hakimfauzi23.boilerplatespringsecurity.jwt.exception.TokenRefreshException; +import com.hakimfauzi23.boilerplatespringsecurity.exception.exception.TokenRefreshException; import com.hakimfauzi23.boilerplatespringsecurity.repository.RoleRepository; import com.hakimfauzi23.boilerplatespringsecurity.repository.UserRepository; import com.hakimfauzi23.boilerplatespringsecurity.service.RefreshTokenService; @@ -22,11 +23,14 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.web.bind.annotation.*; +import java.sql.SQLException; +import java.sql.SQLIntegrityConstraintViolationException; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -100,26 +104,32 @@ public class AuthController { @PostMapping("/signin") public ResponseEntity authenticateUser(@Valid @RequestBody LoginRequest loginRequest) { - Authentication authentication = authenticationManager - .authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword())); + try { - SecurityContextHolder.getContext().setAuthentication(authentication); - String jwt = jwtUtils.generateJwtToken(authentication); + Authentication authentication = authenticationManager + .authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword())); - UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal(); - List roles = userDetails.getAuthorities().stream() - .map(GrantedAuthority::getAuthority) - .collect(Collectors.toList()); + SecurityContextHolder.getContext().setAuthentication(authentication); + String jwt = jwtUtils.generateJwtToken(authentication); - RefreshToken refreshToken = refreshTokenService.createRefreshToken(userDetails.getId()); + UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal(); + List roles = userDetails.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toList()); + + RefreshToken refreshToken = refreshTokenService.createRefreshToken(userDetails.getId()); + + return ResponseEntity.ok(new JwtResponse( + jwt, + refreshToken.getToken(), + userDetails.getId(), + userDetails.getUsername(), + userDetails.getEmail(), + roles)); + } catch (AuthenticationException exception) { + throw new SignInException(exception.getMessage()); + } - return ResponseEntity.ok(new JwtResponse( - jwt, - refreshToken.getToken(), - userDetails.getId(), - userDetails.getUsername(), - userDetails.getEmail(), - roles)); } @PostMapping("/refresh-token") diff --git a/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/exception/advice/AuthControllerAdvice.java b/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/exception/advice/AuthControllerAdvice.java new file mode 100644 index 0000000..13b7d29 --- /dev/null +++ b/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/exception/advice/AuthControllerAdvice.java @@ -0,0 +1,39 @@ +package com.hakimfauzi23.boilerplatespringsecurity.exception.advice; + +import com.hakimfauzi23.boilerplatespringsecurity.exception.exception.ErrorMessage; +import com.hakimfauzi23.boilerplatespringsecurity.exception.exception.SignInException; +import com.hakimfauzi23.boilerplatespringsecurity.exception.exception.TokenRefreshException; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.WebRequest; + +import java.util.Date; + +@RestControllerAdvice +public class AuthControllerAdvice { + + + @ExceptionHandler(value = SignInException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorMessage handleLoginException(SignInException ex, WebRequest request) { + return new ErrorMessage( + HttpStatus.BAD_REQUEST.value(), + new Date(), + ex.getMessage(), + request.getDescription(false) + ); + } + + @ExceptionHandler(value = TokenRefreshException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public ErrorMessage handleTokenRefreshException(TokenRefreshException ex, WebRequest request) { + return new ErrorMessage( + HttpStatus.FORBIDDEN.value(), + new Date(), + ex.getMessage(), + request.getDescription(false) + ); + } +} diff --git a/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/jwt/exception/ErrorMessage.java b/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/exception/exception/ErrorMessage.java similarity index 90% rename from src/main/java/com/hakimfauzi23/boilerplatespringsecurity/jwt/exception/ErrorMessage.java rename to src/main/java/com/hakimfauzi23/boilerplatespringsecurity/exception/exception/ErrorMessage.java index 270d622..2aa201c 100644 --- a/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/jwt/exception/ErrorMessage.java +++ b/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/exception/exception/ErrorMessage.java @@ -1,4 +1,4 @@ -package com.hakimfauzi23.boilerplatespringsecurity.jwt.exception; +package com.hakimfauzi23.boilerplatespringsecurity.exception.exception; import java.util.Date; diff --git a/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/exception/exception/SignInException.java b/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/exception/exception/SignInException.java new file mode 100644 index 0000000..d0b1524 --- /dev/null +++ b/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/exception/exception/SignInException.java @@ -0,0 +1,14 @@ +package com.hakimfauzi23.boilerplatespringsecurity.exception.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.BAD_REQUEST) +public class SignInException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public SignInException(String message) { + super(message); + } +} diff --git a/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/jwt/exception/TokenRefreshException.java b/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/exception/exception/TokenRefreshException.java similarity index 84% rename from src/main/java/com/hakimfauzi23/boilerplatespringsecurity/jwt/exception/TokenRefreshException.java rename to src/main/java/com/hakimfauzi23/boilerplatespringsecurity/exception/exception/TokenRefreshException.java index 4872cb3..1734db2 100644 --- a/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/jwt/exception/TokenRefreshException.java +++ b/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/exception/exception/TokenRefreshException.java @@ -1,4 +1,4 @@ -package com.hakimfauzi23.boilerplatespringsecurity.jwt.exception; +package com.hakimfauzi23.boilerplatespringsecurity.exception.exception; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; diff --git a/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/exception/exception/TokenValidationException.java b/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/exception/exception/TokenValidationException.java new file mode 100644 index 0000000..1bf2592 --- /dev/null +++ b/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/exception/exception/TokenValidationException.java @@ -0,0 +1,13 @@ +package com.hakimfauzi23.boilerplatespringsecurity.exception.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.security.core.AuthenticationException; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.UNAUTHORIZED) +public class TokenValidationException extends AuthenticationException { + + public TokenValidationException(String message) { + super(message); + } +} diff --git a/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/jwt/AuthEntryPointJwt.java b/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/jwt/AuthEntryPointJwt.java index 75bf137..70c8bec 100644 --- a/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/jwt/AuthEntryPointJwt.java +++ b/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/jwt/AuthEntryPointJwt.java @@ -1,17 +1,23 @@ package com.hakimfauzi23.boilerplatespringsecurity.jwt; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.hakimfauzi23.boilerplatespringsecurity.exception.exception.ErrorMessage; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; import java.util.HashMap; import java.util.Map; @@ -25,14 +31,22 @@ public class AuthEntryPointJwt implements AuthenticationEntryPoint { response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + ErrorMessage body = new ErrorMessage( + HttpStatus.UNAUTHORIZED.value(), + new Date(), + "General Authentication Error (Invalid JWT, Unknown JWT Format), Please get new JWT Token!", + String.format("uri=%s", request.getRequestURI()) + ); - final Map body = new HashMap<>(); - body.put("status", HttpServletResponse.SC_UNAUTHORIZED); - body.put("error", "Unauthorized"); - body.put("message", authException.getMessage()); - body.put("path", request.getServletPath()); final ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + mapper.setDateFormat(dateFormat); + mapper.writeValue(response.getOutputStream(), body); } } diff --git a/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/jwt/AuthTokenFilter.java b/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/jwt/AuthTokenFilter.java index 27d363f..335a5a2 100644 --- a/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/jwt/AuthTokenFilter.java +++ b/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/jwt/AuthTokenFilter.java @@ -1,11 +1,18 @@ package com.hakimfauzi23.boilerplatespringsecurity.jwt; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.hakimfauzi23.boilerplatespringsecurity.exception.exception.ErrorMessage; +import com.hakimfauzi23.boilerplatespringsecurity.exception.exception.TokenValidationException; import com.hakimfauzi23.boilerplatespringsecurity.service.UserDetailsServiceImpl; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; @@ -14,6 +21,8 @@ import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; public class AuthTokenFilter extends OncePerRequestFilter { @@ -28,6 +37,7 @@ public class AuthTokenFilter extends OncePerRequestFilter { throws ServletException, IOException { try { String jwt = parseJwt(request); + if (jwt != null && jwtUtils.validateJwtToken(jwt)) { String username = jwtUtils.getUserNameFromJwtToken(jwt); @@ -42,8 +52,25 @@ public class AuthTokenFilter extends OncePerRequestFilter { SecurityContextHolder.getContext().setAuthentication(authentication); } - } catch (Exception e) { - logger.error("Cannot set user authentication: {}", e); + } catch (TokenValidationException e) { + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + + ErrorMessage body = new ErrorMessage( + HttpStatus.UNAUTHORIZED.value(), + new Date(), + e.getMessage(), + String.format("uri=%s", request.getRequestURI()) + ); + + final ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + mapper.setDateFormat(dateFormat); + + mapper.writeValue(response.getOutputStream(), body); + return; } filterChain.doFilter(request, response); diff --git a/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/jwt/JwtUtils.java b/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/jwt/JwtUtils.java index a2950ff..d81e5f6 100644 --- a/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/jwt/JwtUtils.java +++ b/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/jwt/JwtUtils.java @@ -1,9 +1,11 @@ package com.hakimfauzi23.boilerplatespringsecurity.jwt; +import com.hakimfauzi23.boilerplatespringsecurity.exception.exception.TokenValidationException; import com.hakimfauzi23.boilerplatespringsecurity.service.UserDetailsImpl; import io.jsonwebtoken.*; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.security.SignatureException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; @@ -49,21 +51,16 @@ public class JwtUtils { .parseClaimsJws(token).getBody().getSubject(); } - public boolean validateJwtToken(String authToken) { + public boolean validateJwtToken(String authToken) throws TokenValidationException { try { Jwts.parserBuilder().setSigningKey(key()).build().parse(authToken); return true; - } catch (MalformedJwtException e) { + } catch (MalformedJwtException | ExpiredJwtException | UnsupportedJwtException | IllegalArgumentException | + SignatureException e) { LOGGER.error("Invalid JWT token: {}", e.getMessage()); - } catch (ExpiredJwtException e) { - LOGGER.error("JWT token is expired: {}", e.getMessage()); - } catch (UnsupportedJwtException e) { - LOGGER.error("JWT token is unsupported: {}", e.getMessage()); - } catch (IllegalArgumentException e) { - LOGGER.error("JWT claims string is empty: {}", e.getMessage()); + throw new TokenValidationException(e.getMessage()); } - return false; } } diff --git a/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/jwt/advice/TokenControllerAdvice.java b/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/jwt/advice/TokenControllerAdvice.java deleted file mode 100644 index 6cad602..0000000 --- a/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/jwt/advice/TokenControllerAdvice.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.hakimfauzi23.boilerplatespringsecurity.jwt.advice; - -import com.hakimfauzi23.boilerplatespringsecurity.jwt.exception.ErrorMessage; -import com.hakimfauzi23.boilerplatespringsecurity.jwt.exception.TokenRefreshException; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestControllerAdvice; -import org.springframework.web.context.request.WebRequest; - -import java.util.Date; - -@RestControllerAdvice -public class TokenControllerAdvice { - - @ExceptionHandler(value = TokenRefreshException.class) - @ResponseStatus(HttpStatus.FORBIDDEN) - public ErrorMessage handleTokenRefreshException(TokenRefreshException ex, WebRequest request) { - return new ErrorMessage( - HttpStatus.FORBIDDEN.value(), - new Date(), - ex.getMessage(), - request.getDescription(false) - ); - } -} diff --git a/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/repository/RefreshTokenRepository.java b/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/repository/RefreshTokenRepository.java index 6963766..0f32961 100644 --- a/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/repository/RefreshTokenRepository.java +++ b/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/repository/RefreshTokenRepository.java @@ -13,6 +13,9 @@ public interface RefreshTokenRepository extends JpaRepository findByToken(String token); + Optional findByUserId(Long userId); + + @Modifying int deleteByUser(User user); } diff --git a/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/service/RefreshTokenService.java b/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/service/RefreshTokenService.java index 3f900f2..6d36c2a 100644 --- a/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/service/RefreshTokenService.java +++ b/src/main/java/com/hakimfauzi23/boilerplatespringsecurity/service/RefreshTokenService.java @@ -1,7 +1,8 @@ package com.hakimfauzi23.boilerplatespringsecurity.service; import com.hakimfauzi23.boilerplatespringsecurity.data.RefreshToken; -import com.hakimfauzi23.boilerplatespringsecurity.jwt.exception.TokenRefreshException; +import com.hakimfauzi23.boilerplatespringsecurity.exception.exception.SignInException; +import com.hakimfauzi23.boilerplatespringsecurity.exception.exception.TokenRefreshException; import com.hakimfauzi23.boilerplatespringsecurity.repository.RefreshTokenRepository; import com.hakimfauzi23.boilerplatespringsecurity.repository.UserRepository; import org.springframework.beans.factory.annotation.Autowired; @@ -30,8 +31,10 @@ public class RefreshTokenService { } public RefreshToken createRefreshToken(Long userId) { - RefreshToken refreshToken = new RefreshToken(); + Optional isAlreadyExist = refreshTokenRepository.findByUserId(userId); + isAlreadyExist.ifPresent(refreshToken -> refreshTokenRepository.delete(refreshToken)); + RefreshToken refreshToken = new RefreshToken(); refreshToken.setUser(userRepository.findById(userId).get()); refreshToken.setExpirationDate(Instant.now().plusMillis(refreshTokenDurationMs)); refreshToken.setToken(UUID.randomUUID().toString());