Skip to content

Commit

Permalink
feat: added csrf whitelist property
Browse files Browse the repository at this point in the history
  • Loading branch information
mebo4b committed Mar 19, 2021
1 parent df1490d commit 0034f53
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
@Value("${csrf.header.property}")
private String csrfHeaderProperty;

@Value("${csrf.whitelist.header.property}")
private String csrfWhitelistHeaderProperty;

/**
* Processes HTTP requests and checks for a valid spring security authentication for the
* (Keycloak) principal (authorization header).
Expand All @@ -56,8 +59,8 @@ public SecurityConfig(KeycloakClientRequestFactory keycloakClientRequestFactory)
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.csrf().disable()
.addFilterBefore(new StatelessCsrfFilter(csrfCookieProperty, csrfHeaderProperty),
CsrfFilter.class)
.addFilterBefore(new StatelessCsrfFilter(csrfCookieProperty, csrfHeaderProperty,
csrfWhitelistHeaderProperty), CsrfFilter.class)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.sessionAuthenticationStrategy(sessionAuthenticationStrategy()).and().authorizeRequests()
.antMatchers(SpringFoxConfig.WHITE_LIST).permitAll()
Expand Down Expand Up @@ -127,8 +130,7 @@ public FilterRegistrationBean keycloakAuthenticationProcessingFilterRegistration
}

/**
* see above:
* {@link SecurityConfig#keycloakAuthenticationProcessingFilterRegistrationBean(KeycloakAuthenticationProcessingFilter).
* see above: {@link SecurityConfig#keycloakAuthenticationProcessingFilterRegistrationBean(KeycloakAuthenticationProcessingFilter).
*/
@SuppressWarnings({"rawtypes", "unchecked"})
@Bean
Expand All @@ -140,8 +142,7 @@ public FilterRegistrationBean keycloakPreAuthActionsFilterRegistrationBean(
}

/**
* see above:
* {@link SecurityConfig#keycloakAuthenticationProcessingFilterRegistrationBean(KeycloakAuthenticationProcessingFilter).
* see above: {@link SecurityConfig#keycloakAuthenticationProcessingFilterRegistrationBean(KeycloakAuthenticationProcessingFilter).
*/
@SuppressWarnings({"rawtypes", "unchecked"})
@Bean
Expand All @@ -153,8 +154,7 @@ public FilterRegistrationBean keycloakAuthenticatedActionsFilterBean(
}

/**
* see above:
* {@link SecurityConfig#keycloakAuthenticationProcessingFilterRegistrationBean(KeycloakAuthenticationProcessingFilter).
* see above: {@link SecurityConfig#keycloakAuthenticationProcessingFilterRegistrationBean(KeycloakAuthenticationProcessingFilter).
*/
@SuppressWarnings({"rawtypes", "unchecked"})
@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -1,54 +1,52 @@
package de.caritas.cob.messageservice.filter;

import static de.caritas.cob.messageservice.config.SpringFoxConfig.WHITE_LIST;
import static java.util.Objects.isNull;
import static org.apache.commons.lang3.StringUtils.isNotBlank;

import java.io.IOException;
import java.util.Arrays;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.web.filter.OncePerRequestFilter;
import de.caritas.cob.messageservice.config.SpringFoxConfig;

