10 Commits

Author SHA1 Message Date
mirrerror
d90cf83d98 remove unnecessary parameters 2023-12-01 21:16:47 +02:00
mirrerror
ccf43a50e8 add extrapolation and interpolation 2023-12-01 21:06:23 +02:00
Dmitrii Cravcenco
0fcacc3e88 Merge pull request #39 from lumijiez/dimas_timedebug
Transaction filter hotfix
2023-11-28 19:40:54 +02:00
Dima
71448a6c21 Merge pull request #38 from lumijiez/security_branch
Security branch
2023-11-27 08:30:52 +02:00
Dmitrii Cravcenco
fddd02b9ce Clean security code 2023-11-22 08:40:45 +02:00
Dmitrii Cravcenco
3bf3f92551 Merge master 2023-11-22 08:14:16 +02:00
Dmitrii Cravcenco
fa99d42bee Merge master 2023-11-21 15:39:45 +02:00
Dmitrii Cravcenco
c45cd0549f Create endpoint for google oauth, generate JWT token, save to DB by email 2023-11-21 15:36:51 +02:00
Dmitrii Cravcenco
07c9ed63ee Merge remote-tracking branch 'origin/security_branch' into security_branch
# Conflicts:
#	src/main/java/com/faf223/expensetrackerfaf/config/SecurityConfiguration.java
2023-11-20 18:14:33 +02:00
Dmitrii Cravcenco
d03e256425 Add starter redirection for github/google oauth2 2023-11-20 13:42:39 +02:00
15 changed files with 311 additions and 61 deletions

View File

@@ -78,6 +78,12 @@
<artifactId>spring-security-web</artifactId>
<version>6.1.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-math3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-math3</artifactId>
<version>3.6.1</version>
</dependency>
</dependencies>
<build>
<defaultGoal>package</defaultGoal>

View File

@@ -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);
}
}

View File

