Merge pull request #18 from lumijiez/security_branch

Security branch
This commit was merged in pull request #18.
This commit is contained in:
DmitriiKaban
2023-10-12 13:37:01 +03:00
committed by GitHub
10 changed files with 115 additions and 27 deletions

View File

@@ -64,10 +64,9 @@
<version>0.11.5</version> <version>0.11.5</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>jakarta.validation</groupId>
<artifactId>lombok</artifactId> <artifactId>jakarta.validation-api</artifactId>
<version>1.18.20</version> <version>2.0.2</version>
<scope>provided</scope>
</dependency> </dependency>
</dependencies> </dependencies>
<build> <build>

View File

@@ -1,5 +1,8 @@
package com.faf223.expensetrackerfaf.config; package com.faf223.expensetrackerfaf.config;
import com.faf223.expensetrackerfaf.controller.auth.ErrorResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.ExpiredJwtException;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
@@ -15,7 +18,6 @@ import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException; import java.io.IOException;
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter { public class JwtAuthenticationFilter extends OncePerRequestFilter {
@@ -35,23 +37,38 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
} }
final String authHeader = request.getHeader("Authorization"); final String authHeader = request.getHeader("Authorization");
final String jwt; final String jwt;
final String userEmail; String userEmail;
if (authHeader == null || !authHeader.startsWith("Bearer ")) { if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
return; return;
} }
jwt = authHeader.substring(7); jwt = authHeader.substring(7);
try {
userEmail = jwtService.extractUsername(jwt); userEmail = jwtService.extractUsername(jwt);
if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) { if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail); UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail);
if (jwtService.isTokenValid(jwt, userDetails)) { if (jwtService.isTokenValid(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()); userDetails, null, userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource() authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
.buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken); SecurityContextHolder.getContext().setAuthentication(authToken);
} }
} }
} catch (ExpiredJwtException e) {
// Token is expired; return a custom response
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
ErrorResponse errorResponse = new ErrorResponse("TokenExpired", "Your session has expired. Please log in again.");
ObjectMapper objectMapper = new ObjectMapper(); // You may need to import ObjectMapper
response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
response.getWriter().flush();
return;
}
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
} }
} }

View File

@@ -38,6 +38,10 @@ public class JwtService {
return generateToken(new HashMap<>(), userDetails); return generateToken(new HashMap<>(), userDetails);
} }
public String generateRefreshToken(UserDetails userDetails) {
return generateRefreshToken(new HashMap<>(), userDetails);
}
public String generateToken( public String generateToken(
Map<String, Object> extraClaims, Map<String, Object> extraClaims,
UserDetails userDetails UserDetails userDetails
@@ -45,6 +49,13 @@ public class JwtService {
return buildToken(extraClaims, userDetails, jwtExpiration); return buildToken(extraClaims, userDetails, jwtExpiration);
} }
public String generateRefreshToken(
Map<String, Object> extraClaims,
UserDetails userDetails
) {
return buildToken(extraClaims, userDetails, refreshExpiration);
}
private String buildToken(Map<String, Object> extraClaims, UserDetails userDetails, long expiration) { private String buildToken(Map<String, Object> extraClaims, UserDetails userDetails, long expiration) {
return Jwts return Jwts
.builder() .builder()

View File

@@ -4,7 +4,7 @@ import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 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.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.config.http.SessionCreationPolicy;
@@ -14,7 +14,7 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
@RequiredArgsConstructor @RequiredArgsConstructor
@EnableGlobalMethodSecurity(prePostEnabled = true) @EnableMethodSecurity
public class SecurityConfiguration { public class SecurityConfiguration {
private final JwtAuthenticationFilter jwtAuthFilter; private final JwtAuthenticationFilter jwtAuthFilter;

View File

@@ -1,19 +1,17 @@
package com.faf223.expensetrackerfaf.controller.auth; package com.faf223.expensetrackerfaf.controller.auth;
import com.faf223.expensetrackerfaf.service.AuthenticationService; import com.faf223.expensetrackerfaf.service.AuthenticationService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@RestController @RestController
@RequestMapping("api/v1/auth") @RequestMapping("api/v1/auth")
@RequiredArgsConstructor
public class AuthenticationController { public class AuthenticationController {
private final AuthenticationService service; private final AuthenticationService service;
public AuthenticationController(AuthenticationService service) {
this.service = service;
}
@PostMapping("/register") @PostMapping("/register")
public ResponseEntity<AuthenticationResponse> register(@RequestBody RegisterRequest request) { public ResponseEntity<AuthenticationResponse> register(@RequestBody RegisterRequest request) {
return ResponseEntity.ok(service.register(request)); return ResponseEntity.ok(service.register(request));
@@ -23,4 +21,9 @@ public class AuthenticationController {
public ResponseEntity<AuthenticationResponse> authenticate(@RequestBody AuthenticationRequest request) { public ResponseEntity<AuthenticationResponse> authenticate(@RequestBody AuthenticationRequest request) {
return ResponseEntity.ok(service.authenticate(request)); return ResponseEntity.ok(service.authenticate(request));
} }
@PostMapping("/refreshtoken")
public ResponseEntity<AuthenticationResponse> refreshAccessToken(@RequestBody TokenRefreshRequest request) {
return ResponseEntity.ok(service.refreshAccessToken(request));
}
} }

View File

@@ -1,5 +1,6 @@
package com.faf223.expensetrackerfaf.controller.auth; package com.faf223.expensetrackerfaf.controller.auth;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
@@ -11,5 +12,8 @@ import lombok.NoArgsConstructor;
@NoArgsConstructor @NoArgsConstructor
public class AuthenticationResponse { public class AuthenticationResponse {
private String token; @JsonProperty("access_token")
private String accessToken;
@JsonProperty("refresh_token")
private String refreshToken;
} }

View File

@@ -0,0 +1,16 @@
package com.faf223.expensetrackerfaf.controller.auth;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class ErrorResponse {
private String error;
private String message;
public ErrorResponse(String error, String message) {
this.error = error;
this.message = message;
}
}

View File

@@ -0,0 +1,8 @@
package com.faf223.expensetrackerfaf.controller.auth;
import lombok.Data;
@Data
public class TokenRefreshRequest {
private String refreshToken;
}

View File

@@ -7,4 +7,6 @@ import java.util.Optional;
public interface UserRepository extends JpaRepository<User, String> { public interface UserRepository extends JpaRepository<User, String> {
Optional<User> getUserByUserUuid(String userUuid); Optional<User> getUserByUserUuid(String userUuid);
Optional<User> findByUsername(String username);
} }

View File

@@ -4,6 +4,7 @@ import com.faf223.expensetrackerfaf.config.JwtService;
import com.faf223.expensetrackerfaf.controller.auth.AuthenticationRequest; import com.faf223.expensetrackerfaf.controller.auth.AuthenticationRequest;
import com.faf223.expensetrackerfaf.controller.auth.AuthenticationResponse; import com.faf223.expensetrackerfaf.controller.auth.AuthenticationResponse;
import com.faf223.expensetrackerfaf.controller.auth.RegisterRequest; import com.faf223.expensetrackerfaf.controller.auth.RegisterRequest;
import com.faf223.expensetrackerfaf.controller.auth.TokenRefreshRequest;
import com.faf223.expensetrackerfaf.model.Credential; import com.faf223.expensetrackerfaf.model.Credential;
import com.faf223.expensetrackerfaf.model.User; import com.faf223.expensetrackerfaf.model.User;
import com.faf223.expensetrackerfaf.repository.CredentialRepository; import com.faf223.expensetrackerfaf.repository.CredentialRepository;
@@ -12,10 +13,13 @@ import com.faf223.expensetrackerfaf.security.PersonDetails;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.Optional;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class AuthenticationService { public class AuthenticationService {
@@ -38,9 +42,13 @@ public class AuthenticationService {
Credential credential = new Credential(user, request.getEmail(), passwordEncoder.encode(request.getPassword())); Credential credential = new Credential(user, request.getEmail(), passwordEncoder.encode(request.getPassword()));
credentialRepository.save(credential); credentialRepository.save(credential);
String jwtToken = jwtService.generateToken(new PersonDetails(credential)); UserDetails userDetails = new PersonDetails(credential);
String jwtToken = jwtService.generateToken(userDetails);
String refreshToken = jwtService.generateRefreshToken(userDetails);
return AuthenticationResponse.builder() return AuthenticationResponse.builder()
.token(jwtToken) .accessToken(jwtToken)
.refreshToken(refreshToken)
.build(); .build();
} }
@@ -49,10 +57,30 @@ public class AuthenticationService {
Credential credential = credentialRepository.findByEmail(request.getEmail()).orElseThrow((() -> new UsernameNotFoundException("User not found"))); Credential credential = credentialRepository.findByEmail(request.getEmail()).orElseThrow((() -> new UsernameNotFoundException("User not found")));
String jwtToken = jwtService.generateToken(new PersonDetails(credential)); UserDetails userDetails = new PersonDetails(credential);
String jwtToken = jwtService.generateToken(userDetails);
String refreshToken = jwtService.generateRefreshToken(userDetails);
return AuthenticationResponse.builder() return AuthenticationResponse.builder()
.token(jwtToken) .accessToken(jwtToken)
.refreshToken(refreshToken)
.build(); .build();
} }
public AuthenticationResponse refreshAccessToken(TokenRefreshRequest refreshRequest) {
String refreshToken = refreshRequest.getRefreshToken();
Optional<Credential> credential = credentialRepository.findByEmail(jwtService.extractUsername(refreshToken));
if (credential.isPresent()) {
UserDetails userDetails = new PersonDetails(credential.get());
String jwtToken = jwtService.generateToken(userDetails);
return AuthenticationResponse.builder()
.accessToken(jwtToken)
.refreshToken(refreshToken)
.build();
} else {
throw new RuntimeException("Invalid or expired refresh token");
}
}
} }