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

SAML 2 et Liferay – partie 6

Dans la première saison partie de cette série d’article, nous avons vu comment intégrer l’authentification unique SAML 2 dans Liferay, nous allons à présent nous intéresser à la déconnexion centralisé, autrement dit le “Single Logout Profile”.

La déconnexion centralisé se déroule en cinq étapes :

1. L’un des fournisseurs de service demande au fournisseur d’identité de mettre fin à la session

2. Le fournisseur d’identité identifie tous les autres fournisseurs de service pour lequel il existe une session

3. Le fournisseur d’identité demande à l’un des fournisseurs de service de mettre fin à la session

4. Le  fournisseur de service met fin à sa session locale et répond au fournisseur d’identité

Les étapes 3 et 4 sont répété pour tous les fournisseurs de services identifié à l’étape 2

5. Le fournisseur d’identité répond au premier fournisseur de service et celui ci met fin à sa session locale

On peut donc identifier deux cas, soit nous sommes à l’origine de la demande de déconnexion, soit nous recevons du fournisseur d’identité une demande de déconnexion.

Pour le premier cas nous allons utiliser la propriété logout.events.pre du portal-ext.properties pour intercepter la demande de déconnexion de l’utilisateur et envoyer au fournisseur d’identité la demande de déconnexion. Pour cela on crée la classe SAMLLogoutAction qui étend la classe com.liferay.portal.kernel.events.Action. Dans cette classe on génère le message de déconnexion <logoutRequest> et on empêche l’invalidation de la session :


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
HttpSession session = request.getSession();

String logoutUrl = SAMLMetadataManager.getIDPSingleLogoutLocation();
String samlEntityId = PrefsPropsUtil.getString(
                companyId, PropsKeys.SAML_ENTITY_ID,
                PropsValues.SAML_ENTITY_ID);
String signAlg = PrefsPropsUtil.getString(
                companyId, PropsKeys.SAML_REDIRECT_SIGN_ALG,
                PropsValues.SAML_REDIRECT_SIGN_ALG);

Document document = SAXReaderUtil.createDocument();

Namespace samlp = SAXReaderUtil.createNamespace(SAMLConstants.SAMLP, SAMLConstants.PROTOCOL_NAMESPACE);
Namespace saml = SAXReaderUtil.createNamespace(SAMLConstants.SAML, SAMLConstants.ASSERTION_NAMESPACE);

Element logoutRequest = document.addElement(SAXReaderUtil.createQName(SAMLConstants.LOGOUT_REQUEST, samlp));

logoutRequest.add(samlp);
logoutRequest.add(saml);

logoutRequest.addAttribute(SAMLConstants.ID, SAMLUtil.generateMessageID());
logoutRequest.addAttribute(SAMLConstants.VERSION, SAMLConstants.VERSION_2_0);
logoutRequest.addAttribute(SAMLConstants.ISSUE_INSTANT, SAMLUtil.formatDate(new Date()));
logoutRequest.addAttribute(SAMLConstants.DESTINATION, logoutUrl);

Element issuer = logoutRequest.addElement(SAXReaderUtil.createQName(SAMLConstants.ISSUER, saml));
issuer.setText(samlEntityId);

Element nameID = (Element) session.getAttribute(SAMLConstants.NAME_ID_KEY);
if (nameID != null) {
    logoutRequest.add(nameID.createCopy());
}
Element encryptedID = (Element) session.getAttribute(SAMLConstants.ENCRYPTED_ID_KEY);
if (encryptedID != null) {
    logoutRequest.add(encryptedID.createCopy());
}

String sessionIndex = (String) session.getAttribute(SAMLConstants.SESSION_INDEX_KEY);
if (sessionIndex != null) {
    logoutRequest.addElement(
        SAXReaderUtil.createQName(SAMLConstants.SESSION_INDEX, samlp))
                .setText(sessionIndex);
}

String xmlString = document.asXML();
if (_log.isDebugEnabled()) {
    _log.debug("send : " + xmlString);
}

String relayState = SAMLUtil.getRelayStateFromLastPath(request);

SAMLUtil.initHTTPHeader(response);

response.sendRedirect(SAMLUtil.getHTTPRedirectBindingUrl(
    logoutUrl, true, xmlString, relayState, signAlg));

// invalidate session only when we receive response from IdP
session.setAttribute(WebKeys.INVALIDATE_SESSION_FROM_IDP, Boolean.TRUE);

Pour empêcher l’invalidation de la session, il faut aussi modifier com.liferay.portal.action.LogoutAction pour que la presence de INVALIDATE_SESSION_FROM_IDP soit prise en compte.


1
2
3
4
5
6
7
8
9
10
            Object invalidateFromIdp =
                session.getAttribute(WebKeys.INVALIDATE_SESSION_FROM_IDP);
            if (invalidateFromIdp == null ||
                            Boolean.FALSE.equals(invalidateFromIdp)) {
                try {
                    session.invalidate();
                }
                catch (Exception e) {
                }
            }

Il faut ensuite pouvoir recevoir la réponse du fournisseur d’identité. Pour ce faire nous allons mettre en place un filtre de servlet :


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
            String samlIdentityProvider =
                SAMLMetadataManager.getIDPEntityID();

            if (SAMLUtil.validateUrlParameterSignature(request)) {

                String samlResponseParameter =
                    request.getParameter(SAMLConstants.SAML_RESPONSE);
                if (samlResponseParameter != null) {
                    _receiveLogoutResponse(
                        request, samlIdentityProvider,
                        samlResponseParameter);
                }
            }
            else {
                _log.error("couldn't validate signature");
            }

