20 November 2014

Implementing Spring Security with JSF (Login + RedirectStrategy)

When implementing Spring Security in JSF application, many problems happens, and one of the most important one is the redirection in case of HTTP errors!

In this post, I'll take about 2 points, the first point is how to implement the Login page (you will find many resources illustrate this - for example this) but I'll extend it allowing the user to continue on the page he was coming from (aka. Saved Request URL), the second point and the more important one is handling HTTP errors in case of Ajax requests.

Point #1 Handle Login From:

Initially, you have your JSF application (in my case I use Primefaces, but shouldn't matter at all, because the solution is JSF related not primefaces-related)


You have your xhtml login page (login.xhtml) like this:


<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:f="http://java.sun.com/jsf/core"
    xmlns:p="http://primefaces.org/ui">
<f:view>
    <h:form id="loginForm">
        <h:messages style="color: red;" />

        <table>
            <tr>
                <td><p:outputLabel for="usernameTxt" value="Username" /></td>
                <td><p:inputText id="usernameTxt" value="#{loginBean.username}" /></td>
            </tr>
            <tr>
                <td><p:outputLabel for="passwordTxt" value="Password" /></td>
                <td><p:password id="passwordTxt" value="#{loginBean.password}" /></td>
            </tr>
            <tr>
                <td colspan="2"><p:commandButton value="Login"
                        action="#{loginBean.login}" /></td>
            </tr>
        </table>
    </h:form>
</f:view>

</html>

Very simple!, regularly with spring security, we submit the form to /j_spring_security_check but we have different situation here, we have a command button which send POST request to the same URL of the page.

2 solutions here, I'll take about one of them (as appears in Macro-blog link above) which is using a reference for the authenticationManager from the bean action method:

first, you need to expose the authenticationManager in the security context configuration file:


    <authentication-manager alias="authenticationManager">
        <authentication-provider>
            <user-service>
                <user name="admin" password="admin" authorities="ROLE_ADMIN" />
            </user-service>
        </authentication-provider>
    </authentication-manager>

Note the use of "alias" attribute.

Then in you JSF Managed bean, you can inject it and use it as the following:

@Inject
private AuthenticationManager authenticationManager;

@Setter
@Getter
private String username;

@Setter
@Getter
private String password;

public LoginBean() {
}

public String login() {
    try {
        Authentication authentication = authenticationManager
                .authenticate(new UsernamePasswordAuthenticationToken(
                        this.username, this.password));

        SecurityContextHolder.getContext()
                .setAuthentication(authentication);

    } catch (AuthenticationException ex) {
        log.equals(ex.getMessage());
        Util.addMessage("Login Failed: " + ex.getMessage());
        return "";
    }

    return Util.getSavedUrl() + "?faces-redirect=true";
}

The code above is pretty clear, If fail (AuthenticationExcpetion thrown, show error message), else redirect to some URL.

The point here is some URL is critical, because if the user was trying to access resource "A", then spring security asked him to login first, then after login he should access resource "A" itself not a welcome screen.

This handled by spring security, but in our case, we need to ask Spring-security explicity to give us the "SavedURL" from the session. so the implmenetation of Util.getSavedUrl() is below:

public static String getSavedUrl() {
    HttpServletRequest request = ((HttpServletRequest) FacesContext
            .getCurrentInstance().getExternalContext().getRequest());

    SavedRequest savedRequest = new HttpSessionRequestCache().getRequest(
            request, (HttpServletResponse) FacesContext
                    .getCurrentInstance().getExternalContext()
                    .getResponse());

    if (savedRequest != null) {
        try {
            URL url = new URL(savedRequest.getRedirectUrl());
            return url.getFile().substring(
                    request.getContextPath().length());
        } catch (Exception e) {
            log.error(e.getMessage() + " Using default URL");
        }
    }
    return "admin/index.xhtml?faces-redirect=true"; // default page!
}

And that's all for the first point.

Point #2 Handle HTTP error codes for Ajax requests:

JSF (and jsf based frameworks) uses Ajax in many cases,  the PrimeFaces autocomplete is a good example.

Originally with regular HTTP requests (non-Ajax), when the user try to access a resource after session expired, spring security will send error code 302 to the browser, when browser sees the response code, he will do the redirect the Login page.


But in case of Ajax requests, the ajax client should handle this on his own.

In case of JSF, (I am not the JSF expert Guy :D ) JSF uses some internal protocol sending XML between the server and the client, and this XML is well defined in matter if the server wants the client to do redirect, the server doesn't send 302 for the client, instead, he sends a response like:

<?xml version="1.0" encoding="UTF-8"?>
<partial-response>
    <redirect url="URL to redirect to"></redirect>
</partial-response>

So, when the ajax caller (who send the JSF request), see this response, he will do redirect immediately.


So, to integrate this with Spring security we need to tell it override the cod that sends the response code and add the above xml payload just in case of Ajax requests.

To do this Spring security provide a redirect strategy (org.springframework.security.web.RedirectStrategy) that you can extends and hook the spring security at some points.

So, here's a sample implementation:


package testing.ss;

import java.io.IOException;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.util.UrlUtils;

/**
 * Based on code from DefaultDirectStrategy and
 * https://gist.github.com/banterCZ/5160269
 * 
 * @author mhewedy
 *
 */
public class JsfRedirectStrategy implements RedirectStrategy {

    private static final String FACES_REQUEST_HEADER = "faces-request";

    protected final Log logger = LogFactory.getLog(getClass());

    private boolean contextRelative;

    /**
     * Redirects the response to the supplied URL.
     * <p>
     * If <tt>contextRelative</tt> is set, the redirect value will be the value
     * after the request context path. Note that this will result in the loss of
     * protocol information (HTTP or HTTPS), so will cause problems if a
     * redirect is being performed to change to HTTPS, for example.
     */
    public void sendRedirect(HttpServletRequest request,
            HttpServletResponse response, String url) throws IOException {
        String redirectUrl = calculateRedirectUrl(request.getContextPath(), url);
        redirectUrl = response.encodeRedirectURL(redirectUrl);

        boolean ajaxRedirect = "partial/ajax".equals(request
                .getHeader(FACES_REQUEST_HEADER));
        if (ajaxRedirect) {

            String ajaxRedirectXml = createAjaxRedirectXml(redirectUrl);
            logger.debug("Ajax partial response to redirect: "
                    + ajaxRedirectXml);

            response.setContentType("text/xml");
            response.getWriter().write(ajaxRedirectXml);
        } else {
            logger.debug("Non-ajax redirecting to '" + redirectUrl + "'");
            response.sendRedirect(redirectUrl);
        }
    }

    private String calculateRedirectUrl(String contextPath, String url) {
        if (!UrlUtils.isAbsoluteUrl(url)) {
            if (contextRelative) {
                return url;
            } else {
                return contextPath + url;
            }
        }

        // Full URL, including http(s)://

        if (!contextRelative) {
            return url;
        }

        // Calculate the relative URL from the fully qualified URL, minus the
        // last
        // occurrence of the scheme and base context.
        url = url.substring(url.lastIndexOf("://") + 3); // strip off scheme
        url = url.substring(url.indexOf(contextPath) + contextPath.length());

        if (url.length() > 1 && url.charAt(0) == '/') {
            url = url.substring(1);
        }

        return url;
    }

    /**
     * If <tt>true</tt>, causes any redirection URLs to be calculated minus the
     * protocol and context path (defaults to <tt>false</tt>).
     */
    public void setContextRelative(boolean useRelativeContext) {
        this.contextRelative = useRelativeContext;
    }

    // from https://gist.github.com/banterCZ/5160269
    private String createAjaxRedirectXml(String redirectUrl) {
        return new StringBuilder()
                .append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>")
                .append("<partial-response><redirect url=\"")
                .append(redirectUrl)
                .append("\"></redirect></partial-response>").toString();
    }

}


Then you have to hock the LoginUrlAuthenticationEntryPoint, but because the RedirectStrategy is a final variable, you have to extend it like this:

package testing.ss;

import java.io.IOException;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;

/**
 * Based on code from LoginUrlAuthenticationEntryPoint
 * 
 * @author mhewedy
 *
 */
// see http://forum.spring.io/forum/spring-projects/security/88829-is-it-possible-to-change-spring-security-3-redirects-from-full-urls-to-relative-urls
@SuppressWarnings("deprecation")
public class JsfLoginUrlAuthenticationEntryPoint extends
        LoginUrlAuthenticationEntryPoint {

    Log log = LogFactory.getLog(getClass());

    private RedirectStrategy redirectStrategy;

    public void setRedirectStrategy(RedirectStrategy redirectStrategy) {
        this.redirectStrategy = redirectStrategy;
    }

    @Override
    public void commence(HttpServletRequest request,
            HttpServletResponse response, AuthenticationException authException)
            throws IOException, ServletException {

        String redirectUrl = null;

        if (isUseForward()) {

            if (isForceHttps() && "http".equals(request.getScheme())) {
                // First redirect the current request to HTTPS.
                // When that request is received, the forward to the login page
                // will be used.
                redirectUrl = buildHttpsRedirectUrlForRequest(request);
            }

            if (redirectUrl == null) {
                String loginForm = determineUrlToUseForThisRequest(request,
                        response, authException);

                log.debug("Server side forward to: " + loginForm);

                RequestDispatcher dispatcher = request
                        .getRequestDispatcher(loginForm);

                dispatcher.forward(request, response);

                return;
            }
        } else {
            // redirect to login page. Use https if forceHttps true

            redirectUrl = buildRedirectUrlToLoginPage(request, response,
                    authException);
        }
        redirectStrategy.sendRedirect(request, response, redirectUrl);
    }

}


Then go back and configure http tag to reference the Entry Point:

<http auto-config="true" use-expressions="true" entry-point-ref="authenticationEntryPoint">
    .....
</http>

<beans:bean id="authenticationEntryPoint"
    class="testing.ss.JsfLoginUrlAuthenticationEntryPoint" p:loginFormUrl="/login.xhtml"
    p:redirectStrategy-ref="redirectStrategy" />
    
<beans:bean id="redirectStrategy" class="testing.ss.JsfRedirectStrategy" />

And that's all.

The complete source code on github.

No comments: