Accueil > Non classé > SAML 2 et Liferay – partie 5

SAML 2 et Liferay – partie 5

Dans le précédent épisode article, nous avions presque terminé notre mécanisme d’authentification unique SAML 2.0, nous allons maintenant assembler les pièces du puzzle et faire quelque finitions…

Une dernière touche de peinture

Si nous reprenons notre descriptif de l’échange (non, ne me tapez pas ! c’est la dernière fois (pour ce schéma en tous cas)) :

Diagramme de séquence du SSO en SAML

Nous en sommes à la dernière étape, le 5), nous avons presque terminé, une fois les données récupérées de la réponse en xml, il nous suffit de faire retourner l’utilisateur à connecter dans notre méthode login de SAMLAutoLogin. Une fois l’utilisateur authentifié par Liferay (ou éventuellement créé à partir des données récupérées), nous redirigerons sur la ressource initialement demandée (grâce au RelayState), le système de droit va faire le reste du travail, c’est-à-dire accorder ou refuser l’accès à cette ressource.

Tout cela est très bien, mais en réalité, le standard SAML décrit un ensemble de règles qu’il va falloir vérifier pour nous assurer que la réponse est correcte. Ces règles garantissent la cohérence et la validité du message. Par exemple, outre la correctitude de la signature, le message doit être adressé en réponse à notre requête (l’identifiant de la requête est indiqué), les assertions doivent être reçues avant la fin de leur validité (indiquée par un attribut date). Nous allons nous intéresser plus particulièrement à deux d’entre elles (les autres sont trop simples pour être amusantes à réaliser…).

La première concerne la validité de la session, en effet l’élément <AuthStatement> contient un attribut SessionNotOnOrAfter, le standard nous impose de terminer la session si cette date est dépassée, afin de nous garantir la fin de la session, nous allons conserver cette date dans celle-ci et ajouter un filtre de servlet :


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
package com.liferay.portal.servlet.filters.sso.saml;

import com.liferay.portal.kernel.log.Log;
import com.liferay.portal.kernel.log.LogFactoryUtil;
import com.liferay.portal.kernel.util.PropsKeys;
import com.liferay.portal.servlet.filters.BasePortalFilter;
import com.liferay.portal.util.PortalUtil;
import com.liferay.portal.util.PrefsPropsUtil;
import com.liferay.portal.util.PropsValues;

import java.util.Date;

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

/**
 * <p>
 * See http://issues.liferay.com/browse/LPS-8427.
 * </p>
 *
 * @author Denis Vaumoron
 */

public class SAMLSessionFilter extends BasePortalFilter {

        @Override
        protected void processFilter(
                        HttpServletRequest request, HttpServletResponse response,
                        FilterChain filterChain)
                throws Exception {

                try {
                        long companyId = PortalUtil.getCompanyId(request);

                        if (PrefsPropsUtil.getBoolean(
                                companyId, PropsKeys.SAML_AUTH_ENABLED,
                                PropsValues.SAML_AUTH_ENABLED)) {

                                HttpSession session = request.getSession();
                                Date sessionNotAfter = (Date) session.getAttribute(
                                        SAMLConstants.SESSION_NOT_AFTER);

                                if (sessionNotAfter != null) {
                                        if (_log.isDebugEnabled()) {
                                                _log.debug(
                                                        "session " + session.getId()
                                                        + " not after : "
                                                        + sessionNotAfter);
                                        }

                                        if (sessionNotAfter.before(new Date())) {
                                                if (_log.isDebugEnabled()) {
                                                        _log.debug(
                                                                "session not after"
                                                                + " passed : invalidated");
                                                }
                                                session.invalidate();
                                        }
                                }
                        }
                } catch (Exception e) {
                        _log.error(e, e);
                }

                processFilter(
                        SAMLSessionFilter.class, request, response, filterChain);
        }

        private static Log _log =
                LogFactoryUtil.getLog(SAMLSessionFilter.class);

}

