From 704517b19e12d068a2b69e6a1b7fa6204509e661 Mon Sep 17 00:00:00 2001 From: Adam Ottosson Date: Thu, 9 Apr 2026 11:25:30 +0200 Subject: [PATCH 01/15] feat: implement JWT authentication and security configuration - Added JwtService for token generation and validation - Implemented JwtAuthenticationFilter - Configured stateless Spring Security - Disabled default login mechanisms --- .../example/alfs/config/SecurityConfig.java | 59 ++++++++++ .../security/JwtAuthenticationFilter.java | 107 ++++++++++++++++++ .../org/example/alfs/security/JwtService.java | 91 +++++++++++++++ src/main/resources/application.properties | 18 +++ 4 files changed, 275 insertions(+) create mode 100644 src/main/java/org/example/alfs/config/SecurityConfig.java create mode 100644 src/main/java/org/example/alfs/security/JwtAuthenticationFilter.java create mode 100644 src/main/java/org/example/alfs/security/JwtService.java diff --git a/src/main/java/org/example/alfs/config/SecurityConfig.java b/src/main/java/org/example/alfs/config/SecurityConfig.java new file mode 100644 index 0000000..ccb9bea --- /dev/null +++ b/src/main/java/org/example/alfs/config/SecurityConfig.java @@ -0,0 +1,59 @@ +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.http.HttpMethod; +import org.springframework.security.config.Customizer; +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 + .securityMatcher("/**") + + .csrf(csrf -> csrf.disable()) + + .headers(headers -> headers.frameOptions(frame -> frame.disable())) + + // 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(); + } +} \ No newline at end of file diff --git a/src/main/java/org/example/alfs/security/JwtAuthenticationFilter.java b/src/main/java/org/example/alfs/security/JwtAuthenticationFilter.java new file mode 100644 index 0000000..0c1c658 --- /dev/null +++ b/src/main/java/org/example/alfs/security/JwtAuthenticationFilter.java @@ -0,0 +1,107 @@ +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 { + + System.out.println("FILTER RUNNING: " + request.getRequestURI()); + + 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); + System.out.println("JWT: " + jwt); + String username; + try { + username = jwtService.extractUsername(jwt); + System.out.println("USERNAME FROM TOKEN: " + username); + } catch (Exception e) { + System.out.println("TOKEN PARSE FAILED"); + filterChain.doFilter(request, response); + return; + } + + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new RuntimeException("User not found")); + + // 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())) + ); + + System.out.println("USER FROM DB: " + user.getUsername()); + + boolean valid = jwtService.isTokenValid(jwt, user); + System.out.println("TOKEN VALID: " + valid); + + 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); + } +} \ No newline at end of file diff --git a/src/main/java/org/example/alfs/security/JwtService.java b/src/main/java/org/example/alfs/security/JwtService.java new file mode 100644 index 0000000..c5d6938 --- /dev/null +++ b/src/main/java/org/example/alfs/security/JwtService.java @@ -0,0 +1,91 @@ +package org.example.alfs.security; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import org.example.alfs.entities.User; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import javax.crypto.SecretKey; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +@Service +public class JwtService { + + @Value("${jwt.secret}") + private String secretKey; + + @Value("${jwt.expiration}") + private long jwtExpiration; + + public String generateToken(User user) { + Map claims = new HashMap<>(); + + + // NOTE: + // We include the user's role in the JWT for potential future use. + // However, the application does NOT use the role from the token for authorization. + // + // Instead, the user's role is always fetched from the database. + // This ensures that any changes to user permissions take effect immediately, + // without waiting for the JWT to expire. + // + // This design prioritizes security and consistency over performance. + claims.put("role", user.getRole().name()); + + return buildToken(claims, user.getUsername()); + } + + private String buildToken(Map claims, String username) { + return Jwts.builder() + .claims(claims) + .subject(username) + .issuedAt(new Date(System.currentTimeMillis())) + .expiration(new Date(System.currentTimeMillis() + jwtExpiration)) + .signWith(getSigningKey()) + .compact(); + } + + public String extractUsername(String token) { + return extractClaim(token, Claims::getSubject); + } + + public String extractRole(String token) { + return extractAllClaims(token).get("role", String.class); + } + + public boolean isTokenValid(String token, User user) { + final String username = extractUsername(token); + return username.equals(user.getUsername()) && !isTokenExpired(token); + } + + public boolean isTokenExpired(String token) { + return extractExpiration(token).before(new Date()); + } + + private Claims extractAllClaims(String token) { + return Jwts.parser() + .verifyWith(getSigningKey()) + .build() + .parseSignedClaims(token) + .getPayload(); + } + + private SecretKey getSigningKey() { + byte[] keyBytes = Decoders.BASE64.decode(secretKey); + return Keys.hmacShaKeyFor(keyBytes); + } + + private Date extractExpiration(String token) { + return extractClaim(token, Claims::getExpiration); + } + + private T extractClaim(String token, java.util.function.Function claimsResolver) { + Claims claims = extractAllClaims(token); + return claimsResolver.apply(claims); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 41d8bf9..8b46a90 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,2 +1,20 @@ gg.jte.development-mode=true spring.application.name=alfs + +# H2 DATABASE CONFIG +spring.datasource.url=jdbc:h2:mem:testdb +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= + +# H2 CONSOLE +spring.h2.console.enabled=true +spring.h2.console.path=/h2-console + +# JPA / Hibernate +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.jpa.hibernate.ddl-auto=update + + +jwt.secret=bXlTdXBlclNlY3JldEtleU15U3VwZXJTZWNyZXRLZXk= +jwt.expiration=86400000 \ No newline at end of file From 00d3546f9f79a1c8b92a4dabb80d1bddc7691927 Mon Sep 17 00:00:00 2001 From: Adam Ottosson Date: Thu, 9 Apr 2026 11:27:44 +0200 Subject: [PATCH 02/15] feat: add authentication endpoints (login and signup) - Implemented login endpoint with JWT token generation - Added signup endpoint for user registration - Passwords are hashed using BCrypt - Default role set to REPORTER for new users --- .../alfs/controllers/AuthController.java | 33 +++++++++++---- .../alfs/dto/auth/LoginResponseDTO.java | 3 +- .../alfs/dto/auth/SignupRequestDTO.java | 18 ++++++++ .../example/alfs/services/AuthService.java | 41 +++++++++++++++---- 4 files changed, 75 insertions(+), 20 deletions(-) create mode 100644 src/main/java/org/example/alfs/dto/auth/SignupRequestDTO.java diff --git a/src/main/java/org/example/alfs/controllers/AuthController.java b/src/main/java/org/example/alfs/controllers/AuthController.java index fdf7562..a4e58cd 100644 --- a/src/main/java/org/example/alfs/controllers/AuthController.java +++ b/src/main/java/org/example/alfs/controllers/AuthController.java @@ -2,12 +2,11 @@ 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 @@ -15,9 +14,11 @@ 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; } /** @@ -31,9 +32,23 @@ 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 login by validating credentials and returning user details. + */ + @PostMapping("/signup") + public void signup(@Valid @RequestBody SignupRequestDTO request) { + authService.signup(request); + } + + // This is just for development purposes. Should be deleted later! + @GetMapping("/hash") + public String hash() { + return new org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder() + .encode("test123"); } } diff --git a/src/main/java/org/example/alfs/dto/auth/LoginResponseDTO.java b/src/main/java/org/example/alfs/dto/auth/LoginResponseDTO.java index 350e6eb..f2d2dc5 100644 --- a/src/main/java/org/example/alfs/dto/auth/LoginResponseDTO.java +++ b/src/main/java/org/example/alfs/dto/auth/LoginResponseDTO.java @@ -7,6 +7,5 @@ @AllArgsConstructor public class LoginResponseDTO { - private String username; - private String role; + private String token; } diff --git a/src/main/java/org/example/alfs/dto/auth/SignupRequestDTO.java b/src/main/java/org/example/alfs/dto/auth/SignupRequestDTO.java new file mode 100644 index 0000000..79f8ae9 --- /dev/null +++ b/src/main/java/org/example/alfs/dto/auth/SignupRequestDTO.java @@ -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; + +} diff --git a/src/main/java/org/example/alfs/services/AuthService.java b/src/main/java/org/example/alfs/services/AuthService.java index c227f2f..004671e 100644 --- a/src/main/java/org/example/alfs/services/AuthService.java +++ b/src/main/java/org/example/alfs/services/AuthService.java @@ -1,6 +1,8 @@ package org.example.alfs.services; +import org.example.alfs.dto.auth.SignupRequestDTO; import org.example.alfs.entities.User; +import org.example.alfs.enums.Role; import org.example.alfs.repositories.UserRepository; import org.springframework.http.HttpStatus; import org.springframework.security.crypto.password.PasswordEncoder; @@ -24,17 +26,38 @@ public AuthService(PasswordEncoder passwordEncoder, UserRepository userRepositor public User login(String username, String password) { User user = userRepository.findByUsername(username) - .orElseThrow(() -> new ResponseStatusException( - HttpStatus.UNAUTHORIZED, - "Invalid username or password" - )); - if (!passwordEncoder.matches(password, user.getPasswordHash())) { - throw new ResponseStatusException( - HttpStatus.UNAUTHORIZED, - "Invalid username or password" - ); + .orElseThrow(() -> new RuntimeException("User not found")); + + System.out.println("INPUT PASSWORD: " + password); + System.out.println("DB HASH: " + user.getPasswordHash()); + + boolean matches = passwordEncoder.matches(password, user.getPasswordHash()); + + System.out.println("MATCH RESULT: " + matches); + + if (!matches) { + throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Bad credentials"); } return user; } + + + /** + * Registers a new user by creating an account with a hashed password. + * The user is assigned the default role REPORTER. + */ + public void signup(SignupRequestDTO request) { + + if (userRepository.findByUsername(request.getUsername()).isPresent()) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Username already exists"); + } + + User user = new User(); + user.setUsername(request.getUsername()); + user.setPasswordHash(passwordEncoder.encode(request.getPassword())); + user.setRole(Role.REPORTER); + + userRepository.save(user); + } } From 71e4479ef37149db182a9b36486884d2bddc4ef7 Mon Sep 17 00:00:00 2001 From: Adam Ottosson Date: Thu, 9 Apr 2026 11:27:59 +0200 Subject: [PATCH 03/15] feat: add role-based access control with @PreAuthorize - Added protected endpoints for ADMIN, INVESTIGATOR and REPORTER - Verified role-based authorization --- .../alfs/controllers/TestController.java | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 src/main/java/org/example/alfs/controllers/TestController.java diff --git a/src/main/java/org/example/alfs/controllers/TestController.java b/src/main/java/org/example/alfs/controllers/TestController.java new file mode 100644 index 0000000..b7846d7 --- /dev/null +++ b/src/main/java/org/example/alfs/controllers/TestController.java @@ -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"; + } + +} \ No newline at end of file From 0686af054ab6d68dc8404421d0bd9766e1f543b8 Mon Sep 17 00:00:00 2001 From: Adam Ottosson Date: Thu, 9 Apr 2026 11:28:10 +0200 Subject: [PATCH 04/15] feat: add SecurityUtils for accessing authenticated user - Provides helper method to retrieve current user from SecurityContext --- .../example/alfs/security/SecurityUtils.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/main/java/org/example/alfs/security/SecurityUtils.java diff --git a/src/main/java/org/example/alfs/security/SecurityUtils.java b/src/main/java/org/example/alfs/security/SecurityUtils.java new file mode 100644 index 0000000..7aabc2b --- /dev/null +++ b/src/main/java/org/example/alfs/security/SecurityUtils.java @@ -0,0 +1,26 @@ +package org.example.alfs.security; + +import org.example.alfs.entities.User; +import org.example.alfs.repositories.UserRepository; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +@Component +public class SecurityUtils { + + private final UserRepository userRepository; + + public SecurityUtils(UserRepository userRepository) { + this.userRepository = userRepository; + } + + public User getCurrentUser() { + + String username = SecurityContextHolder.getContext() + .getAuthentication() + .getName(); + + return userRepository.findByUsername(username) + .orElseThrow(() -> new RuntimeException("Authenticated user not found in database")); + } +} \ No newline at end of file From 65f00fb03af52dd5733e432f0f65785b6f02212e Mon Sep 17 00:00:00 2001 From: Adam Ottosson Date: Thu, 9 Apr 2026 11:28:51 +0200 Subject: [PATCH 05/15] chore: adds jsonwebtoken dependency --- pom.xml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pom.xml b/pom.xml index 72ea7f6..4d37f7e 100644 --- a/pom.xml +++ b/pom.xml @@ -91,6 +91,23 @@ org.springframework.boot spring-boot-starter-data-jpa + + io.jsonwebtoken + jjwt-api + 0.12.5 + + + io.jsonwebtoken + jjwt-impl + 0.12.5 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.12.5 + runtime + From d9a45ece58c8254226ad7c19de9a58c29f85a175 Mon Sep 17 00:00:00 2001 From: Adam Ottosson Date: Thu, 9 Apr 2026 12:04:11 +0200 Subject: [PATCH 06/15] chore: document JWT secret usage for development - Added comment explaining that the JWT secret is hardcoded for demo purposes - Clarified that environment variables should be used in production --- src/main/resources/application.properties | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8b46a90..50af5ba 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -16,5 +16,8 @@ spring.jpa.database-platform=org.hibernate.dialect.H2Dialect spring.jpa.hibernate.ddl-auto=update +# NOTE: This JWT secret is hardcoded for development/demo purposes only. +# In a production environment, it should be stored securely using +# environment variables or a secret manager. jwt.secret=bXlTdXBlclNlY3JldEtleU15U3VwZXJTZWNyZXRLZXk= jwt.expiration=86400000 \ No newline at end of file From cdfae3b02377f0855e99347eb7a7d20f1bc19eca Mon Sep 17 00:00:00 2001 From: Adam Ottosson Date: Thu, 9 Apr 2026 12:04:27 +0200 Subject: [PATCH 07/15] fix: remove insecure hash endpoint - Removed public BCrypt helper endpoint used for development - Eliminates unnecessary attack surface --- .../java/org/example/alfs/controllers/AuthController.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/main/java/org/example/alfs/controllers/AuthController.java b/src/main/java/org/example/alfs/controllers/AuthController.java index a4e58cd..ad42d39 100644 --- a/src/main/java/org/example/alfs/controllers/AuthController.java +++ b/src/main/java/org/example/alfs/controllers/AuthController.java @@ -45,10 +45,4 @@ public void signup(@Valid @RequestBody SignupRequestDTO request) { authService.signup(request); } - // This is just for development purposes. Should be deleted later! - @GetMapping("/hash") - public String hash() { - return new org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder() - .encode("test123"); - } } From 6194a7dc284b0d6861782002af05aa2cee98d239 Mon Sep 17 00:00:00 2001 From: Adam Ottosson Date: Thu, 9 Apr 2026 12:04:41 +0200 Subject: [PATCH 08/15] fix: improve authentication security and error handling - Unified error response to prevent user enumeration - Removed logging of sensitive data (passwords and hashes) - Return consistent "Bad credentials" for invalid login attempts --- src/main/java/org/example/alfs/services/AuthService.java | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/example/alfs/services/AuthService.java b/src/main/java/org/example/alfs/services/AuthService.java index 004671e..a8cec36 100644 --- a/src/main/java/org/example/alfs/services/AuthService.java +++ b/src/main/java/org/example/alfs/services/AuthService.java @@ -26,16 +26,11 @@ public AuthService(PasswordEncoder passwordEncoder, UserRepository userRepositor public User login(String username, String password) { User user = userRepository.findByUsername(username) - .orElseThrow(() -> new RuntimeException("User not found")); + .orElseThrow(() -> new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Bad credentials")); - System.out.println("INPUT PASSWORD: " + password); - System.out.println("DB HASH: " + user.getPasswordHash()); - boolean matches = passwordEncoder.matches(password, user.getPasswordHash()); - System.out.println("MATCH RESULT: " + matches); - - if (!matches) { + if (!passwordEncoder.matches(password, user.getPasswordHash())) { throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Bad credentials"); } From 374de8756beb013fb7acc251ac2d1f2a297e88bb Mon Sep 17 00:00:00 2001 From: Adam Ottosson Date: Thu, 9 Apr 2026 12:05:05 +0200 Subject: [PATCH 09/15] fix: remove sensitive logging and handle invalid JWT safely - Removed logging of JWT tokens and user information - Prevented server errors by handling missing users gracefully - Treat invalid tokens as unauthenticated requests --- .../alfs/security/JwtAuthenticationFilter.java | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/example/alfs/security/JwtAuthenticationFilter.java b/src/main/java/org/example/alfs/security/JwtAuthenticationFilter.java index 0c1c658..6399db9 100644 --- a/src/main/java/org/example/alfs/security/JwtAuthenticationFilter.java +++ b/src/main/java/org/example/alfs/security/JwtAuthenticationFilter.java @@ -40,8 +40,6 @@ protected void doFilterInternal(HttpServletRequest request, FilterChain filterChain) throws ServletException, IOException { - System.out.println("FILTER RUNNING: " + request.getRequestURI()); - final String authHeader = request.getHeader("Authorization"); // if no token, keep going @@ -51,21 +49,22 @@ protected void doFilterInternal(HttpServletRequest request, } String jwt = authHeader.substring(7); - System.out.println("JWT: " + jwt); String username; try { username = jwtService.extractUsername(jwt); - System.out.println("USERNAME FROM TOKEN: " + username); } catch (Exception e) { - System.out.println("TOKEN PARSE FAILED"); filterChain.doFilter(request, response); return; } if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { - User user = userRepository.findByUsername(username) - .orElseThrow(() -> new RuntimeException("User not found")); + 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. @@ -80,10 +79,7 @@ protected void doFilterInternal(HttpServletRequest request, List.of(new SimpleGrantedAuthority("ROLE_" + user.getRole().name())) ); - System.out.println("USER FROM DB: " + user.getUsername()); - boolean valid = jwtService.isTokenValid(jwt, user); - System.out.println("TOKEN VALID: " + valid); if (valid) { UsernamePasswordAuthenticationToken authToken = From 117ae6a30aad30a19c0681861b58c693b4e168c5 Mon Sep 17 00:00:00 2001 From: Adam Ottosson Date: Thu, 9 Apr 2026 12:05:16 +0200 Subject: [PATCH 10/15] chore: document development-only security configuration - Added comments explaining disabled CSRF and frame options - Clarified that configuration is intended for development only --- src/main/java/org/example/alfs/config/SecurityConfig.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/example/alfs/config/SecurityConfig.java b/src/main/java/org/example/alfs/config/SecurityConfig.java index ccb9bea..2a72a6e 100644 --- a/src/main/java/org/example/alfs/config/SecurityConfig.java +++ b/src/main/java/org/example/alfs/config/SecurityConfig.java @@ -22,6 +22,7 @@ public class SecurityConfig { public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http + // Only for development (H2 console support) .securityMatcher("/**") .csrf(csrf -> csrf.disable()) From ebad5bb197bcbb26d28b44dfabdc692fc77a66fc Mon Sep 17 00:00:00 2001 From: Adam Ottosson Date: Thu, 9 Apr 2026 12:05:28 +0200 Subject: [PATCH 11/15] fix: add null check for authentication in SecurityUtils - Prevent potential NullPointerException when authentication is missing - Ensure user is authenticated before accessing SecurityContext --- .../java/org/example/alfs/security/SecurityUtils.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/example/alfs/security/SecurityUtils.java b/src/main/java/org/example/alfs/security/SecurityUtils.java index 7aabc2b..b2ed239 100644 --- a/src/main/java/org/example/alfs/security/SecurityUtils.java +++ b/src/main/java/org/example/alfs/security/SecurityUtils.java @@ -16,9 +16,13 @@ public SecurityUtils(UserRepository userRepository) { public User getCurrentUser() { - String username = SecurityContextHolder.getContext() - .getAuthentication() - .getName(); + var authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication == null || !authentication.isAuthenticated()) { + throw new RuntimeException("No authenticated user in security context"); + } + + String username = authentication.getName(); return userRepository.findByUsername(username) .orElseThrow(() -> new RuntimeException("Authenticated user not found in database")); From 555bd375e511510e43e2919a0c7f9c3ca88f4611 Mon Sep 17 00:00:00 2001 From: Adam Ottosson Date: Thu, 9 Apr 2026 12:13:09 +0200 Subject: [PATCH 12/15] updates javaDoc text --- .../java/org/example/alfs/controllers/AuthController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/example/alfs/controllers/AuthController.java b/src/main/java/org/example/alfs/controllers/AuthController.java index ad42d39..a21e97a 100644 --- a/src/main/java/org/example/alfs/controllers/AuthController.java +++ b/src/main/java/org/example/alfs/controllers/AuthController.java @@ -38,8 +38,8 @@ public LoginResponseDTO login(@Valid @RequestBody LoginRequestDTO request) { /** - * Handles user login by validating credentials and returning user details. - */ + * Handles user signup by validating input and creating a new account. + * */ @PostMapping("/signup") public void signup(@Valid @RequestBody SignupRequestDTO request) { authService.signup(request); From d7e3c25e6b461828ebb1262c19b7b60e301bd16a Mon Sep 17 00:00:00 2001 From: Adam Ottosson Date: Thu, 9 Apr 2026 12:13:21 +0200 Subject: [PATCH 13/15] removed unused imports --- src/main/java/org/example/alfs/config/SecurityConfig.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/org/example/alfs/config/SecurityConfig.java b/src/main/java/org/example/alfs/config/SecurityConfig.java index 2a72a6e..479f686 100644 --- a/src/main/java/org/example/alfs/config/SecurityConfig.java +++ b/src/main/java/org/example/alfs/config/SecurityConfig.java @@ -3,8 +3,6 @@ import org.example.alfs.security.JwtAuthenticationFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.security.config.Customizer; 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; From 138c1c078a8bb8ef493cb27d3688102eb7f29e23 Mon Sep 17 00:00:00 2001 From: Adam Ottosson Date: Thu, 9 Apr 2026 12:19:06 +0200 Subject: [PATCH 14/15] fix javadoc comment --- .../java/org/example/alfs/controllers/AuthController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/example/alfs/controllers/AuthController.java b/src/main/java/org/example/alfs/controllers/AuthController.java index a21e97a..1299992 100644 --- a/src/main/java/org/example/alfs/controllers/AuthController.java +++ b/src/main/java/org/example/alfs/controllers/AuthController.java @@ -38,8 +38,8 @@ public LoginResponseDTO login(@Valid @RequestBody LoginRequestDTO request) { /** - * Handles user signup by validating input and creating a new account. - * */ + * Handles user signup by validating input and creating a new account. + */ @PostMapping("/signup") public void signup(@Valid @RequestBody SignupRequestDTO request) { authService.signup(request); From 78ceb89675b976e4e211667b91002ad3f0de6a66 Mon Sep 17 00:00:00 2001 From: Adam Ottosson Date: Thu, 9 Apr 2026 12:23:17 +0200 Subject: [PATCH 15/15] fix javadoc comment --- src/main/java/org/example/alfs/controllers/AuthController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/example/alfs/controllers/AuthController.java b/src/main/java/org/example/alfs/controllers/AuthController.java index 1299992..2235a65 100644 --- a/src/main/java/org/example/alfs/controllers/AuthController.java +++ b/src/main/java/org/example/alfs/controllers/AuthController.java @@ -22,7 +22,7 @@ public AuthController(AuthService authService, 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) {