Interaction avec un annuaire LDAP

Un fournisseur d'identité est susceptible d'utiliser un annuaire LDAP pour deux usages distincts:

  1. l'authentification des utilisateurs
  2. la valorisation des attributs

Le premier usage n'est qu'une possibilité parmi d'autres, de nombreux établissements préférant par exemple déléguer l'authentification à un serveur CAS. Le deuxième usage, s'il n'est pas techniquement obligatoire, est en revanche systématique dans les faits.

Ces deux usages présentent des similitudes techniques (dans les deux cas, il est nécessaire d'ouvrir une connexion réseau vers l'annuaire, par exemple) mais se configurent séparément, puisqu'ils correspondent à des sous-systèmes différents pour l'IdP. Les deux étant souvent confondus, les explications suivantes visent à faciliter la compréhension du sujet, et la résolution d'éventuels problèmes, en pré-supposant une connaissance des concepts de base d'un annuaire LDAP.

Authentification des utilisateurs

L'authentification se configure via les propriétés idp.authn.LDAP.*, définies dans le fichier ldap.properties. Ces propriétés n'étant utilisée que pour l'authentification, elles ne servent strictement à rien si l'authentification utilise autre chose qu'un annuaire LDAP.

L'ensemble du mécanisme est documenté ici.

Paramètres de base

Les propriétés de base (idp.authn.LDAP.ldapURL, idp.authn.LDAP.useStartTLS, …) sont relativement classiques pour l'utilisation d'un annuaire, et devraient être compréhensibles facilement. Deux propriétés méritent néanmoins quelques explications complémentaires.

