From 74c0769af8ee9b3aed7a5d3b8b03ae37161605e4 Mon Sep 17 00:00:00 2001 From: markpernia Date: Mon, 22 Apr 2024 08:46:22 +0200 Subject: [PATCH 01/15] SMA-91: add logging for security --- backend/sportsmatch/src/main/resources/application.properties | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/sportsmatch/src/main/resources/application.properties b/backend/sportsmatch/src/main/resources/application.properties index 96924486..79aaaadd 100644 --- a/backend/sportsmatch/src/main/resources/application.properties +++ b/backend/sportsmatch/src/main/resources/application.properties @@ -20,3 +20,5 @@ app.sportsmingle.frontend.url=http://localhost:5173 app.sportsmingle.num-game-threshold=5,15,25 app.sportsmingle.k-factors=25.0,15.0,10.0 app.sportsmingle.k-factor-default=5.0 + +logging.level.org.springframework.security=DEBUG \ No newline at end of file From eb5412f5290f6f78510811b11df5d585cb85c9a6 Mon Sep 17 00:00:00 2001 From: markpernia Date: Mon, 22 Apr 2024 08:47:05 +0200 Subject: [PATCH 02/15] SMA-91: update security filter --- .../com/sportsmatch/auth/JwtAuthFilter.java | 30 +++++++++++-------- .../com/sportsmatch/auth/SecurityConfig.java | 25 ++++++---------- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/backend/sportsmatch/src/main/java/com/sportsmatch/auth/JwtAuthFilter.java b/backend/sportsmatch/src/main/java/com/sportsmatch/auth/JwtAuthFilter.java index 19e7dcd0..0ed165c0 100644 --- a/backend/sportsmatch/src/main/java/com/sportsmatch/auth/JwtAuthFilter.java +++ b/backend/sportsmatch/src/main/java/com/sportsmatch/auth/JwtAuthFilter.java @@ -1,6 +1,7 @@ package com.sportsmatch.auth; import com.sportsmatch.repositories.TokenRepository; +import io.jsonwebtoken.JwtException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -45,16 +46,25 @@ protected void doFilterInternal( try { jwt = authHeader.substring(7); userEmail = jwtService.extractUserName(jwt); - } catch (Exception e) { - filterChain.doFilter(request, response); - return; - } - - if (isUserAuthenticated(userEmail)) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail); - if (jwtService.isTokenValid(jwt, userDetails) && !tokenRepository.existsByToken(jwt)) { - updateSecurityContext(request, userDetails); + + // Check user authentication + if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) { + // Validate the token + if (jwtService.isTokenValid(jwt, userDetails)) { + if (!tokenRepository.existsByToken(jwt)) { + // Token is valid, update security context + updateSecurityContext(request, userDetails); + } + } else { + // Token is invalid or expired + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid or expired token"); + return; + } } + } catch (JwtException e) { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid token"); + return; } filterChain.doFilter(request, response); } @@ -67,10 +77,6 @@ public boolean isBearerTokenNotPresent(String authHeader) { return !authHeader.startsWith("Bearer ") || tokenParts.length != 2; } - public boolean isUserAuthenticated(String userEmail) { - return userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null; - } - public void updateSecurityContext(HttpServletRequest request, UserDetails userDetails) { UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); diff --git a/backend/sportsmatch/src/main/java/com/sportsmatch/auth/SecurityConfig.java b/backend/sportsmatch/src/main/java/com/sportsmatch/auth/SecurityConfig.java index 83b0b2fe..8aa97b28 100644 --- a/backend/sportsmatch/src/main/java/com/sportsmatch/auth/SecurityConfig.java +++ b/backend/sportsmatch/src/main/java/com/sportsmatch/auth/SecurityConfig.java @@ -4,7 +4,6 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpStatus; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; @@ -12,7 +11,6 @@ import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.HttpStatusEntryPoint; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.logout.LogoutHandler; import org.springframework.web.cors.CorsConfiguration; @@ -29,29 +27,24 @@ public class SecurityConfig { @Value("${app.sportsmingle.frontend.url}") private String frontendUrl; - private static final String[] WHITE_LIST_URL = { - "/api/v1/auth/**", - "/h2-console/**", - "/v3/api-docs", - "/v3/api-docs/**", - "/swagger-ui/**", - "/swagger-ui.html", - "/api/v1/places/search", - "/api/v1/event/nearby", - "/api/v1/sports/all" + private static final String[] API_WHITE_LIST_URL = { + "/api/v1/auth/**", "/api/v1/places/search", "/api/v1/event/nearby", "/api/v1/sports/all" }; + private final JwtAuthFilter jwtAuthFilter; private final AuthenticationProvider authenticationProvider; private final LogoutHandler logoutHandler; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http - .exceptionHandling((exception) -> exception.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))) - .csrf(AbstractHttpConfigurer::disable) + http.csrf(AbstractHttpConfigurer::disable) .headers(h -> h.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)) .authorizeHttpRequests( - r -> r.requestMatchers(WHITE_LIST_URL).permitAll().anyRequest().authenticated()) + r -> { + r.requestMatchers("/api/v1/**").authenticated(); + r.requestMatchers(API_WHITE_LIST_URL).permitAll(); + r.anyRequest().permitAll(); + }) .sessionManagement(s -> s.sessionCreationPolicy(STATELESS)) .authenticationProvider(authenticationProvider) .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) From 41f50e7cd17ef577fed978d5a3134fb566c60349 Mon Sep 17 00:00:00 2001 From: markpernia Date: Mon, 22 Apr 2024 09:50:24 +0200 Subject: [PATCH 03/15] SMA-91: rearrange request matchers --- .../src/main/java/com/sportsmatch/auth/SecurityConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/sportsmatch/src/main/java/com/sportsmatch/auth/SecurityConfig.java b/backend/sportsmatch/src/main/java/com/sportsmatch/auth/SecurityConfig.java index 8aa97b28..432a3f9f 100644 --- a/backend/sportsmatch/src/main/java/com/sportsmatch/auth/SecurityConfig.java +++ b/backend/sportsmatch/src/main/java/com/sportsmatch/auth/SecurityConfig.java @@ -41,8 +41,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .headers(h -> h.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)) .authorizeHttpRequests( r -> { - r.requestMatchers("/api/v1/**").authenticated(); r.requestMatchers(API_WHITE_LIST_URL).permitAll(); + r.requestMatchers("/api/v1/**").authenticated(); r.anyRequest().permitAll(); }) .sessionManagement(s -> s.sessionCreationPolicy(STATELESS)) From b9df5f497d6342fccbf3380c89017c0f2fb130da Mon Sep 17 00:00:00 2001 From: markpernia Date: Mon, 22 Apr 2024 09:59:33 +0200 Subject: [PATCH 04/15] SMA-91: added back exceptionHandling() --- .../src/main/java/com/sportsmatch/auth/SecurityConfig.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/backend/sportsmatch/src/main/java/com/sportsmatch/auth/SecurityConfig.java b/backend/sportsmatch/src/main/java/com/sportsmatch/auth/SecurityConfig.java index 432a3f9f..9bf4c821 100644 --- a/backend/sportsmatch/src/main/java/com/sportsmatch/auth/SecurityConfig.java +++ b/backend/sportsmatch/src/main/java/com/sportsmatch/auth/SecurityConfig.java @@ -4,6 +4,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; @@ -11,6 +12,7 @@ import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.HttpStatusEntryPoint; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.logout.LogoutHandler; import org.springframework.web.cors.CorsConfiguration; @@ -38,6 +40,10 @@ public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.csrf(AbstractHttpConfigurer::disable) + .exceptionHandling( + (exception) -> + exception.authenticationEntryPoint( + new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))) .headers(h -> h.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)) .authorizeHttpRequests( r -> { From f85595985ed9d238b460f19a39bc5f82a187919e Mon Sep 17 00:00:00 2001 From: markpernia Date: Mon, 22 Apr 2024 10:02:23 +0200 Subject: [PATCH 05/15] SMA-91: refactor dofilter --- .../com/sportsmatch/auth/JwtAuthFilter.java | 52 +++++++++---------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/backend/sportsmatch/src/main/java/com/sportsmatch/auth/JwtAuthFilter.java b/backend/sportsmatch/src/main/java/com/sportsmatch/auth/JwtAuthFilter.java index 0ed165c0..f3e0f5dc 100644 --- a/backend/sportsmatch/src/main/java/com/sportsmatch/auth/JwtAuthFilter.java +++ b/backend/sportsmatch/src/main/java/com/sportsmatch/auth/JwtAuthFilter.java @@ -28,10 +28,10 @@ public class JwtAuthFilter extends OncePerRequestFilter { @Override protected void doFilterInternal( - @NonNull HttpServletRequest request, - @NonNull HttpServletResponse response, - @NonNull FilterChain filterChain) - throws ServletException, IOException { + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) + throws ServletException, IOException { final String authHeader = request.getHeader("Authorization"); @@ -40,47 +40,45 @@ protected void doFilterInternal( return; } - final String jwt; - final String userEmail; - try { - jwt = authHeader.substring(7); - userEmail = jwtService.extractUserName(jwt); + final String jwt = authHeader.substring(7); + final String userEmail = jwtService.extractUserName(jwt); UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail); - // Check user authentication - if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) { - // Validate the token - if (jwtService.isTokenValid(jwt, userDetails)) { - if (!tokenRepository.existsByToken(jwt)) { - // Token is valid, update security context - updateSecurityContext(request, userDetails); - } - } else { - // Token is invalid or expired - response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid or expired token"); - return; + if (isAuthenticationNeeded(userEmail) && jwtService.isTokenValid(jwt, userDetails)) { + if (!tokenRepository.existsByToken(jwt)) { + updateSecurityContext(request, userDetails); } + } else { + sendUnauthorizedError(response, "Invalid or expired token"); + return; } } catch (JwtException e) { - response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid token"); + sendUnauthorizedError(response, "Invalid token"); return; } + filterChain.doFilter(request, response); } public boolean isBearerTokenNotPresent(String authHeader) { - if (authHeader == null) { - return true; - } + if (authHeader == null) return true; String[] tokenParts = authHeader.split(" "); return !authHeader.startsWith("Bearer ") || tokenParts.length != 2; } - public void updateSecurityContext(HttpServletRequest request, UserDetails userDetails) { + private boolean isAuthenticationNeeded(String userEmail) { + return userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null; + } + + private void updateSecurityContext(HttpServletRequest request, UserDetails userDetails) { UsernamePasswordAuthenticationToken authToken = - new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authToken); } + + private void sendUnauthorizedError(HttpServletResponse response, String errorMessage) throws IOException { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, errorMessage); + } } From 735350ed85c90f9de3ac251f6545f066eac71fb9 Mon Sep 17 00:00:00 2001 From: markpernia Date: Mon, 22 Apr 2024 12:01:20 +0200 Subject: [PATCH 06/15] SMA-91: added validation in login --- .../src/main/java/com/sportsmatch/dtos/AuthRequestDTO.java | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/sportsmatch/src/main/java/com/sportsmatch/dtos/AuthRequestDTO.java b/backend/sportsmatch/src/main/java/com/sportsmatch/dtos/AuthRequestDTO.java index 7716c85b..714711d0 100644 --- a/backend/sportsmatch/src/main/java/com/sportsmatch/dtos/AuthRequestDTO.java +++ b/backend/sportsmatch/src/main/java/com/sportsmatch/dtos/AuthRequestDTO.java @@ -14,6 +14,7 @@ public class AuthRequestDTO { @NotNull(message = "Email address is required.") @Email(message = "Please provide a valid email address") + @NotBlank(message = "email cannot be blank") private String email; @NotBlank(message = "Password cannot be blank") From 845ff0b75e9f143fca1da540210f846702829304 Mon Sep 17 00:00:00 2001 From: markpernia Date: Mon, 22 Apr 2024 12:01:44 +0200 Subject: [PATCH 07/15] SMA-91: removed exceptionHandling() --- .../src/main/java/com/sportsmatch/auth/SecurityConfig.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/backend/sportsmatch/src/main/java/com/sportsmatch/auth/SecurityConfig.java b/backend/sportsmatch/src/main/java/com/sportsmatch/auth/SecurityConfig.java index 9bf4c821..64d7ed0b 100644 --- a/backend/sportsmatch/src/main/java/com/sportsmatch/auth/SecurityConfig.java +++ b/backend/sportsmatch/src/main/java/com/sportsmatch/auth/SecurityConfig.java @@ -29,7 +29,7 @@ public class SecurityConfig { @Value("${app.sportsmingle.frontend.url}") private String frontendUrl; - private static final String[] API_WHITE_LIST_URL = { + static final String[] API_WHITE_LIST_URL = { "/api/v1/auth/**", "/api/v1/places/search", "/api/v1/event/nearby", "/api/v1/sports/all" }; @@ -40,10 +40,6 @@ public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.csrf(AbstractHttpConfigurer::disable) - .exceptionHandling( - (exception) -> - exception.authenticationEntryPoint( - new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))) .headers(h -> h.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)) .authorizeHttpRequests( r -> { From aa14c2abdcb60e878104ea794bf8317bf28c0d8d Mon Sep 17 00:00:00 2001 From: markpernia Date: Mon, 22 Apr 2024 12:02:34 +0200 Subject: [PATCH 08/15] SMA-91: refactor jwtFilter added additional checks --- .../com/sportsmatch/auth/JwtAuthFilter.java | 63 +++++++++++++------ 1 file changed, 44 insertions(+), 19 deletions(-) diff --git a/backend/sportsmatch/src/main/java/com/sportsmatch/auth/JwtAuthFilter.java b/backend/sportsmatch/src/main/java/com/sportsmatch/auth/JwtAuthFilter.java index f3e0f5dc..4ab89398 100644 --- a/backend/sportsmatch/src/main/java/com/sportsmatch/auth/JwtAuthFilter.java +++ b/backend/sportsmatch/src/main/java/com/sportsmatch/auth/JwtAuthFilter.java @@ -7,6 +7,8 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; +import java.util.Arrays; +import java.util.List; import lombok.NonNull; import lombok.RequiredArgsConstructor; @@ -15,9 +17,12 @@ import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; +import static com.sportsmatch.auth.SecurityConfig.API_WHITE_LIST_URL; + @Component @RequiredArgsConstructor public class JwtAuthFilter extends OncePerRequestFilter { @@ -26,33 +31,35 @@ public class JwtAuthFilter extends OncePerRequestFilter { private final UserDetailsService userDetailsService; private final TokenRepository tokenRepository; + private final List protectedPaths = + List.of(new AntPathRequestMatcher("/api/v1/**")); + @Override protected void doFilterInternal( - @NonNull HttpServletRequest request, - @NonNull HttpServletResponse response, - @NonNull FilterChain filterChain) - throws ServletException, IOException { + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) + throws ServletException, IOException { final String authHeader = request.getHeader("Authorization"); + boolean requiresAuthentication = protectedPaths.stream().anyMatch(m -> m.matches(request)); + boolean isWhitelisted = + Arrays.stream(API_WHITE_LIST_URL) + .anyMatch(path -> new AntPathRequestMatcher(path).matches(request)); - if (isBearerTokenNotPresent(authHeader)) { + // Allow unsecured requests to pass through + if (!requiresAuthentication || isWhitelisted) { filterChain.doFilter(request, response); return; } + if (isBearerTokenNotPresent(authHeader)) { + sendUnauthorizedError(response, "Token not present or expired token"); + return; + } + try { - final String jwt = authHeader.substring(7); - final String userEmail = jwtService.extractUserName(jwt); - UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail); - - if (isAuthenticationNeeded(userEmail) && jwtService.isTokenValid(jwt, userDetails)) { - if (!tokenRepository.existsByToken(jwt)) { - updateSecurityContext(request, userDetails); - } - } else { - sendUnauthorizedError(response, "Invalid or expired token"); - return; - } + handleJwtAuthentication(authHeader, request, response); } catch (JwtException e) { sendUnauthorizedError(response, "Invalid token"); return; @@ -61,6 +68,23 @@ protected void doFilterInternal( filterChain.doFilter(request, response); } + private void handleJwtAuthentication( + String authHeader, HttpServletRequest request, HttpServletResponse response) + throws IOException { + final String jwt = authHeader.substring(7); + final String userEmail = jwtService.extractUserName(jwt); + UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail); + + if (isAuthenticationNeeded(userEmail) && jwtService.isTokenValid(jwt, userDetails)) { + if (!tokenRepository.existsByToken(jwt)) { + updateSecurityContext(request, userDetails); + } + } else { + sendUnauthorizedError(response, "Invalid or expired token"); + return; + } + } + public boolean isBearerTokenNotPresent(String authHeader) { if (authHeader == null) return true; String[] tokenParts = authHeader.split(" "); @@ -73,12 +97,13 @@ private boolean isAuthenticationNeeded(String userEmail) { private void updateSecurityContext(HttpServletRequest request, UserDetails userDetails) { UsernamePasswordAuthenticationToken authToken = - new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authToken); } - private void sendUnauthorizedError(HttpServletResponse response, String errorMessage) throws IOException { + private void sendUnauthorizedError(HttpServletResponse response, String errorMessage) + throws IOException { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, errorMessage); } } From e7d5e9aeb61d2842d496c7ce1f8332dc4168ddea Mon Sep 17 00:00:00 2001 From: markpernia Date: Mon, 22 Apr 2024 12:32:40 +0200 Subject: [PATCH 09/15] SMA-91: resolve style check --- .../src/main/java/com/sportsmatch/auth/JwtAuthFilter.java | 4 +++- .../src/main/java/com/sportsmatch/auth/SecurityConfig.java | 2 -- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/sportsmatch/src/main/java/com/sportsmatch/auth/JwtAuthFilter.java b/backend/sportsmatch/src/main/java/com/sportsmatch/auth/JwtAuthFilter.java index 4ab89398..e6f9d7a6 100644 --- a/backend/sportsmatch/src/main/java/com/sportsmatch/auth/JwtAuthFilter.java +++ b/backend/sportsmatch/src/main/java/com/sportsmatch/auth/JwtAuthFilter.java @@ -86,7 +86,9 @@ private void handleJwtAuthentication( } public boolean isBearerTokenNotPresent(String authHeader) { - if (authHeader == null) return true; + if (authHeader == null) { + return true; + } String[] tokenParts = authHeader.split(" "); return !authHeader.startsWith("Bearer ") || tokenParts.length != 2; } diff --git a/backend/sportsmatch/src/main/java/com/sportsmatch/auth/SecurityConfig.java b/backend/sportsmatch/src/main/java/com/sportsmatch/auth/SecurityConfig.java index 64d7ed0b..aca25ff9 100644 --- a/backend/sportsmatch/src/main/java/com/sportsmatch/auth/SecurityConfig.java +++ b/backend/sportsmatch/src/main/java/com/sportsmatch/auth/SecurityConfig.java @@ -4,7 +4,6 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpStatus; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; @@ -12,7 +11,6 @@ import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.HttpStatusEntryPoint; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.logout.LogoutHandler; import org.springframework.web.cors.CorsConfiguration; From aa11164baba9b33d172435bc34a475ddaa04dc2f Mon Sep 17 00:00:00 2001 From: markpernia Date: Mon, 22 Apr 2024 15:19:17 +0200 Subject: [PATCH 10/15] SMA-91: update JwtAuthFilter --- .../src/main/java/com/sportsmatch/auth/JwtAuthFilter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/sportsmatch/src/main/java/com/sportsmatch/auth/JwtAuthFilter.java b/backend/sportsmatch/src/main/java/com/sportsmatch/auth/JwtAuthFilter.java index e6f9d7a6..65e33333 100644 --- a/backend/sportsmatch/src/main/java/com/sportsmatch/auth/JwtAuthFilter.java +++ b/backend/sportsmatch/src/main/java/com/sportsmatch/auth/JwtAuthFilter.java @@ -54,7 +54,7 @@ protected void doFilterInternal( } if (isBearerTokenNotPresent(authHeader)) { - sendUnauthorizedError(response, "Token not present or expired token"); + sendUnauthorizedError(response, "Token not present"); return; } From c917e613fc3655ebeff8bb7087a6c3fdb42a5293 Mon Sep 17 00:00:00 2001 From: markpernia Date: Mon, 22 Apr 2024 15:30:08 +0200 Subject: [PATCH 11/15] SMA-91: remove return --- .../src/main/java/com/sportsmatch/auth/JwtAuthFilter.java | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/sportsmatch/src/main/java/com/sportsmatch/auth/JwtAuthFilter.java b/backend/sportsmatch/src/main/java/com/sportsmatch/auth/JwtAuthFilter.java index 65e33333..8c481f66 100644 --- a/backend/sportsmatch/src/main/java/com/sportsmatch/auth/JwtAuthFilter.java +++ b/backend/sportsmatch/src/main/java/com/sportsmatch/auth/JwtAuthFilter.java @@ -81,7 +81,6 @@ private void handleJwtAuthentication( } } else { sendUnauthorizedError(response, "Invalid or expired token"); - return; } } From 17b3db2c0f2f772a16b7da15b0b1768582a33843 Mon Sep 17 00:00:00 2001 From: markpernia Date: Mon, 22 Apr 2024 18:24:54 +0200 Subject: [PATCH 12/15] SMA-91: refactor JwtAuthFilter and PlaceControllerTest --- .../com/sportsmatch/auth/JwtAuthFilter.java | 7 +- .../controllers/PlaceControllerTest.java | 65 +++++++++++++------ 2 files changed, 51 insertions(+), 21 deletions(-) diff --git a/backend/sportsmatch/src/main/java/com/sportsmatch/auth/JwtAuthFilter.java b/backend/sportsmatch/src/main/java/com/sportsmatch/auth/JwtAuthFilter.java index 8c481f66..3b5fac2a 100644 --- a/backend/sportsmatch/src/main/java/com/sportsmatch/auth/JwtAuthFilter.java +++ b/backend/sportsmatch/src/main/java/com/sportsmatch/auth/JwtAuthFilter.java @@ -75,7 +75,12 @@ private void handleJwtAuthentication( final String userEmail = jwtService.extractUserName(jwt); UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail); - if (isAuthenticationNeeded(userEmail) && jwtService.isTokenValid(jwt, userDetails)) { + if (!isAuthenticationNeeded(userEmail)) { + // No authentication needed for this request + return; + } + + if (jwtService.isTokenValid(jwt, userDetails)) { if (!tokenRepository.existsByToken(jwt)) { updateSecurityContext(request, userDetails); } diff --git a/backend/sportsmatch/src/test/java/com/sportsmatch/controllers/PlaceControllerTest.java b/backend/sportsmatch/src/test/java/com/sportsmatch/controllers/PlaceControllerTest.java index 71de20d2..1e3dc94a 100644 --- a/backend/sportsmatch/src/test/java/com/sportsmatch/controllers/PlaceControllerTest.java +++ b/backend/sportsmatch/src/test/java/com/sportsmatch/controllers/PlaceControllerTest.java @@ -2,7 +2,12 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.sportsmatch.BaseTest; +import com.sportsmatch.auth.JwtService; import com.sportsmatch.dtos.PlaceDTO; +import com.sportsmatch.models.Gender; +import com.sportsmatch.models.Role; +import com.sportsmatch.models.User; +import com.sportsmatch.repositories.UserRepository; import com.sportsmatch.services.PlaceService; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -24,20 +29,20 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; - @ExtendWith(SpringExtension.class) @AutoConfigureMockMvc @SpringBootTest class PlaceControllerTest extends BaseTest { - @Autowired - private MockMvc mockMvc; + @Autowired private MockMvc mockMvc; + + @Autowired private ObjectMapper objectMapper; + + @Autowired private JwtService jwtService; - @Autowired - private ObjectMapper objectMapper; + @Autowired private UserRepository userRepository; - @MockBean - private PlaceService placeService; + @MockBean private PlaceService placeService; PlaceDTO createPlaceDTO1() { return PlaceDTO.builder() @@ -67,9 +72,11 @@ void addNewPlaceShouldReturn403NotAuthenticatedUser() throws Exception { .thenReturn(ResponseEntity.ok("Place added successfully")); // Perform a POST request to add a new place without authentication - mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/places/add") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(placeDTO))) + mockMvc + .perform( + MockMvcRequestBuilders.post("/api/v1/places/add") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(placeDTO))) // Verify that the response status is 401 Forbidden .andExpect(MockMvcResultMatchers.status().isUnauthorized()); } @@ -84,10 +91,25 @@ void addNewPlaceShouldReturn200AndSuccessfulMessageAuthenticatedUser() throws Ex when(placeService.addNewPlace(any(PlaceDTO.class))) .thenReturn(ResponseEntity.ok("Place successfully added")); + // Create User + User user = + User.builder() + .email("testuser@mail.com") + .password("password") + .name("testuser") + .gender(Gender.MALE) + .role(Role.USER) + .build(); + userRepository.save(user); + String token = jwtService.generateToken(user); + // Perform a POST request to add a new place with authentication - mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/places/add") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(placeDTO))) + mockMvc + .perform( + MockMvcRequestBuilders.post("/api/v1/places/add") + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(placeDTO))) // Verify that the response status is 200 OK .andExpect(MockMvcResultMatchers.status().isOk()) // Verify that the response content contains the expected success message @@ -105,16 +127,19 @@ void searchPlaces() throws Exception { when(placeService.searchPlaces(any(String.class))).thenReturn(expectedPlaces); // Perform a GET request to search for places with a name parameter "test" - mockMvc.perform(MockMvcRequestBuilders.get("/api/v1/places/search") - .param("name", "test") - .contentType(MediaType.APPLICATION_JSON)) + mockMvc + .perform( + MockMvcRequestBuilders.get("/api/v1/places/search") + .param("name", "test") + .contentType(MediaType.APPLICATION_JSON)) // Verify the response is an array .andExpect(MockMvcResultMatchers.jsonPath("$").isArray()) - // Verify the name of the first place in the response matches the name of the first expected place + // Verify the name of the first place in the response matches the name of the first expected + // place .andExpect(MockMvcResultMatchers.jsonPath("$[0].name").value("Test Place Name1")) - // Verify the name of the second place in the response matches the name of the second expected place + // Verify the name of the second place in the response matches the name of the second + // expected place .andExpect(MockMvcResultMatchers.jsonPath("$[1].name").value("Test Place Name2")); - } -} \ No newline at end of file +} From 4c639bbaaa7fa754a3f7fbd0adbf6ddfc1d79f0b Mon Sep 17 00:00:00 2001 From: markpernia Date: Mon, 22 Apr 2024 20:07:19 +0200 Subject: [PATCH 13/15] SMA-91: added global ExceptionHandler --- .../configs/GlobalExceptionHandler.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 backend/sportsmatch/src/main/java/com/sportsmatch/configs/GlobalExceptionHandler.java diff --git a/backend/sportsmatch/src/main/java/com/sportsmatch/configs/GlobalExceptionHandler.java b/backend/sportsmatch/src/main/java/com/sportsmatch/configs/GlobalExceptionHandler.java new file mode 100644 index 00000000..e08e8da1 --- /dev/null +++ b/backend/sportsmatch/src/main/java/com/sportsmatch/configs/GlobalExceptionHandler.java @@ -0,0 +1,33 @@ +package com.sportsmatch.configs; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +import javax.security.sasl.AuthenticationException; + +@ControllerAdvice +public class GlobalExceptionHandler { + + // handle request body + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity handleHttpMessageNotReadableException( + HttpMessageNotReadableException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); + } + + // handle parameter with @Valid + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleMethodArgumentNotValidException( + MethodArgumentNotValidException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); + } + + @ExceptionHandler(AuthenticationException.class) + public ResponseEntity handleAuthenticationException(AuthenticationException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(e.getMessage()); + } +} From 54bb2575699b3aec566ca90ed74ab8b9227a6a2c Mon Sep 17 00:00:00 2001 From: markpernia Date: Mon, 22 Apr 2024 20:13:30 +0200 Subject: [PATCH 14/15] SMA-91: updated ExceptionHandler --- .../java/com/sportsmatch/configs/GlobalExceptionHandler.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/backend/sportsmatch/src/main/java/com/sportsmatch/configs/GlobalExceptionHandler.java b/backend/sportsmatch/src/main/java/com/sportsmatch/configs/GlobalExceptionHandler.java index e08e8da1..aaf4def4 100644 --- a/backend/sportsmatch/src/main/java/com/sportsmatch/configs/GlobalExceptionHandler.java +++ b/backend/sportsmatch/src/main/java/com/sportsmatch/configs/GlobalExceptionHandler.java @@ -26,6 +26,11 @@ public ResponseEntity handleMethodArgumentNotValidException( return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); } + @ExceptionHandler(NullPointerException.class) + public ResponseEntity handleNullPointerException(NullPointerException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); + } + @ExceptionHandler(AuthenticationException.class) public ResponseEntity handleAuthenticationException(AuthenticationException e) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(e.getMessage()); From 18e8cd1ad7edd842fdf289681c1e1912075d42bc Mon Sep 17 00:00:00 2001 From: markpernia Date: Tue, 23 Apr 2024 18:13:37 +0200 Subject: [PATCH 15/15] SMA-91: removed exception handler for NullPointerException --- .../java/com/sportsmatch/configs/GlobalExceptionHandler.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/backend/sportsmatch/src/main/java/com/sportsmatch/configs/GlobalExceptionHandler.java b/backend/sportsmatch/src/main/java/com/sportsmatch/configs/GlobalExceptionHandler.java index aaf4def4..e08e8da1 100644 --- a/backend/sportsmatch/src/main/java/com/sportsmatch/configs/GlobalExceptionHandler.java +++ b/backend/sportsmatch/src/main/java/com/sportsmatch/configs/GlobalExceptionHandler.java @@ -26,11 +26,6 @@ public ResponseEntity handleMethodArgumentNotValidException( return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); } - @ExceptionHandler(NullPointerException.class) - public ResponseEntity handleNullPointerException(NullPointerException e) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); - } - @ExceptionHandler(AuthenticationException.class) public ResponseEntity handleAuthenticationException(AuthenticationException e) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(e.getMessage());