google-site-verification: google61fe8ba583a51912.html
CSRF Protection in REST APIs with Java: Comprehensive Security Guide

Article

Cross-Site Request Forgery (CSRF) is a security vulnerability that allows attackers to trick users into performing unwanted actions in web applications. While traditional web applications are vulnerable to CSRF, REST APIs also need protection, especially when dealing with state-changing operations. This guide explores CSRF protection strategies for REST APIs in Java applications.


Understanding CSRF in REST APIs

CSRF Vulnerability in APIs:

  • Traditional CSRF: Browser automatically includes cookies in requests
  • API CSRF: Applications using session cookies or authentication tokens
  • State-Changing Operations: POST, PUT, DELETE, PATCH requests

When CSRF Protection is Needed:

  • Cookie-based authentication: Session cookies are vulnerable
  • Mixed authentication: Apps using both tokens and cookies
  • CORS-enabled APIs: Cross-origin requests need protection

When CSRF Protection Might Not Be Needed:

  • Stateless JWT tokens (if properly implemented)
  • API key authentication
  • OAuth2 with token-based flow

Project Setup and Dependencies

Maven Dependencies

<properties> <spring-boot.version>2.7.0</spring-boot.version> <jjwt.version>0.11.5</jjwt.version> </properties> <dependencies> <!-- Spring Boot Starter Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- Spring Boot Starter Security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- JWT for token-based CSRF --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>${jjwt.version}</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>${jjwt.version}</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>${jjwt.version}</version> <scope>runtime</scope> </dependency> <!-- For testing --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <version>${spring-boot.version}</version> <scope>test</scope> </dependency> </dependencies>

Spring Security CSRF Protection

1. Basic Spring Security CSRF Configuration

import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.csrf.CookieCsrfTokenRepository; import org.springframework.security.web.csrf.CsrfTokenRepository; @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf .csrfTokenRepository(csrfTokenRepository()) // Exclude certain endpoints from CSRF protection .ignoringAntMatchers("/api/public/**", "/api/webhook/**") ) .authorizeHttpRequests(authz -> authz .requestMatchers("/api/public/**").permitAll() .requestMatchers("/api/admin/**").hasRole("ADMIN") .requestMatchers("/api/**").authenticated() .anyRequest().permitAll() ); return http.build(); } @Bean public CsrfTokenRepository csrfTokenRepository() { // Store CSRF token in a cookie named "XSRF-TOKEN" // and expect header "X-XSRF-TOKEN" in requests return CookieCsrfTokenRepository.withHttpOnlyFalse(); } }

2. Custom CSRF Token Repository

