diff --git a/src/main/java/com/faf223/expensetrackerfaf/config/ApplicationConfig.java b/src/main/java/com/faf223/expensetrackerfaf/config/ApplicationConfig.java index fd41b70..e338a3d 100644 --- a/src/main/java/com/faf223/expensetrackerfaf/config/ApplicationConfig.java +++ b/src/main/java/com/faf223/expensetrackerfaf/config/ApplicationConfig.java @@ -1,10 +1,12 @@ package com.faf223.expensetrackerfaf.config; +import com.faf223.expensetrackerfaf.repository.CredentialRepository; +import com.faf223.expensetrackerfaf.repository.UserRepository; import com.faf223.expensetrackerfaf.security.PersonDetails; -import com.faf223.expensetrackerfaf.service.CredentialService; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; @@ -18,11 +20,11 @@ import org.springframework.security.crypto.password.PasswordEncoder; @RequiredArgsConstructor public class ApplicationConfig { - private final CredentialService credentialService; + private final CredentialRepository credentialRepository; @Bean public UserDetailsService userDetailsService() { - return username -> new PersonDetails(credentialService.findByEmail(username).orElseThrow((() -> new UsernameNotFoundException("User not found")))); + return username -> new PersonDetails(credentialRepository.findByEmail(username).orElseThrow(() -> new UsernameNotFoundException("User not found"))); } @Bean @@ -42,4 +44,10 @@ public class ApplicationConfig { public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } + @Bean + @Primary + public JwtAuthenticationFilter customJwtAuthenticationFilter(JwtService jwtService, UserDetailsService userDetailsService) { + return new JwtAuthenticationFilter(jwtService, userDetailsService); + } + } diff --git a/src/main/java/com/faf223/expensetrackerfaf/config/JwtAuthenticationFilter.java b/src/main/java/com/faf223/expensetrackerfaf/config/JwtAuthenticationFilter.java index 9d710a2..848021f 100644 --- a/src/main/java/com/faf223/expensetrackerfaf/config/JwtAuthenticationFilter.java +++ b/src/main/java/com/faf223/expensetrackerfaf/config/JwtAuthenticationFilter.java @@ -62,7 +62,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { response.setContentType("application/json"); ErrorResponse errorResponse = new ErrorResponse("Your session has expired. Refresh your token."); - ObjectMapper objectMapper = new ObjectMapper(); // You may need to import ObjectMapper + ObjectMapper objectMapper = new ObjectMapper(); response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); diff --git a/src/main/java/com/faf223/expensetrackerfaf/config/JwtService.java b/src/main/java/com/faf223/expensetrackerfaf/config/JwtService.java index 9cf6066..d391d56 100644 --- a/src/main/java/com/faf223/expensetrackerfaf/config/JwtService.java +++ b/src/main/java/com/faf223/expensetrackerfaf/config/JwtService.java @@ -5,6 +5,7 @@ import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; +import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Service; @@ -16,6 +17,7 @@ import java.util.Map; import java.util.function.Function; @Service +@RequiredArgsConstructor public class JwtService { @Value("${application.security.jwt.secret-key}") @@ -25,6 +27,7 @@ public class JwtService { @Value("${application.security.jwt.refresh-token.expiration}") private long refreshExpiration; + public String extractUsername(String token) { return extractClaim(token, Claims::getSubject); } diff --git a/src/main/java/com/faf223/expensetrackerfaf/config/SecurityConfiguration.java b/src/main/java/com/faf223/expensetrackerfaf/config/SecurityConfiguration.java index 6c09c65..fcbac0e 100644 --- a/src/main/java/com/faf223/expensetrackerfaf/config/SecurityConfiguration.java +++ b/src/main/java/com/faf223/expensetrackerfaf/config/SecurityConfiguration.java @@ -1,31 +1,43 @@ package com.faf223.expensetrackerfaf.config; -import lombok.RequiredArgsConstructor; +import com.faf223.expensetrackerfaf.controller.auth.JwtAuthenticationSuccessHandler; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; import org.springframework.security.authentication.AuthenticationProvider; 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.config.http.SessionCreationPolicy; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.HttpStatusEntryPoint; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import java.util.Arrays; +import static org.springframework.security.config.Customizer.withDefaults; + @Configuration @EnableWebSecurity -@RequiredArgsConstructor @EnableMethodSecurity public class SecurityConfiguration { private final JwtAuthenticationFilter jwtAuthFilter; private final AuthenticationProvider authenticationProvider; + private final ClientRegistrationRepository clientRegistrationRepository; + + public SecurityConfiguration(JwtAuthenticationFilter jwtAuthFilter, + AuthenticationProvider authenticationProvider, + ClientRegistrationRepository clientRegistrationRepository) { + this.jwtAuthFilter = jwtAuthFilter; + this.authenticationProvider = authenticationProvider; + this.clientRegistrationRepository = clientRegistrationRepository; + } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { @@ -33,15 +45,17 @@ public class SecurityConfiguration { .cors(Customizer.withDefaults()) .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(auth -> auth - .requestMatchers("/api/v1/auth/**").permitAll() .anyRequest().authenticated() ) - .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .authenticationProvider(authenticationProvider) - .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); // will be executed before UsernamePasswordAuthenticationFilter + .oauth2Login(withDefaults()); + return http.build(); } + @Bean + public JwtAuthenticationSuccessHandler jwtAuthenticationSuccessHandler() { + return new JwtAuthenticationSuccessHandler(); + } @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); @@ -54,4 +68,8 @@ public class SecurityConfiguration { return source; } + @Bean + public AuthenticationEntryPoint authenticationEntryPoint() { + return new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED); + } } diff --git a/src/main/java/com/faf223/expensetrackerfaf/controller/auth/JwtAuthenticationSuccessHandler.java b/src/main/java/com/faf223/expensetrackerfaf/controller/auth/JwtAuthenticationSuccessHandler.java new file mode 100644 index 0000000..9706cb8 --- /dev/null +++ b/src/main/java/com/faf223/expensetrackerfaf/controller/auth/JwtAuthenticationSuccessHandler.java @@ -0,0 +1,19 @@ +package com.faf223.expensetrackerfaf.controller.auth; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; + +import java.io.IOException; + +public class JwtAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler implements AuthenticationSuccessHandler { + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { + + super.onAuthenticationSuccess(request, response, authentication); + } +} + diff --git a/src/main/java/com/faf223/expensetrackerfaf/controller/auth/OAuth2SuccessController.java b/src/main/java/com/faf223/expensetrackerfaf/controller/auth/OAuth2SuccessController.java new file mode 100644 index 0000000..571207e --- /dev/null +++ b/src/main/java/com/faf223/expensetrackerfaf/controller/auth/OAuth2SuccessController.java @@ -0,0 +1,24 @@ +package com.faf223.expensetrackerfaf.controller.auth; + +import com.faf223.expensetrackerfaf.service.AuthenticationService; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class OAuth2SuccessController { + + private final AuthenticationService jwtService; + + @GetMapping() + public AuthenticationResponse getUser(@AuthenticationPrincipal OAuth2User oAuth2User) { + + AuthenticationResponse response = jwtService.register(oAuth2User); + System.out.println("Response: " + response); + + return response; + } +} \ No newline at end of file diff --git a/src/main/java/com/faf223/expensetrackerfaf/service/AuthenticationService.java b/src/main/java/com/faf223/expensetrackerfaf/service/AuthenticationService.java index 82924df..6f1bf22 100644 --- a/src/main/java/com/faf223/expensetrackerfaf/service/AuthenticationService.java +++ b/src/main/java/com/faf223/expensetrackerfaf/service/AuthenticationService.java @@ -17,6 +17,7 @@ import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Service; import java.util.Optional; @@ -30,6 +31,7 @@ public class AuthenticationService { private final PasswordEncoder passwordEncoder; private final JwtService jwtService; private final AuthenticationManager authenticationManager; + private final PasswordGenerator passwordGenerator; public AuthenticationResponse register(RegisterRequest request) { @@ -53,6 +55,56 @@ public class AuthenticationService { .build(); } + public AuthenticationResponse register(OAuth2User oAuth2User) { + String userEmail = oAuth2User.getAttribute("email"); + + // Check if the user is already registered + Optional existingCredential = credentialRepository.findByEmail(userEmail); + if (existingCredential.isPresent()) { + UserDetails userDetails = new PersonDetails(existingCredential.get()); + String jwtToken = jwtService.generateToken(userDetails); + String refreshToken = jwtService.generateRefreshToken(userDetails); + + return AuthenticationResponse.builder() + .accessToken(jwtToken) + .refreshToken(refreshToken) + .build(); + } + + + String givenName = oAuth2User.getAttribute("given_name"); + String familyName = oAuth2User.getAttribute("family_name"); + String email = oAuth2User.getAttribute("email"); + + User user = User.builder() + .firstName(givenName) + .lastName(familyName) + .username(email) + .build(); + + String randomPassword = passwordGenerator.generateRandomPassword(8); + + user.setPassword(passwordEncoder.encode(randomPassword)); + + userRepository.save(user); + Credential credential = new Credential(user, email, passwordEncoder.encode(randomPassword)); + credentialRepository.save(credential); + + UserDetails userDetails = new PersonDetails(credential); + String jwtToken = jwtService.generateToken(userDetails); + String refreshToken = jwtService.generateRefreshToken(userDetails); + + System.out.println("New user: " + user); + System.out.println("New credentials: " + credential); + + return AuthenticationResponse.builder() + .accessToken(jwtToken) + .refreshToken(refreshToken) + .build(); + } + + + public AuthenticationResponse authenticate(AuthenticationRequest request) { authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword())); diff --git a/src/main/java/com/faf223/expensetrackerfaf/service/PasswordGenerator.java b/src/main/java/com/faf223/expensetrackerfaf/service/PasswordGenerator.java new file mode 100644 index 0000000..98fc34c --- /dev/null +++ b/src/main/java/com/faf223/expensetrackerfaf/service/PasswordGenerator.java @@ -0,0 +1,18 @@ +package com.faf223.expensetrackerfaf.service; + +import org.springframework.stereotype.Service; + +import java.security.SecureRandom; +import java.util.Base64; + +@Service +public class PasswordGenerator { + + private final SecureRandom secureRandom = new SecureRandom(); + + public String generateRandomPassword(int length) { + byte[] randomBytes = new byte[length]; + secureRandom.nextBytes(randomBytes); + return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes); + } +}