Skip to content
Merged
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
17 changes: 17 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,23 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>
</dependencies>

<build>
Expand Down
58 changes: 58 additions & 0 deletions src/main/java/org/example/alfs/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package org.example.alfs.config;

import org.example.alfs.security.JwtAuthenticationFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableMethodSecurity
public class SecurityConfig {
private final JwtAuthenticationFilter jwtFilter;

public SecurityConfig(JwtAuthenticationFilter jwtFilter) {this.jwtFilter = jwtFilter;}

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

http
// Only for development (H2 console support)
.securityMatcher("/**")

.csrf(csrf -> csrf.disable())

.headers(headers -> headers.frameOptions(frame -> frame.disable()))
Comment on lines +26 to +28

@coderabbitai coderabbitai Bot Apr 9, 2026

Copy link
Copy Markdown

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
# Check if there are profile-specific configurations that might override this
fd -e properties -e yml | xargs rg -l 'h2.console|spring.profiles'

Repository: ithsjava25/project-backend-alfs

Length of output: 116


🏁 Script executed:

cat -n src/main/resources/application.properties

Repository: ithsjava25/project-backend-alfs

Length of output: 720


🏁 Script executed:

# Search for profile-specific property files
fd application- src/main/resources/

Repository: ithsjava25/project-backend-alfs

Length of output: 57


🏁 Script executed:

# Read the full SecurityConfig.java to understand the context
cat -n src/main/java/org/example/alfs/config/SecurityConfig.java

Repository: ithsjava25/project-backend-alfs

Length of output: 2876


Implement Spring profiles to prevent H2 console and disabled security headers from reaching production.

The current configuration exposes the H2 console with direct database access and disabled security headers in all environments. No profile-specific configurations exist to conditionally apply these settings only to development:

  • H2 console is globally enabled (spring.h2.console.enabled=true in application.properties)
  • CSRF and frame options are disabled unconditionally (lines 27, 29)
  • /h2-console/** is permitted without authentication (line 46)

These settings should only apply in development. Create application-dev.properties and application-prod.properties to conditionally enable H2 and relax security only when spring.profiles.active=dev.

Additionally, remove unused imports on lines 6 (HttpMethod) and 7 (Customizer).

πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/alfs/config/SecurityConfig.java` around lines 27 -
29, Security is currently relaxed for all environments; update configuration so
H2 console and disabled security headers apply only in dev: create
application-dev.properties (set spring.profiles.active=dev for local testing or
document how to activate) with spring.h2.console.enabled=true and
application-prod.properties with spring.h2.console.enabled=false, then change
SecurityConfig to apply .csrf(csrf -> csrf.disable()), .headers(...frame ->
frame.disable()) and the permit rule for "/h2-console/**" only when the "dev"
profile is active (use `@Profile`("dev") on a dedicated
WebSecurityConfigurerAdapter/Bean or conditionally build the HttpSecurity in a
method guarded by Environment/Profiles), leaving the stricter defaults for prod;
also remove the unused imports HttpMethod and Customizer from SecurityConfig.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We are intentionally keeping this configuration for development only.

  • H2 console is enabled to simplify local development and testing
  • CSRF is disabled because we are building a stateless REST API using JWT
  • Frame options are disabled to allow the H2 console UI to load

This project is not targeting production deployment, and these settings are acceptable for the current scope.

We have added comments in the code to clarify that this is for development purposes only.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@addee1, that's a clear and reasonable explanation β€” thank you for the context. For a development-only project with a stateless JWT-based REST API, disabling CSRF and frame options and enabling the H2 console are all justified, and adding clarifying comments in the code is a good practice.

One minor item still worth addressing from the original comment: the unused imports HttpMethod (Line 6) and Customizer (Line 7) in SecurityConfig.java can be removed to keep the file clean, since they are not referenced anywhere in the class.


✏️ Learnings added
Learnt from: addee1
Repo: ithsjava25/project-backend-alfs PR: 9
File: src/main/java/org/example/alfs/config/SecurityConfig.java:27-29
Timestamp: 2026-04-09T09:49:08.771Z
Learning: In the project `ithsjava25/project-backend-alfs`, the Spring Security configuration (`src/main/java/org/example/alfs/config/SecurityConfig.java`) intentionally disables CSRF and frame options, and permits the H2 console without authentication. This is by design for development-only use β€” the project is a stateless JWT-based REST API not targeting production deployment. Do not flag these settings as security issues.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


// stateless jwt
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)


// Authorization strategy:
// - JWT is used for authentication (identifying the user)
// - User roles are NOT trusted from the JWT
// - Roles are always loaded from the database
// This ensures that permission changes take effect immediately
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/login").permitAll()
.requestMatchers("/auth/signup").permitAll()
.requestMatchers("/auth/hash").permitAll()
.requestMatchers("/h2-console/**").permitAll()
.anyRequest().authenticated()
)

// disable DEFAULT LOGIN
.formLogin(form -> form.disable())
.httpBasic(basic -> basic.disable())


.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();
}
}
29 changes: 19 additions & 10 deletions src/main/java/org/example/alfs/controllers/AuthController.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,27 @@

import org.example.alfs.dto.auth.LoginRequestDTO;
import org.example.alfs.dto.auth.LoginResponseDTO;
import org.example.alfs.dto.auth.SignupRequestDTO;
import org.example.alfs.entities.User;
import org.example.alfs.security.JwtService;
import org.example.alfs.services.AuthService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;

@RestController
@RequestMapping("/auth")
public class AuthController {

private final AuthService authService;
private final JwtService jwtService;

public AuthController(AuthService authService) {
public AuthController(AuthService authService, JwtService jwtService) {
this.authService = authService;
this.jwtService = jwtService;
}

/**
* Handles user login by validating credentials and returning user details.
* Authenticates a user by validating credentials and returns a JWT token.
*/
@PostMapping("/login")
public LoginResponseDTO login(@Valid @RequestBody LoginRequestDTO request) {
Expand All @@ -31,9 +32,17 @@ public LoginResponseDTO login(@Valid @RequestBody LoginRequestDTO request) {
request.getPassword()
);

return new LoginResponseDTO(
user.getUsername(),
user.getRole().name()
);
String token = jwtService.generateToken(user);
return new LoginResponseDTO(token);
}


/**
* Handles user signup by validating input and creating a new account.
*/
@PostMapping("/signup")
public void signup(@Valid @RequestBody SignupRequestDTO request) {
authService.signup(request);
}

}
64 changes: 64 additions & 0 deletions src/main/java/org/example/alfs/controllers/TestController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package org.example.alfs.controllers;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

