From 36a7aacec76675143c69ffa1b66d4f621817407b Mon Sep 17 00:00:00 2001 From: Florian THIERRY Date: Thu, 30 Nov 2023 10:47:59 +0100 Subject: [PATCH] Implementation of login endpoint. --- pom.xml | 6 +++ sportshub-application/pom.xml | 4 ++ .../configuration/SecurityConfiguration.java | 9 ++-- .../CustomUserDetailsService.java | 4 +- .../application/security/JwtService.java | 46 +++++++++++++++++++ .../model}/CustomUserDetails.java | 2 +- .../application/user/UserUseCases.java | 20 +++++++- .../exception/FunctionnalException.java | 7 +++ .../exception/LoginFailureException.java | 7 +++ .../GlobalControllerExceptionHandler.java | 17 +++++++ .../exposition/user/UserController.java | 8 +++- .../exposition/user/model/LoginRequest.java | 8 ++++ .../src/main/resources/application.yml | 4 ++ 13 files changed, 134 insertions(+), 8 deletions(-) rename sportshub-application/src/main/java/org/sportshub/application/{user => security}/CustomUserDetailsService.java (87%) create mode 100644 sportshub-application/src/main/java/org/sportshub/application/security/JwtService.java rename sportshub-application/src/main/java/org/sportshub/application/{user => security/model}/CustomUserDetails.java (95%) create mode 100644 sportshub-domain/src/main/java/org/sportshub/domain/exception/FunctionnalException.java create mode 100644 sportshub-domain/src/main/java/org/sportshub/domain/exception/LoginFailureException.java create mode 100644 sportshub-exposition/src/main/java/org/sportshub/exposition/configuration/GlobalControllerExceptionHandler.java create mode 100644 sportshub-exposition/src/main/java/org/sportshub/exposition/user/model/LoginRequest.java diff --git a/pom.xml b/pom.xml index dc5dd00..11904b5 100644 --- a/pom.xml +++ b/pom.xml @@ -16,6 +16,7 @@ 21 21 6.0.0 + 4.4.0 @@ -60,6 +61,11 @@ jakarta.servlet-api ${jakarta.servlet-api.version} + + com.auth0 + java-jwt + ${java-jwt.version} + diff --git a/sportshub-application/pom.xml b/sportshub-application/pom.xml index 63fa59d..0e82aa4 100644 --- a/sportshub-application/pom.xml +++ b/sportshub-application/pom.xml @@ -33,5 +33,9 @@ jakarta.servlet jakarta.servlet-api + + com.auth0 + java-jwt + diff --git a/sportshub-application/src/main/java/org/sportshub/application/configuration/SecurityConfiguration.java b/sportshub-application/src/main/java/org/sportshub/application/configuration/SecurityConfiguration.java index 8281378..fc63fa3 100644 --- a/sportshub-application/src/main/java/org/sportshub/application/configuration/SecurityConfiguration.java +++ b/sportshub-application/src/main/java/org/sportshub/application/configuration/SecurityConfiguration.java @@ -12,6 +12,9 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import static jakarta.servlet.DispatcherType.FORWARD; +import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN; +import static jakarta.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; import jakarta.servlet.DispatcherType; import jakarta.servlet.http.HttpServletResponse; @@ -26,7 +29,7 @@ public class SecurityConfiguration { .csrf(AbstractHttpConfigurer::disable) .httpBasic(Customizer.withDefaults()) .authorizeHttpRequests(requests -> requests - .dispatcherTypeMatchers(DispatcherType.FORWARD).permitAll() + .dispatcherTypeMatchers(FORWARD).permitAll() .requestMatchers( HttpMethod.GET, "/api/health/check" @@ -39,10 +42,10 @@ public class SecurityConfiguration { ) .exceptionHandling(configurer -> configurer .defaultAuthenticationEntryPointFor( - (request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED), + (request, response, authException) -> response.sendError(SC_UNAUTHORIZED), new AntPathRequestMatcher("/api/**") ).defaultAccessDeniedHandlerFor( - (request, response, accessDeniedException) -> response.sendError(HttpServletResponse.SC_FORBIDDEN), + (request, response, accessDeniedException) -> response.sendError(SC_FORBIDDEN), new AntPathRequestMatcher("/api/**") ) ); diff --git a/sportshub-application/src/main/java/org/sportshub/application/user/CustomUserDetailsService.java b/sportshub-application/src/main/java/org/sportshub/application/security/CustomUserDetailsService.java similarity index 87% rename from sportshub-application/src/main/java/org/sportshub/application/user/CustomUserDetailsService.java rename to sportshub-application/src/main/java/org/sportshub/application/security/CustomUserDetailsService.java index 5eec933..0945708 100644 --- a/sportshub-application/src/main/java/org/sportshub/application/user/CustomUserDetailsService.java +++ b/sportshub-application/src/main/java/org/sportshub/application/security/CustomUserDetailsService.java @@ -1,7 +1,9 @@ -package org.sportshub.application.user; +package org.sportshub.application.security; import java.util.UUID; +import org.sportshub.application.security.model.CustomUserDetails; +import org.sportshub.application.user.UserUseCases; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; diff --git a/sportshub-application/src/main/java/org/sportshub/application/security/JwtService.java b/sportshub-application/src/main/java/org/sportshub/application/security/JwtService.java new file mode 100644 index 0000000..b3a766d --- /dev/null +++ b/sportshub-application/src/main/java/org/sportshub/application/security/JwtService.java @@ -0,0 +1,46 @@ +package org.sportshub.application.security; + +import java.time.ZonedDateTime; + +import org.sportshub.domain.user.model.User; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.exceptions.JWTVerificationException; + +@Service +public class JwtService { + private final Algorithm algorithm; + private final JWTVerifier jwtVerifier; + + public JwtService(@Value("${application.security.secretKey}") String secretKey) { + algorithm = Algorithm.HMAC512(secretKey); + jwtVerifier = JWT.require(algorithm).build(); + } + + public String createJwt(User user) { + ZonedDateTime expirationDate = ZonedDateTime.now().plusMinutes(30); + + return JWT.create() + .withSubject(user.id().toString()) + .withExpiresAt(expirationDate.toInstant()) + .sign(algorithm); + } + + public boolean isValid(String token) { + boolean result; + try { + jwtVerifier.verify(token); + result = true; + } catch (JWTVerificationException exception) { + result = false; + } + return result; + } + + public String extractUsername(String token) { + return JWT.decode(token).getSubject(); + } +} diff --git a/sportshub-application/src/main/java/org/sportshub/application/user/CustomUserDetails.java b/sportshub-application/src/main/java/org/sportshub/application/security/model/CustomUserDetails.java similarity index 95% rename from sportshub-application/src/main/java/org/sportshub/application/user/CustomUserDetails.java rename to sportshub-application/src/main/java/org/sportshub/application/security/model/CustomUserDetails.java index d91ac08..9ed740d 100644 --- a/sportshub-application/src/main/java/org/sportshub/application/user/CustomUserDetails.java +++ b/sportshub-application/src/main/java/org/sportshub/application/security/model/CustomUserDetails.java @@ -1,4 +1,4 @@ -package org.sportshub.application.user; +package org.sportshub.application.security.model; import static java.util.Collections.emptyList; import java.util.Collection; diff --git a/sportshub-application/src/main/java/org/sportshub/application/user/UserUseCases.java b/sportshub-application/src/main/java/org/sportshub/application/user/UserUseCases.java index 60eaa63..6956dad 100644 --- a/sportshub-application/src/main/java/org/sportshub/application/user/UserUseCases.java +++ b/sportshub-application/src/main/java/org/sportshub/application/user/UserUseCases.java @@ -4,15 +4,26 @@ import java.util.List; import java.util.Optional; import java.util.UUID; +import org.sportshub.application.security.JwtService; +import org.sportshub.domain.exception.LoginFailureException; import org.sportshub.domain.user.model.User; import org.sportshub.domain.user.port.UserPort; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @Service public class UserUseCases { + private final PasswordEncoder passwordEncoder; + private final JwtService jwtService; private final UserPort userPort; - public UserUseCases(final UserPort userPort) { + public UserUseCases( + PasswordEncoder passwordEncoder, + JwtService jwtService, + UserPort userPort + ) { + this.passwordEncoder = passwordEncoder; + this.jwtService = jwtService; this.userPort = userPort; } @@ -23,4 +34,11 @@ public class UserUseCases { public List findAll() { return userPort.findAll(); } + + public String authenticate(final UUID id, final String password) { + return userPort.findById(id) + .filter(user -> passwordEncoder.matches(password, user.password())) + .map(jwtService::createJwt) + .orElseThrow(LoginFailureException::new); + } } diff --git a/sportshub-domain/src/main/java/org/sportshub/domain/exception/FunctionnalException.java b/sportshub-domain/src/main/java/org/sportshub/domain/exception/FunctionnalException.java new file mode 100644 index 0000000..c763d5f --- /dev/null +++ b/sportshub-domain/src/main/java/org/sportshub/domain/exception/FunctionnalException.java @@ -0,0 +1,7 @@ +package org.sportshub.domain.exception; + +public abstract class FunctionnalException extends RuntimeException { + public FunctionnalException(final String message) { + super(message); + } +} diff --git a/sportshub-domain/src/main/java/org/sportshub/domain/exception/LoginFailureException.java b/sportshub-domain/src/main/java/org/sportshub/domain/exception/LoginFailureException.java new file mode 100644 index 0000000..29a949f --- /dev/null +++ b/sportshub-domain/src/main/java/org/sportshub/domain/exception/LoginFailureException.java @@ -0,0 +1,7 @@ +package org.sportshub.domain.exception; + +public class LoginFailureException extends FunctionnalException { + public LoginFailureException() { + super("Login or password incorrect."); + } +} diff --git a/sportshub-exposition/src/main/java/org/sportshub/exposition/configuration/GlobalControllerExceptionHandler.java b/sportshub-exposition/src/main/java/org/sportshub/exposition/configuration/GlobalControllerExceptionHandler.java new file mode 100644 index 0000000..f2e33e4 --- /dev/null +++ b/sportshub-exposition/src/main/java/org/sportshub/exposition/configuration/GlobalControllerExceptionHandler.java @@ -0,0 +1,17 @@ +package org.sportshub.exposition.configuration; + +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import org.sportshub.domain.exception.LoginFailureException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ControllerAdvice +public class GlobalControllerExceptionHandler { + + @ResponseStatus(BAD_REQUEST) + @ExceptionHandler(LoginFailureException.class) + public void handleLoginFailureException() { + // Do nothing. + } +} diff --git a/sportshub-exposition/src/main/java/org/sportshub/exposition/user/UserController.java b/sportshub-exposition/src/main/java/org/sportshub/exposition/user/UserController.java index f39ad2d..7592ddc 100644 --- a/sportshub-exposition/src/main/java/org/sportshub/exposition/user/UserController.java +++ b/sportshub-exposition/src/main/java/org/sportshub/exposition/user/UserController.java @@ -1,11 +1,15 @@ package org.sportshub.exposition.user; import java.util.List; +import java.util.Optional; import org.sportshub.application.user.UserUseCases; import org.sportshub.domain.user.model.User; +import org.sportshub.exposition.user.model.LoginRequest; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -19,8 +23,8 @@ public class UserController { } @PostMapping("/login") - public String login() { - return ""; + public String login(@RequestBody LoginRequest request) { + return userUseCases.authenticate(request.id(), request.password()); } @GetMapping diff --git a/sportshub-exposition/src/main/java/org/sportshub/exposition/user/model/LoginRequest.java b/sportshub-exposition/src/main/java/org/sportshub/exposition/user/model/LoginRequest.java new file mode 100644 index 0000000..67731e8 --- /dev/null +++ b/sportshub-exposition/src/main/java/org/sportshub/exposition/user/model/LoginRequest.java @@ -0,0 +1,8 @@ +package org.sportshub.exposition.user.model; + +import java.util.UUID; + +public record LoginRequest( + UUID id, + String password +) {} diff --git a/sportshub-launcher/src/main/resources/application.yml b/sportshub-launcher/src/main/resources/application.yml index fd51fb8..acf3965 100644 --- a/sportshub-launcher/src/main/resources/application.yml +++ b/sportshub-launcher/src/main/resources/application.yml @@ -1,3 +1,7 @@ +application: + security: + secretKey: "secret-key" + logging: level: org.springframework.security: DEBUG \ No newline at end of file