diff --git a/src/main/java/org/wise/portal/presentation/web/filters/CsrfCookieFilter.java b/src/main/java/org/wise/portal/presentation/web/filters/CsrfCookieFilter.java new file mode 100644 index 0000000000..a36146a85e --- /dev/null +++ b/src/main/java/org/wise/portal/presentation/web/filters/CsrfCookieFilter.java @@ -0,0 +1,30 @@ +package org.wise.portal.presentation.web.filters; + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.web.filter.OncePerRequestFilter; + +/** + * Ensures the CSRF token cookie is sent to the browser. The token is loaded lazily, so on a + * request that does not otherwise read it the cookie would not be written, and the single page + * application would then have no token to send back on state-changing requests. Reading the + * token value here forces it to be generated and the cookie to be written. + */ +public class CsrfCookieFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); + if (csrfToken != null) { + csrfToken.getToken(); + } + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/org/wise/portal/spring/impl/WebSecurityConfig.java b/src/main/java/org/wise/portal/spring/impl/WebSecurityConfig.java index b608d1272e..3e3dd0f64e 100644 --- a/src/main/java/org/wise/portal/spring/impl/WebSecurityConfig.java +++ b/src/main/java/org/wise/portal/spring/impl/WebSecurityConfig.java @@ -52,10 +52,13 @@ import org.springframework.security.web.authentication.logout.LogoutFilter; import org.springframework.security.web.authentication.logout.LogoutHandler; import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; +import org.springframework.security.web.csrf.CookieCsrfTokenRepository; +import org.springframework.security.web.csrf.CsrfFilter; import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter; import org.springframework.security.web.session.HttpSessionEventPublisher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.session.Session; +import org.wise.portal.presentation.web.filters.CsrfCookieFilter; import org.wise.portal.presentation.web.filters.GoogleOpenIdConnectFilter; import org.wise.portal.presentation.web.filters.MicrosoftAuthenticationFailureHandler; import org.wise.portal.presentation.web.filters.MicrosoftOpenIdConnectFilter; @@ -79,7 +82,9 @@ public class WebSecurityConfig extends WebSecurityConfigurerA @Override protected void configure(HttpSecurity http) throws Exception { - http.csrf().disable() + http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) + .and() + .addFilterAfter(new CsrfCookieFilter(), CsrfFilter.class) .addFilterAfter(openSessionInViewFilter(), SecurityContextHolderAwareRequestFilter.class) .addFilterAfter(oAuth2ClientContextFilter(), OpenSessionInViewFilter.class) .addFilterAfter(googleOpenIdConnectFilter(), OAuth2ClientContextFilter.class) diff --git a/src/main/webapp/portal/admin/portal/manage.jsp b/src/main/webapp/portal/admin/portal/manage.jsp index cdf9e98069..95f6446ce2 100644 --- a/src/main/webapp/portal/admin/portal/manage.jsp +++ b/src/main/webapp/portal/admin/portal/manage.jsp @@ -5,6 +5,7 @@ <%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> + " /> + diff --git a/src/main/webapp/portal/javascript/general.js b/src/main/webapp/portal/javascript/general.js index 12c57aed41..ad18ad6859 100644 --- a/src/main/webapp/portal/javascript/general.js +++ b/src/main/webapp/portal/javascript/general.js @@ -1,3 +1,27 @@ +// Attach the CSRF token to state changing AJAX requests sent from these portal pages. +// The server stores the token in a cookie that is readable by this script and expects +// it to be echoed back in a request header, which is how it confirms the request was +// made by the page and not forged by another site. Without this, jQuery POST requests +// on these pages would be rejected once CSRF protection is enabled. +(function () { + function getCookie(name) { + var match = document.cookie.match(new RegExp('(^|;\\s*)' + name + '=([^;]*)')); + return match ? decodeURIComponent(match[2]) : null; + } + $(document).ajaxSend(function (event, jqxhr, settings) { + var method = (settings.type || 'GET').toUpperCase(); + var isStateChanging = + method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' && method !== 'TRACE'; + if (!isStateChanging || settings.crossDomain) { + return; + } + var token = getCookie('XSRF-TOKEN'); + if (token) { + jqxhr.setRequestHeader('X-XSRF-TOKEN', token); + } + }); +})(); + function impersonateUser(username, userType) { $.post( "/api/login/impersonate", {username: username}).always(function( data ) { window.location.href = `/${userType}`;