// This controller demonstrates role-based access control using @PreAuthorize.
// Use this as a reference when implementing real endpoints.
@RestController
public class TestController {

@PreAuthorize("hasAnyRole('ADMIN','INVESTIGATOR','REPORTER')")
@GetMapping("/api/test/all-roles")
public String allRoles() {
return "all roles allowed";
}

@PreAuthorize("hasRole('REPORTER')")
@GetMapping("/api/test/create-ticket")
public String createTicket() {
return "reporter can create ticket";
}


@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/api/test/assign-ticket")
public String assignTicket() {
return "admin can assign ticket";
}

@PreAuthorize("hasRole('INVESTIGATOR')")
@GetMapping("/api/test/update-status")
public String updateStatus() {
return "investigator can update status";
}

// need to be signed in - all roles
@GetMapping("/api/hello")
public String hello() {
return "hello secured";
}

// Only ADMIN
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/api/admin/test")
public String adminOnly() {
return "only admin";
}

// Only INVESTIGATOR
@PreAuthorize("hasRole('INVESTIGATOR')")
@GetMapping("/api/investigator/test")
public String investigatorOnly() {
return "only investigator";
}

// Only REPORTER
@PreAuthorize("hasRole('REPORTER')")
@GetMapping("/api/reporter/test")
public String reporterOnly() {
return "only reporter";
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,5 @@
@AllArgsConstructor
public class LoginResponseDTO {

private String username;
private String role;
private String token;
}
18 changes: 18 additions & 0 deletions src/main/java/org/example/alfs/dto/auth/SignupRequestDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.example.alfs.dto.auth;

import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.Setter;


@Getter
@Setter
public class SignupRequestDTO {

@NotBlank(message = "Username is required")
private String username;

@NotBlank(message = "Password is required")
private String password;

}
103 changes: 103 additions & 0 deletions src/main/java/org/example/alfs/security/JwtAuthenticationFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package org.example.alfs.security;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.example.alfs.entities.User;
import org.example.alfs.repositories.UserRepository;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtService jwtService;
private final UserRepository userRepository;

public JwtAuthenticationFilter(JwtService jwtService, UserRepository userRepository) {
this.jwtService = jwtService;
this.userRepository = userRepository;
}

// Skips filter for LOGIN & H2
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String path = request.getRequestURI();
return path.startsWith("/auth") || path.startsWith("/h2-console");
}


@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {

final String authHeader = request.getHeader("Authorization");

// if no token, keep going
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}

String jwt = authHeader.substring(7);
String username;
try {
username = jwtService.extractUsername(jwt);
} catch (Exception e) {
filterChain.doFilter(request, response);
return;
}

if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {

User user = userRepository.findByUsername(username).orElse(null);

if (user == null) {
filterChain.doFilter(request, response);
return;
}

// IMPORTANT:
// We do NOT trust the role stored in the JWT.
// Instead, we always load the user's role from the database.
//
// This ensures that if a user's role changes (e.g. ADMIN β†’ REPORTER),
// the change takes effect immediately, even if the old JWT is still valid.
org.springframework.security.core.userdetails.UserDetails userDetails =
new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPasswordHash(),
List.of(new SimpleGrantedAuthority("ROLE_" + user.getRole().name()))
);

boolean valid = jwtService.isTokenValid(jwt, user);

if (valid) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);

authToken.setDetails(
new org.springframework.security.web.authentication.WebAuthenticationDetailsSource()
.buildDetails(request)
);

SecurityContextHolder.getContext().setAuthentication(authToken);
}
}

filterChain.doFilter(request, response);
}
}
Loading
Loading