Paul Römer

Adding a form-based login view to a Vaadin application with Spring Security

Right now, our App is super secure. Too secure, in fact as you are not able to log in at all. Time to add our first, form-based, login views.

Even if form based login views feel kind of old-school nowadays, they have several advantages:

  • nearly all security frameworks support them,

  • ability to run frontend and backend on separate nodes,

  • they are a well-defined interface that allows us to change the underlying framework without (or with little) changes to the UI and

  • in general, you do not have to touch as much security-related code as you would have to with other approaches (less code, fewer bugs).

The big disadvantage is the need for a page reload during the login which breaks the UX in modern Single Page Applications.

Tip
With Vaadin 13 we introduce a dedicated login component boiling down the needed code to less than 7 lines. Check Alternative 3: Java-only with LoginForm or LoginOverlay component.

Alternative 1: Java-only

As promised we will start with implementing a Java-only login view. Yes, I know. We cannot avoid all JavaScript, but we reduced it as much as possible.

First of all, we have to add iron-form that allows us to use HTML forms with custom elements. Just add the needed WEBJAR to your dependencies:

<dependency>
    <groupId>org.webjars.bowergithub.polymerelements</groupId>
    <artifactId>iron-form</artifactId>
    <version>2.4.0</version>
</dependency>

Second, we add a vertically layouted login view with all needed fields, forms and buttons:

LoginView.java
@Route(value = LoginView.ROUTE)
@PageTitle("Login")
@HtmlImport("frontend://bower_components/iron-form/iron-form.html") // (1)
public class LoginView extends VerticalLayout {
    public static final String ROUTE = "login";

    public LoginView() {
        TextField userNameTextField = new TextField();
        userNameTextField.getElement().setAttribute("name", "username"); // (2)
        PasswordField passwordField = new PasswordField();
        passwordField.getElement().setAttribute("name", "password"); // (3)
        Button submitButton = new Button("Login");
        submitButton.setId("submitbutton"); // (4)
        UI.getCurrent().getPage().executeJavaScript("document.getElementById('submitbutton').addEventListener('click', () => document.getElementById('ironform').submit());"); // (5)

        FormLayout formLayout = new FormLayout(); // (6)
        formLayout.add(userNameTextField, passwordField, submitButton);

        Element formElement = new Element("form"); // (7)
        formElement.setAttribute("method", "post");
        formElement.setAttribute("action", "login");
        formElement.appendChild(formLayout.getElement());

        Element ironForm = new Element("iron-form"); // (8)
        ironForm.setAttribute("id", "ironform");
        ironForm.setAttribute("allow-redirect", true); // (9)
        ironForm.appendChild(formElement);

        getElement().appendChild(ironForm); // (10)

        setClassName("login-view");
    }
}
  1. Tells Flow that we need iron forms for the frontend.

  2. Adds a Vaadin text field for username and sets the form name attribute.

  3. Same as above but for passwords using Vaadin password field.

  4. Creates a Vaadin button and makes it referencable by defining an id.

  5. Yeah, I know. JS. However, we somehow have to register the click event with the submit() method of the iron-form.

  6. Adds a Vaadin form layout and adds all configured components.

  7. Creates a native form element, defines method and action of the form and appends the Vaadin form layout.

  8. Creates an iron-form that encapsulates the form element. Only that allows the use of Vaadin components (buttons and fields) in forms.

  9. Tells iron-form that we want to handle the redirect in the response.

  10. Finally, attaches everything to the UI.

Alternative 2: Declarative view with a Polymer template

If you prefer, you could also implement the login-form with a Polymer template.

As in the Java-only example, the iron-form component is needed to allow HTML forms with custom elements. Just add the needed WEBJAR to your dependencies:

<dependency>
    <groupId>org.webjars.bowergithub.polymerelements</groupId>
    <artifactId>iron-form</artifactId>
    <version>2.4.0</version>
</dependency>

The Java companion file defines the route and references the Polymer template:

@Tag("sa-login-view")
@HtmlImport("frontend://src/views/sa-login-view.html")
@Route(value = LoginView.ROUTE)
@PageTitle("Login")
public class LoginView extends PolymerTemplate<TemplateModel> {
    public static final String ROUTE = "login";
}

And finally the actual declaration of our login Polymer component:

<link rel="import" href="../../bower_components/polymer/polymer-element.html">
<link rel="import" href="../../bower_components/polymer/lib/mixins/gesture-event-listeners.html">
<link rel="import" href="../../bower_components/iron-form/iron-form.html">
<link rel="import" href="../../bower_components/vaadin-button/vaadin-button.html">
<link rel="import" href="../../bower_components/vaadin-text-field/vaadin-text-field.html">
<link rel="import" href="../../bower_components/vaadin-text-field/vaadin-password-field.html">
<link rel="import" href="../../bower_components/vaadin-ordered-layout/vaadin-vertical-layout.html">

<dom-module id="sa-login-view">
    <template>
        <div class="container">
            <iron-form class="login" id="form" allow-redirect> <!-- (1) -->
                <form method="post" action="login"> <!-- (2) -->
                    <vaadin-vertical-layout>
                        <vaadin-text-field id="username" name="username" autofocus required></vaadin-text-field>
                        <vaadin-password-field id="password" name="password" required></vaadin-password-field>
                        <vaadin-button on-tap="login" theme="primary"> <!-- (3) -->
                            Login
                        </vaadin-button>
                    </vaadin-vertical-layout>
                </form>
            </iron-form>
        </div>
    </template>

    <script>
      class LoginView extends Polymer.GestureEventListeners(Polymer.Element) {
        static get is() {
          return 'sa-login-view';
        }

        login() { <!-- (4) -->
          if (!this.$.username.invalid && !this.$.password.invalid) {
            this.$.form.submit();
          }
        }
      }

      window.customElements.define(LoginView.is, LoginView);
    </script>
