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