Architecture Spring Security : L’authentification
Le but de cet article est de présenter les concepts mis en oeuvre par Spring Security pour la partie Authentification. Il existe bon nombre de sources d’informations sur le sujet (notamment la documentation officielle), la plupart rédigée en anglais et en grande partie très technique. Rare sont les articles qui vulgarise et présente l’architecture de Spring Security qui peut très vite paraître très complexe.
Présentation
Spring Security a été créé en 2003 et fait partie de l’écosystème Spring. Il fournit des services permettant de sécuriser des applications server J2EE. Le Framework en est actuellement à sa version 4 apportant son lot de nouveautés et de corrections de failles de sécurité depuis la version 3.
La sécurité mise en œuvre par Spring Security repose sur deux étapes majeures : L’authentification et l’autorisation.
L’authentification consiste à identifier un Principal auprès d’une ou plusieurs sources de données. L’objectif pour Spring Security, à l’issue de cette étape, étant de définir un Principal qui peut aussi bien être un utilisateur physique, un appareil ou tout autre système en capacité d’effectuer une ou plusieurs actions sur notre serveur et donc de passer par notre sécurité.
L’autorisation est le processus qui permet de déterminer si notre principal a les droits suffisants pour effectuer une action au sein de notre application. Cette étape n’est évidemment possible que si l’authentification s’est déroulée correctement.
Dans cet article, je ne détaillerais que la partie authentification, la partie autorisation faisant l’objet d’un autre article.
Authentification
Spring Security fournit par défaut un ensemble de modèles d’authentification correspondant à différents besoins et différentes technologies. Parmi les plus importants on retiendra :
Nom |
Description |
---|---|
HTTP BASIC authentication headers |
Authentification via une requête http Basic |
HTTP Digest authentication headers |
Authentification via une requête http digest |
HTTP X.509 client certificate exchange |
Authentification à l’aide d’un certificat X.509 |
LDAP |
Authentification via un annuaire supportant la norme LDAP (OpenLdap, ActiveDirectory,..) |
Form-based authentication |
Authentification via formulaire |
OpenID authentication |
Authentification via le protocole OpenID |
Automatic “remember-me” authentication |
Authentification “remember me ” pour ne pas avoir à resoumettre l’authentification si la case est cochée |
Anonymous authentication |
Authentification des utilisateurs anonymes |
Run-as authentication |
Authentification permettant d’exécuter une requête avec une identité différente |
Kerberos |
Authentification sur un système Kerberos |
Cette liste n’est évidemment pas exhaustive.
Autorisation
Spring Security fournit trois niveaux d’autorisations :
- Les autorisations sur les requêtes web, basées sur les URLS.
- Les autorisations sur l’invocation ou non de certaines méthodes (AOP)
- Les autorisations sur l’invocation de certains objets (ACL)
Managers et Providers
Managers
Regardons de plus près comment Spring Security gère en interne l’authentification et quels composants interviennent dans ce processus.
Authentication Manager
C’est le point d’entrée du processus d’authentification de Spring Security. Son rôle est d’établir l’identité de l’émetteur de la requête.
Concrètement il s’agit d’une interface sobrement nommée AuthenticationManager possédant une simple méthode authenticate. Il est donc très facile d’implémenter son propre AuthenticationManager mais Spring fournit une implémentation par défaut à travers la classe ProviderManager.
Provider Manager
Implémentation de l’interface AuthenticationManager, son rôle est de réaliser une authentification basée sur un ou plusieurs “providers” (LDAP, BDD, etc.).
Comme nous le verrons par la suite, les “providers” sont enregistrés dans la classe ProviderManager et doivent implémenter l’interface AuthenticationProvider.
Chacun des “providers” est appelé et retourne un objet Authentication (on y reviendra plus tard) en cas de succès ou lève une exception en cas d’erreur.
Authentication Provider
Interface dont les multiples implémentations définissent l’authentification concrète avec un tiers (LDAP, BDD, etc..) ou localement (“in memory” par exemple).
En cas d’échec d’une authentification une exception AuthentificationException est levée par le “provider” et le ProviderManager passe alors au “provider” suivant s’il existe.
Si aucun “provider” n’a réussi son authentification alors l’authentification est un échec, en revanche si au moins un des “providers” réussi alors un objet Authentication est retourné et l’authentification est un succès.
Certaines exceptions levées par les “providers” interrompent complètement le processus d’authentification, contrairement à l’exception AuthentificationException qui déclenche l’authentification du “provider” suivant.
Exception |
Commentaire |
Actions ProviderManager |
---|---|---|
AccountStatusException |
Exception levée par un AuthenticationProvider si le compte est dans un état invalide (désactivé, verrouillé) |
L’authentification est interrompue et est en échec |
InternalAuthenticationServiceException |
Exception levée par un AuthenticationProvider si un problème est intervenu sur le système tiers ou s’il est injoignable |
L’authentification est interrompue et est en echec |
AuthenticationException |
Exception levée par un AuthenticationProvider si l’authentification échoue : Login et mot de passe invalide |
Le ProviderManager passe au provider suivant s’il existe |
Exceptions gérées par le ProviderManager
Providers
Spring Security fournit un certain nombre d’implémentations de l’interface AuthenticationProvider permettant de se connecter à un système tiers ou local:
Provider |
Commentaire |
---|---|
DaoAuthenticationProvider |
Provider de connexion à une base de données |
LdapAuthenticationProvider |
Provider de connexion à un annuaire LDAP |
ActiveDirectoryAuthenticationProvider |
Provider de connexion à un annuaire Active Directory |
AnonymousAuthenticationProvider | Provider de connexion pour les utilisateurs anonymes |
Cette liste n’est évidemment pas exhaustive et il est naturellement possible de créer un AuthenticationProvider spécifique en créant une classe qui implémente l’interface AuthenticationProvider.
Le contrat de l’interface AuthenticationProvider est assez simple:
Une méthode d’authentification authenticate sur laquelle on passe un objet Authentication en tant que paramètre qui est aussi le résultat.
1 |
Authentication authenticate(Authentication authentication) throws AuthenticationException; |
Une méthode supports qui permet de déterminer dans quels cas le “provider” est compatible avec l’objet Authentication .
1 |
boolean supports(Class<?> authentication); |
Un “provider” va donc effectuer une authentification vers un système tiers ou local et retourner une instance de l’interface Authentication si l’opération est un succès. Dans le cas contraire une exception AuthenticationException sera levée. La liste des “providers” que l’on souhaite ajouter à Spring Security doit être renseignée au moment de la configuration.
Mais que contient cet objet Authentication et quel est son rôle dans le processus d’authentification ?
L’objet Authentication
Interface
L’objet Authentification contient les informations nécessaires à présenter au système tiers pour l’authentification (par exemple un identifiant et un mot de passe). Mais il représente aussi notre utilisateur fraîchement authentifié sur un système tiers ou local après l’authentification (appelé “Principal” par Spring Security).
Si l’authentification est réussie, Spring Security va d’ailleurs stocker cet objet Authentification avec les informations nécessaires pour pouvoir passer la seconde étape : L’autorisation.
Regardons de plus près les méthodes de notre interface Authentification :
Une méthode getAuthorities qui retourne la liste des droits de l’utilisateur connecté (nous verrons cela plus tard) et qui est renseigné par l' AuthenticationManager.
1 |
Collection<? extends GrantedAuthority> getAuthorities(); |
Une méthode getCredentials qui retourne les informations d’identification. La plupart du temps il s’agit simplement du mot de passe.
1 |
Object getCredentials(); |
Une méthode getDetails qui retourne des informations additionnelles que l’on pourrait stocker à l’authentification, telles que l’adresse IP, le numéro de certificat, etc.
1 |
Object getDetails(); |
Une méthode getPrincipal qui retourne le “Principal” c’est à dire l’utilisateur identifié par exemple. Avant l’authentification, dans l’instance de la classe Authentication passée en paramètre de la méthode authenticate, il s’agit généralement de l’identifiant. Puis si l’authentification est un succès, dans l’instance de la classe Authentication retournée par la méthode authenticate, le “Principal” est alors un objet contenant les informations sur l’utilisateur authentifié. La plupart du temps il s’agit d’un objet de type User qui implémente l’interface Principal .
1 |
Object getPrincipal(); |
Il existe de nombreuses implémentations de l’interface Authentication, la plus connue étant UsernamePasswordAuthenticationToken qui prend comme paramètres de construction un identifiant et un mot de passe. Il en existe évidemment d’autre comme RunAsUserToken, RememberMeAuthenticationToken, etc.
Authentification par login et mot de passe
La classe UsernamePasswordAuthenticationToken implémente l’interface Authentication et contient les “credentials” c’est à dire l’identifiant et le mot de passe utilisé lors de la connexion.
Cette instance est donc passée en paramètre de la méthode authenticate du “provider”. Cette dernière retourne également un objet UsernamePasswordAuthenticationToken dont le “principal” contient alors les informations sur l’utilisateur authentifié.
Mais comment fait Spring Security pour tester la validité d’une authentification, c’est à dire l’exactitude des identifiants de connexio
n, avec le ou les systèmes tiers ?
Le principe est simple : il suffit de charger l’utilisateur par son identifiant unique (“credential”) depuis le système tiers (par exemple une base de données) et Spring Security se chargera alors de comparer le mot de passe de l’utilisateur chargé avec le mot de passe passé en paramètre de la méthode authenticate dans l’instance de la classe UsernamePasswordAuthenticationToken.
Mais comment charger un utilisateur sachant que cela va dépendre du “provider” ?
En effet, charger un utilisateur par son identifiant depuis une base de données est une opération totalement différente avec un annuaire LDAP. Il nous faut donc la possibilité de pouvoir effectuer ces opérations pour chacun des “providers”.
Cela tombe bien, Spring Security a tout prévu et nous offre une interface qui répond à notre besoin : UserDetailsService
UserDetailsService
Interface
L’interface UserDetailsService nous propose une méthode loadByUserName pour récupérer un utilisateur par son identifiant principal (par exemple un “login”):
1 |
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; |
La méthode prend donc en paramètre un identifiant et retourne un objet implémentant l’interface UserDetails.
Spring Security propose une implémentation par défaut de cette interface via la classe User. La classe User propose un certain nombre de propriétés intéressantes qui vont être utiles à Spring Security pour la comparaison du mot de passe ou la vérification des autorisations nécessaires : Le mot de passe, l’identifiant et les autorités.
Il est donc tout à fait possible de surcharger la classe User ou simplement d’implémenter l’interface UserDetails si l’on souhaite ajouter des informations supplémentaires et de retourner cette instance dans la méthode loadByUsername de notre classe UserDetailsService.
C’est lors de la configuration d’un “provider” que l’on passe l’instance de UserDetailsService à utiliser pour l’authentification.
En plus de la comparaison de mot de passe, notre objet UserDetails va fournir la liste des autorités de l’utilisateur ce qui va permettre à Spring Security de déterminer s’il est autorisé ou non à accéder à la ressource demandée.
Vous suivez ? Faisons un petit récapitulatif:
Les autorités
La liste des autorités doit implémenter l’interface GrantedAuthority qui comprend une seule méthode:
1 |
String getAuthority(); |
Spring Security fournit une implémentation avec la classe SimpleGrantedAuthority où le rôle associé est une simple chaîne de caractère.
Cette liste d’autorités sera notamment utilisée pour vérifier que l’utilisateur a les droits nécessaires pour accéder à une ressource. Ainsi, si la ressource est accessible uniquement pour un rôle particulier, Spring Security va vérifier que ce rôle est bien présent dans la liste des autorités et si ce n’est pas le cas retourner une erreur HTTP 401.
Dernier point important et qui est non négligeable : l’encodage des mots de passe.
Comme dit précédemment, Spring Security va comparer les mots de passe ce qui suppose donc qu’ils aient tous les deux le même encodage. Dans le cas ou, dans notre base de données, les mots de passe soient encodés en SHA-256, il faudra alors donner l’information à Spring Security. Ce dernier va alors encoder en SHA-256 le mot de passe en entrée (input password) et comparer la chaine en sortie avec le mot de passe issue de la base de données (database password).
L’interface PasswordEncoder nous permet de répondre à cette problématique.
Encodage de mot de passe
Il existe deux implémentations de l’interface PasswordEncoder fournit par Spring Security : StandardPasswordEncoder et BCryptPasswordEncoder.
Spring Security recommande l’utilisation de BCryptPasswordEncoder qui se base sur un algorithme puissant avec un sel généré aléatoirement. Dans les versions précédentes du Framework, il était possible d’utiliser les classes MD5PasswordEncoder ou SHAPasswordEncoder mais elles sont désormais dépréciées à cause de la faiblesse de leur algorithme.
De plus ces deux classes obligeaient le développeur à passer en paramètre du constructeur le sel, alors que BCryptPasswordEncoder va générer en interne un sel de manière aléatoire. La chaine générée par BCryptPasswordEncoder sera d’une taille de 60 caractères et il conviendra donc que la colonne en base accepte une chaîne de cette taille.
La classe StandardPasswordEncoder quant à elle, se base sur un algorithme SHA-256.
Evidemment, les mots de passe des utilisateurs qui seront créés dans les systèmes tiers devront être encodés conformément au type d’encodage choisi dans Spring Security pour que leur authentification soit un succès.
Ainsi si l’on résume tout ce que l’on a vu jusque-là :
Security Filter Chain
Il nous reste désormais à voir comment l' AuthenticationManager est appelé par Spring Security et pour cela plusieurs scénarii sont possibles. Comme dit au tout début de l’article il existe plusieurs type d’authentification : http basic, http digest, based form, etc. et c’est de là que le lien est fait avec notre AuthenticationManager.
Mais avant de rentrer dans le détail, il est important de rappeler que Spring Security est entièrement basé sur des filtres (filters) et qu’ils sont tous exécutés les uns après les autres dans un ordre très précis.
Chacun de ces filtres ayant évidemment un rôle bien définis et indispensable à la sécurité de l’application : C’est ce qu’on appelle la “Security Filter Chain”.
Je ne détaillerai pas dans cet article l’architecture des filtres Spring mais retenez simplement qu’un certain nombre de ces filtres vont être activés en fonction du choix du type d’authentification fait pendant la configuration de Spring Security :
Authentification | Filtre |
---|---|
HTTP BASIC |
|
HTTP DIGEST |
|
Form Based Login |
|
Anonymous authentication |
|
Remember me Authentication |
Il ne s’agit que de quelques exemples de filtres parmi d’autres, mais chaque authentification possède son propre filtre qui sera alors ajouté à la Security Filter Chain suivant le mode d’authentification choisi lors de la configuration. Ces filtres ont tous un point commun : Ils font appel à une instance d' AuthenticationManager et notamment à sa méthode authenticate !
Autre particularité, ces filtres vont également, si l’authentification est un succès, stocker l’objet Authentication délivré par la méthode authenticate. En effet, après une authentification réussie, il est possible d’accéder à un instant T à l’utilisateur authentifié et à ses informations. Pour cela, on utilise la classe SecurityContextHolder qui contient le contexte de sécurité : SecurityContext.
SecurityContext étant une interface qui permet d’accéder à un objet Authentication.
Tous ces filtres vont donc à un moment donné, exécuter le code suivant:
1 |
SecurityContextHolder.getContext().setAuthentication(authentication); |
Et il est donc possible à n’importe quel moment de récupérer l’utilisateur authentifié :
1 |
SecurityContextHolder.getContext().getAuthentication() |
Pour les utilisateurs de Spring MVC, il existe même une annotation pour faire référence au principal stocké par Spring Security au sein de son contexte : @AuthenticationPrincipal.
Enfin, il est possible que vous ayez à exécuter l’authentification manuellement, par exemple si vous êtes dans une application “stateless” avec une authentification par “token”.
Pour cela, rien de très compliqué, il vous suffit de faire une injection (via @Autowired ou @Inject) sur l’interface AuthenticationManager et d’appeler la méthode authenticate. Il vous faudra également ne pas oublier de mettre le résultat dans le contexte via la classe SecurityContextHolder pour que Spring Security puisse correctement valider l’étape d’autorisation.
Conclusion
On peut désormais compléter notre schéma précédant pour avoir une vue globale du mécanisme d’authentification :
Dans le prochain article, nous verrons la deuxième partie majeure de Spring Security: L’autorisation.
Très bon article et très bonne explication.
On attends toujours la deuxième partie qui concerne l’autorisation…
Merci.
Tres bon article, il donne un overview de spring security, et le demistifie par la meme occasion. A cquand le prochain chapitre?
bon tuto
Merci beaucoup pour vos articles bien expliqués. ça fait plaisir de voir un bon article comme ceci écrit en français!.