/**
* This custom filter checks CSRF cookie and header token for equality
*
*/
public class StatelessCsrfFilter extends OncePerRequestFilter {

private final RequestMatcher requireCsrfProtectionMatcher = new DefaultRequiresCsrfMatcher();
private final RequestMatcher requireCsrfProtectionMatcher;
private final AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl();
private final String csrfCookieProperty;
private final String csrfHeaderProperty;

public StatelessCsrfFilter(String cookieProperty, String headerProperty) {
public StatelessCsrfFilter(String cookieProperty, String headerProperty,
String csrfWhitelistHeaderProperty) {
this.csrfCookieProperty = cookieProperty;
this.csrfHeaderProperty = headerProperty;
this.requireCsrfProtectionMatcher = new DefaultRequiresCsrfMatcher(csrfWhitelistHeaderProperty);
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {

if (requireCsrfProtectionMatcher.matches(request)) {
final String csrfTokenValue = request.getHeader(csrfHeaderProperty);
final Cookie[] cookies = request.getCookies();

String csrfCookieValue = null;
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(csrfCookieProperty)) {
csrfCookieValue = cookie.getValue();
}
}
}
final String csrfTokenValue = request.getHeader(this.csrfHeaderProperty);
String csrfCookieValue = retrieveCsrfCookieValue(request);

if (csrfTokenValue == null || !csrfTokenValue.equals(csrfCookieValue)) {
if (isNull(csrfTokenValue) || !csrfTokenValue.equals(csrfCookieValue)) {
accessDeniedHandler.handle(request, response,
new AccessDeniedException("Missing or non-matching CSRF-token"));
return;
Expand All @@ -57,19 +55,38 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
filterChain.doFilter(request, response);
}

public static final class DefaultRequiresCsrfMatcher implements RequestMatcher {
private String retrieveCsrfCookieValue(HttpServletRequest request) {
final Cookie[] cookies = request.getCookies();
return isNull(cookies) ? null : Stream.of(cookies)
.filter(cookie -> cookie.getName().equals(this.csrfCookieProperty))
.map(Cookie::getValue)
.findFirst()
.orElse(null);
}

@RequiredArgsConstructor
private static final class DefaultRequiresCsrfMatcher implements RequestMatcher {

private final Pattern allowedMethods = Pattern.compile("^(HEAD|TRACE|OPTIONS)$");
private final @NonNull String csrfWhitelistHeaderProperty;

@Override
public boolean matches(HttpServletRequest request) {
return !(isWhiteListUrl(request) || isWhiteListHeader(request) || isAllowedMehod(request));
}

// Allow specific whitelist items to disable CSRF protection for Swagger UI documentation
if (Arrays.stream(SpringFoxConfig.WHITE_LIST).parallel()
.anyMatch(request.getRequestURI().toLowerCase()::contains)) {
return false;
}
private boolean isWhiteListUrl(HttpServletRequest request) {
return Arrays.asList(WHITE_LIST).parallelStream()
.anyMatch(request.getRequestURI().toLowerCase()::contains);
}

return !allowedMethods.matcher(request.getMethod()).matches();
private boolean isWhiteListHeader(HttpServletRequest request) {
return isNotBlank(request.getHeader(this.csrfWhitelistHeaderProperty));
}

private boolean isAllowedMehod(HttpServletRequest request) {
return allowedMethods.matcher(request.getMethod()).matches();
}

}
}
1 change: 1 addition & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ user.service.api.liveproxy.url=<containername>:<port>

# CSRF token
csrf.header.property=
csrf.whitelist.header.property=
csrf.cookie.property=

# LIQUIBASE (LiquibaseProperties)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package de.caritas.cob.messageservice.filter;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static org.springframework.test.util.ReflectionTestUtils.setField;

import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.security.web.access.AccessDeniedHandler;

@RunWith(MockitoJUnitRunner.class)
public class StatelessCsrfFilterTest {

private static final String CSRF_HEADER = "csrfHeader";
private static final String CSRF_COOKIE = "csrfCookie";
private static final String CSRF_WHITELIST_COOKIE = "csrfWhitelistHeader";

private final StatelessCsrfFilter csrfFilter = new StatelessCsrfFilter(CSRF_COOKIE, CSRF_HEADER,
CSRF_WHITELIST_COOKIE);

@Mock
private HttpServletRequest request;

@Mock
private HttpServletResponse response;

@Mock
private FilterChain filterChain;

@Mock
private AccessDeniedHandler accessDeniedHandler;

@Before
public void setup() {
setField(csrfFilter, "accessDeniedHandler", accessDeniedHandler);
}

@Test
public void doFilterInternal_Should_executeFilterChain_When_requestMethodIsAllowed()
throws IOException, ServletException {
when(request.getRequestURI()).thenReturn("uri");
when(request.getMethod()).thenReturn("OPTIONS");

this.csrfFilter.doFilterInternal(request, response, filterChain);

verify(this.filterChain, times(1)).doFilter(request, response);
}

@Test
public void doFilterInternal_Should_executeFilterChain_When_requestHasCsrfWhitelistHeader()
throws IOException, ServletException {
when(request.getRequestURI()).thenReturn("uri");
when(request.getHeader(CSRF_WHITELIST_COOKIE)).thenReturn("whitelisted");

this.csrfFilter.doFilterInternal(request, response, filterChain);

verify(this.filterChain, times(1)).doFilter(request, response);
}

@Test
public void doFilterInternal_Should_executeFilterChain_When_requestCsrfHeaderAndCookieAreEqual()
throws IOException, ServletException {
when(request.getRequestURI()).thenReturn("uri");
when(request.getMethod()).thenReturn("POST");
when(request.getHeader(CSRF_HEADER)).thenReturn("csrfTokenValue");
Cookie[] cookies = {new Cookie(CSRF_COOKIE, "csrfTokenValue")};
when(request.getCookies()).thenReturn(cookies);

this.csrfFilter.doFilterInternal(request, response, filterChain);

verify(this.filterChain, times(1)).doFilter(request, response);
}

@Test
public void doFilterInternal_Should_callAccessDeniedHandler_When_csrfHeaderIsNull()
throws IOException, ServletException {
when(request.getRequestURI()).thenReturn("uri");
when(request.getMethod()).thenReturn("POST");
Cookie[] cookies = {new Cookie(CSRF_COOKIE, "csrfTokenValue")};
when(request.getCookies()).thenReturn(cookies);

this.csrfFilter.doFilterInternal(request, response, filterChain);

verify(this.accessDeniedHandler, times(1)).handle(any(), any(), any());
verifyNoMoreInteractions(this.filterChain);
}

@Test
public void doFilterInternal_Should_callAccessDeniedHandler_When_cookiesAreNull()
throws IOException, ServletException {
when(request.getRequestURI()).thenReturn("uri");
when(request.getMethod()).thenReturn("POST");
when(request.getHeader(CSRF_HEADER)).thenReturn("csrfHeaderTokenValue");

this.csrfFilter.doFilterInternal(request, response, filterChain);

verify(this.accessDeniedHandler, times(1)).handle(any(), any(), any());
verifyNoMoreInteractions(this.filterChain);
}

@Test
public void doFilterInternal_Should_callAccessDeniedHandler_When_csrfHeaderIsNotEqualToCookieToken()
throws IOException, ServletException {
when(request.getRequestURI()).thenReturn("uri");
when(request.getMethod()).thenReturn("POST");
when(request.getHeader(CSRF_HEADER)).thenReturn("csrfHeaderTokenValue");
Cookie[] cookies = {new Cookie(CSRF_COOKIE, "csrfCookieTokenValue")};
when(request.getCookies()).thenReturn(cookies);

this.csrfFilter.doFilterInternal(request, response, filterChain);

verify(this.accessDeniedHandler, times(1)).handle(any(), any(), any());
verifyNoMoreInteractions(this.filterChain);
}

}

0 comments on commit 0034f53

Please sign in to comment.