Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,22 @@
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.13.0</version>
</dependency><dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.13.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.13.0</version>
<scope>runtime</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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> T extractClaim(String token, Function<Claims, T> 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());
}

}
Original file line number Diff line number Diff line change
@@ -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) {

Expand All @@ -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);

}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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);


}
}
Original file line number Diff line number Diff line change
@@ -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;
}

}
1 change: 0 additions & 1 deletion src/main/resources/application.properties

This file was deleted.

8 changes: 8 additions & 0 deletions src/main/resources/application.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
spring:
application:
name: SouPulsar Modulith

security:
jwt:
secret: ${JWT_SECRET:jwt-super-secret-key-place-holder}
expiration: ${JWT_EXPIRATION:3600000}
Loading