From fe1e7b138ec96a0b3fa5d9b89586af3996221775 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 14 Aug 2025 00:40:22 +0200 Subject: [PATCH] feature a check --- .../api/v1/AuthenticationRestController.java | 9 ++++ .../CustomOAuth2UserService.java | 49 ++++++++++++++++++ .../configuration/GlobalConfiguration.java | 4 +- .../JwtAuthenticationFilter.java | 4 +- .../OAuth2AuthenticationSuccessHandler.java | 51 +++++++++++++++++++ .../configuration/SecurityConfiguration.java | 22 +++++--- .../src/main/resources/application-dev.yml | 22 ++++++++ .../src/main/resources/application-prod.yml | 22 ++++++++ infrastructure/pom.xml | 4 ++ .../com/dh7789dev/xpeditis/dao/UserDao.java | 6 ++- .../AuthenticationJwtRepository.java | 4 +- 11 files changed, 185 insertions(+), 12 deletions(-) create mode 100644 bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/CustomOAuth2UserService.java create mode 100644 bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/OAuth2AuthenticationSuccessHandler.java diff --git a/application/src/main/java/com/dh7789dev/xpeditis/controller/api/v1/AuthenticationRestController.java b/application/src/main/java/com/dh7789dev/xpeditis/controller/api/v1/AuthenticationRestController.java index 90de943..7a95a66 100644 --- a/application/src/main/java/com/dh7789dev/xpeditis/controller/api/v1/AuthenticationRestController.java +++ b/application/src/main/java/com/dh7789dev/xpeditis/controller/api/v1/AuthenticationRestController.java @@ -11,6 +11,8 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; @@ -33,4 +35,11 @@ public class AuthenticationRestController { @RequestBody @Valid RegisterRequest request) { return ResponseEntity.ok(service.register(request)); } + + // Optional: simple endpoint to start OAuth2 authorization if frontend needs a redirect URL + @GetMapping("/oauth2/authorization") + public ResponseEntity oauth2Authorization(@RequestParam("provider") String provider) { + // Frontend should redirect to /oauth2/authorization/{provider} + return ResponseEntity.status(302).header("Location", "/oauth2/authorization/" + provider).build(); + } } diff --git a/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/CustomOAuth2UserService.java b/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/CustomOAuth2UserService.java new file mode 100644 index 0000000..9d8eee5 --- /dev/null +++ b/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/CustomOAuth2UserService.java @@ -0,0 +1,49 @@ +package com.dh7789dev.xpeditis.configuration; + +import com.dh7789dev.xpeditis.dao.UserDao; +import com.dh7789dev.xpeditis.entity.UserEntity; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +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.DefaultOAuth2User; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; + +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class CustomOAuth2UserService extends DefaultOAuth2UserService { + + private final UserDao userDao; + + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + OAuth2User oAuth2User = super.loadUser(userRequest); + + Map attributes = oAuth2User.getAttributes(); + String registrationId = userRequest.getClientRegistration().getRegistrationId(); + + String email; + if ("google".equals(registrationId)) { + email = (String) attributes.get("email"); + } else if ("linkedin".equals(registrationId)) { + email = (String) attributes.getOrDefault("email", attributes.get("emailAddress")); + } else { + throw new OAuth2AuthenticationException("Unsupported provider: " + registrationId); + } + + if (email == null) { + throw new OAuth2AuthenticationException("Email not provided by OAuth2 provider."); + } + + UserEntity user = userDao.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException("No local user bound to email: " + email)); + + return new DefaultOAuth2User(user.getAuthorities(), attributes, "email"); + } +} + + diff --git a/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/GlobalConfiguration.java b/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/GlobalConfiguration.java index f52b15e..b3bff29 100755 --- a/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/GlobalConfiguration.java +++ b/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/GlobalConfiguration.java @@ -88,8 +88,8 @@ public class GlobalConfiguration { @Bean public UserDetailsService userDetailsService() { - return username -> userDao.findByUsername(username) - .orElseThrow(() -> new UsernameNotFoundException(String.format(USER_NOT_FOUND_MSG, username))); + return identifier -> userDao.findByUsernameOrEmail(identifier) + .orElseThrow(() -> new UsernameNotFoundException(String.format(USER_NOT_FOUND_MSG, identifier))); } } diff --git a/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/JwtAuthenticationFilter.java b/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/JwtAuthenticationFilter.java index deeacf6..d71a288 100644 --- a/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/JwtAuthenticationFilter.java +++ b/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/JwtAuthenticationFilter.java @@ -45,7 +45,9 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { final String jwt; final String username; - if (request.getServletPath().contains("/api/v1/auth")) { + if (request.getServletPath().contains("/api/v1/auth") || + request.getServletPath().startsWith("/oauth2/") || + request.getServletPath().startsWith("/login/oauth2/")) { filterChain.doFilter(request, response); return; } diff --git a/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/OAuth2AuthenticationSuccessHandler.java b/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/OAuth2AuthenticationSuccessHandler.java new file mode 100644 index 0000000..17d0b81 --- /dev/null +++ b/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/OAuth2AuthenticationSuccessHandler.java @@ -0,0 +1,51 @@ +package com.dh7789dev.xpeditis.configuration; + +import com.dh7789dev.xpeditis.dao.TokenDao; +import com.dh7789dev.xpeditis.dao.UserDao; +import com.dh7789dev.xpeditis.entity.TokenEntity; +import com.dh7789dev.xpeditis.entity.UserEntity; +import com.dh7789dev.xpeditis.util.JwtUtil; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class OAuth2AuthenticationSuccessHandler implements AuthenticationSuccessHandler { + + private final UserDao userDao; + private final TokenDao tokenDao; + private final JwtUtil jwtUtil; + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { + String email = authentication.getName(); + UserEntity user = userDao.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException("User not found for email: " + email)); + + String accessToken = jwtUtil.generateToken(user); + String refreshToken = jwtUtil.generateRefreshToken(user); + + // Save token (like classic login) + TokenEntity tokenEntity = new TokenEntity() + .setUser(user) + .setToken(accessToken) + .setTokenType(TokenEntity.Type.BEARER) + .setExpired(false) + .setRevoked(false); + tokenDao.save(tokenEntity); + + response.setContentType("application/json"); + response.getWriter().write("{\"access_token\":\"" + accessToken + "\", \"refresh_token\":\"" + refreshToken + "\"}"); + response.getWriter().flush(); + } +} + + diff --git a/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/SecurityConfiguration.java b/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/SecurityConfiguration.java index e1b6d77..de51415 100755 --- a/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/SecurityConfiguration.java +++ b/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/SecurityConfiguration.java @@ -34,13 +34,15 @@ public class SecurityConfiguration { boolean csrfEnabled; private static final String ADMIN_ROLE = "ADMIN"; - private static final String ADMIN_PLATFORM_ROLE = "ADMIN_PLATFORM"; + // private static final String ADMIN_PLATFORM_ROLE = "ADMIN_PLATFORM"; private static final String API_V1_URI = "/api/v1/**"; private static final String[] WHITE_LIST_URL = { "/api/v1/auth/**", - "/actuator/health/**"}; + "/actuator/health/**", + "/oauth2/**", + "/login/oauth2/**"}; private static final String[] INTERNAL_WHITE_LIST_URL = { "/v2/api-docs", @@ -61,11 +63,17 @@ public class SecurityConfiguration { private final UserDetailsService userDetailsService; private final LogoutHandler logoutHandler; + private final CustomOAuth2UserService customOAuth2UserService; + private final OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler; @Autowired - public SecurityConfiguration(UserDetailsService userDetailsService, LogoutHandler logoutHandler) { + public SecurityConfiguration(UserDetailsService userDetailsService, LogoutHandler logoutHandler, + CustomOAuth2UserService customOAuth2UserService, + OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler) { this.userDetailsService = userDetailsService; this.logoutHandler = logoutHandler; + this.customOAuth2UserService = customOAuth2UserService; + this.oAuth2AuthenticationSuccessHandler = oAuth2AuthenticationSuccessHandler; } @Bean @@ -111,12 +119,14 @@ public class SecurityConfiguration { .sessionManagement(session -> session.sessionCreationPolicy(STATELESS)) .authenticationProvider(authenticationProvider()) .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) + .oauth2Login(oauth -> oauth + .userInfoEndpoint(userInfo -> userInfo.userService(customOAuth2UserService)) + .successHandler(oAuth2AuthenticationSuccessHandler) + ) .logout(logout -> logout .logoutUrl("/api/v1/auth/logout") .addLogoutHandler(logoutHandler) - .logoutSuccessHandler((request, - response, - authentication) -> SecurityContextHolder.clearContext())); + .logoutSuccessHandler((req, res, auth) -> SecurityContextHolder.clearContext())); return http.build(); } diff --git a/bootstrap/src/main/resources/application-dev.yml b/bootstrap/src/main/resources/application-dev.yml index ea2ec7e..5bdbf4e 100644 --- a/bootstrap/src/main/resources/application-dev.yml +++ b/bootstrap/src/main/resources/application-dev.yml @@ -47,6 +47,28 @@ spring: timeout: 3000 writetimeout: 5000 + security: + oauth2: + client: + registration: + google: + client-id: ${GOOGLE_CLIENT_ID:} + client-secret: ${GOOGLE_CLIENT_SECRET:} + scope: openid,profile,email + redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" + linkedin: + client-id: ${LINKEDIN_CLIENT_ID:} + client-secret: ${LINKEDIN_CLIENT_SECRET:} + scope: openid,profile,email + provider: linkedin + redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" + provider: + linkedin: + authorization-uri: https://www.linkedin.com/oauth/v2/authorization + token-uri: https://www.linkedin.com/oauth/v2/accessToken + user-info-uri: https://api.linkedin.com/v2/userinfo + user-name-attribute: sub + application: email: from: randommailjf@gmail.com diff --git a/bootstrap/src/main/resources/application-prod.yml b/bootstrap/src/main/resources/application-prod.yml index 1dcc1ab..085d01f 100644 --- a/bootstrap/src/main/resources/application-prod.yml +++ b/bootstrap/src/main/resources/application-prod.yml @@ -51,6 +51,28 @@ spring: timeout: 3000 writetimeout: 5000 + security: + oauth2: + client: + registration: + google: + client-id: ${GOOGLE_CLIENT_ID:} + client-secret: ${GOOGLE_CLIENT_SECRET:} + scope: openid,profile,email + redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" + linkedin: + client-id: ${LINKEDIN_CLIENT_ID:} + client-secret: ${LINKEDIN_CLIENT_SECRET:} + scope: openid,profile,email + provider: linkedin + redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" + provider: + linkedin: + authorization-uri: https://www.linkedin.com/oauth/v2/authorization + token-uri: https://www.linkedin.com/oauth/v2/accessToken + user-info-uri: https://api.linkedin.com/v2/userinfo + user-name-attribute: sub + application: email: from: contact@leblr.fr diff --git a/infrastructure/pom.xml b/infrastructure/pom.xml index d6ac811..cfe6eed 100755 --- a/infrastructure/pom.xml +++ b/infrastructure/pom.xml @@ -42,6 +42,10 @@ org.springframework.boot spring-boot-starter-security + + org.springframework.boot + spring-boot-starter-oauth2-client + org.springframework.boot spring-boot-starter-data-jpa diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/UserDao.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/UserDao.java index c033c42..5acb717 100755 --- a/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/UserDao.java +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/UserDao.java @@ -6,12 +6,16 @@ import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; import org.springframework.data.jpa.repository.Query; -import org.springframework.stereotype.Repository; public interface UserDao extends JpaRepository { @Query("SELECT u FROM UserEntity u WHERE u.username = :username") Optional findByUsername(String username); boolean existsByUsername(String username); + Optional findByEmail(String email); + boolean existsByEmail(String email); + + @Query("SELECT u FROM UserEntity u WHERE u.username = :identifier OR u.email = :identifier") + Optional findByUsernameOrEmail(String identifier); } diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/repository/AuthenticationJwtRepository.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/repository/AuthenticationJwtRepository.java index 4ef586c..3954a12 100644 --- a/infrastructure/src/main/java/com/dh7789dev/xpeditis/repository/AuthenticationJwtRepository.java +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/repository/AuthenticationJwtRepository.java @@ -33,7 +33,7 @@ public class AuthenticationJwtRepository implements AuthenticationRepository { @Override public AuthenticationResponse authenticate(AuthenticationRequest request) { - log.info("username: {}, password: {}", request.getUsername(), request.getPassword()); + log.info("identifier: {}", request.getUsername()); UsernamePasswordAuthenticationToken authToken = UsernamePasswordAuthenticationToken .unauthenticated(request.getUsername(), request.getPassword()); Authentication authentication = authenticationManager.authenticate(authToken); @@ -42,7 +42,7 @@ public class AuthenticationJwtRepository implements AuthenticationRepository { throw new UsernameNotFoundException("Failed to authenticate"); } - var userEntity = userDao.findByUsername(request.getUsername()).orElseThrow(); + var userEntity = userDao.findByUsernameOrEmail(request.getUsername()).orElseThrow(); var jwtToken = jwtUtil.generateToken(userEntity); var refreshToken = jwtUtil.generateRefreshToken(userEntity);