diff --git a/pom.xml b/pom.xml
index e709f52..af21ca3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -14,8 +14,10 @@
To be Played - Auth
25
+ 0.13.0
+
org.springframework.boot
spring-boot-starter-data-jpa
@@ -32,6 +34,30 @@
org.springframework.boot
spring-boot-starter-webmvc
+
+
+ com.github.ben-manes.caffeine
+ caffeine
+
+
+
+ io.jsonwebtoken
+ jjwt-api
+ ${jwt.version}
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ ${jwt.version}
+ runtime
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ ${jwt.version}
+ runtime
+
+
org.flywaydb
flyway-core
@@ -42,17 +68,21 @@
org.springframework.boot
- spring-boot-starter-actuator
+ spring-boot-starter-flyway
+
+
org.springframework.boot
- spring-boot-starter-flyway
+ spring-boot-starter-actuator
io.micrometer
micrometer-registry-prometheus
runtime
+
+
org.springframework.boot
spring-boot-docker-compose
diff --git a/src/main/java/auth_service/configuration/security/JwtAuthenticationFilter.java b/src/main/java/auth_service/configuration/security/JwtAuthenticationFilter.java
new file mode 100644
index 0000000..a74b047
--- /dev/null
+++ b/src/main/java/auth_service/configuration/security/JwtAuthenticationFilter.java
@@ -0,0 +1,64 @@
+package auth_service.configuration.security;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.jspecify.annotations.NonNull;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+import java.util.Collections;
+
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class JwtAuthenticationFilter extends OncePerRequestFilter {
+
+ private final JwtService jwtService;
+
+ @Override
+ protected void doFilterInternal(
+ HttpServletRequest request,
+ @NonNull HttpServletResponse response,
+ @NonNull FilterChain filterChain
+ ) throws ServletException, IOException {
+
+ final String authHeader = request.getHeader("Authorization");
+
+ if (authHeader == null || !authHeader.startsWith("Bearer ")) {
+ filterChain.doFilter(request, response);
+ return;
+ }
+
+ final String jwt = authHeader.substring(7);
+ if (!jwtService.isTokenValid(jwt)) {
+ filterChain.doFilter(request, response);
+ return;
+ }
+
+ try {
+ final String userEmail = jwtService.extractEmail(jwt);
+
+ if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
+ UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
+ userEmail,
+ null,
+ Collections.emptyList()
+ );
+ authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
+ SecurityContextHolder.getContext().setAuthentication(authToken);
+ }
+ } catch (Exception e) {
+ log.error("JWT Validation failed: {}", e.getMessage());
+ }
+
+ filterChain.doFilter(request, response);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/auth_service/configuration/security/JwtConfig.java b/src/main/java/auth_service/configuration/security/JwtConfig.java
new file mode 100644
index 0000000..b97de6d
--- /dev/null
+++ b/src/main/java/auth_service/configuration/security/JwtConfig.java
@@ -0,0 +1,15 @@
+package auth_service.configuration.security;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+@Getter
+@Setter
+@Configuration
+@ConfigurationProperties(prefix = "security.jwt")
+public class JwtConfig {
+
+ private String secret;
+}
\ No newline at end of file
diff --git a/src/main/java/auth_service/configuration/security/JwtService.java b/src/main/java/auth_service/configuration/security/JwtService.java
new file mode 100644
index 0000000..7cb0537
--- /dev/null
+++ b/src/main/java/auth_service/configuration/security/JwtService.java
@@ -0,0 +1,70 @@
+package auth_service.configuration.security;
+
+import auth_service.domain.login.model.User;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.security.Keys;
+import org.springframework.stereotype.Service;
+
+import javax.crypto.SecretKey;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+
+/**
+ * Service responsible for generating and validating JSON Web Tokens (JWT).
+ */
+@Service
+public class JwtService {
+
+ // Token validity: 24 hours in milliseconds
+ private static final long JWT_EXPIRATION_MS = 86400000;
+ private final JwtConfig jwtConfig;
+
+ public JwtService(JwtConfig jwtConfig) {
+ this.jwtConfig = jwtConfig;
+ }
+
+ /**
+ * Generates a secure JWT for the authenticated user.
+ * * @param user The authenticated user entity
+ *
+ * @return A signed JWT string
+ */
+ public String generateToken(User user) {
+ return Jwts.builder()
+ .subject(user.getEmail())
+ .issuedAt(new Date(System.currentTimeMillis()))
+ .expiration(new Date(System.currentTimeMillis() + JWT_EXPIRATION_MS))
+ .signWith(getSigningKey())
+ .compact();
+ }
+
+ public String extractEmail(String token) {
+ return Jwts.parser()
+ .verifyWith(getSigningKey())
+ .build()
+ .parseSignedClaims(token)
+ .getPayload()
+ .getSubject();
+ }
+
+ public boolean isTokenValid(String token) {
+ try {
+ Jwts.parser()
+ .verifyWith(getSigningKey())
+ .build()
+ .parseSignedClaims(token);
+ return true;
+ } catch (Exception _) {
+ return false;
+ }
+ }
+
+ /**
+ * Retrieves the cryptographic key used for signing the token.
+ */
+ private SecretKey getSigningKey() {
+ String secretKey = jwtConfig.getSecret();
+ byte[] keyBytes = secretKey.getBytes(StandardCharsets.UTF_8);
+ return Keys.hmacShaKeyFor(keyBytes);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/auth_service/configuration/SecurityConfig.java b/src/main/java/auth_service/configuration/security/SecurityConfig.java
similarity index 90%
rename from src/main/java/auth_service/configuration/SecurityConfig.java
rename to src/main/java/auth_service/configuration/security/SecurityConfig.java
index 0aaeb3f..4894832 100644
--- a/src/main/java/auth_service/configuration/SecurityConfig.java
+++ b/src/main/java/auth_service/configuration/security/SecurityConfig.java
@@ -1,5 +1,6 @@
-package auth_service.configuration;
+package auth_service.configuration.security;
+import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -10,6 +11,7 @@ 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;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@@ -23,6 +25,7 @@ import java.util.List;
*/
@Configuration
@EnableWebSecurity
+@RequiredArgsConstructor
public class SecurityConfig {
/**
@@ -34,6 +37,9 @@ public class SecurityConfig {
"/actuator/prometheus", // Allow Prometheus to scrape metrics
"/error" // Default Spring Boot error page routing
};
+
+ private final JwtAuthenticationFilter jwtAuthFilter;
+
/**
* Injects the allowed CORS origins from the active application.yml profile.
*/
@@ -78,7 +84,8 @@ public class SecurityConfig {
.authorizeHttpRequests(auth -> auth
.requestMatchers(WHITE_LIST_URLS).permitAll()
.anyRequest().authenticated()
- );
+ )
+ .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
diff --git a/src/main/java/auth_service/domain/login/controller/LoginController.java b/src/main/java/auth_service/domain/login/controller/LoginController.java
index 91c966b..4042178 100644
--- a/src/main/java/auth_service/domain/login/controller/LoginController.java
+++ b/src/main/java/auth_service/domain/login/controller/LoginController.java
@@ -33,7 +33,7 @@ public class LoginController {
public ResponseEntity login(@RequestBody LoginRequest request) {
var user = ApiMapper.toModel(request);
- loginService.login(user);
- return ResponseEntity.ok(new LoginResponse("Login successful", user.getEmail()));
+ var jwt = loginService.login(user);
+ return ResponseEntity.ok(new LoginResponse("Login successful", jwt));
}
}
\ No newline at end of file
diff --git a/src/main/java/auth_service/domain/login/service/LoginService.java b/src/main/java/auth_service/domain/login/service/LoginService.java
index 724a7f3..eded0fa 100644
--- a/src/main/java/auth_service/domain/login/service/LoginService.java
+++ b/src/main/java/auth_service/domain/login/service/LoginService.java
@@ -1,5 +1,6 @@
package auth_service.domain.login.service;
+import auth_service.configuration.security.JwtService;
import auth_service.domain.login.exception.InvalidCredentialsException;
import auth_service.domain.login.exception.UserNotFoundException;
import auth_service.domain.login.model.User;
@@ -15,14 +16,18 @@ import org.springframework.stereotype.Service;
public class LoginService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
+ private final JwtService jwtService;
public String login(User userToLogin) {
var userEntity = userRepository.findByEmail(userToLogin.getEmail()).orElseThrow(() -> new UserNotFoundException("User not found with given email"));
+
+ log.info("Login attempt for user {}", passwordEncoder.encode(userToLogin.getEncryptedPassword()) + " " + userEntity.getEncryptedPassword());
+
var loginSuccessful = passwordEncoder.matches(userToLogin.getEncryptedPassword(), userEntity.getEncryptedPassword());
if (!loginSuccessful) {
throw new InvalidCredentialsException("Invalid credentials");
}
- return "";
+ return jwtService.generateToken(userToLogin);
}
}
\ No newline at end of file
diff --git a/src/main/java/auth_service/domain/user/controller/UserController.java b/src/main/java/auth_service/domain/user/controller/UserController.java
new file mode 100644
index 0000000..7fb4646
--- /dev/null
+++ b/src/main/java/auth_service/domain/user/controller/UserController.java
@@ -0,0 +1,38 @@
+package auth_service.domain.user.controller;
+
+import io.micrometer.core.annotation.Timed;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.security.Principal;
+
+/**
+ * Controller handling user profile operations for the dashboard.
+ */
+@RestController
+@RequestMapping("/api/users")
+public class UserController {
+
+ /**
+ * Version 1 of the 'me' endpoint, accessed via /api/users/me?v=1
+ * * @return The user's dashboard data
+ */
+ @GetMapping(value = "/me", version = "1.0", produces = "application/json")
+ @Timed(value = "user.me.v1.requests.time", description = "Time taken to process v1 user profile requests")
+ public ResponseEntity getCurrentUserV1(Principal principal) {
+
+ // principal.getName() enthält die E-Mail, die der Türsteher aus dem Token gelesen hat!
+ String authenticatedEmail = principal.getName();
+
+ return ResponseEntity.ok(new UserProfileResponse(
+ authenticatedEmail,
+ "USER",
+ "Welcome to the v1 Dashboard, " + authenticatedEmail + "!"
+ ));
+ }
+
+ public record UserProfileResponse(String email, String role, String dashboardMessage) {
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml
index 0077dc8..1230820 100644
--- a/src/main/resources/application.yaml
+++ b/src/main/resources/application.yaml
@@ -13,6 +13,13 @@ spring:
baseline-on-migrate: true
# Explicitly state where to look for scripts
locations: classpath:db/migration
+ mvc:
+ apiversion:
+ use:
+ query-parameter: version
+ default: "1.0"
+ supported: 1.0, 2,0
+
# Actuator & Micrometer Settings
management:
@@ -29,6 +36,11 @@ management:
export:
enabled: true
+# Security stuff
+security:
+ jwt:
+ secret: ${JWT_SECRET}
+
# Custom properties for CORS
app:
cors:
diff --git a/src/main/resources/db/migration/V1__init_schema.sql b/src/main/resources/db/migration/V1__init_schema.sql
index 2252726..4004588 100644
--- a/src/main/resources/db/migration/V1__init_schema.sql
+++ b/src/main/resources/db/migration/V1__init_schema.sql
@@ -10,4 +10,4 @@ CREATE TABLE users
-- Insert a pragmatic test user for local development and testing
-- The password hash corresponds to 'password123'
INSERT INTO users (email, password, enabled)
-VALUES ('test@example.com', '$2a$10$8.UnVuG9HHgffUDAlk8qfOuVGkqRzgVymGe07xd00dmOn.jptTVyG', true);
\ No newline at end of file
+VALUES ('test@example.com', '$2a$10$3FApyf15qVEvFLEvl/d7aecSMZlslQdXiYlDgbOa4DfGWu4l0VCY.', true);
\ No newline at end of file