XForms (Orbeon), CAS and CMIS (Alfresco) – Part 1 – Authentication

I thought I’d write about my experience of using the Alfresco CMIS interface as a backend to a custom XForms application.
There’s a natural fit here as the atom based syntax of CMIS fits very nicely with XForms however there are a few little wrinkles to work through.

Authentication

The first issue to decide on is how to handle the authentication of the CMIS service – this will very much depend on your application requirements and architecture. As you will see from earlier posts we are running behind CAS with both our Alfresco and Orbeon apps using CAS authentication (Orbeon via Spring Security).

Due to the application requirements I am using two different authentication strategies – a well known generic user and proxy authentication using the logged in user.

Basic Auth

This is the easiest to set up and use

The way I’ve done this is to define some configuration properties to set the user name etc and assign these to variables within the model

 

<xxforms:variable name="alfresco-uri"
     select="xxforms:property('chassis.alfresco.uri')"
     as="xs:anyURI"/>
<xxforms:variable name="alfresco-username"
     select="xxforms:property('chassis.alfresco.username')"
     as="xs:string"/>
<xxforms:variable name="alfresco-credentials"
     select="xxforms:property('chassis.alfresco.credentials')"
     as="xs:string"/>
<xxforms:variable name="is-send-alfresco"
     select="xxforms:property('chassis.alfresco.send')"
     as="xs:boolean"/>

Retrieving information is then via a straightforward submission using xxforms:username and xxforms:password (obviously you need to set the path of the action appropriately)

<xforms:submission id="cmis-rest-get-file-record" method="get"
  action="{$alfresco-uri}service/cmis/p/User Homes/TestUser/children"
  mediatype="application/atom+xml"
  xxforms:username="{$alfresco-username}"
  xxforms:password="{$alfresco-credentials}"
  replace="instance"
  instance="ins-cmis-rest-create-file"
  if="$is-send-alfresco"
  serialization="none"/>

This uses basic auth which will work with an out of box Alfresco but does have the limitations of basic auth i.e. it’s not very secure unless you use https

Proxy Auth

First thing to note in following on is that this has to be done using https as otherwise CAS won’t like it.

The complication here is that the requests to the Alfresco server are being sent from Orbeon not directly from the user so although the user is logged into both Orbeon and Alfresco the CMIS requests will not be authenticated.

The way I’ve chosen to do this is to implement a servlet filter to obtain an Alfresco authentication ticket which can then be passed through to Orbeon as part of the request and appended to the CMIS request.

You will see here that the code uses HttpClient 3.1 (because I’m still using Orbeon 3.8) but it should be fairly trivial to upgrade.

I also use JNDI to retrieve the name of the Alfresco service e.g. <Environment name=”alfrescoApp” type=”java.lang.String” value=”https://alfresco/alfresco”/> – you may want to do this differently.

package org.aelfric.demo.security;

import java.io.IOException;

import javax.naming.NamingException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * @author iwright
 *
 */
public class AlfrescoCASFilter implements Filter {

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

  public static final String ALFRESCO_TICKET = "alf_ticket";

