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