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