diff --git a/Dockerfile.dev b/Dockerfile.dev index d2eebdb..13a088f 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -1,70 +1,9 @@ FROM openjdk:21-jdk-slim WORKDIR /app -COPY pom.xml . -COPY mvnw . COPY .mvn .mvn +COPY mvnw pom.xml ./ RUN chmod +x mvnw -RUN ./mvnw dependency:go-offline +RUN ./mvnw -T 4 dependency:go-offline COPY src ./src -EXPOSE 9091 -CMD ["./mvnw", "spring-boot:run"] - -# FROM openjdk:21-jdk-slim - -# # Instalar Maven -# RUN apt-get update && apt-get install -y maven && rm -rf /var/lib/apt/lists/* - -# WORKDIR /app - -# # ✅ Instalar security-common primero -# COPY security-common /app/security-common -# WORKDIR /app/security-common -# RUN mvn clean install -DskipTests - -# # ✅ Construir msvc-security -# WORKDIR /app/msvc-security -# COPY msvc-security/pom.xml . -# COPY msvc-security/mvnw . -# COPY msvc-security/.mvn .mvn -# RUN chmod +x mvnw - -# COPY msvc-security/src ./src - -# EXPOSE 9091 -# CMD ["./mvnw", "spring-boot:run", "-Dspring-boot.run.fork=false"] - - - -# FROM openjdk:21-jdk-slim - -# RUN apt-get update && \ -# apt-get install -y maven && \ -# rm -rf /var/lib/apt/lists/* && \ -# mvn --version - -# WORKDIR /app - -# COPY security-common/pom.xml /app/security-common/pom.xml -# COPY msvc-security/pom.xml /app/msvc-security/pom.xml - -# WORKDIR /app/security-common -# RUN mvn dependency:resolve || true - -# COPY security-common /app/security-common -# RUN mvn clean install -DskipTests -q - -# WORKDIR /app/msvc-security -# COPY msvc-security/mvnw . -# COPY msvc-security/.mvn .mvn -# RUN chmod +x mvnw - -# RUN ./mvnw dependency:go-offline -q || true - -# COPY msvc-security/src ./src - -# EXPOSE 9091 -# ENV SPRING_PROFILES_ACTIVE=dev - -# CMD ["./mvnw", "spring-boot:run", \ -# "-Dspring-boot.run.fork=false", \ -# "-Dspring-boot.run.jvmArguments=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5005"] \ No newline at end of file +EXPOSE 9091 5005 +CMD ["./mvnw", "spring-boot:run", "-Dspring-boot.run.jvmArguments=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"] \ No newline at end of file diff --git a/pom.xml b/pom.xml index f751108..37ccd51 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ UTF-8 - + org.springframework.boot @@ -135,7 +135,10 @@ ${mapstruct.version} provided - + + org.springframework.kafka + spring-kafka + @@ -150,6 +153,10 @@ org.springframework.security spring-security-oauth2-jose + + org.springframework.boot + spring-boot-starter-oauth2-client + diff --git a/src/main/java/com/security/config/Audit.java b/src/main/java/com/security/config/Audit.java deleted file mode 100644 index 85bf1a0..0000000 --- a/src/main/java/com/security/config/Audit.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.security.config; - -import jakarta.persistence.Column; -import jakarta.persistence.Embeddable; -import jakarta.persistence.PrePersist; -import jakarta.persistence.PreUpdate; - -import java.time.LocalDateTime; - -@Embeddable -public class Audit { - @Column(name = "created_at") - private LocalDateTime createdAt; - @Column(name = "update_at") - private LocalDateTime updateAt; - - @PrePersist - void prePersist() { - createdAt = LocalDateTime.now(); - } - @PreUpdate - void preUpdate() { - updateAt = LocalDateTime.now(); - } -} \ No newline at end of file diff --git a/src/main/java/com/security/config/CommonPointcuts.java b/src/main/java/com/security/config/CommonPointcuts.java index d7f3d95..7f37b67 100644 --- a/src/main/java/com/security/config/CommonPointcuts.java +++ b/src/main/java/com/security/config/CommonPointcuts.java @@ -9,8 +9,8 @@ @Component public class CommonPointcuts { @Pointcut("execution(* com.security.services.*.*(..))") - public void greetingLoggerServices(){}; + public void greetingLoggerServices(){} @Pointcut("execution(* com.security.controllers.*.*(..))") - public void greetingLoggerControllers(){}; + public void greetingLoggerControllers(){} } diff --git a/src/main/java/com/security/config/DataInitializer.java b/src/main/java/com/security/config/DataInitializer.java deleted file mode 100644 index 47bbb9a..0000000 --- a/src/main/java/com/security/config/DataInitializer.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.security.config; - -import com.security.Entity.RoleEntity; -import com.security.Entity.UserEntity; -import com.security.Repository.RoleRepository; -import com.security.Repository.UserRepository; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.CommandLineRunner; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Component; - -import java.util.Set; - -@Component -@RequiredArgsConstructor -@Slf4j -public class DataInitializer implements CommandLineRunner { - - private final UserRepository userRepository; - private final RoleRepository roleRepository; - private final PasswordEncoder passwordEncoder; - - - - @Override - public void run(String... args) throws Exception { - RoleEntity adminRole = roleRepository.findByName("ADMIN") - .orElseGet(() -> { - log.info("Creating ADMIN role"); - return roleRepository.save(RoleEntity.builder() - .name("ADMIN") - .description("Administrator role") - .build()); - }); - - RoleEntity userRole = roleRepository.findByName("USER") - .orElseGet(() -> { - log.info("Creating USER role"); - return roleRepository.save(RoleEntity.builder() - .name("USER") - .description("User role") - .build()); - }); - - RoleEntity trainerRole = roleRepository.findByName("TRAINER") - .orElseGet(() -> { - log.info("Creating TRAINER role"); - return roleRepository.save(RoleEntity.builder() - .name("TRAINER") - .description("Trainer role") - .build()); - }); - - if (!userRepository.existsByUsername("admin")) { - String encodedPassword = passwordEncoder.encode("admin123"); - log.info("Creating admin user with encoded password length: {}", encodedPassword.length()); - - UserEntity admin = UserEntity.builder() - .username("admin") - .email("admin@fitdesk.com") - .password(encodedPassword) - .firstName("Admin") - .lastName("User") - .roles(Set.of(adminRole)) - .build(); - userRepository.save(admin); - log.info("Admin user created successfully"); - } else { - log.info("Admin user already exists"); - } - - if (!userRepository.existsByUsername("user")) { - String encodedPassword = passwordEncoder.encode("user123"); - log.info("Creating regular user"); - - UserEntity user = UserEntity.builder() - .username("user") - .email("user@fitdesk.com") - .password(encodedPassword) - .firstName("Regular") - .lastName("User") - .roles(Set.of(userRole)) - .build(); - userRepository.save(user); - log.info("Regular user created successfully"); - } - - if (!userRepository.existsByUsername("trainer")) { - String encodedPassword = passwordEncoder.encode("trainer123"); - log.info("Creating trainer user"); - - UserEntity trainer = UserEntity.builder() - .username("trainer") - .email("trainer@fitdesk.com") - .password(encodedPassword) - .firstName("Gym") - .lastName("Trainer") - .roles(Set.of(trainerRole)) - .build(); - userRepository.save(trainer); - log.info("Trainer user created successfully"); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/security/config/RoleInitializer.java b/src/main/java/com/security/config/RoleInitializer.java new file mode 100644 index 0000000..e7cbf95 --- /dev/null +++ b/src/main/java/com/security/config/RoleInitializer.java @@ -0,0 +1,34 @@ +package com.security.config; + +import com.security.entity.RoleEntity; +import com.security.repository.RoleRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + + +@Component +@RequiredArgsConstructor +@Slf4j +public class RoleInitializer implements CommandLineRunner { + private final RoleRepository roleRepository; + + @Override + public void run(String... args) throws Exception { + createRoleIfNotExists("ADMIN", "Administrator role"); + createRoleIfNotExists("USER", "User role"); + createRoleIfNotExists("TRAINER", "Trainer role"); + } + + private void createRoleIfNotExists(String name, String description) { + roleRepository.findByName(name) + .orElseGet(() -> { + log.info("Creando {} role", name); + return roleRepository.save(RoleEntity.builder() + .name(name) + .description(description) + .build()); + }); + } +} \ No newline at end of file diff --git a/src/main/java/com/security/config/SecurityConfig.java b/src/main/java/com/security/config/SecurityConfig.java deleted file mode 100644 index 3363df0..0000000 --- a/src/main/java/com/security/config/SecurityConfig.java +++ /dev/null @@ -1,196 +0,0 @@ -package com.security.config; - -import com.nimbusds.jose.jwk.JWKSet; -import com.nimbusds.jose.jwk.RSAKey; -import com.nimbusds.jose.jwk.source.ImmutableJWKSet; -import com.nimbusds.jose.jwk.source.JWKSource; -import com.nimbusds.jose.proc.SecurityContext; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.annotation.Order; -import org.springframework.security.config.Customizer; -import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.core.AuthorizationGrantType; -import org.springframework.security.oauth2.core.ClientAuthenticationMethod; -import org.springframework.security.oauth2.core.oidc.OidcScopes; -import org.springframework.security.oauth2.jwt.JwtDecoder; -import org.springframework.security.oauth2.jwt.JwtEncoder; -import org.springframework.security.oauth2.jwt.NimbusJwtEncoder; -import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository; -import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; -import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; -import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; -import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; -import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; -import org.springframework.security.oauth2.server.authorization.settings.ClientSettings; -import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat; -import org.springframework.security.oauth2.server.authorization.settings.TokenSettings; -import org.springframework.security.web.SecurityFilterChain; - -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.interfaces.RSAPrivateKey; -import java.security.interfaces.RSAPublicKey; -import java.time.Duration; -import java.util.Arrays; -import java.util.List; -import java.util.UUID; - - -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; - -@Configuration -@EnableWebSecurity -@EnableMethodSecurity -@RequiredArgsConstructor -public class SecurityConfig { - - private final AuthProperties authProperties; -// @Value("${auth.client.redirect-uris}") -// private String[] redirectUris; -// -// @Value("${auth.client.post-logout-redirect-uri}") -// private String postLogoutRedirectUri; -// @Value("${auth.server.issuer}") -// private String issuerUri; - - @Bean - @Order(1) - public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { - OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = - OAuth2AuthorizationServerConfigurer.authorizationServer(); - http - .securityMatcher(authorizationServerConfigurer.getEndpointsMatcher()) - .with(authorizationServerConfigurer, (authorizationServer) -> - authorizationServer - .oidc(Customizer.withDefaults()) - ) - .oauth2ResourceServer(oauth2ResourceServer -> - oauth2ResourceServer.jwt(Customizer.withDefaults()) - ); - - return http.build(); - } - - @Bean - @Order(2) - public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http, CookieAuthenticationFilter cookieAuthenticationFilter) throws Exception { - return http - .authorizeHttpRequests(authorize -> authorize - .requestMatchers( - "/actuator/**", - "/swagger-ui/**", - "/v3/api-docs/**", - "/auth/info", - "/auth/status", - "/auth/login", - "/auth/refresh", - "/saludo", - "/" - ).permitAll() - .anyRequest().authenticated() - ) - .addFilterBefore(cookieAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) - .oauth2ResourceServer(oauth2ResourceServer -> - oauth2ResourceServer.jwt(Customizer.withDefaults()) - ) - .csrf(AbstractHttpConfigurer::disable) - .build(); - } - - @Bean - public RegisteredClientRepository registeredClientRepository() { - RegisteredClient.Builder clientBuilder = RegisteredClient.withId(UUID.randomUUID().toString()) - .clientId("gateway-client") - .clientSecret(passwordEncoder().encode("gateway-secret")) - .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) - .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST) - .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) - .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) - .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) - .postLogoutRedirectUri(authProperties.getClient().getPostLogoutRedirectUri()) - .scope(OidcScopes.OPENID) - .scope(OidcScopes.PROFILE) - .scope(OidcScopes.EMAIL) - .scope("read") - .scope("write") - .clientSettings(ClientSettings.builder() - .requireAuthorizationConsent(false) - .requireProofKey(false) - .build()) - .tokenSettings(TokenSettings.builder() - .accessTokenTimeToLive(Duration.ofMinutes(15)) - .refreshTokenTimeToLive(Duration.ofDays(7)) - .reuseRefreshTokens(false) - .accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED) - .build()); - - authProperties.getClient().getRedirectUris() - .forEach(clientBuilder::redirectUri); - - return new InMemoryRegisteredClientRepository(clientBuilder.build()); - } - - @Bean - public JWKSource jwkSource() { - KeyPair keyPair = generateRsaKey(); - RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); - RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); - RSAKey rsaKey = new RSAKey.Builder(publicKey) - .privateKey(privateKey) - .keyID(UUID.randomUUID().toString()) - .build(); - JWKSet jwkSet = new JWKSet(rsaKey); - return new ImmutableJWKSet<>(jwkSet); - } - - private static KeyPair generateRsaKey() { - KeyPair keyPair; - try { - KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); - keyPairGenerator.initialize(2048); - keyPair = keyPairGenerator.generateKeyPair(); - } catch ( - Exception ex) { - throw new IllegalStateException(ex); - } - return keyPair; - } - - @Bean - public JwtDecoder jwtDecoder(JWKSource jwkSource) { - return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); - } - - @Bean - public JwtEncoder jwtEncoder(JWKSource jwkSource) { - return new NimbusJwtEncoder(jwkSource); - } - - @Bean - public AuthorizationServerSettings authorizationServerSettings() { - return AuthorizationServerSettings.builder() - .issuer(authProperties.getServer().getIssuer()) - .authorizationEndpoint("/oauth2/authorize") - .tokenEndpoint("/oauth2/token") - .tokenIntrospectionEndpoint("/oauth2/introspect") - .tokenRevocationEndpoint("/oauth2/revoke") - .jwkSetEndpoint("/.well-known/jwks.json") - .oidcLogoutEndpoint("/connect/logout") - .oidcUserInfoEndpoint("/userinfo") - .oidcClientRegistrationEndpoint("/connect/register") - .build(); - } - - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } -} diff --git a/src/main/java/com/security/config/audit/Audit.java b/src/main/java/com/security/config/audit/Audit.java new file mode 100644 index 0000000..c368bf8 --- /dev/null +++ b/src/main/java/com/security/config/audit/Audit.java @@ -0,0 +1,35 @@ +package com.security.config.audit; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; + + +@Embeddable +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Audit { + + @Column(name = "created_by") + private String createdBy; + + @Column(name = "created_at") + private Instant createdAt; + + @Column(name = "updated_by") + private String updatedBy; + + @Column(name = "updated_at") + private Instant updatedAt; + + @Column(name = "status_reason") + private String statusReason; + +} \ No newline at end of file diff --git a/src/main/java/com/security/config/audit/AuditListener.java b/src/main/java/com/security/config/audit/AuditListener.java new file mode 100644 index 0000000..034504a --- /dev/null +++ b/src/main/java/com/security/config/audit/AuditListener.java @@ -0,0 +1,78 @@ +package com.security.config.audit; + +import jakarta.persistence.PrePersist; +import jakarta.persistence.PreUpdate; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +import java.lang.reflect.Field; +import java.time.Instant; + +public class AuditListener { + + @PrePersist + public void onPrePersist(Object entity) { + applyAudit(entity, true); + } + + @PreUpdate + public void onPreUpdate(Object entity) { + applyAudit(entity, false); + } + + private void applyAudit(Object entity, boolean isCreate) { + try { + Field f = findAuditField(entity); + if (f == null) + return; + f.setAccessible(true); + + Audit audit = (Audit) f.get(entity); + if (audit == null) { + audit = Audit.builder().build(); + } + + String user = currentUsername(); + Instant now = Instant.now(); + + if (isCreate) { + if (audit.getCreatedAt() == null) + audit.setCreatedAt(now); + if (audit.getCreatedBy() == null) + audit.setCreatedBy(user); + } + // siempre actualizar updated* en create y update + audit.setUpdatedAt(now); + audit.setUpdatedBy(user); + + f.set(entity, audit); + } catch ( + Exception ignored) { + // no bloquear persist/update por auditoría + } + } + + private Field findAuditField(Object entity) { + Class cls = entity.getClass(); + while (cls != null && cls != Object.class) { + try { + return cls.getDeclaredField("audit"); + } catch ( + NoSuchFieldException e) { + cls = cls.getSuperclass(); + } + } + return null; + } + + private String currentUsername() { + try { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth != null && auth.getName() != null) + return auth.getName(); + } catch ( + Exception ignored) { + } + return "system"; + } +} diff --git a/src/main/java/com/security/config/AuthProperties.java b/src/main/java/com/security/config/auth/AuthProperties.java similarity index 95% rename from src/main/java/com/security/config/AuthProperties.java rename to src/main/java/com/security/config/auth/AuthProperties.java index 68d548a..62cfd4a 100644 --- a/src/main/java/com/security/config/AuthProperties.java +++ b/src/main/java/com/security/config/auth/AuthProperties.java @@ -1,4 +1,4 @@ -package com.security.config; +package com.security.config.auth; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.cloud.context.config.annotation.RefreshScope; diff --git a/src/main/java/com/security/config/auth/AuthorizationServerConfig.java b/src/main/java/com/security/config/auth/AuthorizationServerConfig.java new file mode 100644 index 0000000..66eb48d --- /dev/null +++ b/src/main/java/com/security/config/auth/AuthorizationServerConfig.java @@ -0,0 +1,77 @@ +package com.security.config.auth; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.oidc.OidcScopes; +import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; +import org.springframework.security.oauth2.server.authorization.settings.ClientSettings; +import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat; +import org.springframework.security.oauth2.server.authorization.settings.TokenSettings; + +import java.time.Duration; +import java.util.UUID; + +@Configuration +@RequiredArgsConstructor +public class AuthorizationServerConfig { + + private final AuthProperties authProperties; + private final PasswordEncoder passwordEncoder; + + @Bean + public RegisteredClientRepository registeredClientRepository() { + RegisteredClient.Builder clientBuilder = RegisteredClient.withId(UUID.randomUUID().toString()) + .clientId("gateway-client") + .clientSecret(passwordEncoder.encode("gateway-secret")) + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .redirectUri("http://localhost:9090/login/oauth2/code/gateway-client") + .redirectUri("http://localhost:9090/authorized") + .postLogoutRedirectUri("http://localhost:9090/logout") + .scope(OidcScopes.OPENID) + .scope(OidcScopes.PROFILE) + .scope(OidcScopes.EMAIL) + .scope("read") + .scope("write") + .clientSettings(ClientSettings.builder() + .requireAuthorizationConsent(false) + .requireProofKey(false) + .build()) + .tokenSettings(TokenSettings.builder() + .accessTokenTimeToLive(Duration.ofMinutes(15)) + .refreshTokenTimeToLive(Duration.ofDays(7)) + .reuseRefreshTokens(false) + .accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED) + .build()); + + authProperties.getClient().getRedirectUris().forEach(clientBuilder::redirectUri); + RegisteredClient client = clientBuilder.build(); + return new InMemoryRegisteredClientRepository(client); + } + + + @Bean + public AuthorizationServerSettings authorizationServerSettings() { + return AuthorizationServerSettings.builder() + .issuer(authProperties.getServer().getIssuer()) + .authorizationEndpoint("/oauth2/authorize") + .tokenEndpoint("/oauth2/token") + .tokenIntrospectionEndpoint("/oauth2/introspect") + .tokenRevocationEndpoint("/oauth2/revoke") + .jwkSetEndpoint("/.well-known/jwks.json") + .oidcLogoutEndpoint("/connect/logout") + .oidcUserInfoEndpoint("/userinfo") + .oidcClientRegistrationEndpoint("/connect/register") + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/security/config/CookieAuthenticationFilter.java b/src/main/java/com/security/config/auth/CookieAuthenticationFilter.java similarity index 91% rename from src/main/java/com/security/config/CookieAuthenticationFilter.java rename to src/main/java/com/security/config/auth/CookieAuthenticationFilter.java index 3e1472a..719c609 100644 --- a/src/main/java/com/security/config/CookieAuthenticationFilter.java +++ b/src/main/java/com/security/config/auth/CookieAuthenticationFilter.java @@ -1,4 +1,4 @@ -package com.security.config; +package com.security.config.auth; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -22,7 +22,8 @@ protected void doFilterInternal(HttpServletRequest request, FilterChain filterChain) throws ServletException, IOException { String token = extractTokenFromCookies(request); - + log.debug("Token from cookies: {}", token); + log.debug("Authorization header: {}", request.getHeader("Authorization")); if (token != null) { HttpServletRequestWrapper wrappedRequest = new HttpServletRequestWrapper(request) { @Override diff --git a/src/main/java/com/security/config/auth/JwtConfig.java b/src/main/java/com/security/config/auth/JwtConfig.java new file mode 100644 index 0000000..5ef5219 --- /dev/null +++ b/src/main/java/com/security/config/auth/JwtConfig.java @@ -0,0 +1,60 @@ +package com.security.config.auth; + +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.jwk.source.ImmutableJWKSet; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtEncoder; +import org.springframework.security.oauth2.jwt.NimbusJwtEncoder; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.util.UUID; + +@Configuration +public class JwtConfig { + + @Bean + public JWKSource jwkSource() { + KeyPair keyPair = generateRsaKey(); + RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); + RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); + RSAKey rsaKey = new RSAKey.Builder(publicKey) + .privateKey(privateKey) + .keyID(UUID.randomUUID().toString()) + .build(); + JWKSet jwkSet = new JWKSet(rsaKey); + return new ImmutableJWKSet<>(jwkSet); + } + + private static KeyPair generateRsaKey() { + KeyPair keyPair; + try { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + keyPair = keyPairGenerator.generateKeyPair(); + } catch ( + Exception ex) { + throw new IllegalStateException(ex); + } + return keyPair; + } + + @Bean + public JwtDecoder jwtDecoder(JWKSource jwkSource) { + return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); + } + + @Bean + public JwtEncoder jwtEncoder(JWKSource jwkSource) { + return new NimbusJwtEncoder(jwkSource); + } + +} diff --git a/src/main/java/com/security/config/auth/SecurityConfig.java b/src/main/java/com/security/config/auth/SecurityConfig.java new file mode 100644 index 0000000..10b62e0 --- /dev/null +++ b/src/main/java/com/security/config/auth/SecurityConfig.java @@ -0,0 +1,153 @@ +package com.security.config.auth; + +import com.security.config.auth.oauth2.OAuth2AuthenticationSuccessHandler; +import com.security.services.oauth2.CustomOAuth2UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; +import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map; + +@Configuration +@EnableWebSecurity +@EnableMethodSecurity +@RequiredArgsConstructor +public class SecurityConfig { + + private final CustomOAuth2UserService customOAuth2UserService; + + @Bean + @Order(1) + public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { + OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = + OAuth2AuthorizationServerConfigurer.authorizationServer(); + + http + .securityMatcher(authorizationServerConfigurer.getEndpointsMatcher()) + .with(authorizationServerConfigurer, (authorizationServer) -> + authorizationServer + .oidc(Customizer.withDefaults()) + ) + .oauth2ResourceServer(oauth2ResourceServer -> + oauth2ResourceServer.jwt(Customizer.withDefaults()) + ); + + return http.build(); + } + + @Bean + @Order(2) + public SecurityFilterChain defaultSecurityFilterChain( + HttpSecurity http, + CookieAuthenticationFilter cookieAuthenticationFilter, + OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler, + OAuth2UserService customOidcUserService, + AuthorizationRequestRepository authorizationRequestRepository) throws Exception { + + return http + .authorizeHttpRequests(authorize -> authorize + .requestMatchers( + "/actuator/**", + "/swagger-ui/**", + "/v3/api-docs/**", + "/auth/info", + "/auth/status", + "/auth/login", + "/auth/refresh", + "/auth/register", + "/oauth2/**", + "/login/oauth2/**", + "/test/saludo", + "/test/notification", + "/" + ).permitAll() + .anyRequest().authenticated() + ) + .oauth2Login(oauth2 -> + oauth2.authorizationEndpoint(authorization -> + authorization.authorizationRequestRepository(authorizationRequestRepository) + ) + .userInfoEndpoint(userInfo -> userInfo + .userService(customOAuth2UserService) + .oidcUserService(customOidcUserService) + ) + .successHandler(oAuth2AuthenticationSuccessHandler) + ) + .oauth2Client(Customizer.withDefaults()) + .addFilterBefore(cookieAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) + .oauth2ResourceServer(oauth2ResourceServer -> + oauth2ResourceServer.jwt(Customizer.withDefaults()) + ) + .csrf(AbstractHttpConfigurer::disable) + .build(); + } + + @Bean + public JwtAuthenticationConverter jwtAuthenticationConverter() { + JwtGrantedAuthoritiesConverter scopeConverter = new JwtGrantedAuthoritiesConverter(); + scopeConverter.setAuthorityPrefix("SCOPE_"); + + JwtAuthenticationConverter converter = new JwtAuthenticationConverter(); + converter.setJwtGrantedAuthoritiesConverter(jwt -> { + Collection authorities = new ArrayList<>(); + + Collection scopeAuth = scopeConverter.convert(jwt); + if (scopeAuth != null) { + authorities.addAll(scopeAuth); + } + + Object claim = jwt.getClaims().get("authorities"); + if (claim instanceof String authString) { + String[] parts = authString.trim().split("\\s+"); + for (String part : parts) { + if (!part.isBlank()) { + authorities.add(new SimpleGrantedAuthority(part)); + } + } + } else if (claim instanceof Collection authCollection) { + authCollection.forEach(o -> { + if (o != null) { + authorities.add(new SimpleGrantedAuthority(o.toString())); + } + }); + } else if (claim instanceof Map authMap) { + authMap.values().forEach(v -> { + if (v != null) { + authorities.add(new SimpleGrantedAuthority(v.toString())); + } + }); + } + + return authorities; + }); + + return converter; + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/src/main/java/com/security/config/auth/oauth2/OAuth2AuthenticationSuccessHandler.java b/src/main/java/com/security/config/auth/oauth2/OAuth2AuthenticationSuccessHandler.java new file mode 100644 index 0000000..f8db052 --- /dev/null +++ b/src/main/java/com/security/config/auth/oauth2/OAuth2AuthenticationSuccessHandler.java @@ -0,0 +1,89 @@ +package com.security.config.auth.oauth2; + +import com.security.entity.UserEntity; +import com.security.services.AuthService; +import com.security.services.CookieService; +import com.security.services.oauth2.CustomOAuth2User; +import com.security.services.oauth2.CustomOidcUser; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; +import org.springframework.web.util.UriComponentsBuilder; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +@Slf4j +public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { + + private final AuthService authService; + private final CookieService cookieService; + + @Override + public void onAuthenticationSuccess( + HttpServletRequest request, + HttpServletResponse response, + Authentication authentication) throws IOException { + + Object principal = authentication.getPrincipal(); + UserEntity user = null; + + if (principal instanceof CustomOidcUser customOidcUser) { + user = customOidcUser.getUser(); + log.info("✅ Usuario obtenido desde CustomOidcUser: {}", user.getEmail()); + } else if (principal instanceof CustomOAuth2User customOAuth2User) { + user = customOAuth2User.getUser(); + log.info("✅ Usuario obtenido desde CustomOAuth2User: {}", user.getEmail()); + } else { + log.error("❌ Principal NO es CustomOAuth2User ni CustomOidcUser. Tipo: {}", principal.getClass().getName()); + if (principal instanceof OAuth2User oauth2User) { + log.error("❌ Atributos: {}", oauth2User.getAttributes()); + } + + getRedirectStrategy().sendRedirect(request, response, + "http://localhost:5173/auth?error=oauth_user_service_failed"); + return; + } + + if (user == null) { + log.error("❌ Usuario es NULL después de obtenerlo del principal"); + getRedirectStrategy().sendRedirect(request, response, + "http://localhost:5173/auth?error=oauth_user_null"); + return; + } + + try { + var loginResponse = authService.createTokensForOAuth2User(user); + cookieService.setSecureTokenCookies(response, loginResponse); + + log.info("✅ Cookies establecidas para usuario OAuth2: {}", user.getEmail()); + + // ✅ Redirigir al frontend + String targetUrl = UriComponentsBuilder + .fromUriString("http://localhost:5173/auth/callback") + .queryParam("success", "true") + .build() + .toUriString(); + + if (response.isCommitted()) { + log.debug("⚠️ La respuesta ya fue enviada. No se puede redirigir a {}", targetUrl); + return; + } + + clearAuthenticationAttributes(request); + getRedirectStrategy().sendRedirect(request, response, targetUrl); + + } catch ( + Exception e) { + log.error("❌ Error generando tokens para OAuth2", e); + getRedirectStrategy().sendRedirect(request, response, + "http://localhost:5173/auth?error=token_generation_failed"); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/security/config/auth/oauth2/OAuth2Config.java b/src/main/java/com/security/config/auth/oauth2/OAuth2Config.java new file mode 100644 index 0000000..0a8e079 --- /dev/null +++ b/src/main/java/com/security/config/auth/oauth2/OAuth2Config.java @@ -0,0 +1,54 @@ +package com.security.config.auth.oauth2; + +import com.security.services.AuthService; +import com.security.services.CookieService; +import com.security.services.oauth2.CustomOAuth2UserService; +import com.security.services.oauth2.CustomOidcUser; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; +import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.oauth2.core.user.OAuth2User; + + +@Configuration +public class OAuth2Config { + + @Bean + public OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler( + AuthService authService, + CookieService cookieService) { + return new OAuth2AuthenticationSuccessHandler(authService, cookieService); + } + + @Bean + public OAuth2UserService customOidcUserService( + CustomOAuth2UserService customOAuth2UserService) { + return new OidcUserService() { + @Override + public OidcUser loadUser(OidcUserRequest userRequest) { + OAuth2UserRequest oauth2UserRequest = new OAuth2UserRequest( + userRequest.getClientRegistration(), + userRequest.getAccessToken(), + userRequest.getAdditionalParameters() + ); + OAuth2User oauth2User = customOAuth2UserService.loadUser(oauth2UserRequest); + if (oauth2User instanceof com.security.services.oauth2.CustomOAuth2User customUser) { + return new CustomOidcUser(customUser, userRequest.getIdToken()); + } + return super.loadUser(userRequest); + } + }; + } + + @Bean + public AuthorizationRequestRepository authorizationRequestRepository() { + return new HttpSessionOAuth2AuthorizationRequestRepository(); + } +} \ No newline at end of file diff --git a/src/main/java/com/security/config/kafka/KafkaProducerConfig.java b/src/main/java/com/security/config/kafka/KafkaProducerConfig.java new file mode 100644 index 0000000..697ba3c --- /dev/null +++ b/src/main/java/com/security/config/kafka/KafkaProducerConfig.java @@ -0,0 +1,76 @@ +package com.security.config.kafka; + +import lombok.extern.slf4j.Slf4j; +import org.apache.kafka.clients.admin.NewTopic; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.common.serialization.StringSerializer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.config.TopicBuilder; +import org.springframework.kafka.core.DefaultKafkaProducerFactory; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.core.ProducerFactory; +import org.springframework.kafka.support.serializer.JsonSerializer; + +import java.util.HashMap; +import java.util.Map; + +@Configuration +@Slf4j +public class KafkaProducerConfig { + + @Value("${spring.kafka.producer.bootstrap-servers}") + private String bootstrapServers; + @Value("${spring.kafka.producer.acks}") + private String acks; + @Value("${spring.kafka.producer.properties.delivery.timeout.ms}") + private String deliveryTimeout; + @Value("${spring.kafka.producer.properties.linger.ms}") + private String linger; + @Value("${spring.kafka.producer.properties.request.timeout.ms}") + private String requestTimeout; + + @Value("${spring.kafka.producer.properties.enable.idempotence}") + private boolean idempotence; + @Value("${spring.kafka.producer.properties.max.in.flight.requests.per.connection:5}") + private Integer inflightRequests; + + Map producerConfigs() { + Map config = new HashMap<>(); + + config.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + config.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + config.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class); + config.put(ProducerConfig.ACKS_CONFIG, acks); + config.put(ProducerConfig.DELIVERY_TIMEOUT_MS_CONFIG, deliveryTimeout); + config.put(ProducerConfig.LINGER_MS_CONFIG, linger); + config.put(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG, requestTimeout); + config.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, idempotence); + config.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, inflightRequests); + config.put(JsonSerializer.TYPE_MAPPINGS, "NotificationEvent:com.security.events.notification.NotificationEvent,CreatedUserEvent:com.security.events.notification.CreatedUserEvent"); + config.put(ProducerConfig.RETRIES_CONFIG, 10); + return config; + } + + @Bean + ProducerFactory producerFactory() { + return new DefaultKafkaProducerFactory<>(producerConfigs()); + } + + @Bean + public KafkaTemplate kafkaTemplate() { + return new KafkaTemplate(producerFactory()); + } + + @Bean + NewTopic createNotificationTopic() { + return TopicBuilder + .name("user-created-event-topic") + .partitions(1) + .replicas(1) + .configs(Map.of("min.insync.replicas", "1")) + .build(); + } + +} diff --git a/src/main/java/com/security/controllers/AdminUserController.java b/src/main/java/com/security/controllers/AdminUserController.java new file mode 100644 index 0000000..70b4e94 --- /dev/null +++ b/src/main/java/com/security/controllers/AdminUserController.java @@ -0,0 +1,72 @@ +package com.security.controllers; + +import com.security.dtos.auth.AuthResponseDTO; +import com.security.dtos.autorization.RoleChangeRequestDTO; +import com.security.dtos.autorization.RolesResponseDTO; +import com.security.annotations.AdminAccess; +import com.security.services.UserAccountService; +import com.security.services.UserRoleService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.*; + +import java.util.UUID; + +@RestController +@RequestMapping("/admin/users") +@RequiredArgsConstructor +@Tag(name = "Autorizacion", description = "Endpoints para manejo de roles") +public class AdminUserController { + + private final UserRoleService userRoleService; + private final UserAccountService userAccountService; + + @Operation(summary = "Listar roles de usuario") + @GetMapping("/{id}/roles") + @AdminAccess + public ResponseEntity getRoles(@PathVariable UUID id) { + return ResponseEntity.ok(userRoleService.getUserRoles(id)); + } + + + @Operation(summary = "Agregar un rol a un usuario") + @PostMapping("/{id}/roles") + @AdminAccess + public ResponseEntity addRole(@PathVariable UUID id, @Valid @RequestBody RoleChangeRequestDTO request) { + return ResponseEntity.ok(userRoleService.addRoleToUser(id, request.role())); + } + + @Operation(summary = "Quitar un rol a un usuario") + @DeleteMapping("/{id}/roles") + @AdminAccess + public ResponseEntity deleteRole(@PathVariable UUID id, @RequestBody RoleChangeRequestDTO requestDTO) { + return ResponseEntity.ok(userRoleService.removeRoleFromUser(id, requestDTO.role())); + } + + + @Operation(summary = "Desactivar cuenta de usuario") + @PatchMapping("/{id}/deactivate") + @AdminAccess + public ResponseEntity deactivate(@PathVariable UUID id, + @RequestParam(required = false) String reason, + Authentication authentication) { + String admin = authentication != null ? authentication.getName() : "system"; + + return ResponseEntity.ok(userAccountService.deactivateUser(id, reason, admin)); + } + + @Operation(summary = "Activar cuenta de usuario") + @PatchMapping("/{id}/activate") + @AdminAccess + public ResponseEntity activate(@PathVariable UUID id, + Authentication authentication) { + String admin = authentication != null ? authentication.getName() : "system"; + + return ResponseEntity.ok(userAccountService.activateUser(id, admin)); + } + +} diff --git a/src/main/java/com/security/controllers/AuthController.java b/src/main/java/com/security/controllers/AuthController.java index 9da0195..dac7ee2 100644 --- a/src/main/java/com/security/controllers/AuthController.java +++ b/src/main/java/com/security/controllers/AuthController.java @@ -1,12 +1,12 @@ package com.security.controllers; - -import com.security.DTOs.LoginRequestDTO; -import com.security.DTOs.LoginResponseDTO; +import com.security.dtos.auth.AuthResponseDTO; +import com.security.dtos.auth.LoginRequestDTO; +import com.security.dtos.auth.LoginResponseDTO; +import com.security.dtos.auth.RegisterRequestDto; import com.security.annotations.AuthenticatedAccess; import com.security.services.AuthService; import com.security.services.CookieService; -import com.security.services.LoginResponseService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; @@ -18,9 +18,12 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; +import java.time.Instant; import java.util.Map; + @RestController @RequestMapping("/auth") @RequiredArgsConstructor @@ -30,11 +33,11 @@ public class AuthController { private final AuthService authService; private final CookieService cookieService; - private final LoginResponseService loginResponseService; +// private final LoginResponseService loginResponseService; @Operation(summary = "Iniciar sesión con email", description = "Autentica un usuario y establece cookies seguras") @PostMapping("/login") - public ResponseEntity> login( + public ResponseEntity login( @Valid @RequestBody LoginRequestDTO loginRequest, HttpServletResponse response) { @@ -44,22 +47,21 @@ public ResponseEntity> login( LoginResponseDTO authResponse = authService.authenticateUser(loginRequest); cookieService.setSecureTokenCookies(response, authResponse); - Map safeResponse = loginResponseService.createSafeLoginResponse(authResponse); log.info("Login successful for email: {}", loginRequest.getEmail()); - return ResponseEntity.ok(safeResponse); + return ResponseEntity.ok(new AuthResponseDTO(true, "Inicio de sesion correctamente", Instant.now())); } catch ( Exception e) { log.error("Login failed for email: {}", loginRequest.getEmail(), e); return ResponseEntity.status(HttpStatus.UNAUTHORIZED) - .body(loginResponseService.createErrorResponse("Credenciales inválidas")); + .body(new AuthResponseDTO(false, "Login fallido", Instant.now())); } } @Operation(summary = "Refrescar token", description = "Renueva el access token usando el refresh token") @PostMapping("/refresh") - public ResponseEntity> refreshToken( + public ResponseEntity refreshToken( HttpServletRequest request, HttpServletResponse response) { @@ -68,26 +70,26 @@ public ResponseEntity> refreshToken( if (refreshToken == null) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED) - .body(loginResponseService.createErrorResponse("No refresh token found")); + .body(new AuthResponseDTO(false, "No ha iniciado sesion , intente iniciar sesion", Instant.now())); } LoginResponseDTO newTokens = authService.refreshToken(refreshToken); cookieService.setSecureTokenCookies(response, newTokens); - return ResponseEntity.ok(loginResponseService.createSuccessResponse("Token renovado")); + return ResponseEntity.ok(new AuthResponseDTO(true, "Token Renovado", Instant.now())); } catch ( Exception e) { - log.error("Token refresh failed", e); + log.error("Error al renovar el token", e); cookieService.clearTokenCookies(response); return ResponseEntity.status(HttpStatus.UNAUTHORIZED) - .body(loginResponseService.createErrorResponse("Token refresh failed")); + .body(new AuthResponseDTO(false, "No se pudo renovar el token", Instant.now())); } } @Operation(summary = "Cerrar sesión", description = "Limpia las cookies seguras") @PostMapping("/logout") - public ResponseEntity> logout( + public ResponseEntity logout( HttpServletRequest request, HttpServletResponse response) { @@ -98,13 +100,33 @@ public ResponseEntity> logout( } cookieService.clearTokenCookies(response); - return ResponseEntity.ok(loginResponseService.createSuccessResponse("Sesión cerrada exitosamente")); + return ResponseEntity.ok(new AuthResponseDTO(true, "Sesión cerrada exitosamente", Instant.now())); } catch ( Exception e) { log.error("Logout failed", e); cookieService.clearTokenCookies(response); - return ResponseEntity.ok(loginResponseService.createSuccessResponse("Sesión cerrada")); + return ResponseEntity.ok(new AuthResponseDTO(false, "Error al cerrar sesion", Instant.now())); + } + } + + + @PostMapping("/register") + public ResponseEntity register(@Valid @RequestBody RegisterRequestDto registerRequestDto, HttpServletResponse response) { + try { + LoginResponseDTO authResponse = authService.registerUser(registerRequestDto); + cookieService.setSecureTokenCookies(response, authResponse); + log.info("Registro correcto con el email: {}", registerRequestDto.email()); + return ResponseEntity.status(HttpStatus.CREATED).body(new AuthResponseDTO(true, "Usuario registrado correctamente", Instant.now())); + } catch ( + ResponseStatusException ex) { + log.warn("Registro fallido: {}", ex.getMessage()); + throw ex; + } catch ( + Exception e) { + log.error("Registro Fallido", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(new AuthResponseDTO(false, "Error al registrar cuenta", Instant.now())); } } diff --git a/src/main/java/com/security/controllers/HelloController.java b/src/main/java/com/security/controllers/HelloController.java index 27c73dc..64f5fb1 100644 --- a/src/main/java/com/security/controllers/HelloController.java +++ b/src/main/java/com/security/controllers/HelloController.java @@ -1,14 +1,27 @@ package com.security.controllers; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; +import com.security.services.Impl.NotificationServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +@RequestMapping("/test") @RestController +@RequiredArgsConstructor public class HelloController { + + private final NotificationServiceImpl notificationService; + @GetMapping("/saludo") - public String saludo() { - return "Hola Microservicio Security"; + public ResponseEntity saludo() { + return ResponseEntity.ok("Hola Microservicio Security"); + } + + @PostMapping("/notification") + public ResponseEntity sendNotification(@RequestBody String message) { + notificationService.sendNotification(message); + return ResponseEntity.ok("Mensaje enviado a kafka: " + message); } } diff --git a/src/main/java/com/security/dtos/auth/AuthResponseDTO.java b/src/main/java/com/security/dtos/auth/AuthResponseDTO.java new file mode 100644 index 0000000..44c65bf --- /dev/null +++ b/src/main/java/com/security/dtos/auth/AuthResponseDTO.java @@ -0,0 +1,10 @@ +package com.security.dtos.auth; + +import java.time.Instant; + +public record AuthResponseDTO( + boolean success, + String message, + Instant timestamp +) { +} diff --git a/src/main/java/com/security/DTOs/LoginRequestDTO.java b/src/main/java/com/security/dtos/auth/LoginRequestDTO.java similarity index 85% rename from src/main/java/com/security/DTOs/LoginRequestDTO.java rename to src/main/java/com/security/dtos/auth/LoginRequestDTO.java index 4524c3b..c3d28d7 100644 --- a/src/main/java/com/security/DTOs/LoginRequestDTO.java +++ b/src/main/java/com/security/dtos/auth/LoginRequestDTO.java @@ -1,8 +1,7 @@ -package com.security.DTOs; +package com.security.dtos.auth; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Size; import lombok.*; @Data diff --git a/src/main/java/com/security/DTOs/LoginResponseDTO.java b/src/main/java/com/security/dtos/auth/LoginResponseDTO.java similarity index 81% rename from src/main/java/com/security/DTOs/LoginResponseDTO.java rename to src/main/java/com/security/dtos/auth/LoginResponseDTO.java index eb1b134..c854b97 100644 --- a/src/main/java/com/security/DTOs/LoginResponseDTO.java +++ b/src/main/java/com/security/dtos/auth/LoginResponseDTO.java @@ -1,5 +1,6 @@ -package com.security.DTOs; +package com.security.dtos.auth; +import com.security.dtos.autorization.UserDTO; import lombok.*; import java.time.Instant; diff --git a/src/main/java/com/security/dtos/auth/RegisterRequestDto.java b/src/main/java/com/security/dtos/auth/RegisterRequestDto.java new file mode 100644 index 0000000..25ace29 --- /dev/null +++ b/src/main/java/com/security/dtos/auth/RegisterRequestDto.java @@ -0,0 +1,32 @@ +package com.security.dtos.auth; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; + +public record RegisterRequestDto( + @NotBlank(message = "El primer nombre es obligatorio") + @Size(max = 50, message = "El primer nombre es demasiado largo") + String firstName, + + @NotBlank(message = "El apellido es obligatorio") + @Size(max = 50, message = "El apellido es demasiado largo") + String lastName, + + @NotBlank(message = "El email es obligatorio") + @Email(message = "El formato del email no es válido") + String email, + + @NotBlank(message = "El DNI es obligatorio") + @Pattern(regexp = "\\d{6,9}", message = "El DNI debe contener sólo dígitos (6-9 digitos)") + String dni, + + @NotBlank(message = "La contraseña es obligatoria") + @Size(min = 8, message = "La contraseña debe tener al menos 8 caracteres") + String password, + @NotBlank(message = "El telefono es obligatorio") + @Size(min = 9, max = 9, message = "El telefono debe tener 9 digitos") + String phone +) { +} diff --git a/src/main/java/com/security/dtos/autorization/RoleChangeRequestDTO.java b/src/main/java/com/security/dtos/autorization/RoleChangeRequestDTO.java new file mode 100644 index 0000000..7bf0a5e --- /dev/null +++ b/src/main/java/com/security/dtos/autorization/RoleChangeRequestDTO.java @@ -0,0 +1,9 @@ +package com.security.dtos.autorization; + +import jakarta.validation.constraints.NotBlank; + +public record RoleChangeRequestDTO( + @NotBlank(message = "El nombre del rol es requerido") + String role +) { +} diff --git a/src/main/java/com/security/DTOs/RoleDTO.java b/src/main/java/com/security/dtos/autorization/RoleDTO.java similarity index 83% rename from src/main/java/com/security/DTOs/RoleDTO.java rename to src/main/java/com/security/dtos/autorization/RoleDTO.java index 55c834c..557ab6e 100644 --- a/src/main/java/com/security/DTOs/RoleDTO.java +++ b/src/main/java/com/security/dtos/autorization/RoleDTO.java @@ -1,4 +1,4 @@ -package com.security.DTOs; +package com.security.dtos.autorization; import lombok.*; diff --git a/src/main/java/com/security/dtos/autorization/RolesResponseDTO.java b/src/main/java/com/security/dtos/autorization/RolesResponseDTO.java new file mode 100644 index 0000000..807c73f --- /dev/null +++ b/src/main/java/com/security/dtos/autorization/RolesResponseDTO.java @@ -0,0 +1,10 @@ +package com.security.dtos.autorization; + +import java.util.Set; +import java.util.UUID; + +public record RolesResponseDTO( + UUID userId, + Set roles +) { +} diff --git a/src/main/java/com/security/DTOs/UserDTO.java b/src/main/java/com/security/dtos/autorization/UserDTO.java similarity index 89% rename from src/main/java/com/security/DTOs/UserDTO.java rename to src/main/java/com/security/dtos/autorization/UserDTO.java index f9e0766..60f6fea 100644 --- a/src/main/java/com/security/DTOs/UserDTO.java +++ b/src/main/java/com/security/dtos/autorization/UserDTO.java @@ -1,4 +1,4 @@ -package com.security.DTOs; +package com.security.dtos.autorization; import lombok.*; diff --git a/src/main/java/com/security/Entity/RoleEntity.java b/src/main/java/com/security/entity/RoleEntity.java similarity index 81% rename from src/main/java/com/security/Entity/RoleEntity.java rename to src/main/java/com/security/entity/RoleEntity.java index a450a3a..c45d4d8 100644 --- a/src/main/java/com/security/Entity/RoleEntity.java +++ b/src/main/java/com/security/entity/RoleEntity.java @@ -1,5 +1,6 @@ -package com.security.Entity; +package com.security.entity; +import com.security.config.audit.Audit; import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Builder; @@ -21,4 +22,6 @@ public class RoleEntity { @Column(unique = true, nullable = false) private String name; private String description; + @Embedded + private Audit audit; } diff --git a/src/main/java/com/security/Entity/UserEntity.java b/src/main/java/com/security/entity/UserEntity.java similarity index 76% rename from src/main/java/com/security/Entity/UserEntity.java rename to src/main/java/com/security/entity/UserEntity.java index 00cfec0..bdda4cc 100644 --- a/src/main/java/com/security/Entity/UserEntity.java +++ b/src/main/java/com/security/entity/UserEntity.java @@ -1,5 +1,8 @@ -package com.security.Entity; +package com.security.entity; +import com.security.config.audit.Audit; +import com.security.config.audit.AuditListener; +import com.security.enums.AuthProvider; import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Builder; @@ -14,6 +17,7 @@ import java.util.UUID; import java.util.stream.Collectors; +@EntityListeners(AuditListener.class) @Entity @Table(name = "users") @Data @@ -25,12 +29,17 @@ public class UserEntity implements UserDetails { @Id @GeneratedValue(strategy = GenerationType.UUID) private UUID id; - private String username; + @Column(unique = true) private String email; private String password; - private String firstName; - private String lastName; + @Column(unique = true) + private String googleId; + @Enumerated(EnumType.STRING) + @Builder.Default + private AuthProvider provider = AuthProvider.LOCAL; + @Embedded + private Audit audit; @Builder.Default private Boolean enabled = true; @@ -47,7 +56,11 @@ public class UserEntity implements UserDetails { private Boolean credentialsNonExpired = true; @ManyToMany(fetch = FetchType.EAGER) - @JoinTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_id")) + @JoinTable( + name = "user_roles", + joinColumns = @JoinColumn(name = "user_id"), + inverseJoinColumns = @JoinColumn(name = "role_id") + ) private Set roles; @Override diff --git a/src/main/java/com/security/enums/AuthProvider.java b/src/main/java/com/security/enums/AuthProvider.java new file mode 100644 index 0000000..7d4d603 --- /dev/null +++ b/src/main/java/com/security/enums/AuthProvider.java @@ -0,0 +1,5 @@ +package com.security.enums; + +public enum AuthProvider { + LOCAL, GOOGLE +} diff --git a/src/main/java/com/security/events/notification/CreatedUserEvent.java b/src/main/java/com/security/events/notification/CreatedUserEvent.java new file mode 100644 index 0000000..606f407 --- /dev/null +++ b/src/main/java/com/security/events/notification/CreatedUserEvent.java @@ -0,0 +1,11 @@ +package com.security.events.notification; + +public record CreatedUserEvent( + String userId, + String firstName, + String lastName, + String dni, + String phone, + String profileImageUrl +) { +} diff --git a/src/main/java/com/security/events/notification/NotificationEvent.java b/src/main/java/com/security/events/notification/NotificationEvent.java new file mode 100644 index 0000000..221f65e --- /dev/null +++ b/src/main/java/com/security/events/notification/NotificationEvent.java @@ -0,0 +1,6 @@ +package com.security.events.notification; + +public record NotificationEvent( + String message +) { +} \ No newline at end of file diff --git a/src/main/java/com/security/exceptions/AuthenticationException.java b/src/main/java/com/security/exceptions/AuthenticationException.java new file mode 100644 index 0000000..a6b18be --- /dev/null +++ b/src/main/java/com/security/exceptions/AuthenticationException.java @@ -0,0 +1,7 @@ +package com.security.exceptions; + +public class AuthenticationException extends RuntimeException { + public AuthenticationException(String message) { + super(message); + } +} diff --git a/src/main/java/com/security/exceptions/GlobalExceptionController.java b/src/main/java/com/security/exceptions/GlobalExceptionController.java index 59b3b87..08c1291 100644 --- a/src/main/java/com/security/exceptions/GlobalExceptionController.java +++ b/src/main/java/com/security/exceptions/GlobalExceptionController.java @@ -15,6 +15,7 @@ import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.server.ResponseStatusException; import org.springframework.web.servlet.NoHandlerFoundException; import java.util.Collections; @@ -174,4 +175,42 @@ public ResponseEntity> handleGenericException(Exception ex) )); } + @ExceptionHandler(UserNotFoundException.class) + public ResponseEntity handleUserNotFoundException(UserNotFoundException ex) { + ErrorResponse errorResponse = new ErrorResponse( + "USER_ERROR", + "Ha ocurrido un error con el usuario", + Collections.singletonList(ex.getMessage()) + ); + + log.warn("User not found: {}", ex.getMessage()); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse); + } + + + @ExceptionHandler(RoleNotFoundException.class) + public ResponseEntity handleRoleNotFoundException(RoleNotFoundException ex) { + ErrorResponse errorResponse = new ErrorResponse( + "USER_ERROR", + "Ha ocurrido un error con el usuario", + Collections.singletonList(ex.getMessage()) + ); + + log.warn("User not found: {}", ex.getMessage()); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse); + } + + + @ExceptionHandler(AuthenticationException.class) + public ResponseEntity handleAuthenticationException(AuthenticationException ex) { + ErrorResponse errorResponse = new ErrorResponse( + "USER_ERROR", + "Error al registrar al usuario", + Collections.singletonList(ex.getMessage()) + ); + + log.warn("User not found: {}", ex.getMessage()); + return ResponseEntity.status(HttpStatus.CONFLICT).body(errorResponse); + } + } \ No newline at end of file diff --git a/src/main/java/com/security/exceptions/RoleNotFoundException.java b/src/main/java/com/security/exceptions/RoleNotFoundException.java new file mode 100644 index 0000000..d20987b --- /dev/null +++ b/src/main/java/com/security/exceptions/RoleNotFoundException.java @@ -0,0 +1,7 @@ +package com.security.exceptions; + +public class RoleNotFoundException extends RuntimeException { + public RoleNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/com/security/exceptions/UserNotFoundException.java b/src/main/java/com/security/exceptions/UserNotFoundException.java new file mode 100644 index 0000000..0cec721 --- /dev/null +++ b/src/main/java/com/security/exceptions/UserNotFoundException.java @@ -0,0 +1,7 @@ +package com.security.exceptions; + +public class UserNotFoundException extends RuntimeException { + public UserNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/com/security/Mappers/RoleMapper.java b/src/main/java/com/security/mappers/RoleMapper.java similarity index 66% rename from src/main/java/com/security/Mappers/RoleMapper.java rename to src/main/java/com/security/mappers/RoleMapper.java index 336dc7b..4d14cbd 100644 --- a/src/main/java/com/security/Mappers/RoleMapper.java +++ b/src/main/java/com/security/mappers/RoleMapper.java @@ -1,7 +1,7 @@ -package com.security.Mappers; +package com.security.mappers; -import com.security.DTOs.RoleDTO; -import com.security.Entity.RoleEntity; +import com.security.dtos.autorization.RoleDTO; +import com.security.entity.RoleEntity; import com.security.config.MapStructConfig; import org.mapstruct.Mapper; diff --git a/src/main/java/com/security/Mappers/UserMapper.java b/src/main/java/com/security/mappers/UserMapper.java similarity index 82% rename from src/main/java/com/security/Mappers/UserMapper.java rename to src/main/java/com/security/mappers/UserMapper.java index 8edd87c..4f7d260 100644 --- a/src/main/java/com/security/Mappers/UserMapper.java +++ b/src/main/java/com/security/mappers/UserMapper.java @@ -1,7 +1,7 @@ -package com.security.Mappers; +package com.security.mappers; -import com.security.DTOs.UserDTO; -import com.security.Entity.UserEntity; +import com.security.dtos.autorization.UserDTO; +import com.security.entity.UserEntity; import com.security.config.MapStructConfig; import org.mapstruct.Mapper; import org.mapstruct.Mapping; diff --git a/src/main/java/com/security/Repository/RoleRepository.java b/src/main/java/com/security/repository/RoleRepository.java similarity index 76% rename from src/main/java/com/security/Repository/RoleRepository.java rename to src/main/java/com/security/repository/RoleRepository.java index a702e97..9b0e5c4 100644 --- a/src/main/java/com/security/Repository/RoleRepository.java +++ b/src/main/java/com/security/repository/RoleRepository.java @@ -1,6 +1,6 @@ -package com.security.Repository; +package com.security.repository; -import com.security.Entity.RoleEntity; +import com.security.entity.RoleEntity; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; diff --git a/src/main/java/com/security/Repository/UserRepository.java b/src/main/java/com/security/repository/UserRepository.java similarity index 61% rename from src/main/java/com/security/Repository/UserRepository.java rename to src/main/java/com/security/repository/UserRepository.java index e01c336..9a91359 100644 --- a/src/main/java/com/security/Repository/UserRepository.java +++ b/src/main/java/com/security/repository/UserRepository.java @@ -1,17 +1,15 @@ -package com.security.Repository; +package com.security.repository; -import com.security.Entity.UserEntity; +import com.security.entity.UserEntity; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; import java.util.UUID; public interface UserRepository extends JpaRepository { - Optional findByUsername(String username); - Optional findByEmail(String email); - boolean existsByUsername(String username); - boolean existsByEmail(String email); + + } diff --git a/src/main/java/com/security/services/AuthService.java b/src/main/java/com/security/services/AuthService.java index 144d785..9bfc407 100644 --- a/src/main/java/com/security/services/AuthService.java +++ b/src/main/java/com/security/services/AuthService.java @@ -1,15 +1,16 @@ package com.security.services; -import com.security.DTOs.LoginRequestDTO; -import com.security.DTOs.LoginResponseDTO; -import com.security.Entity.UserEntity; +import com.security.dtos.auth.LoginRequestDTO; +import com.security.dtos.auth.LoginResponseDTO; +import com.security.dtos.auth.RegisterRequestDto; +import com.security.entity.UserEntity; -public interface AuthService { +public interface AuthService { LoginResponseDTO authenticateUser(LoginRequestDTO loginRequest); - void logout(String token); - + LoginResponseDTO registerUser(RegisterRequestDto registerRequestDto); LoginResponseDTO refreshToken(String refreshToken); + LoginResponseDTO createTokensForOAuth2User(UserEntity user); } \ No newline at end of file diff --git a/src/main/java/com/security/services/CookieService.java b/src/main/java/com/security/services/CookieService.java index 2ddf616..bd9681b 100644 --- a/src/main/java/com/security/services/CookieService.java +++ b/src/main/java/com/security/services/CookieService.java @@ -1,101 +1,16 @@ package com.security.services; -import com.security.DTOs.LoginResponseDTO; -import jakarta.servlet.http.Cookie; + +import com.security.dtos.auth.LoginResponseDTO; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; - -@Service -@Slf4j -public class CookieService { - - @Value("${app.cookie.domain:localhost}") - private String cookieDomain; - - @Value("${app.cookie.secure:false}") - private boolean cookieSecure; - - @Value("${app.cookie.access-token-max-age:900}") - private int accessTokenMaxAge; - - @Value("${app.cookie.refresh-token-max-age:604800}") - private int refreshTokenMaxAge; - - - public void setSecureTokenCookies(HttpServletResponse response, LoginResponseDTO authResponse) { - setSecureCookie(response, "access_token", authResponse.getAccessToken(), accessTokenMaxAge); - - setSecureCookie(response, "refresh_token", authResponse.getRefreshToken(), refreshTokenMaxAge); - - log.debug("Secure cookies set for user: {}", authResponse.getUser().getEmail()); - } - - - private void setSecureCookie(HttpServletResponse response, String name, String value, int maxAge) { - Cookie cookie = new Cookie(name, value); - cookie.setHttpOnly(true); - cookie.setSecure(cookieSecure); - cookie.setPath("/"); - cookie.setMaxAge(maxAge); - - if (!"localhost".equals(cookieDomain)) { - cookie.setDomain(cookieDomain); - } - - response.addCookie(cookie); - - String sameSiteValue = cookieSecure ? "None" : "Strict"; - String cookieHeader = String.format("%s=%s; Path=/; HttpOnly; Max-Age=%d; SameSite=%s%s%s", - name, - value, - maxAge, - sameSiteValue, - cookieSecure ? "; Secure" : "", - !"localhost".equals(cookieDomain) ? "; Domain=" + cookieDomain : "" - ); - - response.addHeader("Set-Cookie", cookieHeader); - } - - - public void clearTokenCookies(HttpServletResponse response) { - clearCookie(response, "access_token"); - clearCookie(response, "refresh_token"); - log.debug("Token cookies cleared"); - } - - private void clearCookie(HttpServletResponse response, String name) { - Cookie cookie = new Cookie(name, ""); - cookie.setMaxAge(0); - cookie.setPath("/"); - cookie.setHttpOnly(true); - response.addCookie(cookie); - - response.addHeader("Set-Cookie", String.format("%s=; Path=/; HttpOnly; Max-Age=0", name)); - } - - - public String extractAccessTokenFromCookies(HttpServletRequest request) { - return extractTokenFromCookies(request, "access_token"); - } +public interface CookieService { + void setSecureTokenCookies(HttpServletResponse response, LoginResponseDTO authResponse); - public String extractRefreshTokenFromCookies(HttpServletRequest request) { - return extractTokenFromCookies(request, "refresh_token"); - } + String extractAccessTokenFromCookies(HttpServletRequest request); + String extractRefreshTokenFromCookies(HttpServletRequest request); - private String extractTokenFromCookies(HttpServletRequest request, String tokenName) { - if (request.getCookies() != null) { - for (Cookie cookie : request.getCookies()) { - if (tokenName.equals(cookie.getName())) { - return cookie.getValue(); - } - } - } - return null; - } + void clearTokenCookies(HttpServletResponse response); } \ No newline at end of file diff --git a/src/main/java/com/security/services/Impl/AuthServiceImpl.java b/src/main/java/com/security/services/Impl/AuthServiceImpl.java index dea12c8..f7a4bf8 100644 --- a/src/main/java/com/security/services/Impl/AuthServiceImpl.java +++ b/src/main/java/com/security/services/Impl/AuthServiceImpl.java @@ -1,14 +1,23 @@ package com.security.services.Impl; -import com.security.DTOs.LoginRequestDTO; -import com.security.DTOs.LoginResponseDTO; -import com.security.Entity.UserEntity; -import com.security.Mappers.UserMapper; -import com.security.Repository.UserRepository; -import com.security.config.AuthProperties; +import com.security.dtos.auth.LoginRequestDTO; +import com.security.dtos.auth.LoginResponseDTO; +import com.security.dtos.auth.RegisterRequestDto; +import com.security.entity.RoleEntity; +import com.security.entity.UserEntity; +import com.security.enums.AuthProvider; +import com.security.events.notification.CreatedUserEvent; +import com.security.exceptions.AuthenticationException; +import com.security.exceptions.RoleNotFoundException; +import com.security.exceptions.UserNotFoundException; +import com.security.mappers.UserMapper; +import com.security.repository.RoleRepository; +import com.security.repository.UserRepository; import com.security.services.AuthService; +import com.security.services.TokenService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.kafka.core.KafkaTemplate; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.jwt.*; @@ -19,7 +28,7 @@ import java.time.temporal.ChronoUnit; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; + @Service @RequiredArgsConstructor @@ -27,13 +36,14 @@ @Transactional public class AuthServiceImpl implements AuthService { - private final AuthProperties authProperties; - private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; + private final RoleRepository roleRepository; private final UserMapper userMapper; private final JwtEncoder jwtEncoder; private final JwtDecoder jwtDecoder; + private final KafkaTemplate kafkaTemplate; + private final TokenService tokenService; private final Set validRefreshTokens = ConcurrentHashMap.newKeySet(); @@ -44,7 +54,7 @@ public LoginResponseDTO authenticateUser(LoginRequestDTO request) { log.info("Authenticating user: {}", request.getEmail()); UserEntity user = userRepository.findByEmail(request.getEmail()) - .orElseThrow(() -> new BadCredentialsException("Usuario no encontrado")); + .orElseThrow(() -> new UserNotFoundException("Usuario no encontrado")); if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) { throw new BadCredentialsException("Credenciales inválidas"); @@ -54,8 +64,8 @@ public LoginResponseDTO authenticateUser(LoginRequestDTO request) { throw new BadCredentialsException("Usuario deshabilitado"); } - String accessToken = generateAccessToken(user); - String refreshToken = generateRefreshToken(user); + String accessToken = tokenService.generateAccessToken(user); + String refreshToken = tokenService.generateRefreshToken(user); validRefreshTokens.add(refreshToken); @@ -83,7 +93,7 @@ public LoginResponseDTO refreshToken(String refreshToken) { String email = jwt.getSubject(); UserEntity user = userRepository.findByEmail(email) - .orElseThrow(() -> new BadCredentialsException("Usuario no encontrado")); + .orElseThrow(() -> new UserNotFoundException("Usuario no encontrado")); if (!user.isEnabled()) { throw new BadCredentialsException("Usuario deshabilitado"); @@ -91,8 +101,8 @@ public LoginResponseDTO refreshToken(String refreshToken) { validRefreshTokens.remove(refreshToken); - String newAccessToken = generateAccessToken(user); - String newRefreshToken = generateRefreshToken(user); + String newAccessToken = tokenService.generateAccessToken(user); + String newRefreshToken = tokenService.generateRefreshToken(user); validRefreshTokens.add(newRefreshToken); @@ -113,6 +123,25 @@ public LoginResponseDTO refreshToken(String refreshToken) { } } + @Override + public LoginResponseDTO createTokensForOAuth2User(UserEntity user) { + log.info("Creando tokens para usuarios OAuth2: {}", user.getEmail()); + + String accessToken = tokenService.generateAccessToken(user); + String refreshToken = tokenService.generateRefreshToken(user); + + validRefreshTokens.add(refreshToken); + return LoginResponseDTO.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .tokenType("Bearer") + .expiresAt(Instant.now().plus(15, ChronoUnit.MINUTES)) + .scope("read write") + .user(userMapper.toDTO(user)) + .message("Login OAuth2 exitoso") + .build(); + } + @Override public void logout(String accessToken) { log.info("Logging out user"); @@ -141,47 +170,49 @@ public void logout(String accessToken) { } - private String generateRefreshToken(UserEntity user) { - Instant now = Instant.now(); + @Override + public LoginResponseDTO registerUser(RegisterRequestDto registerRequestDto) { + if (userRepository.existsByEmail(registerRequestDto.email())) { + throw new AuthenticationException("Error al registrar usuario intente de nuevo"); + } - JwtClaimsSet claims = JwtClaimsSet.builder() - .issuer(authProperties.getServer().getIssuer()) - .issuedAt(now) - .expiresAt(now.plus(7, ChronoUnit.DAYS)) - .subject(user.getEmail()) - .claim("type", "refresh") - .claim("user_id", user.getId().toString()) - .build(); - return jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue(); - } + RoleEntity userRole = roleRepository.findByName("USER").orElseThrow(() -> new RoleNotFoundException("Error al encontrar el rol USER")); - public boolean isTokenBlacklisted(String token) { - return blacklistedTokens.contains(token); - } - private String generateAccessToken(UserEntity user) { - Instant now = Instant.now(); - - String authorities = user.getRoles().stream() - .map(role -> "ROLE_" + role.getName()) - .collect(Collectors.joining(" ")); - - JwtClaimsSet claims = JwtClaimsSet.builder() - .issuer(authProperties.getServer().getIssuer()) - .issuedAt(now) - .expiresAt(now.plus(15, ChronoUnit.MINUTES)) - .subject(user.getEmail()) - .claim("scope", "read write") - .claim("authorities", authorities) - .claim("user_id", user.getId().toString()) - .claim("username", user.getUsername()) - .claim("email", user.getEmail()) - .claim("firstName", user.getFirstName()) - .claim("lastName", user.getLastName()) + UserEntity user = UserEntity.builder() + .email(registerRequestDto.email()) + .password(passwordEncoder.encode(registerRequestDto.password())) + .provider(AuthProvider.LOCAL) + .roles(Set.of(userRole)) + .enabled(true) .build(); - return jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue(); + userRepository.save(user); + + CreatedUserEvent event = new CreatedUserEvent( + user.getId().toString(), + registerRequestDto.firstName(), + registerRequestDto.lastName(), + registerRequestDto.dni(), + registerRequestDto.phone(), + null + ); + + log.info("Enviando evento {}", event); + kafkaTemplate.send("user-created-event-topic", event); + log.info("Evento enviado {}", event); + + return LoginResponseDTO.builder() + .accessToken(tokenService.generateAccessToken(user)) + .refreshToken(tokenService.generateRefreshToken(user)) + .tokenType("Bearer") + .expiresAt(Instant.now().plus(15, ChronoUnit.MINUTES)) + .scope("read write") + .user(userMapper.toDTO(user)) + .message("Registro exitoso") + .build(); } + } \ No newline at end of file diff --git a/src/main/java/com/security/services/Impl/CookieServiceImpl.java b/src/main/java/com/security/services/Impl/CookieServiceImpl.java new file mode 100644 index 0000000..5f96d22 --- /dev/null +++ b/src/main/java/com/security/services/Impl/CookieServiceImpl.java @@ -0,0 +1,110 @@ +package com.security.services.Impl; + +import com.security.dtos.auth.LoginResponseDTO; +import com.security.services.CookieService; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class CookieServiceImpl implements CookieService { + @Value("${app.cookie.domain:localhost}") + private String cookieDomain; + + @Value("${app.cookie.secure:false}") + private boolean cookieSecure; + + @Value("${app.cookie.access-token-max-age:900}") + private int accessTokenMaxAge; + + @Value("${app.cookie.refresh-token-max-age:604800}") + private int refreshTokenMaxAge; + + @Override + public void setSecureTokenCookies(HttpServletResponse response, LoginResponseDTO authResponse) { + setSecureCookie(response, "access_token", authResponse.getAccessToken(), accessTokenMaxAge); + + setSecureCookie(response, "refresh_token", authResponse.getRefreshToken(), refreshTokenMaxAge); + + log.debug("Secure cookies set for user: {}", authResponse.getUser().getEmail()); + } + + @Override + public void clearTokenCookies(HttpServletResponse response) { + clearCookie(response, "access_token"); + clearCookie(response, "refresh_token"); + log.debug("Token cookies cleared"); + } + + @Override + public String extractAccessTokenFromCookies(HttpServletRequest request) { + return extractTokenFromCookies(request, "access_token"); + } + + @Override + public String extractRefreshTokenFromCookies(HttpServletRequest request) { + return extractTokenFromCookies(request, "refresh_token"); + } + + private String extractTokenFromCookies(HttpServletRequest request, String tokenName) { + if (request.getCookies() != null) { + for (Cookie cookie : request.getCookies()) { + if (tokenName.equals(cookie.getName())) { + return cookie.getValue(); + } + } + } + return null; + } + + private void setSecureCookie(HttpServletResponse response, String name, String value, int maxAge) { + Cookie cookie = new Cookie(name, value); + cookie.setHttpOnly(true); + cookie.setSecure(cookieSecure); + cookie.setPath("/"); + cookie.setMaxAge(maxAge); + if (!"localhost".equals(cookieDomain)) { + cookie.setDomain(cookieDomain); + } + response.addCookie(cookie); + log.debug("Cookie '{}' establecida con maxAge={}, secure={}, path={}, domain={}", + name, maxAge, cookieSecure, cookie.getPath(), cookie.getDomain()); + /** + * SameSite previene ataques de CSRF -> Cross Site Request Forgery + * + * Strict -> Las cookies solo se envian en solicitudes realizadas desde el mismo dominio, no se recomineda + * si el front y el back esta en diferentes dominios + * + * Lax -> Las cookies se envian en solicitudes de navegador a nivel superior (no fetch en js), adecuada para mayoria de casos + * + * None -> Las cookies se envian en toda las peticiones incluidas las de cross-origin , requiere que la cookie tenga el atributo Secure habilitado osea + * HTTPS , util si el dominio del back y front es diferente + */ + String sameSiteValue = cookieSecure ? "None" : "Lax"; + + String cookieHeader = String.format("%s=%s; Path=/; HttpOnly; Max-Age=%d; SameSite=%s%s%s", + name, + value, + maxAge, + sameSiteValue, + cookieSecure ? "; Secure" : "", + !"localhost".equals(cookieDomain) ? "; Domain=" + cookieDomain : ""); + + response.addHeader("Set-Cookie", cookieHeader); + } + + private void clearCookie(HttpServletResponse response, String name) { + Cookie cookie = new Cookie(name, ""); + cookie.setMaxAge(0); + cookie.setPath("/"); + cookie.setHttpOnly(true); + response.addCookie(cookie); + + response.addHeader("Set-Cookie", String.format("%s=; Path=/; HttpOnly; Max-Age=0", name)); + } + +} diff --git a/src/main/java/com/security/services/CustomUserDetailsService.java b/src/main/java/com/security/services/Impl/CustomUserDetailsService.java similarity index 60% rename from src/main/java/com/security/services/CustomUserDetailsService.java rename to src/main/java/com/security/services/Impl/CustomUserDetailsService.java index d3ecdae..b7f4885 100644 --- a/src/main/java/com/security/services/CustomUserDetailsService.java +++ b/src/main/java/com/security/services/Impl/CustomUserDetailsService.java @@ -1,6 +1,6 @@ -package com.security.services; +package com.security.services.Impl; -import com.security.Repository.UserRepository; +import com.security.repository.UserRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.userdetails.UserDetails; @@ -16,14 +16,13 @@ public class CustomUserDetailsService implements UserDetailsService { private final UserRepository userRepository; @Override - public UserDetails loadUserByUsername(String emailOrUsername) throws UsernameNotFoundException { - log.debug("Loading user by email or username: {}", emailOrUsername); + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + log.debug("Loading user by email or username: {}", email); - return userRepository.findByEmail(emailOrUsername) - .or(() -> userRepository.findByUsername(emailOrUsername)) + return userRepository.findByEmail(email) .orElseThrow(() -> { - log.warn("User not found with email or username: {}", emailOrUsername); - return new UsernameNotFoundException("Usuario no encontrado: " + emailOrUsername); + log.warn("User not found with email : {}", email); + return new UsernameNotFoundException("Usuario no encontrado: " + email); }); } } diff --git a/src/main/java/com/security/services/Impl/GoogleOAuth2UserInfoImpl.java b/src/main/java/com/security/services/Impl/GoogleOAuth2UserInfoImpl.java new file mode 100644 index 0000000..e322b48 --- /dev/null +++ b/src/main/java/com/security/services/Impl/GoogleOAuth2UserInfoImpl.java @@ -0,0 +1,37 @@ +package com.security.services.Impl; + +import com.security.services.OAuth2UserInfo; +import lombok.RequiredArgsConstructor; + +import java.util.Map; + +@RequiredArgsConstructor +public class GoogleOAuth2UserInfoImpl implements OAuth2UserInfo { + + private final Map attributes; + + @Override + public String getId() { + return (String) attributes.get("sub"); + } + + @Override + public String getEmail() { + return (String) attributes.get("email"); + } + + @Override + public String getFirstName() { + return (String) attributes.get("given_name"); + } + + @Override + public String getLastName() { + return (String) attributes.get("family_name"); + } + + @Override + public String getProfileImageUrl() { + return (String) attributes.get("picture"); + } +} diff --git a/src/main/java/com/security/services/Impl/NotificationServiceImpl.java b/src/main/java/com/security/services/Impl/NotificationServiceImpl.java new file mode 100644 index 0000000..e20de32 --- /dev/null +++ b/src/main/java/com/security/services/Impl/NotificationServiceImpl.java @@ -0,0 +1,24 @@ +package com.security.services.Impl; + +import com.security.events.notification.NotificationEvent; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Slf4j +@RequiredArgsConstructor +public class NotificationServiceImpl { + private final KafkaTemplate kafkaTemplate; + + @Transactional + public void sendNotification(String message) { + log.info("Antes de publicar el mensaje"); + NotificationEvent event = new NotificationEvent(message); + kafkaTemplate.send("user-created-event-topic", event); + log.info("Mensaje enviado {}", event); + } + +} diff --git a/src/main/java/com/security/services/Impl/TokenServiceImpl.java b/src/main/java/com/security/services/Impl/TokenServiceImpl.java new file mode 100644 index 0000000..8bb1e7d --- /dev/null +++ b/src/main/java/com/security/services/Impl/TokenServiceImpl.java @@ -0,0 +1,59 @@ +package com.security.services.Impl; + +import com.security.config.auth.AuthProperties; +import com.security.entity.UserEntity; +import com.security.services.TokenService; +import lombok.RequiredArgsConstructor; +import org.springframework.security.oauth2.jwt.JwtClaimsSet; +import org.springframework.security.oauth2.jwt.JwtEncoder; +import org.springframework.security.oauth2.jwt.JwtEncoderParameters; +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class TokenServiceImpl implements TokenService { + private final JwtEncoder jwtEncoder; + private final AuthProperties authProperties; + + @Override + public String generateAccessToken(UserEntity user) { + Instant now = Instant.now(); + + String authorities = user.getRoles().stream() + .map(role -> "ROLE_" + role.getName()) + .collect(Collectors.joining(" ")); + + JwtClaimsSet claims = JwtClaimsSet.builder() + .issuer(authProperties.getServer().getIssuer()) + .issuedAt(now) + .expiresAt(now.plus(15, ChronoUnit.MINUTES)) + .subject(user.getEmail()) + .claim("scope", "read write") + .claim("authorities", authorities) + .claim("user_id", user.getId().toString()) + .claim("email", user.getEmail()) + .build(); + + return jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue(); + } + + @Override + public String generateRefreshToken(UserEntity user) { + Instant now = Instant.now(); + + JwtClaimsSet claims = JwtClaimsSet.builder() + .issuer(authProperties.getServer().getIssuer()) + .issuedAt(now) + .expiresAt(now.plus(7, ChronoUnit.DAYS)) + .subject(user.getEmail()) + .claim("type", "refresh") + .claim("user_id", user.getId().toString()) + .build(); + + return jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue(); + } +} diff --git a/src/main/java/com/security/services/Impl/UserAccountServiceImpl.java b/src/main/java/com/security/services/Impl/UserAccountServiceImpl.java new file mode 100644 index 0000000..4417ec6 --- /dev/null +++ b/src/main/java/com/security/services/Impl/UserAccountServiceImpl.java @@ -0,0 +1,73 @@ +package com.security.services.Impl; + +import com.security.dtos.auth.AuthResponseDTO; +import com.security.entity.UserEntity; +import com.security.repository.UserRepository; +import com.security.config.audit.Audit; +import com.security.services.UserAccountService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.server.ResponseStatusException; + +import java.time.Instant; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +@Transactional +public class UserAccountServiceImpl implements UserAccountService { + + private final UserRepository userRepository; + + + @Override + public AuthResponseDTO deactivateUser(UUID userId, String reason, String admin) { + UserEntity user = userRepository.findById(userId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Usuario no encontrado")); + + if (!Boolean.TRUE.equals(user.getEnabled())) + return new AuthResponseDTO(false, "Usuario ya esta desactivado", Instant.now()); + + Audit a = user.getAudit(); + if (a == null) { + a = Audit.builder().build(); + } + a.setUpdatedBy(admin); + a.setUpdatedAt(Instant.now()); + a.setStatusReason(reason); + user.setAudit(a); + user.setEnabled(false); + + userRepository.save(user); + + + return new AuthResponseDTO(true, "Usuario desactivado", Instant.now()); + + } + + + @Override + public AuthResponseDTO activateUser(UUID userId, String admin) { + UserEntity user = userRepository.findById(userId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Usuario no encontrado")); + + if (Boolean.TRUE.equals(user.getEnabled())) { + return new AuthResponseDTO(false, "Usuario ya esta activo", Instant.now()); + } + + Audit a = user.getAudit(); + if (a == null) { + a = Audit.builder().build(); + } + a.setUpdatedBy(admin); + a.setUpdatedAt(Instant.now()); + a.setStatusReason(null); + user.setAudit(a); + user.setEnabled(true); + + userRepository.save(user); + return new AuthResponseDTO(true, "Usuario activado", Instant.now()); + } +} diff --git a/src/main/java/com/security/services/Impl/UserRoleServiceImpl.java b/src/main/java/com/security/services/Impl/UserRoleServiceImpl.java new file mode 100644 index 0000000..37f814c --- /dev/null +++ b/src/main/java/com/security/services/Impl/UserRoleServiceImpl.java @@ -0,0 +1,73 @@ +package com.security.services.Impl; + +import com.security.dtos.auth.AuthResponseDTO; +import com.security.dtos.autorization.RolesResponseDTO; +import com.security.entity.RoleEntity; +import com.security.entity.UserEntity; +import com.security.repository.RoleRepository; +import com.security.repository.UserRepository; +import com.security.services.UserRoleService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.server.ResponseStatusException; + +import java.time.Instant; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +@Service +@Slf4j +@RequiredArgsConstructor +public class UserRoleServiceImpl implements UserRoleService { + + private final UserRepository userRepository; + private final RoleRepository roleRepository; + + + @Override + @Transactional(readOnly = true) + public RolesResponseDTO getUserRoles(UUID userId) { + UserEntity user = userRepository.findById(userId).orElseThrow(); + Set roles = user.getRoles().stream().map(RoleEntity::getName).collect(Collectors.toSet()); + return new RolesResponseDTO(userId, roles); + } + + @Override + @Transactional + public AuthResponseDTO addRoleToUser(UUID userId, String roleName) { + UserEntity user = userRepository.findById(userId).orElseThrow(); + + RoleEntity role = roleRepository.findByName(roleName).orElseThrow(); + if (user.getRoles() == null) { + user.setRoles(Set.of(role)); + } else if (user.getRoles().stream().noneMatch(r -> r.getName().equals(roleName))) { + HashSet mutable = new HashSet<>(user.getRoles()); + mutable.add(role); + user.setRoles(mutable); + } + userRepository.save(user); + + return new AuthResponseDTO(true, "Rol agregado", Instant.now()); + } + + @Override + @Transactional + public AuthResponseDTO removeRoleFromUser(UUID userId, String roleName) { + UserEntity user = userRepository.findById(userId).orElseThrow(); + if (user.getRoles() == null || user.getRoles().stream().noneMatch(r -> r.getName().equals(roleName))) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "El usuario no tiene ese rol"); + + } + + var mutable = new HashSet<>(user.getRoles()); + mutable.removeIf(r -> r.getName().equals(roleName)); + user.setRoles(mutable); + userRepository.save(user); + return new AuthResponseDTO(true, "Rol removido", Instant.now()); + } +} diff --git a/src/main/java/com/security/services/LoginResponseService.java b/src/main/java/com/security/services/LoginResponseService.java deleted file mode 100644 index 13f8a14..0000000 --- a/src/main/java/com/security/services/LoginResponseService.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.security.services; - -import com.security.DTOs.LoginResponseDTO; -import org.springframework.stereotype.Service; - -import java.time.Instant; -import java.util.Map; - -@Service -public class LoginResponseService { - - - public Map createSafeLoginResponse(LoginResponseDTO authResponse) { - return Map.of( - "success", true, - "message", authResponse.getMessage(), - "user", Map.of( - "id", authResponse.getUser().getId(), - "username", authResponse.getUser().getUsername(), - "email", authResponse.getUser().getEmail(), - "firstName", authResponse.getUser().getFirstName(), - "lastName", authResponse.getUser().getLastName(), - "roles", authResponse.getUser().getRoles() - ), - "expiresAt", authResponse.getExpiresAt(), - "timestamp", Instant.now() - ); - } - - - private Map createUserResponse(LoginResponseDTO authResponse) { - return Map.of( - "id", authResponse.getUser().getId(), - "username", authResponse.getUser().getUsername(), - "email", authResponse.getUser().getEmail(), - "firstName", authResponse.getUser().getFirstName(), - "lastName", authResponse.getUser().getLastName(), - "roles", authResponse.getUser().getRoles() - ); - } - - - public Map createErrorResponse(String message) { - return Map.of( - "success", false, - "message", message, - "timestamp", Instant.now() - ); - } - - public Map createSuccessResponse(String message) { - return Map.of( - "success", true, - "message", message, - "timestamp", Instant.now() - ); - } -} \ No newline at end of file diff --git a/src/main/java/com/security/services/OAuth2UserInfo.java b/src/main/java/com/security/services/OAuth2UserInfo.java new file mode 100644 index 0000000..3e42a9f --- /dev/null +++ b/src/main/java/com/security/services/OAuth2UserInfo.java @@ -0,0 +1,9 @@ +package com.security.services; + +public interface OAuth2UserInfo { + String getId(); + String getEmail(); + String getFirstName(); + String getLastName(); + String getProfileImageUrl(); +} diff --git a/src/main/java/com/security/services/TokenService.java b/src/main/java/com/security/services/TokenService.java new file mode 100644 index 0000000..b6fc140 --- /dev/null +++ b/src/main/java/com/security/services/TokenService.java @@ -0,0 +1,9 @@ +package com.security.services; + +import com.security.entity.UserEntity; + +public interface TokenService { + String generateRefreshToken(UserEntity user); + + String generateAccessToken(UserEntity user); +} diff --git a/src/main/java/com/security/services/UserAccountService.java b/src/main/java/com/security/services/UserAccountService.java new file mode 100644 index 0000000..4868ed1 --- /dev/null +++ b/src/main/java/com/security/services/UserAccountService.java @@ -0,0 +1,11 @@ +package com.security.services; + +import com.security.dtos.auth.AuthResponseDTO; + +import java.util.UUID; + +public interface UserAccountService { + AuthResponseDTO deactivateUser(UUID userId, String reason, String admin); + + AuthResponseDTO activateUser(UUID userId, String admin); +} diff --git a/src/main/java/com/security/services/UserRoleService.java b/src/main/java/com/security/services/UserRoleService.java new file mode 100644 index 0000000..1df3b26 --- /dev/null +++ b/src/main/java/com/security/services/UserRoleService.java @@ -0,0 +1,15 @@ +package com.security.services; + +import com.security.dtos.auth.AuthResponseDTO; +import com.security.dtos.autorization.RolesResponseDTO; + +import java.util.UUID; + +public interface UserRoleService { + RolesResponseDTO getUserRoles(UUID userId); + + AuthResponseDTO addRoleToUser(UUID userId, String roleName); + + AuthResponseDTO removeRoleFromUser(UUID userId, String roleName); + +} diff --git a/src/main/java/com/security/services/oauth2/CustomOAuth2User.java b/src/main/java/com/security/services/oauth2/CustomOAuth2User.java new file mode 100644 index 0000000..6239c7b --- /dev/null +++ b/src/main/java/com/security/services/oauth2/CustomOAuth2User.java @@ -0,0 +1,34 @@ +package com.security.services.oauth2; + +import com.security.entity.UserEntity; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +@Getter +@RequiredArgsConstructor +public class CustomOAuth2User implements OAuth2User { + private final UserEntity user; + private final Map attributes; + + + @Override + public Map getAttributes() { + return attributes; + } + + @Override + public Collection getAuthorities() { + return user.getAuthorities(); + } + + @Override + public String getName() { + return user.getEmail(); + } +} diff --git a/src/main/java/com/security/services/oauth2/CustomOAuth2UserService.java b/src/main/java/com/security/services/oauth2/CustomOAuth2UserService.java new file mode 100644 index 0000000..0fe8509 --- /dev/null +++ b/src/main/java/com/security/services/oauth2/CustomOAuth2UserService.java @@ -0,0 +1,169 @@ +package com.security.services.oauth2; + +import com.security.entity.RoleEntity; +import com.security.entity.UserEntity; +import com.security.enums.AuthProvider; +import com.security.events.notification.CreatedUserEvent; +import com.security.repository.RoleRepository; +import com.security.repository.UserRepository; +import com.security.services.OAuth2UserInfo; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Set; + +@Service +@RequiredArgsConstructor +@Slf4j +public class CustomOAuth2UserService extends DefaultOAuth2UserService { + + private final UserRepository userRepository; + private final RoleRepository roleRepository; + private final KafkaTemplate kafkaTemplate; + + @Override + @Transactional + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + OAuth2User oAuth2User = super.loadUser(userRequest); + + try { + OAuth2User result = processOAuth2User(userRequest, oAuth2User); + log.info("✅ CustomOAuth2UserService retornó: {}", result.getClass().getName()); + return result; + } catch (Exception ex) { + log.error("❌ Error procesando usuario OAuth2", ex); + throw new OAuth2AuthenticationException("Error procesando usuario OAuth2: " + ex.getMessage()); + } + } + + private OAuth2User processOAuth2User(OAuth2UserRequest userRequest, OAuth2User oAuth2User) { + String registrationId = userRequest.getClientRegistration().getRegistrationId(); + log.info("🔄 Procesando OAuth2 user desde: {}", registrationId); + + OAuth2UserInfo userInfo = OAuth2UserInfoFactory.getOAuth2UserInfo( + registrationId, + oAuth2User.getAttributes() + ); + + if (userInfo.getEmail() == null || userInfo.getEmail().isEmpty()) { + throw new OAuth2AuthenticationException("Email no encontrado en la respuesta de OAuth2"); + } + + log.info("📧 Email extraído: {}", userInfo.getEmail()); + + UserEntity user = userRepository.findByEmail(userInfo.getEmail()) + .map(existingUser -> { + log.info("✅ Usuario existente encontrado: {}", existingUser.getEmail()); + return updateExistingUser(existingUser, userInfo); + }) + .orElseGet(() -> { + log.info("➕ Registrando nuevo usuario: {}", userInfo.getEmail()); + return registerNewUser(userInfo, registrationId); + }); + + if (user == null) { + throw new OAuth2AuthenticationException("Usuario es NULL después de guardar/actualizar"); + } + + if (user.getId() == null) { + throw new OAuth2AuthenticationException("Usuario no tiene ID después de guardar"); + } + + log.info("✅ Usuario procesado correctamente: ID={}, Email={}", user.getId(), user.getEmail()); + + + CustomOAuth2User customUser = new CustomOAuth2User(user, oAuth2User.getAttributes()); + log.info("✅ Retornando CustomOAuth2User para: {}", user.getEmail()); + return customUser; + } + + private UserEntity registerNewUser(OAuth2UserInfo userInfo, String provider) { + log.info("📝 Registrando nuevo usuario OAuth2: {}", userInfo.getEmail()); + + RoleEntity userRole = roleRepository.findByName("USER") + .orElseThrow(() -> { + log.error("❌ Rol USER no encontrado"); + return new OAuth2AuthenticationException("Rol USER no encontrado"); + }); + + UserEntity user = UserEntity.builder() + .email(userInfo.getEmail()) + .googleId(userInfo.getId()) + .provider(AuthProvider.valueOf(provider.toUpperCase())) + .enabled(true) + .roles(Set.of(userRole)) + .build(); + + UserEntity savedUser = userRepository.save(user); + log.info("✅ Usuario guardado: ID={}, Email={}", savedUser.getId(), savedUser.getEmail()); + + try { + publishUserCreatedEvent(savedUser, userInfo); + } catch (Exception e) { + log.error("⚠️ Error publicando evento de usuario creado (no crítico)", e); + } + + return savedUser; + } + + private UserEntity updateExistingUser(UserEntity existingUser, OAuth2UserInfo userInfo) { + log.info("🔄 Actualizando usuario existente: {}", existingUser.getEmail()); + + boolean updated = false; + + if (userInfo.getId() != null && !userInfo.getId().equals(existingUser.getGoogleId())) { + existingUser.setGoogleId(userInfo.getId()); + updated = true; + } + + if (updated) { + UserEntity savedUser = userRepository.save(existingUser); + log.info("✅ Usuario actualizado: ID={}, Email={}", savedUser.getId(), savedUser.getEmail()); + + try { + publishUserUpdateEvent(savedUser, userInfo); + } catch (Exception e) { + log.error("⚠️ Error publicando evento de usuario actualizado (no crítico)", e); + } + + return savedUser; + } + + return existingUser; + } + + private void publishUserCreatedEvent(UserEntity user, OAuth2UserInfo userInfo) { + CreatedUserEvent event = new CreatedUserEvent( + user.getId().toString(), + userInfo.getFirstName(), + userInfo.getLastName(), + null, + null, + userInfo.getProfileImageUrl() + ); + + log.info("📤 Publicando evento de usuario creado: {}", event); + kafkaTemplate.send("user-created-event-topic", event); + } + + private void publishUserUpdateEvent(UserEntity user, OAuth2UserInfo userInfo) { + CreatedUserEvent event = new CreatedUserEvent( + user.getId().toString(), + userInfo.getFirstName(), + userInfo.getLastName(), + null, + null, + userInfo.getProfileImageUrl() + ); + + log.info("📤 Publicando evento de usuario actualizado: {}", event); + kafkaTemplate.send("user-updated-event-topic", event); + } +} \ No newline at end of file diff --git a/src/main/java/com/security/services/oauth2/CustomOidcUser.java b/src/main/java/com/security/services/oauth2/CustomOidcUser.java new file mode 100644 index 0000000..2afffa3 --- /dev/null +++ b/src/main/java/com/security/services/oauth2/CustomOidcUser.java @@ -0,0 +1,53 @@ +package com.security.services.oauth2; + +import com.security.entity.UserEntity; +import lombok.*; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.OidcUserInfo; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; + +import java.util.Collection; +import java.util.Map; + +@Getter +@RequiredArgsConstructor +public class CustomOidcUser implements OidcUser { + private final CustomOAuth2User customOAuth2User; + private final OidcIdToken idToken; + + @Override + public Map getClaims() { + return idToken.getClaims(); + } + + @Override + public OidcUserInfo getUserInfo() { + return null; + } + + @Override + public OidcIdToken getIdToken() { + return idToken; + } + + @Override + public Map getAttributes() { + return customOAuth2User.getAttributes(); + } + + @Override + public Collection getAuthorities() { + return customOAuth2User.getAuthorities(); + } + + @Override + public String getName() { + return customOAuth2User.getName(); + } + + public UserEntity getUser() { + return customOAuth2User.getUser(); + } + +} \ No newline at end of file diff --git a/src/main/java/com/security/services/oauth2/OAuth2UserInfoFactory.java b/src/main/java/com/security/services/oauth2/OAuth2UserInfoFactory.java new file mode 100644 index 0000000..15dc689 --- /dev/null +++ b/src/main/java/com/security/services/oauth2/OAuth2UserInfoFactory.java @@ -0,0 +1,17 @@ +package com.security.services.oauth2; + +import com.security.services.Impl.GoogleOAuth2UserInfoImpl; +import com.security.services.OAuth2UserInfo; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; + + +import java.util.Map; + +public class OAuth2UserInfoFactory { + public static OAuth2UserInfo getOAuth2UserInfo(String registrationId, Map attributes) { + if ("google".equalsIgnoreCase(registrationId)) { + return new GoogleOAuth2UserInfoImpl(attributes); + } + throw new OAuth2AuthenticationException("Login con " + registrationId + " no está soportado"); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml deleted file mode 100644 index 2ae2fcd..0000000 --- a/src/main/resources/application.yml +++ /dev/null @@ -1,9 +0,0 @@ -spring: - application: - name: msvc-security -springdoc: - api-docs: - path: /v3/api-docs - swagger-ui: - path: / - url: /v3/api-docs \ No newline at end of file diff --git a/src/main/resources/banner.txt b/src/main/resources/banner.txt new file mode 100644 index 0000000..02a980d --- /dev/null +++ b/src/main/resources/banner.txt @@ -0,0 +1,8 @@ +______ ____________ __________ _____________________________ _________________________ __ +___ |/ /_ ___/_ | / /_ ____/ __ ___/__ ____/_ ____/_ / / /__ __ \___ _/__ __/ \/ / +__ /|_/ /_____ \__ | / /_ / ____________ \__ __/ _ / _ / / /__ /_/ /__ / __ / __ / +_ / / / ____/ /__ |/ / / /___/_____/___/ /_ /___ / /___ / /_/ / _ _, _/__/ / _ / _ / +/_/ /_/ /____/ _____/ \____/ /____/ /_____/ \____/ \____/ /_/ |_| /___/ /_/ /_/ + +${application.title} ${application.version} +Powered by Spring Boot ${spring-boot.version} \ No newline at end of file