Implementation of login endpoint.

This commit is contained in:
Florian THIERRY
2023-11-30 10:47:59 +01:00
parent 914785a29b
commit 36a7aacec7
13 changed files with 134 additions and 8 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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/**")
) )
); );

View File

@@ -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;

View File

@@ -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();
}
}

View File

@@ -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;

View File

@@ -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);
}
} }

View File

@@ -0,0 +1,7 @@
package org.sportshub.domain.exception;
public abstract class FunctionnalException extends RuntimeException {
public FunctionnalException(final String message) {
super(message);
}
}

View File

@@ -0,0 +1,7 @@
package org.sportshub.domain.exception;
public class LoginFailureException extends FunctionnalException {
public LoginFailureException() {
super("Login or password incorrect.");
}
}

View File

@@ -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.
}
}

View File

@@ -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

View File

@@ -0,0 +1,8 @@
package org.sportshub.exposition.user.model;
import java.util.UUID;
public record LoginRequest(
UUID id,
String password
) {}

View File

@@ -1,3 +1,7 @@
application:
security:
secretKey: "secret-key"
logging: logging:
level: level:
org.springframework.security: DEBUG org.springframework.security: DEBUG