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

SAML 2 et Liferay – partie 2

Dans le premier épisode article, nous avons vu comment intercepter l’accès à une ressource protégée dans Liferay (ou plus précisément comment lui laisser tout le boulot grâce aux mécanisme d’autologin), aujourd’hui nous allons réagir à cette agression évènement.

Une bouteille à la mer

Si nous reprenons le descriptif de l’échange (oui, encore, et ça ne fait que commencer…) :

Diagramme de séquence du SSO en SAML


Nous en sommes au 2), maintenant nous devons envoyer une demande d’authentification au fournisseur d’identité, avant de vouloir aller trop vite en s’intéressant à la demande d’authentification, j’attire votre attention sur le fait que nous ne savons pas envoyer de message !

Comme je l’ai indiqué, plus tôt, le standard SAML utilise des « bindings » pour décrire les différents modes de transmission des messages, il en existe six, cependant le « web browser SSO profile » auquel nous nous intéressons peut être basé sur le HTTP Redirect, le HTTP Post ou le HTTP Artifact.

Le HTTP Redirect Binding, comme son nom l’indique, consiste à envoyer au navigateur le code HTTP Redirect avec une URL dont les paramètres contiendront le message encodé. En pratique, les limitations des navigateurs sur les longueurs des URLs peuvent poser des problèmes pour des messages longs, ce binding sera donc réservé à des messages courts.

Le HTTP Post Binding, consiste à répondre à la requête de l’utilisateur par une page contenant un formulaire ayant pour cible le destinataire, pour plus de transparence, la page contient généralement un script pour valider automatiquement le formulaire.

Le HTTP Artifact Binding se base sur l’un des deux précédents pour transmettre un « artifact », cet identifiant va ensuite être utilisé par le destinataire dans le cadre du protocole de résolution d’artifact pour récupérer le message initial via un appel direct, pouvant par exemple utiliser le binding SOAP. Ce qui a l’avantage d’éviter de faire transiter le message par le navigateur, et l’inconvénient de supposer qu’il existe un chemin de communication direct entre les deux correspondants.

Il est aisé de constater que le plus long à mettre en œuvre est le HTTP Artifact, puisqu’il a parmi ses pré-requis l’existence de l’un des deux autres, ce qui n’est pas très encourageant. Notre requête étant courte, nous allons privilégier le binding Redirect.

En regardant plus en détail, le fonctionnement de ce binding, nous constatons que, outre la requête (ou la réponse), il existe un paramètre RelayState que le fournisseur  d’identité doit nous renvoyer tel que nous l’avons renseigné, cela va nous permettre de nous « souvenir » de la page à laquelle l’utilisateur voulait accéder. Pour envoyer un message par ce mécanisme, nous devons le compresser en utilisant DEFLATE (RFC1951), puis l’encoder au format base 64 et pour finir placer le résultat dans un paramètre nommé SAMLRequest ou SAMLResponse. Malheureusement pour nous Liferay ne fournit pas d’API pour gérer la compression DEFLATE, donc on va devoir la faire nous même, en fait, on va simplement se doter d’une classe utilitaire pour simplifier les appels à Deflater et Inflater de java.util.zip :


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
package com.liferay.portal.kernel.util;

import java.io.UnsupportedEncodingException;

import java.util.ArrayList;
import java.util.List;
import java.util.zip.DataFormatException;
import java.util.zip.Deflater;
import java.util.zip.Inflater;

/**
 * @author Denis Vaumoron
 */

public class Deflate {

        public static byte[] encode(String string)
                throws UnsupportedEncodingException {

                Deflater lDeflater = new Deflater(Deflater.DEFAULT_COMPRESSION, true);
                lDeflater.setInput(string.getBytes(StringPool.UTF8));
                lDeflater.finish();

                int deflateLength;
                byte[] deflateBuffer = new byte[_BUFFER_SIZE];
                List bufferList = new ArrayList();
                while ((deflateLength =
                        lDeflater.deflate(deflateBuffer)) == _BUFFER_SIZE) {

                        bufferList.add(deflateBuffer);
                        deflateBuffer = new byte[_BUFFER_SIZE];
                }

                byte[] deflate =
                        new byte[bufferList.size() * _BUFFER_SIZE + deflateLength];

                for(byte[] buffer : bufferList) {
                        System.arraycopy(buffer, 0, deflate, 0, _BUFFER_SIZE);
                }

                System.arraycopy(deflateBuffer, 0, deflate, 0, deflateLength);

                return deflate;
        }

        public static String decode(byte raw[])
                throws UnsupportedEncodingException, DataFormatException {

                Inflater inflater = new Inflater(true);
                inflater.setInput(raw);

                int resultLength;
                byte[] valueBytes = new byte[_BUFFER_SIZE];
                StringBundler inflateBuffer = new StringBundler();
                while ((resultLength = inflater.inflate(valueBytes)) != 0) {
                        inflateBuffer.append(
                                new String(valueBytes, 0, resultLength, StringPool.UTF8));
                }
                inflater.end();

                return inflateBuffer.toString();
        }

        private static final int _BUFFER_SIZE = 1024;

}

Pour l’encodage base 64, on peut utiliser com.liferay.portal.kernel.util.Base64, ce qui donne le bout de code suivant :


1
2
3
byte[] samlDeflate = Deflate.encode(saml);

String samlBase64 = Base64.encode(samlDeflate);

