├── settings.gradle ├── .gitignore ├── src ├── main │ ├── resources │ │ ├── application.properties │ │ └── logback.xml │ ├── webapp │ │ ├── index.html │ │ └── js │ │ │ └── controllers.js │ └── java │ │ └── com │ │ └── jdriven │ │ └── stateless │ │ └── security │ │ ├── StatelessCSRFSecurityConfig.java │ │ ├── TestController.java │ │ ├── StatelessCSRF.java │ │ └── StatelessCSRFFilter.java └── test │ └── java │ └── com │ └── jdriven │ └── stateless │ └── security │ └── StatelessCSRFIntegrationTest.java └── README.md /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'boot-stateless-csrf' 2 | 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | gradle.properties 3 | build 4 | .idea 5 | *.iml 6 | .settings 7 | .classpath 8 | .project 9 | /bin 10 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | security.basic.enabled=false 2 | spring.jmx.enabled=false 3 | server.tomcat.uri-encoding=UTF-8 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | boot-stateless-csrf 2 | =================== 3 | Needs Gradle 2 and JDK 7 4 | 5 | build with `gradle build` 6 | run with `gradle run` 7 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/webapp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Stateless CSRF Example 5 | 6 | 7 | 8 | 9 | 10 |
11 | {{result}} 12 | 13 | -------------------------------------------------------------------------------- /src/main/java/com/jdriven/stateless/security/StatelessCSRFSecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.jdriven.stateless.security; 2 | 3 | import org.springframework.core.annotation.Order; 4 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 5 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 6 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 7 | import org.springframework.security.web.csrf.CsrfFilter; 8 | 9 | @EnableWebSecurity 10 | @Order(1) 11 | public class StatelessCSRFSecurityConfig extends WebSecurityConfigurerAdapter { 12 | 13 | @Override 14 | protected void configure(HttpSecurity http) throws Exception { 15 | http.csrf().disable().addFilterBefore(new StatelessCSRFFilter(), CsrfFilter.class); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/jdriven/stateless/security/TestController.java: -------------------------------------------------------------------------------- 1 | package com.jdriven.stateless.security; 2 | 3 | import javax.servlet.http.HttpServletRequest; 4 | import javax.servlet.http.HttpServletResponse; 5 | 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RequestMethod; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | @RestController 11 | @RequestMapping("/api/test") 12 | public class TestController { 13 | 14 | @RequestMapping(method = RequestMethod.GET) 15 | public String get(HttpServletRequest request, HttpServletResponse response) { 16 | return "GET Received"; 17 | } 18 | 19 | @RequestMapping(method = RequestMethod.POST) 20 | public String post(HttpServletRequest request, HttpServletResponse response) { 21 | return "POST Received"; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/webapp/js/controllers.js: -------------------------------------------------------------------------------- 1 | var app = angular.module('statelessApp', []); 2 | 3 | app.config(['$httpProvider', function($httpProvider) { 4 | //fancy random token 5 | function b(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e16]+1e16).replace(/[01]/g,b)}; 6 | 7 | $httpProvider.interceptors.push(function() { 8 | return { 9 | 'request': function(config) { 10 | // put a new random secret into our CSRF-TOKEN Cookie after each response 11 | document.cookie = 'CSRF-TOKEN=' + b(); 12 | return config; 13 | } 14 | }; 15 | }); 16 | }]); 17 | 18 | app.controller('CsrfCtrl', function ($scope, $http) { 19 | $scope.result = ""; 20 | 21 | $scope.init = function () { 22 | $http.defaults.xsrfHeaderName = 'X-CSRF-TOKEN'; 23 | $http.defaults.xsrfCookieName = 'CSRF-TOKEN'; 24 | }; 25 | 26 | $scope.testPost = function () { 27 | $http.post('/api/test').success(function (result) { 28 | $scope.result = result; 29 | }); 30 | }; 31 | }); -------------------------------------------------------------------------------- /src/main/java/com/jdriven/stateless/security/StatelessCSRF.java: -------------------------------------------------------------------------------- 1 | package com.jdriven.stateless.security; 2 | 3 | import javax.servlet.Filter; 4 | 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.ComponentScan; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.web.filter.CharacterEncodingFilter; 11 | 12 | @EnableAutoConfiguration 13 | @Configuration 14 | @ComponentScan 15 | public class StatelessCSRF { 16 | 17 | public static void main(String[] args) { 18 | SpringApplication.run(StatelessCSRF.class, args); 19 | } 20 | 21 | @Bean 22 | public Filter characterEncodingFilter() { 23 | CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter(); 24 | characterEncodingFilter.setEncoding("UTF-8"); 25 | characterEncodingFilter.setForceEncoding(true); 26 | return characterEncodingFilter; 27 | } 28 | 29 | @Bean 30 | public StatelessCSRFSecurityConfig applicationSecurity() { 31 | return new StatelessCSRFSecurityConfig(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/jdriven/stateless/security/StatelessCSRFFilter.java: -------------------------------------------------------------------------------- 1 | package com.jdriven.stateless.security; 2 | 3 | import java.io.IOException; 4 | import java.util.regex.Pattern; 5 | 6 | import javax.servlet.FilterChain; 7 | import javax.servlet.ServletException; 8 | import javax.servlet.http.Cookie; 9 | import javax.servlet.http.HttpServletRequest; 10 | import javax.servlet.http.HttpServletResponse; 11 | 12 | import org.springframework.security.access.AccessDeniedException; 13 | import org.springframework.security.web.access.AccessDeniedHandler; 14 | import org.springframework.security.web.access.AccessDeniedHandlerImpl; 15 | import org.springframework.security.web.util.matcher.RequestMatcher; 16 | import org.springframework.web.filter.OncePerRequestFilter; 17 | 18 | public class StatelessCSRFFilter extends OncePerRequestFilter { 19 | 20 | private static final String CSRF_TOKEN = "CSRF-TOKEN"; 21 | private static final String X_CSRF_TOKEN = "X-CSRF-TOKEN"; 22 | private final RequestMatcher requireCsrfProtectionMatcher = new DefaultRequiresCsrfMatcher(); 23 | private final AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl(); 24 | 25 | @Override 26 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) 27 | throws ServletException, IOException { 28 | if (requireCsrfProtectionMatcher.matches(request)) { 29 | final String csrfTokenValue = request.getHeader(X_CSRF_TOKEN); 30 | final Cookie[] cookies = request.getCookies(); 31 | 32 | String csrfCookieValue = null; 33 | if (cookies != null) { 34 | for (Cookie cookie : cookies) { 35 | if (cookie.getName().equals(CSRF_TOKEN)) { 36 | csrfCookieValue = cookie.getValue(); 37 | } 38 | } 39 | } 40 | 41 | if (csrfTokenValue == null || !csrfTokenValue.equals(csrfCookieValue)) { 42 | accessDeniedHandler.handle(request, response, new AccessDeniedException( 43 | "Missing or non-matching CSRF-token")); 44 | return; 45 | } 46 | } 47 | filterChain.doFilter(request, response); 48 | } 49 | 50 | public static final class DefaultRequiresCsrfMatcher implements RequestMatcher { 51 | private final Pattern allowedMethods = Pattern.compile("^(GET|HEAD|TRACE|OPTIONS)$"); 52 | 53 | @Override 54 | public boolean matches(HttpServletRequest request) { 55 | return !allowedMethods.matcher(request.getMethod()).matches(); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/test/java/com/jdriven/stateless/security/StatelessCSRFIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.jdriven.stateless.security; 2 | 3 | import static org.junit.Assert.*; 4 | 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.springframework.boot.test.IntegrationTest; 8 | import org.springframework.boot.test.SpringApplicationConfiguration; 9 | import org.springframework.http.HttpEntity; 10 | import org.springframework.http.HttpHeaders; 11 | import org.springframework.http.HttpMethod; 12 | import org.springframework.http.MediaType; 13 | import org.springframework.http.ResponseEntity; 14 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 15 | import org.springframework.test.context.web.WebAppConfiguration; 16 | import org.springframework.web.client.HttpClientErrorException; 17 | import org.springframework.web.client.RestTemplate; 18 | 19 | @RunWith(SpringJUnit4ClassRunner.class) 20 | @SpringApplicationConfiguration(classes = StatelessCSRF.class) 21 | @WebAppConfiguration 22 | @IntegrationTest("server.port:8181") 23 | public class StatelessCSRFIntegrationTest { 24 | 25 | @Test 26 | public void test_Get_WithoutTokens() { 27 | ResponseEntity response = http(HttpMethod.GET, "/api/test", null); 28 | assertEquals("GET Received", response.getBody()); 29 | } 30 | 31 | @Test(expected = HttpClientErrorException.class) 32 | public void test_Post_WithoutTokens() { 33 | http(HttpMethod.POST, "/api/test", null); 34 | fail("should throw the exception above"); 35 | } 36 | 37 | @Test 38 | public void test_Post_WithTokens() { 39 | final String clientSecret = "my_little_secret"; 40 | HttpHeaders httpHeaders = new HttpHeaders(); 41 | httpHeaders.set("X-CSRF-TOKEN", clientSecret); 42 | httpHeaders.set("Cookie", "CSRF-TOKEN=" + clientSecret); 43 | ResponseEntity response = http(HttpMethod.POST, "/api/test", httpHeaders); 44 | assertEquals("POST Received", response.getBody()); 45 | } 46 | 47 | private ResponseEntity http(final HttpMethod method, final String path, HttpHeaders headers) { 48 | RestTemplate restTemplate = new RestTemplate(); 49 | HttpHeaders httpHeaders = headers == null ? new HttpHeaders() : headers; 50 | httpHeaders.setContentType(MediaType.APPLICATION_JSON); 51 | HttpEntity testRequest = new HttpEntity<>(httpHeaders); 52 | return restTemplate.exchange("http://localhost:8181/" + path, method, testRequest, String.class); 53 | } 54 | } 55 | --------------------------------------------------------------------------------