Adding a login screen to a Vaadin 14 app with Spring Security
Tags: Java, Spring Framework, Spring Boot, Vaadin, Web Development

Please click the link for the video version of the topic: https://youtu.be/xd-7Ak5dTTY

Creating a login view

Start by creating a new package com.vaadin.tutorial.crm.ui.view.login . Create a new class, LoginView , in the new package. LoginView.java

package com.vaadin.tutorial.crm.ui.view.login;



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 java.util.Collections;



@Route("login") (1)

@PageTitle("Login | Vaadin CRM")



public class LoginView extends VerticalLayout implements BeforeEnterObserver {



private LoginForm login = new LoginForm(); (2)



public LoginView(){

addClassName("login-view");

setSizeFull();

setAlignItems(Alignment.CENTER); (3)

setJustifyContentMode(JustifyContentMode.CENTER);



login.setAction("login"); (4)



add(new H1("Vaadin CRM"), login);

}



@Override

public void beforeEnter(BeforeEnterEvent beforeEnterEvent) {

// inform the user about an authentication error

if(beforeEnterEvent.getLocation() (5)

.getQueryParameters()

.getParameters()

.containsKey("error")) {

login.setError(true);

}

}

}

Maps the view to the "login" path. LoginView should take up the whole browser window, so we don’t use MainLayout as the parent. Instantiates a LoginForm component to capture username and password. Makes LoginView full size and centers its content both horizontally and vertically, by calling setAlignItems(Alignment.CENTER) and setJustifyContentMode(JustifyContentMode.CENTER). Sets the LoginForm action to "login" to post the login form to Spring Security. Reads query parameters and shows an error if a login attempt fails. Build the application and navigate to http://localhost/login. You should see a centered login form.

Setting up Spring Security to handle logins

With the login screen in place, we now need to configure Spring Security to perform the authentication and to prevent unauthorized users from accessing views.

Installing Spring Security dependencies

Start by adding the following 2 dependencies in your pom.xml : pom.xml

<dependency>

<groupId>org.springframework.security</groupId>

<artifactId>spring-security-web</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.security</groupId>

<artifactId>spring-security-config</artifactId>

</dependency>

Check that the dependencies are downloaded. If you enabled automatic downloads in an earlier tutorial, you’re all set. If you didn’t, or are unsure, run mvn install from the command line to download the dependencies. Next, disable Spring MVC auto configuration on the Application class, as this interferes with how Vaadin works and can cause strange reloading behavior. Application.class

@SpringBootApplication(exclude = ErrorMvcAutoConfiguration.class)

public class Application extends SpringBootServletInitializer {

...

}

Configuring Spring Security

Create a new package com.vaadin.tutorial.crm.security for classes related to security. In the new package create the following classes using the code detailed below:

SecurityUtils : Utility methods.

: Utility methods. CustomRequestCache : A cache to keep track of unauthenticated requests.

: A cache to keep track of unauthenticated requests. SecurityConfiguration : Spring Security configuration.

: Spring Security configuration. Paste the class code into the package and IntelliJ will automatically create the class for you.

SecurityUtils SecurityUtils.java

package com.vaadin.tutorial.crm.security; import com.vaadin.flow.server.ServletHelper;

import com.vaadin.flow.shared.ApplicationConstants;

import org.springframework.security.authentication.AnonymousAuthenticationToken;

import org.springframework.security.core.Authentication;

import org.springframework.security.core.context.SecurityContextHolder; import javax.servlet.http.HttpServletRequest;

import java.util.stream.Stream; public final class SecurityUtils { private SecurityUtils() {

// Util methods only

} static boolean isFrameworkInternalRequest(HttpServletRequest request) {

final String parameterValue = request.getParameter(ApplicationConstants.REQUEST_TYPE_PARAMETER);

return parameterValue != null

&& Stream.of(ServletHelper.RequestType.values())

.anyMatch(r -> r.getIdentifier().equals(parameterValue));

} static boolean isUserLoggedIn() {

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

return authentication != null

&& !(authentication instanceof AnonymousAuthenticationToken)

&& authentication.isAuthenticated();

}

}

isFrameworkInternalRequest determines if a request is internal to Vaadin. isUserLoggedIn checks if the current user is logged in. CustomRequestCache CustomRequestCache.java

