Wednesday, July 31, 2013

Vaadin, Shiro, and Push

I've been using Vaadin for the past few months on a large project and I've been really impressed with it. I've also been using Apache Shiro for all of the projects authentication, authorization, and crypto needs. Again, very impressed.

Up until Vaadin 7.1, I've just been relying on my old ShiroFilter based configuration of Shiro using the DefaultWebSecurityManager. While this configuration wasn't an exact fit for a Vaadin rich internet application (RIA), it worked well enough that I never changed it. The filter would initialize the security manager and the Subject and it was available via the SecurityUtils as expected.

Then Vaadin 7.1 came along with push support via Atmosphere. Depending on the transport used, Shiro's SecurityUtils can no longer be used because it depends on the filter to bind the Subject to the current thread but, for example, a Websocket transport won't use the normal servlet thread mechanism and a long standing connection may be suspended and resumed on different threads.

There is a helpful tip for using Shiro with Atmosphere where the basic idea is to not use SecurityUtils and to simply bind the subject to the Atmosphere request. Vaadin does a good job of abstracting away the underlying transport which means there is little direct access to the Atmosphere request; however Vaadin does implement a VaadinSession which is the obvious place to stash the Shiro Subject.

First things first, I switched from using the DefaultWebSecurityManager to just using the DefaultSecurityManager. I also removed the ShiroFilter from my web.xml. With the modular design of Shiro I was still able to use my existing Realm implementation and just rely on implementing authc/authz in the application logic itself. The Vaadin wiki has some good, general examples of how to do this. Essentially this changes the security model from web security where you apply authc/authz on each incoming HTTP request to native/application security where you implement authc/authz in the application and assume a persistent connection to the client.

Next up, I needed a way to locate the Subject without relying on SecurityUtils due to the thread limitations mentioned above. Following the general idea of using Shiro with Atmosphere, I wrote a simple VaadinSecurityContext class that provides similar functionality but binds the Subject to the VaadinSession rather than to a thread. Now that I don't have the SecurityUtils singleton anymore, I rely on Spring to inject the context into my views (and view-models) as need using the elegant spring-vaadin plugin.

package org.mpilone.util.shiro;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.mpilone.util.shiro.SecurityContext;
import com.vaadin.server.VaadinSession;
/**
* A {@link SecurityContext} implementation that uses the {@link VaadinSession}
* to store the {@link Subject} for the current user. This allows the Subject to
* be discovered even in a push environment where {@link SecurityUtils} can't be
* used because the server side thread may be suspended and resumed at any time
* on different threads.
*
* @author mpilone
*/
public class VaadinSecurityContext implements SecurityContext {
/**
* The attribute name used in the {@link VaadinSession} to store the
* {@link Subject}.
*/
private final static String SUBJECT_ATTRIBUTE = VaadinSecurityContext.class
.getName() + ".subject";
/**
* The security manager for the application.
*/
private SecurityManager securityManager;
/**
* Sets the security manager for the application. To support push, normally a
* {@link DefaultSecurityManager} is used rather than a web specific one
* because the normal HTTP request/response cycle isn't used.
*
* @param securityManager
* the security manager to set
*/
public void setSecurityManager(SecurityManager securityManager) {
this.securityManager = securityManager;
}
/**
* Returns the subject for the application and thread which represents the
* current user. The subject is always available; however it may represent an
* anonymous user.
*
* @return the subject for the current application and thread
* @see SecurityUtils#getSubject()
*/
@Override
public Subject getSubject() {
VaadinSession session = VaadinSession.getCurrent();
// This should never happen, but just in case we'll check.
if (session == null) {
throw new IllegalStateException("Unable to locate VaadinSession "
+ "to store Shiro Subject.");
}
Subject subject = (Subject) session.getAttribute(SUBJECT_ATTRIBUTE);
if (subject == null) {
// Create a new subject using the configured security manager.
subject = (new Subject.Builder(securityManager)).buildSubject();
session.setAttribute(SUBJECT_ATTRIBUTE, subject);
}
return subject;
}
}
At this point everything was working and I have full authc/authz with Shiro and Vaadin push support. But, the Shiro DefaultSecurityManager uses a DefaultSessionManager internally to manage the security Session for the Subject. While you could leave it like this, I didn't like the fact that my security sessions were being managed separately from my Vaadin/UI sessions. This was going to be a problem when it came to session expiration because Vaadin already has UI expiration times and VaadinSession expiration times and I was now introducing security Session expiration times. The odds of getting them all to work together nicely was slim and I can imagine users getting randomly logged out while still having valid UIs or VaadinSessions.

