Implementation of login endpoint.
This commit is contained in:
6
pom.xml
6
pom.xml
@@ -16,6 +16,7 @@
|
|||||||
<maven.compiler.source>21</maven.compiler.source>
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
<maven.compiler.target>21</maven.compiler.target>
|
<maven.compiler.target>21</maven.compiler.target>
|
||||||
<jakarta.servlet-api.version>6.0.0</jakarta.servlet-api.version>
|
<jakarta.servlet-api.version>6.0.0</jakarta.servlet-api.version>
|
||||||
|
<java-jwt.version>4.4.0</java-jwt.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<modules>
|
<modules>
|
||||||
@@ -60,6 +61,11 @@
|
|||||||
<artifactId>jakarta.servlet-api</artifactId>
|
<artifactId>jakarta.servlet-api</artifactId>
|
||||||
<version>${jakarta.servlet-api.version}</version>
|
<version>${jakarta.servlet-api.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.auth0</groupId>
|
||||||
|
<artifactId>java-jwt</artifactId>
|
||||||
|
<version>${java-jwt.version}</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
|
|
||||||
|
|||||||
@@ -33,5 +33,9 @@
|
|||||||
<groupId>jakarta.servlet</groupId>
|
<groupId>jakarta.servlet</groupId>
|
||||||
<artifactId>jakarta.servlet-api</artifactId>
|
<artifactId>jakarta.servlet-api</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.auth0</groupId>
|
||||||
|
<artifactId>java-jwt</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ import org.springframework.security.crypto.password.PasswordEncoder;
|
|||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
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.DispatcherType;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
@@ -26,7 +29,7 @@ public class SecurityConfiguration {
|
|||||||
.csrf(AbstractHttpConfigurer::disable)
|
.csrf(AbstractHttpConfigurer::disable)
|
||||||
.httpBasic(Customizer.withDefaults())
|
.httpBasic(Customizer.withDefaults())
|
||||||
.authorizeHttpRequests(requests -> requests
|
.authorizeHttpRequests(requests -> requests
|
||||||
.dispatcherTypeMatchers(DispatcherType.FORWARD).permitAll()
|
.dispatcherTypeMatchers(FORWARD).permitAll()
|
||||||
.requestMatchers(
|
.requestMatchers(
|
||||||
HttpMethod.GET,
|
HttpMethod.GET,
|
||||||
"/api/health/check"
|
"/api/health/check"
|
||||||
@@ -39,10 +42,10 @@ public class SecurityConfiguration {
|
|||||||
)
|
)
|
||||||
.exceptionHandling(configurer -> configurer
|
.exceptionHandling(configurer -> configurer
|
||||||
.defaultAuthenticationEntryPointFor(
|
.defaultAuthenticationEntryPointFor(
|
||||||
(request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED),
|
(request, response, authException) -> response.sendError(SC_UNAUTHORIZED),
|
||||||
new AntPathRequestMatcher("/api/**")
|
new AntPathRequestMatcher("/api/**")
|
||||||
).defaultAccessDeniedHandlerFor(
|
).defaultAccessDeniedHandlerFor(
|
||||||
(request, response, accessDeniedException) -> response.sendError(HttpServletResponse.SC_FORBIDDEN),
|
(request, response, accessDeniedException) -> response.sendError(SC_FORBIDDEN),
|
||||||
new AntPathRequestMatcher("/api/**")
|
new AntPathRequestMatcher("/api/**")
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package org.sportshub.application.user;
|
package org.sportshub.application.security;
|
||||||
|
|
||||||
import java.util.UUID;
|
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.UserDetails;
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.sportshub.application.user;
|
package org.sportshub.application.security.model;
|
||||||
|
|
||||||
import static java.util.Collections.emptyList;
|
import static java.util.Collections.emptyList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
@@ -4,15 +4,26 @@ import java.util.List;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
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.model.User;
|
||||||
import org.sportshub.domain.user.port.UserPort;
|
import org.sportshub.domain.user.port.UserPort;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class UserUseCases {
|
public class UserUseCases {
|
||||||
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
private final JwtService jwtService;
|
||||||
private final UserPort userPort;
|
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;
|
this.userPort = userPort;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,4 +34,11 @@ public class UserUseCases {
|
|||||||
public List<User> findAll() {
|
public List<User> findAll() {
|
||||||
return userPort.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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package org.sportshub.domain.exception;
|
||||||
|
|
||||||
|
public abstract class FunctionnalException extends RuntimeException {
|
||||||
|
public FunctionnalException(final String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package org.sportshub.domain.exception;
|
||||||
|
|
||||||
|
public class LoginFailureException extends FunctionnalException {
|
||||||
|
public LoginFailureException() {
|
||||||
|
super("Login or password incorrect.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
package org.sportshub.exposition.user;
|
package org.sportshub.exposition.user;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
import org.sportshub.application.user.UserUseCases;
|
import org.sportshub.application.user.UserUseCases;
|
||||||
import org.sportshub.domain.user.model.User;
|
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.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
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.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
@@ -19,8 +23,8 @@ public class UserController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/login")
|
@PostMapping("/login")
|
||||||
public String login() {
|
public String login(@RequestBody LoginRequest request) {
|
||||||
return "";
|
return userUseCases.authenticate(request.id(), request.password());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package org.sportshub.exposition.user.model;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public record LoginRequest(
|
||||||
|
UUID id,
|
||||||
|
String password
|
||||||
|
) {}
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
application:
|
||||||
|
security:
|
||||||
|
secretKey: "secret-key"
|
||||||
|
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
org.springframework.security: DEBUG
|
org.springframework.security: DEBUG
|
||||||
Reference in New Issue
Block a user