diff --git a/src/main/java/dev/kruhlmann/imgfloat/config/SecurityConfig.java b/src/main/java/dev/kruhlmann/imgfloat/config/SecurityConfig.java index 6fb6f55..d10e149 100644 --- a/src/main/java/dev/kruhlmann/imgfloat/config/SecurityConfig.java +++ b/src/main/java/dev/kruhlmann/imgfloat/config/SecurityConfig.java @@ -18,6 +18,7 @@ import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.authentication.HttpStatusEntryPoint; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.csrf.CookieCsrfTokenRepository; +import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler; import org.springframework.security.web.csrf.CsrfToken; import org.springframework.security.web.csrf.CsrfException; import org.springframework.security.web.csrf.CsrfFilter; @@ -41,6 +42,9 @@ public class SecurityConfig { HttpSecurity http, OAuth2AuthorizedClientRepository authorizedClientRepository ) throws Exception { + CsrfTokenRequestAttributeHandler csrfRequestHandler = new CsrfTokenRequestAttributeHandler(); + csrfRequestHandler.setCsrfRequestAttributeName("_csrf"); + http .authorizeHttpRequests((auth) -> auth @@ -89,6 +93,7 @@ public class SecurityConfig { .csrf((csrf) -> csrf .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) + .csrfTokenRequestHandler(csrfRequestHandler) .ignoringRequestMatchers("/ws/**") ) .addFilterAfter(csrfTokenCookieFilter(), CsrfFilter.class); @@ -140,7 +145,10 @@ public class SecurityConfig { HttpServletResponse response, FilterChain filterChain ) throws java.io.IOException, jakarta.servlet.ServletException { - CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); + CsrfToken csrfToken = (CsrfToken) request.getAttribute("_csrf"); + if (csrfToken == null) { + csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); + } if (csrfToken != null) { String token = csrfToken.getToken(); Cookie existingCookie = WebUtils.getCookie(request, "XSRF-TOKEN"); diff --git a/src/main/resources/static/js/admin.js b/src/main/resources/static/js/admin.js index 5eca481..1092ed9 100644 --- a/src/main/resources/static/js/admin.js +++ b/src/main/resources/static/js/admin.js @@ -438,6 +438,7 @@ function connect() { }, (error) => { console.warn("WebSocket connection issue", error); + fetchAssets(); setTimeout( () => showToast("Live updates connection interrupted. Retrying may be necessary.", "warning"), 1000, @@ -2086,7 +2087,7 @@ function uploadAsset(file = null) { return; } if (selectedFile.size > UPLOAD_LIMIT_BYTES) { - showToast(`File is too large. Maximum upload size is ${UPLOAD_MAX_BYTES / 1024 / 1024} MB.`, "error"); + showToast(`File is too large. Maximum upload size is ${UPLOAD_LIMIT_BYTES / 1024 / 1024} MB.`, "error"); return; } diff --git a/src/main/resources/templates/admin.html b/src/main/resources/templates/admin.html index 7d8d415..2adf23f 100644 --- a/src/main/resources/templates/admin.html +++ b/src/main/resources/templates/admin.html @@ -352,7 +352,7 @@ const broadcaster = /*[[${broadcaster}]]*/ ''; const username = /*[[${username}]]*/ ''; const UPLOAD_LIMIT_BYTES = /*[[${uploadLimitBytes}]]*/ 0; - const SETTINGS = /*[[${settingsJson}]]*/; + const SETTINGS = JSON.parse(/*[[${settingsJson}]]*/); diff --git a/src/test/java/dev/kruhlmann/imgfloat/ChannelApiIntegrationTest.java b/src/test/java/dev/kruhlmann/imgfloat/ChannelApiIntegrationTest.java index ee7f33e..37fa7a5 100644 --- a/src/test/java/dev/kruhlmann/imgfloat/ChannelApiIntegrationTest.java +++ b/src/test/java/dev/kruhlmann/imgfloat/ChannelApiIntegrationTest.java @@ -1,6 +1,7 @@ package dev.kruhlmann.imgfloat; import static org.hamcrest.Matchers.hasSize; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.oauth2Login; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -48,6 +49,7 @@ class ChannelApiIntegrationTest { .contentType(MediaType.APPLICATION_JSON) .content("{\"username\":\"helper\"}") .with(oauth2Login().attributes((attrs) -> attrs.put("preferred_username", broadcaster))) + .with(csrf()) ) .andExpect(status().isOk()); @@ -70,6 +72,7 @@ class ChannelApiIntegrationTest { multipart("/api/channels/{broadcaster}/assets", broadcaster) .file(file) .with(oauth2Login().attributes((attrs) -> attrs.put("preferred_username", broadcaster))) + .with(csrf()) ) .andExpect(status().isOk()) .andReturn() @@ -96,6 +99,7 @@ class ChannelApiIntegrationTest { .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(visibilityRequest)) .with(oauth2Login().attributes((attrs) -> attrs.put("preferred_username", broadcaster))) + .with(csrf()) ) .andExpect(status().isOk()) .andExpect(jsonPath("$.hidden").value(false)); @@ -113,7 +117,7 @@ class ChannelApiIntegrationTest { .perform( delete("/api/channels/{broadcaster}/assets/{id}", broadcaster, assetId).with( oauth2Login().attributes((attrs) -> attrs.put("preferred_username", broadcaster)) - ) + ).with(csrf()) ) .andExpect(status().isOk()); } @@ -126,6 +130,7 @@ class ChannelApiIntegrationTest { .contentType(MediaType.APPLICATION_JSON) .content("{\"username\":\"helper\"}") .with(oauth2Login().attributes((attrs) -> attrs.put("preferred_username", "intruder"))) + .with(csrf()) ) .andExpect(status().isForbidden()); }