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");
+ }
+ }
+
}