Browse Source

Add CookieServerCsrfTokenRepository

A cookie implementation of ServerCsrfTokenRepository (like CookieCsrfTokenRepository)
is missing. In this implementation it would be nice to allow the setting of the domain as well.

Fixes: gh-5083
Eric Deandrea 7 years ago
parent
commit
1eaecc12ec

+ 230 - 0
web/src/main/java/org/springframework/security/web/server/csrf/CookieServerCsrfTokenRepository.java

@@ -0,0 +1,230 @@
+/*
+ * Copyright 2002-2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.web.server.csrf;
+
+import java.util.Optional;
+import java.util.UUID;
+
+import org.springframework.http.HttpCookie;
+import org.springframework.http.ResponseCookie;
+import org.springframework.http.server.PathContainer;
+import org.springframework.http.server.RequestPath;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.util.Assert;
+import org.springframework.web.server.ServerWebExchange;
+
+import reactor.core.publisher.Mono;
+
+/**
+ * A {@link ServerCsrfTokenRepository} that persists the CSRF token in a cookie named "XSRF-TOKEN" and
+ * reads from the header "X-XSRF-TOKEN" following the conventions of AngularJS. When using with
+ * AngularJS be sure to use {@link #withHttpOnlyFalse()} .
+ *
+ * @author Eric Deandrea
+ * @since 5.1
+ */
+public final class CookieServerCsrfTokenRepository implements ServerCsrfTokenRepository {
+	static final String DEFAULT_CSRF_COOKIE_NAME = "XSRF-TOKEN";
+	static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf";
+	static final String DEFAULT_CSRF_HEADER_NAME = "X-XSRF-TOKEN";
+
+	private String parameterName = DEFAULT_CSRF_PARAMETER_NAME;
+	private String headerName = DEFAULT_CSRF_HEADER_NAME;
+	private String cookiePath;
+	private String cookieDomain;
+	private String cookieName = DEFAULT_CSRF_COOKIE_NAME;
+	private boolean cookieHttpOnly = true;
+
+	/**
+	 * Factory method to conveniently create an instance that has
+	 * {@link #setCookieHttpOnly(boolean)} set to false.
+	 *
+	 * @return an instance of CookieCsrfTokenRepository with
+	 * {@link #setCookieHttpOnly(boolean)} set to false
+	 */
+	public static CookieServerCsrfTokenRepository withHttpOnlyFalse() {
+		return new CookieServerCsrfTokenRepository().withCookieHttpOnly(false);
+	}
+
+	@Override
+	public Mono<CsrfToken> generateToken(ServerWebExchange exchange) {
+		return Mono.fromCallable(this::createCsrfToken);
+	}
+
+	@Override
+	public Mono<Void> saveToken(ServerWebExchange exchange, CsrfToken token) {
+		Optional<String> tokenValue = Optional.ofNullable(token).map(CsrfToken::getToken);
+
+		ResponseCookie cookie = ResponseCookie.from(this.cookieName, tokenValue.orElse(""))
+			.domain(this.cookieDomain)
+			.httpOnly(this.cookieHttpOnly)
+			.maxAge(tokenValue.map(val -> -1).orElse(0))
+			.path(Optional.ofNullable(this.cookiePath).orElseGet(() -> getRequestContext(exchange.getRequest())))
+			.secure(Optional.ofNullable(exchange.getRequest().getSslInfo()).map(sslInfo -> true).orElse(false))
+			.build();
+
+		exchange.getResponse().addCookie(cookie);
+
+		return Mono.empty();
+	}
+
+	@Override
+	public Mono<CsrfToken> loadToken(ServerWebExchange exchange) {
+		Optional<CsrfToken> token = Optional.ofNullable(exchange.getRequest())
+			.map(ServerHttpRequest::getCookies)
+			.map(cookiesMap -> cookiesMap.getFirst(this.cookieName))
+			.map(HttpCookie::getValue)
+			.map(this::createCsrfToken);
+
+		return Mono.justOrEmpty(token);
+	}
+
+	/**
+	 * Sets the HttpOnly attribute on the cookie containing the CSRF token
+	 * @param cookieHttpOnly True to mark the cookie as http only. False otherwise.
+	 */
+	public void setCookieHttpOnly(boolean cookieHttpOnly) {
+		this.cookieHttpOnly = cookieHttpOnly;
+	}
+
+	/**
+	 * Sets the HttpOnly attribute on the cookie containing the CSRF token
+	 * @param cookieHttpOnly True to mark the cookie as http only. False otherwise.
+	 * @return This instance
+	 */
+	public CookieServerCsrfTokenRepository withCookieHttpOnly(boolean cookieHttpOnly) {
+		setCookieHttpOnly(cookieHttpOnly);
+		return this;
+	}
+
+	/**
+	 * Sets the cookie name
+	 * @param cookieName The cookie name
+	 */
+	public void setCookieName(String cookieName) {
+		Assert.hasLength(cookieName, "cookieName can't be null");
+		this.cookieName = cookieName;
+	}
+
+	/**
+	 * Sets the cookie name
+	 * @param cookieName The cookie name
+	 * @return This instance
+	 */
+	public CookieServerCsrfTokenRepository withCookieName(String cookieName) {
+		setCookieName(cookieName);
+		return this;
+	}
+
+	/**
+	 * Sets the parameter name
+	 * @param parameterName The parameter name
+	 */
+	public void setParameterName(String parameterName) {
+		Assert.hasLength(parameterName, "parameterName can't be null");
+		this.parameterName = parameterName;
+	}
+
+	/**
+	 * Sets the parameter name
+	 * @param parameterName The parameter name
+	 * @return This instance
+	 */
+	public CookieServerCsrfTokenRepository withParameterName(String parameterName) {
+		setParameterName(parameterName);
+		return this;
+	}
+
+	/**
+	 * Sets the header name
+	 * @param headerName The header name
+	 * @return This instance
+	 */
+	public void setHeaderName(String headerName) {
+		Assert.hasLength(headerName, "headerName can't be null");
+		this.headerName = headerName;
+	}
+
+	/**
+	 * Sets the header name
+	 * @param headerName The header name
+	 * @return This instance
+	 */
+	public CookieServerCsrfTokenRepository withHeaderName(String headerName) {
+		setHeaderName(headerName);
+		return this;
+	}
+
+	/**
+	 * Sets the cookie path
+	 * @param cookiePath The cookie path
+	 * @return This instance
+	 */
+	public void setCookiePath(String cookiePath) {
+		this.cookiePath = cookiePath;
+	}
+
+	/**
+	 * Sets the cookie path
+	 * @param cookiePath The cookie path
+	 * @return This instance
+	 */
+	public CookieServerCsrfTokenRepository withCookiePath(String cookiePath) {
+		setCookiePath(cookiePath);
+		return this;
+	}
+
+	/**
+	 * Sets the cookie domain
+	 * @param cookieDomain The cookie domain
+	 * @return This instance
+	 */
+	public void setCookieDomain(String cookieDomain) {
+		this.cookieDomain = cookieDomain;
+	}
+
+	/**
+	 * Sets the cookie domain
+	 * @param cookieDomain The cookie domain
+	 * @return This instance
+	 */
+	public CookieServerCsrfTokenRepository withCookieDomain(String cookieDomain) {
+		setCookieDomain(cookieDomain);
+		return this;
+	}
+
+	private CsrfToken createCsrfToken() {
+		return createCsrfToken(createNewToken());
+	}
+
+	private CsrfToken createCsrfToken(String tokenValue) {
+		return new DefaultCsrfToken(this.headerName, this.parameterName, tokenValue);
+	}
+
+	private String createNewToken() {
+		return UUID.randomUUID().toString();
+	}
+
+	private String getRequestContext(ServerHttpRequest request) {
+		return Optional.ofNullable(request)
+			.map(ServerHttpRequest::getPath)
+			.map(RequestPath::contextPath)
+			.map(PathContainer::value)
+			.filter(contextPath -> contextPath.length() > 0)
+			.orElse("/");
+	}
+}

