Wednesday, September 4, 2013

Integrating Vaadin + Spring Security + Hibernate and doing it in Scala

Ok, so this is a little project/tutorial to integrate Vaadin to Spring Security, and code everything in Scala. The project also uses Hibernate to persist data into an H2 database. It's developed with Eclipse (+ Scala plugin) and Maven. Code can be found here.

This project implements a web application that allows a user to:
  • register to the site by providing his/her personal info
  • log-in to the site
  • log-out of the site
  • change his/her personal info
  • change his/her password
The application also shows internationalization (i18n). The main challenge in this type of integration is how to define authorization for different pages and how to handle situations where a page is accessed with insufficient credentials, i.e. how to forward the user to a login page or similar.

Other projects have integrated Vaadin to Spring Security, notably these:
  1. http://morevaadin.com/content/spring-security-integration/ 
  2. http://vaadin.xpoft.ru/ 
Both of these alternatives have shortcomings that have been worked around in this project + this one is written in Scala.

The problem

As described in (1), Spring Security (SS) works by securing subcontexts (myhost.com/public vs. myhost.com/private) whereas Vaadin uses fragments for different views (myhost.com/#!public vs. myhost.com/#!private). Fragments are a client side part of the URI that is not sent to the server, so SS cannot distinguish between the last two URIs.

Normally SS handles authentication and authorization (a&a) with exception handlers in a filter chain, so if a user tries to access a page (s)he is not authenticated for, SS will throw an exception and an exception handler will forward the user to a login page. This is illustrated below:

-> user sends request
 -> servlet invoked
  -> exception filter entered
   => page generation (with a&a that throws exceptions)
  -> exception filter catches a&a exception and sets up a login page forward
 -> servlet returns a redirect message to the browser
-> user's browser goes to a login page (that has a different URI i.e. subcontext)

If we want to implement all views (like the login and registration views that don't require authentication) and other views (that do require authentication and authorization) with Vaadin, we cannot use spring security's exception handling mechanism because the exception handling happens after the Vaadin page generation and needs to forward to another subcontext. If you try to forward to another fragment, you'll get a redirect loop:


The other projects mentioned above work around this by setting up a login page that is not a Vaadin view: The login page is implemented with FreeMarker or some other technology where the page can have a distinct subcontext.

Implementation

To avoid using different subcontexts, the access management has to be done in the Vaadin Navigator during the view generation. Here's a high level description of how this is done:
  • When a user navigates to a view, that view is provided by the Spring application context
  • The view object is defined with special annotations describing the roles the user must have in order to access the view
  • If the user does not have the proper role, the access manager throws an exception that is caught in a custom implementation of the navigator, which will then provide a login view
  • When the user provides his/her credentials, SS stores these credentials for the duration of the session (to be read by the access manager)

Spring integration with application context generated views


The integration of Spring to Vaadin starts with an extention of VaadinServlet. This is implemented in net.sf.vaadinturvaa.spring.SpringVaadinServlet. This implementation is pretty much copied from project (2) and loads the Spring application context as well as sets up a UIProvider for the application with net.sf.vaadinturvaa.spring.SpringUIProvider.

The Vaadin views are implemented in the net.sf.vaadinturvaa.views package and are marked with the following annotations:

Authentication

When a user register on the site, (s)he creates an account with personal information and a password. The password is hashed and stored to the database. When the user logs in, the password entered on the login form is hashed and compared to the hash in the database. If they match, the user is authenticated and a token is stored in the SecurityContextHolder:
SecurityContextHolder.getContext().setAuthentication(auth)
The SecurityContextPersistenceFilter will take care of persisting this between user requests.

Custom Navigator and error handling

When a page is accessed in Vaadin, the UI component defined for the VaadinServlet in the web.xml is invoked. More specifically, the Navigator configured in the UI's init method is invoked to access the appropriate view. The view instances are managed by Spring and returned by a custom implementation of Navigator. SS's AccessDecisionManager is responsible for making sure that the user accessing the view has proper credentials and if not, will throw an appropriate exception.

These exceptions are handled in class net.sf.vaadinturvaa.core.NavigatorFactory. This class returns Navigator instances that have a net.sf.vaadinturvaa.core.NavigationErrorHandlingStrategy. This is defined as follows:
abstract class NavigationErrorHandlingStrategy {

  def apply(navigationState: String, navigateTo: String => Unit)

}
The apply method takes the name of a navigation state and a function that takes the navigation state as parameter. This mirrors the way in which the Navigator object is invoked when navigating from one view to the next. The default implementation of this class is:
class DefaultNavigationErrorHandlingStrategy(val authenticationFailureView: String,
                                             val accessDeniedUnauthenticatedView: String,
                                             val accessDeniedAuthenticatedView: String)
    extends NavigationErrorHandlingStrategy {

  override def apply(navigationView: String, navigateTo: String => Unit) {
    try {
      navigateTo(navigationView)
    }
    catch {
      case e: AuthenticationException  => navigateTo(authenticationFailureView)
      case e: AlreadyLoggedInException => navigateTo(accessDeniedAuthenticatedView)
      case e: AccessDeniedException =>
        if(AuthenticationHelper.areWeAuthenticated)
          navigateTo(accessDeniedAuthenticatedView)
        else navigateTo(accessDeniedUnauthenticatedView)
    }
  }
}
This handles three different errors: unauthenticated user, insufficient privileges and a special case in which the user tries to access the login page when already logged in.

The NavigationErrorHandlingStrategy is used by the ExceptionHandlingNavigator returned by the NavigatorFactory:
  private class ExceptionHandlingNavigator(ui: UI,
                                           container: SingleComponentContainer,
                                           errorHandling: NavigationErrorHandlingStrategy)
      extends Navigator(ui, container) {
    addProvider(viewProvider)

    override def navigateTo(navigationState: String) {
      errorHandling(navigationState, super.navigateTo(_))
    }
  }
The viewProvider is an instance of ViewProvider that gets the view instances from the Spring application context.

Internationalization

i18n is made of two parts: localized view labels and localized validation messages. View labels come from ViewMessages_xx.properties files and validation messages come from ValidationMessages_xx.properties files. View labels are made available to the application with a MessageSource and all view implementations get access to it by extending the net.sf.vaadinturvaa.views.MessageSourced trait. Validation messages are accessed directly by Vaadin's BeanValidator.

Running the app

To run the app you need to 
  • start the H2 database as a separate process (./h2.sh -tcpAllowOthers -tcpPort 8043) or start it along with the rest of the application (see root-context.xml). You also need to place the accounts.h2.db file in your home directory (or modify the jdbc.properties file accordingly).
  • build the app as a WAR file and start it in a Servlet engine or from the project directory with mvn jetty:run.
  • go to http://localhost:9090/#!register and register away.

6 comments:

  1. There seems to be an issue if Vaadin Push is used in this setup. I added a @Push annotation to MainUI and added another view (creatively called AnotherView). Then I:

    1) Logged in
    2) Clicked on the button that takes me to AnotherView
    3) Reloaded the webpage (hit F5 in Chrome)
    4) Clicked on the button that takes me back to MainView
    5) Clicked on the button that takes me to AnotherView

    Expected Result: AnotherView is displayed
    Actual Result: LoginView is displayed

    Worth mentioning that the http://vaadin.xpoft.ru/ addon suffers similar problems with Vaadin Push.

    I have attached a patch containing the added @Push and AnotherView at https://sourceforge.net/p/vaadinturvaa/tickets/1/

    ReplyDelete
    Replies
    1. Hi. There's neat way to overtake your problem with @Push. Just use @Scope("ui") provided with:
      dependency
      groupId: com.cybercom
      artifactId: spring-ui-scope
      version: 0.0.2
      /dependency
      Works like a charm. Good luck!

      Delete
  2. Nice Post! ... Do you have any project did it in java like this?. I tried to translate your code to java... but i don't know scala so a lot of thing i din't know how to put it in java.

    ReplyDelete
    Replies
    1. I translate almost all, the only classes i have a doubt how to translate them are the NavigationErrorHandlingStrategy and the NavigatorFactory, can you help me translating these two classes to java please?.

      Delete
    2. Can you share your Java based code? I have a vaadin ui page as public url and rest vaadin ui as protected but its not working.

      Delete
  3. Hi guys, thanks for the feedback! Unfortunately I'm not working on this actively, this was just an example I tried and wanted to share. Hope you can solve any issues you may have if you decide to extend this further.

    ReplyDelete