La deuxième règle consiste à vérifier la non répétition des messages reçus, pour cela il nous faut stocker les identifiants au moins jusqu’à leur fin de validité. L’outil fourni par Liferay qui va nous faciliter la tâche est le « service builder ». Pour l’utiliser, il faut écrire un fichier service.xml, ou dans notre cas rajouter une entité à un fichier préexistant, situé dans le package com.liferay.portal :


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<entity  name="Assertion" local-service="true" remote-service="true">

        <!-- PK fields -->

        <column name="assertionId" type="long" primary="true" />

        <!-- Other fields -->

        <column name="generatedKey" type="String" />
        <column name="notOnOrAfter" type="Date" />

        <!-- Finder methods -->

        <finder name="GeneratedKey" return-type="Assertion" unique="true">
                <finder-column name="generatedKey" />
        </finder>
</entity>

Une fois le fichier modifié, la génération des classes (ou leur mise à jour) est lancée par la tâche Ant build-service-portal du build.xml de portal-impl. Par la suite, nous complétons la classe générée AssertionLocalServiceImpl :


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package com.liferay.portal.service.impl;

import com.liferay.portal.NoSuchAssertionException;
import com.liferay.portal.kernel.exception.SystemException;
import com.liferay.portal.model.Assertion;
import com.liferay.portal.service.base.AssertionLocalServiceBaseImpl;

import java.util.Date;

/**
 * The implementation of the assertion local service.
 *
 * <p>
 * All custom service methods should be put in this class. Whenever methods are added, rerun ServiceBuilder to copy their definitions into the {@link com.liferay.portal.service.AssertionLocalService} interface.
 * </p>
 *
 * <p>
 * Never reference this interface directly. Always use {@link com.liferay.portal.service.AssertionLocalServiceUtil} to access the assertion local service.
 * </p>
 *
 * <p>
 * This is a local service. Methods of this service will not have security checks based on the propagated JAAS credentials because this service can only be accessed from within the same VM.
 * </p>
 *
 * @author Brian Wing Shun Chan
 * @see com.liferay.portal.service.base.AssertionLocalServiceBaseImpl
 * @see com.liferay.portal.service.AssertionLocalServiceUtil
 */

public class AssertionLocalServiceImpl extends AssertionLocalServiceBaseImpl {

        public void cleanDeprecatedAssertion(Date now) throws SystemException {
                assertionFinder.cleanDeprecatedAssertion(now);
        }

        public boolean replayedAssertion(String generatedKey, Date notOnOrAfter)
                throws SystemException {

                try {
                        assertionPersistence.findByGeneratedKey(generatedKey);

                        return true;
                }
                catch (NoSuchAssertionException nsae) {
                        long assertionId = counterLocalService.increment();

                        Assertion assertion = assertionPersistence.create(assertionId);

                        assertion.setGeneratedKey(generatedKey);
                        assertion.setNotOnOrAfter(notOnOrAfter);

                        assertionPersistence.update(assertion, false);

                        return false;
                }
        }

}

Et nous créons la classe AssertionFinderImpl :


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package com.liferay.portal.service.persistence;

import com.liferay.portal.kernel.dao.orm.QueryPos;
import com.liferay.portal.kernel.dao.orm.SQLQuery;
import com.liferay.portal.kernel.dao.orm.Session;
import com.liferay.portal.kernel.exception.SystemException;
import com.liferay.portal.kernel.util.CalendarUtil;
import com.liferay.portal.model.Assertion;
import com.liferay.portal.service.persistence.impl.BasePersistenceImpl;
import com.liferay.util.dao.orm.CustomSQLUtil;

import java.sql.Timestamp;

import java.util.Date;

/**
 * @author Denis Vaumoron
 */

