SSO

After my last post I thought I ought to write something about Single Sign On (SSO) – this post will cover a bit more than just SSO.

I’ve done a lot of work with SSO but it’s one of those things you only visit periodically so it’s easy to forget things – also it’s harder than it feels like it should be!

This post is intended to be fairly generic, although I will use some examples from applications that I’ve worked on.

For anyone especially interested in Alfresco please note that I haven’t looked at any of the version 6/Identity Service or ADF stuff as yet.

My first point is that if you’re working on a new SSO project then the first thing you need to do is to work out how you are going to merge your user data from the different apps – this is generally the hardest part and I’ve seen quite a number of projects give up because of this. It’s a good argument for getting your SSO sorted out as soon as possible.

Single Sign On is where you log in once and are authenticated to all applications (also potentially logged out).

Shared Sign On is where you use a shared user dictionary e.g. LDAP but each application is responsible for it’s own authentication.

Authentication is who are you, authorization is what you can do.

There are many, many guides to this on the internet and if you’re really, really lucky you might find one you can understand.

Single Sign On

My first point is that if you do this right then the protocol/technology doesn’t matter all that much – this area is a lot more mature than it was even a couple of years ago.

What you are trying to achieve is to protect a list of endpoints (URLs) (probably not all e.g. CSS) and communicate a user id through to your application.

Try and keep this as (logically) separate as possible – just identify the user. It can be tempting to link this up to authorization but try not to, it just causes trouble.

The aim here is to intercept the incoming request and process it before it gets to your application. There are different ways of doing this e.g. in Apache, in Tomcat, in filters.

For java what you are aiming for is that a call to request.getRemoteUser() will get you the user id.

For java applications my preference is to try and use a web-fragment.xml to define the filters and endpoints. The problem with this is that any filters defined in the web-fragment are applied after filters in the web.xml so depending on how the application is structured this may not be possible (sometimes you can get away with writing another application specific filter but that’s not ideal – I managed to do this for 5.1 Share and repo but not 5.2 Share).

See https://issues.alfresco.com/jira/browse/ALF-21848 for a suggestion for restructuring the Share web.xml to make this easier.

Another common approach is to just edit the web.xml, sometimes using a maven profile, but that’s not ideal.

A quick aside here – be careful about your username/id – for example we log in using email address but use a different attribute as the user id. Single sign on systems will support this but you might need to be careful in your configuration.

Username/password log in

Sometimes you will want to log in using a username and password – this might be for using a non web or mobile client e.g. in Alfresco CMIS, mobile or IMAP.

Probably the cleanest choice here is to make the client do the work by obtaining a token from SSO and using that in conjunction with your SSO mechanism of choice, however that’s not always practical.

Another alternative is to identify the request, one way is look for the authorization header, and proceed from there either by using SSO proxy authentication (see below) or carrying on to your normal username/password auth method (note Alfresco doesn’t support using a different username attribute out of the box)

Proxy Authentication

This is where you have logged in to one application and want to pass that authentication information through to another.

The idea here is that after the client application (C) has authenticated and thus allowed the server side application (A) to authenticate itself using the SSO mechanism then application A can obtain a token from the SSO server and passes that token through to the other server side application (B) it wants to talk to. Application B then validates the token against the SSO server and as a result obtains the identity information.

What you are doing is intercepting and wrapping the outgoing request from A to B and including the SSO token (typically as part of a header but this can be handled by a library)

An Alfresco specific aside, at least up to version 5.2, Share SSO communicates with the repo/platform/ACS (whatever you want to call it…) by setting a custom HTTP header containing the username (you have to be careful about the security of your configuration!)

So in summary this splits into the following parts:

  • Application A obtains an access token from the SSO server (standard part of libraries)
  • Application A injects the access token into requests to application B (depends on how requests are made)
  • Application B intercepts the request and uses the token to obtain the username (this will be the same configuration as is used for the normal SSO authentication, and subject to the same security constraints)

Authorization

As I said earlier try and keep authentication and authorization separate.

It probably helps to understand this if you consider the evolution of (a lot of) applications.

Start off with a custom authentication and authorization layer, then realize that you need to use shared authentication (probably keeping the original method) so add in synchronization (with LDAP and others), then you realize that you want SSO so add that in later.

There are a few approaches you can take here:

(consider the performance implications, how look ups will be cached – you’ll be doing this a lot and the caching mechanisms, time out etc are not as well considered as for authorization)

Native SSO

