diff --git a/pom.xml b/pom.xml
index d55c78f..e90d662 100644
--- a/pom.xml
+++ b/pom.xml
@@ -52,6 +52,27 @@
mysql-connector-j
8.1.0
+
+ io.jsonwebtoken
+ jjwt-api
+ 0.11.5
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ 0.11.5
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ 0.11.5
+
+
+ org.projectlombok
+ lombok
+ 1.18.20
+ provided
+
diff --git a/src/main/java/com/faf223/expensetrackerfaf/config/ApplicationConfig.java b/src/main/java/com/faf223/expensetrackerfaf/config/ApplicationConfig.java
new file mode 100644
index 0000000..ad1ef47
--- /dev/null
+++ b/src/main/java/com/faf223/expensetrackerfaf/config/ApplicationConfig.java
@@ -0,0 +1,50 @@
+package com.faf223.expensetrackerfaf.config;
+
+import com.faf223.expensetrackerfaf.repository.UserRepository;
+import com.faf223.expensetrackerfaf.security.PersonDetails;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
+import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+@Configuration
+public class ApplicationConfig {
+
+ private final UserRepository repository;
+
+ @Autowired
+ public ApplicationConfig(UserRepository repository) {
+ this.repository = repository;
+ }
+
+ @Bean
+ public UserDetailsService userDetailsService() {
+ return username -> new PersonDetails(repository.findByEmail(username)
+ .orElseThrow(() -> new UsernameNotFoundException("User not found")));
+ }
+
+ @Bean
+ public AuthenticationProvider authenticationProvider() {
+ DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
+ authProvider.setUserDetailsService(userDetailsService());
+ authProvider.setPasswordEncoder(passwordEncoder());
+ return authProvider;
+ }
+
+ @Bean
+ public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
+ return config.getAuthenticationManager();
+ }
+
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+}
diff --git a/src/main/java/com/faf223/expensetrackerfaf/config/JwtAuthenticationFilter.java b/src/main/java/com/faf223/expensetrackerfaf/config/JwtAuthenticationFilter.java
new file mode 100644
index 0000000..ff21f3d
--- /dev/null
+++ b/src/main/java/com/faf223/expensetrackerfaf/config/JwtAuthenticationFilter.java
@@ -0,0 +1,68 @@
+package com.faf223.expensetrackerfaf.config;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.lang.NonNull;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+@Component
+public class JwtAuthenticationFilter extends OncePerRequestFilter {
+
+ private final JwtService jwtService;
+ private final UserDetailsService userDetailsService;
+ private final TokenRepository tokenRepository;
+
+ public JwtAuthenticationFilter(JwtService jwtService, UserDetailsService userDetailsService) {
+ this.jwtService = jwtService;
+ this.userDetailsService = userDetailsService;
+ }
+
+ @Override
+ protected void doFilterInternal(
+ @NonNull HttpServletRequest request,
+ @NonNull HttpServletResponse response,
+ @NonNull FilterChain filterChain
+ ) throws ServletException, IOException {
+ if (request.getServletPath().contains("/api/v1/auth")) {
+ filterChain.doFilter(request, response);
+ return;
+ }
+ final String authHeader = request.getHeader("Authorization");
+ final String jwt;
+ final String userEmail;
+ if (authHeader == null || !authHeader.startsWith("Bearer ")) {
+ filterChain.doFilter(request, response);
+ return;
+ }
+ jwt = authHeader.substring(7);
+ userEmail = jwtService.extractUsername(jwt);
+ if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
+ UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail);
+ var isTokenValid = tokenRepository.findByToken(jwt)
+ .map(t -> !t.isExpired() && !t.isRevoked())
+ .orElse(false);
+ if (jwtService.isTokenValid(jwt, userDetails) && isTokenValid) {
+ UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
+ userDetails,
+ null,
+ userDetails.getAuthorities()
+ );
+ authToken.setDetails(
+ new WebAuthenticationDetailsSource().buildDetails(request)
+ );
+ SecurityContextHolder.getContext().setAuthentication(authToken);
+ }
+ }
+ filterChain.doFilter(request, response);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/faf223/expensetrackerfaf/config/JwtService.java b/src/main/java/com/faf223/expensetrackerfaf/config/JwtService.java
new file mode 100644
index 0000000..ef1483b
--- /dev/null
+++ b/src/main/java/com/faf223/expensetrackerfaf/config/JwtService.java
@@ -0,0 +1,95 @@
+package com.faf223.expensetrackerfaf.config;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import io.jsonwebtoken.io.Decoders;
+import io.jsonwebtoken.security.Keys;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.stereotype.Service;
+
+import java.security.Key;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+
+@Service
+public class JwtService {
+
+ @Value("${application.security.jwt.secret-key}")
+ private String secretKey;
+ @Value("${application.security.jwt.expiration}")
+ private long jwtExpiration;
+ @Value("${application.security.jwt.refresh-token.expiration}")
+ private long refreshExpiration;
+
+ public String extractUsername(String token) {
+ return extractClaim(token, Claims::getSubject);
+ }
+
+ public T extractClaim(String token, Function claimsResolver) {
+ final Claims claims = extractAllClaims(token);
+ return claimsResolver.apply(claims);
+ }
+
+ public String generateToken(UserDetails userDetails) {
+ return generateToken(new HashMap<>(), userDetails);
+ }
+
+ public String generateToken(
+ Map extraClaims,
+ UserDetails userDetails
+ ) {
+ return buildToken(extraClaims, userDetails, jwtExpiration);
+ }
+
+ public String generateRefreshToken(
+ UserDetails userDetails
+ ) {
+ return buildToken(new HashMap<>(), userDetails, refreshExpiration);
+ }
+
+ private String buildToken(
+ Map extraClaims,
+ UserDetails userDetails,
+ long expiration
+ ) {
+ return Jwts
+ .builder()
+ .setClaims(extraClaims)
+ .setSubject(userDetails.getUsername())
+ .setIssuedAt(new Date(System.currentTimeMillis()))
+ .setExpiration(new Date(System.currentTimeMillis() + expiration))
+ .signWith(getSignInKey(), SignatureAlgorithm.HS256)
+ .compact();
+ }
+
+ public boolean isTokenValid(String token, UserDetails userDetails) {
+ final String username = extractUsername(token);
+ return (username.equals(userDetails.getUsername())) && !isTokenExpired(token);
+ }
+
+ private boolean isTokenExpired(String token) {
+ return extractExpiration(token).before(new Date());
+ }
+
+ private Date extractExpiration(String token) {
+ return extractClaim(token, Claims::getExpiration);
+ }
+
+ private Claims extractAllClaims(String token) {
+ return Jwts
+ .parserBuilder()
+ .setSigningKey(getSignInKey())
+ .build()
+ .parseClaimsJws(token)
+ .getBody();
+ }
+
+ private Key getSignInKey() {
+ byte[] keyBytes = Decoders.BASE64.decode(secretKey);
+ return Keys.hmacShaKeyFor(keyBytes);
+ }
+}
diff --git a/src/main/java/com/faf223/expensetrackerfaf/config/SecurityConfiguration.java b/src/main/java/com/faf223/expensetrackerfaf/config/SecurityConfiguration.java
new file mode 100644
index 0000000..bdf878e
--- /dev/null
+++ b/src/main/java/com/faf223/expensetrackerfaf/config/SecurityConfiguration.java
@@ -0,0 +1,40 @@
+package com.faf223.expensetrackerfaf.config;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+
+@Configuration
+@EnableWebSecurity
+public class SecurityConfiguration {
+
+ private final JwtAuthenticationFilter jwtAuthFilter;
+ private final AuthenticationProvider authenticationProvider;
+
+ @Autowired
+ public SecurityConfiguration(JwtAuthenticationFilter jwtAuthFilter, AuthenticationProvider authenticationProvider) {
+ this.jwtAuthFilter = jwtAuthFilter;
+ this.authenticationProvider = authenticationProvider;
+ }
+
+ @Bean
+ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+ http
+ .csrf(csrf -> csrf.disable())
+ .authorizeHttpRequests(auth -> auth
+ .requestMatchers("").permitAll()
+ .anyRequest().authenticated()
+ )
+ .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+ .authenticationProvider(authenticationProvider)
+ .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); // will be executed before UsernamePasswordAuthenticationFilter
+
+ return http.build();
+ }
+}
diff --git a/src/main/java/com/faf223/expensetrackerfaf/controller/auth/AuthenticationController.java b/src/main/java/com/faf223/expensetrackerfaf/controller/auth/AuthenticationController.java
new file mode 100644
index 0000000..1285ee4
--- /dev/null
+++ b/src/main/java/com/faf223/expensetrackerfaf/controller/auth/AuthenticationController.java
@@ -0,0 +1,26 @@
+package com.faf223.expensetrackerfaf.controller.auth;
+
+import com.faf223.expensetrackerfaf.service.AuthenticationService;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("api/v1/auth")
+public class AuthenticationController {
+
+ private final AuthenticationService service;
+
+ public AuthenticationController(AuthenticationService service) {
+ this.service = service;
+ }
+
+ @PostMapping("/register")
+ public ResponseEntity register(@RequestBody RegisterRequest request) {
+ return ResponseEntity.ok(service.register(request));
+ }
+
+ @PostMapping("/authenticate")
+ public ResponseEntity register(@RequestBody AuthenticationRequest request) {
+ return ResponseEntity.ok(service.authenticate(request));
+ }
+}
diff --git a/src/main/java/com/faf223/expensetrackerfaf/controller/auth/AuthenticationRequest.java b/src/main/java/com/faf223/expensetrackerfaf/controller/auth/AuthenticationRequest.java
new file mode 100644
index 0000000..63f7b1c
--- /dev/null
+++ b/src/main/java/com/faf223/expensetrackerfaf/controller/auth/AuthenticationRequest.java
@@ -0,0 +1,16 @@
+package com.faf223.expensetrackerfaf.controller.auth;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class AuthenticationRequest {
+
+ private String email;
+ private String password;
+}
diff --git a/src/main/java/com/faf223/expensetrackerfaf/controller/auth/AuthenticationResponse.java b/src/main/java/com/faf223/expensetrackerfaf/controller/auth/AuthenticationResponse.java
new file mode 100644
index 0000000..bc92552
--- /dev/null
+++ b/src/main/java/com/faf223/expensetrackerfaf/controller/auth/AuthenticationResponse.java
@@ -0,0 +1,15 @@
+package com.faf223.expensetrackerfaf.controller.auth;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class AuthenticationResponse {
+
+ private String token;
+}
diff --git a/src/main/java/com/faf223/expensetrackerfaf/controller/auth/RegisterRequest.java b/src/main/java/com/faf223/expensetrackerfaf/controller/auth/RegisterRequest.java
new file mode 100644
index 0000000..755a1e0
--- /dev/null
+++ b/src/main/java/com/faf223/expensetrackerfaf/controller/auth/RegisterRequest.java
@@ -0,0 +1,22 @@
+package com.faf223.expensetrackerfaf.controller.auth;
+
+
+import com.faf223.expensetrackerfaf.model.Role;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class RegisterRequest {
+
+ private String firstName;
+ private String lastName;
+
+ private String email;
+ private String password;
+ private Role role;
+}
diff --git a/src/main/java/com/faf223/expensetrackerfaf/model/User.java b/src/main/java/com/faf223/expensetrackerfaf/model/User.java
index 1504f28..26b23fd 100644
--- a/src/main/java/com/faf223/expensetrackerfaf/model/User.java
+++ b/src/main/java/com/faf223/expensetrackerfaf/model/User.java
@@ -1,18 +1,29 @@
package com.faf223.expensetrackerfaf.model;
+
import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
import java.util.List;
@Entity
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
@Table(name = "User")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
- private String name;
+ private String firstName;
+ private String lastName;
private String email;
private String password;
+
@Enumerated(EnumType.STRING)
private Role role;
@@ -22,71 +33,4 @@ public class User {
@OneToMany(mappedBy = "user")
private List incomes;
- public User(long id, String name, String email, String password, Role role, List expenses, List incomes) {
- this.id = id;
- this.name = name;
- this.email = email;
- this.password = password;
- this.role = role;
- this.expenses = expenses;
- this.incomes = incomes;
- }
-
- public User() {}
-
- public long getId() {
- return id;
- }
-
- public void setId(long id) {
- this.id = id;
- }
-
- public String getName() {
- return name;
- }
-
- public void setName(String name) {
- this.name = name;
- }
-
- public String getEmail() {
- return email;
- }
-
- public void setEmail(String email) {
- this.email = email;
- }
-
- public String getPassword() {
- return password;
- }
-
- public void setPassword(String password) {
- this.password = password;
- }
-
- public Role getRole() {
- return role;
- }
-
- public void setRole(Role role) {
- this.role = role;
- }
-
- public List getExpenses() {
- return expenses;
- }
-
- public void setExpenses(List expenses) {
- this.expenses = expenses;
- }
-
- public List getIncomes() {
- return incomes;
- }
-
- public void setIncomes(List incomes) {
- this.incomes = incomes;
- }
}
diff --git a/src/main/java/com/faf223/expensetrackerfaf/repository/UserRepository.java b/src/main/java/com/faf223/expensetrackerfaf/repository/UserRepository.java
index c3edd6a..3049a85 100644
--- a/src/main/java/com/faf223/expensetrackerfaf/repository/UserRepository.java
+++ b/src/main/java/com/faf223/expensetrackerfaf/repository/UserRepository.java
@@ -3,5 +3,8 @@ package com.faf223.expensetrackerfaf.repository;
import com.faf223.expensetrackerfaf.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
+import java.util.Optional;
+
public interface UserRepository extends JpaRepository {
+ Optional findByEmail(String username);
}
diff --git a/src/main/java/com/faf223/expensetrackerfaf/security/PersonDetails.java b/src/main/java/com/faf223/expensetrackerfaf/security/PersonDetails.java
index cb18bc5..74770e1 100644
--- a/src/main/java/com/faf223/expensetrackerfaf/security/PersonDetails.java
+++ b/src/main/java/com/faf223/expensetrackerfaf/security/PersonDetails.java
@@ -1,16 +1,24 @@
package com.faf223.expensetrackerfaf.security;
import com.faf223.expensetrackerfaf.model.Role;
+import com.faf223.expensetrackerfaf.model.User;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
-import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
+@Data
+@Builder
+@NoArgsConstructor(force = true)
+@AllArgsConstructor
public class PersonDetails implements UserDetails {
private final User user;
@@ -34,7 +42,7 @@ public class PersonDetails implements UserDetails {
@Override
public String getUsername() {
- return user.getUsername();
+ return user.getEmail();
}
@Override
diff --git a/src/main/java/com/faf223/expensetrackerfaf/service/AuthenticationService.java b/src/main/java/com/faf223/expensetrackerfaf/service/AuthenticationService.java
new file mode 100644
index 0000000..d253038
--- /dev/null
+++ b/src/main/java/com/faf223/expensetrackerfaf/service/AuthenticationService.java
@@ -0,0 +1,45 @@
+package com.faf223.expensetrackerfaf.service;
+
+import com.faf223.expensetrackerfaf.config.JwtService;
+import com.faf223.expensetrackerfaf.controller.auth.AuthenticationRequest;
+import com.faf223.expensetrackerfaf.controller.auth.AuthenticationResponse;
+import com.faf223.expensetrackerfaf.controller.auth.RegisterRequest;
+import com.faf223.expensetrackerfaf.model.Role;
+import com.faf223.expensetrackerfaf.model.User;
+import com.faf223.expensetrackerfaf.repository.UserRepository;
+import com.faf223.expensetrackerfaf.security.PersonDetails;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+
+@Service
+public class AuthenticationService {
+
+ private final UserRepository repository;
+ private final PasswordEncoder passwordEncoder;
+ private final JwtService jwtService;
+
+ public AuthenticationService(UserRepository repository, PasswordEncoder passwordEncoder, JwtService jwtService) {
+ this.repository = repository;
+ this.passwordEncoder = passwordEncoder;
+ this.jwtService = jwtService;
+ }
+
+ public AuthenticationResponse authenticate(AuthenticationRequest request) {
+
+ }
+
+ public AuthenticationResponse register(RegisterRequest request) {
+ User user = User.builder()
+ .firstName(request.getFirstName())
+ .lastName(request.getLastName())
+ .email(request.getEmail())
+ .password(passwordEncoder.encode(request.getPassword()))
+ .role(request.getRole())
+ .build();
+ repository.save(user);
+ String jwtToken = jwtService.generateToken(new PersonDetails(user));
+ return AuthenticationResponse.builder()
+ .token(jwtToken)
+ .build();
+ }
+}