Skip to content

Commit 1f655d1

Browse files
authored
fix: make sure request principal is available (#22368) (#22399)
When Spring Security request matchers are executed within SpringPathAccessChecker the request object is a stub instance that throw UnsupportedOperationException for many methods. This can cause failure when the path access checker is used in combination with request matchers that, for example, try to access the request user principal. An example is the pre-configured 'isAllowedHillaView' matcher. This change wraps the request matchers configured by Vaadin so that the request principal is taken from the Spring Security context, if not available on the request. In addition provides documentation and helper to set a global HttpServletRequestTransformer to augment all requests handled by WebInvocationPrivilegeEvaluator with the proper getUserPrincipal method override. Fixes #22284
1 parent 5bd8e19 commit 1f655d1

File tree

10 files changed

+623
-158
lines changed

10 files changed

+623
-158
lines changed

flow-tests/vaadin-spring-tests/test-spring-security-flow-routepathaccesschecker/pom.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,9 @@
182182
<groupId>com.vaadin</groupId>
183183
<artifactId>flow-maven-plugin</artifactId>
184184
<version>${project.version}</version>
185+
<configuration>
186+
<frontendHotdeploy>false</frontendHotdeploy>
187+
</configuration>
185188
<executions>
186189
<execution>
187190
<goals>

flow-tests/vaadin-spring-tests/test-spring-security-flow-routepathaccesschecker/src/main/java/com/vaadin/flow/spring/flowsecurity/SecurityConfig.java

Lines changed: 59 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,39 @@
11
package com.vaadin.flow.spring.flowsecurity;
22

3+
import com.vaadin.flow.component.UI;
4+
import com.vaadin.flow.internal.UrlUtil;
5+
import com.vaadin.flow.internal.hilla.FileRouterRequestUtil;
6+
import com.vaadin.flow.spring.RootMappedCondition;
7+
import com.vaadin.flow.spring.VaadinConfigurationProperties;
8+
import com.vaadin.flow.spring.flowsecurity.data.UserInfo;
9+
import com.vaadin.flow.spring.flowsecurity.service.UserInfoService;
10+
import com.vaadin.flow.spring.flowsecurity.views.LoginView;
11+
import com.vaadin.flow.spring.security.NavigationAccessControlConfigurer;
12+
import com.vaadin.flow.spring.security.VaadinAwareSecurityContextHolderStrategyConfiguration;
13+
import com.vaadin.flow.spring.security.VaadinSecurityConfigurer;
314
import jakarta.servlet.ServletContext;
4-
5-
import java.util.stream.Collectors;
6-
715
import org.springframework.beans.factory.annotation.Autowired;
816
import org.springframework.context.annotation.Bean;
917
import org.springframework.context.annotation.Configuration;
18+
import org.springframework.context.annotation.Import;
1019
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
1120
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
1221
import org.springframework.security.core.authority.SimpleGrantedAuthority;
1322
import org.springframework.security.core.userdetails.User;
1423
import org.springframework.security.core.userdetails.UserDetails;
1524
import org.springframework.security.core.userdetails.UsernameNotFoundException;
1625
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
26+
import org.springframework.security.web.SecurityFilterChain;
1727

18-
import com.vaadin.flow.component.UI;
19-
import com.vaadin.flow.internal.UrlUtil;
20-
import com.vaadin.flow.spring.RootMappedCondition;
21-
import com.vaadin.flow.spring.VaadinConfigurationProperties;
22-
import com.vaadin.flow.spring.flowsecurity.data.UserInfo;
23-
import com.vaadin.flow.spring.flowsecurity.service.UserInfoService;
24-
import com.vaadin.flow.spring.flowsecurity.views.LoginView;
25-
import com.vaadin.flow.spring.security.NavigationAccessControlConfigurer;
26-
import com.vaadin.flow.spring.security.VaadinWebSecurity;
28+
import java.util.stream.Collectors;
2729

2830
import static com.vaadin.flow.spring.flowsecurity.service.UserInfoService.ROLE_ADMIN;
31+
import static com.vaadin.flow.spring.security.RequestUtil.antMatchers;
2932

3033
@EnableWebSecurity
3134
@Configuration
32-
public class SecurityConfig extends VaadinWebSecurity {
35+
@Import(VaadinAwareSecurityContextHolderStrategyConfiguration.class)
36+
public class SecurityConfig {
3337

3438
@Autowired
3539
private UserInfoService userInfoService;
@@ -46,6 +50,21 @@ static NavigationAccessControlConfigurer navigationAccessControlConfigurer() {
4650
.withRoutePathAccessChecker();
4751
}
4852

53+
/*
54+
* Simulates Hilla implementation that accesses request principal.
55+
*/
56+
@Bean
57+
FileRouterRequestUtil sutbFileRouterRequestUtil() {
58+
return request -> {
59+
var principal = request.getUserPrincipal();
60+
if (principal != null) {
61+
// do nothing, just prevent IDE from complaining about unused
62+
// variable
63+
}
64+
return false;
65+
};
66+
}
67+
4968
public String getLogoutSuccessUrl() {
5069
String logoutSuccessUrl;
5170
String mapping = vaadinConfigurationProperties.getUrlMapping();
@@ -61,40 +80,38 @@ public String getLogoutSuccessUrl() {
6180
return logoutSuccessUrl;
6281
}
6382

64-
@Override
65-
public void configure(HttpSecurity http) throws Exception {
66-
// @formatter:off
83+
@Bean
84+
SecurityFilterChain vaadinSecurityFilterChain(HttpSecurity http)
85+
throws Exception {
6786
http.authorizeHttpRequests(cfg -> cfg
6887
.requestMatchers(antMatchers("/admin-only/**", "/admin"))
69-
.hasAnyRole(ROLE_ADMIN)
70-
.requestMatchers(antMatchers("/private"))
71-
.authenticated()
72-
.requestMatchers(antMatchers("/", "/public/**", "/another", "/menu-list"))
73-
.permitAll()
74-
.requestMatchers(antMatchers("/error"))
75-
.permitAll()
88+
.hasAnyRole(ROLE_ADMIN).requestMatchers(antMatchers("/private"))
89+
.authenticated()
90+
.requestMatchers(antMatchers("/", "/public/**", "/another",
91+
"/menu-list"))
92+
.permitAll().requestMatchers(antMatchers("/error")).permitAll()
7693
// routes aliases
7794
.requestMatchers(antMatchers("/alias-for-admin"))
78-
.hasAnyRole(ROLE_ADMIN)
79-
.requestMatchers(antMatchers("/home", "/hey/**"))
80-
.permitAll()
81-
);
95+
.hasAnyRole(ROLE_ADMIN)
96+
.requestMatchers(antMatchers("/home", "/hey/**")).permitAll()
97+
.requestMatchers(antMatchers("/all-logged-in/**"))
98+
.authenticated());
8299
// @formatter:on
83-
84-
super.configure(http);
85-
if (getLogoutSuccessUrl().equals("/")) {
86-
// Test the default url with empty context path
87-
setLoginView(http, LoginView.class);
88-
} else {
89-
setLoginView(http, LoginView.class, getLogoutSuccessUrl());
90-
}
91-
http.logout(cfg -> cfg
92-
.addLogoutHandler((request, response, authentication) -> {
93-
UI ui = UI.getCurrent();
94-
ui.accessSynchronously(() -> ui.getPage()
95-
.setLocation(UrlUtil.getServletPathRelative(
96-
getLogoutSuccessUrl(), request)));
97-
}));
100+
http.with(VaadinSecurityConfigurer.vaadin(), vaadin -> {
101+
if (getLogoutSuccessUrl().equals("/")) {
102+
// Test the default url with empty context path
103+
vaadin.loginView(LoginView.class);
104+
} else {
105+
vaadin.loginView(LoginView.class, getLogoutSuccessUrl());
106+
}
107+
vaadin.addLogoutHandler((request, response, authentication) -> {
108+
UI ui = UI.getCurrent();
109+
ui.accessSynchronously(() -> ui.getPage().setLocation(
110+
UrlUtil.getServletPathRelative(getLogoutSuccessUrl(),
111+
request)));
112+
});
113+
});
114+
return http.build();
98115
}
99116

100117
@Bean
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright 2000-2025 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*
16+
*/
17+
18+
package com.vaadin.hilla;
19+
20+
// Stub class to simulate the presence of Hilla
21+
class EndpointController {
22+
}

flow-tests/vaadin-spring-tests/test-spring-security-flow-standalone-routepathaccesschecker/src/main/java/com/vaadin/flow/spring/flowsecurity/SecurityConfig.java

Lines changed: 81 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
11
package com.vaadin.flow.spring.flowsecurity;
22

3+
import com.vaadin.flow.component.UI;
4+
import com.vaadin.flow.internal.UrlUtil;
5+
import com.vaadin.flow.server.HandlerHelper;
6+
import com.vaadin.flow.spring.RootMappedCondition;
7+
import com.vaadin.flow.spring.VaadinConfigurationProperties;
8+
import com.vaadin.flow.spring.flowsecurity.data.UserInfo;
9+
import com.vaadin.flow.spring.flowsecurity.service.UserInfoService;
10+
import com.vaadin.flow.spring.flowsecurity.views.LoginView;
11+
import com.vaadin.flow.spring.security.AuthenticationContext;
12+
import com.vaadin.flow.spring.security.NavigationAccessControlConfigurer;
13+
import com.vaadin.flow.spring.security.RequestUtil;
14+
import com.vaadin.flow.spring.security.SpringAccessPathChecker;
15+
import com.vaadin.flow.spring.security.UidlRedirectStrategy;
316
import jakarta.servlet.ServletContext;
4-
5-
import java.util.stream.Collectors;
6-
717
import org.springframework.beans.factory.annotation.Autowired;
818
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
919
import org.springframework.context.annotation.Bean;
1020
import org.springframework.context.annotation.Configuration;
21+
import org.springframework.context.annotation.Primary;
1122
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
1223
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
1324
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
@@ -16,23 +27,17 @@
1627
import org.springframework.security.core.userdetails.UserDetails;
1728
import org.springframework.security.core.userdetails.UsernameNotFoundException;
1829
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
30+
import org.springframework.security.web.DefaultSecurityFilterChain;
1931
import org.springframework.security.web.SecurityFilterChain;
20-
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
32+
import org.springframework.security.web.access.AuthorizationManagerWebInvocationPrivilegeEvaluator.HttpServletRequestTransformer;
33+
import org.springframework.security.web.access.PathPatternRequestTransformer;
34+
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
2135

22-
import com.vaadin.flow.component.UI;
23-
import com.vaadin.flow.internal.UrlUtil;
24-
import com.vaadin.flow.spring.RootMappedCondition;
25-
import com.vaadin.flow.spring.VaadinConfigurationProperties;
26-
import com.vaadin.flow.spring.flowsecurity.data.UserInfo;
27-
import com.vaadin.flow.spring.flowsecurity.service.UserInfoService;
28-
import com.vaadin.flow.spring.flowsecurity.views.LoginView;
29-
import com.vaadin.flow.spring.security.AuthenticationContext;
30-
import com.vaadin.flow.spring.security.NavigationAccessControlConfigurer;
31-
import com.vaadin.flow.spring.security.RequestUtil;
36+
import java.security.Principal;
37+
import java.util.stream.Collectors;
3238

3339
import static com.vaadin.flow.spring.flowsecurity.service.UserInfoService.ROLE_ADMIN;
3440
import static com.vaadin.flow.spring.security.RequestUtil.antMatchers;
35-
import static com.vaadin.flow.spring.security.VaadinSecurityConfigurer.vaadin;
3641

3742
@EnableWebSecurity
3843
@Configuration
@@ -60,51 +65,88 @@ public AuthenticationContext authenticationContext() {
6065
@Bean
6166
static NavigationAccessControlConfigurer navigationAccessControlConfigurer() {
6267
return new NavigationAccessControlConfigurer()
63-
.withRoutePathAccessChecker();
68+
.withLoginView(LoginView.class).withRoutePathAccessChecker();
69+
}
70+
71+
@Bean
72+
@Primary
73+
static HttpServletRequestTransformer customRequestTransformer() {
74+
return SpringAccessPathChecker.principalAwareRequestTransformer(
75+
new PathPatternRequestTransformer());
6476
}
6577

6678
@Bean
6779
public SecurityFilterChain webFilterChain(HttpSecurity http,
6880
AuthenticationContext authenticationContext) throws Exception {
6981
// Setup
7082
http.csrf(AbstractHttpConfigurer::disable); // simple for testing
71-
// purpose
83+
// purpose
7284

7385
// Homemade security for Vaadin application, not fully functional as the
7486
// configuration provided by VaadinWebSecurity
7587
// @formatter:off
7688
http.authorizeHttpRequests(auth -> auth
89+
// Ensures that SpringPathAccessChecker does not fail when matchers get Principal from HTTP request
90+
.requestMatchers(request -> {
91+
Principal principal = request.getUserPrincipal();
92+
if (principal == null) {
93+
// Do nothing, just avoid IDE complain about not used variable
94+
}
95+
return false; // no need to match rule, we just want to access principal.
96+
}).denyAll()
7797
// Permit access to static resources
7898
.requestMatchers(PathRequest.toStaticResources().atCommonLocations())
79-
.permitAll()
99+
.permitAll()
100+
// Permit access to vaadin's internal communication
101+
.requestMatchers(request -> HandlerHelper
102+
.isFrameworkInternalRequest("/*", request))
103+
.permitAll()
104+
.requestMatchers(requestUtil::isAnonymousRoute)
105+
.permitAll()
106+
// Permit technical access to vaadin's static files
107+
.requestMatchers(antMatchers("/VAADIN/**")).permitAll()
108+
// custom request matchers. using 'routeAwareAntMatcher' to
109+
// allow checking route and alias paths against patterns
80110
.requestMatchers(antMatchers("/admin-only/**", "/admin"))
81-
.hasAnyRole(ROLE_ADMIN)
111+
.hasAnyRole(ROLE_ADMIN)
82112
.requestMatchers(antMatchers("/private"))
83-
.authenticated()
113+
.authenticated()
84114
.requestMatchers(antMatchers("/", "/public/**", "/another"))
85-
.permitAll()
86-
.requestMatchers(new AntPathRequestMatcher("/error"))
87-
.permitAll()
115+
.permitAll()
116+
117+
.requestMatchers(antMatchers("/error"))
118+
.permitAll()
88119
// routes aliases
89120
.requestMatchers(antMatchers("/alias-for-admin"))
90-
.hasAnyRole(ROLE_ADMIN)
121+
.hasAnyRole(ROLE_ADMIN)
91122
.requestMatchers(antMatchers("/home", "/hey/**"))
92-
.permitAll()
93-
);
123+
.permitAll()
124+
.requestMatchers(antMatchers("/all-logged-in/**", "/passthrough/**"))
125+
.authenticated()
126+
);
94127
// @formatter:on
95-
http.with(vaadin(),
96-
cfg -> cfg.loginView(LoginView.class, getLogoutSuccessUrl())
97-
.addLogoutHandler(
98-
(request, response, authentication) -> {
99-
UI ui = UI.getCurrent();
100-
ui.accessSynchronously(() -> ui.getPage()
101-
.setLocation(UrlUtil
102-
.getServletPathRelative(
103-
getLogoutSuccessUrl(),
104-
request)));
105-
}));
106-
107-
return http.build();
128+
http.logout(cfg -> {
129+
SimpleUrlLogoutSuccessHandler logoutSuccessHandler = new SimpleUrlLogoutSuccessHandler();
130+
logoutSuccessHandler.setDefaultTargetUrl(getLogoutSuccessUrl());
131+
logoutSuccessHandler
132+
.setRedirectStrategy(new UidlRedirectStrategy());
133+
cfg.logoutSuccessHandler(logoutSuccessHandler);
134+
cfg.addLogoutHandler((request, response, authentication) -> {
135+
UI ui = UI.getCurrent();
136+
ui.accessSynchronously(() -> ui.getPage().setLocation(
137+
UrlUtil.getServletPathRelative(getLogoutSuccessUrl(),
138+
request)));
139+
});
140+
});
141+
// Custom login page with form authentication
142+
http.formLogin(cfg -> cfg.loginPage("/my/login/page").permitAll());
143+
DefaultSecurityFilterChain filterChain = http.build();
144+
// Test application uses AuthenticationContext, configure it with
145+
// the logout handlers
146+
AuthenticationContext.applySecurityConfiguration(http,
147+
authenticationContext);
148+
149+
return filterChain;
108150
}
109151

110152
public String getLogoutSuccessUrl() {

0 commit comments

Comments
 (0)