diff --git a/pom.xml b/pom.xml index b8e53eb..4f2f005 100644 --- a/pom.xml +++ b/pom.xml @@ -42,7 +42,11 @@ org.springframework.boot spring-boot-starter-jdbc - + + com.auth0 + java-jwt + 4.4.0 + org.postgresql postgresql diff --git a/src/main/java/net/hackyourfuture/security/JwtService.java b/src/main/java/net/hackyourfuture/security/JwtService.java new file mode 100644 index 0000000..8cee30b --- /dev/null +++ b/src/main/java/net/hackyourfuture/security/JwtService.java @@ -0,0 +1,49 @@ +package net.hackyourfuture.security; + +import com.auth0.jwt.JWT; +import org.springframework.beans.factory.annotation.Value; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.interfaces.DecodedJWT; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; +import java.util.Date; + +@Service +public class JwtService { + + private final Algorithm algorithm; + + public JwtService(@Value("${JWT_SECRET_KEY}") String secretKey) { + this.algorithm = Algorithm.HMAC256(secretKey); + } + + + // Generating a new JWT token valid for 24 hours. + public String generateToken(UserDetails userDetails) { + return JWT.create() + .withSubject(userDetails.getUsername()) + .withIssuedAt(new Date()) + .withExpiresAt(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000)) + .sign(algorithm); + } + + // Extracting the usernme from the token. + public String extractUsername(String token) { + DecodedJWT decodedJWT = JWT.require(algorithm) + .build() + .verify(token); + return decodedJWT.getSubject(); + } + + + // Validating if the token belongs to the user and isnt expired. + public boolean isTokenValid(String token, UserDetails userDetails) { + String username = extractUsername(token); + return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); + } + + private boolean isTokenExpired(String token) { + DecodedJWT decodedJWT = JWT.require(algorithm).build().verify(token); + return decodedJWT.getExpiresAt().before(new Date()); + } +} \ No newline at end of file diff --git a/src/main/java/net/hackyourfuture/security/MyUserDetailsService.java b/src/main/java/net/hackyourfuture/security/MyUserDetailsService.java new file mode 100644 index 0000000..26b16b1 --- /dev/null +++ b/src/main/java/net/hackyourfuture/security/MyUserDetailsService.java @@ -0,0 +1,33 @@ +package net.hackyourfuture.security; + +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import net.hackyourfuture.security.user.User; +import net.hackyourfuture.security.user.UserRepository; + +@Service +public class MyUserDetailsService implements UserDetailsService { + + private final UserRepository userRepository; + + public MyUserDetailsService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User user = userRepository.findByUsername(username); + if (user == null) { + throw new UsernameNotFoundException("User not found: " + username); + } + + // Converting the User into Spring Security's UserDetails + return org.springframework.security.core.userdetails.User.builder() + .username(user.getUsername()) + .password(user.getPassword()) + .roles("USER") + .build(); + } +} diff --git a/src/main/java/net/hackyourfuture/security/authentication/AuthenticationService.java b/src/main/java/net/hackyourfuture/security/authentication/AuthenticationService.java index 2472a13..0ecdb5f 100644 --- a/src/main/java/net/hackyourfuture/security/authentication/AuthenticationService.java +++ b/src/main/java/net/hackyourfuture/security/authentication/AuthenticationService.java @@ -4,16 +4,32 @@ import net.hackyourfuture.security.authentication.dto.LoginRequest; import net.hackyourfuture.security.authentication.dto.LoginResponse; import org.springframework.stereotype.Service; +import net.hackyourfuture.security.JwtService; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; @Service @AllArgsConstructor public class AuthenticationService { + private final AuthenticationManager authenticationManager; + private final JwtService jwtService; public LoginResponse login(LoginRequest request) { - throw new UnsupportedOperationException("TODO: implement login"); + Authentication authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(request.username(), request.password()) + ); + + // get the authenticated details + UserDetails user = (UserDetails) authentication.getPrincipal(); + + // Generating a token using JwtService + String token = jwtService.generateToken(user); + + return new LoginResponse(token); } public void logout() { - throw new UnsupportedOperationException("TODO: implement logout"); } } diff --git a/src/main/java/net/hackyourfuture/security/config/MyUserAuthenticationFilter.java b/src/main/java/net/hackyourfuture/security/config/MyUserAuthenticationFilter.java new file mode 100644 index 0000000..c878d88 --- /dev/null +++ b/src/main/java/net/hackyourfuture/security/config/MyUserAuthenticationFilter.java @@ -0,0 +1,50 @@ +package net.hackyourfuture.security.config; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import lombok.AllArgsConstructor; +import net.hackyourfuture.security.JwtService; +import net.hackyourfuture.security.MyUserDetailsService; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import java.io.IOException; + +@AllArgsConstructor +public class MyUserAuthenticationFilter extends UsernamePasswordAuthenticationFilter { + + private final JwtService jwtService; + private final MyUserDetailsService myUserDetailsService; + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + + HttpServletRequest httpRequest = (HttpServletRequest) request; + String authHeader = httpRequest.getHeader("Authorization"); + + // Looking for Bearer token in the headers + if (authHeader != null && authHeader.startsWith("Bearer ")) { + String token = authHeader.substring(7); + try { + // Extracting username + String username = jwtService.extractUsername(token); + var userDetails = myUserDetailsService.loadUserByUsername(username); + + // Verifing token validty + if (jwtService.isTokenValid(token, userDetails)) { + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } catch (Exception e) { + } + } + + chain.doFilter(request, response); + } +} \ No newline at end of file diff --git a/src/main/java/net/hackyourfuture/security/config/SecurityConfig.java b/src/main/java/net/hackyourfuture/security/config/SecurityConfig.java index d3ac764..cc84267 100644 --- a/src/main/java/net/hackyourfuture/security/config/SecurityConfig.java +++ b/src/main/java/net/hackyourfuture/security/config/SecurityConfig.java @@ -1,18 +1,56 @@ package net.hackyourfuture.security.config; +import net.hackyourfuture.security.JwtService; +import net.hackyourfuture.security.MyUserDetailsService; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration public class SecurityConfig { + private final JwtService jwtService; + private final MyUserDetailsService myUserDetailsService; + + public SecurityConfig(JwtService jwtService, MyUserDetailsService myUserDetailsService) { + this.jwtService = jwtService; + this.myUserDetailsService = myUserDetailsService; + } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) - .authorizeHttpRequests(auth -> auth.anyRequest().permitAll()); + .sessionManagement(session -> + session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + // public endpoints + .requestMatchers("/auth/login", "/users/register").permitAll() + // private endpoints + .anyRequest().authenticated() + ) + .addFilterBefore( + new MyUserAuthenticationFilter(jwtService, myUserDetailsService), + UsernamePasswordAuthenticationFilter.class + ); return http.build(); } + // BCrypt for hashing + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + // to handl the login authentication checks + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) + throws Exception { + return config.getAuthenticationManager(); + } } diff --git a/src/main/java/net/hackyourfuture/security/user/UserController.java b/src/main/java/net/hackyourfuture/security/user/UserController.java index 9309baf..ea72240 100644 --- a/src/main/java/net/hackyourfuture/security/user/UserController.java +++ b/src/main/java/net/hackyourfuture/security/user/UserController.java @@ -4,12 +4,15 @@ import net.hackyourfuture.security.user.dto.UserRequest; import net.hackyourfuture.security.user.dto.UserResponse; import org.springframework.http.HttpStatus; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; @RestController @RequestMapping("/users") @@ -25,7 +28,10 @@ public UserResponse register(@RequestBody UserRequest request) { } @GetMapping("/profile") - public UserResponse profile() { - return userService.getProfile("REPLACE WITH CURRENTLY LOGGED IN USER ID"); + public UserResponse profile(@AuthenticationPrincipal UserDetails userDetails) { + if (userDetails == null) { + throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); + } + return userService.getProfile(userDetails.getUsername()); } } diff --git a/src/main/java/net/hackyourfuture/security/user/UserService.java b/src/main/java/net/hackyourfuture/security/user/UserService.java index 15afd1e..c9deda0 100644 --- a/src/main/java/net/hackyourfuture/security/user/UserService.java +++ b/src/main/java/net/hackyourfuture/security/user/UserService.java @@ -4,17 +4,28 @@ import net.hackyourfuture.security.user.dto.UserRequest; import net.hackyourfuture.security.user.dto.UserResponse; import org.springframework.http.HttpStatus; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.web.server.ResponseStatusException; +import java.util.UUID; + @Service @AllArgsConstructor public class UserService { private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; public UserResponse register(UserRequest request) { - throw new UnsupportedOperationException("TODO: implement registration"); + String hashedPassword = passwordEncoder.encode(request.password()); + + String userId = UUID.randomUUID().toString(); + User newUser = new User(userId, request.username(), hashedPassword); + userRepository.createUser(newUser); + + return new UserResponse(newUser.getId(), newUser.getUsername()); + } public UserResponse getProfile(String username) { diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 098cbeb..0d73a55 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -8,3 +8,7 @@ spring: sql: init: mode: always + + jwt: + secret: + key: '${JWT_SECRET_KEY}'