From b1b13dc736aa059b5436bffde3d5a5bd2aa95c0b Mon Sep 17 00:00:00 2001 From: Dmitrii Cravcenco Date: Wed, 11 Oct 2023 12:57:33 +0300 Subject: [PATCH 1/4] Add refresh token endpoint --- pom.xml | 7 ++-- .../config/SecurityConfiguration.java | 4 +- .../auth/AuthenticationController.java | 5 +++ .../auth/AuthenticationResponse.java | 6 ++- .../controller/auth/TokenRefreshRequest.java | 8 ++++ .../repository/UserRepository.java | 2 + .../service/AuthenticationService.java | 37 +++++++++++++++++-- 7 files changed, 58 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/faf223/expensetrackerfaf/controller/auth/TokenRefreshRequest.java 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/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..43c25df 100644 --- a/src/main/java/com/faf223/expensetrackerfaf/controller/auth/AuthenticationController.java +++ b/src/main/java/com/faf223/expensetrackerfaf/controller/auth/AuthenticationController.java @@ -23,4 +23,9 @@ public class AuthenticationController { public ResponseEntity authenticate(@RequestBody AuthenticationRequest request) { return ResponseEntity.ok(service.authenticate(request)); } + + @PostMapping("/refresh") + public ResponseEntity refreshAccessToken(@RequestBody TokenRefreshRequest refreshRequest) { + return ResponseEntity.ok(service.refreshAccessToken(refreshRequest)); + } } 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/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..bbc0440 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,14 @@ 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.io.IOException; +import java.util.Optional; + @Service @RequiredArgsConstructor public class AuthenticationService { @@ -38,9 +43,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.generateToken(userDetails); + return AuthenticationResponse.builder() - .token(jwtToken) + .accessToken(jwtToken) + .refreshToken(refreshToken) .build(); } @@ -49,10 +58,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.generateToken(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) // Return the same refresh token + .build(); + }else { + throw new RuntimeException("Invalid or expired refresh token"); + } + } + } From 3160a19fa5cf977a7f054ca397690bb79ff89415 Mon Sep 17 00:00:00 2001 From: Dmitrii Cravcenco Date: Thu, 12 Oct 2023 12:52:39 +0300 Subject: [PATCH 2/4] Add refresh token generation --- .../config/ExceptionHandlers.java | 19 ++++++++++++ .../config/JwtAuthenticationFilter.java | 29 +++++++++++-------- .../expensetrackerfaf/config/JwtService.java | 11 +++++++ .../config/SecurityConfiguration.java | 2 ++ .../config/TokenExpiredException.java | 7 +++++ .../auth/AuthenticationController.java | 12 ++++---- .../controller/auth/ErrorResponse.java | 16 ++++++++++ .../service/AuthenticationService.java | 9 +++--- 8 files changed, 81 insertions(+), 24 deletions(-) create mode 100644 src/main/java/com/faf223/expensetrackerfaf/config/ExceptionHandlers.java create mode 100644 src/main/java/com/faf223/expensetrackerfaf/config/TokenExpiredException.java create mode 100644 src/main/java/com/faf223/expensetrackerfaf/controller/auth/ErrorResponse.java diff --git a/src/main/java/com/faf223/expensetrackerfaf/config/ExceptionHandlers.java b/src/main/java/com/faf223/expensetrackerfaf/config/ExceptionHandlers.java new file mode 100644 index 0000000..9349218 --- /dev/null +++ b/src/main/java/com/faf223/expensetrackerfaf/config/ExceptionHandlers.java @@ -0,0 +1,19 @@ +package com.faf223.expensetrackerfaf.config; + +import com.faf223.expensetrackerfaf.controller.auth.ErrorResponse; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ControllerAdvice +public class ExceptionHandlers { + + @ExceptionHandler(TokenExpiredException.class) + @ResponseStatus(HttpStatus.UNAUTHORIZED) + @ResponseBody + public ErrorResponse handleTokenExpiredException(TokenExpiredException ex) { + return new ErrorResponse("Unauthorized", ex.getMessage()); + } +} diff --git a/src/main/java/com/faf223/expensetrackerfaf/config/JwtAuthenticationFilter.java b/src/main/java/com/faf223/expensetrackerfaf/config/JwtAuthenticationFilter.java index 1f91bc6..4529641 100644 --- a/src/main/java/com/faf223/expensetrackerfaf/config/JwtAuthenticationFilter.java +++ b/src/main/java/com/faf223/expensetrackerfaf/config/JwtAuthenticationFilter.java @@ -1,5 +1,6 @@ package com.faf223.expensetrackerfaf.config; +import io.jsonwebtoken.ExpiredJwtException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -15,7 +16,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 +35,28 @@ 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) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); } 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 430f3ff..0c594fd 100644 --- a/src/main/java/com/faf223/expensetrackerfaf/config/SecurityConfiguration.java +++ b/src/main/java/com/faf223/expensetrackerfaf/config/SecurityConfiguration.java @@ -19,6 +19,7 @@ public class SecurityConfiguration { private final JwtAuthenticationFilter jwtAuthFilter; private final AuthenticationProvider authenticationProvider; +// private final Http401UnauthorizedEntryPoint entryPoint; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { @@ -29,6 +30,7 @@ public class SecurityConfiguration { .anyRequest().authenticated() ) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) +// .exceptionHandling((e) -> e.authenticationEntryPoint(entryPoint)) .authenticationProvider(authenticationProvider) .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); // will be executed before UsernamePasswordAuthenticationFilter diff --git a/src/main/java/com/faf223/expensetrackerfaf/config/TokenExpiredException.java b/src/main/java/com/faf223/expensetrackerfaf/config/TokenExpiredException.java new file mode 100644 index 0000000..62e6db2 --- /dev/null +++ b/src/main/java/com/faf223/expensetrackerfaf/config/TokenExpiredException.java @@ -0,0 +1,7 @@ +package com.faf223.expensetrackerfaf.config; + +public class TokenExpiredException extends RuntimeException { + public TokenExpiredException(String message) { + super(message); + } +} 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 43c25df..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)); @@ -24,8 +22,8 @@ public class AuthenticationController { return ResponseEntity.ok(service.authenticate(request)); } - @PostMapping("/refresh") - public ResponseEntity refreshAccessToken(@RequestBody TokenRefreshRequest refreshRequest) { - return ResponseEntity.ok(service.refreshAccessToken(refreshRequest)); + @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/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/service/AuthenticationService.java b/src/main/java/com/faf223/expensetrackerfaf/service/AuthenticationService.java index bbc0440..4b644f0 100644 --- a/src/main/java/com/faf223/expensetrackerfaf/service/AuthenticationService.java +++ b/src/main/java/com/faf223/expensetrackerfaf/service/AuthenticationService.java @@ -18,7 +18,6 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; -import java.io.IOException; import java.util.Optional; @Service @@ -45,7 +44,7 @@ public class AuthenticationService { UserDetails userDetails = new PersonDetails(credential); String jwtToken = jwtService.generateToken(userDetails); - String refreshToken = jwtService.generateToken(userDetails); + String refreshToken = jwtService.generateRefreshToken(userDetails); return AuthenticationResponse.builder() .accessToken(jwtToken) @@ -60,7 +59,7 @@ public class AuthenticationService { UserDetails userDetails = new PersonDetails(credential); String jwtToken = jwtService.generateToken(userDetails); - String refreshToken = jwtService.generateToken(userDetails); + String refreshToken = jwtService.generateRefreshToken(userDetails); return AuthenticationResponse.builder() .accessToken(jwtToken) .refreshToken(refreshToken) @@ -77,9 +76,9 @@ public class AuthenticationService { String jwtToken = jwtService.generateToken(userDetails); return AuthenticationResponse.builder() .accessToken(jwtToken) - .refreshToken(refreshToken) // Return the same refresh token + .refreshToken(refreshToken) .build(); - }else { + } else { throw new RuntimeException("Invalid or expired refresh token"); } } From f8ec901fa82306c30da50fc066cd958bf34e9ae6 Mon Sep 17 00:00:00 2001 From: Dmitrii Cravcenco Date: Thu, 12 Oct 2023 12:52:39 +0300 Subject: [PATCH 3/4] Add refresh token generation --- .../config/ExceptionHandlers.java | 19 ++++++++++++ .../config/JwtAuthenticationFilter.java | 29 +++++++++++-------- .../expensetrackerfaf/config/JwtService.java | 11 +++++++ .../config/TokenExpiredException.java | 7 +++++ .../auth/AuthenticationController.java | 12 ++++---- .../controller/auth/ErrorResponse.java | 16 ++++++++++ .../service/AuthenticationService.java | 9 +++--- 7 files changed, 79 insertions(+), 24 deletions(-) create mode 100644 src/main/java/com/faf223/expensetrackerfaf/config/ExceptionHandlers.java create mode 100644 src/main/java/com/faf223/expensetrackerfaf/config/TokenExpiredException.java create mode 100644 src/main/java/com/faf223/expensetrackerfaf/controller/auth/ErrorResponse.java diff --git a/src/main/java/com/faf223/expensetrackerfaf/config/ExceptionHandlers.java b/src/main/java/com/faf223/expensetrackerfaf/config/ExceptionHandlers.java new file mode 100644 index 0000000..9349218 --- /dev/null +++ b/src/main/java/com/faf223/expensetrackerfaf/config/ExceptionHandlers.java @@ -0,0 +1,19 @@ +package com.faf223.expensetrackerfaf.config; + +import com.faf223.expensetrackerfaf.controller.auth.ErrorResponse; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ControllerAdvice +public class ExceptionHandlers { + + @ExceptionHandler(TokenExpiredException.class) + @ResponseStatus(HttpStatus.UNAUTHORIZED) + @ResponseBody + public ErrorResponse handleTokenExpiredException(TokenExpiredException ex) { + return new ErrorResponse("Unauthorized", ex.getMessage()); + } +} diff --git a/src/main/java/com/faf223/expensetrackerfaf/config/JwtAuthenticationFilter.java b/src/main/java/com/faf223/expensetrackerfaf/config/JwtAuthenticationFilter.java index 1f91bc6..4529641 100644 --- a/src/main/java/com/faf223/expensetrackerfaf/config/JwtAuthenticationFilter.java +++ b/src/main/java/com/faf223/expensetrackerfaf/config/JwtAuthenticationFilter.java @@ -1,5 +1,6 @@ package com.faf223.expensetrackerfaf.config; +import io.jsonwebtoken.ExpiredJwtException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -15,7 +16,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 +35,28 @@ 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) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); } 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/TokenExpiredException.java b/src/main/java/com/faf223/expensetrackerfaf/config/TokenExpiredException.java new file mode 100644 index 0000000..62e6db2 --- /dev/null +++ b/src/main/java/com/faf223/expensetrackerfaf/config/TokenExpiredException.java @@ -0,0 +1,7 @@ +package com.faf223.expensetrackerfaf.config; + +public class TokenExpiredException extends RuntimeException { + public TokenExpiredException(String message) { + super(message); + } +} 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 43c25df..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)); @@ -24,8 +22,8 @@ public class AuthenticationController { return ResponseEntity.ok(service.authenticate(request)); } - @PostMapping("/refresh") - public ResponseEntity refreshAccessToken(@RequestBody TokenRefreshRequest refreshRequest) { - return ResponseEntity.ok(service.refreshAccessToken(refreshRequest)); + @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/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/service/AuthenticationService.java b/src/main/java/com/faf223/expensetrackerfaf/service/AuthenticationService.java index bbc0440..4b644f0 100644 --- a/src/main/java/com/faf223/expensetrackerfaf/service/AuthenticationService.java +++ b/src/main/java/com/faf223/expensetrackerfaf/service/AuthenticationService.java @@ -18,7 +18,6 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; -import java.io.IOException; import java.util.Optional; @Service @@ -45,7 +44,7 @@ public class AuthenticationService { UserDetails userDetails = new PersonDetails(credential); String jwtToken = jwtService.generateToken(userDetails); - String refreshToken = jwtService.generateToken(userDetails); + String refreshToken = jwtService.generateRefreshToken(userDetails); return AuthenticationResponse.builder() .accessToken(jwtToken) @@ -60,7 +59,7 @@ public class AuthenticationService { UserDetails userDetails = new PersonDetails(credential); String jwtToken = jwtService.generateToken(userDetails); - String refreshToken = jwtService.generateToken(userDetails); + String refreshToken = jwtService.generateRefreshToken(userDetails); return AuthenticationResponse.builder() .accessToken(jwtToken) .refreshToken(refreshToken) @@ -77,9 +76,9 @@ public class AuthenticationService { String jwtToken = jwtService.generateToken(userDetails); return AuthenticationResponse.builder() .accessToken(jwtToken) - .refreshToken(refreshToken) // Return the same refresh token + .refreshToken(refreshToken) .build(); - }else { + } else { throw new RuntimeException("Invalid or expired refresh token"); } } From 0b98fe3db43f4065cf127a67dd46a992ff7280f7 Mon Sep 17 00:00:00 2001 From: Dmitrii Cravcenco Date: Thu, 12 Oct 2023 13:26:01 +0300 Subject: [PATCH 4/4] Change response status when token is expired --- .../config/ExceptionHandlers.java | 19 ------------------- .../config/JwtAuthenticationFilter.java | 12 ++++++++++++ .../config/SecurityConfiguration.java | 2 -- .../config/TokenExpiredException.java | 7 ------- 4 files changed, 12 insertions(+), 28 deletions(-) delete mode 100644 src/main/java/com/faf223/expensetrackerfaf/config/ExceptionHandlers.java delete mode 100644 src/main/java/com/faf223/expensetrackerfaf/config/TokenExpiredException.java diff --git a/src/main/java/com/faf223/expensetrackerfaf/config/ExceptionHandlers.java b/src/main/java/com/faf223/expensetrackerfaf/config/ExceptionHandlers.java deleted file mode 100644 index 9349218..0000000 --- a/src/main/java/com/faf223/expensetrackerfaf/config/ExceptionHandlers.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.faf223.expensetrackerfaf.config; - -import com.faf223.expensetrackerfaf.controller.auth.ErrorResponse; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ControllerAdvice; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.ResponseBody; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ControllerAdvice -public class ExceptionHandlers { - - @ExceptionHandler(TokenExpiredException.class) - @ResponseStatus(HttpStatus.UNAUTHORIZED) - @ResponseBody - public ErrorResponse handleTokenExpiredException(TokenExpiredException ex) { - return new ErrorResponse("Unauthorized", ex.getMessage()); - } -} diff --git a/src/main/java/com/faf223/expensetrackerfaf/config/JwtAuthenticationFilter.java b/src/main/java/com/faf223/expensetrackerfaf/config/JwtAuthenticationFilter.java index 4529641..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,7 @@ 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; @@ -55,7 +57,17 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { } } } 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); } diff --git a/src/main/java/com/faf223/expensetrackerfaf/config/SecurityConfiguration.java b/src/main/java/com/faf223/expensetrackerfaf/config/SecurityConfiguration.java index 0c594fd..430f3ff 100644 --- a/src/main/java/com/faf223/expensetrackerfaf/config/SecurityConfiguration.java +++ b/src/main/java/com/faf223/expensetrackerfaf/config/SecurityConfiguration.java @@ -19,7 +19,6 @@ public class SecurityConfiguration { private final JwtAuthenticationFilter jwtAuthFilter; private final AuthenticationProvider authenticationProvider; -// private final Http401UnauthorizedEntryPoint entryPoint; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { @@ -30,7 +29,6 @@ public class SecurityConfiguration { .anyRequest().authenticated() ) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) -// .exceptionHandling((e) -> e.authenticationEntryPoint(entryPoint)) .authenticationProvider(authenticationProvider) .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); // will be executed before UsernamePasswordAuthenticationFilter diff --git a/src/main/java/com/faf223/expensetrackerfaf/config/TokenExpiredException.java b/src/main/java/com/faf223/expensetrackerfaf/config/TokenExpiredException.java deleted file mode 100644 index 62e6db2..0000000 --- a/src/main/java/com/faf223/expensetrackerfaf/config/TokenExpiredException.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.faf223.expensetrackerfaf.config; - -public class TokenExpiredException extends RuntimeException { - public TokenExpiredException(String message) { - super(message); - } -}