diff --git a/vaadin-spring-tests/test-spring-security-fusion-contextpath/src/main/java/com/vaadin/flow/spring/fusionsecurity/endpoints/SessionEndpoint.java b/vaadin-spring-tests/test-spring-security-fusion-contextpath/src/main/java/com/vaadin/flow/spring/fusionsecurity/endpoints/SessionEndpoint.java new file mode 100644 index 000000000..004bf3650 --- /dev/null +++ b/vaadin-spring-tests/test-spring-security-fusion-contextpath/src/main/java/com/vaadin/flow/spring/fusionsecurity/endpoints/SessionEndpoint.java @@ -0,0 +1,20 @@ +package com.vaadin.flow.spring.fusionsecurity.endpoints; + +import com.vaadin.flow.server.VaadinRequest; +import com.vaadin.flow.server.WrappedSession; +import com.vaadin.flow.server.auth.AnonymousAllowed; +import com.vaadin.fusion.Endpoint; + +@Endpoint +@AnonymousAllowed +public class SessionEndpoint { + + public void invalidateSessionIfPresent() { + WrappedSession wrappedSession = VaadinRequest.getCurrent() + .getWrappedSession(false); + if (wrappedSession != null) { + wrappedSession.invalidate(); + } + } + +} diff --git a/vaadin-spring-tests/test-spring-security-fusion-jwt/src/main/java/com/vaadin/flow/spring/fusionsecurityjwt/endpoints/SessionEndpoint.java b/vaadin-spring-tests/test-spring-security-fusion-jwt/src/main/java/com/vaadin/flow/spring/fusionsecurityjwt/endpoints/SessionEndpoint.java new file mode 100644 index 000000000..f3e6aac40 --- /dev/null +++ b/vaadin-spring-tests/test-spring-security-fusion-jwt/src/main/java/com/vaadin/flow/spring/fusionsecurityjwt/endpoints/SessionEndpoint.java @@ -0,0 +1,20 @@ +package com.vaadin.flow.spring.fusionsecurityjwt.endpoints; + +import com.vaadin.flow.server.VaadinRequest; +import com.vaadin.flow.server.WrappedSession; +import com.vaadin.flow.server.auth.AnonymousAllowed; +import com.vaadin.fusion.Endpoint; + +@Endpoint +@AnonymousAllowed +public class SessionEndpoint { + + public void invalidateSessionIfPresent() { + WrappedSession wrappedSession = VaadinRequest.getCurrent() + .getWrappedSession(false); + if (wrappedSession != null) { + wrappedSession.invalidate(); + } + } + +} diff --git a/vaadin-spring-tests/test-spring-security-fusion-jwt/src/test/java/com/vaadin/flow/spring/fusionsecurityjwt/SecurityIT.java b/vaadin-spring-tests/test-spring-security-fusion-jwt/src/test/java/com/vaadin/flow/spring/fusionsecurityjwt/SecurityIT.java index 5561037b4..11e58a22d 100644 --- a/vaadin-spring-tests/test-spring-security-fusion-jwt/src/test/java/com/vaadin/flow/spring/fusionsecurityjwt/SecurityIT.java +++ b/vaadin-spring-tests/test-spring-security-fusion-jwt/src/test/java/com/vaadin/flow/spring/fusionsecurityjwt/SecurityIT.java @@ -1,10 +1,17 @@ package com.vaadin.flow.spring.fusionsecurityjwt; import java.util.Base64; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; import org.junit.Assert; import org.junit.Test; +import org.openqa.selenium.By; import org.openqa.selenium.Cookie; +import org.openqa.selenium.JavascriptExecutor; + +import com.vaadin.testbench.TestBenchElement; import elemental.json.Json; import elemental.json.JsonObject; @@ -13,32 +20,98 @@ public class SecurityIT extends com.vaadin.flow.spring.fusionsecurity.SecurityIT { @Test - public void cookie_set_for_user() { + public void jwt_cookie_set_for_user() { openLogin(); loginUser(); checkJwtUsername("john"); } @Test - public void cookie_set_for_admin() { + public void jwt_cookie_set_for_admin() { openLogin(); loginAdmin(); checkJwtUsername("emma"); } @Test - public void cookie_reset_on_logout() { + public void jwt_cookie_reset_on_logout() { openLogin(); Assert.assertNull(getJwtCookie()); - loginUser(); logout(); Assert.assertNull(getJwtCookie()); } + @Test + public void csrf_cookie() { + open(""); + + Assert.assertNotNull(getSpringCsrfCookie()); + openLogin(); + loginUser(); + + Assert.assertNotNull(getSpringCsrfCookie()); + + logout(); + + Assert.assertNotNull(getSpringCsrfCookie()); + } + + @Test + public void stateless_for_anonymous_user() { + open(""); + + simulateNewServer(); + + assertPublicEndpointWorks(); + } + + @Test + public void stateless_for_user() { + openLogin(); + loginUser(); + + simulateNewServer(); + + assertPublicEndpointWorks(); + navigateTo("private", false); + assertPrivatePageShown(USER_FULLNAME); + refresh(); + assertPrivatePageShown(USER_FULLNAME); + } + + @Test + public void stateless_for_admin() { + openLogin(); + loginAdmin(); + + simulateNewServer(); + + assertPublicEndpointWorks(); + navigateTo("private", false); + assertPrivatePageShown(ADMIN_FULLNAME); + refresh(); + assertPrivatePageShown(ADMIN_FULLNAME); + } + + @Test + public void stateless_for_anonymous_after_logout() { + openLogin(); + loginUser(); + logout(); + + simulateNewServer(); + + assertPublicEndpointWorks(); + } + private void openLogin() { getDriver().get(getRootURL() + "/login"); } + private Cookie getSpringCsrfCookie() { + return getDriver().manage().getCookieNamed("XSRF-TOKEN"); + } + private Cookie getJwtCookie() { return getDriver().manage().getCookieNamed("jwt" + ".headerAndPayload"); } @@ -52,4 +125,44 @@ private void checkJwtUsername(String expectedUsername) { .parse(new String(Base64.getUrlDecoder().decode(payload))); Assert.assertEquals(expectedUsername, payloadJson.getString("sub")); } + + private void simulateNewServer() { + TestBenchElement mainView = waitUntil(driver -> $("main-view").get(0)); + callAsyncMethod(mainView, "invalidateSessionIfPresent"); + } + + private void assertPublicEndpointWorks() { + TestBenchElement publicView = waitUntil( + driver -> $("public-view").get(0)); + TestBenchElement timeText = publicView.findElement(By.id("time")); + String timeBefore = timeText.getText(); + Assert.assertNotNull(timeBefore); + callAsyncMethod(publicView, "updateTime"); + String timeAfter = timeText.getText(); + Assert.assertNotNull(timeAfter); + Assert.assertNotEquals(timeAfter, timeBefore); + } + + private String formatArgumentRef(int index) { + return String.format("arguments[%d]", index); + } + + private Object callAsyncMethod(TestBenchElement element, String methodName, + Object... args) { + String objectRef = formatArgumentRef(0); + String argRefs = IntStream.range(1, args.length + 1) + .mapToObj(this::formatArgumentRef) + .collect(Collectors.joining(",")); + String callbackRef = formatArgumentRef(args.length + 1); + String script = String.format("%s.%s(%s).then(%s)", objectRef, + methodName, argRefs, callbackRef); + Object[] scriptArgs = Stream.concat(Stream.of(element), Stream.of(args)) + .toArray(); + return getJavascriptExecutor().executeAsyncScript(script, scriptArgs); + } + + private JavascriptExecutor getJavascriptExecutor() { + return (JavascriptExecutor) getDriver(); + } + } diff --git a/vaadin-spring-tests/test-spring-security-fusion/frontend/views/main-view.ts b/vaadin-spring-tests/test-spring-security-fusion/frontend/views/main-view.ts index c7514631a..6020d8dda 100644 --- a/vaadin-spring-tests/test-spring-security-fusion/frontend/views/main-view.ts +++ b/vaadin-spring-tests/test-spring-security-fusion/frontend/views/main-view.ts @@ -10,6 +10,7 @@ import { customElement } from "lit/decorators"; import { router } from "../index"; import { appStore } from "../stores/app-store"; import { Layout } from "./view"; +import {SessionEndpoint} from "Frontend/generated/endpoints"; interface RouteInfo { path: string; @@ -122,4 +123,9 @@ export class MainView extends Layout { super.connectedCallback(); this.id = "main-view"; } + + // Used by SecurityIT#simulateNewServer() + public async invalidateSessionIfPresent(): Promise { + return SessionEndpoint.invalidateSessionIfPresent() + } } diff --git a/vaadin-spring-tests/test-spring-security-fusion/frontend/views/public/public-view.ts b/vaadin-spring-tests/test-spring-security-fusion/frontend/views/public/public-view.ts index f32058391..422f2a168 100644 --- a/vaadin-spring-tests/test-spring-security-fusion/frontend/views/public/public-view.ts +++ b/vaadin-spring-tests/test-spring-security-fusion/frontend/views/public/public-view.ts @@ -12,7 +12,7 @@ export class PublicTSView extends View { async connectedCallback() { super.connectedCallback(); - this.time = await PublicEndpoint.getServerTime(); + await this.updateTime(); } render() { @@ -25,7 +25,11 @@ export class PublicTSView extends View { src="public/images/bank.jpg" />

We are very great and have great amounts of money.

-

This page was updated ${this.time}

+

This page was updated ${this.time}

`; } + + public async updateTime() { + this.time = await PublicEndpoint.getServerTime(); + } } diff --git a/vaadin-spring-tests/test-spring-security-fusion/src/main/java/com/vaadin/flow/spring/fusionsecurity/endpoints/SessionEndpoint.java b/vaadin-spring-tests/test-spring-security-fusion/src/main/java/com/vaadin/flow/spring/fusionsecurity/endpoints/SessionEndpoint.java new file mode 100644 index 000000000..004bf3650 --- /dev/null +++ b/vaadin-spring-tests/test-spring-security-fusion/src/main/java/com/vaadin/flow/spring/fusionsecurity/endpoints/SessionEndpoint.java @@ -0,0 +1,20 @@ +package com.vaadin.flow.spring.fusionsecurity.endpoints; + +import com.vaadin.flow.server.VaadinRequest; +import com.vaadin.flow.server.WrappedSession; +import com.vaadin.flow.server.auth.AnonymousAllowed; +import com.vaadin.fusion.Endpoint; + +@Endpoint +@AnonymousAllowed +public class SessionEndpoint { + + public void invalidateSessionIfPresent() { + WrappedSession wrappedSession = VaadinRequest.getCurrent() + .getWrappedSession(false); + if (wrappedSession != null) { + wrappedSession.invalidate(); + } + } + +} diff --git a/vaadin-spring-tests/test-spring-security-fusion/src/test/java/com/vaadin/flow/spring/fusionsecurity/SecurityIT.java b/vaadin-spring-tests/test-spring-security-fusion/src/test/java/com/vaadin/flow/spring/fusionsecurity/SecurityIT.java index c453c4ac0..1b7e30a2d 100644 --- a/vaadin-spring-tests/test-spring-security-fusion/src/test/java/com/vaadin/flow/spring/fusionsecurity/SecurityIT.java +++ b/vaadin-spring-tests/test-spring-security-fusion/src/test/java/com/vaadin/flow/spring/fusionsecurity/SecurityIT.java @@ -19,8 +19,8 @@ public class SecurityIT extends ChromeBrowserTest { private static final String ROOT_PAGE_HEADER_TEXT = "Welcome to the TypeScript Bank of Vaadin"; private static final int SERVER_PORT = 9999; - private static final String USER_FULLNAME = "John the User"; - private static final String ADMIN_FULLNAME = "Emma the Admin"; + protected static final String USER_FULLNAME = "John the User"; + protected static final String ADMIN_FULLNAME = "Emma the Admin"; @Override protected int getDeploymentPort() { @@ -59,7 +59,7 @@ private void clickLogout() { getMainView().$(ButtonElement.class).id("logout").click(); } - private void open(String path) { + protected void open(String path) { getDriver().get(getRootURL() + "/" + path); } @@ -233,11 +233,11 @@ public void public_app_resources_available_for_all() { shouldBeTextFile.contains("Public document for all users")); } - private void navigateTo(String path) { + protected void navigateTo(String path) { navigateTo(path, true); } - private void navigateTo(String path, boolean assertPathShown) { + protected void navigateTo(String path, boolean assertPathShown) { getMainView().$("a").attribute("href", path).first().click(); if (assertPathShown) { assertPathShown(path); @@ -259,7 +259,7 @@ private void assertRootPageShown() { Assert.assertEquals(ROOT_PAGE_HEADER_TEXT, headerText); } - private void assertPrivatePageShown(String fullName) { + protected void assertPrivatePageShown(String fullName) { assertPathShown("private"); waitUntil(driver -> $("span").attribute("id", "balanceText").exists()); String balance = $("span").id("balanceText").getText(); @@ -299,7 +299,7 @@ private void login(String username, String password) { waitUntilNot(driver -> $(LoginOverlayElement.class).exists()); } - private void refresh() { + protected void refresh() { getDriver().navigate().refresh(); } diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinStatelessSecurityConfigurer.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinStatelessSecurityConfigurer.java index 846b37180..6203b0ace 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinStatelessSecurityConfigurer.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinStatelessSecurityConfigurer.java @@ -60,6 +60,15 @@ public void init(H http) { http.setSharedObject(SecurityContextRepository.class, jwtSecurityContextRepository); } + + CsrfConfigurer csrf = http.getConfigurer(CsrfConfigurer.class); + if (csrf != null) { + // Use cookie for storing CSRF token, as it does not require a + // session (double-submit cookie pattern) + CsrfTokenRepository csrfTokenRepository = new LazyCsrfTokenRepository( + CookieCsrfTokenRepository.withHttpOnlyFalse()); + csrf.csrfTokenRepository(csrfTokenRepository); + } } @Override @@ -91,15 +100,6 @@ public void configure(H http) { ((VaadinDefaultRequestCache) requestCache) .setDelegateRequestCache(new CookieRequestCache()); } - - CsrfConfigurer csrf = http.getConfigurer(CsrfConfigurer.class); - if (csrf != null) { - // Use cookie for storing CSRF token, as it does not require a - // session (double-submit cookie pattern) - CsrfTokenRepository csrfTokenRepository = new LazyCsrfTokenRepository( - CookieCsrfTokenRepository.withHttpOnlyFalse()); - csrf.csrfTokenRepository(csrfTokenRepository); - } } public VaadinStatelessSecurityConfigurer expiresIn(long expiresIn) {