  public void doFilter(ServletRequest request,
                          ServletResponse response,
                          FilterChain chain)
                       throws IOException, ServletException {
    if (request instanceof HttpServletRequest &&
                        response instanceof HttpServletResponse) {
      doHttpFilter((HttpServletRequest)request,
                        (HttpServletResponse)response, chain);
    } else {
      throw
        new ServletException("only HTTP request and responses are
                                         supported by this filter");
    }
  }

  @Override
  public void doHttpFilter(HttpServletRequest request,
    	           HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
    log.debug("request inbound");

    HttpClient client = new HttpClient();
    String ticket = null;
    try {
	ticket = AlfrescoCASFilter.getAlfrescoTicket(request, client);
    } catch (NamingException e) {
	log.error("Need to set JNDI variable alfrescoApp if using Alfresco", e);
    }
    if (ticket != null) {
	request.setAttribute(ALFRESCO_TICKET, ticket);
    }
    log.debug("alfresco ticket:" + ticket);
    chain.doFilter(request, response);

    log.debug("response outbound");
  }

  public static String getAlfrescoTicket(HttpServletRequest req,
                                                 HttpClient client)
            throws UnsupportedEncodingException, IOException, HttpException,
            ServletException, NamingException {

    HttpSession httpSess = req.getSession(true);

    // Get CAS information
    Assertion assertion = (Assertion) httpSess
                .getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION);
    if (assertion == null) {
       return "";
    }
    String username = assertion.getPrincipal().getName();
        // Read out the ticket id
    String ticket = null;
    String alfrescoWebAppURL = LookupJNDI.<String> getEnvEntry(ALFRESCO_WEBAPP_URL_CONFIG);
    if (alfrescoWebAppURL == null) {
       return (null);
    }
    String proxyticket = assertion.getPrincipal().getProxyTicketFor(
                alfrescoWebAppURL);

    if (proxyticket == null) {
      return (null);
    }
    String casLoginUrl = alfrescoWebAppURL + "/service/api/logincas?u="
                + URLEncoder.encode(username, "UTF-8") + "&t="
                + URLEncoder.encode(proxyticket, "UTF-8");

    GetMethod method = new GetMethod(casLoginUrl);
    method.setRequestHeader("cookie", req.getHeader("cookie"));
    int statusCode = client.executeMethod(method);
    // Read back the ticket
    if (statusCode == 200) {
      InputStream is = method.getResponseBodyAsStream();
      // do something with the input stream
      BufferedReader in = new BufferedReader(new InputStreamReader(is));
      String line;
      String responseText = "";
      while ((line = in.readLine()) != null) {
        responseText += line;
      }
      in.close();
      ticket = responseText;

   } else {
      if (log.isDebugEnabled()) {
        log.debug("Authentication failed, received response code: "
                        + statusCode);
      }
   }
   method.releaseConnection();
   return ticket;
  }
}

The next step is to configure the servlet filter in your web.xml for any URLs where you want to proxy authenticate.

 <filter-mapping>
        <filter-name>AlfrescoCASFilter</filter-name>
        <url-pattern>/study/*</url-pattern>
    </filter-mapping>

Once you’ve done this then the request will contain an attribute with the Alfresco ticket.

Now for the Orbeon part.

For information I hold cmis-rest as a separate model but I’m trying to simplify the examples by leaving that out.

When the model is contructed the requested attribute is held in a control instance.

<xforms:instance id="ins-cmis-rest-control">
        <control>
            <ticketAuth/>
        </control>
</xforms:instance>

<xforms:instance id="ins-cmis-rest">
        <request/>
</xforms:instance>

<!-- Note that this has to be done as part of the
                              xforms-model-construct-done stage -->
<xforms:action ev:event="cmis-rest-get-ticket">
   <xforms:insert
      nodeset="instance('ins-cmis-rest')"
      origin="xxforms:get-request-attribute('alf_ticket')"/>
   <xforms:setvalue ref="instance('ins-cmis-rest-control')//ticketAuth"
                value="instance('ins-cmis-rest')"/>
</xforms:action>

<xforms:action ev:event="xforms-model-construct-done">
      <xforms:dispatch name="cmis-rest-get-ticket"/>
</xforms:action>

So now when the submission is sent the Alfresco ticket can be appended to the URL

So the equivalent submission but with proxy authentication is:

 <xforms:submission id="cmis-rest-get-file-record" method="get"
       action="
{$alfresco-uri}service/cmis/p/User Homes/
           TestUser/children?alf_ticket={instance('ins-cmis-rest')}"
mediatype="application/atom+xml" replace="instance"
instance="ins-cmis-rest-create-file" serialization="none"/>

One Reply to “XForms (Orbeon), CAS and CMIS (Alfresco) – Part 1 – Authentication”

Leave a Reply

Your email address will not be published. Required fields are marked *