├── 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 |
--------------------------------------------------------------------------------