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

SAML 2 et Liferay – partie 3

Dans le précédent épisode article (oui, je sais que les meilleures blagues sont les plus courtes, mais non, je n’ai pas l’intention d’arrêter de faire cette blague pourrie, habituez vous), nous avons vu comment envoyer un message en suivant le HTTP Redirect Binding, maintenant que nous avons notre bouteille, il va nous falloir prendre notre plus beau parchemin et écrire ce message de détresse.

Tous les cris, les S O S…

Nous voulons envoyer une demande d’authentification, en voici un exemple :


1
2
3
4
5
6
7
8
9
<samlp:AuthnRequest
               xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
               xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
               ID="sk3k82Vt5uJamRNo1DgtgoKikrLH"
               Version="2.0"
               IssueInstant="2011-07-07T20:46:09Z"
               IsPassive="true">
        <saml:Issuer>http://blog.excilys.com</saml:Issuer>
</samlp:AuthnRequest>

Notre cas est simple, si nous excluons  le formalisme (notamment les déclarations d’espace de noms), nous n’envoyons que quatre informations :

  • ID, qui permet d’identifier le message
  • IssueInstant, la date de génération du message
  • IsPassive, optionnel, permet de demander que le fournisseur d’identité réponde sans interagir avec l’utilisateur
  • Issuer, qui permet d’identifier l’expéditeur du message

Et là ? Révélations ! IsPassive résout notre problème (mais si, vous vous souvenez…), lorsqu’un utilisateur arrivera sur le portail, nous ferons systématiquement la demande d’authentification. Pour savoir si l’utilisateur tente de se connecter, il nous suffira de tester si l’url contient le fameux « /portal/login », dans ce cas, il ne faut pas envoyer IsPassive à true, ainsi le fournisseur d’identité affichera sa page de connexion. Cependant, il ne faut pas oublier de placer un indicateur en session pour éviter que les deux serveurs ne se mettent à jouer au ping pong (En effet, si l’utilisateur n’est pas déjà connecté sur l’IdP, celui-ci renverra une réponse négative (pas de cookie) sans proposer de page de connexion (à cause du isPassive) , et le SP renverra une demande…). Pour générer le message, nous allons profiter de l’API XML intégrée dans Liferay (en fait, c’est une encapsulation de dom4j) :


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
        public static String generateAuthnRequest(
                        HttpServletRequest request, String loginUrl,
                        String samlEntityId, String relayState, String signAlg,
                        Boolean isPassive)
                throws Exception {

                String messageID = generateMessageID();

                // on stocke l'id pour tester la correspondance de la réponse

                request.getSession().setAttribute(
                        SAMLConstants.MESSAGE_ID, messageID);

                Document document = SAXReaderUtil.createDocument();

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

                Element authnRequest = document.addElement(
                        SAXReaderUtil.createQName(
                        SAMLConstants.AUTHN_REQUEST, samlp));

                authnRequest.add(samlp);
                authnRequest.add(saml);

                authnRequest.addAttribute(SAMLConstants.ID, messageID);
                authnRequest.addAttribute(
                        SAMLConstants.VERSION, SAMLConstants.VERSION_2_0);
                authnRequest.addAttribute(
                        SAMLConstants.ISSUE_INSTANT, formatDate(new Date()));
                authnRequest.addAttribute(SAMLConstants.DESTINATION, loginUrl);

                if (isPassive != null) {
                        authnRequest.addAttribute(
                                SAMLConstants.IS_PASSIVE, isPassive.toString());
                }

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

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

                return getHTTPRedirectBindingUrl(
                        loginUrl, true, xmlString, relayState, signAlg);
        }

        public static String generateMessageID() {
                // l'id doit être unique, dans le cas d'une technique
                // aléatoire, le standard recommande une longueur de plus de
                // 160 bit (moins de 2^-160 chances de collision)
                // l'encodage en base 64 rallonge d'1/3, en octets pleins, la longueur supérieure la plus
                // approchante est 21 (21*8=168 > 160), on aura donc en sortie un id sur 28 caractères.
                byte[] randomBytes = new byte[21];
                _random.nextBytes(randomBytes);
                return Base64.encode(randomBytes);
        }

        public static String formatDate(Date date) {
                return _getDateFormat().format(date);
        }

        private static DateFormat _getDateFormat() {
                return new SimpleDateFormat(_DATE_PATTERN);
        }

        private static final String _DATE_PATTERN =
                "yyyy-MM-dd'T'HH:mm:ss'Z'";

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

        private static Random _random = new Random();

Une boite aux lettres

Si nous reprenons notre descriptif de l’échange (ça vous avait presque manqué, non ?) :

Diagramme de séquence du SSO en SAML

Nous avons envoyé la <AuthnRequest>, le fournisseur d’identité va s’occuper de l’étape 3) d’identification de l’utilisateur, puis ensuite à l’étape 4), il va nous renvoyer une <Response>, celle-ci pouvant comporter de nombreuses informations sur l’utilisateur. Pour pouvoir recevoir cette réponse, nous allons mettre en place un endpoint de type assertion consumer service supportant le HTTP Post Binding (étant donné que le standard spécifie que l’on ne doit pas utiliser le HTTP Redirect Binding), autrement dit une url qui va pouvoir recevoir un paramètre Response, celui-ci étant encodé en base64 (et non, pas de DEFLATE dans ce binding). Pour cela, il faut indiquer à Liferay de reconnaitre l’url en ajoutant celle-ci à la propriété virtual.hosts.ignore.paths (situé dans portal.properties) pour éviter une redirection qui nous ferait perdre les paramètres du post (lorsque Liferay ne trouve pas de page associée à l’adresse par son mécanisme de « friendly URL », il redirige vers la page par défaut).

A ce moment, l’utilisateur n’étant pas encore authentifié par Liferay, le système d’AutoLogin va se déclencher, ce qui va nous permettre de simplement tester la présence du paramètre Response au niveau de notre SAMLAutoLogin :


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static boolean isAuthenticated(
                HttpServletRequest request, String loginUrl,
                String samlEntityId, String samlIdentityProviderId,
                String signAlg, HttpServletResponse response)
        throws Exception {

        String samlResponseParameter =
                request.getParameter(SAMLConstants.SAML_RESPONSE);
        if (samlResponseParameter != null) {
                String samlResponse = getSamlFromHTTPPostBindingParameter(
                        samlResponseParameter);

                // TODO lecture de la réponse
        }
        else {
                sendAuthnRequest(
                        request, response, samlEntityId, loginUrl, signAlg);
        }

        return false;
}

C’est fini pour aujourd’hui

Nous sommes désormais capable d’envoyer une demande d’authentification, et nous avons mis en place un endpoint pour recevoir la réponse, dans le prochain article nous analyserons la réponse afin de connecter l’utilisateur à Liferay (ou pas).

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

 

Share
  1. Pas encore de commentaire
  1. Pas encore de trackbacks