Skip to content

Commit

Permalink
Configure the clusterKey Cookie (#110)
Browse files Browse the repository at this point in the history
  • Loading branch information
tamasmak authored Mar 1, 2024
1 parent 0f0f8cb commit dfaba46
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,16 @@ public static class VaadinReplicatedSessionConfiguration {
private static final Predicate<Class<?>> TRANSIENT_INJECTABLE_VAADIN_EXCLUSIONS = type -> !type
.getPackageName().startsWith("com.vaadin.flow.internal");

final KubernetesKitProperties properties;

public VaadinReplicatedSessionConfiguration(
KubernetesKitProperties properties) {
this.properties = properties;
}

SessionTrackerFilter sessionTrackerFilter(
SessionSerializer sessionSerializer) {
return new SessionTrackerFilter(sessionSerializer);
return new SessionTrackerFilter(sessionSerializer, properties);
}

SessionListener sessionListener(BackendConnector backendConnector,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

import org.springframework.boot.context.properties.ConfigurationProperties;

import com.vaadin.kubernetes.starter.sessiontracker.SameSite;

/**
* Definition of configuration properties for the Kubernetes Kit starter.
*
Expand All @@ -30,6 +32,11 @@ public class KubernetesKitProperties {
*/
private boolean autoConfigure = true;

/**
* Value of the distributed storage session key cookie's SameSite attribute.
*/
private SameSite clusterKeyCookieSameSite = SameSite.STRICT;

/**
* Hazelcast configuration properties.
*/
Expand Down Expand Up @@ -57,6 +64,28 @@ public void setAutoConfigure(boolean autoConfigure) {
this.autoConfigure = autoConfigure;
}

/**
* Gets the distributed storage session key cookie's SameSite attribute
* value.
*
* @return the distributed storage session key cookie's SameSite attribute
* value
*/
public SameSite getClusterKeyCookieSameSite() {
return clusterKeyCookieSameSite;
}

/**
* Sets the distributed storage session key cookie's SameSite attribute.
*
* @param sameSite
* value of the distributed storage session key cookie's
* SameSite attribute
*/
public void setClusterKeyCookieSameSite(SameSite sameSite) {
this.clusterKeyCookieSameSite = sameSite;
}

/**
* Gets Hazelcast configuration properties.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.vaadin.kubernetes.starter.sessiontracker;

/**
* Enum for SameSite cookie attribute values.
*/
public enum SameSite {

/**
* Cookies are sent in both first-party and cross-origin requests.
*/
NONE("None"),

/**
* Cookies are sent in a first-party context, also when following a link to
* the origin site.
*/
LAX("Lax"),

/**
* Cookies are only sent in a first-party context (i.e. not when following a
* link to the origin site).
*/
STRICT("Strict");

private final String attributeValue;

SameSite(String attributeValue) {
this.attributeValue = attributeValue;
}

public String attributeValue() {
return this.attributeValue;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import jakarta.servlet.http.HttpSession;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.stream.Stream;

/**
Expand All @@ -39,17 +40,19 @@ private SessionTrackerCookie() {
* the HTTP response.
*/
public static void setIfNeeded(HttpSession session,
HttpServletRequest request, HttpServletResponse response) {
HttpServletRequest request, HttpServletResponse response,
Consumer<Cookie> cookieConsumer) {
Optional<Cookie> clusterKeyCookie = getCookie(request);
if (!clusterKeyCookie.isPresent()) {
if (clusterKeyCookie.isEmpty()) {
String clusterKey = UUID.randomUUID().toString();
session.setAttribute(CurrentKey.COOKIE_NAME, clusterKey);
response.addCookie(new Cookie(CurrentKey.COOKIE_NAME, clusterKey));
Cookie cookie = new Cookie(CurrentKey.COOKIE_NAME, clusterKey);
cookieConsumer.accept(cookie);
response.addCookie(cookie);
} else if (session.getAttribute(CurrentKey.COOKIE_NAME) == null) {
String clusterKey = clusterKeyCookie.get().getValue();
session.setAttribute(CurrentKey.COOKIE_NAME, clusterKey);
}

}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,20 @@

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.util.function.Consumer;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.vaadin.flow.server.HandlerHelper.RequestType;
import com.vaadin.flow.shared.ApplicationConstants;
import com.vaadin.kubernetes.starter.KubernetesKitProperties;

/**
* An HTTP filter implementation that serializes and persists HTTP session on a
Expand All @@ -45,9 +48,12 @@
public class SessionTrackerFilter extends HttpFilter {

private final transient SessionSerializer sessionSerializer;
private final transient KubernetesKitProperties properties;

public SessionTrackerFilter(SessionSerializer sessionSerializer) {
public SessionTrackerFilter(SessionSerializer sessionSerializer,
KubernetesKitProperties properties) {
this.sessionSerializer = sessionSerializer;
this.properties = properties;
}

@Override
Expand All @@ -67,7 +73,8 @@ protected void doFilter(HttpServletRequest request,
HttpSession session = request.getSession(false);

if (session != null) {
SessionTrackerCookie.setIfNeeded(session, request, response);
SessionTrackerCookie.setIfNeeded(session, request, response,
cookieConsumer(request));
}
super.doFilter(request, response, chain);

Expand All @@ -82,6 +89,20 @@ protected void doFilter(HttpServletRequest request,
}
}

private Consumer<Cookie> cookieConsumer(HttpServletRequest request) {
return (Cookie cookie) -> {
cookie.setHttpOnly(true);

String path = request.getContextPath().isEmpty() ? "/" : request.getContextPath();
cookie.setPath(path);

SameSite sameSite = properties.getClusterKeyCookieSameSite();
if (sameSite != null && !sameSite.attributeValue().isEmpty()) {
cookie.setAttribute("SameSite", sameSite.attributeValue());
}
};
}

private Logger getLogger() {
return LoggerFactory.getLogger(getClass());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package com.vaadin.kubernetes.starter.sessiontracker;

import java.util.Optional;
import java.util.UUID;
import java.util.function.Consumer;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;

import java.util.Optional;
import java.util.UUID;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
Expand All @@ -21,29 +21,38 @@
import static org.mockito.Mockito.when;

public class SessionTrackerCookieTest {

@Test
void setIfNeeded_nullCookies_attributeIsSet() {
void setIfNeeded_nullCookies_attributeIsSetAndCookieIsConfigured() {
HttpSession session = mock(HttpSession.class);
HttpServletRequest request = mock(HttpServletRequest.class);
when(request.getCookies()).thenReturn(null);
HttpServletResponse response = mock(HttpServletResponse.class);
@SuppressWarnings("unchecked")
Consumer<Cookie> cookieConsumer = (Consumer<Cookie>) mock(Consumer.class);

SessionTrackerCookie.setIfNeeded(session, request, response);
SessionTrackerCookie.setIfNeeded(session, request, response,
cookieConsumer);

verify(session).setAttribute(eq(CurrentKey.COOKIE_NAME), anyString());
verify(cookieConsumer).accept(any());
verify(response).addCookie(any());
}

@Test
void setIfNeeded_emptyCookies_attributeIsSet() {
void setIfNeeded_emptyCookies_attributeIsSetAndCookieIsConfigured() {
HttpSession session = mock(HttpSession.class);
HttpServletRequest request = mock(HttpServletRequest.class);
when(request.getCookies()).thenReturn(new Cookie[0]);
HttpServletResponse response = mock(HttpServletResponse.class);
@SuppressWarnings("unchecked")
Consumer<Cookie> cookieConsumer = (Consumer<Cookie>) mock(Consumer.class);

SessionTrackerCookie.setIfNeeded(session, request, response);
SessionTrackerCookie.setIfNeeded(session, request, response,
cookieConsumer);

verify(session).setAttribute(eq(CurrentKey.COOKIE_NAME), anyString());
verify(cookieConsumer).accept(any());
verify(response).addCookie(any());
}

Expand All @@ -57,8 +66,10 @@ void setIfNeeded_nullSessionAttribute_attributeIsSet() {
when(request.getCookies()).thenReturn(new Cookie[] {
new Cookie(CurrentKey.COOKIE_NAME, clusterKey) });
HttpServletResponse response = mock(HttpServletResponse.class);
Consumer<Cookie> cookieConsumer = (Cookie cookie) -> {};

SessionTrackerCookie.setIfNeeded(session, request, response);
SessionTrackerCookie.setIfNeeded(session, request, response,
cookieConsumer);

verify(session).setAttribute(eq(CurrentKey.COOKIE_NAME),
eq(clusterKey));
Expand All @@ -75,8 +86,10 @@ void setIfNeeded_nonNullSessionAttribute_attributeIsNotSet() {
when(request.getCookies()).thenReturn(new Cookie[] {
new Cookie(CurrentKey.COOKIE_NAME, clusterKey) });
HttpServletResponse response = mock(HttpServletResponse.class);
Consumer<Cookie> cookieConsumer = (Cookie cookie) -> {};

SessionTrackerCookie.setIfNeeded(session, request, response);
SessionTrackerCookie.setIfNeeded(session, request, response,
cookieConsumer);

verify(session, never()).setAttribute(any(), any());
verify(response, never()).addCookie(any());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,43 @@
package com.vaadin.kubernetes.starter.sessiontracker;

import java.io.IOException;
import java.util.UUID;
import java.util.function.Consumer;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.UUID;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.mock.web.MockHttpSession;

import com.vaadin.flow.server.HandlerHelper;
import com.vaadin.flow.shared.ApplicationConstants;
import com.vaadin.kubernetes.starter.KubernetesKitProperties;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;

@ExtendWith({ MockitoExtension.class })
class SessionTrackerFilterTest {

SessionSerializer serializer = mock(SessionSerializer.class);
SessionTrackerFilter filter = new SessionTrackerFilter(serializer);
SessionTrackerFilter filter = new SessionTrackerFilter(serializer,
new KubernetesKitProperties());

HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
Expand All @@ -34,6 +47,9 @@ class SessionTrackerFilterTest {
Cookie cookie = new Cookie(CurrentKey.COOKIE_NAME,
UUID.randomUUID().toString());

@Captor
private ArgumentCaptor<Consumer<Cookie>> cookieConsumerArgumentCaptor;

@AfterEach
void assertFilterChainIsAlwaysExecuted()
throws ServletException, IOException {
Expand Down Expand Up @@ -117,6 +133,25 @@ void invalidatedHttpSession_UIDLRequest_sessionNotSerialized()
.isEqualTo(cookie.getValue());
}

@Test
void validHttpSession_cookieConsumer_configuresCookie() throws Exception {
Cookie cookie = new Cookie("clusterKey", "value");
setupHttpSession();
when(request.getContextPath()).thenReturn("contextpath");

try (MockedStatic<SessionTrackerCookie> mockedStatic = mockStatic(SessionTrackerCookie.class)) {
filter.doFilter(request, response, filterChain);
mockedStatic.verify(() -> SessionTrackerCookie.setIfNeeded(any(), any(), any(),
cookieConsumerArgumentCaptor.capture()));
Consumer<Cookie> cookieConsumer = cookieConsumerArgumentCaptor.getValue();
cookieConsumer.accept(cookie);
}

assertTrue(cookie.isHttpOnly());
assertEquals("contextpath", cookie.getPath());
assertEquals("Strict", cookie.getAttribute("SameSite"));
}

private MockHttpSession setupHttpSession() {
MockHttpSession httpSession = new MockHttpSession();
when(request.getSession(false)).thenReturn(httpSession);
Expand All @@ -125,6 +160,5 @@ private MockHttpSession setupHttpSession() {

private void setupCookie() {
when(request.getCookies()).thenReturn(new Cookie[] { cookie });

}
}

0 comments on commit dfaba46

Please sign in to comment.