From ef69bd769940582bfc15ce3fbc650665c02c244e Mon Sep 17 00:00:00 2001 From: Nour Eldien Ayman Date: Sun, 27 Apr 2025 14:05:43 +0300 Subject: [PATCH 1/9] Add authentication, security, and JWT functionality Implement foundational authentication features, including user and role management, JWT-based authentication, and role-based security configurations. Add REST endpoints for login, signup, and logout, along with a Dockerized development setup, CI/CD workflows, and logging configurations. # Conflicts: # .github/workflows/ci-cd.yml # src/main/java/com/podzilla/auth/controller/AuthenticationController.java # src/main/java/com/podzilla/auth/model/User.java # src/main/java/com/podzilla/auth/security/JWTAuthenticationFilter.java # src/main/java/com/podzilla/auth/security/SecurityConfig.java # src/main/java/com/podzilla/auth/service/AuthenticationService.java --- .../com/podzilla/auth/service/JWTService.java | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 src/main/java/com/podzilla/auth/service/JWTService.java diff --git a/src/main/java/com/podzilla/auth/service/JWTService.java b/src/main/java/com/podzilla/auth/service/JWTService.java new file mode 100644 index 0000000..8efd941 --- /dev/null +++ b/src/main/java/com/podzilla/auth/service/JWTService.java @@ -0,0 +1,90 @@ +package com.podzilla.auth.service; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.util.WebUtils; + +import javax.crypto.SecretKey; +import java.util.Date; + +@Service +public class JWTService { + + // set in .env + @Value("${jwt.token.secret}") + private String secret; + + @Value("${jwt.token.expires}") + private Long jwtExpiresMinutes; + + private Claims claims; + + private static final Integer EXPIRATION_TIME = 60 * 1000; + private static final Integer MAX_AGE = 24 * 60 * 60; + + public void generateToken(final String email, + final HttpServletResponse response) { + String jwt = Jwts.builder() + .subject(email) + .issuedAt(new Date(System.currentTimeMillis())) + .expiration(new Date(System.currentTimeMillis() + + jwtExpiresMinutes * EXPIRATION_TIME)) + .signWith(getSignInKey()) + .compact(); + + Cookie cookie = new Cookie("JWT", jwt); + cookie.setHttpOnly(true); + cookie.setSecure(true); + cookie.setPath("/"); + cookie.setMaxAge(MAX_AGE); + response.addCookie(cookie); + } + + public String getJwtFromCookie(final HttpServletRequest request) { + Cookie cookie = WebUtils.getCookie(request, "JWT"); + if (cookie != null) { + return cookie.getValue(); + } + return null; + + } + + public void validateToken(final String token) throws JwtException { + try { + claims = Jwts.parser() + .verifyWith(getSignInKey()) + .build() + .parseSignedClaims(token) + .getPayload(); + + + } catch (JwtException e) { + throw new JwtException(e.getMessage()); + } + } + + public void removeTokenFromCookie(final HttpServletResponse response) { + Cookie cookie = new Cookie("JWT", null); + cookie.setPath("/"); + + response.addCookie(cookie); + } + + private SecretKey getSignInKey() { + byte[] keyBytes = Decoders.BASE64.decode(this.secret); + return Keys.hmacShaKeyFor(keyBytes); + } + + public String extractEmail() { + return claims.getSubject(); + } + +} From dfa106459af00e191b7459e1251d3e5fa33322d4 Mon Sep 17 00:00:00 2001 From: Nour Eldien Ayman Date: Sun, 27 Apr 2025 16:06:43 +0300 Subject: [PATCH 2/9] Create tmp --- tmp | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tmp diff --git a/tmp b/tmp new file mode 100644 index 0000000..e69de29 From cfa49220561d40abe4db69f432993c847b1a7b45 Mon Sep 17 00:00:00 2001 From: Nour Eldien Ayman Date: Sun, 27 Apr 2025 16:20:00 +0300 Subject: [PATCH 3/9] Fix authApplication --- tmp | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tmp diff --git a/tmp b/tmp deleted file mode 100644 index e69de29..0000000 From cd4a62768065c97f09ac5af98574b208da0c2f3d Mon Sep 17 00:00:00 2001 From: Nour Eldien Ayman Date: Sun, 27 Apr 2025 18:39:32 +0300 Subject: [PATCH 4/9] Add refresh token --- .../java/com/podzilla/auth/dto/AuthenticationResponse.java | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src/main/java/com/podzilla/auth/dto/AuthenticationResponse.java diff --git a/src/main/java/com/podzilla/auth/dto/AuthenticationResponse.java b/src/main/java/com/podzilla/auth/dto/AuthenticationResponse.java new file mode 100644 index 0000000..6d45a2d --- /dev/null +++ b/src/main/java/com/podzilla/auth/dto/AuthenticationResponse.java @@ -0,0 +1,6 @@ +package com.podzilla.auth.dto; + +import java.util.UUID; + +public record AuthenticationResponse(String email, UUID refreshToken) { +} From b2a67270fa6df19d38e5814928ae5ad78944af70 Mon Sep 17 00:00:00 2001 From: Nour Eldien Ayman Date: Sun, 27 Apr 2025 22:37:46 +0300 Subject: [PATCH 5/9] Add refresh token to the cookie instead of returning it back to the frontend. --- .../auth/dto/AuthenticationResponse.java | 6 - .../com/podzilla/auth/service/JWTService.java | 111 ++++++++++++++++-- 2 files changed, 100 insertions(+), 17 deletions(-) delete mode 100644 src/main/java/com/podzilla/auth/dto/AuthenticationResponse.java diff --git a/src/main/java/com/podzilla/auth/dto/AuthenticationResponse.java b/src/main/java/com/podzilla/auth/dto/AuthenticationResponse.java deleted file mode 100644 index 6d45a2d..0000000 --- a/src/main/java/com/podzilla/auth/dto/AuthenticationResponse.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.podzilla.auth.dto; - -import java.util.UUID; - -public record AuthenticationResponse(String email, UUID refreshToken) { -} diff --git a/src/main/java/com/podzilla/auth/service/JWTService.java b/src/main/java/com/podzilla/auth/service/JWTService.java index 8efd941..86623ce 100644 --- a/src/main/java/com/podzilla/auth/service/JWTService.java +++ b/src/main/java/com/podzilla/auth/service/JWTService.java @@ -1,19 +1,28 @@ package com.podzilla.auth.service; +import com.podzilla.auth.model.RefreshToken; +import com.podzilla.auth.model.User; +import com.podzilla.auth.repository.RefreshTokenRepository; +import com.podzilla.auth.repository.UserRepository; import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtException; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; +import jakarta.persistence.EntityExistsException; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.ValidationException; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.util.WebUtils; import javax.crypto.SecretKey; +import java.time.Instant; +import java.time.temporal.TemporalAmount; import java.util.Date; +import java.util.UUID; @Service public class JWTService { @@ -27,29 +36,92 @@ public class JWTService { private Claims claims; - private static final Integer EXPIRATION_TIME = 60 * 1000; - private static final Integer MAX_AGE = 24 * 60 * 60; + private static final Integer ACCESS_TOKEN_EXPIRATION_TIME = 60 * 1000; + private static final Integer ACCESS_TOKEN_COOKIE_EXPIRATION_TIME = 60 * 30; + private static final TemporalAmount REFRESH_TOKEN_EXPIRATION_TIME = + java.time.Duration.ofDays(10); + private static final Integer REFRESH_TOKEN_COOKIE_EXPIRATION_TIME = + 60 * 60 * 24 * 10; - public void generateToken(final String email, - final HttpServletResponse response) { + private final UserRepository userRepository; + private final RefreshTokenRepository refreshTokenRepository; + + public JWTService(final UserRepository userRepository, + final RefreshTokenRepository refreshTokenRepository) { + this.userRepository = userRepository; + this.refreshTokenRepository = refreshTokenRepository; + } + + public void generateAccessToken(final String email, + final HttpServletResponse response) { String jwt = Jwts.builder() .subject(email) .issuedAt(new Date(System.currentTimeMillis())) .expiration(new Date(System.currentTimeMillis() - + jwtExpiresMinutes * EXPIRATION_TIME)) + + jwtExpiresMinutes * ACCESS_TOKEN_EXPIRATION_TIME)) .signWith(getSignInKey()) .compact(); - Cookie cookie = new Cookie("JWT", jwt); + Cookie cookie = new Cookie("accessToken", jwt); cookie.setHttpOnly(true); cookie.setSecure(true); cookie.setPath("/"); - cookie.setMaxAge(MAX_AGE); + cookie.setMaxAge(ACCESS_TOKEN_COOKIE_EXPIRATION_TIME); + response.addCookie(cookie); + } + + public void generateRefreshToken(final String email, + final HttpServletResponse response) { + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new EntityExistsException("User not found")); + + RefreshToken refreshToken = new RefreshToken(); + refreshToken.setUser(user); + refreshToken.setExpiresAt(Instant.now().plus( + REFRESH_TOKEN_EXPIRATION_TIME)); + refreshTokenRepository.save(refreshToken); + + String refreshTokenString = refreshToken.getId().toString(); + addRefreshTokenToCookie(refreshTokenString, response); + } + + public String renewRefreshToken(final String refreshToken, + final HttpServletResponse response) { + RefreshToken token = + refreshTokenRepository + .findByIdAndExpiresAtAfter( + UUID.fromString(refreshToken), Instant.now()) + .orElseThrow(() -> + new ValidationException( + "Invalid refresh token")); + + token.setExpiresAt(Instant.now()); + refreshTokenRepository.save(token); + + RefreshToken newRefreshToken = new RefreshToken(); + newRefreshToken.setUser(token.getUser()); + newRefreshToken.setExpiresAt(Instant.now().plus( + REFRESH_TOKEN_EXPIRATION_TIME)); + refreshTokenRepository.save(newRefreshToken); + + String newRefreshTokenString = newRefreshToken.getId().toString(); + addRefreshTokenToCookie(newRefreshTokenString, response); + + return token.getUser().getEmail(); + } + + private void addRefreshTokenToCookie(final String refreshToken, + final HttpServletResponse response) { + Cookie cookie = new Cookie("refreshToken", refreshToken); + cookie.setHttpOnly(true); + cookie.setSecure(true); + cookie.setPath("/api/auth/refresh-token"); + cookie.setMaxAge(REFRESH_TOKEN_COOKIE_EXPIRATION_TIME); response.addCookie(cookie); } - public String getJwtFromCookie(final HttpServletRequest request) { - Cookie cookie = WebUtils.getCookie(request, "JWT"); + public String getAccessTokenFromCookie(final HttpServletRequest request) { + Cookie cookie = WebUtils.getCookie(request, "accessToken"); if (cookie != null) { return cookie.getValue(); } @@ -57,7 +129,15 @@ public String getJwtFromCookie(final HttpServletRequest request) { } - public void validateToken(final String token) throws JwtException { + public String getRefreshTokenFromCookie(final HttpServletRequest request) { + Cookie cookie = WebUtils.getCookie(request, "refreshToken"); + if (cookie != null) { + return cookie.getValue(); + } + return null; + } + + public void validateAccessToken(final String token) throws JwtException { try { claims = Jwts.parser() .verifyWith(getSignInKey()) @@ -71,13 +151,22 @@ public void validateToken(final String token) throws JwtException { } } - public void removeTokenFromCookie(final HttpServletResponse response) { + public void removeAccessTokenFromCookie( + final HttpServletResponse response) { Cookie cookie = new Cookie("JWT", null); cookie.setPath("/"); response.addCookie(cookie); } + public void removeRefreshTokenFromCookie( + final HttpServletResponse response) { + Cookie cookie = new Cookie("refreshToken", null); + cookie.setPath("/api/auth/refresh-token"); + + response.addCookie(cookie); + } + private SecretKey getSignInKey() { byte[] keyBytes = Decoders.BASE64.decode(this.secret); return Keys.hmacShaKeyFor(keyBytes); From 87db186534d7cad8bb0cc50ba9d39134f393bba2 Mon Sep 17 00:00:00 2001 From: Nour Eldien Ayman Date: Mon, 28 Apr 2025 20:22:52 +0300 Subject: [PATCH 6/9] Rename signup endpoint to register and enhance refresh token management --- .../com/podzilla/auth/service/JWTService.java | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/podzilla/auth/service/JWTService.java b/src/main/java/com/podzilla/auth/service/JWTService.java index 86623ce..8be6c33 100644 --- a/src/main/java/com/podzilla/auth/service/JWTService.java +++ b/src/main/java/com/podzilla/auth/service/JWTService.java @@ -74,19 +74,25 @@ public void generateRefreshToken(final String email, final HttpServletResponse response) { User user = userRepository.findByEmail(email) .orElseThrow(() -> new EntityExistsException("User not found")); + RefreshToken userRefreshToken = + refreshTokenRepository.findByUserIdAndExpiresAtAfter( + user.getId(), Instant.now()).orElse(null); + + if (userRefreshToken == null) { + userRefreshToken = new RefreshToken(); + userRefreshToken.setUser(user); + userRefreshToken.setCreatedAt(Instant.now()); + userRefreshToken.setExpiresAt(Instant.now().plus( + REFRESH_TOKEN_EXPIRATION_TIME)); + refreshTokenRepository.save(userRefreshToken); + } - RefreshToken refreshToken = new RefreshToken(); - refreshToken.setUser(user); - refreshToken.setExpiresAt(Instant.now().plus( - REFRESH_TOKEN_EXPIRATION_TIME)); - refreshTokenRepository.save(refreshToken); - - String refreshTokenString = refreshToken.getId().toString(); + String refreshTokenString = userRefreshToken.getId().toString(); addRefreshTokenToCookie(refreshTokenString, response); } public String renewRefreshToken(final String refreshToken, - final HttpServletResponse response) { + final HttpServletResponse response) { RefreshToken token = refreshTokenRepository .findByIdAndExpiresAtAfter( @@ -100,6 +106,7 @@ public String renewRefreshToken(final String refreshToken, RefreshToken newRefreshToken = new RefreshToken(); newRefreshToken.setUser(token.getUser()); + newRefreshToken.setCreatedAt(Instant.now()); newRefreshToken.setExpiresAt(Instant.now().plus( REFRESH_TOKEN_EXPIRATION_TIME)); refreshTokenRepository.save(newRefreshToken); From 64eeff346a127d206b867522c02077d27f501415 Mon Sep 17 00:00:00 2001 From: Nour Eldien Ayman Date: Mon, 28 Apr 2025 22:37:19 +0300 Subject: [PATCH 7/9] Add Swagger annotations for API documentation and implement custom exception handling --- .../auth/controller/AdminController.java | 6 ++ .../controller/AuthenticationController.java | 82 +++++++++++-------- .../auth/controller/ResourceController.java | 23 ------ .../auth/exception/ErrorResponse.java | 19 +++++ .../exception/GlobalExceptionHandler.java | 42 ++++++++++ .../exception/InvalidActionException.java | 7 ++ .../auth/exception/NotFoundException.java | 7 ++ .../auth/exception/ValidationException.java | 7 ++ .../auth/service/AuthenticationService.java | 6 +- .../service/CustomUserDetailsService.java | 7 +- .../com/podzilla/auth/service/JWTService.java | 9 +- 11 files changed, 147 insertions(+), 68 deletions(-) delete mode 100644 src/main/java/com/podzilla/auth/controller/ResourceController.java create mode 100644 src/main/java/com/podzilla/auth/exception/ErrorResponse.java create mode 100644 src/main/java/com/podzilla/auth/exception/GlobalExceptionHandler.java create mode 100644 src/main/java/com/podzilla/auth/exception/InvalidActionException.java create mode 100644 src/main/java/com/podzilla/auth/exception/NotFoundException.java create mode 100644 src/main/java/com/podzilla/auth/exception/ValidationException.java diff --git a/src/main/java/com/podzilla/auth/controller/AdminController.java b/src/main/java/com/podzilla/auth/controller/AdminController.java index 2f91bff..29915ad 100644 --- a/src/main/java/com/podzilla/auth/controller/AdminController.java +++ b/src/main/java/com/podzilla/auth/controller/AdminController.java @@ -2,6 +2,8 @@ import com.podzilla.auth.model.User; import com.podzilla.auth.service.AdminService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -19,6 +21,10 @@ public AdminController(final AdminService adminService) { } @GetMapping("/users") + @Operation(summary = "Get all users", + description = "Fetches all users in the system.") + @ApiResponse(responseCode = "200", + description = "Users fetched successfully") public List getUsers() { return adminService.getUsers(); } diff --git a/src/main/java/com/podzilla/auth/controller/AuthenticationController.java b/src/main/java/com/podzilla/auth/controller/AuthenticationController.java index 7f23f24..2d29298 100644 --- a/src/main/java/com/podzilla/auth/controller/AuthenticationController.java +++ b/src/main/java/com/podzilla/auth/controller/AuthenticationController.java @@ -3,6 +3,8 @@ import com.podzilla.auth.dto.LoginRequest; import com.podzilla.auth.dto.SignupRequest; import com.podzilla.auth.service.AuthenticationService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.slf4j.Logger; @@ -35,59 +37,73 @@ public AuthenticationController( } @PostMapping("/login") + @Operation( + summary = "Login", + description = "Logs in a user and generates JWT tokens." + ) + @ApiResponse( + responseCode = "200", + description = "User logged in successfully" + ) public ResponseEntity login( @RequestBody final LoginRequest loginRequest, final HttpServletResponse response) { - try { - String email = authenticationService.login(loginRequest, response); - LOGGER.info("User {} logged in", email); - return new ResponseEntity<>( - "User " + email + " logged in successfully", - HttpStatus.OK); - } catch (Exception e) { - LOGGER.error(e.getMessage()); - return new ResponseEntity<>(e.getMessage(), - HttpStatus.UNAUTHORIZED); - } + String email = authenticationService.login(loginRequest, response); + LOGGER.info("User {} logged in", email); + return new ResponseEntity<>( + "User " + email + " logged in successfully", + HttpStatus.OK); } @PostMapping("/register") + @Operation( + summary = "Register", + description = "Registers a new user." + ) + @ApiResponse( + responseCode = "201", + description = "User registered successfully" + ) public ResponseEntity registerUser( @RequestBody final SignupRequest signupRequest, final HttpServletRequest request) { - try { - authenticationService.registerAccount(signupRequest); - LOGGER.info("User {} registered", signupRequest.getEmail()); - return new ResponseEntity<>("Account registered.", - HttpStatus.CREATED); - } catch (Exception e) { - LOGGER.error(e.getMessage()); - return new ResponseEntity<>(e.getMessage(), - HttpStatus.UNAUTHORIZED); - } + authenticationService.registerAccount(signupRequest); + LOGGER.info("User {} registered", signupRequest.getEmail()); + return new ResponseEntity<>("Account registered.", + HttpStatus.CREATED); } @PostMapping("/logout") + @Operation( + summary = "Logout", + description = "Logs out a user and invalidates JWT tokens." + ) + @ApiResponse( + responseCode = "200", + description = "User logged out successfully" + ) public ResponseEntity logoutUser(final HttpServletResponse response) { authenticationService.logoutUser(response); return new ResponseEntity<>("You've been signed out!", HttpStatus.OK); } @PostMapping("/refresh-token") + @Operation( + summary = "Refresh Token", + description = "Refreshes the JWT tokens for a logged-in user." + ) + @ApiResponse( + responseCode = "200", + description = "Token refreshed successfully" + ) public ResponseEntity refreshToken( final HttpServletRequest request, final HttpServletResponse response) { - try { - String email = authenticationService.refreshToken( - request, response); - LOGGER.info("User {} refreshed token", email); - return new ResponseEntity<>( - "User " + email + " refreshed token successfully", - HttpStatus.OK); - } catch (Exception e) { - LOGGER.error(e.getMessage()); - return new ResponseEntity<>(e.getMessage(), - HttpStatus.UNAUTHORIZED); - } + String email = authenticationService.refreshToken( + request, response); + LOGGER.info("User {} refreshed token", email); + return new ResponseEntity<>( + "User " + email + " refreshed tokens successfully", + HttpStatus.OK); } } diff --git a/src/main/java/com/podzilla/auth/controller/ResourceController.java b/src/main/java/com/podzilla/auth/controller/ResourceController.java deleted file mode 100644 index 62c464a..0000000 --- a/src/main/java/com/podzilla/auth/controller/ResourceController.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.podzilla.auth.controller; - -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -public class ResourceController { - - @GetMapping("/secret_resource") - public ResponseEntity secret() { - return new ResponseEntity<>( - "You are viewing my secret", HttpStatus.OK); - } - - @GetMapping("/public_resource") - public ResponseEntity noSecret() { - // assuming no existing user - - return new ResponseEntity<>("You are in public area", HttpStatus.OK); - } -} diff --git a/src/main/java/com/podzilla/auth/exception/ErrorResponse.java b/src/main/java/com/podzilla/auth/exception/ErrorResponse.java new file mode 100644 index 0000000..f5fc0e8 --- /dev/null +++ b/src/main/java/com/podzilla/auth/exception/ErrorResponse.java @@ -0,0 +1,19 @@ +package com.podzilla.auth.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +import java.time.LocalDateTime; + +@Getter +public class ErrorResponse { + private final String message; + private final HttpStatus status; + private final LocalDateTime timestamp; + + public ErrorResponse(final String message, final HttpStatus status) { + this.message = message; + this.status = status; + this.timestamp = LocalDateTime.now(); + } +} diff --git a/src/main/java/com/podzilla/auth/exception/GlobalExceptionHandler.java b/src/main/java/com/podzilla/auth/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..527126e --- /dev/null +++ b/src/main/java/com/podzilla/auth/exception/GlobalExceptionHandler.java @@ -0,0 +1,42 @@ +package com.podzilla.auth.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(NotFoundException.class) + public ResponseEntity handleNotFoundException( + final NotFoundException exception) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(new ErrorResponse(exception.getMessage(), + HttpStatus.NOT_FOUND)); + } + + @ExceptionHandler(ValidationException.class) + public ResponseEntity handleValidationException( + final ValidationException exception) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(new ErrorResponse(exception.getMessage(), + HttpStatus.BAD_REQUEST)); + } + + @ExceptionHandler(InvalidActionException.class) + public ResponseEntity handleInvalidActionException( + final InvalidActionException exception) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(new ErrorResponse(exception.getMessage(), + HttpStatus.BAD_REQUEST)); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleException( + final Exception exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(new ErrorResponse(exception.getMessage(), + HttpStatus.INTERNAL_SERVER_ERROR)); + } +} diff --git a/src/main/java/com/podzilla/auth/exception/InvalidActionException.java b/src/main/java/com/podzilla/auth/exception/InvalidActionException.java new file mode 100644 index 0000000..b9ed654 --- /dev/null +++ b/src/main/java/com/podzilla/auth/exception/InvalidActionException.java @@ -0,0 +1,7 @@ +package com.podzilla.auth.exception; + +public class InvalidActionException extends RuntimeException { + public InvalidActionException(final String message) { + super("Invalid action: " + message); + } +} diff --git a/src/main/java/com/podzilla/auth/exception/NotFoundException.java b/src/main/java/com/podzilla/auth/exception/NotFoundException.java new file mode 100644 index 0000000..bd16723 --- /dev/null +++ b/src/main/java/com/podzilla/auth/exception/NotFoundException.java @@ -0,0 +1,7 @@ +package com.podzilla.auth.exception; + +public class NotFoundException extends RuntimeException { + public NotFoundException(final String message) { + super("Not Found: " + message); + } +} diff --git a/src/main/java/com/podzilla/auth/exception/ValidationException.java b/src/main/java/com/podzilla/auth/exception/ValidationException.java new file mode 100644 index 0000000..58df19a --- /dev/null +++ b/src/main/java/com/podzilla/auth/exception/ValidationException.java @@ -0,0 +1,7 @@ +package com.podzilla.auth.exception; + +public class ValidationException extends RuntimeException { + public ValidationException(final String message) { + super("Validation error: " + message); + } +} diff --git a/src/main/java/com/podzilla/auth/service/AuthenticationService.java b/src/main/java/com/podzilla/auth/service/AuthenticationService.java index db361b5..406dd52 100644 --- a/src/main/java/com/podzilla/auth/service/AuthenticationService.java +++ b/src/main/java/com/podzilla/auth/service/AuthenticationService.java @@ -2,12 +2,12 @@ import com.podzilla.auth.dto.LoginRequest; import com.podzilla.auth.dto.SignupRequest; +import com.podzilla.auth.exception.ValidationException; import com.podzilla.auth.model.ERole; import com.podzilla.auth.model.Role; import com.podzilla.auth.model.User; import com.podzilla.auth.repository.RoleRepository; import com.podzilla.auth.repository.UserRepository; -import jakarta.persistence.EntityExistsException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.security.authentication.AuthenticationManager; @@ -66,7 +66,7 @@ public String login(final LoginRequest loginRequest, public void registerAccount(final SignupRequest signupRequest) { if (userRepository.existsByEmail(signupRequest.getEmail())) { - throw new EntityExistsException("Email already used"); + throw new ValidationException("Email already in use."); } User account = new User( @@ -92,7 +92,7 @@ public String refreshToken(final HttpServletRequest request, tokenService.generateAccessToken(email, response); return email; } catch (IllegalArgumentException e) { - throw new IllegalArgumentException("Invalid refresh token"); + throw new ValidationException("Invalid refresh token."); } } } diff --git a/src/main/java/com/podzilla/auth/service/CustomUserDetailsService.java b/src/main/java/com/podzilla/auth/service/CustomUserDetailsService.java index 5e6da6e..aa23490 100644 --- a/src/main/java/com/podzilla/auth/service/CustomUserDetailsService.java +++ b/src/main/java/com/podzilla/auth/service/CustomUserDetailsService.java @@ -1,5 +1,6 @@ package com.podzilla.auth.service; +import com.podzilla.auth.exception.NotFoundException; import com.podzilla.auth.model.User; import com.podzilla.auth.repository.UserRepository; import org.springframework.beans.factory.annotation.Autowired; @@ -7,7 +8,6 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import java.util.Set; @@ -24,11 +24,10 @@ public CustomUserDetailsService(final UserRepository userRepository) { } @Override - public UserDetails loadUserByUsername(final String email) - throws UsernameNotFoundException { + public UserDetails loadUserByUsername(final String email) { User user = userRepository.findByEmail(email) .orElseThrow(() -> - new UsernameNotFoundException( + new NotFoundException( email + " not found.")); Set authorities = user diff --git a/src/main/java/com/podzilla/auth/service/JWTService.java b/src/main/java/com/podzilla/auth/service/JWTService.java index 8be6c33..e8577dc 100644 --- a/src/main/java/com/podzilla/auth/service/JWTService.java +++ b/src/main/java/com/podzilla/auth/service/JWTService.java @@ -1,5 +1,6 @@ package com.podzilla.auth.service; +import com.podzilla.auth.exception.ValidationException; import com.podzilla.auth.model.RefreshToken; import com.podzilla.auth.model.User; import com.podzilla.auth.repository.RefreshTokenRepository; @@ -9,11 +10,9 @@ import io.jsonwebtoken.Jwts; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; -import jakarta.persistence.EntityExistsException; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import jakarta.validation.ValidationException; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.util.WebUtils; @@ -73,7 +72,7 @@ public void generateAccessToken(final String email, public void generateRefreshToken(final String email, final HttpServletResponse response) { User user = userRepository.findByEmail(email) - .orElseThrow(() -> new EntityExistsException("User not found")); + .orElseThrow(() -> new ValidationException("User not found")); RefreshToken userRefreshToken = refreshTokenRepository.findByUserIdAndExpiresAtAfter( user.getId(), Instant.now()).orElse(null); @@ -144,7 +143,7 @@ public String getRefreshTokenFromCookie(final HttpServletRequest request) { return null; } - public void validateAccessToken(final String token) throws JwtException { + public void validateAccessToken(final String token) { try { claims = Jwts.parser() .verifyWith(getSignInKey()) @@ -154,7 +153,7 @@ public void validateAccessToken(final String token) throws JwtException { } catch (JwtException e) { - throw new JwtException(e.getMessage()); + throw new ValidationException(e.getMessage()); } } From 2ebee383935ca54b1b0fe57d1aa95b4a520682e2 Mon Sep 17 00:00:00 2001 From: Nour Eldien Ayman Date: Mon, 28 Apr 2025 22:37:58 +0300 Subject: [PATCH 8/9] Add Swagger annotations for API documentation and implement custom exception handling --- .../podzilla/auth/controller/AuthenticationController.java | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/main/java/com/podzilla/auth/controller/AuthenticationController.java b/src/main/java/com/podzilla/auth/controller/AuthenticationController.java index 2d29298..f441f96 100644 --- a/src/main/java/com/podzilla/auth/controller/AuthenticationController.java +++ b/src/main/java/com/podzilla/auth/controller/AuthenticationController.java @@ -12,7 +12,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -24,9 +23,6 @@ public class AuthenticationController { private final AuthenticationService authenticationService; - private final SecurityContextLogoutHandler logoutHandler = - new SecurityContextLogoutHandler(); - private static final Logger LOGGER = LoggerFactory.getLogger(AuthenticationController.class); @@ -65,8 +61,7 @@ public ResponseEntity login( description = "User registered successfully" ) public ResponseEntity registerUser( - @RequestBody final SignupRequest signupRequest, - final HttpServletRequest request) { + @RequestBody final SignupRequest signupRequest) { authenticationService.registerAccount(signupRequest); LOGGER.info("User {} registered", signupRequest.getEmail()); return new ResponseEntity<>("Account registered.", From 1ddf5f2458349d20b05db12c0088d5e950532b2f Mon Sep 17 00:00:00 2001 From: Nour Eldien Ayman Date: Mon, 28 Apr 2025 23:42:31 +0300 Subject: [PATCH 9/9] Configure SpringDoc OpenAPI settings and permit access to Swagger UI endpoints --- src/main/java/com/podzilla/auth/security/SecurityConfig.java | 2 ++ src/main/resources/application.properties | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/src/main/java/com/podzilla/auth/security/SecurityConfig.java b/src/main/java/com/podzilla/auth/security/SecurityConfig.java index 11e868e..d8b83f0 100644 --- a/src/main/java/com/podzilla/auth/security/SecurityConfig.java +++ b/src/main/java/com/podzilla/auth/security/SecurityConfig.java @@ -47,6 +47,8 @@ SecurityFilterChain securityFilterChain(final HttpSecurity http) .requestMatchers("/auth/**").permitAll() .requestMatchers("/admin/**") .hasRole("ADMIN") + .requestMatchers("/swagger-ui/**", + "/v3/api-docs/**").permitAll() .anyRequest().authenticated() ) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index e9d1807..be2b6ed 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,5 +1,9 @@ spring.application.name=auth +# SpringDoc OpenAPI configuration, uncomment on production +#springdoc.api-docs.enabled=false +#springdoc.swagger-ui.enabled=false + logging.file.name=./logs/app.log logging.level.root=info logging.level.com.podzilla.auth=debug