View-Based Access Control

Vaadin Flow applications can use a view-based access control mechanism out of the box. If an application is based on Spring Boot, this mechanism can be enabled by using a set of annotations and with minimum Spring Security configurations. However, it is also possible to use it in Vaadin Spring applications that are not based on Spring Boot, with some extra configurations.

Introduction

You can protect restricted views in Spring-based Vaadin Flow applications in the following ways:

  • By using Spring Security’s URL pattern based HTTP security configurations

  • By adding View-Based Access Control mechanism

Navigating between routes of a Vaadin Flow application does not send traditional page requests to the server. So checking authorized access to views using only the Spring Security URL-based-filtering mechanism would either secure only the first requested route, or there would simply be no security at all for the views. This happens generally for any single-page application that tries to secure its routes using only Spring Security.

Note
Do not rely on Spring Security’s URL-based authorization in single-page applications
View-based access control is the main mechanism for restricting access to views. You should not use the traditional Spring Security URL-based HttpSecurity configurations as a mechanism for securing views at all (except for static resources, as URLs are the only way to load them).

This is where view-based access control comes in, to fully secure views in Vaadin Flow applications in a very flexible way, based on different access level annotations. The view-based access control mechanism uses the @AnonymousAllowed, @PermitAll, @RolesAllowed, and @DenyAll annotations on view classes to define the access control rules. By default any view without an annotation is secured - it acts like it would have the @DenyAll annotation in it.

Tip
The use of annotations in Vaadin Fusion
Although the same set of annotations are used to define the security access control rules on endpoints of a Vaadin Fusion application, this document does not describe how to secure client-side views. It is described in Configuring Security in Fusion documentation.

The approach of this documentation is based on Vaadin’s Spring Boot and Spring Security auto-configuration. For using view-based access control in non-Spring projects, see the documentation in the Security best practices section.

View-Based Access Control in Spring Boot-Based Applications

As mentioned in the Introduction, the mechanism is based on Spring Security and Vaadin’s Spring Boot autoconfiguration. To enable the mechanism in a Vaadin Flow Spring Boot application without any security, add the following to the project (if they do not exist already):

  • A Login view.

  • Spring Security dependencies.

  • Logout capability.

  • A security configuration class that extends VaadinWebSecurityConfigurerAdapter.

  • Annotate the view classes with the @AnonymousAllowed, @PermitAll, or @RolesAllowed annotations.

A complete example project, including the source code from this document, is available at github.com/vaadin-learning-center/crm-tutorial.

Login View

Having a login view is a basic requirement of many authentication and authorization mechanisms, in order to be able to redirect anonymous users to that page before giving access to view any protected resources.

@Route("login")
@PageTitle("Login")
public class LoginView extends VerticalLayout implements BeforeEnterObserver {

    private LoginForm login = new LoginForm();

    public LoginView() {
        addClassName("login-view");
        setSizeFull();

        setJustifyContentMode(JustifyContentMode.CENTER);
        setAlignItems(Alignment.CENTER);

        login.setAction("login");

        add(new H1("Test Application"), login);
    }

    @Override
    public void beforeEnter(BeforeEnterEvent beforeEnterEvent) {
        if(beforeEnterEvent.getLocation()
            .getQueryParameters()
            .getParameters()
            .containsKey("error")) {
            login.setError(true);
        }
    }
}
Note
Usage of Vaadin’s Login Form component
In this example, we use Vaadin’s Login Form component for the sake of brevity in this implementation. However, there is no obligation to do so; feel free to implement your own login view, as you wish.

Spring Security Dependencies

To enable Spring Security there are certain dependencies that should be added to the project. As this documentation is based on Spring Boot, we recommend adding the following dependency:

<dependencies>
    <!-- other dependencies -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <!-- other dependencies -->
</dependencies>

Logout Capability

For handling logout in web applications, one typically uses a logout button. Here is a basic implementation of logout button shown on the main layout’s header:

public class MainLayout extends AppLayout {

    private SecurityService securityService;

    public MainLayout(@Autowired SecurityService securityService) {
        this.securityService = securityService;

        H1 logo = new H1("Vaadin CRM");
        logo.addClassName("logo");
        HorizontalLayout header;
        if (securityService.getAuthenticatedUser() != null) {
            Button logout = new Button("Logout", click ->
                    securityService.logout());
            header = new HorizontalLayout(logo, logout);
        } else {
            header = new HorizontalLayout(logo);
        }

        // Other page components omitted.

        addToNavbar(header);
    }
}

The way how to get the authenticated user and how to logout the user may vary from application to application. Here is a basic example how to do that with Spring Security API:

@Component
public class SecurityService {

    private static final String LOGOUT_SUCCESS_URL = "/";

    public UserDetails getAuthenticatedUser() {
        SecurityContext context = SecurityContextHolder.getContext();
        Object principal = context.getAuthentication().getPrincipal();
        if (principal instanceof UserDetails) {
            return (UserDetails) context.getAuthentication().getPrincipal();
        }
        // Anonymous or no authentication.
        return null;
    }

    public void logout() {
        SecurityContextLogoutHandler logoutHandler = new SecurityContextLogoutHandler();
        logoutHandler.setInvalidateHttpSession(false);
        logoutHandler.logout(
                VaadinServletRequest.getCurrent().getHttpServletRequest(), null,
                null);
        UI.getCurrent().getPage().setLocation(LOGOUT_SUCCESS_URL);
    }
}

Security Configuration Class

The next step is to have a Spring Security Configuration class that extends VaadinWebSecurityConfigurerAdapter. There is no convention for naming this class, so in this documentation it is named SecurityConfiguration. However, take care with Spring Security annotations. Here is a minimal implementation of such a class:

@EnableWebSecurity 1
@Configuration
public class SecurityConfiguration
                extends VaadinWebSecurityConfigurerAdapter { 2

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // Delegating the responsibility of general configurations
        // of http security to the super class. It is configuring
        // the followings: Vaadin's CSRF protection by ignoring
        // framework's internal requests, default request cache,
        // ignoring public views annotated with @AnonymousAllowed,
        // restricting access to other views/endpoints, and enabling
        // ViewAccessChecker authorization.
        // You can add any possible extra configurations of your own
        // here (the following is just an example):

        // http.rememberMe().alwaysRemember(false);

        super.configure(http); 3

        // This is important to register your login view to the
        // view access checker mechanism:
        setLoginView(http, LoginView.class); 4
    }

    /**
     * Allows access to static resources, bypassing Spring security.
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        // Configure your static resources with public access here:
        web.ignoring().antMatchers(
                "/images/**"
        );

        // Delegating the ignoring configuration for Vaadin's
        // related static resources to the super class:
        super.configure(web); 3
    }

    /**
     * Demo UserDetailService which only provide two hardcoded
     * in memory users and their roles.
     * NOTE: This should not be used in real world applications.
     */
    @Bean
    @Override
    public UserDetailsService userDetailsService() {
        UserDetails user =
                User.withUsername("user")
                        .password("{noop}user")
                        .roles("USER")
                        .build();
        UserDetails admin =
                User.withUsername("admin")
                        .password("{noop}admin")
                        .roles("ADMIN")
                        .build();
        return new InMemoryUserDetailsManager(user, admin);
    }
}
  1. Notice the presence of @EnableWebSecurity and @Configuration annotations on top of the above class. As their names imply, they tell Spring to enable its security features.

  2. VaadinWebSecurityConfigurerAdapter is a helper class that extends Spring’s WebSecurityConfigurerAdapter and configures the common Vaadin related Spring security settings. By extending it the view-based access control mechanism is enabled automatically, and no further configurations are needed to enable it. Other benefits are covered as follows.

  3. The default implementation of the configure methods takes care of all the Vaadin-related configurations, for example ignoring static resources, or enabling CSRF checking while ignoring unnecessary checking for Vaadin internal requests, etc.

  4. The login view can be configured simply via the provided setLoginView() method.

Warning
Never use hard-coded credentials in production
The implementation of userDetailsService() method is obviously just an in-memory implementation for the sake of brevity in this documentation. In a real-world application, you can change the Spring Security configuration to use an authentication provider for LDAP, JAAS, and other real-world sources. Read more about Spring Security authentication providers.

The most important configuration in the above example is the call to the setLoginView(http, LoginView.class); inside the first configure method. This is how the view-based access control mechanism knows where to redirect the users when they attempt to navigate to a protected view.

Now that the LoginView is ready, and it is set as the login view in the security configuration, it is time to move forward and see how the security annotations work on the views.

Annotating the View Classes

Before we provide some usage examples of the access annotations, it would be useful to have a closer look at the annotations, and their meaning when applied to a view:

  • @AnonymousAllowed permits anyone to navigate to the view without any authentication or authorization.

  • @PermitAll allows any authenticated user to navigate to the view.

  • @RolesAllowed grants access to users having the roles specified in the annotation value.

  • @DenyAll disallows everyone from navigating to the view. This is the default, which means that, if a view is not annotated at all, the @DenyAll logic is applied.

Note that when the security configuration class extends from VaadinWebSecurityConfigurerAdapter, Vaadin’s SpringSecurityAutoConfiguration comes into play and enables the view-based access control mechanism. Therefore, none of the views are accessible until one of the above annotations (except @DenyAll) is applied to them.

Some examples:

@Route(value = "", layout = MainView.class)
@PageTitle("Public View")
@AnonymousAllowed
public class PublicView extends VerticalLayout {
    // ...
}
@Route(value = "private", layout = MainView.class)
@PageTitle("Private View")
@PermitAll
public class PrivateView extends VerticalLayout {
    // ...
}
@Route(value = "admin", layout = MainView.class)
@PageTitle("Admin View")
@RolesAllowed("ROLE_ADMIN") // <- Should match one of the user's roles (case-sensitive)
public class AdminView extends VerticalLayout {
    // ...
}
@RolesAllowed("ROLE_ADMIN")
public abstract class AbstractAdminView extends VerticalLayout {
    // ...
}

@Route(value = "user-listing", layout = MainView.class)
@PageTitle("User Listing")
public class UserListingView extends AbstractAdminView {
    // ...
}

Like shown by the last example above, the security annotations are inherited from the closest parent class that has them. Annotating a child class will override any inherited annotations. Interfaces are not checked for annotations, but only classes. By design, the annotations are not read from parent layouts or "parent views", as this would make things unnecessarily complex to determine which security level should be applied. If multiple annotations are specified on a single view class, the following rules are applied:

  • DenyAll overrides other annotations

  • AnonymousAllowed overrides RolesAllowed and PermitAll

  • RolesAllowed overrides PermitAll

However, specifying more than one of the above access annotations on a view class is not recommended, as it is confusing and there is probably no logical reason to do so.

Limitations

Mixing any of the view access annotations with Spring’s URL-based HTTP security (which probably exist in older Vaadin Spring Boot applications) may result in unwanted access configurations or unnecessary complications.

Important
Do not mix Spring’s URL-based HTTP security and view-based access control on a single view
Vaadin strongly recommends not mixing Spring’s URL-pattern-based HTTP security and this view-based access control mechanism targeting the same views, since it may lead to unwanted access configurations, or at the very least an unnecessary complication in the authorization of views.