public class AssertionFinderImpl
        extends BasePersistenceImpl implements AssertionFinder {

        public static String CLEAN_DEPRECATED_ASSERTION =
                AssertionFinder.class.getName() + ".cleanDeprecatedAssertion";

        public void cleanDeprecatedAssertion(Date now) throws SystemException {

                Timestamp now_TS = CalendarUtil.getTimestamp(now);

                Session session = null;

                try {
                        session = openSession();

                        String sql =
                                CustomSQLUtil.get(CLEAN_DEPRECATED_ASSERTION);

                        SQLQuery q = session.createSQLQuery(sql);

                        QueryPos qPos = QueryPos.getInstance(q);

                        qPos.add(now_TS);

                        q.executeUpdate();
                }
                catch (Exception e) {
                        throw new SystemException(e);
                }
                finally {
                        closeSession(session);
                }
        }

}

Pour être plus efficace, cela fait appel à une requête SQL enregistrée dans custom-sql/portal.xml :


1
2
3
4
5
6
7
8
9
<sql id="com.liferay.portal.service.persistence.AssertionFinder.cleanDeprecatedAssertion">
        <![CDATA[
               DELETE
               FROM
                       Assertion
               WHERE
                       (Assertion.notOnOrAfter < ?)
       ]]>
</sql>

Pour finir, un petit coup de baguette magique relancez le service builder, et vous pouvez utiliser les méthodes cleanDeprecatedAssertion et replayedAssertion de AssertionLocalServiceUtil (le service builder s’occupe de tout le reste, configuration de Spring, d’Hibernate, etc.).

Conclusion

Nous voilà déjà enfin arrivés à la fin de cette première saison partie de cette série d’article, l’authentification unique fonctionne, j’ai essayé de mettre en lumière les principales étapes pour y parvenir, si toutefois des zones d’ombre subsistent, le fichier patch contenant les sources est joint à la LPS-8427 (mais vous risquez le spoil des prochains épisodes de la série), et surtout n’hésitez pas à poser des questions dans les commentaires (non, ce n’est pas un piège, je ne suis pas payé au commentaire). Pour finir, je vous rappelle que la prochaine partie concernera la déconnexion centralisée, aussi appelée Single Logout.

A bientôt pour de nouvelles aventures sur le blog Excilys…

 

Share
  1. Alberto Rama
    12/07/2012 à 00:21 | #1

    Dennis,

    I really appreciate your sharing all these technical details on Liferay authentication. I am trying to put together a proof of concept using Liferay 6.1 CE GA1 as SP using a SimpleSAML server as the IdP. I followed your blog and tried to apply the patch you referred to (http://issues.liferay.com/browse/LPS-8427), but it fails with same error reported by another user. I see at the end a comment that says “Duplicated ticket…EE only features”, can you confirm that this patch is only applicable to EE? Is there a quick way to make the CE a SAML 2.0 SP?

    Thanks,
    Alberto

  2. Charles
    19/09/2012 à 09:11 | #2

    Comme Alberto, je me pose la question de l’intégration dans la version 6.1 CE de Liferay.
    N’ayant travaillé que sur version CE, j’ai du mal à voir les impacts et les éventuels points de blocage …

    Merci d’avance !

  3. Denis VAUMORON
    03/10/2012 à 15:03 | #3

    Le patch LPS-8427-build-70637.patch est prévu pour la version communautaire révision svn 70637, il permet de faire de Liferay un fournisseur de service. Mika Koivisto a de son coté travaillé pour faire de Liferay un fournisseur d’identité. Il a ensuite été décidé de n’integrer cela que pour la version entreprise.

  4. 21/08/2014 à 17:01 | #4

    That’s really thkniing at a high level

  5. sysoncloud
    10/06/2016 à 09:08 | #5

    hi Denis,
    Can you tell me where is assertionFinder defined, I cannot found this java file.
    Thanks

  6. Brucenut
    25/06/2018 à 14:15 | #6

    Хорошего дня.

  1. 24/07/2015 à 16:58 | #1
  2. 28/07/2015 à 10:32 | #2
  3. 02/06/2016 à 03:55 | #3