Refactor : More informative exception for Token Validation

This commit is contained in:
Hanif Hakim
2024-01-21 17:17:22 +07:00
parent c607230f51
commit f430a2239a
12 changed files with 157 additions and 63 deletions

View File

@@ -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.JwtResponse;
import com.hakimfauzi23.boilerplatespringsecurity.data.payload.response.MessageResponse; import com.hakimfauzi23.boilerplatespringsecurity.data.payload.response.MessageResponse;
import com.hakimfauzi23.boilerplatespringsecurity.data.payload.response.TokenRefreshResponse; 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.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.RoleRepository;
import com.hakimfauzi23.boilerplatespringsecurity.repository.UserRepository; import com.hakimfauzi23.boilerplatespringsecurity.repository.UserRepository;
import com.hakimfauzi23.boilerplatespringsecurity.service.RefreshTokenService; 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.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.sql.SQLException;
import java.sql.SQLIntegrityConstraintViolationException;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@@ -100,26 +104,32 @@ public class AuthController {
@PostMapping("/signin") @PostMapping("/signin")
public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) { public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {
Authentication authentication = authenticationManager try {
.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authentication); Authentication authentication = authenticationManager
String jwt = jwtUtils.generateJwtToken(authentication); .authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));
UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal(); SecurityContextHolder.getContext().setAuthentication(authentication);
List<String> roles = userDetails.getAuthorities().stream() String jwt = jwtUtils.generateJwtToken(authentication);
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());
RefreshToken refreshToken = refreshTokenService.createRefreshToken(userDetails.getId()); UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
List<String> 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") @PostMapping("/refresh-token")

View File

@@ -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)
);
}
}

View File

@@ -1,4 +1,4 @@
package com.hakimfauzi23.boilerplatespringsecurity.jwt.exception; package com.hakimfauzi23.boilerplatespringsecurity.exception.exception;
import java.util.Date; import java.util.Date;

View File

@@ -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);
}
}

View File

@@ -1,4 +1,4 @@
package com.hakimfauzi23.boilerplatespringsecurity.jwt.exception; package com.hakimfauzi23.boilerplatespringsecurity.exception.exception;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.ResponseStatus;

View File

@@ -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);
}
}

View File

@@ -1,17 +1,23 @@
package com.hakimfauzi23.boilerplatespringsecurity.jwt; package com.hakimfauzi23.boilerplatespringsecurity.jwt;
import com.fasterxml.jackson.databind.ObjectMapper; 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.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.io.IOException; import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@@ -25,14 +31,22 @@ public class AuthEntryPointJwt implements AuthenticationEntryPoint {
response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); 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(); 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); mapper.writeValue(response.getOutputStream(), body);
} }
} }

View File

@@ -1,11 +1,18 @@
package com.hakimfauzi23.boilerplatespringsecurity.jwt; 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 com.hakimfauzi23.boilerplatespringsecurity.service.UserDetailsServiceImpl;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired; 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.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
@@ -14,6 +21,8 @@ import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException; import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class AuthTokenFilter extends OncePerRequestFilter { public class AuthTokenFilter extends OncePerRequestFilter {
@@ -28,6 +37,7 @@ public class AuthTokenFilter extends OncePerRequestFilter {
throws ServletException, IOException { throws ServletException, IOException {
try { try {
String jwt = parseJwt(request); String jwt = parseJwt(request);
if (jwt != null && jwtUtils.validateJwtToken(jwt)) { if (jwt != null && jwtUtils.validateJwtToken(jwt)) {
String username = jwtUtils.getUserNameFromJwtToken(jwt); String username = jwtUtils.getUserNameFromJwtToken(jwt);
@@ -42,8 +52,25 @@ public class AuthTokenFilter extends OncePerRequestFilter {
SecurityContextHolder.getContext().setAuthentication(authentication); SecurityContextHolder.getContext().setAuthentication(authentication);
} }
} catch (Exception e) { } catch (TokenValidationException e) {
logger.error("Cannot set user authentication: {}", 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); filterChain.doFilter(request, response);

View File

@@ -1,9 +1,11 @@
package com.hakimfauzi23.boilerplatespringsecurity.jwt; package com.hakimfauzi23.boilerplatespringsecurity.jwt;
import com.hakimfauzi23.boilerplatespringsecurity.exception.exception.TokenValidationException;
import com.hakimfauzi23.boilerplatespringsecurity.service.UserDetailsImpl; import com.hakimfauzi23.boilerplatespringsecurity.service.UserDetailsImpl;
import io.jsonwebtoken.*; import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys; import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SignatureException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
@@ -49,21 +51,16 @@ public class JwtUtils {
.parseClaimsJws(token).getBody().getSubject(); .parseClaimsJws(token).getBody().getSubject();
} }
public boolean validateJwtToken(String authToken) { public boolean validateJwtToken(String authToken) throws TokenValidationException {
try { try {
Jwts.parserBuilder().setSigningKey(key()).build().parse(authToken); Jwts.parserBuilder().setSigningKey(key()).build().parse(authToken);
return true; return true;
} catch (MalformedJwtException e) { } catch (MalformedJwtException | ExpiredJwtException | UnsupportedJwtException | IllegalArgumentException |
SignatureException e) {
LOGGER.error("Invalid JWT token: {}", e.getMessage()); LOGGER.error("Invalid JWT token: {}", e.getMessage());
} catch (ExpiredJwtException e) { throw new TokenValidationException(e.getMessage());
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());
} }
return false;
} }
} }

View File

@@ -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)
);
}
}

View File

@@ -13,6 +13,9 @@ public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long
Optional<RefreshToken> findByToken(String token); Optional<RefreshToken> findByToken(String token);
Optional<RefreshToken> findByUserId(Long userId);
@Modifying @Modifying
int deleteByUser(User user); int deleteByUser(User user);
} }

View File

@@ -1,7 +1,8 @@
package com.hakimfauzi23.boilerplatespringsecurity.service; package com.hakimfauzi23.boilerplatespringsecurity.service;
import com.hakimfauzi23.boilerplatespringsecurity.data.RefreshToken; 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.RefreshTokenRepository;
import com.hakimfauzi23.boilerplatespringsecurity.repository.UserRepository; import com.hakimfauzi23.boilerplatespringsecurity.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -30,8 +31,10 @@ public class RefreshTokenService {
} }
public RefreshToken createRefreshToken(Long userId) { 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.setUser(userRepository.findById(userId).get());
refreshToken.setExpirationDate(Instant.now().plusMillis(refreshTokenDurationMs)); refreshToken.setExpirationDate(Instant.now().plusMillis(refreshTokenDurationMs));
refreshToken.setToken(UUID.randomUUID().toString()); refreshToken.setToken(UUID.randomUUID().toString());