Created LoginExceptionView to explain to client why login or refresh have failed.

This commit is contained in:
Jesse Brault 2024-08-08 12:29:26 -05:00
parent 1976e345b6
commit 08787d50b0
6 changed files with 84 additions and 51 deletions

View File

@ -46,29 +46,24 @@ public final class AuthController {
.body(loginView);
}
@ExceptionHandler(LoginException.class)
public ResponseEntity<LoginExceptionView> onLoginException(LoginException ex) {
final LoginExceptionView loginExceptionView = new LoginExceptionView(ex.getReason(), ex.getMessage());
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(loginExceptionView);
}
@PostMapping("/login")
public ResponseEntity<LoginView> login(@RequestBody LoginBody loginBody) {
try {
public ResponseEntity<LoginView> login(@RequestBody LoginBody loginBody) throws LoginException {
final LoginDetails loginDetails = this.authService.login(loginBody.getUsername(), loginBody.getPassword());
return this.getLoginViewResponseEntity(loginDetails);
} catch (LoginException loginException) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}
@PostMapping("/refresh")
public ResponseEntity<LoginView> refresh(
@CookieValue(value = "refresh-token", required = false) @Nullable String oldRefreshToken
) {
if (oldRefreshToken != null) {
try {
) throws LoginException {
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();
}
@PostMapping("/logout")

View File

@ -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;
}

View File

@ -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,16 +65,23 @@ public final class AuthServiceImpl implements AuthService {
}
@Override
public LoginDetails refresh(String refreshToken) throws LoginException {
try {
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("No such refresh-token: " + refreshToken));
.orElseThrow(() -> new LoginException(
LoginExceptionReason.INVALID_REFRESH_TOKEN,
"No such refresh-token: " + refreshToken
));
if (old.isRevoked()) {
throw new LoginException("RefreshToken is revoked.");
throw new LoginException(LoginExceptionReason.INVALID_REFRESH_TOKEN, "RefreshToken is revoked.");
}
if (old.getExpires().isBefore(LocalDateTime.now())) {
throw new LoginException("RefreshToken is expired.");
throw new LoginException(LoginExceptionReason.EXPIRED_REFRESH_TOKEN, "RefreshToken is expired.");
}
final UserEntity principal = old.getOwner();
this.refreshTokenRepository.delete(old);
@ -85,9 +91,6 @@ public final class AuthServiceImpl implements AuthService {
this.jwtService.generateAccessToken(username),
this.createRefreshToken(principal)
);
} catch (JwtException e) {
throw new LoginException(e);
}
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,8 @@
package app.mealsmadeeasy.api.auth;
public enum LoginExceptionReason {
INVALID_CREDENTIALS,
EXPIRED_REFRESH_TOKEN,
INVALID_REFRESH_TOKEN,
NO_REFRESH_TOKEN
}

View File

@ -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;
}
}