diff --git a/pom.xml b/pom.xml index 89d0a60..49ac77e 100644 --- a/pom.xml +++ b/pom.xml @@ -64,10 +64,9 @@ 0.11.5 - org.projectlombok - lombok - 1.18.20 - provided + jakarta.validation + jakarta.validation-api + 2.0.2 diff --git a/src/main/java/com/faf223/expensetrackerfaf/config/JwtAuthenticationFilter.java b/src/main/java/com/faf223/expensetrackerfaf/config/JwtAuthenticationFilter.java index 1f91bc6..043f61f 100644 --- a/src/main/java/com/faf223/expensetrackerfaf/config/JwtAuthenticationFilter.java +++ b/src/main/java/com/faf223/expensetrackerfaf/config/JwtAuthenticationFilter.java @@ -1,5 +1,8 @@ package com.faf223.expensetrackerfaf.config; +import com.faf223.expensetrackerfaf.controller.auth.ErrorResponse; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.ExpiredJwtException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -15,7 +18,6 @@ import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; - @Component @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { @@ -35,23 +37,38 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { } final String authHeader = request.getHeader("Authorization"); final String jwt; - final String userEmail; + String userEmail; if (authHeader == null || !authHeader.startsWith("Bearer ")) { filterChain.doFilter(request, response); return; } + jwt = authHeader.substring(7); - userEmail = jwtService.extractUsername(jwt); - if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) { - UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail); - if (jwtService.isTokenValid(jwt, userDetails)) { - UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( - userDetails, null, userDetails.getAuthorities()); - authToken.setDetails(new WebAuthenticationDetailsSource() - .buildDetails(request)); - SecurityContextHolder.getContext().setAuthentication(authToken); + + try { + userEmail = jwtService.extractUsername(jwt); + if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail); + if (jwtService.isTokenValid(jwt, userDetails)) { + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities()); + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authToken); + } } + } catch (ExpiredJwtException e) { + // Token is expired; return a custom response + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json"); + + ErrorResponse errorResponse = new ErrorResponse("TokenExpired", "Your session has expired. Please log in again."); + ObjectMapper objectMapper = new ObjectMapper(); // You may need to import ObjectMapper + response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); + + + response.getWriter().flush(); + return; } filterChain.doFilter(request, response); } -} \ No newline at end of file +} diff --git a/src/main/java/com/faf223/expensetrackerfaf/config/JwtService.java b/src/main/java/com/faf223/expensetrackerfaf/config/JwtService.java index a9904f9..9cf6066 100644 --- a/src/main/java/com/faf223/expensetrackerfaf/config/JwtService.java +++ b/src/main/java/com/faf223/expensetrackerfaf/config/JwtService.java @@ -38,6 +38,10 @@ public class JwtService { return generateToken(new HashMap<>(), userDetails); } + public String generateRefreshToken(UserDetails userDetails) { + return generateRefreshToken(new HashMap<>(), userDetails); + } + public String generateToken( Map extraClaims, UserDetails userDetails @@ -45,6 +49,13 @@ public class JwtService { return buildToken(extraClaims, userDetails, jwtExpiration); } + public String generateRefreshToken( + Map extraClaims, + UserDetails userDetails + ) { + return buildToken(extraClaims, userDetails, refreshExpiration); + } + private String buildToken(Map extraClaims, UserDetails userDetails, long expiration) { return Jwts .builder() diff --git a/src/main/java/com/faf223/expensetrackerfaf/config/SecurityConfiguration.java b/src/main/java/com/faf223/expensetrackerfaf/config/SecurityConfiguration.java index b494885..430f3ff 100644 --- a/src/main/java/com/faf223/expensetrackerfaf/config/SecurityConfiguration.java +++ b/src/main/java/com/faf223/expensetrackerfaf/config/SecurityConfiguration.java @@ -4,7 +4,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationProvider; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; @@ -14,7 +14,7 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic @Configuration @EnableWebSecurity @RequiredArgsConstructor -@EnableGlobalMethodSecurity(prePostEnabled = true) +@EnableMethodSecurity public class SecurityConfiguration { private final JwtAuthenticationFilter jwtAuthFilter; diff --git a/src/main/java/com/faf223/expensetrackerfaf/controller/auth/AuthenticationController.java b/src/main/java/com/faf223/expensetrackerfaf/controller/auth/AuthenticationController.java index 1f375ea..b53fc63 100644 --- a/src/main/java/com/faf223/expensetrackerfaf/controller/auth/AuthenticationController.java +++ b/src/main/java/com/faf223/expensetrackerfaf/controller/auth/AuthenticationController.java @@ -1,19 +1,17 @@ package com.faf223.expensetrackerfaf.controller.auth; import com.faf223.expensetrackerfaf.service.AuthenticationService; +import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("api/v1/auth") +@RequiredArgsConstructor public class AuthenticationController { private final AuthenticationService service; - public AuthenticationController(AuthenticationService service) { - this.service = service; - } - @PostMapping("/register") public ResponseEntity register(@RequestBody RegisterRequest request) { return ResponseEntity.ok(service.register(request)); @@ -23,4 +21,9 @@ public class AuthenticationController { public ResponseEntity authenticate(@RequestBody AuthenticationRequest request) { return ResponseEntity.ok(service.authenticate(request)); } + + @PostMapping("/refreshtoken") + public ResponseEntity refreshAccessToken(@RequestBody TokenRefreshRequest request) { + return ResponseEntity.ok(service.refreshAccessToken(request)); + } } diff --git a/src/main/java/com/faf223/expensetrackerfaf/controller/auth/AuthenticationResponse.java b/src/main/java/com/faf223/expensetrackerfaf/controller/auth/AuthenticationResponse.java index bc92552..b182217 100644 --- a/src/main/java/com/faf223/expensetrackerfaf/controller/auth/AuthenticationResponse.java +++ b/src/main/java/com/faf223/expensetrackerfaf/controller/auth/AuthenticationResponse.java @@ -1,5 +1,6 @@ package com.faf223.expensetrackerfaf.controller.auth; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -11,5 +12,8 @@ import lombok.NoArgsConstructor; @NoArgsConstructor public class AuthenticationResponse { - private String token; + @JsonProperty("access_token") + private String accessToken; + @JsonProperty("refresh_token") + private String refreshToken; } diff --git a/src/main/java/com/faf223/expensetrackerfaf/controller/auth/ErrorResponse.java b/src/main/java/com/faf223/expensetrackerfaf/controller/auth/ErrorResponse.java new file mode 100644 index 0000000..54db638 --- /dev/null +++ b/src/main/java/com/faf223/expensetrackerfaf/controller/auth/ErrorResponse.java @@ -0,0 +1,16 @@ +package com.faf223.expensetrackerfaf.controller.auth; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class ErrorResponse { + private String error; + private String message; + + public ErrorResponse(String error, String message) { + this.error = error; + this.message = message; + } +} diff --git a/src/main/java/com/faf223/expensetrackerfaf/controller/auth/TokenRefreshRequest.java b/src/main/java/com/faf223/expensetrackerfaf/controller/auth/TokenRefreshRequest.java new file mode 100644 index 0000000..bffe531 --- /dev/null +++ b/src/main/java/com/faf223/expensetrackerfaf/controller/auth/TokenRefreshRequest.java @@ -0,0 +1,8 @@ +package com.faf223.expensetrackerfaf.controller.auth; + +import lombok.Data; + +@Data +public class TokenRefreshRequest { + private String refreshToken; +} diff --git a/src/main/java/com/faf223/expensetrackerfaf/repository/UserRepository.java b/src/main/java/com/faf223/expensetrackerfaf/repository/UserRepository.java index d370b0a..e044309 100644 --- a/src/main/java/com/faf223/expensetrackerfaf/repository/UserRepository.java +++ b/src/main/java/com/faf223/expensetrackerfaf/repository/UserRepository.java @@ -7,4 +7,6 @@ import java.util.Optional; public interface UserRepository extends JpaRepository { Optional getUserByUserUuid(String userUuid); + + Optional findByUsername(String username); } diff --git a/src/main/java/com/faf223/expensetrackerfaf/service/AuthenticationService.java b/src/main/java/com/faf223/expensetrackerfaf/service/AuthenticationService.java index 96cb4a4..4b644f0 100644 --- a/src/main/java/com/faf223/expensetrackerfaf/service/AuthenticationService.java +++ b/src/main/java/com/faf223/expensetrackerfaf/service/AuthenticationService.java @@ -4,6 +4,7 @@ import com.faf223.expensetrackerfaf.config.JwtService; import com.faf223.expensetrackerfaf.controller.auth.AuthenticationRequest; import com.faf223.expensetrackerfaf.controller.auth.AuthenticationResponse; import com.faf223.expensetrackerfaf.controller.auth.RegisterRequest; +import com.faf223.expensetrackerfaf.controller.auth.TokenRefreshRequest; import com.faf223.expensetrackerfaf.model.Credential; import com.faf223.expensetrackerfaf.model.User; import com.faf223.expensetrackerfaf.repository.CredentialRepository; @@ -12,10 +13,13 @@ import com.faf223.expensetrackerfaf.security.PersonDetails; import lombok.RequiredArgsConstructor; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import java.util.Optional; + @Service @RequiredArgsConstructor public class AuthenticationService { @@ -38,9 +42,13 @@ public class AuthenticationService { Credential credential = new Credential(user, request.getEmail(), passwordEncoder.encode(request.getPassword())); credentialRepository.save(credential); - String jwtToken = jwtService.generateToken(new PersonDetails(credential)); + UserDetails userDetails = new PersonDetails(credential); + String jwtToken = jwtService.generateToken(userDetails); + String refreshToken = jwtService.generateRefreshToken(userDetails); + return AuthenticationResponse.builder() - .token(jwtToken) + .accessToken(jwtToken) + .refreshToken(refreshToken) .build(); } @@ -49,10 +57,30 @@ public class AuthenticationService { Credential credential = credentialRepository.findByEmail(request.getEmail()).orElseThrow((() -> new UsernameNotFoundException("User not found"))); - String jwtToken = jwtService.generateToken(new PersonDetails(credential)); + UserDetails userDetails = new PersonDetails(credential); + String jwtToken = jwtService.generateToken(userDetails); + String refreshToken = jwtService.generateRefreshToken(userDetails); return AuthenticationResponse.builder() - .token(jwtToken) + .accessToken(jwtToken) + .refreshToken(refreshToken) .build(); } + public AuthenticationResponse refreshAccessToken(TokenRefreshRequest refreshRequest) { + String refreshToken = refreshRequest.getRefreshToken(); + + Optional credential = credentialRepository.findByEmail(jwtService.extractUsername(refreshToken)); + if (credential.isPresent()) { + UserDetails userDetails = new PersonDetails(credential.get()); + + String jwtToken = jwtService.generateToken(userDetails); + return AuthenticationResponse.builder() + .accessToken(jwtToken) + .refreshToken(refreshToken) + .build(); + } else { + throw new RuntimeException("Invalid or expired refresh token"); + } + } + }