Most SSO systems support some form of attribute release – you can use this information to set the rights within your application e.g. OAuth scope or parse a list of LDAP group memberships.

This means that the application must be used in an SSO context.

This can be, and probably would be, done with the same validation request as is used for authorization but do try to keep it logically separate.

Mixed SSO/Shared Auth backend

Use the SSO authorization and then an additional query to the shared backend to determine rights e.g. run a query against LDAP to determine group membership.

Mixed SSO/Custom/Shared Auth

This is probably the most common model that I’ve seen even though it’s relatively painful to configure.

Custom authorization model is set to sync with the shared auth backend.

Use the SSO authorization and then an additional query to the custom model to determine rights.

Custom model can contain rights(groups/group membership) not held in the shared backend.

There can be timing problems waiting for the custom model to be updated from the shared backend.

(Technically can be done without the shared auth but that’s really not a good idea so I’m not including it)

Proxy authorization service

Make a proxy authenticated request to a separate service which can manage the lookup(s)

This is a more flexible version of native SSO but comes with potential performance issues.

User Information

I’m throwing this in as an extra because it’s pretty similar to authorization in concept.

You might want to have information about the user, for example, their name to use in your application.

This information can be retrieved using the same methods as for the authorization information.

I’ve put this separately mainly because if you want to use an avatar or picture for the user then you potentially have a much larger piece of data to consider (and might want to convert the image into a more suitable format)

(Alfresco note – the use of the an image isn’t supported out of the box but it’s something that can be done with customizations)

Authorization Management

Chances are that if you are using SSO then there will some external process for managing authorization e.g. LDAP groups.

If you want to manage authorization from within your application then ideally you need to authenticate against the existing management system before making any changes – this is one of the few occasions where you may want to be able to retrieve the user password from the SSO system e.g. to authenticate against LDAP. The alternative is use some form of super user from within your application but that isn’t ideal as it potentially gives elevated rights to your user by mistake.

Single Sign Out

You may not care about this e.g. by default Share in SSO configuration removes the log out option from the menu and there’s no way to log out. (It’s not too hard to put back in however – see previous posts and the alfresco-cas project on github)

Most applications have their own way of determining whether you are logged in as well as whatever is used by SSO. This is to support non SSO log ins.

The important thing to remember here is that you are logged in to both the SSO system and the application and when you log out of one, you need to log out of both (otherwise you’ll just be logged straight back in again)

Normally the idea would be to log out of the application first and then forward to the SSO logout page (probably with a redirection parameter to send you somewhere afterwards).

This doesn’t cover the more complex case where the user logs out of a different application. In this case the SSO application will send a logout request to your application (as part of the SSO logout process) – this logout request can then be handled to ensure that the local application logout also happens. i.e. logging out from application A also logs you out from applications B, C, D… as well as SSO.

It’s common for the first case to be handled but more unusual to handle the second case.

Summary

SSO is in a much better place than it was a few years ago but there’s still no one right way to do it and that’s likely to remain the case. (I’ve spent a lot of time helping people with CAS SSO for Alfresco)

SSO brings big benefits, both to the users and by providing a single place to manage authentication and, potentially, authorization.

Be careful! SSO provides a single point of failure for the organization and while this can mitigated by suitable configuration you still need to be careful especially during upgrades.

Don’t forget to keep on top of the upgrades!

Keep it as simple as possible as any customization makes upgrading more difficult (you are keeping on top of the upgrades aren’t you?)

Try to keep to only using one SSO system otherwise it’s not really SSO, as well as being more difficult to maintain – you’ll probably end up with some sort of shared LDAP based backend e.g. OpenLDAP or ActiveDirectory.

Make sure that you keep authentication and authorization (logically) separate.

If you write your application in a suitably flexible way then it should be possible to easily support any current or future protocols. To achieve this there are three main parts to consider:

  • Make sure the client application can handle the authentication protocol – don’t forget authentication failures (should be fairly easy with client libraries, response interceptors etc)
  • abstract the mechanism for protecting incoming requests – how to specify the endpoints to protect, and how to pass the validated information through to your application. (e.g. web-fragment.xml and getRemoteUser for java)
  • Provide an abstraction layer for proxy requests i.e. make sure it’s easy to modify any requests between different application components.

 

Further adventures with CAS and Alfresco (and LDAP)

Like Alfresco in the cloud and myriad other systems we’ve decided to use the email address as the user name for logging in. This works fine until you want to allow the user to be able to change their email.

The problem here is that Alfresco doesn’t support changing user names (I believe that it can be done with some database hacking but not recommended)

