diff --git a/pom.xml b/pom.xml
index ec5a460..e3b8e68 100644
--- a/pom.xml
+++ b/pom.xml
@@ -107,6 +107,22 @@
spring-security-test
test
+
+ io.jsonwebtoken
+ jjwt-api
+ 0.13.0
+
+ io.jsonwebtoken
+ jjwt-impl
+ 0.13.0
+ runtime
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ 0.13.0
+ runtime
+
diff --git a/src/main/java/com/soupulsar/modulith/auth/application/config/AuthUseCaseConfig.java b/src/main/java/com/soupulsar/modulith/auth/application/config/AuthUseCaseConfig.java
index 743a1ad..6eac44b 100644
--- a/src/main/java/com/soupulsar/modulith/auth/application/config/AuthUseCaseConfig.java
+++ b/src/main/java/com/soupulsar/modulith/auth/application/config/AuthUseCaseConfig.java
@@ -1,5 +1,6 @@
package com.soupulsar.modulith.auth.application.config;
+import com.soupulsar.modulith.auth.application.security.JwtService;
import com.soupulsar.modulith.auth.application.security.PasswordHasher;
import com.soupulsar.modulith.auth.application.usecase.AuthenticateUserUseCase;
import com.soupulsar.modulith.auth.application.usecase.RegisterUserUseCase;
@@ -11,8 +12,8 @@
public class AuthUseCaseConfig {
@Bean
- public AuthenticateUserUseCase authenticateUserUseCase(UserRepository userRepository, PasswordHasher passwordHasher) {
- return new AuthenticateUserUseCase(userRepository, passwordHasher);
+ public AuthenticateUserUseCase authenticateUserUseCase(UserRepository userRepository, PasswordHasher passwordHasher, JwtService jwtService) {
+ return new AuthenticateUserUseCase(userRepository, passwordHasher, jwtService);
}
@Bean
diff --git a/src/main/java/com/soupulsar/modulith/auth/application/security/JwtService.java b/src/main/java/com/soupulsar/modulith/auth/application/security/JwtService.java
new file mode 100644
index 0000000..478f5b7
--- /dev/null
+++ b/src/main/java/com/soupulsar/modulith/auth/application/security/JwtService.java
@@ -0,0 +1,80 @@
+package com.soupulsar.modulith.auth.application.security;
+
+import com.soupulsar.modulith.auth.domain.model.User;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.security.Keys;
+import jakarta.annotation.PostConstruct;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import javax.crypto.SecretKey;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.function.Function;
+
+@Service
+public class JwtService {
+
+ private final String secret;
+ private SecretKey secretKey;
+ private final Long expirationMs;
+
+ public JwtService(@Value("${spring.security.jwt.secret}") String secret,
+ @Value("${spring.security.jwt.expiration}") Long expirationMs) {
+ this.secret = secret;
+ this.expirationMs = expirationMs;
+ }
+
+ @PostConstruct
+ public void init() {
+ if (secret == null || secret.isBlank()) {
+ throw new IllegalArgumentException("JWT Secret não configurada!");
+ }
+ this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
+ }
+
+
+ public String generateToken(User user) {
+
+ Date issueDate = new Date();
+ Date expiryDate = new Date(issueDate.getTime() + expirationMs);
+
+ return Jwts.builder()
+ .subject(user.getEmail())
+ .claim("userId", user.getUserId().toString())
+ .claim("role", user.getRole())
+ .claim("status", user.getStatus())
+ .issuer("SouPulsar-AuthService")
+ .audience().add("SouPulsar-API").and()
+ .issuedAt(issueDate)
+ .expiration(expiryDate)
+ .signWith(secretKey)
+ .compact();
+ }
+
+
+ public boolean isTokenValid(String token, String username) {
+ final String extractedUsername = extractClaim(token, Claims::getSubject);
+ return (extractedUsername.equals(username) && !isTokenExpired(token));
+ }
+
+
+ public T extractClaim(String token, Function claimsResolver) {
+ final Claims claims = extractAllClaims(token);
+ return claimsResolver.apply(claims);
+ }
+
+ public Claims extractAllClaims(String token) {
+ return Jwts.parser()
+ .verifyWith(secretKey)
+ .build()
+ .parseSignedClaims(token)
+ .getPayload();
+ }
+
+ private boolean isTokenExpired(String token) {
+ return extractClaim(token, Claims::getExpiration).before(new Date());
+ }
+
+}
diff --git a/src/main/java/com/soupulsar/modulith/auth/application/usecase/AuthenticateUserUseCase.java b/src/main/java/com/soupulsar/modulith/auth/application/usecase/AuthenticateUserUseCase.java
index 5fb4101..927b489 100644
--- a/src/main/java/com/soupulsar/modulith/auth/application/usecase/AuthenticateUserUseCase.java
+++ b/src/main/java/com/soupulsar/modulith/auth/application/usecase/AuthenticateUserUseCase.java
@@ -1,18 +1,19 @@
package com.soupulsar.modulith.auth.application.usecase;
import com.soupulsar.modulith.auth.application.dto.AuthUserRequest;
+import com.soupulsar.modulith.auth.application.security.JwtService;
import com.soupulsar.modulith.auth.application.security.PasswordHasher;
import com.soupulsar.modulith.auth.domain.model.User;
import com.soupulsar.modulith.auth.domain.model.enums.UserStatus;
import com.soupulsar.modulith.auth.domain.repository.UserRepository;
import lombok.RequiredArgsConstructor;
-import org.springframework.security.crypto.password.PasswordEncoder;
@RequiredArgsConstructor
public class AuthenticateUserUseCase {
private final UserRepository userRepository;
private final PasswordHasher passwordHasher;
+ private final JwtService jwtService;
public String execute(AuthUserRequest request) {
@@ -27,8 +28,7 @@ public String execute(AuthUserRequest request) {
throw new IllegalArgumentException("Invalid email or password");
}
- return "JWT-TOKEN"; // Placeholder for actual JWT generation logic
+ return jwtService.generateToken(user);
}
-
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/soupulsar/modulith/auth/infrastructure/persistence/entity/UserEntity.java b/src/main/java/com/soupulsar/modulith/auth/infrastructure/persistence/entity/UserEntity.java
index 4531628..53414bc 100644
--- a/src/main/java/com/soupulsar/modulith/auth/infrastructure/persistence/entity/UserEntity.java
+++ b/src/main/java/com/soupulsar/modulith/auth/infrastructure/persistence/entity/UserEntity.java
@@ -24,7 +24,7 @@ public class UserEntity {
@Column(nullable = false)
private String name;
- @Column(nullable = false)
+ @Column(nullable = false, unique = true)
private String cpf;
@Column(nullable = false)
@@ -37,8 +37,10 @@ public class UserEntity {
private String telephone;
@Column(nullable = false)
+ @Enumerated(EnumType.STRING)
private UserRole role;
@Column(nullable = false)
+ @Enumerated(EnumType.STRING)
private UserStatus status;
}
\ No newline at end of file
diff --git a/src/main/java/com/soupulsar/modulith/auth/infrastructure/persistence/mapper/UserMapper.java b/src/main/java/com/soupulsar/modulith/auth/infrastructure/persistence/mapper/UserMapper.java
index 277311c..6f41663 100644
--- a/src/main/java/com/soupulsar/modulith/auth/infrastructure/persistence/mapper/UserMapper.java
+++ b/src/main/java/com/soupulsar/modulith/auth/infrastructure/persistence/mapper/UserMapper.java
@@ -10,22 +10,27 @@ public class UserMapper {
public static UserEntity toEntity(User user){
UserEntity entity = new UserEntity();
+ entity.setName(user.getName());
+ entity.setCpf(user.getCpf());
+ entity.setTelephone(user.getTelephone());
entity.setUserId(user.getUserId());
entity.setEmail(user.getEmail());
entity.setPassword(user.getPasswordHash());
+ entity.setStatus(user.getStatus());
entity.setRole(user.getRole());
return entity;
}
public static User toModel(UserEntity entity){
- return User.restore(entity.getUserId(),
+ return User.restore(
+ entity.getUserId(),
entity.getName(),
+ entity.getCpf(),
+ entity.getTelephone(),
entity.getEmail(),
entity.getPassword(),
- entity.getTelephone(),
- entity.getCpf(),
entity.getRole(),
- entity.getStatus());
+ entity.getStatus()
+ );
}
-
}
\ No newline at end of file
diff --git a/src/main/java/com/soupulsar/modulith/auth/infrastructure/security/BCryptPasswordHasher.java b/src/main/java/com/soupulsar/modulith/auth/infrastructure/security/BCryptPasswordHasher.java
index 4e35da0..3a366a0 100644
--- a/src/main/java/com/soupulsar/modulith/auth/infrastructure/security/BCryptPasswordHasher.java
+++ b/src/main/java/com/soupulsar/modulith/auth/infrastructure/security/BCryptPasswordHasher.java
@@ -16,6 +16,6 @@ public String hash(String password) {
@Override
public boolean matches(String rawPassword, String hashedPassword) {
- return passwordEncoder.matches(hashedPassword, rawPassword);
+ return passwordEncoder.matches(rawPassword, hashedPassword);
}
}
\ No newline at end of file
diff --git a/src/main/java/com/soupulsar/modulith/auth/infrastructure/security/CustomUserDetailsService.java b/src/main/java/com/soupulsar/modulith/auth/infrastructure/security/CustomUserDetailsService.java
new file mode 100644
index 0000000..237b721
--- /dev/null
+++ b/src/main/java/com/soupulsar/modulith/auth/infrastructure/security/CustomUserDetailsService.java
@@ -0,0 +1,30 @@
+package com.soupulsar.modulith.auth.infrastructure.security;
+
+import com.soupulsar.modulith.auth.domain.repository.UserRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class CustomUserDetailsService implements UserDetailsService {
+
+ private final UserRepository userRepository;
+
+
+ @Override
+ public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
+
+ var user = userRepository.findByEmail(email)
+ .orElseThrow(() -> new UsernameNotFoundException("User not found with email: " + email));
+
+ return User.builder()
+ .username(user.getEmail())
+ .password(user.getPasswordHash())
+ .roles(user.getRole().name())
+ .build();
+ }
+}
diff --git a/src/main/java/com/soupulsar/modulith/auth/infrastructure/security/JwtAuthenticationFilter.java b/src/main/java/com/soupulsar/modulith/auth/infrastructure/security/JwtAuthenticationFilter.java
new file mode 100644
index 0000000..db61031
--- /dev/null
+++ b/src/main/java/com/soupulsar/modulith/auth/infrastructure/security/JwtAuthenticationFilter.java
@@ -0,0 +1,54 @@
+package com.soupulsar.modulith.auth.infrastructure.security;
+
+import com.soupulsar.modulith.auth.application.security.JwtService;
+import io.jsonwebtoken.Claims;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+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;
+
+import java.io.IOException;
+
+@Component
+@RequiredArgsConstructor
+public class JwtAuthenticationFilter extends OncePerRequestFilter {
+
+
+ private final JwtService jwtService;
+ private final UserDetailsService userDetailsService;
+
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
+
+ String authHeader = request.getHeader("Authorization");
+ if (authHeader == null || !authHeader.startsWith("Bearer ")) {
+ filterChain.doFilter(request, response);
+ return;
+ }
+
+ String jwt = authHeader.substring(7);
+ String username = jwtService.extractClaim(jwt, Claims::getSubject);
+
+ if(username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
+ var userDetails = userDetailsService.loadUserByUsername(username);
+
+ if(jwtService.isTokenValid(jwt, userDetails.getUsername())) {
+ UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
+
+ authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
+ SecurityContextHolder.getContext().setAuthentication(authToken);
+ }
+ }
+ filterChain.doFilter(request, response);
+
+
+ }
+}
diff --git a/src/main/java/com/soupulsar/modulith/auth/infrastructure/security/SecurityConfig.java b/src/main/java/com/soupulsar/modulith/auth/infrastructure/security/SecurityConfig.java
new file mode 100644
index 0000000..f5ec906
--- /dev/null
+++ b/src/main/java/com/soupulsar/modulith/auth/infrastructure/security/SecurityConfig.java
@@ -0,0 +1,55 @@
+package com.soupulsar.modulith.auth.infrastructure.security;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpMethod;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
+import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+
+@Configuration
+@RequiredArgsConstructor
+public class SecurityConfig {
+
+ private final JwtAuthenticationFilter jwtFilter;
+
+ @Bean
+ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+ return http
+ .csrf(AbstractHttpConfigurer::disable)
+ .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+ .authorizeHttpRequests(auth -> auth
+ .requestMatchers(HttpMethod.POST, "/api/auth/register", "/api/auth/login").permitAll()
+ .anyRequest().authenticated()
+ )
+ .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
+ .build();
+ }
+
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+
+ @Bean
+ public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
+ return config.getAuthenticationManager();
+ }
+
+ @Bean
+ public AuthenticationProvider authenticationProvider(CustomUserDetailsService userDetailsService) {
+ DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(userDetailsService);
+ daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
+ return daoAuthenticationProvider;
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
deleted file mode 100644
index 257be11..0000000
--- a/src/main/resources/application.properties
+++ /dev/null
@@ -1 +0,0 @@
-spring.application.name=SouPulsar Modulith
diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml
new file mode 100644
index 0000000..e68634b
--- /dev/null
+++ b/src/main/resources/application.yaml
@@ -0,0 +1,8 @@
+spring:
+ application:
+ name: SouPulsar Modulith
+
+ security:
+ jwt:
+ secret: ${JWT_SECRET:jwt-super-secret-key-place-holder}
+ expiration: ${JWT_EXPIRATION:3600000}
\ No newline at end of file
diff --git a/src/test/java/com/soupulsar/modulith/auth/application/usecase/AuthenticateUserUseCaseTest.java b/src/test/java/com/soupulsar/modulith/auth/application/usecase/AuthenticateUserUseCaseTest.java
index 830b736..fdb016b 100644
--- a/src/test/java/com/soupulsar/modulith/auth/application/usecase/AuthenticateUserUseCaseTest.java
+++ b/src/test/java/com/soupulsar/modulith/auth/application/usecase/AuthenticateUserUseCaseTest.java
@@ -1,6 +1,7 @@
package com.soupulsar.modulith.auth.application.usecase;
import com.soupulsar.modulith.auth.application.dto.AuthUserRequest;
+import com.soupulsar.modulith.auth.application.security.JwtService;
import com.soupulsar.modulith.auth.application.security.PasswordHasher;
import com.soupulsar.modulith.auth.domain.model.User;
import com.soupulsar.modulith.auth.domain.model.enums.UserRole;
@@ -20,12 +21,14 @@ class AuthenticateUserUseCaseTest {
private UserRepository userRepository;
private PasswordHasher passwordHasher;
private AuthenticateUserUseCase useCase;
+ private JwtService jwtService;
@BeforeEach
void setUp() {
userRepository = mock(UserRepository.class);
passwordHasher = mock(PasswordHasher.class);
- useCase = new AuthenticateUserUseCase(userRepository, passwordHasher);
+ jwtService = mock(JwtService.class);
+ useCase = new AuthenticateUserUseCase(userRepository, passwordHasher, jwtService);
}
@Test
@@ -38,10 +41,12 @@ void shouldAuthenticateActiveUserWithCorrectPassword() {
when(userRepository.findByEmail(request.email())).thenReturn(Optional.of(user));
when(passwordHasher.matches(request.password(), user.getPasswordHash())).thenReturn(true);
+ when(jwtService.generateToken(user)).thenReturn("JWT-TOKEN");
String token = useCase.execute(request);
assertEquals("JWT-TOKEN", token);
+ verify(jwtService).generateToken(user);
}
@Test
@@ -56,6 +61,7 @@ void shouldThrowWhenUserIsInactive() {
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> useCase.execute(request));
assertEquals("User is not active", ex.getMessage());
+ verify(jwtService, never()).generateToken(user);
}
@Test
@@ -71,6 +77,7 @@ void shouldThrowWhenPasswordIsInvalid() {
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> useCase.execute(request));
assertEquals("Invalid email or password", ex.getMessage());
+ verify(jwtService, never()).generateToken(user);
}
@Test
@@ -81,5 +88,6 @@ void shouldThrowWhenUserNotFound() {
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> useCase.execute(request));
assertEquals("Invalid email or password", ex.getMessage());
+ verify(jwtService, never()).generateToken(any());
}
-}
+}
\ No newline at end of file