mirror of
https://github.com/imgfloat/server.git
synced 2026-02-05 03:39:26 +00:00
Setup access denied and csrf
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
package dev.kruhlmann.imgfloat.config;
|
package dev.kruhlmann.imgfloat.config;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.http.HttpMethod;
|
import org.springframework.http.HttpMethod;
|
||||||
@@ -12,15 +14,28 @@ import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResp
|
|||||||
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
|
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
|
||||||
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
|
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
import org.springframework.security.web.access.AccessDeniedHandler;
|
||||||
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
|
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
|
||||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||||
|
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
|
||||||
|
import org.springframework.security.web.csrf.CsrfToken;
|
||||||
|
import org.springframework.security.web.csrf.CsrfException;
|
||||||
|
import org.springframework.security.web.csrf.CsrfFilter;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
import org.springframework.web.util.WebUtils;
|
||||||
import org.springframework.web.client.RestTemplate;
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.http.Cookie;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
@EnableMethodSecurity
|
@EnableMethodSecurity
|
||||||
public class SecurityConfig {
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(SecurityConfig.class);
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
SecurityFilterChain securityFilterChain(
|
SecurityFilterChain securityFilterChain(
|
||||||
HttpSecurity http,
|
HttpSecurity http,
|
||||||
@@ -69,9 +84,14 @@ public class SecurityConfig {
|
|||||||
exceptions.defaultAuthenticationEntryPointFor(
|
exceptions.defaultAuthenticationEntryPointFor(
|
||||||
new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED),
|
new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED),
|
||||||
new AntPathRequestMatcher("/api/**")
|
new AntPathRequestMatcher("/api/**")
|
||||||
)
|
).accessDeniedHandler(csrfAccessDeniedHandler())
|
||||||
)
|
)
|
||||||
.csrf((csrf) -> csrf.ignoringRequestMatchers("/ws/**", "/api/**"));
|
.csrf((csrf) ->
|
||||||
|
csrf
|
||||||
|
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
|
||||||
|
.ignoringRequestMatchers("/ws/**")
|
||||||
|
)
|
||||||
|
.addFilterAfter(csrfTokenCookieFilter(), CsrfFilter.class);
|
||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,4 +109,51 @@ public class SecurityConfig {
|
|||||||
TwitchOAuth2UserService twitchOAuth2UserService() {
|
TwitchOAuth2UserService twitchOAuth2UserService() {
|
||||||
return new TwitchOAuth2UserService();
|
return new TwitchOAuth2UserService();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private AccessDeniedHandler csrfAccessDeniedHandler() {
|
||||||
|
return (request, response, accessDeniedException) -> {
|
||||||
|
if (accessDeniedException instanceof CsrfException) {
|
||||||
|
LOG.warn(
|
||||||
|
"CSRF failure for {} {} - referer: {}, origin: {}, message: {}",
|
||||||
|
request.getMethod(),
|
||||||
|
request.getRequestURI(),
|
||||||
|
request.getHeader("Referer"),
|
||||||
|
request.getHeader("Origin"),
|
||||||
|
accessDeniedException.getMessage()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
response.sendError(HttpServletResponse.SC_FORBIDDEN, accessDeniedException.getMessage());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the XSRF-TOKEN cookie is always present for browser clients and mirror the current
|
||||||
|
* token value on every request. This helps client-side fetch calls include the token header and
|
||||||
|
* aids debugging when tokens are missing.
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
OncePerRequestFilter csrfTokenCookieFilter() {
|
||||||
|
return new OncePerRequestFilter() {
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(
|
||||||
|
HttpServletRequest request,
|
||||||
|
HttpServletResponse response,
|
||||||
|
FilterChain filterChain
|
||||||
|
) throws java.io.IOException, jakarta.servlet.ServletException {
|
||||||
|
CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
|
||||||
|
if (csrfToken != null) {
|
||||||
|
String token = csrfToken.getToken();
|
||||||
|
Cookie existingCookie = WebUtils.getCookie(request, "XSRF-TOKEN");
|
||||||
|
if (existingCookie == null || !token.equals(existingCookie.getValue())) {
|
||||||
|
Cookie cookie = new Cookie("XSRF-TOKEN", token);
|
||||||
|
cookie.setPath("/");
|
||||||
|
cookie.setSecure(request.isSecure());
|
||||||
|
cookie.setHttpOnly(false);
|
||||||
|
response.addCookie(cookie);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
41
src/main/resources/static/js/csrf.js
Normal file
41
src/main/resources/static/js/csrf.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
(function () {
|
||||||
|
const CSRF_COOKIE_NAME = "XSRF-TOKEN";
|
||||||
|
const DEFAULT_HEADER_NAME = "X-XSRF-TOKEN";
|
||||||
|
const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS", "TRACE"]);
|
||||||
|
const originalFetch = window.fetch;
|
||||||
|
|
||||||
|
function getCookie(name) {
|
||||||
|
return document.cookie
|
||||||
|
.split(";")
|
||||||
|
.map((c) => c.trim())
|
||||||
|
.filter((c) => c.startsWith(name + "="))
|
||||||
|
.map((c) => c.substring(name.length + 1))[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSameOrigin(url) {
|
||||||
|
const parsed = new URL(url, window.location.href);
|
||||||
|
return parsed.origin === window.location.origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMeta(name) {
|
||||||
|
const el = document.querySelector(`meta[name=\"${name}\"]`);
|
||||||
|
return el ? el.getAttribute("content") : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.fetch = function patchedFetch(input, init = {}) {
|
||||||
|
const request = new Request(input, init);
|
||||||
|
const method = (request.method || "GET").toUpperCase();
|
||||||
|
|
||||||
|
if (!SAFE_METHODS.has(method) && isSameOrigin(request.url)) {
|
||||||
|
const token = getCookie(CSRF_COOKIE_NAME) || getMeta("_csrf");
|
||||||
|
const headerName = getMeta("_csrf_header") || DEFAULT_HEADER_NAME;
|
||||||
|
if (token) {
|
||||||
|
const headers = new Headers(request.headers || {});
|
||||||
|
headers.set(headerName, token);
|
||||||
|
return originalFetch(new Request(request, { headers }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalFetch(request);
|
||||||
|
};
|
||||||
|
})();
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>Imgfloat Admin</title>
|
<title>Imgfloat Admin</title>
|
||||||
|
<meta name="_csrf" th:content="${_csrf.token}" />
|
||||||
|
<meta name="_csrf_header" th:content="${_csrf.headerName}" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<link rel="stylesheet" href="/css/styles.css" />
|
<link rel="stylesheet" href="/css/styles.css" />
|
||||||
<link
|
<link
|
||||||
@@ -14,6 +16,7 @@
|
|||||||
/>
|
/>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js"></script>
|
||||||
|
<script src="/js/csrf.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body class="admin-body">
|
<body class="admin-body">
|
||||||
<div class="admin-frame">
|
<div class="admin-frame">
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>Imgfloat Dashboard</title>
|
<title>Imgfloat Dashboard</title>
|
||||||
|
<meta name="_csrf" th:content="${_csrf.token}" />
|
||||||
|
<meta name="_csrf_header" th:content="${_csrf.headerName}" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<link rel="stylesheet" href="/css/styles.css" />
|
<link rel="stylesheet" href="/css/styles.css" />
|
||||||
</head>
|
</head>
|
||||||
@@ -109,6 +111,7 @@
|
|||||||
<section class="card download-card-block" th:insert="fragments/downloads :: downloads"></section>
|
<section class="card download-card-block" th:insert="fragments/downloads :: downloads"></section>
|
||||||
</div>
|
</div>
|
||||||
<script src="/js/cookie-consent.js"></script>
|
<script src="/js/cookie-consent.js"></script>
|
||||||
|
<script src="/js/csrf.js"></script>
|
||||||
<script src="/js/toast.js"></script>
|
<script src="/js/toast.js"></script>
|
||||||
<script src="/js/downloads.js"></script>
|
<script src="/js/downloads.js"></script>
|
||||||
<script th:inline="javascript">
|
<script th:inline="javascript">
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>Imgfloat Admin</title>
|
<title>Imgfloat Admin</title>
|
||||||
|
<meta name="_csrf" th:content="${_csrf.token}" />
|
||||||
|
<meta name="_csrf_header" th:content="${_csrf.headerName}" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<link rel="stylesheet" href="/css/styles.css" />
|
<link rel="stylesheet" href="/css/styles.css" />
|
||||||
<link
|
<link
|
||||||
@@ -14,6 +16,7 @@
|
|||||||
/>
|
/>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js"></script>
|
||||||
|
<script src="/js/csrf.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body class="settings-body">
|
<body class="settings-body">
|
||||||
<div class="settings-shell">
|
<div class="settings-shell">
|
||||||
|
|||||||
Reference in New Issue
Block a user