Documentation

Documentation versions (currently viewingVaadin 24.4 (pre))

Security

Learn how to secure a Flow application.

The application you’re developing in this tutorial is fairly open to users: any user can post messages and create new channels. All of the messages have also been sent by the author "John Doe".

In this part, you’ll secure the application so that only authenticated users are permitted to access the application and only administrators are permitted to create new chat channels. Additionally, you’ll set it so that the usernames are used as the message author rather than the name, "John Doe".

Necessary Dependencies

The chat application is a Spring Boot application. Therefore, use Spring Security to secure it. Start by adding this dependency to your pom.xml file:

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

Add Login Screen

To restrict access, add a login screen. Create a class named, LoginView in the com.example.application.views.login package, like this:

package com.example.application.views.login;

import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.H1;
import com.vaadin.flow.component.login.LoginForm;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.BeforeEnterEvent;
import com.vaadin.flow.router.BeforeEnterObserver;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.server.auth.AnonymousAllowed;

@Route("login") // (1)
@PageTitle("Chat Login")
@AnonymousAllowed // (2)
public class LoginView extends VerticalLayout implements BeforeEnterObserver {

    private final LoginForm loginForm;

    public LoginView() {
        loginForm = new LoginForm(); // (3)
        setSizeFull();
        setAlignItems(Alignment.CENTER); // (4)
        setJustifyContentMode(JustifyContentMode.CENTER);

        loginForm.setAction("login"); // (5)

        add(new H1("Vaadin Chat"), new Div("You can log in as 'alice', 'bob' or 'admin'. The password for all of them is 'password'."), loginForm);
    }

    @Override
    public void beforeEnter(BeforeEnterEvent event) {
        if (event.getLocation().getQueryParameters().getParameters().containsKey("error")) {
            loginForm.setError(true); // (6)
        }
    }
}
  1. The login view is available at /login.

  2. Once security is enabled, access to all views is denied by default. However, the login view must be accessible to anonymous users.

  3. LoginForm is a built-in Vaadin component that works nicely with Spring Security.

  4. VerticalLayout is actually a flex layout. To center the login form on the screen, some flexbox configuration is needed.

  5. When the user clicks the login button, a POST request will be submitted to /login.

  6. If there’s a query parameter called, "error" (e.g., /login?error), the login form will show an error message.

Define Roles

The application should have two user roles: USER for ordinary users, who are allowed to post and receive messages; and ADMIN for administrators, who are allowed to create new channels.

Even though roles are just strings, it’s good practice to declare them as constants. Create a new class named, Roles in the com.example.application.security package, like this:

package com.example.application.security;

public final class Roles {

    private Roles() {
    }

    public static final String USER = "USER";
    public static final String ADMIN = "ADMIN";
}

Add Security Configuration

With the login view and roles in place, you now have to configure Spring Security to use it. You also have to configure Spring Security to protect your views and your application services. Fortunately, Vaadin has a base class (i.e., VaadinWebSecurity) that makes this easy.

Create a class named, SecurityConfig in the com.example.application.security package, like this:

package com.example.application.security;

import com.example.application.views.login.LoginView;
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.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@EnableWebSecurity // (1)
@EnableMethodSecurity(jsr250Enabled = true) // (2)
@Configuration
class SecurityConfig extends VaadinWebSecurity { // (3)

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http); // (4)
        setLoginView(http, LoginView.class); // (5)
    }

    @Bean
    public UserDetailsService users() { // (6)
        var alice = User.builder()
                .username("alice")
                // password = password with this hash, don't tell anybody :-)
                .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
                .roles(Roles.USER)
                .build();
        var bob = User.builder()
                .username("bob")
                // password = password with this hash, don't tell anybody :-)
                .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
                .roles(Roles.USER)
                .build();
        var admin = User.builder()
                .username("admin")
                // password = password with this hash, don't tell anybody :-)
                .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
                .roles(Roles.ADMIN, Roles.USER)
                .build();
        return new InMemoryUserDetailsManager(alice, bob, admin);
    }
}
  1. @EnableWebSecurity instructs Spring to use this class when configuring Spring Security.

  2. You’ll use Jakarta Annotations (JSR-250) to secure ChatService.

  3. Here you’re extending VaadinWebSecurity, which does most of the work.

  4. Always call super.configure(http) first to apply the default configuration, before making any customizations.

  5. Spring Security will now use your LoginView when asking users to authenticate themselves.

  6. In this example, you’re using an InMemoryUserDetailsManager — which is not recommended in real-world applications.

You can find more information about securing Vaadin applications on the Security page of the Flow documentation.

Grant Access to Views

By default, Vaadin will deny access to all views unless instructed otherwise. You’ve already granted anonymous users access to the login view. You should now grant all authenticated users access to the LobbyView and ChannelView. Do this by adding the @PermitAll annotation to both classes, like this:

@Route(value = "", layout = MainLayout.class)
@PageTitle("Lobby")
// tag::snippet[]
@PermitAll
// end::snippet[]
public class LobbyView extends VerticalLayout {
    //...
}

@Route(value = "channel", layout = MainLayout.class)
// tag::snippet[]
@PermitAll
// end::snippet[]
public class ChannelView extends VerticalLayout implements HasUrlParameter<String>, HasDynamicTitle {
    //...
}

Secure Application Layer

By default, Spring Security will grant access to all application services unless told otherwise. You should now protect ChatService so that only users with the USER role can invoke it. Do this by adding the @RolesAllowed annotation to the class like this:

