diff --git a/pom.xml b/pom.xml
index b8e53eb..fb831e3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -42,7 +42,23 @@
org.springframework.boot
spring-boot-starter-jdbc
-
+
+ io.jsonwebtoken
+ jjwt-api
+ 0.12.6
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ 0.12.6
+ runtime
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ 0.12.6
+ runtime
+
org.postgresql
postgresql
diff --git a/src/main/java/net/hackyourfuture/security/authentication/AppUserService.java b/src/main/java/net/hackyourfuture/security/authentication/AppUserService.java
new file mode 100644
index 0000000..da36705
--- /dev/null
+++ b/src/main/java/net/hackyourfuture/security/authentication/AppUserService.java
@@ -0,0 +1,33 @@
+package net.hackyourfuture.security.authentication;
+
+
+import lombok.AllArgsConstructor;
+import net.hackyourfuture.security.user.User;
+import net.hackyourfuture.security.user.UserRepository;
+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;
+
+
+@Service
+@AllArgsConstructor
+public class AppUserService implements UserDetailsService {
+
+ private final UserRepository userRepository;
+
+ @Override
+ public UserDetails loadUserByUsername(String username){
+ User user = userRepository.findByUsername(username);
+
+ if(user == null){
+ throw new UsernameNotFoundException("User not found: " + username);
+ }
+
+ return org.springframework.security.core.userdetails.User.builder()
+ .username(user.getUsername())
+ .password(user.getPassword())
+ .authorities("USER")
+ .build();
+ }
+}
diff --git a/src/main/java/net/hackyourfuture/security/authentication/AuthenticationController.java b/src/main/java/net/hackyourfuture/security/authentication/AuthenticationController.java
index e25fd2c..85ef3ea 100644
--- a/src/main/java/net/hackyourfuture/security/authentication/AuthenticationController.java
+++ b/src/main/java/net/hackyourfuture/security/authentication/AuthenticationController.java
@@ -17,6 +17,7 @@ public class AuthenticationController {
@PostMapping("/login")
public LoginResponse login(@RequestBody LoginRequest request) {
+
return authenticationService.login(request);
}
diff --git a/src/main/java/net/hackyourfuture/security/authentication/AuthenticationService.java b/src/main/java/net/hackyourfuture/security/authentication/AuthenticationService.java
index 2472a13..a107c71 100644
--- a/src/main/java/net/hackyourfuture/security/authentication/AuthenticationService.java
+++ b/src/main/java/net/hackyourfuture/security/authentication/AuthenticationService.java
@@ -3,17 +3,28 @@
import lombok.AllArgsConstructor;
import net.hackyourfuture.security.authentication.dto.LoginRequest;
import net.hackyourfuture.security.authentication.dto.LoginResponse;
+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;
import org.springframework.stereotype.Service;
@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()));
+
+ UserDetails user = (UserDetails) authentication.getPrincipal();
+ 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/authentication/JwtAuthenticationFilter.java b/src/main/java/net/hackyourfuture/security/authentication/JwtAuthenticationFilter.java
new file mode 100644
index 0000000..9c87483
--- /dev/null
+++ b/src/main/java/net/hackyourfuture/security/authentication/JwtAuthenticationFilter.java
@@ -0,0 +1,57 @@
+package net.hackyourfuture.security.authentication;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+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.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;
+
+ public JwtAuthenticationFilter(JwtService jwtService, UserDetailsService userDetailsService) {
+ this.jwtService = jwtService;
+ this.userDetailsService = userDetailsService;
+ }
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest request,
+ HttpServletResponse response,
+ FilterChain filterChain) throws ServletException, IOException {
+
+ String authHeader = request.getHeader("Authorization");
+
+ if (authHeader == null || !authHeader.startsWith("Bearer ")) {
+ filterChain.doFilter(request, response);
+ return;
+ }
+
+ String token = authHeader.substring(7);
+ String username = jwtService.extractUsername(token);
+
+ if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
+ UserDetails userDetails = userDetailsService.loadUserByUsername(username);
+
+ if (jwtService.isTokenValid(token, userDetails)) {
+ var authentication = new UsernamePasswordAuthenticationToken(
+ userDetails,
+ null,
+ userDetails.getAuthorities());
+
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+ }
+ }
+
+ filterChain.doFilter(request, response);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/net/hackyourfuture/security/authentication/JwtService.java b/src/main/java/net/hackyourfuture/security/authentication/JwtService.java
new file mode 100644
index 0000000..4d55206
--- /dev/null
+++ b/src/main/java/net/hackyourfuture/security/authentication/JwtService.java
@@ -0,0 +1,57 @@
+package net.hackyourfuture.security.authentication;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+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 javax.crypto.SecretKey;
+import java.util.Date;
+import java.util.function.Function;
+
+@Service
+public class JwtService {
+
+ @Value("${jwt.secret}")
+ private String secretKey;
+
+ @Value("${jwt.expiration-ms}")
+ private long expirationMs;
+
+ private SecretKey getSigningKey() {
+ return Keys.hmacShaKeyFor(secretKey.getBytes());
+ }
+
+ public String generateToken(UserDetails user) {
+ return Jwts.builder()
+ .subject(user.getUsername())
+ .issuedAt(new Date())
+ .expiration(new Date(System.currentTimeMillis() + expirationMs))
+ .signWith(getSigningKey())
+ .compact();
+ }
+
+ public String extractUsername(String token) {
+ return extractClaim(token, Claims::getSubject);
+ }
+
+ public boolean isTokenValid(String token, UserDetails user) {
+ final String username = extractUsername(token);
+ return username.equals(user.getUsername()) && !isTokenExpired(token);
+ }
+
+ private boolean isTokenExpired(String token) {
+ return extractClaim(token, Claims::getExpiration).before(new Date());
+ }
+
+ private T extractClaim(String token, Function resolver) {
+ Claims claims = Jwts.parser()
+ .verifyWith(getSigningKey())
+ .build()
+ .parseSignedClaims(token)
+ .getPayload();
+ return resolver.apply(claims);
+ }
+}
diff --git a/src/main/java/net/hackyourfuture/security/config/SecurityConfig.java b/src/main/java/net/hackyourfuture/security/config/SecurityConfig.java
index d3ac764..097e446 100644
--- a/src/main/java/net/hackyourfuture/security/config/SecurityConfig.java
+++ b/src/main/java/net/hackyourfuture/security/config/SecurityConfig.java
@@ -1,18 +1,44 @@
package net.hackyourfuture.security.config;
+import net.hackyourfuture.security.authentication.JwtAuthenticationFilter;
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.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.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
+@EnableWebSecurity
+@EnableMethodSecurity
public class SecurityConfig {
@Bean
- public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+ public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthenticationFilter jwtFilter) throws Exception {
http
.csrf(csrf -> csrf.disable())
- .authorizeHttpRequests(auth -> auth.anyRequest().permitAll());
+ .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+ .authorizeHttpRequests(auth -> auth
+ .requestMatchers("/users/register", "/auth/login").permitAll()
+ .anyRequest().authenticated())
+ .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
+
return http.build();
}
+
+ @Bean
+ public PasswordEncoder passwordEncoder(){
+ return new BCryptPasswordEncoder(12);
+ }
+
+ @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..9f3af37 100644
--- a/src/main/java/net/hackyourfuture/security/user/UserController.java
+++ b/src/main/java/net/hackyourfuture/security/user/UserController.java
@@ -4,6 +4,7 @@
import net.hackyourfuture.security.user.dto.UserRequest;
import net.hackyourfuture.security.user.dto.UserResponse;
import org.springframework.http.HttpStatus;
+import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -24,8 +25,11 @@ public UserResponse register(@RequestBody UserRequest request) {
return userService.register(request);
}
+
+
@GetMapping("/profile")
- public UserResponse profile() {
- return userService.getProfile("REPLACE WITH CURRENTLY LOGGED IN USER ID");
+ public UserResponse profile(Authentication authentication) {
+
+ return userService.getProfile(authentication.getName());
}
}
diff --git a/src/main/java/net/hackyourfuture/security/user/UserService.java b/src/main/java/net/hackyourfuture/security/user/UserService.java
index 15afd1e..e09d1ee 100644
--- a/src/main/java/net/hackyourfuture/security/user/UserService.java
+++ b/src/main/java/net/hackyourfuture/security/user/UserService.java
@@ -4,19 +4,30 @@
import net.hackyourfuture.security.user.dto.UserRequest;
import net.hackyourfuture.security.user.dto.UserResponse;
import org.springframework.http.HttpStatus;
+import org.springframework.security.access.prepost.PreAuthorize;
+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 encoderPassword = passwordEncoder.encode(request.password());
+ User newUser = new User(UUID.randomUUID().toString(), request.username(), encoderPassword);
+
+ userRepository.createUser(newUser);
+
+ return new UserResponse(newUser.getId(), newUser.getUsername());
}
+ @PreAuthorize("#username == authentication.name")
public UserResponse getProfile(String username) {
User user = userRepository.findByUsername(username);
if (user == null) {
diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml
index 098cbeb..280d8e4 100644
--- a/src/main/resources/application.yaml
+++ b/src/main/resources/application.yaml
@@ -8,3 +8,7 @@ spring:
sql:
init:
mode: always
+
+jwt:
+ secret: '${JWT_SECRET}'
+ expiration-ms: 900000