</dom-module>
  1. Declares the encapsulating iron-form, allows redirects and makes the form referenceable.

  2. Declares the actual HTML form and adds needed fields and button.

  3. The button calls some interceptor to allow adding custom stuff…​

  4. Which in this case does some client side evaluation of the input before submitting the form

Alternative 3: Java-only with LoginForm or LoginOverlay component

With the release of Vaadin 13 we now provide an extremely simplified way to get a login form via dedicated login components: The LoginOverlay component is a full-featured login dialog whereby LoginForm can be integrated wherever you need it. For details check the documentation.

Using them boils down the whole LoginView to

@Tag("sa-login-view")
@Route(value = LoginView.ROUTE)
@PageTitle("Login")
public class LoginView extends VerticalLayout {
        public static final String ROUTE = "login";

        private LoginOverlay login = new LoginOverlay(); // (1)

        public LoginView(){
            login.setAction("login"); // (2)
            login.setOpened(true); // (3)
            login.setTitle("Spring Secured Vaadin");
            login.setDescription("Login Overlay Example");
            getElement().appendChild(login.getElement()); // (4)
        }
}
  1. Gets the dialog.

  2. Sets the action aka the endpoint Spring Security is expecting the form data at.

  3. Opens the dialog immediately. Depending on your application behavior you can defer opening the dialog until some user interaction.

  4. Adds the form to the view.

@Tag("sa-login-view")
@Route(value = LoginView.ROUTE)
@PageTitle("Login")
public class LoginView extends VerticalLayout {
    public static final String ROUTE = "login";

    private LoginForm login = new LoginForm(); // (1)

    public LoginView(){
            login.setAction("login"); // (2)
            getElement().appendChild(login.getElement()); // (3)
    }
}
  1. Gets the form.

  2. Sets the action aka the endpoint Spring Security is expecting the form data at.

  3. Adds the form to the view.

Nice!

In sum, all presented approaches will create a straightforward login form allowing users to enter their credentials and to use a button to log in to the application in one or the other way.

Try them by running mvn spring-boot:run and use the configured credentials user/password. When successful, you will get redirected, and the main view of the Vaadin + Spring starter shows up.

Handling authentication failures

Magnus Konze asked me how to display a proper error message on a failed login attempt. The simplest approach is to add a query parameter to the login view’s URL in case of a failed authentication and show an error message in the login dialog.

SecurityConfiguration.java
@EnableWebSecurity
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

	private static final String LOGIN_PROCESSING_URL = "/login";
	private static final String LOGIN_FAILURE_URL = "/login?error"; // (1)
	private static final String LOGIN_URL = "/login";
	private static final String LOGOUT_SUCCESS_URL = "/login";
  1. Adds an empty query parameter to the login failure URL.

LoginView.java
@Tag("sa-login-view")
@Route(value = LoginView.ROUTE)
@PageTitle("Login")
public class LoginView extends VerticalLayout implements BeforeEnterObserver { // (1)
	public static final String ROUTE = "login";

	private LoginForm login = new LoginForm();

	public LoginView(){
		login.setAction("login");
		getElement().appendChild(login.getElement());
	}

	@Override
	public void beforeEnter(BeforeEnterEvent event) { // (2)
		// inform the user about an authentication error
		// (yes, the API for resolving query parameters is annoying...)
		if(!event.getLocation().getQueryParameters().getParameters().getOrDefault("error", Collections.emptyList()).isEmpty()) {
			login.setError(true); // (3)
		}
	}
}
  1. Allows receiving navigation events before the view is rendered.

  2. BeforeEnterEvent gives us access to query parameters.

  3. Shows the default error message the login dialog provides out of the box.

As you noticed this example is based on alternative 3 discussed above but the same approach can be used for the other two alternatives, too.

Enjoy!

Vaadin is an open-source framework offering the fastest way to build web apps on Java backends
GET STARTED

Comments (36)

Paul Römer
2 years ago Jan 17, 2020 5:38am
Paul Römer
2 years ago Jan 17, 2020 5:24am
Paul Römer
2 years ago Nov 25, 2019 12:18pm
Matt Asgari
2 years ago Nov 26, 2019 2:49am
Paul Römer
2 years ago Nov 25, 2019 1:00pm
Paul Römer
2 years ago Nov 06, 2019 1:16pm
Weles Siqueira
2 years ago Nov 06, 2019 1:51pm
Paul Römer
2 years ago Mar 29, 2019 9:08pm
nat go
2 years ago Sep 04, 2019 8:41pm
Paul Römer
2 years ago Sep 05, 2019 11:20am
Paul Römer
2 years ago Sep 13, 2019 7:57pm
Matthew Crocker
2 years ago Sep 25, 2019 2:06am
Paul Römer
2 years ago Sep 27, 2019 3:04am
Paul Römer
2 years ago Jun 04, 2019 11:56am
Madeline K
2 years ago May 14, 2019 11:17pm
Olli Tietäväinen
2 years ago May 10, 2019 10:11am
olu mide
2 years ago May 10, 2019 4:05pm
Paul Römer
2 years ago May 09, 2019 6:24am
Paul Römer
2 years ago Apr 15, 2019 10:02am
Paul Römer
2 years ago Apr 09, 2019 11:10am