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
+ * 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
+ * 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