Abstractions are important, but mismatched abstractions can be problematic. One particular mismatch that I encounter every now and then is a lot more than problematic: it can be outright dangerous. This is the mismatch between URL-based filtering in various security frameworks and view-based navigation in Vaadin.
If you only remember one thing from this post, let it be that access control for Vaadin views must be based on the actual Vaadin views, and not only on the URL. Keep reading to learn why that is, and what a safe solution looks like.
Security frameworks are designed to check request URLs
Almost anything you do in the browser that involves a server, sends an HTTP request to one URL or another. If you enter https://app.com/private-view in the browser's address bar, the browser sends a request to that URL and renders the HTML that it receives in the response. If the HTML contains something that shouldn't be publicly accessible, you can configure your server to handle requests to that URL in different ways, depending on whether the request contains proof that the user is logged in and has appropriate permissions. An unauthorized request can, for instance, receive a redirect to a login page, instead of the sensitive HTML.
Generic web security frameworks, such as Spring Security, Shiro, or JAAS make it really easy to add protection on this level of abstraction, by configuring access-control requirements based on individual URLs or URL patterns. The problem is that Vaadin operates on a slightly different level of abstraction, and securing individual URLs may indeed appear to work, even though it actually doesn't work.
All internal Vaadin requests go to the same URL
The application developer doesn't have to think about requests and URLs thanks to Vaadin's abstractions. There are still internal requests to deliver events, such as button clicks, to the server. All these requests are sent to the same URL, regardless of the kind of event or the view from which it originates. To make anything related to Vaadin work, you must allow all requests to this URL. Once you have done this, access control based on URLs provided by your security framework doesn’t help you.
let link = document.createElement('a'); link.href = 'private-view'; link.setAttribute('router-link', ''); document.body.appendChild(link); link.click();
In general, Vaadin only allows a user to perform actions if the UI component that triggers the action is visible to the user. However, this general principle does not hold true for navigation between views, because the user can navigate back and forth using the browser's back and forward buttons regardless of visible components. This also means that you need to apply access control for views based on the actual views, instead of only relying on URL pattern restrictions offered by a web security framework.
Check access for views instead of for URLs
To guard against this potential vulnerability, you need to block access based on the view that Vaadin is about to show, instead of doing so based on the URL to which the browser sends a request. You can, and should, still use your security framework for all other matters, such as logging in users, keeping track of the current user, and managing what permissions or roles each user has.
The most straightforward approach is to use callbacks in the view components. Add a check to the onAttach callback in every view and use the security framework to find or inject the current user and verify that they are authorized. You can make this slightly more sophisticated by using the callback from the BeforeEnterObserver or HasUrlParameter interfaces, instead of onAttach. In this way, you will get access to a BeforeEnterEvent that allows you to forward or reroute the user to another view.
While this approach is straightforward, it does have its drawbacks. The most obvious problem is that you have to remember to add the check separately to each view in your application: if you forget, the view will be publicly accessible. It can, in some cases, also be problematic that the view instance needs to be created before the access control check is done.
An alternative approach is to use a global BeforeEnterListener that is invoked every time navigation occurs. This listener can inspect the target view class to determine its access control restrictions based, for example, on a hardcoded lookup map or on annotations on the class. This approach allows you to report an error when encountering a view without any associated access control definition, instead of allowing everything by default.
At the time of writing, BeforeEnterListener still has the same potential limitation as manual checking does, in that the instances are created before you can check access control. This issue was recently fixed, but the improvement has not yet ended up in a release. Keep an eye on this ticket to find out when the change is released.
To add your BeforeEnterListener to each UI, you need to add a UIInitListener to VaadinService. This can be done with a VaadinServiceInitListener. If you're using Spring, you can simply add @Component to your listener implementation to make it a managed bean, since Vaadin's integration with Spring automatically uses such beans. If you're using CDI, you can use @Observes for ServiceInitEvent, since Vaadin's CDI integration fires it as a CDI event. Finally, if you use neither of these, you can create a META-INF/services/com.vaadin.flow.server.VaadinServiceInitListener file and put the fully qualified class name of your listener implementation class as the file contents.
You can see a practical example of this in the Securing your app with Spring Security tutorial series. In particular, have a look at the Secure Router Navigation section that sets up the navigation listener. Even though that tutorial is focused on Spring Security, the same general concepts apply, regardless of the security and DI frameworks you use.