@@ -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));

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -9,11 +9,9 @@ import com.faf223.expensetrackerfaf.model.User;
import com.faf223.expensetrackerfaf.service.ExpenseCategoryService;
import com.faf223.expensetrackerfaf.service.ExpenseService;
import com.faf223.expensetrackerfaf.service.UserService;
import com.faf223.expensetrackerfaf.util.DataExtender;
import com.faf223.expensetrackerfaf.util.errors.ErrorResponse;
import com.faf223.expensetrackerfaf.util.exceptions.TransactionDoesNotBelongToTheUserException;
import com.faf223.expensetrackerfaf.util.exceptions.TransactionNotCreatedException;
import com.faf223.expensetrackerfaf.util.exceptions.TransactionNotUpdatedException;
import com.faf223.expensetrackerfaf.util.exceptions.TransactionsNotFoundException;
import com.faf223.expensetrackerfaf.util.exceptions.*;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
@@ -26,6 +24,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.Month;
import java.util.Collections;
@@ -134,6 +133,32 @@ public class ExpenseController {
throw new TransactionsNotFoundException("The expenses have not been found");
}
@GetMapping("/extend-data")
public ResponseEntity<List<BigDecimal>> extendData(@RequestParam Optional<String> extendValue,
@RequestParam Optional<Integer> extrapolationCount) {
if(extendValue.isEmpty())
throw new RequiredParamMissingException("Extend value has not been specified");
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
User user;
if (authentication != null && authentication.getPrincipal() instanceof UserDetails userDetails)
user = userService.getUserByEmail(userDetails.getUsername());
else
user = null;
if(user == null)
throw new UserNotFoundException("User with the specified UUID has not been found");
if(extendValue.get().equalsIgnoreCase("i"))
return ResponseEntity.ok(DataExtender.interpolate(user.getExpenses()));
if(extendValue.get().equalsIgnoreCase("e"))
return extrapolationCount.map(integer -> ResponseEntity.ok(DataExtender.extrapolate(user.getExpenses(), integer))).orElseGet(() -> ResponseEntity.ok(DataExtender.extrapolate(user.getExpenses(), 10)));
throw new WrongParamValueException("Wrong extend value has been specified (use either \"i\" or \"e\")");
}
@GetMapping("/categories")
public ResponseEntity<List<ExpenseCategory>> getAllCategories() {
List<ExpenseCategory> categories = expenseCategoryService.getAllCategories();

View File

@@ -12,72 +12,47 @@ public class GlobalExceptionHandler {
@ExceptionHandler
private ResponseEntity<ErrorResponse> handleDoesNotBelongException(TransactionDoesNotBelongToTheUserException e) {
ErrorResponse response = new ErrorResponse(
e.getMessage(),
System.currentTimeMillis()
);
return new ResponseEntity<>(response, HttpStatus.FORBIDDEN);
return new ResponseEntity<>(new ErrorResponse(e.getMessage()), HttpStatus.FORBIDDEN);
}
@ExceptionHandler
private ResponseEntity<ErrorResponse> handleTransactionNotCreatedException(TransactionNotCreatedException e) {
ErrorResponse response = new ErrorResponse(
e.getMessage(),
System.currentTimeMillis()
);
return new ResponseEntity<>(response, HttpStatus.NOT_MODIFIED);
return new ResponseEntity<>(new ErrorResponse(e.getMessage()), HttpStatus.NOT_MODIFIED);
}
@ExceptionHandler
private ResponseEntity<ErrorResponse> handleTransactionsNotFoundException(TransactionsNotFoundException e) {
ErrorResponse response = new ErrorResponse(
e.getMessage(),
System.currentTimeMillis()
);
return new ResponseEntity<>(response, HttpStatus.NOT_FOUND);
return new ResponseEntity<>(new ErrorResponse(e.getMessage()), HttpStatus.NOT_FOUND);
}
@ExceptionHandler
private ResponseEntity<ErrorResponse> handleUserNotFoundException(UserNotFoundException e) {
ErrorResponse response = new ErrorResponse(
e.getMessage(),
System.currentTimeMillis()
);
return new ResponseEntity<>(response, HttpStatus.NOT_FOUND);
return new ResponseEntity<>(new ErrorResponse(e.getMessage()), HttpStatus.NOT_FOUND);
}
@ExceptionHandler
private ResponseEntity<ErrorResponse> handleTransactionNotUpdatedException(TransactionNotUpdatedException e) {
ErrorResponse response = new ErrorResponse(
e.getMessage(),
System.currentTimeMillis()
);
return new ResponseEntity<>(response, HttpStatus.NOT_MODIFIED);
return new ResponseEntity<>(new ErrorResponse(e.getMessage()), HttpStatus.NOT_MODIFIED);
}
@ExceptionHandler
private ResponseEntity<ErrorResponse> handleUserNotAuthenticatedException(UserNotAuthenticatedException e) {
ErrorResponse response = new ErrorResponse(
e.getMessage(),
System.currentTimeMillis()
);
return new ResponseEntity<>(response, HttpStatus.FORBIDDEN);
return new ResponseEntity<>(new ErrorResponse(e.getMessage()), HttpStatus.FORBIDDEN);
}
@ExceptionHandler
private ResponseEntity<ErrorResponse> handleUserNotCreatedException(UserNotCreatedException e) {
ErrorResponse response = new ErrorResponse(
e.getMessage(),
System.currentTimeMillis()
);
return new ResponseEntity<>(new ErrorResponse(e.getMessage()), HttpStatus.NOT_MODIFIED);
}
return new ResponseEntity<>(response, HttpStatus.NOT_MODIFIED);
@ExceptionHandler
private ResponseEntity<ErrorResponse> handleRequiredParamMissingException(RequiredParamMissingException e) {
return new ResponseEntity<>(new ErrorResponse(e.getMessage()), HttpStatus.BAD_REQUEST);
}
@ExceptionHandler
private ResponseEntity<ErrorResponse> handleWrongParamValueException(WrongParamValueException e) {
return new ResponseEntity<>(new ErrorResponse(e.getMessage()), HttpStatus.BAD_REQUEST);
}
}

View File

@@ -9,11 +9,9 @@ import com.faf223.expensetrackerfaf.model.User;
import com.faf223.expensetrackerfaf.service.IncomeCategoryService;
import com.faf223.expensetrackerfaf.service.IncomeService;
import com.faf223.expensetrackerfaf.service.UserService;
import com.faf223.expensetrackerfaf.util.DataExtender;
import com.faf223.expensetrackerfaf.util.errors.ErrorResponse;
import com.faf223.expensetrackerfaf.util.exceptions.TransactionDoesNotBelongToTheUserException;
import com.faf223.expensetrackerfaf.util.exceptions.TransactionNotCreatedException;
import com.faf223.expensetrackerfaf.util.exceptions.TransactionNotUpdatedException;
import com.faf223.expensetrackerfaf.util.exceptions.TransactionsNotFoundException;
import com.faf223.expensetrackerfaf.util.exceptions.*;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
@@ -26,6 +24,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.Month;
import java.util.Collections;
@@ -134,6 +133,32 @@ public class IncomeController {
throw new TransactionsNotFoundException("The expenses have not been found");
}
@GetMapping("/extend-data")
public ResponseEntity<List<BigDecimal>> extendData(@RequestParam Optional<String> extendValue,
@RequestParam Optional<Integer> extrapolationCount) {
if(extendValue.isEmpty())
throw new RequiredParamMissingException("Extend value has not been specified");
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
User user;
if (authentication != null && authentication.getPrincipal() instanceof UserDetails userDetails)
user = userService.getUserByEmail(userDetails.getUsername());
else
user = null;
if(user == null)
throw new UserNotFoundException("User with the specified UUID has not been found");
if(extendValue.get().equalsIgnoreCase("i"))
return ResponseEntity.ok(DataExtender.interpolate(user.getIncomes()));
if(extendValue.get().equalsIgnoreCase("e"))
return extrapolationCount.map(integer -> ResponseEntity.ok(DataExtender.extrapolate(user.getIncomes(), integer))).orElseGet(() -> ResponseEntity.ok(DataExtender.extrapolate(user.getIncomes(), 10)));
throw new WrongParamValueException("Wrong extend value has been specified (use either \"i\" or \"e\")");
}
@GetMapping("/categories")
public ResponseEntity<List<IncomeCategory>> getAllCategories() {
List<IncomeCategory> categories = incomeCategoryService.getAllCategories();

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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<Credential> 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()));

