diff --git a/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/configs/AuthenticationConfig.java b/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/configs/AuthenticationConfig.java new file mode 100644 index 0000000..6470780 --- /dev/null +++ b/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/configs/AuthenticationConfig.java @@ -0,0 +1,17 @@ +package br.com.rayankonecny.authserviceapi.configs; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; + +@Configuration +public class AuthenticationConfig { + + @Bean + public AuthenticationManager authenticationManager( + AuthenticationConfiguration config + ) throws Exception { + return config.getAuthenticationManager(); + } +} diff --git a/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/controllers/AuthController.java b/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/controllers/AuthController.java index dd9d4a5..326a88d 100644 --- a/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/controllers/AuthController.java +++ b/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/controllers/AuthController.java @@ -30,6 +30,6 @@ public interface AuthController { @ApiResponse(responseCode = "500", description = "Internal server error", content = @Content(mediaType = APPLICATION_JSON_VALUE, schema = @Schema(implementation = StandardError.class))), }) @PostMapping("/login") - ResponseEntity authenticate(@Valid @RequestBody final AuthenticateRequest requests); + ResponseEntity authenticate(@Valid @RequestBody final AuthenticateRequest requests) throws Exception; } diff --git a/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/controllers/exceptions/ControllerExceptionHandler.java b/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/controllers/exceptions/ControllerExceptionHandler.java new file mode 100644 index 0000000..c9ec3f8 --- /dev/null +++ b/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/controllers/exceptions/ControllerExceptionHandler.java @@ -0,0 +1,48 @@ +package br.com.rayankonecny.authserviceapi.controllers.exceptions; + +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +import jakarta.servlet.http.HttpServletRequest; + +import br.com.rayankonecny.hdcommoslib.models.exceptions.ValidationException; +import br.com.rayankonecny.hdcommoslib.models.exceptions.ResourceNotFoundException; +import br.com.rayankonecny.hdcommoslib.models.exceptions.StandardError; + +import static java.time.LocalDateTime.now; +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.NOT_FOUND; + +import java.util.ArrayList; + +@ControllerAdvice +public class ControllerExceptionHandler { + + @ExceptionHandler(UsernameNotFoundException.class) + ResponseEntity handleNotFoundException(final ResourceNotFoundException ex, + final HttpServletRequest request) { + return ResponseEntity.status(NOT_FOUND).body( + + StandardError.builder().timestamp(now()).status(NOT_FOUND.value()).error(NOT_FOUND.getReasonPhrase()) + .message(ex.getMessage()).path(request.getRequestURI()).build()); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + ResponseEntity handleMethodArgumentNotValidException(final MethodArgumentNotValidException ex, + final HttpServletRequest request) { + + var error = ValidationException.builder().timestamp(now()).status(BAD_REQUEST.value()).error("Validation Exception") + .message("Exception in validation attributes").path(request.getRequestURI()).errors(new ArrayList<>()).build(); + + for (FieldError fieldError : ex.getBindingResult().getFieldErrors()) { + error.addError(fieldError.getField(), fieldError.getDefaultMessage()); + } + + return ResponseEntity.badRequest().body(error); + } +} diff --git a/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/controllers/impl/AuthControllerImpl.java b/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/controllers/impl/AuthControllerImpl.java new file mode 100644 index 0000000..0b78ab3 --- /dev/null +++ b/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/controllers/impl/AuthControllerImpl.java @@ -0,0 +1,27 @@ +package br.com.rayankonecny.authserviceapi.controllers.impl; + +import org.springframework.http.ResponseEntity; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.web.bind.annotation.RestController; + +import br.com.rayankonecny.authserviceapi.controllers.AuthController; +import br.com.rayankonecny.authserviceapi.security.JWTAuthenticationImpl; +import br.com.rayankonecny.authserviceapi.utils.JWTUtils; +import br.com.rayankonecny.hdcommoslib.models.requests.AuthenticateRequest; +import br.com.rayankonecny.hdcommoslib.models.responses.AuthenticationResponse; +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +public class AuthControllerImpl implements AuthController { + + private final JWTUtils jwtUtils; + private final AuthenticationConfiguration authenticationConfiguration; + + @Override + public ResponseEntity authenticate(final AuthenticateRequest requests) throws Exception { + return ResponseEntity.ok().body( + new JWTAuthenticationImpl(jwtUtils, authenticationConfiguration.getAuthenticationManager()).authentication(requests)); + } + +} diff --git a/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/models/User.java b/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/models/User.java new file mode 100644 index 0000000..ea0939a --- /dev/null +++ b/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/models/User.java @@ -0,0 +1,20 @@ +package br.com.rayankonecny.authserviceapi.models; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Set; + +import br.com.rayankonecny.hdcommoslib.models.enums.ProfileEnum; + +@Getter +@AllArgsConstructor +public class User { + + private String id; + private String name; + private String email; + private String password; + private Set profiles; + +} diff --git a/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/repository/UserRepository.java b/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/repository/UserRepository.java new file mode 100644 index 0000000..d45ccd7 --- /dev/null +++ b/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/repository/UserRepository.java @@ -0,0 +1,13 @@ +package br.com.rayankonecny.authserviceapi.repository; + +import java.util.Optional; + +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; + +import br.com.rayankonecny.authserviceapi.models.User; + +@Repository +public interface UserRepository extends MongoRepository { + Optional findByEmail(final String email); +} diff --git a/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/security/JWTAuthenticationImpl.java b/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/security/JWTAuthenticationImpl.java new file mode 100644 index 0000000..0221872 --- /dev/null +++ b/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/security/JWTAuthenticationImpl.java @@ -0,0 +1,40 @@ +package br.com.rayankonecny.authserviceapi.security; + +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; + +import br.com.rayankonecny.authserviceapi.utils.JWTUtils; +import br.com.rayankonecny.hdcommoslib.models.requests.AuthenticateRequest; +import br.com.rayankonecny.hdcommoslib.models.responses.AuthenticationResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; + +@Log4j2 +@RequiredArgsConstructor +public class JWTAuthenticationImpl { + + private final JWTUtils jwtUtils; + private final AuthenticationManager authenticationManager; + + public AuthenticationResponse authentication(final AuthenticateRequest request) { + try { + log.info("Authenticating user {}:", request.email()); + + final var authResult = authenticationManager + .authenticate(new UsernamePasswordAuthenticationToken(request.email(), request.password())); + + return buildAuthenticationResponse((UserDetailsDTO) authResult.getPrincipal()); + + } catch (BadCredentialsException e) { + log.error("Error no authenticate user: {}", request.email()); + throw new BadCredentialsException("Email or password invalid"); + } + } + + protected AuthenticationResponse buildAuthenticationResponse(UserDetailsDTO dto) { + log.info("Successfuly authenticated user: {}", dto.getUsername()); + final var token = jwtUtils.generateToken(dto); + return AuthenticationResponse.builder().type("Bearer " + token).token(token).build(); + } +} diff --git a/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/security/UserDetailsDTO.java b/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/security/UserDetailsDTO.java new file mode 100644 index 0000000..afa0100 --- /dev/null +++ b/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/security/UserDetailsDTO.java @@ -0,0 +1,55 @@ +package br.com.rayankonecny.authserviceapi.security; + +import java.io.Serial; +import java.util.Collection; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class UserDetailsDTO implements UserDetails { + + @Serial + private static final long serialVersionUID = 1L; + + private String id; + private String name; + private String username; + private String password; + private Collection authorities; + + @Override + public Collection getAuthorities() { + return this.authorities; + } + + @Override + public String getPassword() { + return this.password; + } + + @Override + public String getUsername() { + return this.username; + } + + public String getName() { + return this.name; + } + + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + +} diff --git a/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/services/UserDetailsServiceImpl.java b/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/services/UserDetailsServiceImpl.java new file mode 100644 index 0000000..0037ff1 --- /dev/null +++ b/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/services/UserDetailsServiceImpl.java @@ -0,0 +1,36 @@ +package br.com.rayankonecny.authserviceapi.services; + +import java.util.stream.Collector; +import java.util.stream.Collectors; + +import org.springframework.security.core.authority.SimpleGrantedAuthority; +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; + +import br.com.rayankonecny.authserviceapi.repository.UserRepository; +import br.com.rayankonecny.authserviceapi.security.UserDetailsDTO; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class UserDetailsServiceImpl implements UserDetailsService { + + private final UserRepository repository; + + @Override + public UserDetails loadUserByUsername(final String email) throws UsernameNotFoundException { + + final var entity = repository.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException("User not found: " + email)); + + return UserDetailsDTO.builder() + .id(entity.getId()) + .name(entity.getName()) + .username(entity.getEmail()) + .password(entity.getPassword()) + .authorities(entity.getProfiles().stream().map(x -> new SimpleGrantedAuthority(x.getDescription())).collect(Collectors.toSet())) + .build(); + } +} diff --git a/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/utils/JWTUtils.java b/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/utils/JWTUtils.java new file mode 100644 index 0000000..3706391 --- /dev/null +++ b/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/utils/JWTUtils.java @@ -0,0 +1,33 @@ +package br.com.rayankonecny.authserviceapi.utils; + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; + +import java.util.Date; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import br.com.rayankonecny.authserviceapi.security.UserDetailsDTO; + +@Component +public class JWTUtils { + + @Value("${jwt.secret}") + private String secret; + + @Value("${jwt.expiration}") + private Long expiration; + + public String generateToken(final UserDetailsDTO dto) { + return Jwts.builder() + .claim("id", dto.getId()) + .claim("name", dto.getName()) + .claim("authorities", dto.getAuthorities()) + .setSubject(dto.getUsername()) + .signWith(SignatureAlgorithm.HS512, secret.getBytes()) + .setExpiration(new Date(System.currentTimeMillis()+ expiration)) + .compact(); + + } +} diff --git a/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/utils/JwtKeyProvider.java b/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/utils/JwtKeyProvider.java new file mode 100644 index 0000000..11eba14 --- /dev/null +++ b/auth-service-api/src/main/java/br/com/rayankonecny/authserviceapi/utils/JwtKeyProvider.java @@ -0,0 +1,23 @@ +package br.com.rayankonecny.authserviceapi.utils; + +import io.jsonwebtoken.security.Keys; + +import javax.crypto.SecretKey; +import java.util.Base64; + +public final class JwtKeyProvider { + + // 🔥 REGRA DE OURO: + // Isso NÃO deve ficar hardcoded em produção. + // Use ENV VAR, Vault, Kubernetes Secret, etc. + private static final String BASE64_SECRET = "c3VwZXItc2VndXJhLWNoYXZlLWp3dC1jb20tMzItYnl0ZXM="; + + private JwtKeyProvider() { + // evita instância acidental + } + + public static SecretKey getKey() { + byte[] keyBytes = Base64.getDecoder().decode(BASE64_SECRET); + return Keys.hmacShaKeyFor(keyBytes); + } +} diff --git a/auth-service-api/src/main/resources/application.yml b/auth-service-api/src/main/resources/application.yml index d2dccde..83a5098 100644 --- a/auth-service-api/src/main/resources/application.yml +++ b/auth-service-api/src/main/resources/application.yml @@ -1,4 +1,10 @@ spring: application: name: "auth-service-api" - \ No newline at end of file + data: + mongodb: + uri: mongodb://175.15.15.6:27017/olympdb + auto-index-creation: true + +jwt.secret: "IHf3Yua/byvtA+iIcGWmkrLvpKEXTb5ClkXaZ0VDmYbr/6b1otCs38x68bidvZLAOB7anUtVQlCid6YDULO5XA==" +jwt.expiration: 120000