Monday, September 16, 2013

Logging out with Apache Shiro and Spring Web Flow

In this article I will attempt to save others the frustration of figuring out how to log out with Apache Shiro security in a Spring Web Flow app. The Shiro site has a nice link on Spring integration and from incorporating that and finding the Subject.logout() method, only to use it resulting in a Spring Web Flow error about being unable to serialize a non-existent session (since you just killed it):
[INFO] Caused by: org.springframework.webflow.execution.repository.NoSuchFlowExecutionException: No flow execution could be found with key 'e1s2' -- perhaps this executing flow has ended or expired? This could happen if your users are relying on browser history (typically via the back button) that references ended flows.
[INFO]  at org.springframework.webflow.execution.repository.support.AbstractFlowExecutionRepository.getConversation(AbstractFlowExecutionRepository.java:172)
[INFO]  at org.springframework.webflow.execution.repository.impl.DefaultFlowExecutionRepository.updateFlowExecutionSnapshot(DefaultFlowExecutionRepository.java:141)
[INFO]  at org.springframework.webflow.engine.impl.FlowExecutionImpl.updateCurrentFlowExecutionSnapshot(FlowExecutionImpl.java:427)
[INFO]  at org.springframework.webflow.engine.impl.RequestControlContextImpl.updateCurrentFlowExecutionSnapshot(RequestControlContextImpl.java:222)
[INFO]  at org.springframework.webflow.engine.ViewState.updateHistory(ViewState.java:324)
[INFO]  at org.springframework.webflow.engine.ViewState.exit(ViewState.java:248)
[INFO]  at org.springframework.webflow.engine.Transition.execute(Transition.java:225)
[INFO]  at org.springframework.webflow.engine.impl.FlowExecutionImpl.execute(FlowExecutionImpl.java:395)
[INFO]  at org.springframework.webflow.engine.impl.RequestControlContextImpl.execute(RequestControlContextImpl.java:214)
[INFO]  at org.springframework.webflow.engine.TransitionableState.handleEvent(TransitionableState.java:116)
[INFO]  at org.springframework.webflow.engine.Flow.handleEvent(Flow.java:547)
[INFO]  at org.springframework.webflow.engine.impl.FlowExecutionImpl.handleEvent(FlowExecutionImpl.java:390)
[INFO]  at org.springframework.webflow.engine.impl.RequestControlContextImpl.handleEvent(RequestControlContextImpl.java:210)
[INFO]  at org.springframework.webflow.engine.ViewState.handleEvent(ViewState.java:231)
[INFO]  at org.springframework.webflow.engine.ViewState.resume(ViewState.java:195)
[INFO]  at org.springframework.webflow.engine.Flow.resume(Flow.java:537)
[INFO]  at org.springframework.webflow.engine.impl.FlowExecutionImpl.resume(FlowExecutionImpl.java:259)
[INFO]  ... 54 more
[INFO] Caused by: org.springframework.webflow.conversation.NoSuchConversationException: No conversation could be found with id '1' -- perhaps this conversation has ended? 
[INFO]  at org.springframework.webflow.conversation.impl.ConversationContainer.getConversation(ConversationContainer.java:126)
[INFO]  at org.springframework.webflow.conversation.impl.SessionBindingConversationManager.getConversation(SessionBindingConversationManager.java:117)
[INFO]  at org.springframework.webflow.execution.repository.support.AbstractFlowExecutionRepository.getConversation(AbstractFlowExecutionRepository.java:183)
[INFO]  at org.springframework.webflow.execution.repository.support.AbstractFlowExecutionRepository.getConversation(AbstractFlowExecutionRepository.java:170)
[INFO]  ... 70 more
[INFO] 
From digging deeper into the documentation you can find a log out filter for Shiro, but using this instead of Subject.logout() in an action method (i.e. the action attribute of a button or link, not an action-state) will still give the same error. Lastly, you can find a blog post by BalusC (under Programmatic Logout) that describes how to tell JSF that you just logged out via Omnifaces in the action but that doesn't quite do the trick either. However, calling Subject.logout(), the code BalusC describes and then returning NULL from the action does it. Apparently the log out code doesn't stop SWF from trying to resume the flow, but returning null does. To summarize, the following will let you log out of Apache Shiro and Spring Web Flow when bound to the action of something like an h:button:
 public String logoutAction() throws IOException {
  SecurityUtils.getSubject().logout();
  Faces.invalidateSession();
  Faces.redirect("/faces/loginflow");
  return null;
 }