+ 254 - 0
web/src/test/java/org/springframework/security/web/server/csrf/CookieServerCsrfTokenRepositoryTests.java

@@ -0,0 +1,254 @@
+/*
+ * Copyright 2002-2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.web.server.csrf;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.time.Duration;
+import java.util.function.Supplier;
+
+import org.junit.Test;
+
+import org.springframework.http.ResponseCookie;
+import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
+import org.springframework.mock.web.server.MockServerWebExchange;
+
+import reactor.core.publisher.Mono;
+
+/**
+ * @author Eric Deandrea
+ * @since 5.1
+ */
+public class CookieServerCsrfTokenRepositoryTests {
+	@Test
+	public void generateTokenDefault() {
+		MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/someUri"));
+		CookieServerCsrfTokenRepository csrfTokenRepository =
+			CookieServerCsrfTokenRepositoryFactory.createRepository(CookieServerCsrfTokenRepository::new);
+		Mono<CsrfToken> csrfTokenMono = csrfTokenRepository.generateToken(exchange);
+
+		assertThat(csrfTokenMono).isNotNull();
+		assertThat(csrfTokenMono.block())
+			.isNotNull()
+			.extracting("headerName", "parameterName")
+			.containsExactly(CookieServerCsrfTokenRepository.DEFAULT_CSRF_HEADER_NAME, CookieServerCsrfTokenRepository.DEFAULT_CSRF_PARAMETER_NAME);
+		assertThat(csrfTokenMono.block().getToken()).isNotBlank();
+	}
+
+	@Test
+	public void generateTokenChangeHeaderName() {
+		MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/someUri"));
+		CookieServerCsrfTokenRepository csrfTokenRepository =
+			CookieServerCsrfTokenRepositoryFactory.createRepository(CookieServerCsrfTokenRepository::new,
+				CookieServerCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME,
+				"someHeader",
+				CookieServerCsrfTokenRepository.DEFAULT_CSRF_PARAMETER_NAME);
+		Mono<CsrfToken> csrfTokenMono = csrfTokenRepository.generateToken(exchange);
+
+		assertThat(csrfTokenMono).isNotNull();
+		assertThat(csrfTokenMono.block())
+			.isNotNull()
+			.extracting("headerName", "parameterName")
+			.containsExactly("someHeader", CookieServerCsrfTokenRepository.DEFAULT_CSRF_PARAMETER_NAME);
+		assertThat(csrfTokenMono.block().getToken()).isNotBlank();
+	}
+
+	@Test
+	public void generateTokenChangeParameterName() {
+		MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/someUri"));
+		CookieServerCsrfTokenRepository csrfTokenRepository =
+			CookieServerCsrfTokenRepositoryFactory.createRepository(CookieServerCsrfTokenRepository::new,
+				CookieServerCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME,
+				CookieServerCsrfTokenRepository.DEFAULT_CSRF_HEADER_NAME,
+				"someParam");
+		Mono<CsrfToken> csrfTokenMono = csrfTokenRepository.generateToken(exchange);
+
+		assertThat(csrfTokenMono).isNotNull();
+		assertThat(csrfTokenMono.block())
+			.isNotNull()
+			.extracting("headerName", "parameterName")
+			.containsExactly(CookieServerCsrfTokenRepository.DEFAULT_CSRF_HEADER_NAME, "someParam");
+		assertThat(csrfTokenMono.block().getToken()).isNotBlank();
+	}
+
+	@Test
+	public void generateTokenChangeHeaderAndParameterName() {
+		MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/someUri"));
+		CookieServerCsrfTokenRepository csrfTokenRepository =
+			CookieServerCsrfTokenRepositoryFactory.createRepository(CookieServerCsrfTokenRepository::new,
+				CookieServerCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME,
+				"someHeader",
+				"someParam");
+		Mono<CsrfToken> csrfTokenMono = csrfTokenRepository.generateToken(exchange);
+
+		assertThat(csrfTokenMono).isNotNull();
+		assertThat(csrfTokenMono.block())
+			.isNotNull()
+			.extracting("headerName", "parameterName")
+			.containsExactly("someHeader", "someParam");
+		assertThat(csrfTokenMono.block().getToken()).isNotBlank();
+	}
+
+	@Test
+	public void saveTokenDefault() {
+		MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/someUri"));
+		CookieServerCsrfTokenRepository csrfTokenRepository =
+			CookieServerCsrfTokenRepositoryFactory.createRepository(CookieServerCsrfTokenRepository::new);
+
+		Mono<Void> csrfTokenMono = csrfTokenRepository.saveToken(exchange, createToken("someTokenValue"));
+		ResponseCookie cookie = exchange
+			.getResponse()
+			.getCookies()
+			.getFirst(CookieServerCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
+
+		assertThat(csrfTokenMono).isNotNull();
+		assertThat(cookie)
+			.isNotNull()
+			.extracting("maxAge", "domain", "path", "secure", "httpOnly", "name", "value")
+			.containsExactly(Duration.ofSeconds(-1), null, "/", false, true, "XSRF-TOKEN", "someTokenValue");
+	}
+
+	@Test
+	public void saveTokenMaxAge() {
+		MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/someUri"));
+		CookieServerCsrfTokenRepository csrfTokenRepository =
+			CookieServerCsrfTokenRepositoryFactory.createRepository(CookieServerCsrfTokenRepository::new);
+
+		Mono<Void> csrfTokenMono = csrfTokenRepository.saveToken(exchange, null);
+		ResponseCookie cookie = exchange
+			.getResponse()
+			.getCookies()
+			.getFirst(CookieServerCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
+
+		assertThat(csrfTokenMono).isNotNull();
+		assertThat(cookie)
+			.isNotNull()
+			.extracting("maxAge", "domain", "path", "secure", "httpOnly", "name", "value")
+			.containsExactly(Duration.ofSeconds(0), null, "/", false, true, "XSRF-TOKEN", "");
+	}
+
+	@Test
+	public void saveTokenHttpOnly() {
+		MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/someUri"));
+		CookieServerCsrfTokenRepository csrfTokenRepository =
+			CookieServerCsrfTokenRepositoryFactory.createRepository(CookieServerCsrfTokenRepository::withHttpOnlyFalse);
+
+		Mono<Void> csrfTokenMono = csrfTokenRepository.saveToken(exchange, createToken("someTokenValue"));
+		ResponseCookie cookie = exchange
+			.getResponse()
+			.getCookies()
+			.getFirst(CookieServerCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
+
+		assertThat(csrfTokenMono).isNotNull();
+		assertThat(cookie)
+			.isNotNull()
+			.extracting("maxAge", "domain", "path", "secure", "httpOnly", "name", "value")
+			.containsExactly(Duration.ofSeconds(-1), null, "/", false, false, "XSRF-TOKEN", "someTokenValue");
+	}
+
+	@Test
+	public void saveTokenOverriddenViaCsrfProps() {
+		MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/someUri"));
+		CookieServerCsrfTokenRepository csrfTokenRepository =
+			CookieServerCsrfTokenRepositoryFactory.createRepository(CookieServerCsrfTokenRepository::new,
+			".spring.io",  "csrfCookie", "/some/path",
+				"headerName", "paramName");
+
+		Mono<Void> csrfTokenMono =
+			csrfTokenRepository.saveToken(exchange, createToken("headerName", "paramName", "someTokenValue"));
+		ResponseCookie cookie = exchange.getResponse().getCookies().getFirst("csrfCookie");
+
+		assertThat(csrfTokenMono).isNotNull();
+		assertThat(cookie)
+			.isNotNull()
+			.extracting("maxAge", "domain", "path", "secure", "httpOnly", "name", "value")
+			.containsExactly(Duration.ofSeconds(-1), ".spring.io", "/some/path", false, true, "csrfCookie", "someTokenValue");
+	}
+
+	@Test
+	public void loadTokenThatExists() {
+		MockServerWebExchange exchange = MockServerWebExchange.from(
+			MockServerHttpRequest.post("/someUri")
+				.cookie(ResponseCookie.from(CookieServerCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME, "someTokenValue").build()));
+
+		CookieServerCsrfTokenRepository csrfTokenRepository =
+			CookieServerCsrfTokenRepositoryFactory.createRepository(CookieServerCsrfTokenRepository::new);
+		Mono<CsrfToken> csrfTokenMono = csrfTokenRepository.loadToken(exchange);
+
+		assertThat(csrfTokenMono).isNotNull();
+		assertThat(csrfTokenMono.block())
+			.isNotNull()
+			.extracting("headerName", "parameterName", "token")
+			.containsExactly(
+				CookieServerCsrfTokenRepository.DEFAULT_CSRF_HEADER_NAME,
+				CookieServerCsrfTokenRepository.DEFAULT_CSRF_PARAMETER_NAME,
+				"someTokenValue");
+	}
+
+	@Test
+	public void loadTokenThatDoesntExists() {
+		MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.post("/someUri"));
+		CookieServerCsrfTokenRepository csrfTokenRepository =
+			CookieServerCsrfTokenRepositoryFactory.createRepository(CookieServerCsrfTokenRepository::new);
+
+		Mono<CsrfToken> csrfTokenMono = csrfTokenRepository.loadToken(exchange);
+		assertThat(csrfTokenMono).isNotNull();
+		assertThat(csrfTokenMono.block()).isNull();
+	}
+
+	private static CsrfToken createToken(String tokenValue) {
+		return createToken(CookieServerCsrfTokenRepository.DEFAULT_CSRF_HEADER_NAME,
+			CookieServerCsrfTokenRepository.DEFAULT_CSRF_PARAMETER_NAME, tokenValue);
+	}
+
+	private static CsrfToken createToken(String headerName, String parameterName, String tokenValue) {
+		return new DefaultCsrfToken(headerName, parameterName, tokenValue);
+	}
+
+	static final class CookieServerCsrfTokenRepositoryFactory {
+		private CookieServerCsrfTokenRepositoryFactory() {
+			super();
+		}
+
+		static CookieServerCsrfTokenRepository createRepository(Supplier<CookieServerCsrfTokenRepository> cookieServerCsrfTokenRepositorySupplier) {
+			return createRepository(cookieServerCsrfTokenRepositorySupplier,
+				CookieServerCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME,
+				CookieServerCsrfTokenRepository.DEFAULT_CSRF_HEADER_NAME,
+				CookieServerCsrfTokenRepository.DEFAULT_CSRF_PARAMETER_NAME);
+		}
+
+		static CookieServerCsrfTokenRepository createRepository(
+			Supplier<CookieServerCsrfTokenRepository> cookieServerCsrfTokenRepositorySupplier,
+			String cookieName, String headerName, String parameterName) {
+
+			return createRepository(cookieServerCsrfTokenRepositorySupplier,
+				null, cookieName, null, headerName, parameterName);
+		}
+
+		static CookieServerCsrfTokenRepository createRepository(
+			Supplier<CookieServerCsrfTokenRepository> cookieServerCsrfTokenRepositorySupplier,
+			String cookieDomain, String cookieName, String cookiePath, String headerName, String parameterName) {
+
+			return cookieServerCsrfTokenRepositorySupplier.get()
+				.withCookieDomain(cookieDomain)
+				.withCookieName(cookieName)
+				.withCookiePath(cookiePath)
+				.withHeaderName(headerName)
+				.withParameterName(parameterName);
+		}
+	}
+}