From 08787d50b0103a02320871702271c2a2022e7de5 Mon Sep 17 00:00:00 2001 From: Jesse Brault Date: Thu, 8 Aug 2024 12:29:26 -0500 Subject: [PATCH] Created LoginExceptionView to explain to client why login or refresh have failed. --- .../api/auth/AuthController.java | 29 ++++------ .../mealsmadeeasy/api/auth/AuthService.java | 4 +- .../api/auth/AuthServiceImpl.java | 57 ++++++++++--------- .../api/auth/LoginException.java | 16 ++++-- .../api/auth/LoginExceptionReason.java | 8 +++ .../api/auth/LoginExceptionView.java | 21 +++++++ 6 files changed, 84 insertions(+), 51 deletions(-) create mode 100644 src/main/java/app/mealsmadeeasy/api/auth/LoginExceptionReason.java create mode 100644 src/main/java/app/mealsmadeeasy/api/auth/LoginExceptionView.java diff --git a/src/main/java/app/mealsmadeeasy/api/auth/AuthController.java b/src/main/java/app/mealsmadeeasy/api/auth/AuthController.java index 5352ffe..15e6ca7 100644 --- a/src/main/java/app/mealsmadeeasy/api/auth/AuthController.java +++ b/src/main/java/app/mealsmadeeasy/api/auth/AuthController.java @@ -46,29 +46,24 @@ public final class AuthController { .body(loginView); } + @ExceptionHandler(LoginException.class) + public ResponseEntity onLoginException(LoginException ex) { + final LoginExceptionView loginExceptionView = new LoginExceptionView(ex.getReason(), ex.getMessage()); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(loginExceptionView); + } + @PostMapping("/login") - public ResponseEntity login(@RequestBody LoginBody loginBody) { - try { - final LoginDetails loginDetails = this.authService.login(loginBody.getUsername(), loginBody.getPassword()); - return this.getLoginViewResponseEntity(loginDetails); - } catch (LoginException loginException) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } + public ResponseEntity login(@RequestBody LoginBody loginBody) throws LoginException { + final LoginDetails loginDetails = this.authService.login(loginBody.getUsername(), loginBody.getPassword()); + return this.getLoginViewResponseEntity(loginDetails); } @PostMapping("/refresh") public ResponseEntity refresh( @CookieValue(value = "refresh-token", required = false) @Nullable String oldRefreshToken - ) { - if (oldRefreshToken != null) { - try { - final LoginDetails loginDetails = this.authService.refresh(oldRefreshToken); - return this.getLoginViewResponseEntity(loginDetails); - } catch (LoginException loginException) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } - } - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + ) throws LoginException { + final LoginDetails loginDetails = this.authService.refresh(oldRefreshToken); + return this.getLoginViewResponseEntity(loginDetails); } @PostMapping("/logout") diff --git a/src/main/java/app/mealsmadeeasy/api/auth/AuthService.java b/src/main/java/app/mealsmadeeasy/api/auth/AuthService.java index c85f5d0..e1cecf8 100644 --- a/src/main/java/app/mealsmadeeasy/api/auth/AuthService.java +++ b/src/main/java/app/mealsmadeeasy/api/auth/AuthService.java @@ -1,7 +1,9 @@ package app.mealsmadeeasy.api.auth; +import org.jetbrains.annotations.Nullable; + public interface AuthService { LoginDetails login(String username, String password) throws LoginException; void logout(String refreshToken); - LoginDetails refresh(String refreshToken) throws LoginException; + LoginDetails refresh(@Nullable String refreshToken) throws LoginException; } diff --git a/src/main/java/app/mealsmadeeasy/api/auth/AuthServiceImpl.java b/src/main/java/app/mealsmadeeasy/api/auth/AuthServiceImpl.java index 910ccb5..fe134eb 100644 --- a/src/main/java/app/mealsmadeeasy/api/auth/AuthServiceImpl.java +++ b/src/main/java/app/mealsmadeeasy/api/auth/AuthServiceImpl.java @@ -2,7 +2,7 @@ package app.mealsmadeeasy.api.auth; import app.mealsmadeeasy.api.jwt.JwtService; import app.mealsmadeeasy.api.user.UserEntity; -import io.jsonwebtoken.JwtException; +import org.jetbrains.annotations.Nullable; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -45,10 +45,9 @@ public final class AuthServiceImpl implements AuthService { @Override public LoginDetails login(String username, String password) throws LoginException { try { - final Authentication authentication = this.authenticationManager.authenticate(new UsernamePasswordAuthenticationToken( - username, - password - )); + final Authentication authentication = this.authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(username, password) + ); final UserEntity principal = (UserEntity) authentication.getPrincipal(); return new LoginDetails( username, @@ -56,7 +55,7 @@ public final class AuthServiceImpl implements AuthService { this.createRefreshToken(principal) ); } catch (AuthenticationException e) { - throw new LoginException(e); + throw new LoginException(LoginExceptionReason.INVALID_CREDENTIALS, e); } } @@ -66,28 +65,32 @@ public final class AuthServiceImpl implements AuthService { } @Override - public LoginDetails refresh(String refreshToken) throws LoginException { - try { - final RefreshTokenEntity old = this.refreshTokenRepository.findByToken(refreshToken) - .orElseThrow(() -> new LoginException("No such refresh-token: " + refreshToken)); - if (old.isRevoked()) { - throw new LoginException("RefreshToken is revoked."); - } - if (old.getExpires().isBefore(LocalDateTime.now())) { - throw new LoginException("RefreshToken is expired."); - } - final UserEntity principal = old.getOwner(); - this.refreshTokenRepository.delete(old); - - final String username = principal.getUsername(); - return new LoginDetails( - username, - this.jwtService.generateAccessToken(username), - this.createRefreshToken(principal) - ); - } catch (JwtException e) { - throw new LoginException(e); + public LoginDetails refresh(@Nullable String refreshToken) throws LoginException { + if (refreshToken == null) { + throw new LoginException(LoginExceptionReason.NO_REFRESH_TOKEN, "No refresh token provided."); } + + final RefreshTokenEntity old = this.refreshTokenRepository.findByToken(refreshToken) + .orElseThrow(() -> new LoginException( + LoginExceptionReason.INVALID_REFRESH_TOKEN, + "No such refresh-token: " + refreshToken + )); + if (old.isRevoked()) { + throw new LoginException(LoginExceptionReason.INVALID_REFRESH_TOKEN, "RefreshToken is revoked."); + } + if (old.getExpires().isBefore(LocalDateTime.now())) { + throw new LoginException(LoginExceptionReason.EXPIRED_REFRESH_TOKEN, "RefreshToken is expired."); + } + + final UserEntity principal = old.getOwner(); + this.refreshTokenRepository.delete(old); + + final String username = principal.getUsername(); + return new LoginDetails( + username, + this.jwtService.generateAccessToken(username), + this.createRefreshToken(principal) + ); } } diff --git a/src/main/java/app/mealsmadeeasy/api/auth/LoginException.java b/src/main/java/app/mealsmadeeasy/api/auth/LoginException.java index 33578d4..04af68a 100644 --- a/src/main/java/app/mealsmadeeasy/api/auth/LoginException.java +++ b/src/main/java/app/mealsmadeeasy/api/auth/LoginException.java @@ -2,16 +2,20 @@ package app.mealsmadeeasy.api.auth; public final class LoginException extends Exception { - public LoginException(String message) { + private final LoginExceptionReason reason; + + public LoginException(LoginExceptionReason reason, String message) { super(message); + this.reason = reason; } - public LoginException(String message, Throwable cause) { - super(message, cause); - } - - public LoginException(Throwable cause) { + public LoginException(LoginExceptionReason reason, Throwable cause) { super(cause); + this.reason = reason; + } + + public LoginExceptionReason getReason() { + return this.reason; } } diff --git a/src/main/java/app/mealsmadeeasy/api/auth/LoginExceptionReason.java b/src/main/java/app/mealsmadeeasy/api/auth/LoginExceptionReason.java new file mode 100644 index 0000000..b66d47c --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/auth/LoginExceptionReason.java @@ -0,0 +1,8 @@ +package app.mealsmadeeasy.api.auth; + +public enum LoginExceptionReason { + INVALID_CREDENTIALS, + EXPIRED_REFRESH_TOKEN, + INVALID_REFRESH_TOKEN, + NO_REFRESH_TOKEN +} diff --git a/src/main/java/app/mealsmadeeasy/api/auth/LoginExceptionView.java b/src/main/java/app/mealsmadeeasy/api/auth/LoginExceptionView.java new file mode 100644 index 0000000..6948da0 --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/auth/LoginExceptionView.java @@ -0,0 +1,21 @@ +package app.mealsmadeeasy.api.auth; + +public class LoginExceptionView { + + private final LoginExceptionReason reason; + private final String message; + + public LoginExceptionView(LoginExceptionReason reason, String message) { + this.reason = reason; + this.message = message; + } + + public LoginExceptionReason getReason() { + return this.reason; + } + + public String getMessage() { + return this.message; + } + +}