Skip to content

Commit

Permalink
fix: ensure CSRF is compatible with stateless authentication (#919)
Browse files Browse the repository at this point in the history
Fixes #918
  • Loading branch information
platosha authored Oct 15, 2021
1 parent 51dabeb commit c23d710
Show file tree
Hide file tree
Showing 8 changed files with 205 additions and 22 deletions.
Original file line number Diff line number Diff line change
@@ -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();
}
}

}
Original file line number Diff line number Diff line change
@@ -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();
}
}

}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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");
}
Expand All @@ -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();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -122,4 +123,9 @@ export class MainView extends Layout {
super.connectedCallback();
this.id = "main-view";
}

// Used by SecurityIT#simulateNewServer()
public async invalidateSessionIfPresent(): Promise<void> {
return SessionEndpoint.invalidateSessionIfPresent()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export class PublicTSView extends View {

async connectedCallback() {
super.connectedCallback();
this.time = await PublicEndpoint.getServerTime();
await this.updateTime();
}

render() {
Expand All @@ -25,7 +25,11 @@ export class PublicTSView extends View {
src="public/images/bank.jpg"
/>
<p>We are very great and have great amounts of money.</p>
<p>This page was updated ${this.time}</p>
<p>This page was updated <span id="time">${this.time}</span></p>
</div>`;
}

public async updateTime() {
this.time = await PublicEndpoint.getServerTime();
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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);
Expand All @@ -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();
Expand Down Expand Up @@ -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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,15 @@ public void init(H http) {
http.setSharedObject(SecurityContextRepository.class,
jwtSecurityContextRepository);
}

CsrfConfigurer<H> 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
Expand Down Expand Up @@ -91,15 +100,6 @@ public void configure(H http) {
((VaadinDefaultRequestCache) requestCache)
.setDelegateRequestCache(new CookieRequestCache());
}

CsrfConfigurer<H> 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<H> expiresIn(long expiresIn) {
Expand Down

0 comments on commit c23d710

Please sign in to comment.