diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/config/KeycloakWebSecurityConfigurerAdapter.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/config/KeycloakWebSecurityConfigurerAdapter.java
index 7a3d772e9b..34dd14d622 100644
--- a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/config/KeycloakWebSecurityConfigurerAdapter.java
+++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/config/KeycloakWebSecurityConfigurerAdapter.java
@@ -5,6 +5,7 @@ import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticatio
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
import org.keycloak.adapters.springsecurity.authentication.KeycloakLogoutHandler;
import org.keycloak.adapters.springsecurity.filter.KeycloakAuthenticationProcessingFilter;
+import org.keycloak.adapters.springsecurity.filter.KeycloakCsrfRequestMatcher;
import org.keycloak.adapters.springsecurity.filter.KeycloakPreAuthActionsFilter;
import org.keycloak.adapters.springsecurity.management.HttpSessionManager;
import org.springframework.context.annotation.Bean;
@@ -59,6 +60,10 @@ public abstract class KeycloakWebSecurityConfigurerAdapter extends WebSecurityCo
return new KeycloakPreAuthActionsFilter(httpSessionManager());
}
+ protected KeycloakCsrfRequestMatcher keycloakCsrfRequestMatcher() {
+ return new KeycloakCsrfRequestMatcher();
+ }
+
@Bean
protected HttpSessionManager httpSessionManager() {
return new HttpSessionManager();
diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakCsrfRequestMatcher.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakCsrfRequestMatcher.java
new file mode 100644
index 0000000000..77c8e7dc21
--- /dev/null
+++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakCsrfRequestMatcher.java
@@ -0,0 +1,37 @@
+package org.keycloak.adapters.springsecurity.filter;
+
+import org.keycloak.constants.AdapterConstants;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.util.StringUtils;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Arrays;
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * CSRF protection matcher that allows administrative POST requests from the Keycloak server.
+ *
+ * @author Scott Rossillo
+ */
+public class KeycloakCsrfRequestMatcher implements RequestMatcher {
+
+ private static final List ALLOWED_ENDPOINTS = Arrays.asList(
+ AdapterConstants.K_LOGOUT,
+ AdapterConstants.K_PUSH_NOT_BEFORE,
+ AdapterConstants.K_QUERY_BEARER_TOKEN,
+ AdapterConstants.K_TEST_AVAILABLE,
+ AdapterConstants.K_VERSION
+ );
+
+ private Pattern allowedMethods = Pattern.compile("^(GET|HEAD|TRACE|OPTIONS)$");
+ private Pattern allowedEndpoints = Pattern.compile(String.format("^\\/(%s)$", StringUtils.arrayToDelimitedString(ALLOWED_ENDPOINTS.toArray(), "|")));
+
+ /* (non-Javadoc)
+ * @see org.springframework.security.web.util.matcher.RequestMatcher#matches(javax.servlet.http.HttpServletRequest)
+ */
+ public boolean matches(HttpServletRequest request) {
+ String uri = request.getRequestURI().replaceFirst(request.getContextPath(), "");
+ return !allowedEndpoints.matcher(uri).matches() && !allowedMethods.matcher(request.getMethod()).matches();
+ }
+}
diff --git a/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/filter/KeycloakCsrfRequestMatcherTest.java b/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/filter/KeycloakCsrfRequestMatcherTest.java
new file mode 100644
index 0000000000..e729e5310b
--- /dev/null
+++ b/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/filter/KeycloakCsrfRequestMatcherTest.java
@@ -0,0 +1,98 @@
+package org.keycloak.adapters.springsecurity.filter;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.constants.AdapterConstants;
+import org.springframework.http.HttpMethod;
+import org.springframework.mock.web.MockHttpServletRequest;
+
+import static org.junit.Assert.*;
+
+/**
+ * Keycloak CSRF request matcher tests.
+ */
+public class KeycloakCsrfRequestMatcherTest {
+
+ private static final String ROOT_CONTEXT_PATH = "";
+ private static final String SUB_CONTEXT_PATH = "/foo";
+
+ private KeycloakCsrfRequestMatcher matcher = new KeycloakCsrfRequestMatcher();
+
+ private MockHttpServletRequest request;
+
+ @Before
+ public void setUp() throws Exception {
+ request = new MockHttpServletRequest();
+ }
+
+ @Test
+ public void testMatchesMethodGet() throws Exception {
+ request.setMethod(HttpMethod.GET.name());
+ assertFalse(matcher.matches(request));
+ }
+
+ @Test
+ public void testMatchesMethodPost() throws Exception {
+ prepareRequest(HttpMethod.POST, ROOT_CONTEXT_PATH, "some/random/uri");
+ assertTrue(matcher.matches(request));
+
+ prepareRequest(HttpMethod.POST, SUB_CONTEXT_PATH, "some/random/uri");
+ assertTrue(matcher.matches(request));
+ }
+
+ @Test
+ public void testMatchesKeycloakLogout() throws Exception {
+
+ prepareRequest(HttpMethod.POST, ROOT_CONTEXT_PATH, AdapterConstants.K_LOGOUT);
+ assertFalse(matcher.matches(request));
+
+ prepareRequest(HttpMethod.POST, SUB_CONTEXT_PATH, AdapterConstants.K_LOGOUT);
+ assertFalse(matcher.matches(request));
+ }
+
+ @Test
+ public void testMatchesKeycloakPushNotBefore() throws Exception {
+
+ prepareRequest(HttpMethod.POST, ROOT_CONTEXT_PATH, AdapterConstants.K_PUSH_NOT_BEFORE);
+ assertFalse(matcher.matches(request));
+
+ prepareRequest(HttpMethod.POST, SUB_CONTEXT_PATH, AdapterConstants.K_PUSH_NOT_BEFORE);
+ assertFalse(matcher.matches(request));
+ }
+
+ @Test
+ public void testMatchesKeycloakQueryBearerToken() throws Exception {
+
+ prepareRequest(HttpMethod.POST, ROOT_CONTEXT_PATH, AdapterConstants.K_QUERY_BEARER_TOKEN);
+ assertFalse(matcher.matches(request));
+
+ prepareRequest(HttpMethod.POST, SUB_CONTEXT_PATH, AdapterConstants.K_QUERY_BEARER_TOKEN);
+ assertFalse(matcher.matches(request));
+ }
+
+ @Test
+ public void testMatchesKeycloakTestAvailable() throws Exception {
+
+ prepareRequest(HttpMethod.POST, ROOT_CONTEXT_PATH, AdapterConstants.K_TEST_AVAILABLE);
+ assertFalse(matcher.matches(request));
+
+ prepareRequest(HttpMethod.POST, SUB_CONTEXT_PATH, AdapterConstants.K_TEST_AVAILABLE);
+ assertFalse(matcher.matches(request));
+ }
+
+ @Test
+ public void testMatchesKeycloakVersion() throws Exception {
+
+ prepareRequest(HttpMethod.POST, ROOT_CONTEXT_PATH, AdapterConstants.K_VERSION);
+ assertFalse(matcher.matches(request));
+
+ prepareRequest(HttpMethod.POST, SUB_CONTEXT_PATH, AdapterConstants.K_VERSION);
+ assertFalse(matcher.matches(request));
+ }
+
+ private void prepareRequest(HttpMethod method, String contextPath, String uri) {
+ request.setMethod(method.name());
+ request.setContextPath(contextPath);
+ request.setRequestURI(contextPath + "/" + uri);
+ }
+}