diff --git a/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/controllers/AuthController.java b/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/controllers/AuthController.java index e6beb05..2c01aed 100644 --- a/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/controllers/AuthController.java +++ b/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/controllers/AuthController.java @@ -6,7 +6,10 @@ import org.springframework.web.bind.annotation.RequestMapping; import br.com.rayankonecny.hdcommoslib.models.exceptions.StandardError; import br.com.rayankonecny.hdcommoslib.models.requests.AuthenticateRequest; +import br.com.rayankonecny.hdcommoslib.models.requests.RefreshTokenRequest; import br.com.rayankonecny.hdcommoslib.models.responses.AuthenticationResponse; +import br.com.rayankonecny.hdcommoslib.models.responses.RefreshTokenResponse; + import org.springframework.web.bind.annotation.RequestBody; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; @@ -32,4 +35,16 @@ public interface AuthController { @Valid final AuthenticateRequest requests) throws Exception; + @Operation(summary = "Refresh token") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Token refreshed", content = @Content(mediaType = APPLICATION_JSON_VALUE, schema = @Schema(implementation = RefreshTokenResponse.class))), + @ApiResponse(responseCode = "400", description = "Bad request", content = @Content(mediaType = APPLICATION_JSON_VALUE, schema = @Schema(implementation = StandardError.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized", content = @Content(mediaType = APPLICATION_JSON_VALUE, schema = @Schema(implementation = StandardError.class))), + @ApiResponse(responseCode = "404", description = "Username not found", content = @Content(mediaType = APPLICATION_JSON_VALUE, schema = @Schema(implementation = StandardError.class))), + @ApiResponse(responseCode = "500", description = "Internal server error", content = @Content(mediaType = APPLICATION_JSON_VALUE, schema = @Schema(implementation = StandardError.class))) }) + @PostMapping("/refreshtoken") + ResponseEntity refreshToken(@Valid + @RequestBody + final RefreshTokenRequest refreshToken) throws Exception; + } diff --git a/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/controllers/impl/AuthControllerImpl.java b/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/controllers/impl/AuthControllerImpl.java index b3183cc..8fa2b3e 100644 --- a/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/controllers/impl/AuthControllerImpl.java +++ b/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/controllers/impl/AuthControllerImpl.java @@ -4,9 +4,13 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; import br.com.rayankonecny.authserviceapi.controllers.AuthController; +import br.com.rayankonecny.authserviceapi.models.RefreshToken; import br.com.rayankonecny.authserviceapi.services.AuthService; +import br.com.rayankonecny.authserviceapi.services.RefreshTokenService; import br.com.rayankonecny.hdcommoslib.models.requests.AuthenticateRequest; +import br.com.rayankonecny.hdcommoslib.models.requests.RefreshTokenRequest; import br.com.rayankonecny.hdcommoslib.models.responses.AuthenticationResponse; +import br.com.rayankonecny.hdcommoslib.models.responses.RefreshTokenResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -15,6 +19,7 @@ import lombok.RequiredArgsConstructor; public class AuthControllerImpl implements AuthController { private final AuthService authService; + private final RefreshTokenService refreshService; @Override public ResponseEntity authenticate(@Valid @@ -23,4 +28,10 @@ public class AuthControllerImpl implements AuthController { return ResponseEntity.ok(authService.authenticate(request)); } + @Override + public ResponseEntity refreshToken(@Valid + RefreshTokenRequest request) throws Exception { + return ResponseEntity.ok().body(refreshService.refreshToken(request.refreshToken())); + } + } diff --git a/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/models/RefreshToken.java b/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/models/RefreshToken.java new file mode 100644 index 0000000..07896ad --- /dev/null +++ b/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/models/RefreshToken.java @@ -0,0 +1,22 @@ +package br.com.rayankonecny.authserviceapi.models; + +import java.time.LocalDateTime; + +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +@Document +public class RefreshToken { + + @Id + private String id; + private String username; + private LocalDateTime createdAt; + private LocalDateTime expiresAt; + +} diff --git a/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/repository/RefreshTokenRepository.java b/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/repository/RefreshTokenRepository.java new file mode 100644 index 0000000..a0b3b89 --- /dev/null +++ b/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/repository/RefreshTokenRepository.java @@ -0,0 +1,11 @@ +package br.com.rayankonecny.authserviceapi.repository; + +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; + +import br.com.rayankonecny.authserviceapi.models.RefreshToken; + +@Repository +public interface RefreshTokenRepository extends MongoRepository { + +} diff --git a/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/services/AuthService.java b/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/services/AuthService.java index ebf1bda..be3fed2 100644 --- a/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/services/AuthService.java +++ b/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/services/AuthService.java @@ -5,6 +5,7 @@ import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import br.com.rayankonecny.authserviceapi.models.RefreshToken; import br.com.rayankonecny.authserviceapi.utils.JWTUtils; import br.com.rayankonecny.hdcommoslib.models.requests.AuthenticateRequest; import br.com.rayankonecny.hdcommoslib.models.responses.AuthenticationResponse; @@ -15,6 +16,7 @@ import lombok.RequiredArgsConstructor; public class AuthService { private final UserDetailsServiceImpl userDetailsService; + private final RefreshTokenService refreshTokenService; private final PasswordEncoder passwordEncoder; private final JWTUtils jwtUtils; @@ -27,7 +29,8 @@ public class AuthService { } String token = jwtUtils.generateToken(user); + RefreshToken refresh = refreshTokenService.save(user.getUsername()); - return new AuthenticationResponse(token,user.getUsername()); + return new AuthenticationResponse(token, refresh.getId(), user.getUsername()); } } diff --git a/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/services/BadCredentialsExceptionion.java b/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/services/BadCredentialsExceptionion.java deleted file mode 100644 index caa8480..0000000 --- a/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/services/BadCredentialsExceptionion.java +++ /dev/null @@ -1,5 +0,0 @@ -package br.com.rayankonecny.authserviceapi.services; - -public class BadCredentialsExceptionion { - -} diff --git a/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/services/RefreshTokenService.java b/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/services/RefreshTokenService.java new file mode 100644 index 0000000..4450c8a --- /dev/null +++ b/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/services/RefreshTokenService.java @@ -0,0 +1,48 @@ +package br.com.rayankonecny.authserviceapi.services; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.stereotype.Service; + +import br.com.rayankonecny.authserviceapi.models.RefreshToken; +import br.com.rayankonecny.authserviceapi.repository.RefreshTokenRepository; +import br.com.rayankonecny.authserviceapi.security.UserDetailsDTO; +import br.com.rayankonecny.authserviceapi.utils.JWTUtils; +import br.com.rayankonecny.hdcommoslib.models.exceptions.RefreshTokenExpired; +import br.com.rayankonecny.hdcommoslib.models.exceptions.ResourceNotFoundException; +import br.com.rayankonecny.hdcommoslib.models.responses.RefreshTokenResponse; +import lombok.RequiredArgsConstructor; + +import static java.time.LocalDateTime.now; + +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class RefreshTokenService { + + @Value("${jwt.expiration-sec.refresh-token}") + private Long refreshTokenExpirationSec; + + private final RefreshTokenRepository repository; + private final UserDetailsService userDetail; + private final JWTUtils jwtUtils; + + public RefreshToken save(final String username) { + return repository.save(RefreshToken.builder().id(UUID.randomUUID().toString()) + .expiresAt(now().plusSeconds(refreshTokenExpirationSec)).username(username).build()); + } + + public RefreshTokenResponse refreshToken(final String refreshTokenId) { + final var refreshToken = repository.findById(refreshTokenId) + .orElseThrow(() -> new ResourceNotFoundException("Refresh token not found. Id: " + refreshTokenId)); + + if (refreshToken.getExpiresAt().isBefore((now()))) { + throw new RefreshTokenExpired("Refresh token expired. Id: " + refreshTokenId); + } + + return new RefreshTokenResponse( + jwtUtils.generateToken((UserDetailsDTO) userDetail.loadUserByUsername(refreshToken.getUsername()))); + + } +} diff --git a/auth-service-api/src/main/resources/application.yml b/auth-service-api/src/main/resources/application.yml index 4636bec..cae4a6f 100644 --- a/auth-service-api/src/main/resources/application.yml +++ b/auth-service-api/src/main/resources/application.yml @@ -9,3 +9,4 @@ spring: enabled: false jwt.secret: "IHf3Yua/byvtA+iIcGWmkrLvpKEXTb5ClkXaZ0VDmYbr/6b1otCs38x68bidvZLAOB7anUtVQlCid6YDULO5XA==" jwt.expiration: 120000 +jwt.expiration-sec.refresh-token: 3600 diff --git a/hd-commons-lib/src/main/java/br/com/rayankonecny/hdcommoslib/models/exceptions/RefreshTokenExpired.java b/hd-commons-lib/src/main/java/br/com/rayankonecny/hdcommoslib/models/exceptions/RefreshTokenExpired.java new file mode 100644 index 0000000..b867e4a --- /dev/null +++ b/hd-commons-lib/src/main/java/br/com/rayankonecny/hdcommoslib/models/exceptions/RefreshTokenExpired.java @@ -0,0 +1,7 @@ +package br.com.rayankonecny.hdcommoslib.models.exceptions; + +public class RefreshTokenExpired extends RuntimeException { + public RefreshTokenExpired(String message) { + super(message); + } +} diff --git a/hd-commons-lib/src/main/java/br/com/rayankonecny/hdcommoslib/models/requests/RefreshTokenRequest.java b/hd-commons-lib/src/main/java/br/com/rayankonecny/hdcommoslib/models/requests/RefreshTokenRequest.java new file mode 100644 index 0000000..8a17198 --- /dev/null +++ b/hd-commons-lib/src/main/java/br/com/rayankonecny/hdcommoslib/models/requests/RefreshTokenRequest.java @@ -0,0 +1,11 @@ +package br.com.rayankonecny.hdcommoslib.models.requests; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record RefreshTokenRequest( + @Size(min = 16, max = 30, message = "Refresh token must be between 16 and 30 characters") + @NotBlank(message = "Refresh token is required") + String refreshToken +) { +} \ No newline at end of file diff --git a/hd-commons-lib/src/main/java/br/com/rayankonecny/hdcommoslib/models/responses/AuthenticationResponse.java b/hd-commons-lib/src/main/java/br/com/rayankonecny/hdcommoslib/models/responses/AuthenticationResponse.java index ca54db0..bedc489 100644 --- a/hd-commons-lib/src/main/java/br/com/rayankonecny/hdcommoslib/models/responses/AuthenticationResponse.java +++ b/hd-commons-lib/src/main/java/br/com/rayankonecny/hdcommoslib/models/responses/AuthenticationResponse.java @@ -5,6 +5,7 @@ import lombok.Builder; @Builder public record AuthenticationResponse( String token, + String refreshToken, String type ) { } diff --git a/hd-commons-lib/src/main/java/br/com/rayankonecny/hdcommoslib/models/responses/RefreshTokenResponse.java b/hd-commons-lib/src/main/java/br/com/rayankonecny/hdcommoslib/models/responses/RefreshTokenResponse.java new file mode 100644 index 0000000..30f1de2 --- /dev/null +++ b/hd-commons-lib/src/main/java/br/com/rayankonecny/hdcommoslib/models/responses/RefreshTokenResponse.java @@ -0,0 +1,3 @@ +package br.com.rayankonecny.hdcommoslib.models.responses; + +public record RefreshTokenResponse(String refreshToken) {} diff --git a/rest.http b/rest.http index eac52c1..4b0f688 100644 --- a/rest.http +++ b/rest.http @@ -44,4 +44,14 @@ Accept: application/json { "email": "rayanvix@gmail.com", "password": "123456" +} + + +POST http://175.15.15.91:8080/auth/refresh-token +Content-Type: application/json +Accept: application/json +# Authorization: Bearer {{token}} + +{ + "refreshToken": "73b32ec3-9933-4a60-ae06-b23c965dd1b2" } \ No newline at end of file