×
☰ See All Chapters

How to create custom CsrfTokenRepository in Spring Security

By default, Spring Security stores CSRF (Cross-Site Request Forgery) tokens in the HTTP session on the server side. While suitable for smaller applications, this approach becomes less efficient for larger applications needing horizontal scalability due to the stateful nature of HTTP sessions.

To customize CSRF token management, Spring Security provides two key contracts: CsrfToken and CsrfTokenRepository.

CsrfToken: This interface describes the CSRF token itself. It includes three main characteristics: the name of the header in the request containing the token value (default: X-CSRF-TOKEN), the name of the attribute of the request storing the token value (default: _csrf), and the token value itself. An example implementation is DefaultCsrfToken.

CsrfTokenRepository: Responsible for managing CSRF tokens within Spring Security, this interface defines methods for creating, storing, and loading CSRF tokens. Implementing this interface allows developers to customize token management, such as storing tokens in a database rather than the HTTP session.

In a custom implementation, CSRF tokens might be stored in a database. Clients are assumed to have unique identifiers for token retrieval and validation, akin to session IDs. Alternatively, tokens may have defined lifetimes, expiring after a specified duration, without being tied to specific user IDs. This approach simplifies token validation by checking token existence and expiration for each incoming request.

Example Implementation

CREATE TABLE IF NOT EXISTS TOKEN (
   `id`
INT NOT NULL AUTO_INCREMENT,
   `identifier`
VARCHAR(45) NULL,
   `token` TEXT
NULL,
   
PRIMARY KEY (`id`)
);

 

 

package com.java4coding.security;

import com.java4coding.entity.Token;
import com.java4coding.repository.JpaTokenRepository;
import org.springframework.beans.factory.annotation.Autowired;
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.Optional;
import java.util.UUID;

public class CustomCsrfTokenRepository implements CsrfTokenRepository {
   
@Autowired
   
private JpaTokenRepository jpaTokenRepository;

   
@Override
   
public CsrfToken generateToken(HttpServletRequest httpServletRequest) {
       
String uuid = UUID.randomUUID().toString();
       
return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", uuid);
   }

   @Override
   
public void saveToken(CsrfToken csrfToken, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) {
       
String identifier = httpServletRequest.getHeader("X-IDENTIFIER");
       
Optional<Token> existingToken = jpaTokenRepository.findTokenByIdentifier(identifier);
       
if (existingToken.isPresent()) {
           
Token token = existingToken.get();
           
token.setToken(csrfToken.getToken());
       }
else {
           
Token token = new Token();
           
token.setToken(csrfToken.getToken());
           
token.setIdentifier(identifier);
           
jpaTokenRepository.save(token);
       }
   }

   
@Override
   
public CsrfToken loadToken(HttpServletRequest httpServletRequest) {
       
String identifier = httpServletRequest.getHeader("X-IDENTIFIER");
       
Optional<Token> existingToken =
               
jpaTokenRepository
                       
.findTokenByIdentifier(identifier);
       
if (existingToken.isPresent()) {
           
Token token = existingToken.get();
           
return new DefaultCsrfToken(
                   
"X-CSRF-TOKEN",
                   
"_csrf",
                   
token.getToken());
       }
       
return null;
   }
}

 

package com.java4coding.repository;

import com.java4coding.entity.Token;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface JpaTokenRepository extends JpaRepository<Token, Integer> {
   
Optional<Token> findTokenByIdentifier(String identifier);
}

 

package com.java4coding.entity;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
@Setter
@Getter
public class Token {
   
@Id
   @GeneratedValue
(strategy = GenerationType.IDENTITY)
   
private int id;
   
private String identifier;
   
private String token;
}

package com.java4coding.config;

import com.java4coding.security.CustomCsrfTokenRepository;
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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.csrf.CsrfTokenRepository;

@Configuration
public class ApplicationWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

   
/*
   Since H2 has its own authentication provider, you can skip the Spring Security for the
   path of h2 console entirely in the same way that you do for your static content.
   In order to do that, in your Spring security config, you have to override the configuration
    method which takes an instance of org.springframework.security.config.annotation.web.builders.WebSecurity
   
as a parameter instead of the one which takes an instance of org.springframework.security.config.annotation.web.builders.HttpSecurity
    */
   
@Override
   
public void configure(WebSecurity web) throws Exception {
       web.ignoring().antMatchers(
"/h2-console/**");
   }

   
@Bean
   
public CsrfTokenRepository customTokenRepository() {
       
return new CustomCsrfTokenRepository();
   }

   
@Override
   
protected void configure(HttpSecurity http) throws Exception {
       http.csrf(c -> {
           c.csrfTokenRepository(customTokenRepository());
           c.ignoringAntMatchers(
"/hi");
       });
       http.authorizeRequests()
               .anyRequest().permitAll();
   }

}

 

package com.java4coding.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration
public class ApplicationConfig {

   
@Bean
   
public UserDetailsService userDetailsService() {
       
var inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
       
inMemoryUserDetailsManager.createUser(User.withUsername("manu")
               .password(
"pass")
               .roles(
"ADMIN")
               .build());
       inMemoryUserDetailsManager.createUser(User.withUsername("advith")
               .password(
"xyz123")
               .roles(
"MANAGER")
               .build());
       
inMemoryUserDetailsManager.createUser(User.withUsername("aashvith")
               .password(
"xyz123")
               .roles(
"MANAGER")
               .build());
       
return inMemoryUserDetailsManager;
   }

   
@Bean
   
public PasswordEncoder passwordEncoder() {
       
return NoOpPasswordEncoder.getInstance();
   }
}

 

package com.java4coding.controller;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DemoController {

   
@PostMapping(value = "/hello")
   
public String sayHelloPost() {
       
return "Hello!";
   }

   
@PostMapping(value = "/hi")
   
public String sayHi() {
       
return "Hi!";
   }

}

 

package com.java4coding;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@SpringBootApplication
@EntityScan
@EnableJpaRepositories
public class SpringBootDemo {
   
public static void main(String[] args) {
       
SpringApplication.run(SpringBootDemo.class, args);
   }
}

 

 


All Chapters
Author