Added jwt token validation and api endpoint versioning
Build, Test & Push Auth Service / quality-check (push) Failing after 41s
Build, Test & Push Auth Service / docker (push) Has been skipped

This commit is contained in:
lobo committed 2026-04-03 12:25:03 +02:00
1 parent 2e8ff1b079
commit 64b84b1dc0
11 files changed
+254 -9

No files matched your search

@@ -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);
}
}
@@ -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;
}
@@ -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);
}
}
@@ -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();
}
@@ -33,7 +33,7 @@ public class LoginController {
public ResponseEntity<LoginResponse> 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));
}
}
@@ -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);
}
}
@@ -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<UserProfileResponse> 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) {
}
}
+5 -1
View File
@@ -11,4 +11,8 @@ app:
# Optional: Make SQL logs visible for debugging
spring:
jpa:
show-sql: true
show-sql: true
security:
jwt:
secret: "NzC9tQP1eTp496SxoVYslH1bqn81Tbfz74NjkFqdcVe7wayjcRN8IeZvJWd1s6NEWgwEXe9rdccvOn2HtJ4fdq"
+12
View File
@@ -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:
@@ -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);
VALUES ('test@example.com', '$2a$10$3FApyf15qVEvFLEvl/d7aecSMZlslQdXiYlDgbOa4DfGWu4l0VCY.', true);