My solution here is to allow logging in via CAS to use the mail attribute as the user name but to pass the uid to Alfresco to use as the Alfresco user name while this means that the Alfresco user name is not the same as they’ve used to log in, it does allow you to change the mail attribute and as the user name isn’t often visible this works quite well – actually it’s not too bad to set the uid as the mail address especially if the rate of change is low although there are some situations where this is potentially confusing.

So how to do it…

First configure CAS (I’m using 4.0_RC2 at the moment)

In your deployerConfigContext.xml find your registeredServices and add

 <property name="usernameAttribute" value="uid"/>

so you end up with something like this:

<bean class="org.jasig.cas.services.RegexRegisteredService" p:id="0"
	p:name="HTTP and IMAP" p:description="Allows HTTP(S) and IMAP(S) protocols"
	p:serviceId="^(https?|imaps?)://alfresco.wrighting.org/.*" p:evaluationOrder="10000001">
    <property name="usernameAttribute" value="uid"/>
</bean>

For 4.1 you’ll need:

Note that you need the allowedAttributes to contain the usernameAttribute otherwise the value of the usernameAttribute will be ignored.

<bean class="org.jasig.cas.services.RegexRegisteredService" p:id="0"
p:serviceId="^(https?|imaps?)://xxx.*"
p:evaluationOrder="10000000">
<property name="usernameAttributeProvider">
<bean
class="org.jasig.cas.services.PrincipalAttributeRegisteredServiceUsernameProvider"
c:usernameAttribute="uid" />
</property>
<property name="attributeReleasePolicy">
<bean class="org.jasig.cas.services.ReturnAllowedAttributeReleasePolicy">
<property name="allowedAttributes">
<list>
<value>uid</value>
</list>
</property>
</bean>
</property>
</bean>

Now to configure Share and Alfresco (see previous posts)

If you are using CAS 4.0_RC2 then make sure that you are using the CAS 2 protocol (or SAML but I’d go with CAS 2) so if you are using the java client the in the web.xml your CAS Validation Filter will be:

  <filter>
   <filter-name>CAS Validation Filter</filter-name>
   <filter-class>org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter</filter-class>
   <init-param>
       <param-name>casServerUrlPrefix</param-name>
        <param-value>${cas.server.prefix}</param-value>
   </init-param>
     <init-param>
         <param-name>serverName</param-name>
         <param-value>${alfresco.server.name}</param-value>
     </init-param>
 </filter>

(This will work for CAS 1 in later versions)

CAS for Alfresco 4.2 on Ubuntu

Lots of confusion around on this subject so I’m going to attempt to distill some wisdom into this post and tweak it for Ubuntu

2 good blogs Nick with mod_auth_cas and Martin with CAS client and the Alfresco docs

I’m not going to talk about setting up CAS here as this post is complex enough already – I’ll just say be careful if using self signed certs.

I’ve used Martin’s method before with Alfresco 3.4

It’s a tricky decision as to which approach to use:

  • the mod_auth_cas approach is the approach supported by Alfresco but it introduces the Apache plug in which isn’t as well supported by CAS and you have problems with managing the mod_auth_cas cookie management, caching etc
  • the java client is a bit more involved and intrusive but seems to work quite well in the end
  • I haven’t tried container managed auth but it looks promising

Using mod_auth_cas

For a more detailed explanation look at Nick’s blog – this entry is more about how rather than why and is specific to using apt-get packages on Ubuntu.

First set up your mod_auth_cas

Next tell Tomcat to trust the Apache authentication by setting the following attribute tomcatAuthentication=”false” on the AJP Connector (port 8009)

Now you need to set up the Apache Tomcat Connectors module – mod-jk

apt-get install libapache2-mod-jk

Edit the properties file defined in /etc/apache2/mods-enables/jk.conf – /etc/libapache2-mod-jk/workers.properties – to set the following values

workers.tomcat_home=/opt/alfresco-4.2.c/tomcat
workers.java_home=/opt/alfresco-4.2.c/java

Add to your sites file e.g. /etc/apache2/sites-enabled/000-default

