Refactor : More informative exception for Token Validation
This commit is contained in:
@@ -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,6 +104,8 @@ public class AuthController {
|
||||
@PostMapping("/signin")
|
||||
public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {
|
||||
|
||||
try {
|
||||
|
||||
Authentication authentication = authenticationManager
|
||||
.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));
|
||||
|
||||
@@ -120,6 +126,10 @@ public class AuthController {
|
||||
userDetails.getUsername(),
|
||||
userDetails.getEmail(),
|
||||
roles));
|
||||
} catch (AuthenticationException exception) {
|
||||
throw new SignInException(exception.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@PostMapping("/refresh-token")
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.hakimfauzi23.boilerplatespringsecurity.jwt.exception;
|
||||
package com.hakimfauzi23.boilerplatespringsecurity.exception.exception;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,9 @@ public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long
|
||||
|
||||
Optional<RefreshToken> findByToken(String token);
|
||||
|
||||
Optional<RefreshToken> findByUserId(Long userId);
|
||||
|
||||
|
||||
@Modifying
|
||||
int deleteByUser(User user);
|
||||
}
|
||||
|
||||
@@ -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<RefreshToken> 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());
|
||||
|
||||
Reference in New Issue
Block a user