From dffb74b1c099edd9a7fb51fda7f65315f2a24a8c Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 3 Dec 2025 09:42:36 +1100 Subject: [PATCH 1/2] Refactor authentication filter and entrypoint --- .../security/JwtAuthenticationEntryPoint.java | 29 ++++-- .../security/JwtAuthenticationFilter.java | 97 +++++++++++-------- 2 files changed, 79 insertions(+), 47 deletions(-) diff --git a/src/main/java/org/nkcoder/security/JwtAuthenticationEntryPoint.java b/src/main/java/org/nkcoder/security/JwtAuthenticationEntryPoint.java index 51ced52..4be99fe 100644 --- a/src/main/java/org/nkcoder/security/JwtAuthenticationEntryPoint.java +++ b/src/main/java/org/nkcoder/security/JwtAuthenticationEntryPoint.java @@ -1,10 +1,11 @@ package org.nkcoder.security; import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.servlet.ServletException; +import io.jsonwebtoken.ExpiredJwtException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; +import org.apache.logging.log4j.util.Strings; import org.nkcoder.dto.common.ApiResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,6 +19,8 @@ public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationEntryPoint.class); + private static final String CONTENT_TYPE_JSON = "application/json"; + private final ObjectMapper objectMapper; @Autowired @@ -30,18 +33,30 @@ public void commence( HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) - throws IOException, ServletException { + throws IOException { - logger.error("Unauthorized error: {}", authException.getMessage()); + logger.debug("Unauthorized access attempt to: {}", request.getRequestURI()); - response.setContentType("application/json"); + response.setContentType(CONTENT_TYPE_JSON); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - ApiResponse apiResponse = - ApiResponse.error("Unauthorized: " + authException.getMessage()); + String errorMessage = determineErrorMessage(authException); + ApiResponse apiResponse = ApiResponse.error(errorMessage); objectMapper.writeValue(response.getOutputStream(), apiResponse); response.getOutputStream().flush(); - response.getOutputStream().close(); + // Do NOT close the stream - let the servlet container manage it + } + + private String determineErrorMessage(AuthenticationException authException) { + if (authException.getCause() instanceof ExpiredJwtException) { + return "Token has expired"; + } + + if (Strings.isNotBlank(authException.getMessage())) { + return "Authentication required: " + authException.getMessage(); + } + + return "Authentication required"; } } diff --git a/src/main/java/org/nkcoder/security/JwtAuthenticationFilter.java b/src/main/java/org/nkcoder/security/JwtAuthenticationFilter.java index cb7b26e..37f9988 100644 --- a/src/main/java/org/nkcoder/security/JwtAuthenticationFilter.java +++ b/src/main/java/org/nkcoder/security/JwtAuthenticationFilter.java @@ -1,13 +1,16 @@ package org.nkcoder.security; import io.jsonwebtoken.Claims; -import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.UnsupportedJwtException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.List; +import java.util.Optional; import java.util.UUID; import org.jetbrains.annotations.NotNull; import org.nkcoder.enums.Role; @@ -29,6 +32,12 @@ @Component public class JwtAuthenticationFilter extends OncePerRequestFilter { + private static final String AUTHORIZATION_HEADER = "Authorization"; + private static final String BEARER_PREFIX = "Bearer "; + private static final String ATTRIBUTE_USER_ID = "userId"; + private static final String ATTRIBUTE_ROLE = "role"; + private static final String ATTRIBUTE_EMAIL = "email"; + private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class); private final JwtUtil jwtUtil; @@ -46,48 +55,56 @@ protected void doFilterInternal( throws ServletException, IOException { logger.debug("Processing authentication for request: {}", request.getRequestURI()); - try { - String jwt = getJwtFromRequest(request); - - if (StringUtils.hasText(jwt) && !jwtUtil.isTokenExpired(jwt)) { - Claims claims = jwtUtil.validateAccessToken(jwt); - - UUID userId = UUID.fromString(claims.getSubject()); - String email = claims.get("email", String.class); - String roleString = claims.get("role", String.class); - Role role = Role.valueOf(roleString); - - // Create authorities - List authorities = - List.of(new SimpleGrantedAuthority("ROLE_" + role.name())); - - UserDetails userDetails = new User(email, "", authorities); - UsernamePasswordAuthenticationToken authentication = - new UsernamePasswordAuthenticationToken(userDetails, null, authorities); - - authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - - // Set custom attributes - request.setAttribute("userId", userId); - request.setAttribute("email", email); - request.setAttribute("role", role); - - SecurityContextHolder.getContext().setAuthentication(authentication); - - logger.debug("Set authentication for user: {}", email); - } - } catch (JwtException e) { - logger.error("Cannot set user authentication: {}", e.getMessage()); - } + extractTokenFromRequest(request) + .ifPresent( + token -> { + try { + Claims claims = jwtUtil.validateAccessToken(token); + + UUID userId = UUID.fromString(claims.getSubject()); + String email = claims.get("email", String.class); + String roleString = claims.get("role", String.class); + Role role = Role.valueOf(roleString); + + // Create authorities + List authorities = + List.of(new SimpleGrantedAuthority("ROLE_" + role.name())); + + UserDetails userDetails = new User(email, "", authorities); + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken(userDetails, null, authorities); + + authentication.setDetails( + new WebAuthenticationDetailsSource().buildDetails(request)); + + // Set custom attributes + request.setAttribute(ATTRIBUTE_USER_ID, userId); + request.setAttribute(ATTRIBUTE_EMAIL, email); + request.setAttribute(ATTRIBUTE_ROLE, role); + + SecurityContextHolder.getContext().setAuthentication(authentication); + + logger.debug("Set authentication for userId: {}", userId); + } catch (ExpiredJwtException e) { + logger.error("JWT token expired: {}", e.getMessage()); + } catch (MalformedJwtException e) { + logger.error("Malformed JWT token: {}", e.getMessage()); + } catch (UnsupportedJwtException e) { + logger.error("Unsupported JWT token: {}", e.getMessage()); + } catch (SecurityException e) { + logger.error("JWT signature validation failed: {}", e.getMessage()); + } catch (IllegalArgumentException e) { + logger.error("JWT token compact of handler are invalid: {}", e.getMessage()); + } + }); filterChain.doFilter(request, response); } - private String getJwtFromRequest(HttpServletRequest request) { - String bearerToken = request.getHeader("Authorization"); - if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { - return bearerToken.substring(7); - } - return null; + private Optional extractTokenFromRequest(HttpServletRequest request) { + return Optional.ofNullable(request.getHeader(AUTHORIZATION_HEADER)) + .filter(StringUtils::hasText) + .filter(token -> token.startsWith(BEARER_PREFIX)) + .map(token -> token.substring(BEARER_PREFIX.length())); } } From cbfcb1d727244dff811b0c5da8285eecb4950d07 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 3 Dec 2025 09:52:32 +1100 Subject: [PATCH 2/2] Add gradle.properties and clean/test scripts --- auto/clean | 4 ++++ auto/test | 4 ++++ build.gradle.kts | 12 +++++++++++- gradle.properties | 12 ++++++++++++ 4 files changed, 31 insertions(+), 1 deletion(-) create mode 100755 auto/clean create mode 100755 auto/test create mode 100644 gradle.properties diff --git a/auto/clean b/auto/clean new file mode 100755 index 0000000..3bcd1a9 --- /dev/null +++ b/auto/clean @@ -0,0 +1,4 @@ +#!/usr/bin/env sh + +export SPRING_PROFILES_ACTIVE=test +./gradlew clean \ No newline at end of file diff --git a/auto/test b/auto/test new file mode 100755 index 0000000..27e39fe --- /dev/null +++ b/auto/test @@ -0,0 +1,4 @@ +#!/usr/bin/env sh + +export SPRING_PROFILES_ACTIVE=test +./gradlew test \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index e9af8dd..def86a1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -17,7 +17,9 @@ group = "org.nkcoder" version = "0.1.0" java { - sourceCompatibility = JavaVersion.VERSION_21 + toolchain { + languageVersion.set(JavaLanguageVersion.of(21)) + } } repositories { @@ -174,6 +176,14 @@ sourceSets { } } +tasks.test { + maxParallelForks = Runtime.getRuntime().availableProcessors() + failFast = true + + // Cache and incremental test execution + outputs.cacheIf { true } +} + // Test coverage: jacoco jacoco { toolVersion = "0.8.14" diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..5d24fb0 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,12 @@ +# gradle.properties +org.gradle.daemon=true +org.gradle.parallel=true +org.gradle.configureondemand=true +org.gradle.caching=true +# Configuration cache (ensure plugins/tasks support it) +org.gradle.configuration-cache=true +# Better memory for large projects +org.gradle.jvmargs=-Xmx2g -XX:+UseG1GC -Dfile.encoding=UTF-8 +# Kotlin incremental compilation +kotlin.incremental=true +kotlin.compiler.execution.strategy=daemon