JkMount /alfresco ajp13_worker
JkMount /alfresco/* ajp13_worker
JkMount /share ajp13_worker
JkMount /share/* ajp13_worker

And don’t forget to tell Apache which URLs to check

<Location />
Authtype CAS
require valid-user
</Location>

A more complex example in the wiki here

Add the following to tomcat/shared/classes/alfresco-global.properties

authentication.chain=external-apache:external,alfrescoNtlm1:alfrescoNtlm
external.authentication.enabled=true
external.authentication.proxyHeader=X-Alfresco-Remote-User
external.authentication.proxyUserName=

Finally add the following section to tomcat/shared/classes/alfresco/web-extension/share-config-custom.xml

Note that if you have customizations you may need this in the share-config-custom.xml in your jar

 	<config evaluator="string-compare" condition="Remote">
		<remote>
			<endpoint>
				<id>alfresco-noauth</id>
				<name>Alfresco - unauthenticated access</name>
				<description>Access to Alfresco Repository WebScripts that do not
					require authentication
				</description>
				<connector-id>alfresco</connector-id>
				<endpoint-url>http://localhost:8080/alfresco/s</endpoint-url>
				<identity>none</identity>
			</endpoint>

			<endpoint>
				<id>alfresco</id>
				<name>Alfresco - user access</name>
				<description>Access to Alfresco Repository WebScripts that require
					user authentication
				</description>
				<connector-id>alfresco</connector-id>
				<endpoint-url>http://localhost:8080/alfresco/s</endpoint-url>
				<identity>user</identity>
			</endpoint>

			<endpoint>
				<id>alfresco-feed</id>
				<name>Alfresco Feed</name>
				<description>Alfresco Feed - supports basic HTTP authentication via
					the EndPointProxyServlet
				</description>
				<connector-id>http</connector-id>
				<endpoint-url>http://localhost:8080/alfresco/s</endpoint-url>
				<basic-auth>true</basic-auth>
				<identity>user</identity>
                                <external-auth>true</external-auth>
			</endpoint>

			<endpoint>
				<id>activiti-admin</id>
				<name>Activiti Admin UI - user access</name>
				<description>Access to Activiti Admin UI, that requires user
					authentication
				</description>
				<connector-id>activiti-admin-connector</connector-id>
				<endpoint-url>http://localhost:8080/alfresco/activiti-admin
				</endpoint-url>
				<identity>user</identity>
			</endpoint>
		</remote>
	</config>

 

This gets you logged in but you still need to logout! Share CAS logout.
One thing to be careful about with using mod_auth_cas here is that you need to be aware of the mod_auth_cas caching – if you are not careful you’ll log out but mod_auth_cas will still think that you are logged in. There are some options here – set the cache timeout to be low (inefficient), use single sign out (experimental)

Using CAS java client

Martin’s blog works for Alfresco 3.4 and here are some notes I made for 4.2.d

Note that it is not supported to make changes to the web.xml

Make the following jars available:

cas-client-core-3.2.1.jar, commons-logging-1.1.1.jar, commons-logging-api-1.1.1.jar

You can do this by including them in the wars or by copying the following jars into <<alfresco home>>/tomcat/lib
N.B. If you place them into the endorsed directory then you will get error messages like this:
SEVERE: Exception starting filter CAS java.lang.NoClassDefFoundError: javax/servlet/Filter

You need to make the same changes to tomcat/shared/classes/alfresco-global.properties and share-config-custom.xml as for the mod_auth_cas method

Now add the following to share/WEB-INF/web.xml and alfresco/WEB-INF/web.xml

There’s some fine tuning to do on the url-pattern probably the best way is to copy the filter mappings for the existing authentication filter and add /page for share and /faces for alfresco.

Using the values below works but is a little crude (shown here to be concise)

 <filter>
    <filter-name>CAS Authentication Filter</filter-name>
    <filter-class>org.jasig.cas.client.authentication.AuthenticationFilter
    </filter-class>
    <init-param>
        <param-name>casServerLoginUrl</param-name>
        <param-value>https://www.wrighting.org/sso/login</param-value>
    </init-param>
    <init-param>
        <param-name>serverName</param-name>
        <param-value>https://alfresco.wrighting.org</param-value>
    </init-param>
</filter>
<filter>
    <filter-name>CAS Validation Filter</filter-name>
    <filter-class>org.jasig.cas.client.validation.Cas10TicketValidationFilter
    </filter-class>
    <init-param>
        <param-name>casServerUrlPrefix</param-name>
        <param-value>https://www.wrighting.org/sso</param-value>
    </init-param>
    <init-param>
        <param-name>serverName</param-name>
        <param-value>https://alfresco.wrighting.org</param-value>
    </init-param>
</filter>
<filter>
    <filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
    <filter-class>org.jasig.cas.client.util.HttpServletRequestWrapperFilter
    </filter-class>
</filter>
<filter-mapping>
    <filter-name>CAS Authentication Filter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
    <filter-name>CAS Validation Filter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
    <filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

Next add the following to the session-config section of the web.xml which relates to this issue which may be solved via removing the jsessionid from the url (this may cause problems with the flash uploader if you’re still using it see here)

<tracking-mode>COOKIE</tracking-mode>

There’s also a case for using web-fragments to avoid changing the main web.xml

You will need to redirect the change password link in the header (how to depends on version)

Container managed auth

This looks quite interesting CAS Tomcat container auth as it allows the use of the CAS java client within tomcat so being closer to the mod_auth_cas approach but without needing to configure Apache.

This issue referenced above gives some details of how somebody tried it – I think it should work if the session tracking mode is set to COOKIE but haven’t tried it.

More complex configurations

This is beyond what I’m trying to do but if you’ve got a load balanced configuration you may need to think about the session management – the easiest way to approach may be to use sticky sessions e.g.

ProxyRequests Off
ProxyPassReverse /share balancer://app
ProxyPass /share balancer://app stickysession=JSESSIONID|jsessionid nofailover=On

BalancerMember ajp://localhost:8019/share route=tomcat3
BalancerMember ajp://localhost:8024/share route=tomcat4

 

CAS, OpenLDAP and groups

This is actually fairly straightforward if you know what you’re doing unfortunately it takes a while, for me at least, to get to that level of understanding.

Probably the most important thing missing from the pages I’ve seen describing this is that you need to configure OpenLDAP first.

OpenLDAP

What you want is to enable the memberOf overlay

For Ubuntu 12.04 the steps are as follows:
Create the files
module.ldif

dn: cn=module,cn=config
objectClass: olcModuleList
cn: module
olcModulePath: /usr/lib/ldap
olcModuleLoad: memberof

overlay.ldif

dn: olcOverlay=memberof,olcDatabase={1}hdb,cn=config
objectClass: olcMemberOf
objectClass: olcOverlayConfig
objectClass: olcConfig
objectClass: top
olcOverlay: memberof
olcMemberOfDangling: ignore
olcMemberOfRefInt: TRUE
olcMemberOfGroupOC: groupOfNames
olcMemberOfMemberAD: member
olcMemberOfMemberOfAD: memberOf

Then configure OpenLDAP as follows:

ldapadd -Y EXTERNAL -H ldapi:/// -f module.ldif
ldapadd -Y EXTERNAL -H ldapi:/// -f overlay.ldif

You should probably read up on this a bit more – in particular note that retrospectively adding this won’t achieve what you want without extra steps to reload the groups

CAS

The CAS documentation is actually reasonably good once you understand that you are after the memberOf attribute but for example I’ll show some config here

deployerConfigContext.xml

<bean id="attributeRepository"
    class="org.jasig.services.persondir.support.ldap.LdapPersonAttributeDao">
    <property name="contextSource" ref="contextSource" />
    <property name="baseDN" value="ou=people,dc=wrighting,dc=org" />
    <property name="requireAllQueryAttributes" value="true" />

    <!-- Attribute mapping between principal (key) and LDAP (value) names used 
        to perform the LDAP search. By default, multiple search criteria are ANDed 
        together. Set the queryType property to change to OR. -->
    <property name="queryAttributeMapping">
        <map>
            <entry key="username" value="uid" />
        </map>
    </property>

    <property name="resultAttributeMapping">
        <map>
            <!-- Mapping beetween LDAP entry attributes (key) and Principal's (value) -->
            <entry value="Name" key="cn" />
            <entry value="Telephone" key="telephoneNumber" />
            <entry value="Fax" key="facsimileTelephoneNumber" />
            <entry value="memberOf" key="memberOf" />
        </map>
    </property>
</bean>

 

After that you can setup your CAS to use SAML1.1 or modify view/jsp/protocol/2.0/casServiceValidationSuccess.jsp according to your preferences.

Don’t forget to allow the attributes for the registered services as well

<bean id="serviceRegistryDao" class="org.jasig.cas.services.InMemoryServiceRegistryDaoImpl">
		<property name="registeredServices">
			<list>
				<bean class="org.jasig.cas.services.RegexRegisteredService">
					<property name="id" value="0" />
					<property name="name" value="HTTP and IMAP" />
					<property name="description" value="Allows HTTP(S) and IMAP(S) protocols" />
					<property name="serviceId" value="^(https?|imaps?)://.*" />
					<property name="evaluationOrder" value="10000001" />
					<property name="allowedAttributes">
						<list>
							<value>Name</value>
							<value>Telephone</value>
							<value>memberOf</value>
						</list>
					</property>
				</bean>
			</list>
		</property>
	</bean>

 

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"/>

Share CAS logout

Update – since surf 1.2 (at least) the Surf LogoutController understands the redirectURL request parameter (and redirectURLQueryKey, redirectURLQueryValue) which makes this all unnecessary – add the parameters to the end of the dologout call in the header – how depends on the version of Share – in share-config-custom.xml for the old style header – see  here for the new style header

I’ve finally had the time to work out how to configure Alfresco Share to successfully log out when using CAS to log in.

If you log out of just CAS you will still be logged into Share and vice versa so you need to log out of both – and just for good measure log out of Alfresco at the same time.

This follows on from Martin Bergljung’s blog on configuring for CAS

(Note this doesn’t do anything about single sign out as that’s a different problem)

The way I’ve done this is to override the logoutController by defined a new java class and referencing it in custom-slingshot-application-context.xml

 <!-- Override Logout Controller - to expire Alfresco tickets -->
   <bean id="logoutController" class="org.wrighting.web.site.servlet.CASSlingshotLogoutController">
      <property name="cacheSeconds" value="-1" />
      <property name="useExpiresHeader"><value>true</value></property>
      <property name="useCacheControlHeader"><value>true</value></property>
      <property name="connectorService" ref="connector.service" />
      <!-- if blank assumes the same as Share -->

      <property name="casHost"><value>https://alfresco</value></property>
      <property name="casPath"><value>sso/logout</value></property>

   </bean>

You’ll see here that I’ve defined a couple to properties to indicate where the CAS log out page lies – this is where the user will be redirected when they log out. This fits quite closely with the method of configuring the CAS server in the web.xml but you could use an alternative approach such as properties.

The java code is very closely based on org.alfresco.web.site.servlet.SlingshotLogoutController however there is one significant difference in that it doesn’t call handleRequestInternal in LogoutController.
This is because LogoutController.handleRequestInternal ends up redirecting to request.getContextPath whereas we want to go to our CAS logout page however it’s still necessary to make sure that you call AuthenticationUtil.logout which would normally be done in the LogoutController.

package org.wrighting.web.site.servlet;

import java.net.URL;

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

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.extensions.surf.UserFactory;
import org.springframework.extensions.surf.mvc.LogoutController;
import org.springframework.extensions.surf.site.AuthenticationUtil;
import org.springframework.extensions.surf.support.AlfrescoUserFactory;
import org.springframework.extensions.webscripts.connector.AlfrescoAuthenticator;
import org.springframework.extensions.webscripts.connector.Connector;
import org.springframework.extensions.webscripts.connector.ConnectorContext;
import org.springframework.extensions.webscripts.connector.ConnectorService;
import org.springframework.extensions.webscripts.connector.HttpMethod;
import org.springframework.extensions.webscripts.connector.Response;
import org.springframework.web.servlet.ModelAndView;

/**
 * CAS specific override of the Share specific override of the SpringSurf
 * dologout controller.
 * <p>
 * The implementation ensures Alfresco tickets are removed if appropriate and as
 * it can't delegates to the SpringSurf implementation for framework cleanup
 * does that clean up and then sends a logout to the CAS host
 *
 * @see org.alfresco.web.site.servlet.SlingshotLogoutController
 *
 * @author Ian Wright
 */
