commit a94100042547be3d53d3fe96b2a8061367ba6378 Author: Lobo Date: Sat Apr 18 12:08:17 2026 +0200 Initial commit diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..61314d0 --- /dev/null +++ b/pom.xml @@ -0,0 +1,162 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 4.1.0-M4 + + + eu.andreasdahm.tbp + core-service + 0.0.1-SNAPSHOT + To be Played - Core + + 25 + 0.13.0 + 1.21.4 + 1.4.1 + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-webmvc + + + org.springframework.boot + spring-boot-starter-cache + + + 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 + + + org.flywaydb + flyway-database-postgresql + + + org.springframework.boot + spring-boot-starter-flyway + + + org.springframework.boot + spring-boot-starter-actuator + + + io.micrometer + micrometer-registry-prometheus + runtime + + + org.postgresql + postgresql + runtime + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-security-test + test + + + com.tngtech.archunit + archunit-junit5 + ${archunit.version} + test + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + false + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + false + + + + \ No newline at end of file diff --git a/src/main/java/core_service/CoreServiceApplication.java b/src/main/java/core_service/CoreServiceApplication.java new file mode 100644 index 0000000..fadb064 --- /dev/null +++ b/src/main/java/core_service/CoreServiceApplication.java @@ -0,0 +1,12 @@ +package core_service; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class CoreServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(CoreServiceApplication.class, args); + } +} diff --git a/src/main/java/core_service/configuration/security/JwtAuthenticationFilter.java b/src/main/java/core_service/configuration/security/JwtAuthenticationFilter.java new file mode 100644 index 0000000..0c17c4c --- /dev/null +++ b/src/main/java/core_service/configuration/security/JwtAuthenticationFilter.java @@ -0,0 +1,68 @@ +package core_service.configuration.security; + +import core_service.shared.security.UserPrincipal; +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( + @NonNull 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); + final String userId = jwtService.extractUserId(jwt); + + if (userEmail != null && userId != null && SecurityContextHolder.getContext().getAuthentication() == null) { + + UserPrincipal principal = new UserPrincipal(userId, userEmail); + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + principal, + 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); + } +} diff --git a/src/main/java/core_service/configuration/security/JwtConfig.java b/src/main/java/core_service/configuration/security/JwtConfig.java new file mode 100644 index 0000000..237e834 --- /dev/null +++ b/src/main/java/core_service/configuration/security/JwtConfig.java @@ -0,0 +1,15 @@ +package core_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; +} diff --git a/src/main/java/core_service/configuration/security/JwtService.java b/src/main/java/core_service/configuration/security/JwtService.java new file mode 100644 index 0000000..f7510ec --- /dev/null +++ b/src/main/java/core_service/configuration/security/JwtService.java @@ -0,0 +1,55 @@ +package core_service.configuration.security; + +import core_service.shared.security.UserPrincipal; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import org.springframework.stereotype.Service; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; + +@Service +public class JwtService { + + private final JwtConfig jwtConfig; + + public JwtService(JwtConfig jwtConfig) { + this.jwtConfig = jwtConfig; + } + + public String extractEmail(String token) { + return Jwts.parser() + .verifyWith(getSigningKey()) + .build() + .parseSignedClaims(token) + .getPayload() + .getSubject(); + } + + public String extractUserId(String token) { + return Jwts.parser() + .verifyWith(getSigningKey()) + .build() + .parseSignedClaims(token) + .getPayload() + .get("userId", String.class); + } + + public boolean isTokenValid(String token) { + try { + Jwts.parser() + .verifyWith(getSigningKey()) + .build() + .parseSignedClaims(token); + return true; + } catch (Exception e) { + return false; + } + } + + private SecretKey getSigningKey() { + String secretKey = jwtConfig.getSecret(); + byte[] keyBytes = secretKey.getBytes(StandardCharsets.UTF_8); + return Keys.hmacShaKeyFor(keyBytes); + } +} diff --git a/src/main/java/core_service/configuration/security/SecurityConfig.java b/src/main/java/core_service/configuration/security/SecurityConfig.java new file mode 100644 index 0000000..009ae38 --- /dev/null +++ b/src/main/java/core_service/configuration/security/SecurityConfig.java @@ -0,0 +1,66 @@ +package core_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; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +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; + +import java.util.Arrays; +import java.util.List; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + + private static final String[] WHITE_LIST_URLS = { + "/internal/**", // Allow internal service communication + "/actuator/health", + "/actuator/prometheus", + "/error" + }; + + private final JwtAuthenticationFilter jwtAuthFilter; + + @Value("${app.cors.allowed-origins}") + private String[] allowedOrigins; + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOriginPatterns(Arrays.asList(allowedOrigins)); + configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); + configuration.setAllowedHeaders(List.of("*")); + configuration.setAllowCredentials(false); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + .requestMatchers(WHITE_LIST_URLS).permitAll() + .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() + .anyRequest().authenticated() + ) + .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } +} diff --git a/src/main/java/core_service/shared/security/UserPrincipal.java b/src/main/java/core_service/shared/security/UserPrincipal.java new file mode 100644 index 0000000..5eef2d7 --- /dev/null +++ b/src/main/java/core_service/shared/security/UserPrincipal.java @@ -0,0 +1,18 @@ +package core_service.shared.security; + +import java.security.Principal; + +public record UserPrincipal( + String id, + String email +) implements Principal { + + @Override + public String getName() { + return id; + } + + public long getIdAsLong() { + return Long.parseLong(id); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..deb882c --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,37 @@ +spring: + application: + name: tbp-core-service + profiles: + active: prod + datasource: + url: jdbc:postgresql://${DB_HOST}:5432/tbp_core + username: ${DB_USER} + password: ${DB_PASS} + jpa: + open-in-view: true + hibernate: + ddl-auto: validate + flyway: + enabled: true + baseline-on-migrate: true + locations: classpath:db/migration + +server: + port: 8081 + +management: + endpoints: + web: + exposure: + include: health, prometheus, metrics + +security: + jwt: + secret: ${JWT_SECRET} + +app: + cors: + allowed-origins: "https://tbp.andreas-dahm.eu" + +internal: + api-key: ${INTERNAL_API_KEY}