@Service
// tag::snippet[]
@RolesAllowed(Roles.USER) // (1)
// end::snippet[]
public class ChatService {
    // ...
}
  1. @RolesAllowed is a JSR-250 annotation that you enabled in SecurityConfig.

As mentioned earlier, you only want users with the ADMIN role to be able to invoke the createChannel() method. To set this restriction, add the @RolesAllowed annotation to the method like this:

// tag::snippet[]
@RolesAllowed(Roles.ADMIN)
// end::snippet[]
public Channel createChannel(String name) {
        // ...
}

The @RolesAllowed annotation on the method will take precedense over an annotation on the class.

Hide Channel Creation

At this point, the application will still show the channel creation components to all users. However, if an ordinary user tries to create a channel, they would get an AccessDeniedException. Even though the application is secure, this kind of user experience is undesirable.

It’s a good practice to show only actions that the user is allowed to perform. In this case, the text field and button for creating new channels should only be visible to users that hold the ADMIN role. Vaadin provides a class called, AuthenticationContext. You can add it to your views to use for this purpose.

Change the constructor of LobbyView as follows:

public LobbyView(ChatService chatService, AuthenticationContext authenticationContext) { // (1)
    this.chatService = chatService;
    setSizeFull();

    channels = new VirtualList<>();
    add(channels);
    expand(channels);

    channelNameField = new TextField();
    channelNameField.setPlaceholder("New channel name");

    addChannelButton = new Button("Add channel", event -> addChannel());
    addChannelButton.setDisableOnClick(true);

// tag::snippet[]
    if (authenticationContext.hasRole(Roles.ADMIN)) { // (2)
        var toolbar = new HorizontalLayout(channelNameField,
                addChannelButton);
        toolbar.setWidthFull();
        toolbar.expand(channelNameField);
        add(toolbar);
    }
// end::snippet[]
}
  1. Use constructor injection to inject an instance of AuthenticationContext.

  2. Only show the toolbar if the current user has the ADMIN role.

Add Logout Button

When securing a web application, much focus is often put on the login functionality. However, it is just as important to implement the logout functionality, properly. Otherwise, another user who later uses the same device get unintended access to the application.

For a better user experience and better security, add a logout button to the navbar of the main layout. Open MainLayout and change the addNavbarContent() method as follows:

private void addNavbarContent() {
    var toggle = new DrawerToggle();
    toggle.setAriaLabel("Menu toggle");
    toggle.setTooltipText("Menu toggle");

    viewTitle = new H2();
    viewTitle.addClassNames(LumoUtility.FontSize.LARGE, LumoUtility.Margin.NONE,
        LumoUtility.Flex.GROW);

// tag::snippet[]
    var logout = new Button("Logout " + authenticationContext.getPrincipalName().orElse(""), // (1)
        event -> authenticationContext.logout()); // (2)

    var header = new Header(toggle, viewTitle, logout); // (3)
// end::snippet[]
    header.addClassNames(LumoUtility.AlignItems.CENTER, LumoUtility.Display.FLEX,
        LumoUtility.Padding.End.MEDIUM, LumoUtility.Width.FULL);

    addToNavbar(false, header);
}
  1. AuthenticationContext can be used to get the name of the current user, not just the roles.

  2. AuthenticationContext has a method for logging out.

  3. Remember to add the logout button to the header.

If you now try to compile the code, you’ll get an error because authenticationContext isn’t defined yet. Since the navbar is configured inside its own private method and not inside the constructor, you have to store a reference to AuthenticationContext in a private field like this:

public class MainLayout extends AppLayout {
// tag::snippet[]
    private final AuthenticationContext authenticationContext;
// end::snippet[]
    // ...

// tag::snippet[]
    public MainLayout(AuthenticationContext authenticationContext) {
        this.authenticationContext = authenticationContext;
// end::snippet[]
        // ...
    }
    // ...
}

After making that change, the code should now compile.

User’s Name as Message Author

There remains one task for this part of the tutorial: replace "John Doe" as the author name with the user’s actual username. Since you’re using Spring Security, you can get this name from the current SecurityContext, which in turn can be retrieved from SecurityContextHolder.

Open ChatService and change the postMessage() method as follows:

public void postMessage(String channelId, String message) throws InvalidChannelException {
    if (!channelRepository.exists(channelId)) {
        throw new InvalidChannelException();
    }
// tag::snippet[]
    var author = SecurityContextHolder.getContext().getAuthentication().getName(); // (1)
// end::snippet[]
    var msg = messageRepository.save(new NewMessage(channelId, clock.instant(), author, message));
    var result = sink.tryEmitNext(msg);
    if (result.isFailure()) {
        log.error("Error posting message to channel {}: {}", channelId, result);
    }
}
  1. Retrieve the current user’s name. Since this method is protected by @RolesAllowed, the security context is guaranteed always to contain a valid authentication token.

Try It!

Your application is now ready for you to try the new security features. Open your browser at http://localhost:8080/ (start the application if it is not already running). You should be redirected to the login screen. Log in with the username "admin" and password "password". When you do so, you should be taken to the lobby screen.

As admin, try to create a new channel. This should work as before. Go to the new channel and send a message. The message author should display as "admin".

Logout as admin by clicking the Logout button. You should be back at the login screen. Login with the username "bob" and the password "password". You should be taken to the lobby screen. The components for creating new channels should not be visible.

Still logged in as bob, go to the channel you created as admin. You should see the message sent by admin. Send another message. The author should show up as "bob".