public class CASSlingshotLogoutController extends LogoutController {
    private static Log logger = LogFactory
            .getLog(CASSlingshotLogoutController.class);
    private ConnectorService connectorService;
    private String casHost;
    private String casPath;

    /**
     * @param connectorService
     *            the ConnectorService to set
     */
    public void setConnectorService(ConnectorService connectorService) {
        this.connectorService = connectorService;
    }

    @Override
    public ModelAndView handleRequestInternal(HttpServletRequest request,
            HttpServletResponse response) throws Exception {

        try {
            HttpSession session = request.getSession(false);
            if (session != null) {
                // retrieve the current user ID from the session
                String userId = (String) session
                        .getAttribute(UserFactory.SESSION_ATTRIBUTE_KEY_USER_ID);

                if (userId != null) {
                    // get the ticket from the Alfresco connector
                    Connector connector = connectorService.getConnector(
                            AlfrescoUserFactory.ALFRESCO_ENDPOINT_ID, userId,
                            session);
                    String ticket = connector.getConnectorSession()
                            .getParameter(
                                    AlfrescoAuthenticator.CS_PARAM_ALF_TICKET);

                    if (ticket != null) {
                        // if we found a ticket, then expire it via REST API -
                        // not all auth will have a ticket i.e. SSO
                        Response res = connector.call("/api/login/ticket/"
                                + ticket, new ConnectorContext(
                                HttpMethod.DELETE));
                        if (logger.isDebugEnabled())
                            logger.debug("Expired ticket: " + ticket
                                    + " user: " + userId + " - status: "
                                    + res.getStatus().getCode());
                    }
                }
            }
        } finally {
            AuthenticationUtil.logout(request, response);
            String target = request.getContextPath();
            if (casHost != null && casHost.length() > 0) {
                target = casHost;
            } else {
                URL reconstructedURL = new URL(request.getScheme(),
                        request.getServerName(),
                        request.getServerPort(),
                        "");
                target = reconstructedURL.toExternalForm();
            }
            if (casPath != null && casPath.length() > 0) {
                target += '/' + casPath;
            }
            if (logger.isDebugEnabled()) {
                logger.debug("Logout to:" + target);
            }
            response.sendRedirect(target);
        }

        return null;
    }

    /**
     * @param casHostValue
     *            the casHost to set - defaults to the same as Share
     */
    public void setCasHost(String casHostValue) {
        casHost = casHostValue;
    }

    /**
     * @param casPathValue
     *            the location of the CAS logout servlet
     */
    public void setCasPath(String casPathValue) {
        casPath = casPathValue;
    }
}

 

CAS, Alfresco and WebDAV

Having successfully configured Alfresco (and Share) to authenticate using CAS – it’s documented here

The next challenge is to try and work out how to get this authentication working with webdav
Update:

One way to do this this is to bypass CAS and authenticate against the underlying CAS datastore directly by adding a new authentication component.

Of course this means that you are not using CAS but then as you are likely to be accessing WebDAV outside of the browser the single sign on capabilities are not particularly relevant

We use Drupal as our underlying CAS data store so there’s a bit of custom code here – you may be able to just configure the authentication chain if you’re using a different method e.g. LDAP

So in alfresco-global.properties add to the authentication chain:
authentication.chain=cas:external,localDrupal:drupal

While you’re there set up some database connection properties e.g.

drupal.db.driver=org.gjt.mm.mysql.Driver
drupal.db.username=drupal
drupal.db.password=drupal
drupal.db.url=jdbc:mysql://localhost:3306/drupal6

I’m not going to cover ensuring that you have access to the mysql database here. If it’s on a different machine you’ll need to configure MySQL as well as any firewall rules.

Next it’s time to tell Alfresco about the new component that you are creating so create the directory alfresco/subsystems/Authentication/drupal and add the following files:

drupal-authentication-context.xml

<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
xmlns="http://www.springframework.org/schema/beans" 
xsi:schemalocation="http://www.springframework.org/schema/beans  
  http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

    <bean class="org.apache.commons.dbcp.BasicDataSource" 
                       destroy-method="close" id="drupalDataSource">
       <property name="driverClassName" value="${drupal.db.driver}"/>
        <property name="url" value="${drupal.db.url}"/>
        <property name="username" value="${drupal.db.username}"/>
        <property name="password" value="${drupal.db.password}"/>
        <property name="validationQuery" value="SELECT 1"/>
        <property name="testOnBorrow" value="true"/>
        <property name="defaultAutoCommit" value="false"/>
        <property name="maxWait" value="5000"/>
    </bean>

    <bean class="mypackage.authentication.DrupalAuthenticationComponentImpl" 
id="drupalAuthenticationComponent" parent="authenticationComponentBase"> 
<property name="dataSource" ref="drupalDataSource"/>
        <property name="nodeService">
            <ref bean="nodeService"/>
        </property>
        <property name="personService">
            <ref bean="personService"/>
        </property>
        <property name="transactionService">
            <ref bean="transactionService"/>
        </property>
</bean>

      <!-- Wrapped Drupal authentication component to be used within subsystem -->
    <bean id="AuthenticationComponent">
        <property name="proxyInterfaces">
            <value>org.alfresco.repo.security.authentication.AuthenticationComponent</value>
        </property>
        <property name="transactionManager">
            <ref bean="transactionManager"/>
        </property>
        <property name="target">
            <ref bean="drupalAuthenticationComponent"/>
        </property>
        <property name="transactionAttributes">
            <props>
                <prop key="*">${server.transaction.mode.default}</prop>
            </props>
        </property>
    </bean>
     <!-- Authentication service for authentication component chaining

     Note. the id of this bean must be 'localAuthenticationService' for it to
     be picked up, as an authentication service, by the
     Subsystem Chaining Authentication Service
    -->
    <bean id="localAuthenticationService">
        <property name="ticketComponent">
            <ref bean="ticketComponent"/>
        </property>
        <property name="authenticationComponent">
            <ref bean="drupalAuthenticationComponent"/>
        </property>
        <property name="sysAdminParams">
            <ref bean="sysAdminParams"/>
        </property>
    </bean>
</beans>

 

drupal-authentication.properties

drupal.db.driver=org.gjt.mm.mysql.Driver
drupal.db.username=drupal
drupal.db.password=drupal
drupal.db.url=jdbc:mysql://localhost:3306/drupal6

 

Then create the bean that is referenced from the config

package mypackage.cms.authentication;

import org.alfresco.repo.management.subsystems.ActivateableBean;
import org.alfresco.repo.security.authentication.AbstractAuthenticationComponent;
import org.alfresco.repo.security.authentication.AuthenticationException;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;

import javax.sql.DataSource;
import java.security.MessageDigest;

/**
 * Authenticates a user by Drupal.
 * <p/>
 * The authentication is done against the database table USERS and the
 * column NAME (username) and column PASS (MD5 password)
 * <p/>
 * Tested with Drupal 6
 *
 * @author martin.bergljung@ixxus.co.uk
 */
public class DrupalAuthenticationComponentImpl 
extends AbstractAuthenticationComponent implements ActivateableBean {
    public static final String GET_USER_PWD_SQL = "SELECT pass FROM users WHERE name=?";

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

    /**
     * Spring JDBC template used to query or update a JDBC data source
     */
    private JdbcTemplate m_jdbcTemplate;

    /**
     * Is this bean active? I.e. should this part of the subsystem be used?
     */
    private boolean m_active = true;

    public DrupalAuthenticationComponentImpl() {
        super();
    }

    /**
     * Controls whether this bean is active. I.e. should this part of the subsystem be used?
     *
     * @param active <code>true</code> if this bean is active
     */
    public void setActive(boolean active) {
        m_active = active;
    }

    /**
     * Dependeny Injects the data source to be used for querying Drupal database
     *
     * @param dataSource the data source to use
     */
    public void setDataSource(DataSource dataSource) {
        m_jdbcTemplate = new JdbcTemplate(dataSource);
    }

    /*
    * (non-Javadoc)
    * @see org.alfresco.repo.management.subsystems.ActivateableBean#isActive()
    */
    public boolean isActive() {
        return m_active;
    }

    /**
     * Authenticate against the Drupal database
     *
     * @param userName the username to authenticate
     * @param password the password to authenticate (passed in as plain text)
     * @throws AuthenticationException if authentication failed
     */
    @Override
    protected void authenticateImpl(String userName, char[] password) throws AuthenticationException {
        String userPwd = new String(password);

        // Generate an MD5 hash for the password as that is what we get back from Drupal
        // Get the value as hex
        String userPwdMd5 = DigestUtils.md5Hex(userPwd);

        if (logger.isDebugEnabled()) {
            logger.debug("About to authenticate user: " + userName + " with MD5 password: " + userPwdMd5);
        }

        try {
            String drupalPwdMd5 = m_jdbcTemplate.queryForObject(GET_USER_PWD_SQL, new Object[]{userName}, String.class);

            if (logger.isDebugEnabled()) {
                logger.debug("Got MD5 password from Drupal database: " + drupalPwdMd5);
            }

            if (StringUtils.isNotBlank(drupalPwdMd5)) {
                if (MessageDigest.isEqual(userPwdMd5.getBytes(), drupalPwdMd5.getBytes())) {
                    // Authentication has been successful.
                    // Set the current user, they are now authenticated.
                    setCurrentUser(userName);
                } else {
                    throw new AuthenticationException("Access denied for user: " + userName +
                            ", incorrect password provided.");
                }
            } else {
                throw new AuthenticationException(
                        "Password in Drupal database is blank, empty, or null for user: " + userName);
            }
        } catch (DataAccessException dae) {
            throw new AuthenticationException(
                    "Error getting password from Drupal database for user: " + userName +
                            ", user may not exist in the Drupal database", dae);
        }
    }

    @Override
    protected boolean implementationAllowsGuestLogin() {
        return true;
    }
}