Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
POSTGRES_DB=p2p_shopping
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
MONGO_DB=p2p_shopping_mongo
JWT_SECRET=your-secret-key-here-at-least-32-characters-long
6 changes: 3 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: Set up JDK 17
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: 17
distribution: 'zulu' # Alternative distribution options are available
java-version: 21
distribution: 'temurin'
Comment on lines +16 to +20
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify Java version alignment between workflow and Gradle toolchain
rg -n 'java-version:\s*[0-9]+' .github/workflows/build.yml
rg -n 'JavaLanguageVersion\.of\([0-9]+\)' build.gradle.kts

Repository: P2P-Shopping/server

Length of output: 148


Align CI JDK version with Gradle toolchain target.

CI workflow uses Java 21 (line 19) but build.gradle.kts targets Java 25 (line 15). Update the workflow to use Java 25 or update the Gradle toolchain to match Java 21 to maintain consistency and avoid build inconsistencies.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/build.yml around lines 16 - 20, CI JDK version (the "Set
up JDK 21" step with java-version: 21) is mismatched with the Gradle toolchain
target (Java 25 in build.gradle.kts); either update the workflow step to use
java-version: 25 to match the Gradle toolchain, or change the Gradle toolchain
JavaLanguageVersion (the toolchain block /
languageVersion.set(JavaLanguageVersion.of(25))) to 21 so both match—pick one
approach and make the corresponding change so the CI java-version and Gradle
toolchain value are identical.

- name: Cache SonarQube packages
uses: actions/cache@v4
with:
Expand Down
82 changes: 82 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
name: Tests
on:
push:
branches:
- main
- develop
pull_request:
types: [opened, synchronize, reopened]

jobs:
test:
name: Run Tests
runs-on: ubuntu-latest

services:
postgres:
image: postgis/postgis:16-3.4
env:
POSTGRES_DB: p2p_shopping_test
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres -d p2p_shopping_test"
--health-interval 10s
--health-timeout 5s
--health-retries 5

mongodb:
image: mongo:7
env:
MONGO_INITDB_DATABASE: p2p_shopping_test
ports:
- 27017:27017
options: >-
--health-cmd "mongosh --eval 'db.adminCommand(\"ping\")'"
--health-interval 10s
--health-timeout 5s
--health-retries 5

steps:
- uses: actions/checkout@v4

- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: 21
distribution: 'temurin'

- name: Cache Gradle packages
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: ${{ runner.os }}-gradle-

- name: Grant execute permission for gradlew
run: chmod +x gradlew

- name: Run tests
env:
SPRING_DATA_MONGODB_URI: mongodb://localhost:27017/p2p_shopping_test
run: ./gradlew test --info

- name: Upload test reports
if: always()
uses: actions/upload-artifact@v4
with:
name: test-reports
path: build/reports/tests/
retention-days: 7

- name: Upload coverage report
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-reports
path: build/reports/jacoco/
retention-days: 7
4 changes: 2 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ dependencies {

testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.junit.platform:junit-platform-suite-api")
testImplementation("org.testcontainers:testcontainers:1.19.0")
testImplementation("org.testcontainers:postgresql:1.19.0")
testImplementation("org.testcontainers:testcontainers")
testImplementation("org.testcontainers:postgresql:1.21.4")
testRuntimeOnly("org.junit.platform:junit-platform-suite-engine")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
Expand Down
19 changes: 18 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,22 @@ services:
timeout: 5s
retries: 5

mongodb:
image: mongo:7
container_name: p2p_shopping_mongodb
restart: unless-stopped
environment:
MONGO_INITDB_DATABASE: ${MONGO_DB:-p2p_shopping_mongo}
ports:
- "27017:27017"
volumes:
- mongodata:/data/db
healthcheck:
test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
interval: 10s
timeout: 5s
retries: 5

volumes:
pgdata:
pgdata:
mongodata:
60 changes: 37 additions & 23 deletions src/main/java/com/p2ps/auth/security/JwtAuthFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,41 +22,55 @@ public JwtAuthFilter(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}

@Override
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
public String extractBearerToken(String authorizationHeader) {
if (authorizationHeader == null || authorizationHeader.isBlank()) {
return null;
}

String token = authorizationHeader.trim();

String token = null;
String userEmail = null;
if (token.regionMatches(true, 0, "Bearer ", 0, 7)) {
return token.substring(7).trim();
}

return token;
}

// Extract token from Authorization header
String authorizationHeader = request.getHeader("Authorization");
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
token = authorizationHeader.substring(7);
public UsernamePasswordAuthenticationToken authenticateToken(String token) {
if (token == null || token.isBlank()) {
return null;
}

try {
if (token != null) {
userEmail = jwtUtil.extractEmail(token);
String userEmail = jwtUtil.extractEmail(token);

if (userEmail != null && !jwtUtil.isTokenExpired(token)) {
return new UsernamePasswordAuthenticationToken(userEmail, null, new ArrayList<>());
}
} catch (Exception _) {
return null;
}

// Authenticate if user is not already in the SecurityContext
if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null && !jwtUtil.isTokenExpired(token)){
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userEmail, null, new ArrayList<>()
);
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
return null;
}

// Set user as authenticated
SecurityContextHolder.getContext().setAuthentication(authToken);
}
@Override
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {

String token = extractBearerToken(request.getHeader("Authorization"));

try {
UsernamePasswordAuthenticationToken authToken = authenticateToken(token);

if (authToken != null && SecurityContextHolder.getContext().getAuthentication() == null) {
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
} catch (Exception _) {
// Clear context if token is expired, malformed, or invalid
SecurityContextHolder.clearContext();
}

// Continue the filter chain
filterChain.doFilter(request, response);
}
}
}
2 changes: 2 additions & 0 deletions src/main/java/com/p2ps/auth/security/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/ws/**").permitAll()
.requestMatchers("/").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
Expand Down
64 changes: 64 additions & 0 deletions src/main/java/com/p2ps/config/JwtHandshakeInterceptor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.p2ps.config;

import com.p2ps.auth.security.JwtAuthFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import org.springframework.web.util.UriComponentsBuilder;

import java.util.Map;

@Component
public class JwtHandshakeInterceptor implements HandshakeInterceptor {

public static final String SESSION_TOKEN_ATTRIBUTE = "wsJwtToken";

private static final Logger logger = LoggerFactory.getLogger(JwtHandshakeInterceptor.class);

private final JwtAuthFilter jwtAuthFilter;
private final boolean enableUrlToken;

public JwtHandshakeInterceptor(JwtAuthFilter jwtAuthFilter,
@Value("${websocket.compatibility.enableUrlToken:false}") boolean enableUrlToken) {
this.jwtAuthFilter = jwtAuthFilter;
this.enableUrlToken = enableUrlToken;
}

@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Map<String, Object> attributes) {
if (!enableUrlToken) {
return true;
}

String token = UriComponentsBuilder.fromUri(request.getURI())
.build()
.getQueryParams()
.getFirst("token");

if (token == null || token.isBlank()) {
return true;
}

if (jwtAuthFilter.authenticateToken(token) == null) {
logger.warn("Rejecting websocket handshake with invalid JWT query token");
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return false;
}

attributes.put(SESSION_TOKEN_ATTRIBUTE, token);
return true;
}

@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Exception exception) {
// No-op.
}
}
11 changes: 9 additions & 2 deletions src/main/java/com/p2ps/config/RoomSubscriptionInterceptor.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.lang.Nullable;
import org.jspecify.annotations.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.stereotype.Component;
import org.springframework.security.core.Authentication;

import java.security.Principal;
import java.util.regex.Pattern;

/**
Expand Down Expand Up @@ -41,9 +43,14 @@ public Message<?> preSend(Message<?> message, MessageChannel channel) {
String destination = accessor.getDestination();

if (destination != null && destination.startsWith("/topic/list/")) {
Principal principal = accessor.getUser();
if (!(principal instanceof Authentication authentication) || !authentication.isAuthenticated()) {
logger.warn("Security Alert: Blocked subscription attempt without authenticated principal");
return null;
}

String listId = destination.substring("/topic/list/".length());

// Security Check: Only allow alphanumeric list IDs (plus hyphens). Prevents directory traversal or wildcard injection.
if (!VALID_LIST_ID.matcher(listId).matches()) {
logger.warn("Security Alert: Blocked malformed room subscription attempt");
return null;
Expand Down
Loading
Loading