View File

@@ -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);
}
}

View File

@@ -0,0 +1,63 @@
package com.faf223.expensetrackerfaf.util;
import com.faf223.expensetrackerfaf.model.IMoneyTransaction;
import org.apache.commons.math3.analysis.interpolation.LinearInterpolator;
import org.apache.commons.math3.analysis.polynomials.PolynomialSplineFunction;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
public class DataExtender {
public static List<BigDecimal> interpolate(List<? extends IMoneyTransaction> transactions) {
List<BigDecimal> values = new ArrayList<>();
// Perform linear interpolation on the amount field
if (transactions.size() > 1) {
LinearInterpolator interpolator = new LinearInterpolator();
double[] xValues = new double[transactions.size()];
double[] yValues = new double[transactions.size()];
for (int i = 0; i < transactions.size(); i++) {
xValues[i] = i;
yValues[i] = transactions.get(i).getAmount().doubleValue();
}
PolynomialSplineFunction splineFunction = interpolator.interpolate(xValues, yValues);
// Interpolate values between the first and last data points
for (int i = 1; i < transactions.size() - 1; i++)
values.add(BigDecimal.valueOf(splineFunction.value(i + 0.5))); // Interpolate at the midpoint
}
return values;
}
public static List<BigDecimal> extrapolate(List<? extends IMoneyTransaction> transactions, int count) {
List<BigDecimal> values = new ArrayList<>();
// Perform linear extrapolation on the amount field
if (transactions.size() > 1) {
LinearInterpolator interpolator = new LinearInterpolator();
double[] xValues = new double[transactions.size()];
double[] yValues = new double[transactions.size()];
for (int i = 0; i < transactions.size(); i++) {
xValues[i] = i;
yValues[i] = transactions.get(i).getAmount().doubleValue();
}
PolynomialSplineFunction splineFunction = interpolator.interpolate(xValues, yValues);
// Extrapolate values beyond the last data point
for (int i = transactions.size(); i < transactions.size() + count; i++)
values.add(BigDecimal.valueOf(splineFunction.value(i))); // Extrapolate by extending the x-axis
}
return values;
}
}

View File

@@ -0,0 +1,7 @@
package com.faf223.expensetrackerfaf.util.exceptions;
public class RequiredParamMissingException extends RuntimeException {
public RequiredParamMissingException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,7 @@
package com.faf223.expensetrackerfaf.util.exceptions;
public class WrongParamValueException extends RuntimeException {
public WrongParamValueException(String message) {
super(message);
}
}