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); .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") @PostMapping("/login")
public ResponseEntity<LoginView> login(@RequestBody LoginBody loginBody) { public ResponseEntity<LoginView> login(@RequestBody LoginBody loginBody) throws LoginException {
try { final LoginDetails loginDetails = this.authService.login(loginBody.getUsername(), loginBody.getPassword());
final LoginDetails loginDetails = this.authService.login(loginBody.getUsername(), loginBody.getPassword()); return this.getLoginViewResponseEntity(loginDetails);
return this.getLoginViewResponseEntity(loginDetails);
} catch (LoginException loginException) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
} }
@PostMapping("/refresh") @PostMapping("/refresh")
public ResponseEntity<LoginView> refresh( public ResponseEntity<LoginView> refresh(
@CookieValue(value = "refresh-token", required = false) @Nullable String oldRefreshToken @CookieValue(value = "refresh-token", required = false) @Nullable String oldRefreshToken
) { ) throws LoginException {
if (oldRefreshToken != null) { final LoginDetails loginDetails = this.authService.refresh(oldRefreshToken);
try { return this.getLoginViewResponseEntity(loginDetails);
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") @PostMapping("/logout")

View File

@ -1,7 +1,9 @@
package app.mealsmadeeasy.api.auth; package app.mealsmadeeasy.api.auth;
import org.jetbrains.annotations.Nullable;
public interface AuthService { public interface AuthService {
LoginDetails login(String username, String password) throws LoginException; LoginDetails login(String username, String password) throws LoginException;
void logout(String refreshToken); 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.jwt.JwtService;
import app.mealsmadeeasy.api.user.UserEntity; 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.beans.factory.annotation.Value;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
@ -45,10 +45,9 @@ public final class AuthServiceImpl implements AuthService {
@Override @Override
public LoginDetails login(String username, String password) throws LoginException { public LoginDetails login(String username, String password) throws LoginException {
try { try {
final Authentication authentication = this.authenticationManager.authenticate(new UsernamePasswordAuthenticationToken( final Authentication authentication = this.authenticationManager.authenticate(
username, new UsernamePasswordAuthenticationToken(username, password)
password );
));
final UserEntity principal = (UserEntity) authentication.getPrincipal(); final UserEntity principal = (UserEntity) authentication.getPrincipal();
return new LoginDetails( return new LoginDetails(
username, username,
@ -56,7 +55,7 @@ public final class AuthServiceImpl implements AuthService {
this.createRefreshToken(principal) this.createRefreshToken(principal)
); );
} catch (AuthenticationException e) { } 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 @Override
public LoginDetails refresh(String refreshToken) throws LoginException { public LoginDetails refresh(@Nullable String refreshToken) throws LoginException {
try { if (refreshToken == null) {
final RefreshTokenEntity old = this.refreshTokenRepository.findByToken(refreshToken) throw new LoginException(LoginExceptionReason.NO_REFRESH_TOKEN, "No refresh token provided.");
.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);
} }
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)
);
} }
} }

View File

@ -2,16 +2,20 @@ package app.mealsmadeeasy.api.auth;
public final class LoginException extends Exception { public final class LoginException extends Exception {
public LoginException(String message) { private final LoginExceptionReason reason;
public LoginException(LoginExceptionReason reason, String message) {
super(message); super(message);
this.reason = reason;
} }
public LoginException(String message, Throwable cause) { public LoginException(LoginExceptionReason reason, Throwable cause) {
super(message, cause);
}
public LoginException(Throwable cause) {
super(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;
}
}