Skip to content

Commit 781295a

Browse files
committed
Implement wrapper for VaadinSecurityConfigurer
Fixes #196
1 parent 317328a commit 781295a

File tree

3 files changed

+252
-5
lines changed

3 files changed

+252
-5
lines changed

demo/webapp-vaadin/src/main/java/software/xdev/sse/demo/security/MainWebSecurity.java

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@
55
import org.slf4j.Logger;
66
import org.slf4j.LoggerFactory;
77
import org.springframework.beans.factory.annotation.Autowired;
8+
import org.springframework.context.annotation.Bean;
89
import org.springframework.context.annotation.Configuration;
910
import org.springframework.security.config.Customizer;
1011
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
1112
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
13+
import org.springframework.security.web.DefaultSecurityFilterChain;
14+
import org.springframework.security.web.SecurityFilterChain;
1215
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
1316
import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter;
1417

@@ -19,12 +22,12 @@
1922
import software.xdev.sse.oauth2.loginurl.OAuth2LoginUrlStoreAdapter;
2023
import software.xdev.sse.oauth2.rememberloginproviderredirect.CookieBasedRememberRedirectOAuth2LoginProvider;
2124
import software.xdev.sse.oauth2.rememberme.OAuth2CookieRememberMeServices;
22-
import software.xdev.sse.vaadin.TotalVaadinFlowWebSecurity;
25+
import software.xdev.sse.vaadin.TotalVaadinFlowSecurityConfigurer;
2326

2427

