diff --git a/gateway/pom.xml b/gateway/pom.xml
index 826928d..c3e5e21 100644
--- a/gateway/pom.xml
+++ b/gateway/pom.xml
@@ -57,6 +57,42 @@
spring-boot-starter-test
test
+
+ com.auth0
+ java-jwt
+ 4.4.0
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+ org.springframework.security
+ spring-security-test
+ test
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+ org.springframework.boot
+ spring-boot-devtools
+ runtime
+ true
+
+
+ org.postgresql
+ postgresql
+ runtime
+ 42.6.0
+
+
+ org.projectlombok
+ lombok
+ true
+
diff --git a/gateway/src/main/java/com/lucasmoraist/gateway/RestClientConfig.java b/gateway/src/main/java/com/lucasmoraist/gateway/config/rest/RestClientConfig.java
similarity index 87%
rename from gateway/src/main/java/com/lucasmoraist/gateway/RestClientConfig.java
rename to gateway/src/main/java/com/lucasmoraist/gateway/config/rest/RestClientConfig.java
index 1c88903..fcd9f0e 100644
--- a/gateway/src/main/java/com/lucasmoraist/gateway/RestClientConfig.java
+++ b/gateway/src/main/java/com/lucasmoraist/gateway/config/rest/RestClientConfig.java
@@ -1,4 +1,4 @@
-package com.lucasmoraist.gateway;
+package com.lucasmoraist.gateway.config.rest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
diff --git a/gateway/src/main/java/com/lucasmoraist/gateway/config/security/SecurityConfig.java b/gateway/src/main/java/com/lucasmoraist/gateway/config/security/SecurityConfig.java
new file mode 100644
index 0000000..fd9d4d0
--- /dev/null
+++ b/gateway/src/main/java/com/lucasmoraist/gateway/config/security/SecurityConfig.java
@@ -0,0 +1,48 @@
+package com.lucasmoraist.gateway.config.security;
+
+import org.springframework.beans.factory.annotation.Autowired;
+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.config.annotation.authentication.configuration.AuthenticationConfiguration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+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
+@EnableWebSecurity
+public class SecurityConfig {
+
+ @Autowired
+ SecurityFilter securityFilter;
+
+ @Bean
+ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+ http
+ .csrf(csrf -> csrf.disable())
+ .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+ .authorizeHttpRequests(authorize -> authorize
+ .requestMatchers(HttpMethod.POST, "/auth/login").permitAll()
+ .requestMatchers(HttpMethod.POST, "/auth/register").permitAll()
+ .anyRequest().authenticated()
+ )
+ .addFilterBefore(securityFilter, UsernamePasswordAuthenticationFilter.class);
+ return http.build();
+ }
+
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+
+ @Bean
+ public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
+ return authenticationConfiguration.getAuthenticationManager();
+ }
+
+}
diff --git a/gateway/src/main/java/com/lucasmoraist/gateway/config/security/SecurityFilter.java b/gateway/src/main/java/com/lucasmoraist/gateway/config/security/SecurityFilter.java
new file mode 100644
index 0000000..d4022e6
--- /dev/null
+++ b/gateway/src/main/java/com/lucasmoraist/gateway/config/security/SecurityFilter.java
@@ -0,0 +1,47 @@
+package com.lucasmoraist.gateway.config.security;
+
+import com.lucasmoraist.gateway.domain.entity.User;
+import com.lucasmoraist.gateway.repository.UserRepository;
+import com.lucasmoraist.gateway.service.TokenService;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+import java.util.Collections;
+
+@Component
+public class SecurityFilter extends OncePerRequestFilter {
+
+ @Autowired
+ private TokenService tokenService;
+ @Autowired
+ private UserRepository userRepository;
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain filterChain) throws ServletException, IOException {
+ var token = this.recoverToken(req);
+ var login = tokenService.validateToken(token);
+ if(login != null){
+ User user = userRepository.findByEmail(login).orElseThrow(() -> new RuntimeException("User Not Found"));
+ var authorities = Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"));
+ var authentication = new UsernamePasswordAuthenticationToken(user, null, authorities);
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+ }
+ filterChain.doFilter(req, res);
+ }
+
+ private String recoverToken(HttpServletRequest request){
+ var authHeader = request.getHeader("Authorization");
+ if(authHeader == null) return null;
+ return authHeader.replace("Bearer ", "");
+ }
+
+}
diff --git a/gateway/src/main/java/com/lucasmoraist/gateway/controller/UserController.java b/gateway/src/main/java/com/lucasmoraist/gateway/controller/UserController.java
new file mode 100644
index 0000000..d47de99
--- /dev/null
+++ b/gateway/src/main/java/com/lucasmoraist/gateway/controller/UserController.java
@@ -0,0 +1,38 @@
+package com.lucasmoraist.gateway.controller;
+
+import com.lucasmoraist.gateway.domain.dto.TokenDTO;
+import com.lucasmoraist.gateway.domain.model.LoginRequest;
+import com.lucasmoraist.gateway.domain.model.RegisterRequest;
+import com.lucasmoraist.gateway.service.UserService;
+import jakarta.validation.Valid;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@Slf4j
+@RestController
+@RequestMapping("/auth")
+public class UserController {
+
+ @Autowired
+ private UserService service;
+
+ @PostMapping("register")
+ public ResponseEntity register(@Valid @RequestBody RegisterRequest request) throws Exception {
+ this.service.register(request);
+ log.info("Registering");
+ return ResponseEntity.ok().build();
+ }
+
+ @PostMapping("login")
+ public ResponseEntity login(@Valid @RequestBody LoginRequest request) throws Exception {
+ var response = this.service.login(request);
+ log.info("Logging in");
+ return ResponseEntity.ok().body(response);
+ }
+
+}
diff --git a/gateway/src/main/java/com/lucasmoraist/gateway/domain/dto/TokenDTO.java b/gateway/src/main/java/com/lucasmoraist/gateway/domain/dto/TokenDTO.java
new file mode 100644
index 0000000..0bd9285
--- /dev/null
+++ b/gateway/src/main/java/com/lucasmoraist/gateway/domain/dto/TokenDTO.java
@@ -0,0 +1,4 @@
+package com.lucasmoraist.gateway.domain.dto;
+
+public record TokenDTO(String token) {
+}
diff --git a/gateway/src/main/java/com/lucasmoraist/gateway/domain/entity/User.java b/gateway/src/main/java/com/lucasmoraist/gateway/domain/entity/User.java
new file mode 100644
index 0000000..01526f8
--- /dev/null
+++ b/gateway/src/main/java/com/lucasmoraist/gateway/domain/entity/User.java
@@ -0,0 +1,42 @@
+package com.lucasmoraist.gateway.domain.entity;
+
+import com.lucasmoraist.gateway.domain.model.LoginRequest;
+import com.lucasmoraist.gateway.domain.model.RegisterRequest;
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@Entity(name = "t_user")
+@Table(name = "t_user")
+public class User {
+
+ @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(nullable = false, length = 80)
+ private String name;
+
+ @Column(nullable = false, length = 255, unique = true)
+ private String email;
+
+ @Column(nullable = false, length = 20)
+ private String password;
+
+ public User(RegisterRequest request) {
+ this.name = request.getName();
+ this.email = request.getEmail();
+ this.password = request.getPassword();
+ }
+
+ public User(LoginRequest request) {
+ this.email = request.getEmail();
+ this.password = request.getPassword();
+ }
+
+}
diff --git a/gateway/src/main/java/com/lucasmoraist/gateway/domain/model/LoginRequest.java b/gateway/src/main/java/com/lucasmoraist/gateway/domain/model/LoginRequest.java
new file mode 100644
index 0000000..d4b4669
--- /dev/null
+++ b/gateway/src/main/java/com/lucasmoraist/gateway/domain/model/LoginRequest.java
@@ -0,0 +1,21 @@
+package com.lucasmoraist.gateway.domain.model;
+
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class LoginRequest {
+
+ @NotBlank
+ @Email
+ private String email;
+
+ @NotBlank
+ private String password;
+
+}
diff --git a/gateway/src/main/java/com/lucasmoraist/gateway/domain/model/RegisterRequest.java b/gateway/src/main/java/com/lucasmoraist/gateway/domain/model/RegisterRequest.java
new file mode 100644
index 0000000..4feec75
--- /dev/null
+++ b/gateway/src/main/java/com/lucasmoraist/gateway/domain/model/RegisterRequest.java
@@ -0,0 +1,30 @@
+package com.lucasmoraist.gateway.domain.model;
+
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Size;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class RegisterRequest {
+
+ @Size(min = 3, max = 80)
+ private String name;
+
+ @NotBlank
+ @Email(message = "Invalid email")
+ @Size(max = 255)
+ private String email;
+
+ @NotBlank
+ @Size(min = 8, max = 20)
+ @Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).*$",
+ message = "Password must contain at least one uppercase letter, one lowercase letter and one digit")
+ private String password;
+
+}
diff --git a/gateway/src/main/java/com/lucasmoraist/gateway/repository/UserRepository.java b/gateway/src/main/java/com/lucasmoraist/gateway/repository/UserRepository.java
new file mode 100644
index 0000000..46d537f
--- /dev/null
+++ b/gateway/src/main/java/com/lucasmoraist/gateway/repository/UserRepository.java
@@ -0,0 +1,10 @@
+package com.lucasmoraist.gateway.repository;
+
+import com.lucasmoraist.gateway.domain.entity.User;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.Optional;
+
+public interface UserRepository extends JpaRepository {
+ Optional findByEmail(String email);
+}
diff --git a/gateway/src/main/java/com/lucasmoraist/gateway/service/TokenService.java b/gateway/src/main/java/com/lucasmoraist/gateway/service/TokenService.java
new file mode 100644
index 0000000..f9245a4
--- /dev/null
+++ b/gateway/src/main/java/com/lucasmoraist/gateway/service/TokenService.java
@@ -0,0 +1,8 @@
+package com.lucasmoraist.gateway.service;
+
+import com.lucasmoraist.gateway.domain.entity.User;
+
+public interface TokenService {
+ String generateToken(User user);
+ String validateToken(String token);
+}
diff --git a/gateway/src/main/java/com/lucasmoraist/gateway/service/UserDetailsService.java b/gateway/src/main/java/com/lucasmoraist/gateway/service/UserDetailsService.java
new file mode 100644
index 0000000..550de21
--- /dev/null
+++ b/gateway/src/main/java/com/lucasmoraist/gateway/service/UserDetailsService.java
@@ -0,0 +1,8 @@
+package com.lucasmoraist.gateway.service;
+
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+
+public interface UserDetailsService {
+ UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
+}
diff --git a/gateway/src/main/java/com/lucasmoraist/gateway/service/UserService.java b/gateway/src/main/java/com/lucasmoraist/gateway/service/UserService.java
new file mode 100644
index 0000000..06a5e39
--- /dev/null
+++ b/gateway/src/main/java/com/lucasmoraist/gateway/service/UserService.java
@@ -0,0 +1,10 @@
+package com.lucasmoraist.gateway.service;
+
+import com.lucasmoraist.gateway.domain.dto.TokenDTO;
+import com.lucasmoraist.gateway.domain.model.LoginRequest;
+import com.lucasmoraist.gateway.domain.model.RegisterRequest;
+
+public interface UserService {
+ void register(RegisterRequest request) throws Exception;
+ TokenDTO login(LoginRequest request) throws Exception;
+}
diff --git a/gateway/src/main/java/com/lucasmoraist/gateway/service/impl/TokenServiceImpl.java b/gateway/src/main/java/com/lucasmoraist/gateway/service/impl/TokenServiceImpl.java
new file mode 100644
index 0000000..3862443
--- /dev/null
+++ b/gateway/src/main/java/com/lucasmoraist/gateway/service/impl/TokenServiceImpl.java
@@ -0,0 +1,54 @@
+package com.lucasmoraist.gateway.service.impl;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.algorithms.Algorithm;
+import com.auth0.jwt.exceptions.JWTCreationException;
+import com.auth0.jwt.exceptions.JWTVerificationException;
+import com.lucasmoraist.gateway.domain.entity.User;
+import com.lucasmoraist.gateway.service.TokenService;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+
+@Service
+public class TokenServiceImpl implements TokenService {
+
+ @Value("${jwt.secret}")
+ private String secret;
+
+ @Override
+ public String generateToken(User user) {
+ try {
+ Algorithm algorithm = Algorithm.HMAC256(secret);
+
+ return JWT.create()
+ .withIssuer("task-list")
+ .withSubject(user.getEmail())
+ .withExpiresAt(this.generateExpirationDate())
+ .sign(algorithm);
+ } catch(JWTCreationException e){
+ throw new RuntimeException("Error while authentication");
+ }
+ }
+
+ @Override
+ public String validateToken(String token) {
+ try {
+ Algorithm algorithm = Algorithm.HMAC256(secret);
+ return JWT.require(algorithm)
+ .withIssuer("task-list")
+ .build()
+ .verify(token)
+ .getSubject();
+ } catch(JWTVerificationException e) {
+ return null;
+ }
+ }
+
+ private Instant generateExpirationDate(){
+ return LocalDateTime.now().plusHours(2).toInstant(ZoneOffset.of("-03:00"));
+ }
+}
diff --git a/gateway/src/main/java/com/lucasmoraist/gateway/service/impl/UserDetailsServiceImpl.java b/gateway/src/main/java/com/lucasmoraist/gateway/service/impl/UserDetailsServiceImpl.java
new file mode 100644
index 0000000..3772db3
--- /dev/null
+++ b/gateway/src/main/java/com/lucasmoraist/gateway/service/impl/UserDetailsServiceImpl.java
@@ -0,0 +1,26 @@
+package com.lucasmoraist.gateway.service.impl;
+
+import com.lucasmoraist.gateway.domain.entity.User;
+import com.lucasmoraist.gateway.repository.UserRepository;
+import com.lucasmoraist.gateway.service.UserDetailsService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+
+@Service
+public class UserDetailsServiceImpl implements UserDetailsService {
+
+ @Autowired
+ private UserRepository repository;
+
+ @Override
+ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+ User user = this.repository.findByEmail(username)
+ .orElseThrow(() -> new UsernameNotFoundException("User Not Found"));
+
+ return new org.springframework.security.core.userdetails.User(user.getEmail(), user.getPassword(), new ArrayList<>());
+ }
+}
diff --git a/gateway/src/main/java/com/lucasmoraist/gateway/service/impl/UserServiceImpl.java b/gateway/src/main/java/com/lucasmoraist/gateway/service/impl/UserServiceImpl.java
new file mode 100644
index 0000000..e111571
--- /dev/null
+++ b/gateway/src/main/java/com/lucasmoraist/gateway/service/impl/UserServiceImpl.java
@@ -0,0 +1,64 @@
+package com.lucasmoraist.gateway.service.impl;
+
+import com.lucasmoraist.gateway.domain.dto.TokenDTO;
+import com.lucasmoraist.gateway.domain.entity.User;
+import com.lucasmoraist.gateway.domain.model.LoginRequest;
+import com.lucasmoraist.gateway.domain.model.RegisterRequest;
+import com.lucasmoraist.gateway.repository.UserRepository;
+import com.lucasmoraist.gateway.service.TokenService;
+import com.lucasmoraist.gateway.service.UserService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+
+import java.util.Optional;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class UserServiceImpl implements UserService {
+
+ private final UserRepository repository;
+ private final TokenService tokenService;
+ private final PasswordEncoder passwordEncoder;
+
+ @Override
+ public void register(RegisterRequest request) throws Exception {
+ log.info("Attempting to register user with email: {}", request.getEmail());
+
+ Optional optionalUser = this.repository.findByEmail(request.getEmail());
+ if (optionalUser.isPresent()) {
+ log.error("Registration failed: email already in use: {}", request.getEmail());
+ throw new Exception("Email already in use");
+ }
+
+ User newUser = new User(request);
+ newUser.setPassword(this.passwordEncoder.encode(request.getPassword()));
+
+ this.repository.save(newUser);
+
+ log.info("Registration successful for email: {}", request.getEmail());
+ }
+
+ @Override
+ public TokenDTO login(LoginRequest request) throws Exception {
+ log.info("Attempting to authenticate user with email: {}", request.getEmail());
+
+ User user = this.repository.findByEmail(request.getEmail())
+ .orElseThrow(() -> {
+ log.error("Authentication failed: email not found for email: {}", request.getEmail());
+ return new Exception("Email not found");
+ });
+
+ if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
+ log.error("Authentication failed: incorrect password for email: {}", request.getEmail());
+ throw new Exception("Incorrect password");
+ }
+
+ String token = this.tokenService.generateToken(user);
+
+ log.info("Authentication successful for email: {}", request.getEmail());
+ return new TokenDTO(token);
+ }
+}
diff --git a/gateway/src/main/resources/application.properties b/gateway/src/main/resources/application.properties
index 5471b43..8f0d68b 100644
--- a/gateway/src/main/resources/application.properties
+++ b/gateway/src/main/resources/application.properties
@@ -1,7 +1,15 @@
+spring.datasource.url=jdbc:postgresql://localhost:5434/db_user
+spring.datasource.username=postgres
+spring.datasource.password=password
+spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
+
spring.application.name=gateway
server.port=8082
eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/
spring.cloud.gateway.discovery.locator.enabled=true
spring.cloud.gateway.discovery.locator.lower-case-service-id=true
-spring.main.web-application-type=reactive
\ No newline at end of file
+spring.main.web-application-type=reactive
+spring.main.allow-bean-definition-overriding=true
+
+jwt.secret=secret
\ No newline at end of file