Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implementation of the csrf signed double submit pattern #3409

Merged
merged 15 commits into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.bakdata.conquery.models.auth.web;

import static com.bakdata.conquery.models.auth.web.AuthCookieFilter.PRIORITY;

import java.io.IOException;

import com.bakdata.conquery.models.config.ConqueryConfig;
Expand Down Expand Up @@ -31,10 +33,12 @@
@Slf4j
@PreMatching
// Chain this filter before the Authentication filter
@Priority(Priorities.AUTHENTICATION-100)
@Priority(PRIORITY)
@RequiredArgsConstructor(onConstructor_ = {@Inject})
public class AuthCookieFilter implements ContainerRequestFilter, ContainerResponseFilter {

public static final int PRIORITY = Priorities.AUTHENTICATION - 100;

public static final String ACCESS_TOKEN = "access_token";
private static final String PREFIX = "bearer";

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.bakdata.conquery.models.auth.web.csrf;

import java.io.IOException;
import java.util.Optional;

import com.bakdata.conquery.models.auth.web.AuthCookieFilter;
import jakarta.annotation.Priority;
import jakarta.ws.rs.ForbiddenException;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.core.Cookie;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

/**
* Implementation of the Double-Submit-Cookie Pattern.
* Checks if tokens in cookie and header match if a cookie is present.
* Otherwise the request is refused.
*/
@Priority(AuthCookieFilter.PRIORITY - 100)
@Slf4j
public class CsrfTokenCheckFilter implements ContainerRequestFilter {
public static final String CSRF_TOKEN_HEADER = "X-Csrf-Token";

@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
final String cookieToken = Optional.ofNullable(requestContext.getCookies().get(CsrfTokenSetFilter.CSRF_COOKIE_NAME)).map(Cookie::getValue).orElse(null);
final String headerToken = requestContext.getHeaders().getFirst(CSRF_TOKEN_HEADER);

if (cookieToken == null) {
log.trace("Request had no csrf token set. Accepting request");
return;
}

if (StringUtils.isBlank(headerToken)) {
log.warn("Request contained csrf cookie but the header token was empty");
throw new ForbiddenException("CSRF Attempt");
}

if (!cookieToken.equals(headerToken)) {
log.warn("Request csrf cookie and header did not match");
throw new ForbiddenException("CSRF Attempt");
}

log.trace("Csrf check successful");

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.bakdata.conquery.models.auth.web.csrf;

import java.io.IOException;
import java.security.SecureRandom;
import java.util.Random;

import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.container.ContainerResponseContext;
import jakarta.ws.rs.container.ContainerResponseFilter;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.NewCookie;
import org.apache.commons.lang3.RandomStringUtils;

/**
* Implementation of the Double-Submit-Cookie Pattern.
* This filter generates a random token which is injected in to the response.
* <ul>
* <li>In a Set-Cookie header, so that browser requests send the token via cookie back to us</li>
* <li>In the response payload. This filter sets a request property, which is eventually provided to freemarker.
* Freemarker then writes the token into payload (see base.html.ftl)</li>
* </ul>
*/
public class CsrfTokenSetFilter implements ContainerRequestFilter, ContainerResponseFilter {

public static final String CSRF_COOKIE_NAME = "csrf_token";
public static final String CSRF_TOKEN_PROPERTY = "csrf_token";
public static final int TOKEN_LENGTH = 30;

Random random = new SecureRandom();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

private final?


@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
final String token = RandomStringUtils.random(TOKEN_LENGTH, 0, 0, true, true,
null, random
);
requestContext.setProperty(CSRF_TOKEN_PROPERTY, token);
}

@Override
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException {
final String csrfToken = getCsrfTokenProperty(requestContext);

responseContext.getHeaders()
.add(HttpHeaders.SET_COOKIE, new NewCookie(CSRF_COOKIE_NAME, csrfToken, "/", null, 0, null, 3600, null, requestContext.getSecurityContext()
.isSecure(), false));
}

public static String getCsrfTokenProperty(ContainerRequestContext requestContext) {
return (String) requestContext.getProperty(CSRF_TOKEN_PROPERTY);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import com.bakdata.conquery.io.jersey.RESTServer;
import com.bakdata.conquery.io.storage.MetaStorage;
import com.bakdata.conquery.models.auth.web.AuthCookieFilter;
import com.bakdata.conquery.models.auth.web.csrf.CsrfTokenCheckFilter;
import com.bakdata.conquery.models.auth.web.csrf.CsrfTokenSetFilter;
import com.bakdata.conquery.models.config.ConqueryConfig;
import com.bakdata.conquery.models.jobs.JobManager;
import com.bakdata.conquery.models.worker.DatasetRegistry;
Expand Down Expand Up @@ -120,7 +122,8 @@ protected void configure() {
.register(IdRefPathParamConverterProvider.class)
.register(new MultiPartFeature())
.register(IdParamConverter.Provider.INSTANCE)
.register(AuthCookieFilter.class);
.register(AuthCookieFilter.class)
.register(CsrfTokenCheckFilter.class);


jerseyConfigUI.register(new ViewMessageBodyWriter(manager.getEnvironment().metrics(), Collections.singleton(Freemarker.HTML_RENDERER)))
Expand All @@ -136,7 +139,8 @@ protected void configure() {
})
.register(AdminPermissionFilter.class)
.register(IdRefPathParamConverterProvider.class)
.register(AuthCookieFilter.class);
.register(AuthCookieFilter.class)
.register(CsrfTokenSetFilter.class);

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ public MetaStorage getStorage() {
return adminProcessor.getStorage();
}

public UIContext getUIContext() {
return new UIContext(adminProcessor.getNodeProvider());
public UIContext getUIContext(String csrfToken) {
return new UIContext(adminProcessor.getNodeProvider(), csrfToken);
}

public Set<IndexKey<?>> getLoadedIndexes() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.util.Objects;

import com.bakdata.conquery.models.auth.entities.Subject;
import com.bakdata.conquery.models.auth.web.csrf.CsrfTokenSetFilter;
import com.bakdata.conquery.models.config.auth.AuthenticationConfig;
import com.bakdata.conquery.resources.ResourceConstants;
import com.bakdata.conquery.resources.admin.rest.UIProcessor;
Expand All @@ -27,34 +28,36 @@
public class AdminUIResource {

private final UIProcessor uiProcessor;

@Context
private ContainerRequestContext requestContext;
@GET
public View getIndex() {
return new UIView<>("index.html.ftl", uiProcessor.getUIContext());
return new UIView<>("index.html.ftl", uiProcessor.getUIContext(CsrfTokenSetFilter.getCsrfTokenProperty(requestContext)));
}

@GET
@Path("script")
public View getScript() {
return new UIView<>("script.html.ftl", uiProcessor.getUIContext());
return new UIView<>("script.html.ftl", uiProcessor.getUIContext(CsrfTokenSetFilter.getCsrfTokenProperty(requestContext)));
}

@GET
@Path("jobs")
public View getJobs() {
return new UIView<>("jobs.html.ftl", uiProcessor.getUIContext(), uiProcessor.getAdminProcessor().getJobs());
return new UIView<>("jobs.html.ftl", uiProcessor.getUIContext(CsrfTokenSetFilter.getCsrfTokenProperty(requestContext)), uiProcessor.getAdminProcessor()
.getJobs());
}

@GET
@Path("queries")
public View getQueries() {
return new UIView<>("queries.html.ftl", uiProcessor.getUIContext());
return new UIView<>("queries.html.ftl", uiProcessor.getUIContext(CsrfTokenSetFilter.getCsrfTokenProperty(requestContext)));
}


@GET
@Path("logout")
public Response logout(@Context ContainerRequestContext requestContext, @Auth Subject user) {
public Response logout(@Auth Subject user) {
// Invalidate all cookies. At the moment the adminEnd uses cookies only for authentication, so this does not interfere with other things
final NewCookie[] expiredCookies = requestContext.getCookies().keySet().stream().map(AuthenticationConfig::expireCookie).toArray(NewCookie[]::new);
final URI logout = user.getAuthenticationInfo().getFrontChannelLogout();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.bakdata.conquery.resources.admin.ui;

import com.bakdata.conquery.models.auth.web.csrf.CsrfTokenSetFilter;
import com.bakdata.conquery.resources.ResourceConstants;
import com.bakdata.conquery.resources.admin.rest.UIProcessor;
import com.bakdata.conquery.resources.admin.ui.model.UIView;
Expand All @@ -8,6 +9,8 @@
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import lombok.RequiredArgsConstructor;

Expand All @@ -19,8 +22,8 @@ public class AuthOverviewUIResource {
protected final UIProcessor uiProcessor;

@GET
public View getOverview() {
return new UIView<>("authOverview.html.ftl", uiProcessor.getUIContext(), uiProcessor.getAuthOverview());
public View getOverview(@Context ContainerRequestContext request) {
return new UIView<>("authOverview.html.ftl", uiProcessor.getUIContext(CsrfTokenSetFilter.getCsrfTokenProperty(request)), uiProcessor.getAuthOverview());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static com.bakdata.conquery.resources.ResourceConstants.DATASET;

import com.bakdata.conquery.io.jersey.ExtraMimeTypes;
import com.bakdata.conquery.models.auth.web.csrf.CsrfTokenSetFilter;
import com.bakdata.conquery.models.datasets.Dataset;
import com.bakdata.conquery.models.datasets.concepts.Concept;
import com.bakdata.conquery.resources.admin.rest.UIProcessor;
Expand All @@ -15,6 +16,8 @@
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
Expand All @@ -35,12 +38,14 @@ public class ConceptsUIResource {
protected Concept<?> concept;
@PathParam(DATASET)
protected Dataset dataset;
@Context
ContainerRequestContext request;

@GET
public View getConceptView() {
return new UIView<>(
"concept.html.ftl",
uiProcessor.getUIContext(),
uiProcessor.getUIContext(CsrfTokenSetFilter.getCsrfTokenProperty(request)),
concept
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.util.Collection;
import java.util.stream.Collectors;

import com.bakdata.conquery.models.auth.web.csrf.CsrfTokenSetFilter;
import com.bakdata.conquery.models.datasets.Dataset;
import com.bakdata.conquery.models.datasets.Import;
import com.bakdata.conquery.models.datasets.SecondaryIdDescription;
Expand All @@ -16,13 +17,16 @@
import com.bakdata.conquery.models.index.search.SearchIndex;
import com.bakdata.conquery.models.worker.Namespace;
import com.bakdata.conquery.resources.admin.rest.UIProcessor;
import com.bakdata.conquery.resources.admin.ui.model.UIContext;
import com.bakdata.conquery.resources.admin.ui.model.UIView;
import io.dropwizard.views.common.View;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import lombok.AllArgsConstructor;
import lombok.Data;
Expand All @@ -45,13 +49,16 @@ public class DatasetsUIResource {

private final UIProcessor uiProcessor;

@Context
private ContainerRequestContext requestContext;


@GET
@Produces(MediaType.TEXT_HTML)
public View listDatasetsUI() {
return new UIView<>(
"datasets.html.ftl",
uiProcessor.getUIContext(),
uiProcessor.getUIContext(CsrfTokenSetFilter.getCsrfTokenProperty(requestContext)),
uiProcessor.getDatasetRegistry().getAllDatasets()
);
}
Expand All @@ -63,7 +70,7 @@ public View getDataset(@PathParam(DATASET) Dataset dataset) {
final Namespace namespace = uiProcessor.getDatasetRegistry().get(dataset.getId());
return new UIView<>(
"dataset.html.ftl",
uiProcessor.getUIContext(),
uiProcessor.getUIContext(CsrfTokenSetFilter.getCsrfTokenProperty(requestContext)),
new DatasetInfos(
namespace.getDataset(),
namespace.getStorage().getSecondaryIds(),
Expand Down Expand Up @@ -102,11 +109,13 @@ public View getDataset(@PathParam(DATASET) Dataset dataset) {
@Path("{" + DATASET + "}/mapping")
public View getIdMapping(@PathParam(DATASET) Dataset dataset) {
final Namespace namespace = uiProcessor.getDatasetRegistry().get(dataset.getId());
EntityIdMap mapping = namespace.getStorage().getIdMapping();
final EntityIdMap mapping = namespace.getStorage().getIdMapping();
final UIContext uiContext = uiProcessor.getUIContext(CsrfTokenSetFilter.getCsrfTokenProperty(requestContext));

if (mapping != null && mapping.getInternalToPrint() != null) {
return new UIView<>("idmapping.html.ftl", uiProcessor.getUIContext(), mapping.getInternalToPrint());
return new UIView<>("idmapping.html.ftl", uiContext, mapping.getInternalToPrint());
}
return new UIView<>("add_idmapping.html.ftl", uiProcessor.getUIContext(), namespace.getDataset().getId());
return new UIView<>("add_idmapping.html.ftl", uiContext, namespace.getDataset().getId());
}

@Data
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,36 @@
package com.bakdata.conquery.resources.admin.ui;

import static com.bakdata.conquery.resources.ResourceConstants.GROUPS_PATH_ELEMENT;
import static com.bakdata.conquery.resources.ResourceConstants.GROUP_ID;

import com.bakdata.conquery.models.auth.entities.Group;
import com.bakdata.conquery.models.auth.web.csrf.CsrfTokenSetFilter;
import com.bakdata.conquery.resources.admin.rest.UIProcessor;
import com.bakdata.conquery.resources.admin.ui.model.UIView;
import io.dropwizard.views.common.View;
import lombok.RequiredArgsConstructor;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;

import static com.bakdata.conquery.resources.ResourceConstants.GROUPS_PATH_ELEMENT;
import static com.bakdata.conquery.resources.ResourceConstants.GROUP_ID;
import lombok.RequiredArgsConstructor;

@Produces(MediaType.TEXT_HTML)
@Path(GROUPS_PATH_ELEMENT)
@RequiredArgsConstructor(onConstructor_ = @Inject)
public class GroupUIResource {

protected final UIProcessor uiProcessor;
@Context
private ContainerRequestContext requestContext;

@GET
public View getGroups() {
return new UIView<>("groups.html.ftl", uiProcessor.getUIContext(), uiProcessor.getAdminProcessor().getAllGroups());
return new UIView<>("groups.html.ftl", uiProcessor.getUIContext(CsrfTokenSetFilter.getCsrfTokenProperty(requestContext)), uiProcessor.getAdminProcessor()
.getAllGroups());
}

/**
Expand All @@ -37,6 +42,6 @@ public View getGroups() {
@Path("{" + GROUP_ID + "}")
@GET
public View getGroup(@PathParam(GROUP_ID) Group group) {
return new UIView<>("group.html.ftl", uiProcessor.getUIContext(), uiProcessor.getGroupContent(group));
return new UIView<>("group.html.ftl", uiProcessor.getUIContext(CsrfTokenSetFilter.getCsrfTokenProperty(requestContext)), uiProcessor.getGroupContent(group));
}
}
Loading
Loading