Sans oublier de vérifier la validité de la signature :


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
    public static boolean validateUrlParameterSignature(
            HttpServletRequest request)
        throws Exception {

        String uri =
            GetterUtil.getString(request.getParameter(SAMLConstants.SIG_ALG));
        String urlSignature = GetterUtil.getString(
            request.getParameter(SAMLConstants.SIGNATURE));

        String urlParameters = GetterUtil.getString(request.getQueryString());

        // reconstructs the url's parameter correctly ordered
        // (let the url encoding)

        String saml = null;
        boolean isRequest = false;
        String relayState = null;
        String sigAlg = null;

        String[] split = urlParameters.split("[&=]");
        for(int i = 0; i < split.length; ++i) {
            String name = split[i];
            String value = split[++i];
            if (SAMLConstants.SAML_REQUEST.equals(name)) {
                saml = value;
                isRequest = true;
            }
            else if (SAMLConstants.SAML_RESPONSE.equals(name)) {
                saml = value;
            }
            else if (SAMLConstants.RELAY_STATE.equals(name)) {
                relayState = value;
            }
            else if (SAMLConstants.SIG_ALG.equals(name)) {
                sigAlg = value;
            }
        }

        StringBundler urlParamBuffer = new StringBundler(11);
        if (isRequest) {
            urlParamBuffer.append(SAMLConstants.SAML_REQUEST);
        }
        else {
            urlParamBuffer.append(SAMLConstants.SAML_RESPONSE);
        }
        urlParamBuffer.append(StringPool.EQUAL).append(saml);

        if (Validator.isNotNull(relayState)) {
            urlParamBuffer.append(StringPool.AMPERSAND)
                .append(SAMLConstants.RELAY_STATE)
                .append(StringPool.EQUAL)
                .append(relayState);
        }

        urlParamBuffer.append(StringPool.AMPERSAND)
            .append(SAMLConstants.SIG_ALG)
            .append(StringPool.EQUAL)
            .append(sigAlg);

        Certificate certificate =
            SAMLMetadataManager.getIDPSigningCertificate();

        Signature signature = Signature.getInstance(_getAlgorithmFromURI(uri));
        signature.initVerify(certificate);

        urlParameters = urlParamBuffer.toString();

        signature.update(urlParameters.getBytes(StringPool.UTF8));

        return signature.verify(Base64.decode(urlSignature));
    }

On vérifie au passage que c’est bien notre fournisseur d’identité qui nous envoie la réponse et que nous somme le bon destinataire :


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
    private void _receiveLogoutResponse(
            HttpServletRequest request, String samlIdentityProvider,
            String samlResponseParameter)
        throws DataFormatException, DocumentException,
            UnsupportedEncodingException {

        HttpSession session = request.getSession();

        String samlResponse =
            SAMLUtil.getSamlFromHTTPRedirectBindingParameter(
                samlResponseParameter);

        Document document = SAXReaderUtil.read(samlResponse);

        Element logoutResponse = document.getRootElement();

        String destination =
            logoutResponse.attributeValue(SAMLConstants.DESTINATION);

        Element issuer = logoutResponse.element(SAMLConstants.ISSUER);
        if (issuer == null) {
            _log.error("logoutResponse without issuer");
        }
        else if (!issuer.getText().equals(samlIdentityProvider)) {
            _log.error("incorrect issuer in logoutResponse");
        }
        else if (destination != null &&
                !destination.equals(
                    SAMLMetadataManager.getSPSingleLogoutLocation(request))) {
            _log.error("destination must contains the service "
                + "provider's single logout URL");
        } else {

            // we were waiting from IdP for invalidate session,
            // and it have correctly finished session on other SP

            Element statusCode =
                logoutResponse.element(SAMLConstants.STATUS)
                    .element(SAMLConstants.STATUS_CODE);
            if (SAMLConstants.URN_STATUS_SUCCESS.equals(
                    statusCode.attributeValue(SAMLConstants.VALUE)) &&
                Boolean.TRUE.equals(session.getAttribute(
                    WebKeys.INVALIDATE_SESSION_FROM_IDP))) {
                session.invalidate();
            }
        }
    }

C’est fini pour aujourd’hui

Nous avons fini le premier cas, dans le prochain épisode article, nous traiterons le deuxième cas…

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

Share
  1. 06/03/2015 à 16:47 | #1

    Une autre approche pour être sur d’éviter la faute de frappe entre le nom de la méthode et la clé de la propriété est celle d’android ou de play, la compilation de la configuration, poc est dispo là http://www.filmrally.com/

  2. Nicolas Figay
    13/06/2015 à 13:21 | #2

    Merci beaucoup sur cette série d’articles très instructive, et permettant de mieux voir comment tirer partie de SAML avec Liferay.
    Il reste un point obscur pour moi, qui concerne la manière dont l’accès aux ressources de Liferay (document, article, portlet, etc.) lorsque l’on passe par SAML. En effet, le principe de la fédération d’identité, si j’ai bien compris, est de déléguer à une organisation externe qui fourni l’IDP et gère un ensemble d’utilisateur. En retour de la demande d’authentication, on recoit des information comme quoi la personne est bien authentifiée, mais pas les informations personnelles sur l’utilisateur, juste son profil dans l’organisation à qui nous faisons confiance. Quel impact cela a t il dans la manière d’administrer le portail? Faut il travailler à partir de roles ou à des profils liferay associée à une organisation et alignés avec ceux renvoyé via les profils SAML? Et dans ce cas comment cela se gère t il?

    Bravo encore pour ces articles

  1. Pas encore de trackbacks