Saturday, 16 January 2010

Using a NavigiationHandler to do HTTP/HTTPS switching

In a project I was working on, we wanted the log in page and anything to do with payment to be secure using HTTPS, whilst the majority of the site would be unsecure using HTTP. JSF 1.2 doesn't really give you anyway of doing this other than manually specifying the full url for every single link and button on the site. This removes a lot of the flexibility and simplicity of JSF. I came up with a pretty good solution. For any link or action that you know will switch between secure and unsecure, which isn't that hard to know (if you design the site with that in mind), then you configure it to be a redirect. Then you create your own navigation handler which applies rules you create yourself, to determine which parts should be secure or not.

This is a very simple solution that when a redirect occurs, it checks the path of the destination page (its viewId). If it starts with "/secure" it gives a full HTTPS url and for all others, it gives a full HTTP url. To activate it, have a navigation rule with a redirect. e.g. a login button with a result "userLogin":
<navigation-case>
 <from-outcome>userLogin</from-outcome>
 <to-view-id>/secure/login.xhtml</to-view-id>
 <redirect/>
</navigation-case>
<navigation-case>
 <from-outcome>home</from-outcome>
 <to-view-id>/index.xhtml</to-view-id>
 <redirect/>
</navigation-case>
Then on successful login return result "home" to redirect back to HTTP.

Next you'll need to create and configure your own navigation handler. The easiest way is to copy the entire contents of the mojarra NavigationHandlerImpl and change one line. In the handleNavigation function the line String newPath = viewHandler.getActionURL(context, caseStruct.viewId); is replaced as shown below:
public void handleNavigation(FacesContext context, String fromAction, String outcome) {

   // ...
   
   if (caseStruct != null) {
      ViewHandler viewHandler = Util.getViewHandler(context);
      assert (null != viewHandler);

      if (caseStruct.navCase.hasRedirect()) {
         // perform a 302 redirect.
         // ---- start change ---
         String newPath = prependSecure(context, viewHandler, caseStruct.viewId);
         // ---- end change -----
         
   //....
}
Download the full source here.

Below is the additional code to support to prependSecure function, that builds up a full url with scheme for the page being redirected to.
private static final String SCHEME_HTTP = "http";
private static final String SCHEME_HTTPS = "https";
private static final int HTTPS_DEFAULT_PORT = 443;
private static final int HTTP_DEFAULT_PORT = 80;
private static final String PREFIX_SECURE = "/secure/";

private String prependSecure(FacesContext context, ViewHandler viewHandler,
      String viewId) {
   boolean isSecure = (viewId != null && (viewId.startsWith(PREFIX_SECURE)));

   String prefix = getServerUrlForceScheme(context, isSecure);

   return prefix + viewHandler.getActionURL(context, viewId);
}

public static String getServerUrlForceScheme(FacesContext context,
      boolean secure) {
   String scheme;
   int defaultPort;
   if (secure) {
      scheme = SCHEME_HTTPS;
      defaultPort = HTTPS_DEFAULT_PORT;
   } else {
      scheme = SCHEME_HTTP;
      defaultPort = HTTP_DEFAULT_PORT;
   }

   HttpServletRequest request = (HttpServletRequest) context
         .getExternalContext().getRequest();

   int port;
   //use current port if known (ideally make this configuration driven)
   if ((!SCHEME_HTTPS.equalsIgnoreCase(request.getScheme()) && !secure)
         || (!SCHEME_HTTP.equalsIgnoreCase(request.getScheme()) && secure))
      port = request.getServerPort();
   else
      port = defaultPort;

   String basePath = scheme + "://" + request.getServerName()
         + (defaultPort == port ? "" : ":" + port);

   return basePath;
}

No comments:

Post a Comment