Convert login by id into login by email.
This commit is contained in:
@@ -1,14 +1,24 @@
|
||||
package org.codiki.application.security;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.codiki.domain.user.model.builder.UserBuilder.anUser;
|
||||
import org.codiki.domain.user.model.User;
|
||||
import org.codiki.domain.user.model.UserRole;
|
||||
import org.codiki.domain.user.model.builder.UserBuilder;
|
||||
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;
|
||||
import com.auth0.jwt.interfaces.Claim;
|
||||
|
||||
@Service
|
||||
public class JwtService {
|
||||
@@ -31,6 +41,7 @@ public class JwtService {
|
||||
return JWT.create()
|
||||
.withSubject(user.id().toString())
|
||||
.withExpiresAt(expirationDate.toInstant())
|
||||
.withPayload(user.toJwtPayload())
|
||||
.sign(algorithm);
|
||||
}
|
||||
|
||||
@@ -45,7 +56,54 @@ public class JwtService {
|
||||
return result;
|
||||
}
|
||||
|
||||
public String extractUsername(String token) {
|
||||
return JWT.decode(token).getSubject();
|
||||
public Optional<User> extractUser(String token) {
|
||||
Map<String, Claim> claims = JWT.decode(token).getClaims();
|
||||
|
||||
UserBuilder userBuilder = anUser()
|
||||
.withPassword("****");
|
||||
|
||||
Optional.ofNullable(claims.get("sub"))
|
||||
.map(Claim::asString)
|
||||
.map(this::mapUuid)
|
||||
.ifPresent(userBuilder::withId);
|
||||
|
||||
Optional.ofNullable(claims.get("pseudo"))
|
||||
.map(Claim::asString)
|
||||
.ifPresent(userBuilder::withPseudo);
|
||||
|
||||
Optional.ofNullable(claims.get("email"))
|
||||
.map(Claim::asString)
|
||||
.ifPresent(userBuilder::withEmail);
|
||||
|
||||
Optional.ofNullable(claims.get("photoId"))
|
||||
.map(Claim::asString)
|
||||
.map(this::mapUuid)
|
||||
.ifPresent(userBuilder::withPhotoId);
|
||||
|
||||
extractRoles(claims)
|
||||
.stream()
|
||||
.flatMap(Collection::stream)
|
||||
.map(UserRole::from)
|
||||
.flatMap(Optional::stream)
|
||||
.forEach(userBuilder::withRole);
|
||||
|
||||
return Optional.of(userBuilder.build());
|
||||
}
|
||||
|
||||
private static Optional<List<String>> extractRoles(Map<String, Claim> claims) {
|
||||
return Optional.ofNullable(claims.get("roles"))
|
||||
.map(Claim::asString)
|
||||
.map(roles -> roles.split(","))
|
||||
.map(Arrays::asList);
|
||||
}
|
||||
|
||||
private UUID mapUuid(String uuidAsString) {
|
||||
UUID result;
|
||||
try {
|
||||
result = UUID.fromString(uuidAsString);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
result = null;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,8 +60,8 @@ public class UserUseCases {
|
||||
return userPort.findAll();
|
||||
}
|
||||
|
||||
public UserAuthenticationData authenticate(UUID userId, String password) {
|
||||
User user = userPort.findById(userId)
|
||||
public UserAuthenticationData authenticate(String userEmail, String password) {
|
||||
User user = userPort.findByEmail(userEmail)
|
||||
.orElseThrow(LoginFailureException::new);
|
||||
|
||||
if (!passwordEncoder.matches(password, user.password())) {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package org.codiki.domain.user.model;
|
||||
|
||||
import static java.util.Objects.isNull;
|
||||
import static java.util.stream.Collectors.joining;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
public record User(
|
||||
@@ -10,4 +14,21 @@ public record User(
|
||||
String password,
|
||||
UUID photoId,
|
||||
List<UserRole> roles
|
||||
) {}
|
||||
) {
|
||||
public Map<String, Object> toJwtPayload() {
|
||||
Map<String, Object> result = new HashMap<>(4);
|
||||
|
||||
result.put("pseudo", pseudo);
|
||||
result.put("email", email);
|
||||
if (!isNull(photoId)) {
|
||||
result.put("photoId", photoId.toString());
|
||||
}
|
||||
|
||||
String rolesAsString = roles.stream()
|
||||
.map(UserRole::name)
|
||||
.collect(joining(","));
|
||||
result.put("roles", rolesAsString);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
package org.codiki.domain.user.model;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public enum UserRole {
|
||||
STANDARD,
|
||||
ADMIN
|
||||
ADMIN;
|
||||
|
||||
public static Optional<UserRole> from(String roleAsString) {
|
||||
return Stream.of(UserRole.values())
|
||||
.filter(role -> role.name().equals(roleAsString))
|
||||
.findFirst();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package org.codiki.domain.user.model.builder;
|
||||
|
||||
import static java.util.Objects.isNull;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.codiki.domain.user.exception.UserCreationException;
|
||||
import org.codiki.domain.user.model.User;
|
||||
import org.codiki.domain.user.model.UserRole;
|
||||
|
||||
@@ -59,6 +61,14 @@ public class UserBuilder {
|
||||
}
|
||||
|
||||
public User build() {
|
||||
return new User(id, pseudo,email, password, photoId, new LinkedList<>(roles));
|
||||
if (isNull(id) || isNull(pseudo) || isNull(email) || isNull(password) || isEmpty(roles)) {
|
||||
throw new UserCreationException();
|
||||
}
|
||||
|
||||
return new User(id, pseudo, email, password, photoId, new LinkedList<>(roles));
|
||||
}
|
||||
|
||||
private static boolean isEmpty(Set<UserRole> roles) {
|
||||
return isNull(roles) || roles.isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import org.codiki.domain.user.model.User;
|
||||
public interface UserPort {
|
||||
Optional<User> findById(UUID userId);
|
||||
|
||||
Optional<User> findByEmail(String userEmail);
|
||||
|
||||
List<User> findAll();
|
||||
|
||||
void save(User user);
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
package org.codiki.exposition.configuration.security;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.springframework.http.HttpHeaders.AUTHORIZATION;
|
||||
import static org.springframework.util.ObjectUtils.isEmpty;
|
||||
import org.codiki.application.security.JwtService;
|
||||
import org.codiki.application.security.model.CustomUserDetails;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
@@ -23,11 +23,9 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
private static final String BEARER_PREFIX = "Bearer ";
|
||||
|
||||
private final JwtService jwtService;
|
||||
private final UserDetailsService userDetailsService;
|
||||
|
||||
public JwtAuthenticationFilter(JwtService jwtService, UserDetailsService userDetailsService) {
|
||||
public JwtAuthenticationFilter(JwtService jwtService) {
|
||||
this.jwtService = jwtService;
|
||||
this.userDetailsService = userDetailsService;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -36,25 +34,26 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
HttpServletResponse response,
|
||||
FilterChain filterChain
|
||||
) throws ServletException, IOException {
|
||||
String authorizationHeader = request.getHeader(AUTHORIZATION);
|
||||
|
||||
if (!isEmpty(authorizationHeader) && authorizationHeader.startsWith(BEARER_PREFIX)) {
|
||||
String token = authorizationHeader.substring(BEARER_PREFIX.length());
|
||||
String username = jwtService.extractUsername(token);
|
||||
|
||||
if (!isEmpty(username) && SecurityContextHolder.getContext().getAuthentication() == null) {
|
||||
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
|
||||
if (jwtService.isValid(token) && userDetails.getUsername().equals(username)) {
|
||||
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
|
||||
userDetails,
|
||||
null,
|
||||
userDetails.getAuthorities()
|
||||
);
|
||||
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
Optional.ofNullable(request.getHeader(AUTHORIZATION))
|
||||
.filter(authorizationHeader -> !isEmpty(authorizationHeader))
|
||||
.filter(authorizationHeader -> authorizationHeader.startsWith(BEARER_PREFIX))
|
||||
.map(authorizationHeader -> authorizationHeader.substring(BEARER_PREFIX.length()))
|
||||
.filter(token -> {
|
||||
String authorizationHeader = request.getHeader(AUTHORIZATION);
|
||||
return !isEmpty(authorizationHeader) && authorizationHeader.startsWith(BEARER_PREFIX);
|
||||
})
|
||||
.filter(jwtService::isValid)
|
||||
.flatMap(jwtService::extractUser)
|
||||
.map(CustomUserDetails::new)
|
||||
.map(userDetails -> new UsernamePasswordAuthenticationToken(
|
||||
userDetails,
|
||||
null,
|
||||
userDetails.getAuthorities()
|
||||
))
|
||||
.ifPresent(authenticationToken -> {
|
||||
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
|
||||
});
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import org.codiki.exposition.user.model.LoginRequest;
|
||||
import org.codiki.exposition.user.model.LoginResponse;
|
||||
import org.codiki.exposition.user.model.RefreshTokenRequest;
|
||||
import org.codiki.exposition.user.model.SignInRequestDto;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
@@ -32,7 +31,7 @@ public class UserController {
|
||||
@PostMapping("/login")
|
||||
@AllowedToAnonymous
|
||||
public LoginResponse login(@RequestBody LoginRequest request) {
|
||||
UserAuthenticationData userAuthenticationData = userUseCases.authenticate(request.id(), request.password());
|
||||
UserAuthenticationData userAuthenticationData = userUseCases.authenticate(request.email(), request.password());
|
||||
return new LoginResponse(userAuthenticationData);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package org.codiki.exposition.user.model;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public record LoginRequest(
|
||||
UUID id,
|
||||
String email,
|
||||
String password
|
||||
) {}
|
||||
|
||||
@@ -32,6 +32,12 @@ public class UserJpaAdapter implements UserPort {
|
||||
.map(UserEntity::toUser);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<User> findByEmail(String userEmail) {
|
||||
return userJpaRepository.findByEmail(userEmail)
|
||||
.map(UserEntity::toUser);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<User> findAll() {
|
||||
return userJpaRepository.findAll()
|
||||
|
||||
@@ -15,6 +15,9 @@ public interface UserJpaRepository extends JpaRepository<UserEntity, UUID> {
|
||||
@Query("SELECT u FROM UserEntity u JOIN FETCH u.roles WHERE u.id = :userId")
|
||||
Optional<UserEntity> findById(@Param("userId") UUID userId);
|
||||
|
||||
@Query("SELECT u FROM UserEntity u JOIN FETCH u.roles WHERE u.email = :email")
|
||||
Optional<UserEntity> findByEmail(@Param("email") String userEmail);
|
||||
|
||||
@Query("SELECT u FROM UserEntity u JOIN FETCH u.roles")
|
||||
List<UserEntity> findAll();
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ post {
|
||||
|
||||
body:json {
|
||||
{
|
||||
"id": "5ad462b8-8f9e-4a26-bb86-c74fef5d11b6",
|
||||
"email": "standard.user@codiki.org",
|
||||
"password": "password"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
vars {
|
||||
url: http://localhost:8080
|
||||
publicationId: e23831a6-9cc0-4f3d-9efa-7a1cae191cb1
|
||||
bearerToken: eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1YWQ0NjJiOC04ZjllLTRhMjYtYmI4Ni1jNzRmZWY1ZDExYjYiLCJleHAiOjE3MTA1MzkzMjd9.qskbb1_AKlY74GmcDHVzFV7wGP7nDPk1St8OlUmTy08ut4SWRvi0WnrD90Y1cBAnsiu2UTjV5v6LXkX5W_pLfg
|
||||
publicationId: ec76602f-5501-4091-868e-b471611e63de
|
||||
bearerToken: eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1YWQ0NjJiOC04ZjllLTRhMjYtYmI4Ni1jNzRmZWY1ZDExYjYiLCJleHAiOjE3MTA4Mzc2ODQsInBzZXVkbyI6IlN0YW5kYXJkIHVzZXIiLCJlbWFpbCI6InN0YW5kYXJkLnVzZXJAY29kaWtpLm9yZyIsInJvbGVzIjoiU1RBTkRBUkQifQ.2HggC3T_4I14IpW02DZJiYfgYwc074kU8Y4AmuGf1mZzv0U8OUxpAw_xEhnKtn8NcaCozz_2vFv4o_CaBqS8Ag
|
||||
categoryId: 172fa901-3f4b-4540-92f3-1c15820e8ec9
|
||||
pictureId: 65b660b7-66bb-4e4a-a62c-fd0ca101f972
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user