SupportWriter space ↗
← Back to the journal
Vaadin

Securing Vaadin applications with Spring Security

Add login, logout, and role-based access control to a Vaadin Flow application using Spring Security, view access annotations, and AuthenticationContext.

DPutu Adi Guna Permana · 08 Oct 2026 · 6 min read

Security in a server-side UI

Because Vaadin Flow runs your UI logic on the server, users cannot bypass a hidden button by editing JavaScript: if a view or component is not created on the server, it simply does not exist for that user. That is a strong foundation, but you still need to answer the classic questions: who is the user, and which views and actions may they access?

Vaadin integrates with Spring Security to handle authentication and to protect views with simple annotations. The examples target Vaadin 24 with Spring Boot 3. Newer Vaadin versions introduce an updated configuration API; if a class used below is marked as deprecated in your version, follow the migration notes in the Vaadin documentation. The concepts stay the same.

Dependencies

Add Spring Security to a Vaadin Spring Boot project:

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

The security configuration

Vaadin provides a base class that configures Spring Security correctly for Vaadin's internal requests, static resources, and CSRF handling:

import com.vaadin.flow.spring.security.VaadinWebSecurity;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@EnableWebSecurity
@Configuration
public class SecurityConfig extends VaadinWebSecurity {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(auth -> auth
                .requestMatchers("/images/**", "/public/**").permitAll());

        super.configure(http);

        setLoginView(http, LoginView.class);
    }

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

The order matters: add your own rules for public resources first, then call super.configure(http), which secures everything else, and finally register the login view.

Where users come from

Spring Security needs a UserDetailsService. In a real application it loads users from your database:

@Service
class DatabaseUserDetailsService implements UserDetailsService {

    private final AppUserRepository users;

    DatabaseUserDetailsService(AppUserRepository users) {
        this.users = users;
    }

    @Override
    public UserDetails loadUserByUsername(String username) {
        AppUser user = users.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("Unknown user"));

        return User.withUsername(user.getUsername())
                .password(user.getPasswordHash())
                .roles(user.getRoles().toArray(String[]::new))
                .disabled(!user.isActive())
                .build();
    }
}

Store only password hashes produced by the PasswordEncoder, never plain text passwords. If your organization already has an identity provider such as Keycloak, Microsoft Entra ID, or Google Workspace, you can use Spring Security's OAuth2 login instead and skip password handling entirely.

The login view

Vaadin includes a ready-made LoginForm component that posts to Spring Security's login processing URL:

@Route("login")
@PageTitle("Sign in")
@AnonymousAllowed
public class LoginView extends VerticalLayout implements BeforeEnterObserver {

    private final LoginForm login = new LoginForm();

    public LoginView() {
        setSizeFull();
        setAlignItems(Alignment.CENTER);
        setJustifyContentMode(JustifyContentMode.CENTER);

        login.setAction("login");
        login.setForgotPasswordButtonVisible(false);

        add(new H1("Shop Admin"), login);
    }

    @Override
    public void beforeEnter(BeforeEnterEvent event) {
        boolean failed = event.getLocation().getQueryParameters()
                .getParameters().containsKey("error");
        login.setError(failed);
    }
}

After a failed attempt, Spring Security redirects to /login?error, and the form shows an error message. LoginOverlay is an alternative that displays the same form as a full-screen overlay with a title and description.

Protecting views with annotations

Once security is enabled, every view is denied by default. You must explicitly declare who may access each one:

Annotation Who can access
@AnonymousAllowed everyone, including users who are not logged in
@PermitAll any authenticated user
@RolesAllowed("ADMIN") only users with the listed roles
@DenyAll nobody (the default for unannotated views)
@Route(value = "", layout = MainLayout.class)
@PermitAll
public class DashboardView extends VerticalLayout { }

@Route(value = "users", layout = MainLayout.class)
@RolesAllowed("ADMIN")
public class UserManagementView extends VerticalLayout { }

The "deny by default" behavior is a safe design: forgetting an annotation hides a view instead of exposing it. Unauthenticated users who open a protected view are redirected to the login view.

Annotations on layouts work too, but remember that a view needs access to both its own route and its parent layout.

The current user and logout

Inject AuthenticationContext to read the logged-in user and to log out:

public class MainLayout extends AppLayout {

    public MainLayout(AuthenticationContext authContext) {
        var nav = new SideNav();
        nav.addItem(new SideNavItem("Dashboard", DashboardView.class));

        if (authContext.hasRole("ADMIN")) {
            nav.addItem(new SideNavItem("Users", UserManagementView.class));
        }

        var header = new HorizontalLayout(new DrawerToggle(), new H1("Shop Admin"));
        authContext.getAuthenticatedUser(UserDetails.class).ifPresent(user -> {
            var logout = new Button("Log out " + user.getUsername(), e -> authContext.logout());
            header.add(logout);
        });

        addToNavbar(header);
        addToDrawer(nav);
    }
}

Hiding the "Users" menu item is about user experience. The real protection is the @RolesAllowed("ADMIN") annotation on the view, which applies even if someone types the URL directly.

Protecting actions and services

Views are not the only entry points. A button that deletes data should be safe even if it is rendered in a view many roles can see. Protect service methods with method security:

@Configuration
@EnableMethodSecurity(jsr250Enabled = true)
class MethodSecurityConfig { }

@Service
public class OrderService {

    @RolesAllowed("ADMIN")
    public void deleteOrder(long id) { /* ... */ }

    @PreAuthorize("hasRole('ADMIN') or #order.createdBy == authentication.name")
    public void cancel(Order order) { /* ... */ }
}

If an unauthorized user triggers the method, Spring throws an AccessDeniedException. Defending the service layer protects you from mistakes in the UI and keeps the rules consistent if the same services are later exposed through a REST API.

Session and production tips

  • Use HTTPS everywhere. Session cookies must never travel over plain HTTP.
  • Configure session timeouts with server.servlet.session.timeout to match your security requirements.
  • Add remember-me carefully, only if users really need it, and with a persistent token store.
  • Audit sensitive actions such as role changes and deletions, including who did them and when.
  • Test access rules. Write tests that verify an ordinary user cannot reach admin views or call admin services.

Exercise

Create three roles: USER, MANAGER, and ADMIN. Give everyone access to the dashboard, managers access to a reports view, and admins access to user management. Show only the permitted menu items, display the user's name next to a logout button, and protect a deleteUser service method so that only admins can call it.

← Explore more notes