2528
@EnableWebSecurity
2629
@Configuration
27-
public class MainWebSecurity extends TotalVaadinFlowWebSecurity
30+
public class MainWebSecurity
2831
{
2932
private static final Logger LOG = LoggerFactory.getLogger(MainWebSecurity.class);
3033

@@ -43,8 +46,8 @@ public class MainWebSecurity extends TotalVaadinFlowWebSecurity
4346
@Autowired
4447
protected OAuth2LoginUrlStoreAdapter oAuth2LoginUrlStoreAdapter;
4548

46-
@Override
47-
protected void configure(final HttpSecurity http) throws Exception
49+
@Bean
50+
protected SecurityFilterChain httpSecurityFilterChain(final HttpSecurity http) throws Exception
4851
{
4952
http
5053
.with(new AdvancedLoginPageAdapter<>(http), c -> c
@@ -71,8 +74,12 @@ protected void configure(final HttpSecurity http) throws Exception
7174

7275
this.cookieRememberMeServices.install(http);
7376

74-
super.configure(http);
77+
final DefaultSecurityFilterChain build = http
78+
.with(new TotalVaadinFlowSecurityConfigurer(), Customizer.withDefaults())
79+
.build();
7580

7681
LOG.info("Configuration finished - {} is spooled up and operational", this.getClass().getSimpleName());
82+
83+
return build;
7784
}
7885
}
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
package software.xdev.sse.vaadin;
2+
3+
import java.util.List;
4+
import java.util.Objects;
5+
import java.util.Set;
6+
import java.util.function.Consumer;
7+
import java.util.stream.Collectors;
8+
import java.util.stream.Stream;
9+
10+
import org.springframework.context.ApplicationContext;
11+
import org.springframework.http.HttpMethod;
12+
import org.springframework.http.HttpStatus;
13+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
14+
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
15+
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
16+
import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer;
17+
import org.springframework.security.web.access.AccessDeniedHandler;
18+
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
19+
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
20+
import org.springframework.security.web.util.matcher.OrRequestMatcher;
21+
import org.springframework.security.web.util.matcher.RequestMatcher;
22+
23+
import com.vaadin.flow.server.auth.NavigationAccessControl;
24+
import com.vaadin.flow.spring.security.VaadinSecurityConfigurer;
25+
26+
import software.xdev.sse.vaadin.csrf.VaadinCSRFDisableRequestMatcherProvider;
27+
import software.xdev.sse.web.loginurl.LoginUrlStore;
28+
29+
30+
/**
31+
* Wrapper for {@link VaadinSecurityConfigurer} that doesn't allow any VaadinSession to be created without previous
32+
* authentication.
33+
* <p>
34+
* Example usage:
35+
* <pre>
36+
* &#64;Configuration
37+
* &#64;EnableWebSecurity
38+
* public class MyWebSecurity {
39+
*
40+
* &#64;Bean
41+
* SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
42+
* return http.with(new TotalVaadinFlowSecurityConfigurer(), Customizer.withDefaults()).build();
43+
* }
44+
* }
45+
* </pre>
46+
* </p>
47+
*/
48+
public class TotalVaadinFlowSecurityConfigurer
49+
extends AbstractHttpConfigurer<TotalVaadinFlowSecurityConfigurer, HttpSecurity>
50+
{
51+
protected static final Set<String> DEFAULT_DO_NOT_RESPOND_UNAUTHORIZED_METHODS = Stream.of(
52+
HttpMethod.GET,
53+
HttpMethod.OPTIONS,
54+
HttpMethod.HEAD,
55+
HttpMethod.TRACE)
56+
.map(HttpMethod::name)
57+
.collect(Collectors.toSet());
58+
59+
protected final VaadinSecurityConfigurer vaadinSecurityConfigurer;
60+
61+
protected Consumer<VaadinSecurityConfigurer> customizeVaadinSecurityConfigurer;
62+
63+
public TotalVaadinFlowSecurityConfigurer()
64+
{
65+
this(VaadinSecurityConfigurer.vaadin()
66+
.enableCsrfConfiguration(false)
67+
.enableExceptionHandlingConfiguration(false)
68+
.enableRequestCacheConfiguration(false)
69+
.enableAuthorizedRequestsConfiguration(false));
70+
}
71+
72+
protected TotalVaadinFlowSecurityConfigurer(final VaadinSecurityConfigurer vaadinSecurityConfigurer)
73+
{
74+
this.vaadinSecurityConfigurer = vaadinSecurityConfigurer;
75+
}
76+
77+
public TotalVaadinFlowSecurityConfigurer customizeVaadin(
78+
final Consumer<VaadinSecurityConfigurer> customizeVaadinSecurityConfigurer)
79+
{
80+
this.customizeVaadinSecurityConfigurer = customizeVaadinSecurityConfigurer;
81+
return this;
82+
}
83+
84+
@Override
85+
public void setBuilder(final HttpSecurity builder)
86+
{
87+
super.setBuilder(builder);
88+
this.vaadinSecurityConfigurer.setBuilder(builder);
89+
}
90+
91+
@Override
92+
public void init(final HttpSecurity http) throws Exception
93+
{
94+
if(this.customizeVaadinSecurityConfigurer != null)
95+
{
96+
this.customizeVaadinSecurityConfigurer.accept(this.vaadinSecurityConfigurer);
97+
}
98+
this.vaadinSecurityConfigurer.init(http);
99+
100+
this.initExceptionHandling(http);
101+
this.initCSRF(http);
102+
this.initRequestCache(http);
103+
this.initAuthorizeHttpRequests(http);
104+
}
105+
106+
@Override
107+
public void configure(final HttpSecurity http) throws Exception
108+
{
109+
this.vaadinSecurityConfigurer.configure(http);
110+
this.configureLoginViewFromLoginUrlStore(http);
111+
}
112+
113+
// region Exception Handling
114+
protected void initExceptionHandling(final HttpSecurity http) throws Exception
115+
{
116+
http.exceptionHandling(cfg -> {
117+
this.configureExceptionHandler(cfg);
118+
this.addDefaultAuthenticationEntryPointFor(cfg);
119+
});
120+
}
121+
122+
protected void configureExceptionHandler(final ExceptionHandlingConfigurer<HttpSecurity> cfg)
123+
{
124+
// Removed support for Hilla endpoints
125+
cfg.accessDeniedHandler(this.createAccessDeniedHandler());
126+
}
127+
128+
protected void addDefaultAuthenticationEntryPointFor(final ExceptionHandlingConfigurer<HttpSecurity> cfg)
129+
{
130+
// This is required for so that stuff like Vaadin's POST requests are not redirected to the
131+
// login site (causes JavaScript crash as HTML can't be parsed).
132+
cfg.defaultAuthenticationEntryPointFor(
133+
new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED),
134+
request -> !DEFAULT_DO_NOT_RESPOND_UNAUTHORIZED_METHODS.contains(request.getMethod()));
135+
}
136+
137+
protected AccessDeniedHandler createAccessDeniedHandler()
138+
{
139+
return new AccessDeniedHandlerImpl();
140+
}
141+
142+
// endregion
143+
144+
// region CSRF
145+
protected void initCSRF(final HttpSecurity http) throws Exception
146+
{
147+
final List<RequestMatcher> vaadinCSRFDisableRequestMatchers = this.getApplicationContext()
148+
.getBeansOfType(VaadinCSRFDisableRequestMatcherProvider.class)
149+
.values()
150+
.stream()
151+
.map(VaadinCSRFDisableRequestMatcherProvider::getMatcher)
152+
.filter(Objects::nonNull)
153+
.toList();
154+
155+
// Removed out-of-the-box Spring CSRF as Vaadin bring its own CSRF
156+
// Otherwise sessions are created on rogue POST request
157+
// If CSRF is required for other parts of the app, whitelist them using requireCsrfProtectionMatcher
158+
if(vaadinCSRFDisableRequestMatchers.isEmpty())
159+
{
160+
// NOTE that Springs default logout view will not show up when CSRF is disabled!
161+
http.csrf(AbstractHttpConfigurer::disable);
162+
}
163+
else
164+
{
165+
final OrRequestMatcher matcher = new OrRequestMatcher(vaadinCSRFDisableRequestMatchers);
166+
// Using ignoringRequestMatchers is not working as it's joined (using AND)
167+
// with CsrfFilter.DEFAULT_CSRF_MATCHER
168+
http.csrf(c -> c.requireCsrfProtectionMatcher(matcher::matches));
169+
}
170+
}
171+
172+
// endregion
173+
174+
// region RequestCache
175+
176+
protected void initRequestCache(final HttpSecurity http) throws Exception
177+
{
178+
final SecureVaadinRequestCache vaadinDefaultRequestCache = Objects.requireNonNull(
179+
this.getApplicationContext().getBean(SecureVaadinRequestCache.class),
180+
"Failed to find SecureVaadinRequestCache");
181+
182+
// NOTE: Creates session by default for redirect after auth
183+
// Example: /settings -> /login -> OIDC-Server -> /login/oauth2/code/... -> /settings
184+
// See also HttpSessionRequestCache#createSessionAllowed
185+
http.requestCache(cfg -> cfg.requestCache(vaadinDefaultRequestCache));
186+
}
187+
188+
// endregion
189+
190+
// region HTTP Requests
191+
192+
protected void initAuthorizeHttpRequests(final HttpSecurity http) throws Exception
193+
{
194+
http.authorizeHttpRequests(this::configureAuthorizeHttpRequests);
195+
}
196+
197+
protected void configureAuthorizeHttpRequests(
198+
final AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry urlRegistry)
199+
{
200+
// By default, Vaadin configures authorizeHttpRequests to allow anonymous requests for certain
201+
// endpoints like /favicon.ico or /VAADIN/**
202+
// This is a problem because:
203+
// 1. A new (heavy) Vaadin session is created, which might cause Session leak and performance problems
204+
// 2. There is no user associated with it which the app is not designed for
205+
206+
// The registered filters may also cause a performance impact
207+
208+
// ALL VAADIN REQUESTS REQUIRE AUTHENTICATION
209+
urlRegistry.anyRequest().authenticated();
210+
}
211+
212+
// endregion
213+
214+
// region LoginUrl Store
215+
@SuppressWarnings("java:S1172") // API: Might be required downstream
216+
protected void configureLoginViewFromLoginUrlStore(final HttpSecurity http)
217+
{
218+
final LoginUrlStore loginUrlStore = this.getSharedObject(LoginUrlStore.class);
219+
if(loginUrlStore != null)
220+
{
221+
// This is usually only needed when the authentication is anonymous
222+
// and navigation to a view that requires non-anonymous authentication happens
223+
this.getSharedObject(NavigationAccessControl.class).setLoginView(loginUrlStore.getLoginUrl());
224+
}
225+
}
226+
// endregion
227+
228+
protected ApplicationContext getApplicationContext()
229+
{
230+
return this.getSharedObject(ApplicationContext.class);
231+
}
232+
233+
protected <T> T getSharedObject(final Class<T> clazz)
234+
{
235+
return this.getBuilder().getSharedObject(clazz);
236+
}
237+
}

vaadin/src/main/java/software/xdev/sse/vaadin/TotalVaadinFlowWebSecurity.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,10 @@
4343
/**
4444
* Override of {@link VaadinWebSecurity} that doesn't allow any VaadinSession to be created without previous
4545
* authentication.
46+
* @deprecated Vaadin has decided that they will remove {@link VaadinWebSecurity}.
47+
* Use {@link TotalVaadinFlowSecurityConfigurer} instead.
4648
*/
49+
@Deprecated(forRemoval = true)
4750
@SuppressWarnings("java:S6813")
4851
public abstract class TotalVaadinFlowWebSecurity extends VaadinWebSecurity
4952
{

0 commit comments

Comments
 (0)