diff --git a/flow-tests/vaadin-spring-tests/test-spring-security-flow/src/main/java/com/vaadin/flow/spring/flowsecurity/SecurityConfig.java b/flow-tests/vaadin-spring-tests/test-spring-security-flow/src/main/java/com/vaadin/flow/spring/flowsecurity/SecurityConfig.java index 38a44d03300..6a2caa65425 100644 --- a/flow-tests/vaadin-spring-tests/test-spring-security-flow/src/main/java/com/vaadin/flow/spring/flowsecurity/SecurityConfig.java +++ b/flow-tests/vaadin-spring-tests/test-spring-security-flow/src/main/java/com/vaadin/flow/spring/flowsecurity/SecurityConfig.java @@ -1,32 +1,34 @@ package com.vaadin.flow.spring.flowsecurity; -import java.util.stream.Collectors; - import javax.servlet.ServletContext; -import com.vaadin.flow.spring.RootMappedCondition; -import com.vaadin.flow.spring.VaadinConfigurationProperties; -import com.vaadin.flow.spring.flowsecurity.data.UserInfo; -import com.vaadin.flow.spring.flowsecurity.data.UserInfoRepository; -import com.vaadin.flow.spring.flowsecurity.views.LoginView; -import com.vaadin.flow.spring.security.VaadinWebSecurityConfigurerAdapter; +import java.util.stream.Collectors; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.SecurityFilterChain; + +import com.vaadin.flow.spring.RootMappedCondition; +import com.vaadin.flow.spring.VaadinConfigurationProperties; +import com.vaadin.flow.spring.flowsecurity.data.UserInfo; +import com.vaadin.flow.spring.flowsecurity.data.UserInfoRepository; +import com.vaadin.flow.spring.flowsecurity.views.LoginView; +import com.vaadin.flow.spring.security.VaadinWebSecurity; @EnableWebSecurity @Configuration -public class SecurityConfig extends VaadinWebSecurityConfigurerAdapter { +public class SecurityConfig extends VaadinWebSecurity { public static String ROLE_USER = "user"; public static String ROLE_ADMIN = "admin"; @@ -60,38 +62,34 @@ public String getLogoutSuccessUrl() { } @Override - protected void configure(HttpSecurity http) throws Exception { - // Admin only access for given resources + public void configure(HttpSecurity http) throws Exception { http.authorizeRequests().antMatchers("/admin-only/**") .hasAnyRole(ROLE_ADMIN); - + http.authorizeRequests().antMatchers("/public/**").permitAll(); super.configure(http); - setLoginView(http, LoginView.class, getLogoutSuccessUrl()); } - @Override - public void configure(WebSecurity web) throws Exception { - super.configure(web); - web.ignoring().antMatchers("/public/**"); - } - - @Override - protected void configure(AuthenticationManagerBuilder auth) - throws Exception { - auth.userDetailsService(username -> { - UserInfo userInfo = userInfoRepository.findByUsername(username); - if (userInfo == null) { - throw new UsernameNotFoundException( - "No user present with username: " + username); - } else { - return new User(userInfo.getUsername(), - userInfo.getEncodedPassword(), - userInfo.getRoles().stream() - .map(role -> new SimpleGrantedAuthority( - "ROLE_" + role)) - .collect(Collectors.toList())); + @Bean + public InMemoryUserDetailsManager userDetailsService() { + return new InMemoryUserDetailsManager() { + @Override + public UserDetails loadUserByUsername(String username) + throws UsernameNotFoundException { + UserInfo userInfo = userInfoRepository.findByUsername(username); + if (userInfo == null) { + throw new UsernameNotFoundException( + "No user present with username: " + username); + } else { + return new User(userInfo.getUsername(), + userInfo.getEncodedPassword(), + userInfo.getRoles().stream() + .map(role -> new SimpleGrantedAuthority( + "ROLE_" + role)) + .collect(Collectors.toList())); + } } - }); + }; } + } diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinWebSecurity.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinWebSecurity.java new file mode 100644 index 00000000000..d5656e63807 --- /dev/null +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinWebSecurity.java @@ -0,0 +1,480 @@ +/* + * Copyright 2000-2022 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.spring.security; + +import javax.crypto.SecretKey; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration; +import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; +import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; +import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.jose.jws.MacAlgorithm; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.access.AccessDeniedHandlerImpl; +import org.springframework.security.web.access.DelegatingAccessDeniedHandler; +import org.springframework.security.web.access.RequestMatcherDelegatingAccessDeniedHandler; +import org.springframework.security.web.authentication.HttpStatusEntryPoint; +import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; +import org.springframework.security.web.csrf.CsrfException; +import org.springframework.security.web.savedrequest.RequestCache; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.AnyRequestMatcher; +import org.springframework.security.web.util.matcher.OrRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.internal.AnnotationReader; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.internal.RouteUtil; +import com.vaadin.flow.server.HandlerHelper; +import com.vaadin.flow.server.auth.ViewAccessChecker; +import com.vaadin.flow.spring.security.stateless.VaadinStatelessSecurityConfigurer; + +/** + * Provides basic Vaadin component-based security configuration for the project. + *

+ * Sets up security rules for a Vaadin application and restricts all URLs except + * for public resources and internal Vaadin URLs to authenticated user. + *

+ * The default behavior can be altered by extending the public/protected methods + * in the class. + *

+ * Provides default bean implementations for {@link SecurityFilterChain} and + * {@link WebSecurityCustomizer}. + *

+ * To use this, create your own web security class by extending this class and + * annotate it with @EnableWebSecurity and + * @Configuration. + *

+ * For example +@EnableWebSecurity +@Configuration +public class MyWebSecurity extends VaadinWebSecurity { + +} + * + */ +public abstract class VaadinWebSecurity { + + @Autowired + private VaadinDefaultRequestCache vaadinDefaultRequestCache; + + @Autowired + private RequestUtil requestUtil; + + @Autowired + private ViewAccessChecker viewAccessChecker; + + /** + * Registers default {@link SecurityFilterChain} bean. + *

+ * Defines a filter chain which is capable of being matched against an + * {@code HttpServletRequest}. in order to decide whether it applies to that + * request. + *

+ * {@link HttpSecurity} configuration can be customized by overriding + * {@link VaadinWebSecurity#configure(HttpSecurity)}. + */ + @Bean(name = "VaadinSecurityFilterChainBean") + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + configure(http); + return http.build(); + } + + /** + * Applies Vaadin default configuration to {@link HttpSecurity}. + * + * Typically, subclasses should call super to apply default Vaadin + * configuration in addition to custom rules. + * + * @param http + * the {@link HttpSecurity} to modify + * @throws Exception + * if an error occurs + */ + protected void configure(HttpSecurity http) throws Exception { + // Use a security context holder that can find the context from Vaadin + // specific classes + SecurityContextHolder.setStrategyName( + VaadinAwareSecurityContextHolderStrategy.class.getName()); + + // Respond with 401 Unauthorized HTTP status code for unauthorized + // requests for protected Hilla endpoints, so that the response could + // be handled on the client side using e.g. `InvalidSessionMiddleware`. + http.exceptionHandling() + .accessDeniedHandler(createAccessDeniedHandler()) + .defaultAuthenticationEntryPointFor( + new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED), + requestUtil::isEndpointRequest); + + // Vaadin has its own CSRF protection. + // Spring CSRF is not compatible with Vaadin internal requests + http.csrf().ignoringRequestMatchers( + requestUtil::isFrameworkInternalRequest); + + // Ensure automated requests to e.g. closing push channels, service + // workers, + // endpoints are not counted as valid targets to redirect user to on + // login + http.requestCache().requestCache(vaadinDefaultRequestCache); + + ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry urlRegistry = http + .authorizeRequests(); + // Vaadin internal requests must always be allowed to allow public Flow + // pages + // and/or login page implemented using Flow. + urlRegistry.requestMatchers(requestUtil::isFrameworkInternalRequest) + .permitAll(); + // Public endpoints are OK to access + urlRegistry.requestMatchers(requestUtil::isAnonymousEndpoint) + .permitAll(); + // Public routes are OK to access + urlRegistry.requestMatchers(requestUtil::isAnonymousRoute).permitAll(); + urlRegistry.requestMatchers(getDefaultHttpSecurityPermitMatcher( + requestUtil.getUrlMapping())).permitAll(); + + // matcher for Vaadin static (public) resources + urlRegistry.requestMatchers( + getDefaultWebSecurityIgnoreMatcher(requestUtil.getUrlMapping())) + .permitAll(); + + // all other requests require authentication + urlRegistry.anyRequest().authenticated(); + + // Enable view access control + viewAccessChecker.enable(); + } + + /** + * Registers default {@link WebSecurityCustomizer} bean. + *

+ * Beans of this type will automatically be used by + * {@link WebSecurityConfiguration} to customize {@link WebSecurity}. + *

+ * {@link WebSecurity} configuration can be customized by overriding + * {@link VaadinWebSecurity#configure(WebSecurity)} + *

+ * Default no {@link WebSecurity} customization is performed. + */ + @Bean(name = "VaadinWebSecurityCustomizerBean") + public WebSecurityCustomizer webSecurityCustomizer() { + return (web) -> { + try { + configure(web); + } catch (Exception e) { + throw new RuntimeException(e); + } + }; + } + + protected void configure(WebSecurity web) throws Exception { + // no-operation + } + + /** + * Matcher for framework internal requests. + * + * Assumes Vaadin servlet to be mapped on root path ({@literal /*}). + * + * @return default {@link HttpSecurity} bypass matcher + */ + public static RequestMatcher getDefaultHttpSecurityPermitMatcher() { + return getDefaultHttpSecurityPermitMatcher("/*"); + } + + /** + * Matcher for framework internal requests, with Vaadin servlet mapped on + * the given path. + * + * @param urlMapping + * url mapping for the Vaadin servlet. + * @return default {@link HttpSecurity} bypass matcher + */ + public static RequestMatcher getDefaultHttpSecurityPermitMatcher( + String urlMapping) { + Objects.requireNonNull(urlMapping, + "Vaadin servlet url mapping is required"); + Stream.Builder paths = Stream.builder(); + Stream.of(HandlerHelper.getPublicResourcesRequiringSecurityContext()) + .map(path -> RequestUtil.applyUrlMapping(urlMapping, path)) + .forEach(paths::add); + + String mappedRoot = RequestUtil.applyUrlMapping(urlMapping, ""); + if (!"/".equals(mappedRoot)) { + // When using an url path, static resources are still fetched from + // /VAADIN/ in the context root + paths.add("/VAADIN/**"); + } + return new OrRequestMatcher(paths.build() + .map(AntPathRequestMatcher::new).collect(Collectors.toList())); + } + + /** + * Matcher for Vaadin static (public) resources. + * + * Assumes Vaadin servlet to be mapped on root path ({@literal /*}). + * + * @return default {@link WebSecurity} ignore matcher + */ + public static RequestMatcher getDefaultWebSecurityIgnoreMatcher() { + return getDefaultWebSecurityIgnoreMatcher("/*"); + } + + /** + * Matcher for Vaadin static (public) resources, with Vaadin servlet mapped + * on the given path. + * + * Assumes Vaadin servlet to be mapped on root path ({@literal /*}). + * + * @param urlMapping + * the url mapping for the Vaadin servlet + * @return default {@link WebSecurity} ignore matcher + */ + public static RequestMatcher getDefaultWebSecurityIgnoreMatcher( + String urlMapping) { + Objects.requireNonNull(urlMapping, + "Vaadin servlet url mapping is required"); + return new OrRequestMatcher(Stream + .of(HandlerHelper.getPublicResources()) + .map(path -> RequestUtil.applyUrlMapping(urlMapping, path)) + .map(AntPathRequestMatcher::new).collect(Collectors.toList())); + } + + /** + * Sets up login for the application using form login with the given path + * for the login view. + *

+ * This is used when your application uses a Hilla based login view + * available at the given path. + * + * @param http + * the http security from {@link #filterChain(HttpSecurity)} + * @param hillaLoginViewPath + * the path to the login view + * @throws Exception + * if something goes wrong + */ + protected void setLoginView(HttpSecurity http, String hillaLoginViewPath) + throws Exception { + setLoginView(http, hillaLoginViewPath, "/"); + } + + /** + * Sets up login for the application using form login with the given path + * for the login view. + *

+ * This is used when your application uses a Hilla based login view + * available at the given path. + * + * @param http + * the http security from {@link #filterChain(HttpSecurity)} + * @param hillaLoginViewPath + * the path to the login view + * @param logoutUrl + * the URL to redirect the user to after logging out + * @throws Exception + * if something goes wrong + */ + protected void setLoginView(HttpSecurity http, String hillaLoginViewPath, + String logoutUrl) throws Exception { + hillaLoginViewPath = applyUrlMapping(hillaLoginViewPath); + FormLoginConfigurer formLogin = http.formLogin(); + formLogin.loginPage(hillaLoginViewPath).permitAll(); + formLogin.successHandler( + getVaadinSavedRequestAwareAuthenticationSuccessHandler(http)); + http.logout().logoutSuccessUrl(logoutUrl); + http.exceptionHandling().defaultAuthenticationEntryPointFor( + new LoginUrlAuthenticationEntryPoint(hillaLoginViewPath), + AnyRequestMatcher.INSTANCE); + viewAccessChecker.setLoginView(hillaLoginViewPath); + } + + /** + * Sets up login for the application using the given Flow login view. + * + * @param http + * the http security from {@link #filterChain(HttpSecurity)} + * @param flowLoginView + * the login view to use + * @throws Exception + * if something goes wrong + */ + protected void setLoginView(HttpSecurity http, + Class flowLoginView) throws Exception { + setLoginView(http, flowLoginView, "/"); + } + + /** + * Sets up login for the application using the given Flow login view. + * + * @param http + * the http security from {@link #filterChain(HttpSecurity)} + * @param flowLoginView + * the login view to use + * @param logoutUrl + * the URL to redirect the user to after logging out + * + * @throws Exception + * if something goes wrong + */ + protected void setLoginView(HttpSecurity http, + Class flowLoginView, String logoutUrl) + throws Exception { + Optional route = AnnotationReader.getAnnotationFor(flowLoginView, + Route.class); + + if (!route.isPresent()) { + throw new IllegalArgumentException( + "Unable find a @Route annotation on the login view " + + flowLoginView.getName()); + } + + String loginPath = RouteUtil.getRoutePath(flowLoginView, route.get()); + if (!loginPath.startsWith("/")) { + loginPath = "/" + loginPath; + } + loginPath = applyUrlMapping(loginPath); + + // Actually set it up + FormLoginConfigurer formLogin = http.formLogin(); + formLogin.loginPage(loginPath).permitAll(); + formLogin.successHandler( + getVaadinSavedRequestAwareAuthenticationSuccessHandler(http)); + http.csrf().ignoringAntMatchers(loginPath); + http.logout().logoutSuccessUrl(logoutUrl); + http.exceptionHandling().defaultAuthenticationEntryPointFor( + new LoginUrlAuthenticationEntryPoint(loginPath), + AnyRequestMatcher.INSTANCE); + viewAccessChecker.setLoginView(flowLoginView); + } + + /** + * Sets up stateless JWT authentication using cookies. + * + * @param http + * the http security from {@link #filterChain(HttpSecurity)} + * @param secretKey + * the secret key for encoding and decoding JWTs, must use a + * {@link MacAlgorithm} algorithm name + * @param issuer + * the issuer JWT claim + * @throws Exception + * if something goes wrong + */ + protected void setStatelessAuthentication(HttpSecurity http, + SecretKey secretKey, String issuer) throws Exception { + setStatelessAuthentication(http, secretKey, issuer, 1800L); + } + + /** + * Sets up stateless JWT authentication using cookies. + * + * @param http + * the http security from {@link #filterChain(HttpSecurity)} + * @param secretKey + * the secret key for encoding and decoding JWTs, must use a + * {@link MacAlgorithm} algorithm name + * @param issuer + * the issuer JWT claim + * @param expiresIn + * lifetime of the JWT and cookies, in seconds + * @throws Exception + * if something goes wrong + */ + protected void setStatelessAuthentication(HttpSecurity http, + SecretKey secretKey, String issuer, long expiresIn) + throws Exception { + VaadinStatelessSecurityConfigurer vaadinStatelessSecurityConfigurer = new VaadinStatelessSecurityConfigurer<>(); + http.apply(vaadinStatelessSecurityConfigurer); + + vaadinStatelessSecurityConfigurer.withSecretKey().secretKey(secretKey) + .and().issuer(issuer).expiresIn(expiresIn); + } + + /** + * Helper method to prepend configured servlet path to the given path. + * + * Path will always be considered as relative to servlet path, even if it + * starts with a slash character. + * + * @param path + * path to be prefixed with servlet path + * @return the input path prepended by servlet path. + */ + protected String applyUrlMapping(String path) { + return requestUtil.applyUrlMapping(path); + } + + private VaadinSavedRequestAwareAuthenticationSuccessHandler getVaadinSavedRequestAwareAuthenticationSuccessHandler( + HttpSecurity http) { + VaadinSavedRequestAwareAuthenticationSuccessHandler vaadinSavedRequestAwareAuthenticationSuccessHandler = new VaadinSavedRequestAwareAuthenticationSuccessHandler(); + vaadinSavedRequestAwareAuthenticationSuccessHandler + .setDefaultTargetUrl(applyUrlMapping("")); + RequestCache requestCache = http.getSharedObject(RequestCache.class); + if (requestCache != null) { + vaadinSavedRequestAwareAuthenticationSuccessHandler + .setRequestCache(requestCache); + } + return vaadinSavedRequestAwareAuthenticationSuccessHandler; + } + + private AccessDeniedHandler createAccessDeniedHandler() { + final AccessDeniedHandler defaultHandler = new AccessDeniedHandlerImpl(); + + final AccessDeniedHandler http401UnauthorizedHandler = new Http401UnauthorizedAccessDeniedHandler(); + + final LinkedHashMap, AccessDeniedHandler> exceptionHandlers = new LinkedHashMap<>(); + exceptionHandlers.put(CsrfException.class, http401UnauthorizedHandler); + + final LinkedHashMap matcherHandlers = new LinkedHashMap<>(); + matcherHandlers.put(requestUtil::isEndpointRequest, + new DelegatingAccessDeniedHandler(exceptionHandlers, + new AccessDeniedHandlerImpl())); + + return new RequestMatcherDelegatingAccessDeniedHandler(matcherHandlers, + defaultHandler); + } + + private static class Http401UnauthorizedAccessDeniedHandler + implements AccessDeniedHandler { + @Override + public void handle(HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException accessDeniedException) + throws IOException, ServletException { + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + } + } +} diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinWebSecurityConfigurerAdapter.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinWebSecurityConfigurerAdapter.java index 1a361371059..5a654c712de 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinWebSecurityConfigurerAdapter.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinWebSecurityConfigurerAdapter.java @@ -19,7 +19,6 @@ import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; - import java.io.IOException; import java.util.LinkedHashMap; import java.util.Objects; @@ -78,8 +77,10 @@ public class MySecurityConfigurerAdapter extends VaadinWebSecurityConfigurerAdapter { } + * @deprecated Use component-based security configuration {@link VaadinWebSecurity} * */ +@Deprecated public abstract class VaadinWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { diff --git a/vaadin-spring/src/test/java/com/vaadin/flow/spring/SpringClassesSerializableTest.java b/vaadin-spring/src/test/java/com/vaadin/flow/spring/SpringClassesSerializableTest.java index 719241e2d2f..3998ec818a2 100644 --- a/vaadin-spring/src/test/java/com/vaadin/flow/spring/SpringClassesSerializableTest.java +++ b/vaadin-spring/src/test/java/com/vaadin/flow/spring/SpringClassesSerializableTest.java @@ -90,7 +90,8 @@ protected Stream getExcludedPatterns() { "com\\.vaadin\\.flow\\.spring\\.scopes\\.AbstractScope", "com\\.vaadin\\.flow\\.spring\\.scopes\\.VaadinUIScope", "com\\.vaadin\\.flow\\.spring\\.security\\.VaadinAwareSecurityContextHolderStrategy", - "com\\.vaadin\\.flow\\.spring\\.security\\.VaadinWebSecurityConfigurerAdapter", + "com\\.vaadin\\.flow\\.spring\\.security\\.VaadinWebSecurity", + "com\\.vaadin\\.flow\\.spring\\.security\\.VaadinWebSecurity\\$Http401UnauthorizedAccessDeniedHandler", "com\\.vaadin\\.flow\\.spring\\.security\\.VaadinWebSecurityConfigurerAdapter", "com\\.vaadin\\.flow\\.spring\\.security\\.VaadinWebSecurityConfigurerAdapter\\$Http401UnauthorizedAccessDeniedHandler", "com\\.vaadin\\.flow\\.spring\\.security\\.VaadinDefaultRequestCache",