From d5da3e539aedb9c152c672618d353fa984f7ee9f Mon Sep 17 00:00:00 2001 From: Raydberg Gabriel Date: Mon, 6 Oct 2025 16:05:10 -0500 Subject: [PATCH] complete login with oauth2 for google --- pom.xml | 4 + .../auth/AuthorizationServerConfig.java | 77 ++++++++ .../com/security/config/auth/JwtConfig.java | 60 ++++++ .../security/config/auth/SecurityConfig.java | 187 +++++------------- .../OAuth2AuthenticationSuccessHandler.java | 89 +++++++++ .../config/auth/oauth2/OAuth2Config.java | 54 +++++ .../java/com/security/entity/UserEntity.java | 18 +- .../java/com/security/enums/AuthProvider.java | 5 + .../events/notification/CreatedUserEvent.java | 3 +- .../exceptions/AuthenticationException.java | 7 + .../exceptions/GlobalExceptionController.java | 39 ++++ .../exceptions/RoleNotFoundException.java | 7 + .../exceptions/UserNotFoundException.java | 7 + .../com/security/services/AuthService.java | 7 +- .../services/Impl/AuthServiceImpl.java | 91 ++++----- .../services/Impl/CookieServiceImpl.java | 3 +- .../Impl/GoogleOAuth2UserInfoImpl.java | 37 ++++ .../services/Impl/TokenServiceImpl.java | 59 ++++++ .../com/security/services/OAuth2UserInfo.java | 9 + .../com/security/services/TokenService.java | 9 + .../services/oauth2/CustomOAuth2User.java | 34 ++++ .../oauth2/CustomOAuth2UserService.java | 169 ++++++++++++++++ .../services/oauth2/CustomOidcUser.java | 53 +++++ .../oauth2/OAuth2UserInfoFactory.java | 17 ++ 24 files changed, 847 insertions(+), 198 deletions(-) create mode 100644 src/main/java/com/security/config/auth/AuthorizationServerConfig.java create mode 100644 src/main/java/com/security/config/auth/JwtConfig.java create mode 100644 src/main/java/com/security/config/auth/oauth2/OAuth2AuthenticationSuccessHandler.java create mode 100644 src/main/java/com/security/config/auth/oauth2/OAuth2Config.java create mode 100644 src/main/java/com/security/enums/AuthProvider.java create mode 100644 src/main/java/com/security/exceptions/AuthenticationException.java create mode 100644 src/main/java/com/security/exceptions/RoleNotFoundException.java create mode 100644 src/main/java/com/security/exceptions/UserNotFoundException.java create mode 100644 src/main/java/com/security/services/Impl/GoogleOAuth2UserInfoImpl.java create mode 100644 src/main/java/com/security/services/Impl/TokenServiceImpl.java create mode 100644 src/main/java/com/security/services/OAuth2UserInfo.java create mode 100644 src/main/java/com/security/services/TokenService.java create mode 100644 src/main/java/com/security/services/oauth2/CustomOAuth2User.java create mode 100644 src/main/java/com/security/services/oauth2/CustomOAuth2UserService.java create mode 100644 src/main/java/com/security/services/oauth2/CustomOidcUser.java create mode 100644 src/main/java/com/security/services/oauth2/OAuth2UserInfoFactory.java diff --git a/pom.xml b/pom.xml index 838e28a..37ccd51 100644 --- a/pom.xml +++ b/pom.xml @@ -153,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/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/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 index feb31da..10b62e0 100644 --- a/src/main/java/com/security/config/auth/SecurityConfig.java +++ b/src/main/java/com/security/config/auth/SecurityConfig.java @@ -1,10 +1,7 @@ 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 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; @@ -18,42 +15,29 @@ 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.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.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.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.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.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.interfaces.RSAPrivateKey; -import java.security.interfaces.RSAPublicKey; -import java.time.Duration; import java.util.ArrayList; import java.util.Collection; import java.util.Map; -import java.util.UUID; - -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity -@EnableMethodSecurity(prePostEnabled = true) +@EnableMethodSecurity @RequiredArgsConstructor public class SecurityConfig { + + private final CustomOAuth2UserService customOAuth2UserService; + @Bean @Order(1) public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { @@ -75,7 +59,13 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h @Bean @Order(2) - public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http, CookieAuthenticationFilter cookieAuthenticationFilter) throws Exception { + public SecurityFilterChain defaultSecurityFilterChain( + HttpSecurity http, + CookieAuthenticationFilter cookieAuthenticationFilter, + OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler, + OAuth2UserService customOidcUserService, + AuthorizationRequestRepository authorizationRequestRepository) throws Exception { + return http .authorizeHttpRequests(authorize -> authorize .requestMatchers( @@ -87,12 +77,25 @@ public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http, CookieA "/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()) @@ -105,33 +108,36 @@ public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http, CookieA public JwtAuthenticationConverter jwtAuthenticationConverter() { JwtGrantedAuthoritiesConverter scopeConverter = new JwtGrantedAuthoritiesConverter(); scopeConverter.setAuthorityPrefix("SCOPE_"); + JwtAuthenticationConverter converter = new JwtAuthenticationConverter(); converter.setJwtGrantedAuthoritiesConverter(jwt -> { Collection authorities = new ArrayList<>(); - Collection scopeAuth = (Collection) scopeConverter.convert(jwt); - if (scopeAuth != null) + Collection scopeAuth = scopeConverter.convert(jwt); + if (scopeAuth != null) { authorities.addAll(scopeAuth); + } Object claim = jwt.getClaims().get("authorities"); - if (claim != null) { - if (claim instanceof String) { - String[] parts = ((String) claim).trim().split("\\s+"); - for (String p : parts) { - if (!p.isBlank()) - authorities.add(new SimpleGrantedAuthority(p)); + 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) { - ((Collection) claim).forEach(o -> { - if (o != null) - authorities.add(new SimpleGrantedAuthority(o.toString())); - }); - } else if (claim instanceof Map) { - ((Map) claim).values().forEach(v -> { - if (v != null) - authorities.add(new SimpleGrantedAuthority(v.toString())); - }); } + } 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; @@ -140,93 +146,8 @@ public JwtAuthenticationConverter jwtAuthenticationConverter() { return converter; } - @Bean - public RegisteredClientRepository registeredClientRepository() { - RegisteredClient gatewayClient = 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()) - .build(); - - return new InMemoryRegisteredClientRepository(gatewayClient); - } - - @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("http://localhost:9091") - .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(); } -} +} \ 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/entity/UserEntity.java b/src/main/java/com/security/entity/UserEntity.java index b6141f3..bdda4cc 100644 --- a/src/main/java/com/security/entity/UserEntity.java +++ b/src/main/java/com/security/entity/UserEntity.java @@ -2,6 +2,7 @@ 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; @@ -28,13 +29,14 @@ 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; -// private String dni; -// private String phone; + @Column(unique = true) + private String googleId; + @Enumerated(EnumType.STRING) + @Builder.Default + private AuthProvider provider = AuthProvider.LOCAL; @Embedded private Audit audit; @@ -54,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 index c1521ab..606f407 100644 --- a/src/main/java/com/security/events/notification/CreatedUserEvent.java +++ b/src/main/java/com/security/events/notification/CreatedUserEvent.java @@ -5,6 +5,7 @@ public record CreatedUserEvent( String firstName, String lastName, String dni, - String phone + String phone, + String profileImageUrl ) { } 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/services/AuthService.java b/src/main/java/com/security/services/AuthService.java index ff0456f..9bfc407 100644 --- a/src/main/java/com/security/services/AuthService.java +++ b/src/main/java/com/security/services/AuthService.java @@ -3,17 +3,14 @@ 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 { - 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/Impl/AuthServiceImpl.java b/src/main/java/com/security/services/Impl/AuthServiceImpl.java index af37d70..f7a4bf8 100644 --- a/src/main/java/com/security/services/Impl/AuthServiceImpl.java +++ b/src/main/java/com/security/services/Impl/AuthServiceImpl.java @@ -5,27 +5,30 @@ 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.http.HttpStatus; 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.*; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.server.ResponseStatusException; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; + @Service @RequiredArgsConstructor @@ -40,6 +43,7 @@ public class AuthServiceImpl implements AuthService { private final JwtEncoder jwtEncoder; private final JwtDecoder jwtDecoder; private final KafkaTemplate kafkaTemplate; + private final TokenService tokenService; private final Set validRefreshTokens = ConcurrentHashMap.newKeySet(); @@ -50,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"); @@ -60,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); @@ -89,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"); @@ -97,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); @@ -119,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"); @@ -150,16 +173,17 @@ public void logout(String accessToken) { @Override public LoginResponseDTO registerUser(RegisterRequestDto registerRequestDto) { if (userRepository.existsByEmail(registerRequestDto.email())) { - throw new ResponseStatusException(HttpStatus.CONFLICT, "Error al inicial sesion"); + throw new AuthenticationException("Error al registrar usuario intente de nuevo"); } - RoleEntity userRole = roleRepository.findByName("USER").orElseThrow(() -> new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Error al encontrar el rol de Usuario")); + RoleEntity userRole = roleRepository.findByName("USER").orElseThrow(() -> new RoleNotFoundException("Error al encontrar el rol USER")); UserEntity user = UserEntity.builder() .email(registerRequestDto.email()) .password(passwordEncoder.encode(registerRequestDto.password())) + .provider(AuthProvider.LOCAL) .roles(Set.of(userRole)) .enabled(true) .build(); @@ -171,7 +195,8 @@ public LoginResponseDTO registerUser(RegisterRequestDto registerRequestDto) { registerRequestDto.firstName(), registerRequestDto.lastName(), registerRequestDto.dni(), - registerRequestDto.phone() + registerRequestDto.phone(), + null ); log.info("Enviando evento {}", event); @@ -179,8 +204,8 @@ public LoginResponseDTO registerUser(RegisterRequestDto registerRequestDto) { log.info("Evento enviado {}", event); return LoginResponseDTO.builder() - .accessToken(generateAccessToken(user)) - .refreshToken(generateRefreshToken(user)) + .accessToken(tokenService.generateAccessToken(user)) + .refreshToken(tokenService.generateRefreshToken(user)) .tokenType("Bearer") .expiresAt(Instant.now().plus(15, ChronoUnit.MINUTES)) .scope("read write") @@ -190,42 +215,4 @@ public LoginResponseDTO registerUser(RegisterRequestDto registerRequestDto) { } - private String generateRefreshToken(UserEntity user) { - Instant now = Instant.now(); - - JwtClaimsSet claims = JwtClaimsSet.builder() - .issuer("http://localhost:9091") - .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(); - } - - 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("http://localhost:9091") - .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()) - .build(); - - return jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue(); - } - - } \ 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 index 607207a..5f96d22 100644 --- a/src/main/java/com/security/services/Impl/CookieServiceImpl.java +++ b/src/main/java/com/security/services/Impl/CookieServiceImpl.java @@ -71,7 +71,8 @@ private void setSecureCookie(HttpServletResponse response, String name, String v 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 * 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/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/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/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"); + } +}