Security
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
}
}
}
-
The login view is available at
/login
. -
Once security is enabled, access to all views is denied by default. However, the login view must be accessible to anonymous users.
-
LoginForm
is a built-in Vaadin component that works nicely with Spring Security. -
VerticalLayout
is actually a flex layout. To center the login form on the screen, some flexbox configuration is needed. -
When the user clicks the login button, a
POST
request will be submitted to/login
. -
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);
}
}
-
@EnableWebSecurity
instructs Spring to use this class when configuring Spring Security. -
You’ll use Jakarta Annotations (JSR-250) to secure
ChatService
. -
Here you’re extending
VaadinWebSecurity
, which does most of the work. -
Always call
super.configure(http)
first to apply the default configuration, before making any customizations. -
Spring Security will now use your
LoginView
when asking users to authenticate themselves. -
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")
@PermitAll
public class LobbyView extends VerticalLayout {
//...
}
@Route(value = "channel", layout = MainLayout.class)
@PermitAll
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
@RolesAllowed(Roles.USER) 1
public class ChatService {
// ...
}
-
@RolesAllowed
is a JSR-250 annotation that you enabled inSecurityConfig
.
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:
@RolesAllowed(Roles.ADMIN)
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<>();
channels.setRenderer(new ComponentRenderer<>(this::createChannelComponent));
add(channels);
expand(channels);
channelNameField = new TextField();
channelNameField.setPlaceholder("New channel name");
addChannelButton = new Button("Add channel", event -> addChannel());
addChannelButton.setDisableOnClick(true);
if (authenticationContext.hasRole(Roles.ADMIN)) { 2
var toolbar = new HorizontalLayout(channelNameField,
addChannelButton);
toolbar.setWidthFull();
toolbar.expand(channelNameField);
add(toolbar);
}
}
-
Use constructor injection to inject an instance of
AuthenticationContext
. -
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);
var logout = new Button("Logout " + authenticationContext.getPrincipalName().orElse(""), 1
event -> authenticationContext.logout()); 2
var header = new Header(toggle, viewTitle, logout); 3
header.addClassNames(LumoUtility.AlignItems.CENTER, LumoUtility.Display.FLEX,
LumoUtility.Padding.End.MEDIUM, LumoUtility.Width.FULL);
addToNavbar(false, header);
}
-
AuthenticationContext
can be used to get the name of the current user, not just the roles. -
AuthenticationContext
has a method for logging out. -
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 {
private final AuthenticationContext authenticationContext;
// ...
public MainLayout(AuthenticationContext authenticationContext) {
this.authenticationContext = authenticationContext;
// ...
}
// ...
}
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();
}
var author = SecurityContextHolder.getContext().getAuthentication().getName(); 1
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);
}
}
-
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".