My solution was to write a custom Shiro SessionManager and inject it into the DefaultSecurityManager. My implementation is very simple with the assumption that whenever a Shiro Session is needed, a user specific VaadinSession is available. The VaadinSessionManager creates a new session (using Shiro's SimpleSessionFactory) and stashes it in the user specific VaadinSession. Expiration of the Shiro Session (and Subject) are now tied to the expiration of the VaadinSession. While I could have used the DefaultSessionMananger and implemented a custom Shiro SessionDAO, I didn't see that the DefaultSessionManager offered me much given that I did not want Session expiration/validation support.

package org.prss.mpilone.util.shiro;
import java.util.UUID;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.SessionException;
import org.apache.shiro.session.mgt.*;
import com.vaadin.server.VaadinSession;
/**
* A {@link SessionManager} implementation that uses the {@link VaadinSession}
* for the current user to persist and locate the Shiro {@link Session}. This
* tightly ties the Shiro security Session lifecycle to that of the
* VaadinSession allowing expiration, persistence, and clustering to be handled
* only in the Vaadin configuration rather than be duplicated in both the Vaadin
* and Shiro configuration.
*
* @author mpilone
*
*/
public class VaadinSessionManager implements SessionManager {
/**
* The session attribute name prefix used for storing the Shiro Session in the
* VaadinSession.
*/
private final static String SESSION_ATTRIBUTE_PREFIX = VaadinSessionManager.class
.getName() + ".session.";
/**
* The session factory used to create new sessions. In the future, it may make
* more sense to simply implement a {@link Session} that is a lightweight
* wrapper on the {@link VaadinSession} rather than storing a
* {@link SimpleSession} in the {@link VaadinSession}. However by using a
* SimpleSession, the security information is contained in a neat bucket
* inside the overall VaadinSession.
*/
private SessionFactory sessionFactory;
/**
* Constructs the VaadinSessionManager.
*/
public VaadinSessionManager() {
sessionFactory = new SimpleSessionFactory();
}
/*
* (non-Javadoc)
*
* @see
* org.apache.shiro.session.mgt.SessionManager#start(org.apache.shiro.session
* .mgt.SessionContext)
*/
@Override
public Session start(SessionContext context) {
// Retrieve the VaadinSession for the current user.
VaadinSession vaadinSession = VaadinSession.getCurrent();
// Assuming security is used within a Vaadin application, there should
// always be a VaadinSession available.
if (vaadinSession == null) {
throw new IllegalStateException("Unable to locate VaadinSession "
+ "to store Shiro Session.");
}
// Create a new security session using the session factory.
SimpleSession shiroSession = (SimpleSession) sessionFactory
.createSession(context);
// Assign a unique ID to the session now because this session manager
// doesn't use a SessionDAO for persistence as it delegates to any
// VaadinSession configured persistence.
shiroSession.setId(UUID.randomUUID().toString());
// Put the security session in the VaadinSession. We use the session's ID as
// part of the key just to be safe so we can double check that the security
// session matches when it is requested in getSession.
vaadinSession.setAttribute(SESSION_ATTRIBUTE_PREFIX + shiroSession.getId(),
shiroSession);
return shiroSession;
}
/*
* (non-Javadoc)
*
* @see
* org.apache.shiro.session.mgt.SessionManager#getSession(org.apache.shiro
* .session.mgt.SessionKey)
*/
@Override
public Session getSession(SessionKey key) throws SessionException {
// Retrieve the VaadinSession for the current user.
VaadinSession vaadinSession = VaadinSession.getCurrent();
String attributeName = SESSION_ATTRIBUTE_PREFIX + key.getSessionId();
if (vaadinSession != null) {
// If we have a valid VaadinSession, try to get the Shiro Session.
SimpleSession shiroSession = (SimpleSession) vaadinSession
.getAttribute(attributeName);
if (shiroSession != null) {
// Make sure the Shiro Session hasn't been stopped or expired (i.e. the
// user logged out).
if (shiroSession.isValid()) {
return shiroSession;
}
else {
// This is an invalid or expired session so we'll clean it up.
vaadinSession.setAttribute(attributeName, null);
}
}
}
return null;
}
}
So that's it. I wire it all up with Spring and I now have Shiro working happily with Vaadin. The best part is that none of my existing authc/authz code changed because it all simply works with the Shiro Subject obtained via the VaadinSecurityContext. In the future if I need to change up this configuration, I expect that my authc/authz code will remain exactly the same and all the changes will be under the hood with some Spring context updates.

<bean id="sessionManager"
class="org.mpilone.util.shiro.VaadinSessionManager" />
<bean id="securityManager" class="org.apache.shiro.mgt.DefaultSecurityManager"
p:realm-ref="mpiloneRealm"
p:sessionManager-ref="sessionManager" />
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<bean id="mpiloneRealm" class="org.mpilone.util.shiro.MyCustomRealm" />
<bean id="securityContext"
class="org.mpilone.util.shiro.VaadinSecurityContext"
p:securityManager-ref="securityManager" />
I'm interested to hear if anyone else found a good way to link up these two great frameworks or if you see any holes in my approach. I'm no expert on Atmosphere and Vaadin does a good bit of magic to dynamically kickoff server push, but so far things have been working well. Best of luck!