March 13th, 2013

Nick - Authenticating to Alfresco Share using Apache Auth (eg for SSO)

This post carries on from my earlier one on Alfresco Repo SSO, and builds on the information on how the Alfresco WebScript authentication methods work.

At this point, we can hopefully log into the Alfresco repository (Explorer or /alfresco/wcservice) automatically by authenticating to Apache. Our next step, before moving onto SSO in Share, is to also allow header based authentication to Alfresco. We need to do this because of how Share will talk to the Repo - it will know the user's username from Apache, but it won't have their password (and equally won't have their client cert if doing SSL auth). Share needs to be able to tell the repo who you are, and have that trusted. For that, we need to tweak our External Authentication SubSystem.

Before doing that, be aware that by turning on header based auth, anyone who can make http requests to the repo could add in that header and impersonate anyone, so take great care! You should do one of use firewall / auth rules it so that only Share can, use something like an Apache module to strip out the header except from Share, or use SSL + client auth + proxyUserName.

Currently, our file looks like:
To support header based auth, we add two more lines, giving us:

If you don't specify a proxyHeader, the default is X-Alfresco-Remote-User (that comes from here).

With that in place, from a machine that is allowed to talk to Alfresco with the header, try executing something like:
    $ curl -X GET -L -H "X-My-Custom-SSO-Header: nick" http://localhost:8080/alfresco/ | less
Take a look through the HTML, and check you see something like Logout (nick) and not Login (guest)

Now, we can turn to Share. There is information on this in the wiki and the Enterprise docs, but they're a bit short on explanation to understand why you need to do things.

First, two things we can safely ignore now. One of those is the KeyStore entry. That's used if you want Share to talk to the Repo over SSL with client Auth. You may well want to turn that on for production, but not for getting started. (If you do, you should set the SSL client cert username as your proxyUserName, then the Repo will only check for the proxyHeader HTTP Header for the username for requests from that user). Secondly, we're going to be using a HTTP Header to carry the username, so we don't need the alfrescoCookie connector.

If you don't have one already, create a alfresco/web-extension/share-config-custom.xml file, typically either in a module, or in the Tomcat shared classes. If you're creating a fresh one, use share-config-custom.xml.sample as a basis (it has nearly the config in it you need, along with other bits you won't want now so remove / comment them out). The key bit for us is to add/modify a Remote config section. This is where you define how Share talks to other systems over http/https, especially to the repository (via the alfresco endpoint). Define a Remote section as follows:
   <config evaluator="string-compare" condition="Remote">
               <name>Alfresco Connector</name>
               <description>Connects to an Alfresco instance using header and cookie-based authentication</description>

                <name>Alfresco - user access</name>
                <description>Access to Alfresco Repository WebScripts that require user authentication</description>

With that in place, if you go to Share in your browser via the Apache connection - http://local-alfresco/share/ if you followed part 1 - you get an Apache basic auth prompt, then you should be automatically logged into Share with the user details you gave to Alfresco. SSO done!

One other thing to try before we understand how it works (and you lock down the security!), we'll try curl again:
$ curl -X GET -L -H "X-My-Custom-SSO-Header: nick" http://localhost:8080/share/ | less
Look for something near the top showing Alfresco.constants.USERNAME = "nick" and not guest to confirm header based auth worked for Share.

So, what's going on, and how does it all work? At this point, you'll probably want to grab the Spring Surf source code if you want to dig around, as most of the logic isn't in Share itself, but the underlying Surf framework.

Looking at the config, there are a few things to note. In the alfresco endpoint, we have <endpoint-url>http://localhost:8080/alfresco/wcs</endpoint-url> - note that it has changed to the WCServices URL from the regular Services one. As seen in the how the Alfresco WebScript authentication methods work blog post, this is so that we can use the Authentication subsystems via the filters to authenticate, while services just does basic auth + tickets. The next thing is <external-auth>true</external-auth>, which tells Share that we're using External Authentication and it should hide a few bits. AuthenticationUtil exposes this via isExternalAuthentication, and a few bits of Share use that. Finally, the connector-id ties us back to the Header and some Java.

In the connector section of the config, the route taken is that Share finds an endpoint with the id alfresco, then uses that to find the connector to use by the connector-id defined there. For us, that's our new alfrescoHeader one. That includes <userHeader>X-My-Custom-SSO-Header</userHeader> which is the HTTP Header used by Share and the Repo to pass along the username to use. The other thing in there is the class that drives it all, SlingshotAlfrescoConnector.

At this point, I should perhaps point out a gotcha. The underlying External Authentication SubSystem supports external.authentication.userIdPattern to only use part of the Header as the username, but due to ALF-18393 Share doesn't and it all goes a bit wrong... The option isn't (currently!) Share Compatible

Looking some more at SlingshotAlfrescoConnector which we defined above, there's one key method in there, applyRequestHeaders. This is the part where Share is tweaking the headers on any connections it makes to the Repo. In it, it checks to see if you authenticated without a password (i.e. with SSO). If you have, your username is sent to the repo in the header X-Alfresco-Remote-User and if you set a config entry userHeader then with that too as well. There's also setConnectorSession which feeds your custom header down the stack.

So, if you want to send some extra headers to the Repo to prove to it you are really Share, and you don't want to do it with SSL client certs (for which there is an example in share-config-custom.xml.sample), then overriding SlingshotAlfrescoConnector + applyRequestHeaders + specifying that class instead is likely the way to go.

Where does the username come from to pass along? That's within Spring Surf, in AbstractUserFactory inside initialiseUser

The final bit of magic is explaining how specifying our magic header gets passed through Share to the Repo. The solution to that is to be found in SSOAuthenticationFilter. As well as handling Kerberos and NTML challenge stuff, it also does a bit of magic in the case of custom username headers. Specifically, in wrapHeaderAuthenticatedRequest it makes the value of the custom header available as the result of getRemoteUser(), which allows the rest of Surf to know who you are.

Phew, and we're done! You should now have a working Alfresco + Share SSO setup, which can work either through Apache with Apache handling the Authentication, or direct to Tomcat with special headers. We (hopefully) understand how it works, why, and have a testbed. At this point, we can tweak it for our final SSO setup, put in any security around it we need, and go live!