La propriété idp.authn.LDAP.sslConfig détermine le moyen de valider le certificat TLS présenté par le serveur, si une connexion sécurisée est utilisée (soit via une URL de type ldaps://, soit via l'utilisation de StartTLS), et elle peut prendre l'une des trois valeurs ci-dessous:

  • certificateTrust: un fichier contenant l'autorité de certification au format PEM
  • keyStoreTrust: un fichier contenant l'autorité de certification au format keystore
  • jvmTrust: délégation de la vérification à la machine virtuel Java

La propriété idp.authn.LDAP.returnAttributes consiste en une liste d'attributs qui seront récupérés lors de l'authentification. Le but n'est pas d'anticiper le besoin d'attributs pour la phase de valorisation des attributs, qui est toujours indépendante, mais éventuellement de modifier le comportement de l'IdP lors de l'authentification. Par exemple, tenir compte coté IdP de l'expiration du mot de passe, enregistré dans un attribut passwordExpirationTime. Il s'agit clairement d'un cas d'usage avancé, avec du code spécifique à écrire, et dans la majorité des cas, il est inutile de récupérer le moindre attribut lors de cette phase, via la syntaxe cabalistique suivante:

idp.authn.LDAP.returnAttributes = 1.1

Stratégie d'authentification

L'authentification des utilisateurs via un annuaire LDAP consiste à valider le mot de passe de celui-ci, à partir des informations fournies par celui-ci:

  • son identifiant
  • son mot de passe

Une telle validation se fait via une opération de type bind, qui nécessite deux informations:

  • l'identifiant de l'utilisateur sur l'annuaire (DN)
  • son mot de passe

La méthode à utiliser pour obtenir le DN de l'utilisateur à partir de son identifiant, en général l'un des attributs de l'utilisateur, dépend principalement de la structure de l'annuaire, et va conditionner le choix d'une stratégie d'authentification (Authenticator strategy).

directAuthenticator

Cette méthode nécessite de pouvoir déduire directement le DN de l'utilisateur à partir de son identifiant. Par exemple, l'utilisateur s'identifie via son attribut uid, et son DN est par construction uid=<identifiant>,ou=people,dc=example,dc=org.

Cette stratégie est la plus efficace, puisque l'IdP se contentera alors d'une seule connexion à l'annuaire LDAP pour valider le mot de passe de l'utilisateur, avec son DN et son mot de passe, sans avoir à effectuer une recherche au préalable. La cinématique des opérations sur l'annuaire est alors la suivante:

  1. connexion authentifiée, avec le couple DN/mot de passe de l'utilisateur

Elle est également la plus simple à configurer, puisqu'elle se résume aux deux propriétés suivantes:

idp.authn.LDAP.authenticator = directAuthenticator
idp.authn.LDAP.dnFormat      = uid=%s,ou=people,dc=example,dc=org

anonSearchAuthenticator

Cette méthode permet d'obtenir le DN à partir d'une recherche anonyme, quand ce DN n'est pas directement déductible de l'identifiant de l'utilisateur. Par exemple, l'utilisateur s'identifie via son attribut uid, mais son DN varie en fonction de la branche de l'annuaire utilisée.

Cette stratégie est moins efficace, parce qu'elle va nécessiter une première connexion anonyme de l'IdP, suivie d'une recherche, avant d'effectuer une nouvelle connexions, cette fois-ci avec le DN et le mot de passe de l'utilisateur. La cinématique des opérations sur l'annuaire est la suivante:

  1. connexion anonyme
  2. recherche de l'objet utilisateur
  3. connexion authentifiée, avec le couple DN/mot de passe de l'utilisateur

Elle est également légèrement plus complexe à configurer, puisqu'il faut définir des critères de recherche:

idp.authn.LDAP.authenticator = anonSearchAuthenticator
idp.authn.LDAP.baseDN        = ou=people,dc=example,dc=org
idp.authn.LDAP.subtreeSearch = true
idp.authn.LDAP.userFilter    = (uid={user})

bindSearchAuthenticator

Cette méthode est identique à la précédente, avec cette fois-ci la définition d'un couple DN/mot de passe spécifique à l'IdP (un compte de service, autrement dit), pour effectuer la recherche.

La cinématique des opérations sur l'annuaire est cette fois-ci la suivante:

  1. connexion authentifiée, avec le couple DN/mot de passe de l'IdP
  2. recherche de l'objet utilisateur
  3. connexion authentifiée, avec le couple DN/mot de passe de l'utilisateur

La configuration est la même que précédemment, les identifiants de connexion en plus:

idp.authn.LDAP.authenticator    = anonSearchAuthenticator
idp.authn.LDAP.baseDN           = ou=people,dc=example,dc=org
idp.authn.LDAP.subtreeSearch    = true
idp.authn.LDAP.userFilter       = (uid={user})
idp.authn.LDAP.bindDN           = cn=idp,dc=example,dc=org
idp.authn.LDAP.bindDNCredential = p4ssw0rd

Valorisation des attributs

La valorisation des attributs se configure via les propriétés idp.attribute.resolver.LDAP.*, définies dans le fichier ldap.properties. Ces propriétés peuvent être définies pour réutiliser les valeurs des propriétés utilisées pour l'authentification, de façon à éviter la duplication, ou utiliser des valeurs différentes.

Elle nécessite également la définition d'un connecteur distant, sous la forme d'un composant java (bean), dans n'importe quel fichier XML chargé par l'IdP, réutilisant les propriétés évoquées précédemment. Typiquement, ce connecteur ressemble à ceci:

<DataConnector id="myLDAP" xsi:type="LDAPDirectory"
    ldapURL="%{idp.attribute.resolver.LDAP.ldapURL}"
    baseDN="%{idp.attribute.resolver.LDAP.baseDN}"
    principal="%{idp.attribute.resolver.LDAP.bindDN}"
    principalCredential="%{idp.attribute.resolver.LDAP.bindDNCredential}"
    useStartTLS="%{idp.attribute.resolver.LDAP.useStartTLS:true}"
    connectTimeout="%{idp.attribute.resolver.LDAP.connectTimeout}"
    trustFile="%{idp.attribute.resolver.LDAP.trustCertificates}"
    responseTimeout="%{idp.attribute.resolver.LDAP.responseTimeout}">
    <FilterTemplate>
        <![CDATA[
        %{idp.attribute.resolver.LDAP.searchFilter}
        ]]>
    </FilterTemplate>
</DataConnector>

Comme pour l'authentification, la définition d'identifiants de connexion spécifique à l'IdP (idp.attribute.resolver.LDAP.bindDN, idp.attribute.resolver.LDAP.bindDNCredential) n'est nécessaire que si l'annuaire LDAP ne permet pas d'effectuer des recherches anonymes sur l'ensemble des attributs nécessaires.

La cinématique des opérations sur l'annuaire est celle-ci, après une authentification réussie:

  1. connexion anonyme ou authentifiée avec le couple DN/mot de passe de l'IdP, en fonction de la configuration
  2. recherche de l'objet utilisateur et des attributs associés

L'ensemble du mécanisme est documenté ici.

Mise en cache des connexions

La mise en cache des connexions (pooling, en anglais) est une technique d'optimisation visant à maintenir des connexions vers l'annuaire ouvertes en permanence, plutôt que des les ouvrir et les fermer lorsqu'elles sont nécessaires uniquement. Cette technique n'est nullement obligatoire, et elle présente deux inconvénients:

  • elle a également un cout pour l'infrastructure, puisqu'elle maintient en permanence des connexions TCPs, et entraine éventuellement des requêtes régulières de validation dans l'annuaire, même si ces connexions ne sont jamais utilisées: en fonction de l'utilisation réelle de l'IdP, ce cout peut être surdimensionné par rapport au gain obtenu
  • elle complexifie la situation, puisqu'il faut vérifier qu'une connexion existante soit toujours valide avant de l'utiliser: avec des connexions fiables, ce n'est généralement pas un problème, mais si ces connexions sont susceptibles d'être coupées pour une raison ou une autre (fermeture coté serveur, pare-feu facétieux, …), cette validation devient critique

Ce qui devrait donc être un choix de configuration résultant d'une analyse cout/bénéfice est malheureusement activée par défaut dans la configuration upstream, et se retrouve donc souvent utilisée sans jamais avoir été choisie explicitement.

Le cache de connexion se configure via les propriétés idp.pool.LDAP.*, dans le fichier ldap.properties. Parmi les propriétés les plus importantes, on peut citer:

  • la taille du pool (propriétés idp.pool.LDAP.minSize et idp.pool.LDAP.maxSize)
  • le choix d'une stratégie de validation (propriétés idp.pool.LDAP.validateOnCheckout et idp.pool.LDAP.validatePeriodically)
  • le paramétrage de cette stratégie

Il n'y a pas de recette magique pour identifier la bonne valeur de ces paramètres, seule la métrologie permet de le faire. Attention également au fait qu'une taille minimale de 0 ne désactive pas le cache pour autant.

Pour désactiver la mise en cache, il suffit de définir la propriété idp.authn.LDAP.disablePooling à la valeur true pour l'authentification, depuis la version 4.x. Sur les versions antérieurs de l'IdP, il faut changer la définition du composant java (bean) d'identifiant authHandler dans le fichier ldap-authn-config.xml.

La mise en cache peut également être utilisée pour la valorisation des attributs, en ajoutant un objet ConnectionPool au connecteur distant:

<DataConnector id="myLDAP" xsi:type="LDAPDirectory"
    ldapURL="%{idp.attribute.resolver.LDAP.ldapURL}"
    baseDN="%{idp.attribute.resolver.LDAP.baseDN}"
    principal="%{idp.attribute.resolver.LDAP.bindDN}"
    principalCredential="%{idp.attribute.resolver.LDAP.bindDNCredential}"
    useStartTLS="%{idp.attribute.resolver.LDAP.useStartTLS:true}"
    connectTimeout="%{idp.attribute.resolver.LDAP.connectTimeout}"
    trustFile="%{idp.attribute.resolver.LDAP.trustCertificates}"
    responseTimeout="%{idp.attribute.resolver.LDAP.responseTimeout}">
    <FilterTemplate>
        <![CDATA[
        %{idp.attribute.resolver.LDAP.searchFilter}
        ]]>
    </FilterTemplate>
    <ConnectionPool
        minPoolSize="%{idp.pool.LDAP.minSize:3}"
        maxPoolSize="%{idp.pool.LDAP.maxSize:10}"
        blockWaitTime="%{idp.pool.LDAP.blockWaitTime:PT3S}"
        validatePeriodically="%{idp.pool.LDAP.validatePeriodically:true}"
        validateTimerPeriod="%{idp.pool.LDAP.validatePeriod:PT5M}"
        expirationTime="%{idp.pool.LDAP.idleTime:PT10M}"
        failFastInitialize="%{idp.pool.LDAP.failFastInitialize:false}" />
</DataConnector>

Attention, cette syntaxe revient à créer un cache différent de celui utilisé pour l'authentification.

Résolution des problèmes

Lorsque tout ne fonctionne pas comme prévu, il est indispensable d'utiliser les journaux d'activité des deux coté (IdP et annuaire LDAP) pour comprendre le problème.

Pour l'annuaire LDAP, ceci dépend de l'implémentation utilisée. Pour OpenLDAP, par exemple, il s'agit de la directive loglevel, qui doit enregistrer (au moins) les opérations effectuées:

loglevel stats

Pour l'IdP, il faut utiliser le niveau DEBUG pour toutes les opérations effectuées par la couche LDAP, ce qui peut s'obtenir facilement via la propriété idp.loglevel.ldap:

idp.loglevel.ldap = DEBUG

Enfin, il est notable que la partie valorisation des attributs peut être testée indépendamment de l'authentification, via la commande aacli.