A web application normally has a security filter - an own solution or from Spring framework or something else. Such security filter does a redirect if user is not authenticated to use requested web resources. This is not an JSF redirect. The filter doesn't pass current request to the FacesServlet at all. Here is an example:
public class SecurityFilter implements Filter { private Authenticator authenticator; private String loginPage; public void init(FilterConfig config) throws ServletException { ... // create an Authenticator authenticator = AuthenticatorFactory.createAuthenticator(config); loginPage = getLoginPage(config); ... } public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest hReq = (HttpServletRequest) request; HttpServletResponse hRes = (HttpServletResponse) response; ... // get user roles Collection roles = ... // get principal Principal principal = hReq.getUserPrincipal(); if (!roles.isEmpty() && principal == null) { // user needs to be authenticated boolean bRet = authenticator.showLogin(hReq, hRes); if (!bRet) { // redirect the a login page or error sending ... } } ... } }Problem: indeed an Ajax request is redirected to a specified page after session timeout, but the response can not be proper treated on the client-side. We have a quite regular redirect in this case and the specified page in the response. Client-side Ajax code doesn't expect the entire page coming back.
A possible solution would be to pass "not authenticated" request through FacesServlet. We need to write an Authenticator class which stores the page URL we want to be redirected to in the request scope.
public class FormAuthenticator { ... public boolean showLogin(HttpServletRequest request, HttpServletResponse response, String loginPage) throws IOException { ... request.setAttribute("web.secfilter.authenticator.showLogin", loginPage); return true; } }The method showLogin returns true in order to avoid a redirect by filter. Let us execute the redirect by JSF! The call gets now a third parameter "loginPage".
authenticator.showLogin(hReq, hRes, loginPage);If you use Spring security you should have an entry point in your XML configuration file like this one
<beans:bean id="authenticationProcessingFilterEntryPoint" class="com.xyz.webapp.security.AuthenticationProcessingFilterEntryPoint"> <beans:property name="loginFormUrl" value="/login.html"/> <beans:property name="serverSideRedirect" value="true"/> </beans:bean>The class AuthenticationProcessingFilterEntryPoint is derived from Spring's one
/** * Extended Spring AuthenticationProcessingFilterEntryPoint for playing together with JSF Ajax redirects. */ public class AuthenticationProcessingFilterEntryPoint extends org.springframework.security.ui.webapp.AuthenticationProcessingFilterEntryPoint { public static final String ATTRIBUTE_LOGIN_PAGE = "web.secfilter.authenticator.showLogin"; public void commence(ServletRequest request, ServletResponse response, AuthenticationException authException) throws IOException, ServletException { request.setAttribute(ATTRIBUTE_LOGIN_PAGE, getLoginFormUrl()); super.commence(request, response, authException); } }On the JSF side we need a special phase listener to do the redirect by ExternalContext. We check at first the buffered page URL in the beforePhase. If it was set - an JSF redirect is needed and will be done.
/** * Phase listener for the Restore View Phase which manages display of login page. */ public class SecurityPhaseListener implements PhaseListener { private static final Log LOG = LogFactory.getLog(SecurityPhaseListener.class); public void afterPhase(PhaseEvent event) { ; } public void beforePhase(PhaseEvent event) { FacesContext fc = event.getFacesContext(); String loginPage = (String) fc.getExternalContext().getRequestMap(). get("web.secfilter.authenticator.showLogin"); if (StringUtils.isNotBlank(loginPage)) { doRedirect(fc, loginPage); } } public PhaseId getPhaseId() { return PhaseId.RESTORE_VIEW; } /** * Does a regular or ajax redirect. */ public void doRedirect(FacesContext fc, String redirectPage) throws FacesException { ExternalContext ec = fc.getExternalContext(); try { if (ec.isResponseCommitted()) { // redirect is not possible return; } // fix for renderer kit (Mojarra's and PrimeFaces's ajax redirect) if ((RequestContext.getCurrentInstance().isAjaxRequest() || fc.getPartialViewContext().isPartialRequest()) && fc.getResponseWriter() == null && fc.getRenderKit() == null) { ServletResponse response = (ServletResponse) ec.getResponse(); ServletRequest request = (ServletRequest) ec.getRequest(); response.setCharacterEncoding(request.getCharacterEncoding()); RenderKitFactory factory = (RenderKitFactory) FactoryFinder.getFactory(FactoryFinder.RENDER_KIT_FACTORY); RenderKit renderKit = factory.getRenderKit(fc, fc.getApplication().getViewHandler().calculateRenderKitId(fc)); ResponseWriter responseWriter = renderKit.createResponseWriter( response.getWriter(), null, request.getCharacterEncoding()); fc.setResponseWriter(responseWriter); } ec.redirect(ec.getRequestContextPath() + (redirectPage != null ? redirectPage : "")); } catch (IOException e) { LOG.error("Redirect to the specified page '" + redirectPage + "' failed"); throw new FacesException(e); } } }It works in both cases - for Ajax and regular request.
Realy helpfull. Thanks a lot.
ReplyDeleteHello,
ReplyDeleteSorry but this does not work for me. I tried this with an autoComplete component : when a timeout occurs, the ajax redirect response is sent (with the right xml including the tag), but on the client side nothing happens, I still see the animated gif. Any help would be appreciated ...
(Thanks anyway for the solution, it works on the server side)
Which JSF library do you use to send an Ajax request with the autoComplete component? How does the response look like for the Ajax redirect? Are you sure that your filter doesn't catch the response and redirect to the login page?
ReplyDeleteWhich framework are you using? thanks.
ReplyDeleteHello Anonymous,
ReplyDeleteI'm using Mojarra 2.x for standard JSF 2 components and PrimeFaces 2.x as component library.
Hello Oleg,
ReplyDeleteExcuse me for the delay !
I am using PrimeFaces 2.2-RC2 and MyFaces 2.0.3.
I have also tried it with Mojarra, but the code does not work : line 58 ec.redirect(...) throws a NPE because at some point the ViewRoot is null.
Anyway, I would prefer to stay with MyFaces.
So my problem is that even when the browser (in my case firefox 3.6) receives the ajax redirect response (xml fragment with redirect tag, I verified that it is correct with firebug), nothing more happens, I still see the "waiting" animated gif.
I also have another question : I feel quite "uncomfortable" with the fact that we let an unauthenticated request go through spring security filters. Couldn't this be a kind of security hole ? And would it be long/difficult to reproduce the behavior of the externalContext.redirect method in another component (for example a Spring Security RedirectStrategy), so that the request does not go past the security filter ?
Thanks for your help ...
Hello,
ReplyDeleteI have faced a NPE with Mojarra 2.0.3 as well. But it's resolved since 2.0.4. I didn't change yet the code to be fit with the last JSF impl. I will update this post after changing to the new JSF impl.
You can try to reproduce the behavior of the externalContext.redirect method outside of FacesServlet. I never tried that but I found this post how you can create FacesContext manually:
http://ocpsoft.com/java/jsf-java/jsf-20-extension-development-accessing-facescontext-in-a-filter/
Hi,
ReplyDeleteThanks for the link.
Do you have any clue for my ajax/javascript problem ? (ajax redirect requests not being properly recognize on the client side ?)
Do you know some way to add a listener or a kind of hook to PrimeFaces javascript, in order to do the proper redirection ?
I use p:autoComplete, session timed out and when starting to type animated gif appears and does not go away. Response has redirect url set but redirect is not happening. I have some openfaces components on page is this because of that or bug on primefaces?
ReplyDelete2{"validationFailed":false}
Response from previous post looks like
ReplyDelete<?xml version=\"1.0\" encoding=\"utf-8\"?>
2<partial-response><redirect url=\"/loginfailed.jsf\"></redirect><extension ln=\"openfaces\" ajaxResult=\"null\" type=\"ajaxResult\"></extension><extension primefacesCallbackParam=\"validationFailed\">{\"validationFailed\":false}</extension></partial-response>
Thanks for your post, but it doesn't seem to work for me.
ReplyDeleteThe problem is that my authenticationEntryPoint is not called on every request and the PhaseListener always finds a null value for the loginPage.
Could you post a working security config xml for this case?
Could it be a problem that I'm using the element in my configuration (which adds a LoginUrlAuthenticationEntryPoint). I tried replacing the Element but i didn't really succeed and I depend on it's functionality.
Thank you and best regards,
Robert
I forgot to write that the use of "AuthenticationProcessingFilterEntryPoint" is deprecated and should be replaced by "LoginUrlAuthenticationEntryPoint"
ReplyDeleteAlso doesn't work for me. The request is already sent back with AuthenticationProcessingFilterEntryPoint, and then it reaches PhaseListener which now doesn't have request attributes from the original ajax request.
ReplyDeleteRequest should not be sent back with AuthenticationProcessingFilterEntryPoint. Don't make a redirect. You should pass it through.
ReplyDeleteI managed to redirect to the login.jsf page using custom response... But now when I login again it redirects me to the page where I should go, but with additional xml in the body:
ReplyDelete<partial-response>
<change
<update id="javax.faces.ViewState">
Some text
</update>
</change>
</partial-response>
Oleg, do you know how can I fix this?
Sasha, what do you use? If you use a security filter, it seems to buffer the last page location. We had a similar issue in our security filter. I think, you should look deeper into your filter and reset the last page location if it was stored in session.
ReplyDeleteYes I use security filter. I added my custom filter so I can manage ajax request from jsf. And now it works. Thanx.
ReplyDeleteMy xml-tags have been stripped out of my comment, so i put this onto pastebin:
ReplyDeletehttp://pastebin.com/wemY8EFW
As it seems that my comment has been removed, there it is on pastebin:
ReplyDeletehttp://pastebin.com/MuVexQHj
Thank you in advance for any advice!
Best Regards,
Robert
FWIW I found a simpler solution using container security:
ReplyDeletehttp://community.jboss.org/thread/166752?tstart=0
Regards.
That's almost the same. XML you shown in the example is automatically generated by ExternalContext.redirect() Many component libraries and JSF implementations have their own syntax and it's better to let generate partial response than try to write it self.
ReplyDeleteHi Oleg
ReplyDeleteWere you able to update the code to use the newer Mojarra implementation to resolve the NPE on line 58?
Sure, it's here http://paste.kde.org/101389/
ReplyDeleteSorry, but I couldn't get your solution working. The attribute_login_page is always null in the phase listener.
ReplyDeleteSeems that spring-security calls the entry-point multiple times and when the request reaches the phase listener, the attribute is gone.
I have to agree with ssamayoa. The solution in http://community.jboss.org/thread/166752?tstart=0
is so much simpler.
I got a problem with implementing your solution on mojarra 2.1.0. Method createPartialResponseWriter in ParialViewContext is throwing NPE in line
ReplyDeleteresponseWriter = ctx.getRenderKit().createResponseWriter(out,
"text/xml", encoding);
(it comes from redirect method in jsf phase listener you proposed)
Any idea how to get over that exception?
To get rid of the NPE problem in redirect method just change lines:
ReplyDeleteResponseWriter responseWriter =
renderKit.createResponseWriter(
response.getWriter(), null, request.getCharacterEncoding());
fc.setResponseWriter(responseWriter);
to
ResponseWriter responseWriter = renderKit.createResponseWriter(response.getWriter(), null, request.getCharacterEncoding());
responseWriter = new PartialResponseWriter(responseWriter);
fc.setResponseWriter(responseWriter);
Hello,
ReplyDeleteI have a Problem when getting a org.springframework.security.authentication.InsufficientAuthenticationException: Full authentication is required to access this resource
In this case the listener is not fired und no redirect will be done.
Does anyone have an idea?
I do not know if the solution can recognize Spring security configuration?
ReplyDeleteIn my project, some resources are public, some are private and protected only for authenticated user.
And I only want to process ajax session time out...normal page session time out worked correctly.
ReplyDeleteTry this session time out warning using primefaces
ReplyDeletehttp://primefaces-tips.blogspot.in/2012/03/prime-faces-session-timeout-countdown.html
I have a simpler solution, by using a custom RedirectStrategy implementation.
ReplyDeletehttp://wowpleasedoitagain.blogspot.com/2012/08/web-dev-spring-security-jsf-ajax.html
thanks a lot
ReplyDeleteI have a solution using java script, it is easy to implement.
ReplyDeleteTake a look at this adress http://javacodetips.blogspot.com.tr/2014/03/how-to-implement-session-expired-alert.html