package com.vaadin.tutorial.crm.security; import org.springframework.security.web.savedrequest.HttpSessionRequestCache; import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse; class CustomRequestCache extends HttpSessionRequestCache { @Override

public void saveRequest(HttpServletRequest request, HttpServletResponse response) {

if (!SecurityUtils.isFrameworkInternalRequest(request)) {

super.saveRequest(request, response);

}

} public void saveRequest(HttpServletRequest request, HttpServletResponse response) {if (!SecurityUtils.isFrameworkInternalRequest(request)) {super.saveRequest(request, response); }

Saves unauthenticated requests so we can redirect the user to the page they were trying to access once they’re logged in. SecurityConfiguration SecurityConfiguration.java

package com.vaadin.tutorial.crm.security; 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.builders.WebSecurity;

import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;

import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

import org.springframework.security.core.userdetails.User;

import org.springframework.security.core.userdetails.UserDetails;

import org.springframework.security.core.userdetails.UserDetailsService;

import org.springframework.security.provisioning.InMemoryUserDetailsManager; @EnableWebSecurity

@Configuration

public class SecurityConfiguration extends WebSecurityConfigurerAdapter { public class SecurityConfiguration extends WebSecurityConfigurerAdapter { private static final String LOGIN_PROCESSING_URL = "/login";

private static final String LOGIN_FAILURE_URL = "/login?error";

private static final String LOGIN_URL = "/login";

private static final String LOGOUT_SUCCESS_URL = "/login"; }

@EnableWebSecurity turns on Spring Security for the application. @Configuration tells Spring Boot to use this class for configuring security. Add a method to block unauthenticated requests to all pages, except the login page. SecurityConfiguration.java

@Override

protected void configure(HttpSecurity http) throws Exception {

http.csrf().disable()

.requestCache().requestCache(new CustomRequestCache())

.and().authorizeRequests()

.requestMatchers(SecurityUtils::isFrameworkInternalRequest).permitAll() protected void configure(HttpSecurity http) throws Exception {http.csrf().disable().requestCache().requestCache(new CustomRequestCache()).and().authorizeRequests().requestMatchers(SecurityUtils::isFrameworkInternalRequest).permitAll() .anyRequest().authenticated() .and().formLogin()

.loginPage(LOGIN_URL).permitAll()

.loginProcessingUrl(LOGIN_PROCESSING_URL)

.failureUrl(LOGIN_FAILURE_URL)

.and().logout().logoutSuccessUrl(LOGOUT_SUCCESS_URL);

}

Disables cross-site request forgery (CSRF) protection, as Vaadin already has CSRF protection. Uses CustomRequestCache to track unauthorized requests so that users are redirected appropriately after login. Turns on authorization. Allows all internal traffic from the Vaadin framework. Allows all authenticated traffic. Enables form-based login and permits unauthenticated access to it. Configures the login page URLs. Configures the logout URL. Add another method to configure test users. SecurityConfiguration.java

@Bean

@Override

public UserDetailsService userDetailsService() {

UserDetails user =

User.withUsername("user")

.password("{noop}password")

.roles("USER")

.build(); public UserDetailsService userDetailsService() {UserDetails user =User.withUsername("user").password("{noop}password").roles("USER").build(); return new InMemoryUserDetailsManager(user);

}

Defines a single user with the username "user" and password "password" in an in-memory DetailsManager .

and password in an in-memory . We do not recommend that you configure users directly in the code for applications in production. You can easily change this Spring Security configuration to use an authentication provider for LDAP, JAAS, and other real world sources. Read more about Spring Security authentication providers.

Finally, exclude Vaadin-framework communication and static assets from Spring Security. SecuirtyConfiguration.java

@Override

public void configure(WebSecurity web) {

web.ignoring().antMatchers(

"/VAADIN/**",

"/favicon.ico",

"/robots.txt",

"/manifest.webmanifest",

"/sw.js",

"/offline.html",

"/icons/**",

"/images/**",

"/styles/**",

"/h2-console/**");

} public void configure(WebSecurity web) {web.ignoring().antMatchers("/VAADIN/**","/favicon.ico","/robots.txt","/manifest.webmanifest","/sw.js","/offline.html","/icons/**","/images/**","/styles/**","/h2-console/**");

Restricting access to Vaadin views

Spring Security restricts access to content based on paths. Vaadin applications are single-page applications. This means that they do not trigger a full browser refresh when you navigate between views, even though the path does change.To secure a Vaadin application, we need to wire Spring Security to the Vaadin navigation system.

To do this, create a new class in the security package, ConfigureUIServiceInitListener .

ConfigureUIServiceInitListener.java

package com.vaadin.tutorial.crm.security; import com.vaadin.flow.component.UI;

import com.vaadin.flow.router.BeforeEnterEvent;

import com.vaadin.flow.server.ServiceInitEvent;

import com.vaadin.flow.server.VaadinServiceInitListener;

import com.vaadin.tutorial.crm.ui.view.login.LoginView;

import org.springframework.stereotype.Component; @Component (1)

public class ConfigureUIServiceInitListener implements VaadinServiceInitListener { @Override

public void serviceInit(ServiceInitEvent event) {

event.getSource().addUIInitListener(uiEvent -> { (2)

final UI ui = uiEvent.getUI();

ui.addBeforeEnterListener(this::authenticateNavigation);

});

} private void authenticateNavigation(BeforeEnterEvent event) {

if (!LoginView.class.equals(event.getNavigationTarget())

&& !SecurityUtils.isUserLoggedIn()) { (3)

event.rerouteTo(LoginView.class);

}

}

}

The @Component annotation registers the listener. Vaadin will pick it up on startup. In serviceInit , we listen for the initialization of the UI (the internal root component in Vaadin) and then add a listener before every view transition. In authenticateNavigation, we reroute all requests to the login, if the user is not logged in

You can read more about fine-grained access control in the Spring Security tutorial series.

Adding a logout link

You can now log in to the application. The final thing we need to do is add a logout link to the application header.

In MainLayout , add a link to the header: MainLayout.java

private void createHeader() {

H1 logo = new H1("Vaadin CRM");

logo.addClassName("logo"); Anchor logout = new Anchor("logout", "Log out"); HorizontalLayout header = new HorizontalLayout(new DrawerToggle(), logo, logout);

header.expand(logo);

header.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER);

header.setWidth("100%");

header.addClassName("header"); addToNavbar(header);

}