diff --git a/.gitignore b/.gitignore
index e792a47..cb5a54e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -189,4 +189,6 @@ build/
!**/src/test/**/build/
### VS Code ###
-.vscode/
\ No newline at end of file
+.vscode/
+
+.env
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index b8e53eb..b3370cb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -38,10 +38,48 @@
org.springframework.boot
spring-boot-starter-webmvc
+
+ io.jsonwebtoken
+ jjwt-api
+ 0.12.6
+
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ 0.12.6
+ runtime
+
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ 0.12.6
+ runtime
+
org.springframework.boot
spring-boot-starter-jdbc
+
+ org.springframework.boot
+ spring-boot-starter-oauth2-resource-server
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+
+ org.springframework.security
+ spring-security-oauth2-jose
+
+
+ io.github.cdimascio
+ java-dotenv
+ 5.2.2
+
+
org.postgresql
@@ -63,6 +101,10 @@
spring-boot-starter-webmvc-test
test
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
diff --git a/src/main/java/net/hackyourfuture/security/SecurityApplication.java b/src/main/java/net/hackyourfuture/security/SecurityApplication.java
index 181fefa..fe96a49 100644
--- a/src/main/java/net/hackyourfuture/security/SecurityApplication.java
+++ b/src/main/java/net/hackyourfuture/security/SecurityApplication.java
@@ -3,6 +3,7 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+
@SpringBootApplication
public class SecurityApplication {
diff --git a/src/main/java/net/hackyourfuture/security/authentication/AuthenticationService.java b/src/main/java/net/hackyourfuture/security/authentication/AuthenticationService.java
index 2472a13..8d0739c 100644
--- a/src/main/java/net/hackyourfuture/security/authentication/AuthenticationService.java
+++ b/src/main/java/net/hackyourfuture/security/authentication/AuthenticationService.java
@@ -3,17 +3,32 @@
import lombok.AllArgsConstructor;
import net.hackyourfuture.security.authentication.dto.LoginRequest;
import net.hackyourfuture.security.authentication.dto.LoginResponse;
+import net.hackyourfuture.security.user.User;
+import net.hackyourfuture.security.user.UserRepository;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
+import org.springframework.web.server.ResponseStatusException;
@Service
@AllArgsConstructor
public class AuthenticationService {
+ private final UserRepository userRepository;
+ private final PasswordEncoder passwordEncoder;
+ private final JwtService jwtService;
+
public LoginResponse login(LoginRequest request) {
- throw new UnsupportedOperationException("TODO: implement login");
+ User user = userRepository.findByUsername(request.username());
+
+ if (user == null || !passwordEncoder.matches(request.password(), user.getPassword())) {
+ throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid username or password");
+ }
+
+ return new LoginResponse(jwtService.generateToken(user.getUsername()));
}
public void logout() {
- throw new UnsupportedOperationException("TODO: implement logout");
+ // JWT logout: client deletes token
}
-}
+}
\ No newline at end of file
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..930c347
--- /dev/null
+++ b/src/main/java/net/hackyourfuture/security/authentication/JwtAuthenticationFilter.java
@@ -0,0 +1,62 @@
+package net.hackyourfuture.security.authentication;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.AllArgsConstructor;
+import net.hackyourfuture.security.user.User;
+import net.hackyourfuture.security.user.UserRepository;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+import java.util.List;
+
+@Component
+@AllArgsConstructor
+public class JwtAuthenticationFilter extends OncePerRequestFilter {
+
+ private final JwtService jwtService;
+ private final UserRepository userRepository;
+
+ @Override
+ protected void doFilterInternal(
+ HttpServletRequest request,
+ HttpServletResponse response,
+ FilterChain filterChain
+ ) throws ServletException, IOException {
+
+ String header = request.getHeader("Authorization");
+
+ if (header == null || !header.startsWith("Bearer ")) {
+ filterChain.doFilter(request, response);
+ return;
+ }
+
+ String token = header.substring(7);
+
+ if (!jwtService.isTokenValid(token)) {
+ filterChain.doFilter(request, response);
+ return;
+ }
+
+ String username = jwtService.extractUsername(token);
+ User user = userRepository.findByUsername(username);
+
+ if (user != null) {
+ UsernamePasswordAuthenticationToken authentication =
+ new UsernamePasswordAuthenticationToken(
+ user.getUsername(),
+ null,
+ List.of()
+ );
+
+ 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..2c7517e
--- /dev/null
+++ b/src/main/java/net/hackyourfuture/security/authentication/JwtService.java
@@ -0,0 +1,48 @@
+package net.hackyourfuture.security.authentication;
+
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.security.Keys;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import javax.crypto.SecretKey;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+
+@Service
+public class JwtService {
+
+ @Value("${jwt.secret}")
+ private String secret;
+
+ private SecretKey getSigningKey() {
+ return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
+ }
+
+ public String generateToken(String username) {
+ return Jwts.builder()
+ .subject(username)
+ .issuedAt(new Date())
+ .expiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60))
+ .signWith(getSigningKey())
+ .compact();
+ }
+
+ public String extractUsername(String token) {
+ return Jwts.parser()
+ .verifyWith(getSigningKey())
+ .build()
+ .parseSignedClaims(token)
+ .getPayload()
+ .getSubject();
+ }
+
+ public boolean isTokenValid(String token) {
+ try {
+ extractUsername(token);
+ return true;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+}
\ 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..f751ec0 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 lombok.AllArgsConstructor;
+import net.hackyourfuture.security.authentication.JwtAuthenticationFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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
+@AllArgsConstructor
public class SecurityConfig {
+ private final JwtAuthenticationFilter jwtAuthenticationFilter;
+
@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
+ .requestMatchers("/users/register", "/auth/login").permitAll()
+ .requestMatchers("/users/profile", "/auth/logout").authenticated()
+ .anyRequest().authenticated()
+ )
+ .addFilterBefore(
+ jwtAuthenticationFilter,
+ UsernamePasswordAuthenticationFilter.class
+ );
+
return http.build();
}
-}
+
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder(12);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/net/hackyourfuture/security/user/UserController.java b/src/main/java/net/hackyourfuture/security/user/UserController.java
index 9309baf..3610b06 100644
--- a/src/main/java/net/hackyourfuture/security/user/UserController.java
+++ b/src/main/java/net/hackyourfuture/security/user/UserController.java
@@ -10,6 +10,7 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
+import org.springframework.security.core.Authentication;
@RestController
@RequestMapping("/users")
@@ -25,7 +26,7 @@ public UserResponse register(@RequestBody UserRequest 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..74b8912 100644
--- a/src/main/java/net/hackyourfuture/security/user/UserService.java
+++ b/src/main/java/net/hackyourfuture/security/user/UserService.java
@@ -4,24 +4,44 @@
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");
+ User existingUser = userRepository.findByUsername(request.username());
+
+ if (existingUser != null) {
+ throw new ResponseStatusException(HttpStatus.CONFLICT, "Username already exists");
+ }
+
+ User user = new User(
+ UUID.randomUUID().toString(),
+ request.username(),
+ passwordEncoder.encode(request.password())
+ );
+
+ userRepository.createUser(user);
+
+ return new UserResponse(user.getId(), user.getUsername());
}
public UserResponse getProfile(String username) {
User user = userRepository.findByUsername(username);
+
if (user == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "User not found");
}
+
return new UserResponse(user.getId(), user.getUsername());
}
}
diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml
index 098cbeb..701bfd9 100644
--- a/src/main/resources/application.yaml
+++ b/src/main/resources/application.yaml
@@ -2,9 +2,14 @@ spring:
application:
name: security
datasource:
- url: '${DB_URL}'
- username: '${DB_USERNAME}'
- password: '${DB_PASSWORD}'
+ url: ${DB_URL}
+ username: ${DB_USERNAME}
+ password: ${DB_PASSWORD}
+ driver-class-name: org.postgresql.Driver
+ jwt:
+ secret: ${JWT_SECRET}
sql:
init:
mode: always
+
+