Add Refresh Token API
This commit is contained in:
		| @@ -1,6 +1,7 @@ | ||||
| package com.hakimfauzi23.boilerplatespringsecurity.modules.auth.controllers; | ||||
|  | ||||
| import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.ERole; | ||||
| import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.RefreshToken; | ||||
| import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.Role; | ||||
| import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.User; | ||||
| import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.payload.request.LoginRequest; | ||||
| @@ -8,9 +9,12 @@ import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.payload.requ | ||||
| import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.payload.response.MessageResponse; | ||||
| import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.payload.response.UserInfoResponse; | ||||
| import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.jwt.JwtUtils; | ||||
| import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.jwt.exception.TokenRefreshException; | ||||
| import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.repository.RoleRepository; | ||||
| import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.repository.UserRepository; | ||||
| import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.service.RefreshTokenService; | ||||
| import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.service.UserDetailsImpl; | ||||
| import jakarta.servlet.http.HttpServletRequest; | ||||
| import jakarta.validation.Valid; | ||||
| import org.springframework.beans.factory.annotation.Autowired; | ||||
| import org.springframework.http.HttpHeaders; | ||||
| @@ -19,12 +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.GrantedAuthority; | ||||
| import org.springframework.security.core.context.SecurityContextHolder; | ||||
| import org.springframework.security.crypto.password.PasswordEncoder; | ||||
| import org.springframework.web.bind.annotation.*; | ||||
|  | ||||
| import java.util.HashSet; | ||||
| import java.util.List; | ||||
| import java.util.Objects; | ||||
| import java.util.Set; | ||||
| import java.util.stream.Collectors; | ||||
|  | ||||
| @@ -48,6 +54,9 @@ public class AuthController { | ||||
|     @Autowired | ||||
|     JwtUtils jwtUtils; | ||||
|  | ||||
|     @Autowired | ||||
|     RefreshTokenService refreshTokenService; | ||||
|  | ||||
|     @PostMapping("/signup") | ||||
|     public ResponseEntity<?> registerUser(@Valid @RequestBody SignupRequest signUpRequest) { | ||||
|         if (userRepository.existsByUsername(signUpRequest.getUsername())) { | ||||
| @@ -103,20 +112,55 @@ public class AuthController { | ||||
|         ResponseCookie jwtCookie = jwtUtils.generateJwtCookie(userDetails); | ||||
|  | ||||
|         List<String> roles = userDetails.getAuthorities().stream() | ||||
|                 .map(item -> item.getAuthority()) | ||||
|                 .map(GrantedAuthority::getAuthority) | ||||
|                 .collect(Collectors.toList()); | ||||
|  | ||||
|         return ResponseEntity.ok().header(HttpHeaders.SET_COOKIE, jwtCookie.toString()) | ||||
|                 .body(new UserInfoResponse(userDetails.getId(), | ||||
|                         userDetails.getUsername(), | ||||
|                         userDetails.getEmail(), | ||||
|                         roles)); | ||||
|         RefreshToken refreshToken = refreshTokenService.createRefreshToken(userDetails.getId()); | ||||
|  | ||||
|         ResponseCookie jwtRefreshCookie = jwtUtils.generateRefreshJwtCookie(refreshToken.getToken()); | ||||
|  | ||||
|         return ResponseEntity.ok() | ||||
|                 .header(HttpHeaders.SET_COOKIE, jwtCookie.toString()) | ||||
|                 .header(HttpHeaders.SET_COOKIE, jwtRefreshCookie.toString()) | ||||
|                 .body(new UserInfoResponse(userDetails.getId(), userDetails.getUsername(), userDetails.getEmail(), roles)); | ||||
|     } | ||||
|  | ||||
|     @PostMapping("/signout") | ||||
|     public ResponseEntity<?> logoutUser() { | ||||
|         ResponseCookie cookie = jwtUtils.getCleanJwtCookie(); | ||||
|         return ResponseEntity.ok().header(HttpHeaders.SET_COOKIE, cookie.toString()) | ||||
|         Object principle = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); | ||||
|         if (!Objects.equals(principle.toString(), "anonymousUser")) { | ||||
|             Long userId = ((UserDetailsImpl) principle).getId(); | ||||
|             refreshTokenService.deleteByUserId(userId); | ||||
|         } | ||||
|  | ||||
|         ResponseCookie jwtCookie = jwtUtils.getCleanJwtCookie(); | ||||
|         ResponseCookie jwtRefreshCookie = jwtUtils.getCleanJwtRefreshCookie(); | ||||
|  | ||||
|         return ResponseEntity.ok() | ||||
|                 .header(HttpHeaders.SET_COOKIE, jwtCookie.toString()) | ||||
|                 .header(HttpHeaders.SET_COOKIE, jwtRefreshCookie.toString()) | ||||
|                 .body(new MessageResponse("You've been signed out!")); | ||||
|     } | ||||
|  | ||||
|     @PostMapping("/refresh-token") | ||||
|     public ResponseEntity<?> refreshToken(HttpServletRequest request) { | ||||
|         String refreshToken = jwtUtils.getJwtRefreshFromCookies(request); | ||||
|  | ||||
|         if ((refreshToken != null) && (refreshToken.length() > 0)) { | ||||
|             return refreshTokenService.findByToken(refreshToken) | ||||
|                     .map(refreshTokenService::verifyExpiration) | ||||
|                     .map(RefreshToken::getUser) | ||||
|                     .map(user -> { | ||||
|                         ResponseCookie jwtCookie = jwtUtils.generateJwtCookie(user); | ||||
|  | ||||
|                         return ResponseEntity.ok() | ||||
|                                 .header(HttpHeaders.SET_COOKIE, jwtCookie.toString()) | ||||
|                                 .body(new MessageResponse("Token is refreshed successfully!")); | ||||
|                     }) | ||||
|                     .orElseThrow(() -> new TokenRefreshException(refreshToken, | ||||
|                             "Refresh token is not in database!")); | ||||
|         } | ||||
|  | ||||
|         return ResponseEntity.badRequest().body(new MessageResponse("Refresh Token is empty!")); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,65 @@ | ||||
| package com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data; | ||||
|  | ||||
| import jakarta.persistence.*; | ||||
|  | ||||
| import java.time.Instant; | ||||
|  | ||||
| @Entity(name = "refresh_token") | ||||
| public class RefreshToken { | ||||
|  | ||||
|     @Id | ||||
|     @GeneratedValue(strategy = GenerationType.AUTO) | ||||
|     private Long id; | ||||
|  | ||||
|     @OneToOne | ||||
|     @JoinColumn(name = "user_id", referencedColumnName = "id") | ||||
|     private User user; | ||||
|  | ||||
|     @Column(nullable = false, unique = true) | ||||
|     private String token; | ||||
|  | ||||
|     @Column(nullable = false) | ||||
|     private Instant expirationDate; | ||||
|  | ||||
|     public RefreshToken() { | ||||
|     } | ||||
|  | ||||
|     public RefreshToken(Long id, User user, String token, Instant expirationDate) { | ||||
|         this.id = id; | ||||
|         this.user = user; | ||||
|         this.token = token; | ||||
|         this.expirationDate = expirationDate; | ||||
|     } | ||||
|  | ||||
|     public Long getId() { | ||||
|         return id; | ||||
|     } | ||||
|  | ||||
|     public void setId(Long id) { | ||||
|         this.id = id; | ||||
|     } | ||||
|  | ||||
|     public User getUser() { | ||||
|         return user; | ||||
|     } | ||||
|  | ||||
|     public void setUser(User user) { | ||||
|         this.user = user; | ||||
|     } | ||||
|  | ||||
|     public String getToken() { | ||||
|         return token; | ||||
|     } | ||||
|  | ||||
|     public void setToken(String token) { | ||||
|         this.token = token; | ||||
|     } | ||||
|  | ||||
|     public Instant getExpirationDate() { | ||||
|         return expirationDate; | ||||
|     } | ||||
|  | ||||
|     public void setExpirationDate(Instant expirationDate) { | ||||
|         this.expirationDate = expirationDate; | ||||
|     } | ||||
| } | ||||
| @@ -1,5 +1,6 @@ | ||||
| package com.hakimfauzi23.boilerplatespringsecurity.modules.auth.jwt; | ||||
|  | ||||
| import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.User; | ||||
| import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.service.UserDetailsImpl; | ||||
| import io.jsonwebtoken.*; | ||||
| import io.jsonwebtoken.io.Decoders; | ||||
| @@ -30,26 +31,40 @@ public class JwtUtils { | ||||
|     @Value("${spring.app.jwtCookieName}") | ||||
|     private String jwtCookie; | ||||
|  | ||||
|     @Value("${spring.app.jwtRefreshCookieName}") | ||||
|     private String jwtRefreshCookie; | ||||
|  | ||||
|     public String getJwtFromCookies(HttpServletRequest request) { | ||||
|         Cookie cookie = WebUtils.getCookie(request, jwtCookie); | ||||
|         if (cookie != null) { | ||||
|             return cookie.getValue(); | ||||
|         } else { | ||||
|             return null; | ||||
|         } | ||||
|         return getCookieValueByName(request, jwtCookie); | ||||
|     } | ||||
|  | ||||
|     public String getJwtRefreshFromCookies(HttpServletRequest request) { | ||||
|         return getCookieValueByName(request, jwtRefreshCookie); | ||||
|     } | ||||
|  | ||||
|     public ResponseCookie getCleanJwtCookie() { | ||||
|         return ResponseCookie.from(jwtCookie, null).path("/api").build(); | ||||
|     } | ||||
|  | ||||
|     public ResponseCookie getCleanJwtRefreshCookie() { | ||||
|         return ResponseCookie.from(jwtRefreshCookie, null).path("/api/auth/refresh-token").build(); | ||||
|     } | ||||
|  | ||||
|     public ResponseCookie generateJwtCookie(UserDetailsImpl userDetails) { | ||||
|         String jwt = generateTokenFromUsername(userDetails.getUsername()); | ||||
|         ResponseCookie cookie = ResponseCookie.from(jwtCookie, jwt).path("/api").maxAge(24 * 60 * 60).httpOnly(true).build(); | ||||
|         return cookie; | ||||
|         return generateCookie(jwtCookie, jwt, "/api"); | ||||
|     } | ||||
|  | ||||
|     public ResponseCookie getCleanJwtCookie() { | ||||
|         ResponseCookie cookie = ResponseCookie.from(jwtCookie, null).path("/api").build(); | ||||
|         return cookie; | ||||
|     public ResponseCookie generateJwtCookie(User user) { | ||||
|         String jwt = generateTokenFromUsername(user.getUsername()); | ||||
|         return generateCookie(jwtCookie, jwt, "/api"); | ||||
|     } | ||||
|  | ||||
|     public ResponseCookie generateRefreshJwtCookie(String refreshToken) { | ||||
|         return generateCookie(jwtRefreshCookie, refreshToken, "/api/auth/refresh-token"); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     public String getUserNameFromJwtToken(String token) { | ||||
|         return Jwts.parserBuilder().setSigningKey(key()).build() | ||||
|                 .parseClaimsJws(token).getBody().getSubject(); | ||||
| @@ -84,4 +99,17 @@ public class JwtUtils { | ||||
|                 .signWith(key(), SignatureAlgorithm.HS256) | ||||
|                 .compact(); | ||||
|     } | ||||
|  | ||||
|     private ResponseCookie generateCookie(String name, String value, String path) { | ||||
|         return ResponseCookie.from(name, value).path(path).maxAge(24 * 60 * 60).httpOnly(true).build(); | ||||
|     } | ||||
|  | ||||
|     private String getCookieValueByName(HttpServletRequest request, String name) { | ||||
|         Cookie cookie = WebUtils.getCookie(request, name); | ||||
|         if (cookie != null) { | ||||
|             return cookie.getValue(); | ||||
|         } else { | ||||
|             return null; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,26 @@ | ||||
| package com.hakimfauzi23.boilerplatespringsecurity.modules.auth.jwt.advice; | ||||
|  | ||||
| import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.jwt.exception.ErrorMessage; | ||||
| import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.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) | ||||
|         ); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,34 @@ | ||||
| package com.hakimfauzi23.boilerplatespringsecurity.modules.auth.jwt.exception; | ||||
|  | ||||
| import java.util.Date; | ||||
|  | ||||
| public class ErrorMessage { | ||||
|  | ||||
|     private int statusCode; | ||||
|     private Date timestamp; | ||||
|     private String message; | ||||
|     private String description; | ||||
|  | ||||
|     public ErrorMessage(int statusCode, Date timestamp, String message, String description) { | ||||
|         this.statusCode = statusCode; | ||||
|         this.timestamp = timestamp; | ||||
|         this.message = message; | ||||
|         this.description = description; | ||||
|     } | ||||
|  | ||||
|     public int getStatusCode() { | ||||
|         return statusCode; | ||||
|     } | ||||
|  | ||||
|     public Date getTimestamp() { | ||||
|         return timestamp; | ||||
|     } | ||||
|  | ||||
|     public String getMessage() { | ||||
|         return message; | ||||
|     } | ||||
|  | ||||
|     public String getDescription() { | ||||
|         return description; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,14 @@ | ||||
| package com.hakimfauzi23.boilerplatespringsecurity.modules.auth.jwt.exception; | ||||
|  | ||||
| import org.springframework.http.HttpStatus; | ||||
| import org.springframework.web.bind.annotation.ResponseStatus; | ||||
|  | ||||
| @ResponseStatus(HttpStatus.FORBIDDEN) | ||||
| public class TokenRefreshException extends RuntimeException { | ||||
|  | ||||
|     private static final long serialVersionUID = 1L; | ||||
|  | ||||
|     public TokenRefreshException(String token, String message) { | ||||
|         super(String.format("Failed for [%s]: %s", token, message)); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,18 @@ | ||||
| package com.hakimfauzi23.boilerplatespringsecurity.modules.auth.repository; | ||||
|  | ||||
| import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.RefreshToken; | ||||
| import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.User; | ||||
| import org.springframework.data.jpa.repository.JpaRepository; | ||||
| import org.springframework.data.jpa.repository.Modifying; | ||||
| import org.springframework.stereotype.Repository; | ||||
|  | ||||
| import java.util.Optional; | ||||
|  | ||||
| @Repository | ||||
| public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> { | ||||
|  | ||||
|     Optional<RefreshToken> findByToken(String token); | ||||
|  | ||||
|     @Modifying | ||||
|     int deleteByUser(User user); | ||||
| } | ||||
| @@ -0,0 +1,57 @@ | ||||
| package com.hakimfauzi23.boilerplatespringsecurity.modules.auth.service; | ||||
|  | ||||
| import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.data.RefreshToken; | ||||
| import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.jwt.exception.TokenRefreshException; | ||||
| import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.repository.RefreshTokenRepository; | ||||
| import com.hakimfauzi23.boilerplatespringsecurity.modules.auth.repository.UserRepository; | ||||
| import org.springframework.beans.factory.annotation.Autowired; | ||||
| import org.springframework.beans.factory.annotation.Value; | ||||
| import org.springframework.stereotype.Service; | ||||
| import org.springframework.transaction.annotation.Transactional; | ||||
|  | ||||
| import java.time.Instant; | ||||
| import java.util.Optional; | ||||
| import java.util.UUID; | ||||
|  | ||||
| @Service | ||||
| public class RefreshTokenService { | ||||
|  | ||||
|     @Value("${spring.app.jwtRefreshExpirationMs}") | ||||
|     private Long refreshTokenDurationMs; | ||||
|  | ||||
|     @Autowired | ||||
|     private RefreshTokenRepository refreshTokenRepository; | ||||
|  | ||||
|     @Autowired | ||||
|     private UserRepository userRepository; | ||||
|  | ||||
|     public Optional<RefreshToken> findByToken(String token) { | ||||
|         return refreshTokenRepository.findByToken(token); | ||||
|     } | ||||
|  | ||||
|     public RefreshToken createRefreshToken(Long userId) { | ||||
|         RefreshToken refreshToken = new RefreshToken(); | ||||
|  | ||||
|         refreshToken.setUser(userRepository.findById(userId).get()); | ||||
|         refreshToken.setExpirationDate(Instant.now().plusMillis(refreshTokenDurationMs)); | ||||
|         refreshToken.setToken(UUID.randomUUID().toString()); | ||||
|  | ||||
|         refreshToken = refreshTokenRepository.save(refreshToken); | ||||
|         return refreshToken; | ||||
|     } | ||||
|  | ||||
|     public RefreshToken verifyExpiration(RefreshToken token) { | ||||
|         if (token.getExpirationDate().compareTo(Instant.now()) < 0) { | ||||
|             refreshTokenRepository.delete(token); | ||||
|             throw new TokenRefreshException(token.getToken(), "Refresh token was expired!. Please make a new signin request"); | ||||
|         } | ||||
|  | ||||
|         return token; | ||||
|     } | ||||
|  | ||||
|     @Transactional | ||||
|     public int deleteByUserId(Long userId) { | ||||
|         return refreshTokenRepository.deleteByUser(userRepository.findById(userId).get()); | ||||
|     } | ||||
|  | ||||
| } | ||||
		Reference in New Issue
	
	Block a user