import org.springframework.security.web.csrf.CsrfToken; import org.springframework.security.web.csrf.CsrfTokenRepository; import org.springframework.security.web.csrf.DefaultCsrfToken; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.UUID; public class CustomCsrfTokenRepository implements CsrfTokenRepository { private static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf"; private static final String DEFAULT_CSRF_HEADER_NAME = "X-CSRF-TOKEN"; private static final String DEFAULT_CSRF_TOKEN_ATTR_NAME = CustomCsrfTokenRepository.class.getName() + ".TOKEN"; @Override public CsrfToken generateToken(HttpServletRequest request) { return new DefaultCsrfToken( DEFAULT_CSRF_HEADER_NAME, DEFAULT_CSRF_PARAMETER_NAME, createNewToken() ); } @Override public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) { if (token == null) { // Remove token from session if null request.getSession().removeAttribute(DEFAULT_CSRF_TOKEN_ATTR_NAME); } else { // Store token in session request.getSession().setAttribute(DEFAULT_CSRF_TOKEN_ATTR_NAME, token); } } @Override public CsrfToken loadToken(HttpServletRequest request) { return (CsrfToken) request.getSession().getAttribute(DEFAULT_CSRF_TOKEN_ATTR_NAME); } private String createNewToken() { return UUID.randomUUID().toString(); } }

3. CSRF Token Controller

import org.springframework.security.web.csrf.CsrfToken; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; @RestController public class CsrfTokenController { @GetMapping("/api/csrf-token") public CsrfTokenResponse getCsrfToken(HttpServletRequest request) { CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); if (csrfToken != null) { return new CsrfTokenResponse( csrfToken.getToken(), csrfToken.getHeaderName(), csrfToken.getParameterName() ); } throw new RuntimeException("CSRF token not available"); } public static class CsrfTokenResponse { private final String token; private final String headerName; private final String parameterName; public CsrfTokenResponse(String token, String headerName, String parameterName) { this.token = token; this.headerName = headerName; this.parameterName = parameterName; } // Getters public String getToken() { return token; } public String getHeaderName() { return headerName; } public String getParameterName() { return parameterName; } } }

Custom CSRF Protection Implementation

1. Custom CSRF Filter

import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; import java.util.Arrays; import java.util.HashSet; import java.util.Set; import java.util.UUID; public class CustomCsrfFilter extends OncePerRequestFilter { private static final String CSRF_TOKEN_HEADER = "X-CSRF-TOKEN"; private static final String CSRF_TOKEN_SESSION_ATTR = "CSRF_TOKEN"; // HTTP methods that require CSRF protection private final Set<String> protectedMethods = new HashSet<>( Arrays.asList("POST", "PUT", "PATCH", "DELETE") ); // Paths to exclude from CSRF protection private final Set<String> excludedPaths = new HashSet<>( Arrays.asList("/api/public/", "/api/webhook/") ); @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // Skip CSRF check for excluded paths and non-protected methods if (shouldSkipCsrfCheck(request)) { filterChain.doFilter(request, response); return; } // Get CSRF token from header String csrfToken = request.getHeader(CSRF_TOKEN_HEADER); // Validate CSRF token if (!isValidCsrfToken(request, csrfToken)) { response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.getWriter().write("Invalid CSRF token"); return; } // Generate new CSRF token for next request generateNewCsrfToken(request); filterChain.doFilter(request, response); } private boolean shouldSkipCsrfCheck(HttpServletRequest request) { String method = request.getMethod(); String path = request.getRequestURI(); // Skip for non-protected methods if (!protectedMethods.contains(method)) { return true; } // Skip for excluded paths return excludedPaths.stream().anyMatch(path::startsWith); } private boolean isValidCsrfToken(HttpServletRequest request, String csrfToken) { if (csrfToken == null || csrfToken.trim().isEmpty()) { return false; } HttpSession session = request.getSession(false); if (session == null) { return false; } String sessionToken = (String) session.getAttribute(CSRF_TOKEN_SESSION_ATTR); return csrfToken.equals(sessionToken); } private void generateNewCsrfToken(HttpServletRequest request) { String newToken = UUID.randomUUID().toString(); request.getSession().setAttribute(CSRF_TOKEN_SESSION_ATTR, newToken); // Add token to response header for client to use in next request ((HttpServletResponse) request.getAttribute("response")).setHeader(CSRF_TOKEN_HEADER, newToken); } }

2. CSRF Token Service

import org.springframework.stereotype.Service; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import java.security.SecureRandom; import java.util.Base64; import java.util.concurrent.ConcurrentHashMap; @Service public class CsrfTokenService { private static final String CSRF_TOKEN_SESSION_ATTR = "CSRF_TOKENS"; private static final int TOKEN_LENGTH = 32; private final SecureRandom secureRandom = new SecureRandom(); /** * Generate a new CSRF token and store it in session */ public String generateToken(HttpServletRequest request, String tokenId) { byte[] tokenBytes = new byte[TOKEN_LENGTH]; secureRandom.nextBytes(tokenBytes); String token = Base64.getUrlEncoder().withoutPadding().encodeToString(tokenBytes); // Store token in session HttpSession session = request.getSession(); ConcurrentHashMap<String, String> tokens = getTokensFromSession(session); tokens.put(tokenId, token); session.setAttribute(CSRF_TOKEN_SESSION_ATTR, tokens); return token; } /** * Validate CSRF token */ public boolean validateToken(HttpServletRequest request, String tokenId, String token) { if (token == null || token.trim().isEmpty()) { return false; } HttpSession session = request.getSession(false); if (session == null) { return false; } ConcurrentHashMap<String, String> tokens = getTokensFromSession(session); String storedToken = tokens.get(tokenId); // Remove token after use (optional - depends on your strategy) if (storedToken != null && storedToken.equals(token)) { tokens.remove(tokenId); return true; } return false; } /** * Get all active tokens for a session */ public ConcurrentHashMap<String, String> getActiveTokens(HttpServletRequest request) { HttpSession session = request.getSession(false); if (session == null) { return new ConcurrentHashMap<>(); } return getTokensFromSession(session); } /** * Clean up expired tokens (call periodically) */ public void cleanupExpiredTokens(HttpServletRequest request) { // Implementation for token expiration logic // You might want to add timestamps to tokens and remove old ones } @SuppressWarnings("unchecked") private ConcurrentHashMap<String, String> getTokensFromSession(HttpSession session) { Object tokensObj = session.getAttribute(CSRF_TOKEN_SESSION_ATTR); if (tokensObj instanceof ConcurrentHashMap) { return (ConcurrentHashMap<String, String>) tokensObj; } return new ConcurrentHashMap<>(); } /** * Create a token ID based on request characteristics */ public String createTokenId(HttpServletRequest request) { String path = request.getRequestURI(); String method = request.getMethod(); return method + ":" + path; } }

3. CSRF Protection Aspect

import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; @Aspect @Component public class CsrfProtectionAspect { @Autowired private CsrfTokenService csrfTokenService; /** * Protect state-changing methods with CSRF validation */ @Before("@annotation(RequiresCsrfProtection)") public void validateCsrfToken() { HttpServletRequest request = getCurrentRequest(); if (request == null) { throw new SecurityException("CSRF validation failed: No request context"); } String token = request.getHeader("X-CSRF-TOKEN"); String tokenId = csrfTokenService.createTokenId(request); if (!csrfTokenService.validateToken(request, tokenId, token)) { throw new SecurityException("Invalid CSRF token"); } } /** * Generate CSRF token for methods that need it */ @Before("@annotation(GenerateCsrfToken)") public void generateCsrfToken() { HttpServletRequest request = getCurrentRequest(); if (request != null) { String tokenId = csrfTokenService.createTokenId(request); String token = csrfTokenService.generateToken(request, tokenId); // Store token in request attribute for controller to access request.setAttribute("csrfToken", token); } } private HttpServletRequest getCurrentRequest() { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); return attributes != null ? attributes.getRequest() : null; } } /** * Annotation to mark methods that require CSRF protection */ import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RequiresCsrfProtection { } /** * Annotation to mark methods that should generate CSRF tokens */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface GenerateCsrfToken { }

JWT-Based CSRF Tokens

1. JWT CSRF Token Service

import io.jsonwebtoken.*; import io.jsonwebtoken.security.Keys; 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 JwtCsrfTokenService { private final SecretKey secretKey; private final long tokenValidityMs; public JwtCsrfTokenService( @Value("${app.csrf.jwt.secret}") String secret, @Value("${app.csrf.jwt.validity:3600000}") long tokenValidityMs) { this.secretKey = Keys.hmacShaKeyFor(secret.getBytes()); this.tokenValidityMs = tokenValidityMs; } /** * Generate JWT-based CSRF token */ public String generateToken(String sessionId, String requestPath) { Map<String, Object> claims = new HashMap<>(); claims.put("sessionId", sessionId); claims.put("path", requestPath); claims.put("type", "csrf"); Date now = new Date(); Date expiration = new Date(now.getTime() + tokenValidityMs); return Jwts.builder() .setClaims(claims) .setIssuedAt(now) .setExpiration(expiration) .signWith(secretKey, SignatureAlgorithm.HS256) .compact(); } /** * Validate JWT CSRF token */ public boolean validateToken(String token, String sessionId, String requestPath) { try { Claims claims = Jwts.parserBuilder() .setSigningKey(secretKey) .build() .parseClaimsJws(token) .getBody(); // Validate token type if (!"csrf".equals(claims.get("type"))) { return false; } // Validate session ID if (!sessionId.equals(claims.get("sessionId"))) { return false; } // Validate path (optional - for path-bound tokens) String tokenPath = (String) claims.get("path"); if (tokenPath != null && !tokenPath.equals(requestPath)) { return false; } // Check expiration return !claims.getExpiration().before(new Date()); } catch (JwtException | IllegalArgumentException e) { return false; } } /** * Extract session ID from token (for debugging) */ public String extractSessionId(String token) { try { Claims claims = Jwts.parserBuilder() .setSigningKey(secretKey) .build() .parseClaimsJws(token) .getBody(); return (String) claims.get("sessionId"); } catch (JwtException e) { return null; } } }

2. JWT CSRF Filter

import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; import java.util.Arrays; import java.util.HashSet; import java.util.Set; public class JwtCsrfFilter extends OncePerRequestFilter { private static final String CSRF_TOKEN_HEADER = "X-CSRF-TOKEN"; private final JwtCsrfTokenService csrfTokenService; private final Set<String> protectedMethods = new HashSet<>( Arrays.asList("POST", "PUT", "PATCH", "DELETE") ); private final Set<String> excludedPaths = new HashSet<>( Arrays.asList("/api/public/", "/api/auth/login") ); public JwtCsrfFilter(JwtCsrfTokenService csrfTokenService) { this.csrfTokenService = csrfTokenService; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // Skip CSRF check for safe methods and excluded paths if (shouldSkipCsrfCheck(request)) { filterChain.doFilter(request, response); return; } // Get CSRF token from header String csrfToken = request.getHeader(CSRF_TOKEN_HEADER); // Validate CSRF token if (!isValidCsrfToken(request, csrfToken)) { response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.getWriter().write("{\"error\": \"Invalid CSRF token\"}"); response.setContentType("application/json"); return; } // Generate new CSRF token for next request generateNewCsrfToken(request, response); filterChain.doFilter(request, response); } private boolean shouldSkipCsrfCheck(HttpServletRequest request) { String method = request.getMethod(); String path = request.getRequestURI(); if (!protectedMethods.contains(method)) { return true; } return excludedPaths.stream().anyMatch(path::startsWith); } private boolean isValidCsrfToken(HttpServletRequest request, String csrfToken) { if (csrfToken == null || csrfToken.trim().isEmpty()) { return false; } HttpSession session = request.getSession(false); if (session == null) { return false; } String sessionId = session.getId(); String requestPath = request.getRequestURI(); return csrfTokenService.validateToken(csrfToken, sessionId, requestPath); } private void generateNewCsrfToken(HttpServletRequest request, HttpServletResponse response) { HttpSession session = request.getSession(); String sessionId = session.getId(); String requestPath = request.getRequestURI(); String newToken = csrfTokenService.generateToken(sessionId, requestPath); response.setHeader(CSRF_TOKEN_HEADER, newToken); } }

REST Controller with CSRF Protection

1. Protected REST Controller

import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletRequest; import java.util.HashMap; import java.util.Map; @RestController @RequestMapping("/api") public class ProtectedApiController { @Autowired private CsrfTokenService csrfTokenService; @Autowired private JwtCsrfTokenService jwtCsrfTokenService; /** * Get initial CSRF token */ @GetMapping("/csrf-token") @GenerateCsrfToken public ResponseEntity<Map<String, String>> getCsrfToken(HttpServletRequest request) { String tokenId = csrfTokenService.createTokenId(request); String token = csrfTokenService.generateToken(request, tokenId); // Also generate JWT token String jwtToken = jwtCsrfTokenService.generateToken( request.getSession().getId(), request.getRequestURI()); Map<String, String> response = new HashMap<>(); response.put("csrfToken", token); response.put("jwtCsrfToken", jwtToken); response.put("headerName", "X-CSRF-TOKEN"); return ResponseEntity.ok(response); } /** * Protected endpoint - requires CSRF token */ @PostMapping("/users") @RequiresCsrfProtection public ResponseEntity<Map<String, Object>> createUser(@RequestBody UserCreateRequest userRequest, HttpServletRequest request) { // Business logic to create user Map<String, Object> response = new HashMap<>(); response.put("status", "success"); response.put("message", "User created successfully"); response.put("userId", System.currentTimeMillis()); // Simulated ID // Generate new CSRF token for next request String newToken = csrfTokenService.generateToken( request, csrfTokenService.createTokenId(request)); response.put("newCsrfToken", newToken); return ResponseEntity.ok(response); } /** * Update user - protected with manual CSRF check */ @PutMapping("/users/{id}") public ResponseEntity<Map<String, Object>> updateUser(@PathVariable Long id, @RequestBody UserUpdateRequest userRequest, HttpServletRequest request) { // Manual CSRF validation String csrfToken = request.getHeader("X-CSRF-TOKEN"); String tokenId = csrfTokenService.createTokenId(request); if (!csrfTokenService.validateToken(request, tokenId, csrfToken)) { throw new SecurityException("Invalid CSRF token"); } // Business logic to update user Map<String, Object> response = new HashMap<>(); response.put("status", "success"); response.put("message", "User updated successfully"); return ResponseEntity.ok(response); } /** * Delete user - protected with JWT CSRF */ @DeleteMapping("/users/{id}") public ResponseEntity<Map<String, Object>> deleteUser(@PathVariable Long id, HttpServletRequest request) { // JWT CSRF validation String jwtCsrfToken = request.getHeader("X-CSRF-TOKEN"); String sessionId = request.getSession().getId(); String requestPath = request.getRequestURI(); if (!jwtCsrfTokenService.validateToken(jwtCsrfToken, sessionId, requestPath)) { throw new SecurityException("Invalid CSRF token"); } // Business logic to delete user Map<String, Object> response = new HashMap<>(); response.put("status", "success"); response.put("message", "User deleted successfully"); // Generate new JWT CSRF token String newToken = jwtCsrfTokenService.generateToken(sessionId, requestPath); response.put("newCsrfToken", newToken); return ResponseEntity.ok(response); } // Request DTOs public static class UserCreateRequest { private String username; private String email; // Getters and setters public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } } public static class UserUpdateRequest { private String username; private String email; // Getters and setters public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } } }

2. Public API Controller (No CSRF Protection)

import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.HashMap; import java.util.Map; @RestController @RequestMapping("/api/public") public class PublicApiController { /** * Public endpoint - no CSRF protection needed */ @GetMapping("/info") public ResponseEntity<Map<String, String>> getPublicInfo() { Map<String, String> response = new HashMap<>(); response.put("message", "This is public information"); response.put("version", "1.0.0"); return ResponseEntity.ok(response); } /** * Login endpoint - typically doesn't need CSRF protection */ @PostMapping("/login") public ResponseEntity<Map<String, Object>> login(@RequestBody LoginRequest loginRequest) { // Authentication logic here Map<String, Object> response = new HashMap<>(); response.put("status", "success"); response.put("message", "Login successful"); response.put("token", "jwt-token-here"); // In real app, generate proper JWT return ResponseEntity.ok(response); } public static class LoginRequest { private String username; private String password; // Getters and setters public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } } }

Testing CSRF Protection

1. CSRF Test Configuration

import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.mock.web.MockHttpSession; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @SpringBootTest @AutoConfigureMockMvc public class CsrfProtectionTest { @Autowired private MockMvc mockMvc; private MockHttpSession session; @BeforeEach void setUp() throws Exception { // Start a session MvcResult result = mockMvc.perform(get("/api/public/info")) .andReturn(); this.session = (MockHttpSession) result.getRequest().getSession(); } @Test void testCsrfTokenGeneration() throws Exception { mockMvc.perform(get("/api/csrf-token").session(session)) .andExpect(status().isOk()) .andExpect(jsonPath("$.csrfToken").exists()) .andExpect(jsonPath("$.headerName").value("X-CSRF-TOKEN")); } @Test void testProtectedEndpointWithoutCsrfToken() throws Exception { mockMvc.perform(post("/api/users") .session(session) .contentType("application/json") .content("{\"username\": \"test\", \"email\": \"[email protected]\"}")) .andExpect(status().isForbidden()); } @Test void testProtectedEndpointWithValidCsrfToken() throws Exception { // First, get CSRF token MvcResult tokenResult = mockMvc.perform(get("/api/csrf-token").session(session)) .andReturn(); String csrfToken = tokenResult.getResponse().getContentAsString() .split("\"csrfToken\":\"")[1].split("\"")[0]; // Then use it in protected request mockMvc.perform(post("/api/users") .session(session) .header("X-CSRF-TOKEN", csrfToken) .contentType("application/json") .content("{\"username\": \"test\", \"email\": \"[email protected]\"}")) .andExpect(status().isOk()) .andExpect(jsonPath("$.status").value("success")); } @Test void testPublicEndpointNoCsrfRequired() throws Exception { mockMvc.perform(get("/api/public/info")) .andExpect(status().isOk()) .andExpect(jsonPath("$.message").exists()); } }

2. Integration Test with Test Rest Template

import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.http.*; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import java.net.URI; import static org.junit.jupiter.api.Assertions.*; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class CsrfIntegrationTest { @Autowired private TestRestTemplate restTemplate; @Test void testCsrfProtectionFlow() { // Step 1: Make initial request to establish session and get CSRF token ResponseEntity<String> initialResponse = restTemplate.getForEntity("/api/csrf-token", String.class); assertEquals(HttpStatus.OK, initialResponse.getStatusCode()); // Extract cookies for session HttpHeaders headers = new HttpHeaders(); headers.add("Cookie", initialResponse.getHeaders().getFirst("Set-Cookie")); // Extract CSRF token from response (you'd parse the JSON in real test) String csrfToken = "extracted-csrf-token"; // Extract from JSON response // Step 2: Use CSRF token in protected request headers.add("X-CSRF-TOKEN", csrfToken); headers.setContentType(MediaType.APPLICATION_JSON); String userJson = "{\"username\": \"testuser\", \"email\": \"[email protected]\"}"; HttpEntity<String> request = new HttpEntity<>(userJson, headers); ResponseEntity<String> createUserResponse = restTemplate.postForEntity( "/api/users", request, String.class); assertEquals(HttpStatus.OK, createUserResponse.getStatusCode()); } }

Configuration and Best Practices

1. Application Configuration

application.yml:

app: csrf: enabled: true # JWT CSRF configuration jwt: secret: "your-very-secure-secret-key-at-least-32-chars" validity: 3600000 # 1 hour in milliseconds # Session CSRF configuration session: token-length: 32 cleanup-interval: 300000 # 5 minutes # Spring Security CSRF configuration spring: security: csrf: enabled: true # Customize cookie settings cookie: name: XSRF-TOKEN http-only: false secure: true path: / same-site: strict # Logging for debugging logging: level: org.springframework.security: DEBUG com.yourpackage.csrf: DEBUG

2. Security Best Practices

import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import java.util.Arrays; @Configuration public class AdvancedSecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http // CORS configuration .cors(cors -> cors.configurationSource(corsConfigurationSource())) // CSRF configuration .csrf(csrf -> csrf .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .ignoringAntMatchers( "/api/public/**", "/api/auth/login", "/api/webhook/**" ) ) // Session management .sessionManagement(session -> session .sessionFixation().migrateSession() .maximumSessions(1) ) // Headers security .headers(headers -> headers .contentSecurityPolicy("default-src 'self'") .httpStrictTransportSecurity(hsts -> hsts .includeSubDomains(true) .maxAgeInSeconds(31536000) ) .frameOptions().deny() ); return http.build(); } @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(Arrays.asList("https://trusted-domain.com")); configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); configuration.setAllowedHeaders(Arrays.asList("*")); configuration.setAllowCredentials(true); configuration.setExposedHeaders(Arrays.asList("X-CSRF-TOKEN")); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/api/**", configuration); return source; } }

Conclusion

CSRF protection is essential for REST APIs that use session-based authentication or handle state-changing operations. While traditional web applications rely on synchronizer token patterns, REST APIs can benefit from more flexible approaches like JWT-based CSRF tokens or custom implementations. The key is to balance security with usability, ensuring that legitimate clients can easily obtain and use CSRF tokens while preventing malicious requests. By implementing the patterns shown in this guide—from Spring Security's built-in protection to custom JWT-based solutions—you can effectively secure your REST APIs against CSRF attacks while maintaining a good developer experience.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper