One of the reasons I like Alfresco is that it’s opensource and as every opensource software you can get the source code, inspect it and learn how things work. Another reason is that Alfresco can be highly customized, being it based on the Spring framework; thanks to the IoC principle, Spring makes it very simple to replace components in Alfresco.
To demonstrate how simple this could be in practice, I’ll explain how to override the login webscript so that users could authenticate themeselves using a custom authentication component and, in case this fails, using the Alfresco default one. This kind of configuration is known as “authentication chain”.
The authentication system in Alfresco has gone under a major reorganization which has made the instructions that follow outdated for Alfresco Community 3.2 (and outcoming Alfresco Enterprise). Nonetheless, what’s written here is still valid for Alfresco Enterprise 3.1.1 and Alfresco 3Labs final and could be considered as a helpful example for those who want to start learning how Alfresco plays with Spring. Maybe in a future post I’ll write about how this could be done for release 3.2.
Since this post is quite long, if you are impatient you can download the source code and defer explanations. Package is tailored to Alfresco release 3Labs final but code will work for Alfresco Enterprise 3.1.1 as well. Edit build.properties and fix the path to $TOMCAT_HOME. Run ant deploy to build, package and deploy.
0. Summary
This is a brief summary of the steps we’ll take:
- understand how the login bean works and which components are implied
- understand how authentication components work and track the components’ dependencies down to the smallest component we can replace
- create a custom authentication component
- redefine necessary beans (login webscript and other components)
- deploy and test
1. How the login bean works
There’re two login webscripts in the Alfresco repository; the one I’m considering here is that whose classpath is alfresco/templates/webscripts/org/alfresco/repository/login.post.desc.xml. This is a java-backed webscript and is defined in web-scripts-application-context.xml under $TOMCAT_HOME/webapps/alfresco/WEB-INF/classes/alfresco. If you look at its source code, you’ll see that it delegates the authentication to the authenticationService instance (see AbstractLoginBean.java).
2. How authentication works
Which component is actually injected as the authenticationService instance depends on how the login webscript bean is declared. There’s a small difference here between the Enterprise and the Labs versions: in the former authenticationService references the bean called AuthenticationService which is a proxy object that targets the authenticationService bean (see how Spring realizes the AOP paradigm); in the Labs version a reference to the authenticationService bean is directly injected in the login bean. Anyway, we are interested in the authenticationService bean.
Its definition is found in authentication-services-context.xml under $TOMCAT_HOME/webapps/alfresco/WEB-INF/classes/alfresco and looks like this:
<bean id="authenticationService" class="org.alfresco.repo.security.authentication.AuthenticationServiceImpl"> <property name="authenticationDao"> <ref bean="authenticationDao" /> </property> <property name="ticketComponent"> <ref bean="ticketComponent" /> </property> <property name="authenticationComponent"> <ref bean="authenticationComponent" /> </property> <property name="sysAdminCache"> <ref bean="sysAdminCache"/> </property> </bean>
Alfresco documentation and source code come into handy. The authenticationService bean is actually an instance of the AuthenticationServiceImpl class. This one uses the authenticationDao object to create, update and retrieve authentication information (such as the MD4 checksum of the password). The ticketComponent is responsible for issuing and renewing tickets. The user-password authentication is delegated to the authenticationComponent; by default this bean is an instance of AuthenticationComponentImpl which implements the AuthenticationComponent interface (this also can be inferred from authentication-services-context.xml).
Following the same reasoning it turns out that authenticationComponent depends on authenticationManager which is an instance of net.sf.acegisecurity.providers.ProviderManager. This class uses a list of authentication provider objects to validate the credentials. The logic is as follows: every provider is tested in turn in the order they are declared; if one is found to accept the credentials then authentication succeeds.
3. Create our own authentication component
Now that we’ve got the bottom component in the delegation chain of beans, we need to create our custom authentication provider and insert it into the list of providers used by the provider manager. In order to achieve this we have two chances:
- override just the authenticationManager bean, or
- redefine every bean involved in the afore paragraphs, starting from the authentication manager up to the authentication service and the login webscript
In the first case we’ll force the overriding authentication manager to be used whenever the authenticationManager bean is referenced (that is every authentication request). For the purpose of this example we’ll pick the second choice so that changes will only affect those beans that use the new authentication service (in this case the login webscript bean).
We start defining a CustomAuthenticationProvider provider class:
public class CustomAuthenticationProvider implements AuthenticationProvider { private MutableAuthenticationDao authenticationDao; @Override public Authentication authenticate(Authentication auth) throws AuthenticationException { if (!supports(auth.getClass())) return null; UsernamePasswordAuthenticationToken upat = (UsernamePasswordAuthenticationToken) auth; String username = (String) upat.getPrincipal(); if (authenticationDao.getAccountHasExpired(username) || authenticationDao.getAccountlocked(username) || !authenticationDao.getEnabled(username) || !username.equals((String) upat.getCredentials())) return null; return auth; }
Providers must implement the authenticate method of the AuthenticationProvider interface; this method must return a non-null value if credentials are acceptable and false otherwise. We referenced the authenticationDao object to check that user is not locked or disabled and that their account is not expired. Authentication logic is trivial on purpose: credentials are valid if username equals the password. In a real case scenario we would rather ask the dao component to retrieve the salt and the password hash and then compare the latter with the hash of the input password (combined in some way with the salt), but for this example this will do.
4. Use the provider
We create a new file named my-auth-component.xml under $TOMCAT_HOME/shared/classes/alfresco/extension. These are the most important beans that must be declared (refer to the source code for the others):
<bean id="CustomAuthenticationService" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="proxyInterfaces"> <value> org.alfresco.service.cmr.security.AuthenticationService </value> </property> <property name="target"> <ref bean="customAuthenticationService" /> </property> <property name="interceptorNames"> <list> <idref local="AuthenticationService_transaction" /> <idref local="AuditMethodInterceptor" /> <idref local="exceptionTranslator" /> <idref bean="AuthenticationService_security" /> </list> </property> </bean> <bean id="customAuthenticationService" class="org.alfresco.repo.security.authentication.AuthenticationServiceImpl"> <property name="authenticationDao"> <ref bean="authenticationDao" /> </property> <property name="ticketComponent"> <ref bean="ticketComponent" /> </property> <property name="authenticationComponent"> <ref bean="customAuthenticationComponent" /> </property> <property name="sysAdminCache"> <ref bean="sysAdminCache" /> </property> </bean> <bean id="customAuthenticationComponent" class="org.alfresco.repo.security.authentication.AuthenticationComponentImpl" parent="authenticationComponent"> <property name="authenticationManager"> <ref bean="customAuthenticationManager" /> </property> </bean> <bean id="customAuthenticationManager" class="net.sf.acegisecurity.providers.ProviderManager"> <property name="providers"> <list> <ref bean="authenticatedAuthenticationPassthroughProvider" /> <ref bean="customAuthenticationProvider" /> <ref bean="daoAuthenticationProvider" /> </list> </property> </bean> <bean id="customAuthenticationProvider" class="eu.fabiostrozzi.auth.CustomAuthenticationProvider"> <property name="authenticationDao"> <ref bean="authenticationDao" /> </property> </bean>
Finally, we override the login webscript bean in $TOMCAT_HOME/shared/classes/alfresco/extension/custom-web-context.xml:
<?xml version='1.0' encoding='UTF-8'?> <!DOCTYPE beans PUBLIC '-//SPRING//DTD BEAN 2.0//EN' 'http://www.springframework.org/dtd/spring-beans-2.0.dtd'> <beans> <bean id="webscript.org.alfresco.repository.login.post" class="org.alfresco.repo.web.scripts.bean.LoginPost" parent="webscript"> <property name="authenticationService" ref="CustomAuthenticationService" /> </bean> </beans>
Instead of referencing authenticationService we now reference CustomAuthenticationService which has been declared in my-auth-component.xml.
5. Deploy and test
Download the source code and deploy the customizations (see afore instructions). Restart Alfresco and create a new user account (say johndoe) with a password different from the username (say password). Now open a terminal and use curl to call the login webscript, be sure to use the username as password:
echo '{username:"johndoe", password:"johndoe"}' | curl -d@- -H'Content-type: application/json' -XPOST http://localhost:8080/alfresco/s/api/login
If authentication worked correctly the response should be a json message containing the Alfresco ticket:
{ data: { ticket:TICKET_3522058be33d5bf50b1b7eee3a3df7eafcd3f112 } }
Aknowledgments
Thanks to Mauro Strozzi who supervised translation.
Ciao,
ho seguito le tue indicazioni per modificare il mio alfresco, premetto che non sono un programmatore java, ma devo fare una cosa simile alla tua.
A meno della parte relativa al curl(che pur avendolo installato non parte) ho fatto tutto come illustravi tu e ho usato net bean.
Il problema è che quando lancio il run mi viene fuori la index.jsp con “hello world”! come arrivare al risultato della nuova login?
ti ringrazio in anticipo.
luigi.
Ciao Luigi,
che versione di Alfresco stai usando?
Se stai usando una versione successiva alla 3.2 ti consiglio di aspettare qualche giorno (se puoi) perché sto scrivendo un articolo sullo stesso argomento apposta per le nuove versioni di Alfresco (che fanno uso dei subsystem).
Ciao
Ciao Fabio, l’articolo per le versioni successive di alfresco lo hai poi scritto ??
😆
Ciao, purtroppo no, non ho avuto tempo 🙂
Però puoi capire come customizzare il webscript del login prendendo spunto da questo articolo per l’idea generale e dall’articolo sulla preview dei tiff per la parte relativa ai subsystems.
Srivimi pure se serve aiuto.
Ciao e grazie per avermi sritto
Ciao
Grazie per l’articolo mi è stato molto utile! 🙂
Vorrei porti una domanda se hai un attimo di tempo da dedicarmi. Ho la necessità di leggere degli space ogni volta che un utente effettua il login su alfresco. Puoi aiutarmi?
Ciao e grazie.
Ciao, ci posso provare.
Cosa devi fare di preciso?
Ciao
Ciao! Grazie ancora per la tua disponibilità! 😛
Mah! guarda avrei la necessità che ad ogni login di un utente in alfresco venga eseguita la lettura di un nodo specifico presente all’interno della stessa userHome dell’utente che si è appena loggato. Questo nodo è in realtà un semplice file di properties dove sono elencati alcuni collegamenti che, se non presenti sempre nella userhome dell’utente devono essere creati. Ora io non ho avuto nessun problema a creare un piccolo client che prende gli utenti alfresco, ne legge il file nella userhome e poi crea o non crea tali collegamenti. Il punto è che avrei la necessità di farlo girare ad ogni login dell’utente su quell’utente. Non so se mi sono espresso abbastanza chiaramente?!? 😯 – Cmq ti chiedevo la cortesia se avevi qualche idea per il meccanismo della login. Lavoro su alfresco 3.1
Grazie mille
Ciao
Ciao, potresti provare una cosa di questo tipo:
1. crei una custom action (o uno script javascript) che esegua il codice che vorrai far eseguire all’utente in fase di login.
2. come nel mio articolo, ti scrivi un custom AuthenticationProvider: questo provider punterà a un altro provider al quale demanderà l’autenticazione e, cosa più importante, si salverà l’esito.
3. in caso di esito positivo (utente autenticato), il tuo provider eseguirà la custom action (o lo script) in maniera asincrona.
Questo approccio non è pulitissimo e potresti perfino incontrare problemi per quanto riguarda l’esecuzione della tua action per via dei privilegi con cui verrebbe eseguita (immagino siano quelli dell’utente che si sta autenticando…). Al momento una soluzione migliore non l’ho però questa non mi sembra così difficile e proverei a percorrerla (magari valuta se non sia meglio implementare un authentication manager piuttosto che il provider).
Eventualmente prova a sentire anche sul forum di Alfresco.
Ciao
Ciao!
Grazie ancora per la tua disponibilità!
Si ho chiesto anche sul forum di alfresco e la loro idea è stata quella di modificare il metodo authenticate 😕 – proverò senz’altro prima la tua soluzione, la quale mi sembra meno invasiva… Grazie ancora riscriverò per farti sapere come è andata 😛
My alfresco don´t debug… :S
My alfresco call custom-web-context.xml when I start only. I used Alfresco is 3.0
My alfresco call custom-web-context.xml when I start only. I need other configuration? have Alfresco 3.0
hello, i like to test version alfresco community 4.2, have i to change anything in your code?
regards