Plutôt simple non ? C’est probablement parce que je n’ai pas mentionné que pour le profile web browser SSO, il est recommandé d’utiliser https (bon ok, c’est une fausse difficulté, cela se configure au niveau du serveur d’application), et de signer les messages (dans le cas où l’identification du fournisseur de service doit être vérifiée (ce n’est pas toujours le cas ? je me demande à quoi sert cette précision, si le fournisseur d’identité répond à « n’importe qui » avec toutes les informations sur l’utilisateur…)). Pour signer le message, il faut rajouter deux paramètres, SigAlg pour indiquer l’URI  de l’algorithme de signature utilisé et Signature pour la signature (que de surprise dans cet article) qui doit être encodé en base 64. La signature s’effectue sur les paramètres classés dans l’ordre SAMLRequest (ou SAMLResponse), RelayState (le paramètre ne doit pas apparaitre si la valeur est vide), puis SigAlg. Les algorithmes de signature DSAwithSHA1 et RSAwithSHA1 doivent être supportés, pour nous faciliter la vie nous allons utiliser java.security.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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
        public static String getHTTPRedirectBindingUrl(
                        String serviceUrl, boolean isRequest,
                        String saml, String relayState, String signAlg)
                throws SystemException, IOException, GeneralSecurityException {

                byte[] samlDeflate = Deflate.encode(saml);

                String samlBase64 = Base64.encode(samlDeflate);

                StringBundler url = new StringBundler(7);
                url.append(serviceUrl).append(StringPool.QUESTION);

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

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

                urlParamBuffer.append(StringPool.AMPERSAND)
                        .append(SAMLConstants.SIG_ALG)
                        .append(StringPool.EQUAL)
                        .append(HttpUtil.encodeURL(
                                _getURIFromSignAlg(signAlg)));
                String urlParameters = urlParamBuffer.toString();

                url.append(urlParameters);

                url.append(StringPool.AMPERSAND)
                        .append(SAMLConstants.SIGNATURE)
                        .append(StringPool.EQUAL)
                        .append(_signUrlParameter(urlParameters, signAlg));

                return url.toString();
        }

        /**
         * Cette  méthode doit être utilisée avant l'envoi de la réponse,
         * afin d'initialiser les en-têtes corrects.
         *
         * @param response
         */

        public static void initHTTPHeader(HttpServletResponse response) {
                response.setHeader("Cache-Control", "no-cache, no-store");
                response.setHeader("Pragma", "no-cache");
        }

        private static String _signUrlParameter(
                        String urlParameter, String signAlg)
                throws SystemException, IOException, GeneralSecurityException {

                // les noms d'algorithme ne suivent pas la même nomenclature
                // dans SAML et dans java.security, donc on doit convertir.

                Signature signature = Signature.getInstance(
                        _getAlgorithmFromSignAlg(signAlg));
                signature.initSign(SAMLMetadataManager.getSPPrivateKey());

                signature.update(urlParameter.getBytes(StringPool.UTF8));
                return HttpUtil.encodeURL(Base64.encode(signature.sign()));
        }

        private static String _getURIFromSignAlg(String signAlg) {
                if (_DSA_WITH_SHA1.equals(signAlg)) {
                        return SignatureMethod.DSA_SHA1;
                }
                return SignatureMethod.RSA_SHA1;
        }

        private static String _getAlgorithmFromSignAlg(String signAlg) {
                if (_DSA_WITH_SHA1.equals(signAlg)) {
                        return _SHA1_WITH_DSA;
                }
                return _SHA1_WITH_RSA;
        }

        private static final String _DSA_WITH_SHA1 = "DSAwithSHA1";

        private static final String _SHA1_WITH_DSA = "SHA1withDSA";

        private static final String _SHA1_WITH_RSA = "SHA1withRSA";

Remarque : la méthode getSPPrivateKey de la classe SAMLMetadataManager (comme son nom ne l’indique pas) me permet de récupérer la PrivateKey associée au KeyStore qui contient ma clé. En attendant le sublime final de la troisième saison la troisième partie de cette série d’articles (qui concernera la gestion des métadonnées), vous pouvez lire la clé dans votre keystore de la manière suivante :


1
2
3
4
5
6
7
8
KeyStore keystore = KeyStore.getInstance(“jks”);
keystore.load(
        new FileInputStream(“chemin/vers/monkeystore.jks),
        “MotDePasseDuKeyStore”.toCharArray());

PrivateKey spPrivateKey = (PrivateKey) keystore.getKey(
        “aliasDeLaClePrivee”,
        “MotDePasseDeLaClePrivee”.toCharArray());

Pour générer le fameux keystore et la clé correspondante, vous n’avez qu’à utiliser la commande suivante (keytool fait partie des outils fournit avec le JDK, faites un keytool –help si besoin) :

keytool –genkey -keyalg RSA -alias aliasDeLaClePrivee –keypass MotDePasseDeLaClePrivee –keystore monkeystore.jks
    –storepass MotDePasseDuKeyStore

 

C’est fini pour aujourd’hui

Et voila, nous avons notre bouteille, autrement dit nous sommes maintenant capable d’envoyer un message, vous en savez aussi un peu plus sur les « bindings » de SAML, nous verrons dans le prochain article comment générer la demande d’authentification, après tout on jette rarement à la mer une bouteille vide.

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

 

Share