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());
}