From 2ce3925ba98ac647927a4e3cc060725d4fa4f37f Mon Sep 17 00:00:00 2001 From: Scott Rossillo Date: Thu, 7 May 2015 19:58:27 -0400 Subject: [PATCH] Permit Spring Security adapter to process admin tasks with CSRF enabled Spring Security's CSRF protection blocks Keycloak administrative actions when configured with the default request matcher. This provides a CSRF request matcher that permits Keycloak administrative actions without the CSRF token. --- .../KeycloakWebSecurityConfigurerAdapter.java | 5 + .../filter/KeycloakCsrfRequestMatcher.java | 37 +++++++ .../KeycloakCsrfRequestMatcherTest.java | 98 +++++++++++++++++++ 3 files changed, 140 insertions(+) create mode 100644 integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/filter/KeycloakCsrfRequestMatcher.java create mode 100644 integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/filter/KeycloakCsrfRequestMatcherTest.java 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); + } +}