From 35fb05ac01b189b72c90bc88c25eaf709886546a Mon Sep 17 00:00:00 2001 From: DmitriiCravcenco Date: Mon, 2 Oct 2023 08:18:57 +0300 Subject: [PATCH] Add JWT authorization --- pom.xml | 21 ++++ .../config/ApplicationConfig.java | 50 ++++++++++ .../config/JwtAuthenticationFilter.java | 68 +++++++++++++ .../expensetrackerfaf/config/JwtService.java | 95 +++++++++++++++++++ .../config/SecurityConfiguration.java | 40 ++++++++ .../auth/AuthenticationController.java | 26 +++++ .../auth/AuthenticationRequest.java | 16 ++++ .../auth/AuthenticationResponse.java | 15 +++ .../controller/auth/RegisterRequest.java | 22 +++++ .../faf223/expensetrackerfaf/model/User.java | 80 +++------------- .../repository/UserRepository.java | 3 + .../security/PersonDetails.java | 12 ++- .../service/AuthenticationService.java | 45 +++++++++ 13 files changed, 423 insertions(+), 70 deletions(-) create mode 100644 src/main/java/com/faf223/expensetrackerfaf/config/ApplicationConfig.java create mode 100644 src/main/java/com/faf223/expensetrackerfaf/config/JwtAuthenticationFilter.java create mode 100644 src/main/java/com/faf223/expensetrackerfaf/config/JwtService.java create mode 100644 src/main/java/com/faf223/expensetrackerfaf/config/SecurityConfiguration.java create mode 100644 src/main/java/com/faf223/expensetrackerfaf/controller/auth/AuthenticationController.java create mode 100644 src/main/java/com/faf223/expensetrackerfaf/controller/auth/AuthenticationRequest.java create mode 100644 src/main/java/com/faf223/expensetrackerfaf/controller/auth/AuthenticationResponse.java create mode 100644 src/main/java/com/faf223/expensetrackerfaf/controller/auth/RegisterRequest.java create mode 100644 src/main/java/com/faf223/expensetrackerfaf/service/AuthenticationService.java 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(); + } +}