Accueil Nos publications Blog Les bases de la sécurité en développement Java EE

Les bases de la sécurité en développement Java EE

En tant que développeur, lorsque vous arrivez sur un projet, les concepts de sécurité ont déjà été traités et structurés.
Ainsi, pris dans l’urgence et la demande du client, on part tambour battant sur l’ajout d’une nouvelle fonctionnalité capitale, tel le changement crucial d’un libellé par exemple, sans forcément se pencher sur la sécurisation des ressources mises à disposition et des mécanismes que Java EE permet.
Je voudrais donc vous présenter un condensé de quelques concepts basiques concernant les aspects de sécurité de Java EE qui peuvent être oubliés.

Rappels

Authentication vs Authorization

En premier lieu, je pense qu’il est nécessaire pour la compréhension de la suite de définir deux termes :

  • Authentication : procédé permettant d’établir l’identité ou “principal” (en anglais) d’un utilisateur se connectant.
  • Authorization : procédé permettant d’établir le rôle d’une entité. Le rôle d’une entité identifiée détermine ses droits d’accès.

 

Ces 2 termes (en anglais) peuvent être confondus par abus de langage, mais ils représentent les 2 notions fondamentales de la sécurité d’une application Java EE. A retenir donc que l’authentification ne détermine pas directement les droits dont un utilisateur va disposer. C’est le procédé de « contrôle d’accès » ou authorization mis en place qui va associer à une entité le rôle.

La sécurité à tous les niveaux

Les problématiques de sécurité se répartissent sur 3 niveaux bien connues :

  • La sécurité de la couche applicative, où interviennent les mécanismes Java EE de sécurité déclarative et programmatique. Cette partie va déterminer les droits nécessaires pour accéder aux ressources applicatives.
  • La sécurité de la couche transport, où on cherche avant tout à appliquer un protocole de communication entre les interlocuteurs. Cette partie en Java EE est gérée au niveau des realms paramétrés dans le serveur.
  • La sécurité de la couche message, qui correspond grossièrement au chiffrage des données transmises. L’application peut différer selon le protocole de communication utilisé.

Realm

Un realm représente la politique de sécurité d’accès à une application. Un realm est une base de données d’utilisateurs qui peut être partagée entre plusieurs applications. Ce realm peut se présenter sous la forme d’un fichier, d’une base de données etc…
Un realm permet également la définition des éventuels protocoles de sécurité mis en place. Il est par exemple possible de paramétrer un realm pour réaliser une authentification via HTTPS/SSL, JAAS, LDAP etc…
C’est donc dans la configuration du realm que va se gérer la sécurité au niveau transport et tout ce qui concerne l’interface accès-distant.

User, Group et Role

 

Gérés au niveau applicatif, les Users, Groups et Roles vont nous permettre de définir les identités des utilisateurs, ou applications, et leurs droits d’accès.

  • Un User représente l’identité d’un utilisateur physique ou d’une application qui a été définie au sein d’un realm. On peut les associer au sein d’un Group et leur appliquer un Role. Lors de la phase d’authentification, c’est l’identité du user (ou principal) qui est récupérée.
  • Un Group est un ensemble de Users. Il est possible de lui associer un Role.
    Un Role représente les droits d’accès d’un User ou d’un Group. Lors de la phase d’autorisation, c’est le Role de l’entité identifiée (User ou Group) qui va définir ses droits d’accès.

 

realmusergrouproleapp

Quelques Mécanismes Java

Pour en finir avec les rappels, voici quelques mécanismes de sécurité présents dans le Java SE, qui sont utilisés par les serveurs Java EE (sans que l’on ait forcément à les manipuler directement) :

  • JAAS (Java Authentication and Authorization Service) : Ensemble d’API permettant l’authentification des user, group et la gestion des droits d’accès associés aux Roles.
  • Java GSS-API (Java Generic Security Service) : API d’échanges de messages basée sur l’échange de jeton/token.
  • JCE (Java cryptography extension) : Framework de chiffrement, génération et décryptement de clefs.
  • JSSE (Java Secure Socket Extension) : Implémentation des protocoles SSL (Secure Socket Layer) et TLS (Transport Layer Security).
  • SASL (Simple Authentication and Security Layer) : Standard Internet (voir RFC 2222) qui définit un protocole d’échange client-serveur.

 

Mise en place de mécanismes JAAS

Voyons maintenant ce que nous pouvons faire avec les concepts vus précédemment.
Le but ici va être de présenter la mise en place d’un realm et la définition des Users, Groups et Roles applicatifs.
Pour ce faire, nous allons utiliser les mécanismes JAAS que proposent de base les serveurs GlassFish et Wildfly. Il existe la possibilité de mettre en place des politiques de sécurité plus poussées utilisant SSL par exemple. Généralement ces mécaniques plus poussées seront principalement à configurer au niveau du realm.
Wildfly et GlassFish sont tous les deux des serveurs qui implémentent les spécifications Java EE. Wildfly est une distribution fournie par Jboss tandis que GlassFish est fournie par Oracle.

Utilisation d’un realm-file

Un realm est avant tout une base de données donnant accès aux Users. Cette base peut se présenter sous la forme d’un simple fichier (qui est parfois le realm par défaut).
Dans le cas de GlassFish : en version 4.1.1, la création du realm passe par la console d’administration du serveur :

 

consoleglassfish

On a ainsi accès au menu de gestions des realms du serveur :

consoleglassfish_1

Dans la liste des realms affichés, on peut alors sélectionner le realm file. Il s’agit du FileRealm par défaut livré dans la distribution GlassFish.

consoleglassfish_2

On voit alors les éléments suivants :

consoleglassfish_3

  • JAAS Context : désigne le module utilisé pour gérer la phase *authentication*. Il s’agit en fait d’une variable permettant au serveur de déterminer la classe à utiliser selon le type de login souhaité.
  • Key file : représente le chemin où l’on pourra trouver le fichier contenant la liste des utilisateurs et les mots de passe associés.

NOTE : la variable ${com.sun.aas.instanceRoot}} correspond au répertoire dans lequel se trouve le domaine applicatif et pas à l’installation directe du serveur. Ex : (glassfish-4.1.1\glassfish\domains\domain1\config\keyfile).
On peut également voir sur ce dernier écran le bouton Manage Users. Celui-ci nous permet de gérer les différents Users du realm :

consoleglassfish_5

Pour créer un user, il suffit alors de rentrer un identifiant et un mot de passe (le groupe est optionnel) :

 

consoleglassfish_6

 

Une fois le user créé, on pourra aller vérifier dans le fichier configKeyFile la présence du nouveau user (id, mot de passe crypté ici par SSH256, group):

consoleglassfish_7

Dans le cas de Wildfly, nous allons voir que les choses sont un peu différentes.
En effet, tout tourne autour du fichier wildfly-8.1.0.Final\wildfly-8.1.0.Final\standalone\configuration\standalone.xml (ou bien domain.xml si l’on tourne sur des architectures plus complexes).
Wildfly propose par défaut un realm-file ApplicationRealm. Ce realm par défaut est basé sur les fichiers suivants :

  • wildfly-8.1.0.Final\wildfly-8.1.0.Final\domain\configuration\application-users.properties
  • wildfly-8.1.0.Final\wildfly-8.1.0.Final\domain\configuration\application-roles.properties

Si l’on regarde dans le fichier standalone.xml, on y trouve la description xml du realm :



<security-realm name="ApplicationRealm">
 <authentication>
 <local default-user="$lo
 cal" allowed-users="*" skip-group-loading="true"/>
 <properties path="application-users.properties" relative-to="jboss.server.config.dir"/>
 </authentication>
 <authorization>
 <properties path="application-roles.properties" relative-to="jboss.server.config.dir"/>
 </authorization>
 </security-realm>


  • authentication : Cette partie définit les mécanismes d’identification des utilisateurs. Il définit également où va se trouver le fichier contenant la liste des users.
    • local : Cette balise permet de préciser le comportement du serveur lors d’une connexion réalisée localement. On a la propriété allowed-users qui autorisera toutes les tentatives de connexion.
    • properties path : On définit ici le fichier à utiliser pour contenir les users. Par défaut, le fichier est application-users.properties.
  • authorization : Cette partie définit les mécanismes d’autorisation du realm.
    • properties path : Comme pour l’identification, on a ici un fichier qui va stocker les différents rôles application-roles.properties.

 

Il faut donc soit modifier soi-même les fichiers nécessaires, soit utiliser les scripts fournis par Jboss dans le répertoire suivant :

  • wildfly-8.1.0.Final\wildfly-8.1.0.Final\bin

En utilisant le script add-user.bat (.sh si vous êtes un vrai), on obtient la console suivante :

consoleglassfish_8

 

En gros, ce script demande la sélection du type de user à rajouter (ici on cherche à appliquer un Application user), un identifiant, un mot de passe et éventuellement l’ajout dans un Role ou non. Il est également proposé de préciser si le nouveau user sera utilisé pour des appels à distance EJB.
Après son exécution, on a dans les 2 fichiers concernés les entrées suivantes :

consoleglassfish_9

On voit que les approches Wildfly et GlassFish sont assez différentes.
Dans un cas, vous avez une approche basée sur une IHM et dans l’autre, une vision plus manuelle. A chacun de se faire son avis sur l’intérêt de telle ou telle approche.
La chose à retenir principalement est que la façon de configurer un realm est dépendante du serveur sur lequel vous travaillez. Même si le but reste le même, pour définir une base de données, il sera nécessaire de s’adapter à l’environnement à chaque fois.
NOTE : Pour la suite de l’article, j’utiliserai uniquement Wildfly pour réaliser mes exemples.

Utilisation d’un « realm » « Jdbc »

A présent, si l’on cherche à corser un peu plus les choses et ne plus stocker nos utilisateurs dans un fichier géré par le serveur, mais dans plutôt dans une base de données relationnelle indépendante, on va devoir mettre en place un realm-Jdbc dont la configuration permettra l’identification et l’autorisation des utilisateurs à partir de tables en base de données.

Dans un premier temps, définissons la partie SQL.

Pour cet article, j’ai travaillé avec une base MariaDb. Pour l’utiliser, vous devrez vous assurer que votre serveur dispose des drivers MysqlJdbc. S’ils ne sont pas disponibles, il est nécessaire de les déployer sur votre serveur.
Voici un exemple de structure SQL à insérer en base :


CREATE TABLE `users` (
`username` VARCHAR(255) NOT NULL COLLATE 'utf8_bin',
`passwd` VARCHAR(255) NULL DEFAULT NULL COLLATE 'utf8_bin',
PRIMARY KEY (`username`)
)
COLLATE='utf8_bin'
ENGINE=InnoDB
;
CREATE TABLE `userroles` (
`username` VARCHAR(255) NULL DEFAULT NULL COLLATE 'utf8_bin',
`role` VARCHAR(32) NULL DEFAULT NULL COLLATE 'utf8_bin'
)
COLLATE='utf8_bin'
ENGINE=InnoDB
;
Une fois les tables créées on pourra insérer les utilisateurs nécessaires pour les tests :
INSERT INTO `userroles` (`username`, `role`) VALUES
('admin', 'ADMIN');
INSERT INTO `users` (`username`, `passwd`) VALUES
('admin', 'jGl25bVBBBW96Qi9Te4V37Fnqchz/Eu4qB9vKrRIqRg=');

NOTE : le champ passwd correspond à la valeur admin encodée par là l’outil fournit avec wildfly (SHA-256 correspond à l’algorithme d’encryptage utilisé) :
java -cp %JBOSS_HOME%\modules\system\layers\base\org\picketbox\main\picketbox-4.0.21.Final.jar org.jboss.security.Base64Encoder admin SHA-256

Configuration xml du realm dans wildfly

Nous avons maintenant notre utilisateur disponible dans une base de données. A présent nous allons avoir pour but de configurer la partie realm de notre serveur Wildfly.
Wildfly différencie les realms et les security-domain. Globalement, la partie realm définit l’interface vers l’extérieur tandis que le security-domain peut se voir déléguer l’authentification.
Voici un exemple de configuration de realm-Jdbc pour wildfly :


<security-domains>
....
<security-domain name="jdbcDomain" cache-type="default">
 <authentication>
 <login-module code="Database" flag="required">
 <module-option name="dsJndiName" value="java:jboss/datasources/security_ds"/>
 <module-option name="principalsQuery" value="select passwd from Users where username=?"/>
 <module-option name="rolesQuery" value="select role, 'Roles' from UserRoles where username=?"/>
 <module-option name="hashAlgorithm" value="SHA-256"/>
 <module-option name="hashEncoding" value="base64"/>
 </login-module>
 </authentication>
 </security-domain>

</security-domains>

 

On crée le security-domain “jdbcDomain” :

  • login-module : définit le module à utiliser pour le login. Le champ code précise au serveur d’utiliser le module database. Le module, pour fonctionner, va avoir besoin de se voir valoriser quelques champs. On peut trouver la liste des sous-modules prévus pour Wildfly ici : https://docs.jboss.org/author/display/WFLY8/Authentication+Modules
  • module-option name=”dsJndiName” : Ce champ permet de préciser la datasource à utiliser. La datasource est une uri qui pointe sur notre base de données.
  • module-option name=”principalsQuery” : Ce champ définit la requête à utiliser pour récupérer les mots de passe à partir des identifiants des utilisateurs. Cette requête sera utilisée pour s’assurer de la véracité des mots de passe transmis.
  • module-option name=”rolesQuery” : La requête qui va charger en mémoire les rôles de l’utilisateur.
  • module-option name=”hashAlgorithm” : L’algorithme à utiliser pour encrypter/décrypter le mot de passe.
  • module-option name=”hashEncoding” : L’encodage à utiliser.

Authentification basic vs form

Nous avons pour l’instant 2 manières de gérer notre base d’utilisateurs. Voyons ce que la configuration de l’application en elle-même peut fournir comme mécanismes.
Concernant l’authentification, 3 mécanismes principaux sont disponibles :

  • basic : L’authentification basique. Lors d’une demande d’accès à une ressource protégée, une popup apparaît demandant à l’utilisateur de s’identifier.
  • form : L’authentification par formulaire. Lors de la demande d’accès à une ressource protégée, le serveur redirige l’utilisateur vers une page de login dédiée.
  • digest : Authentification basique dans laquelle les informations envoyées sont cryptées.

Nous allons nous pencher sur la mise en place des 2 premières.

Authentification basic

Pour mettre en place une authentification basique dans notre application, nous allons configurer 3 éléments dans le fichier web.xml :

  • login-config : définit la méthode de login utilisée, le realm à utiliser.


<login-config>
<auth-method>BASIC</auth-method>
<realm-name>ApplicationRealm</realm-name>
</login-config>

 

  • security-constraint : définit les web-resource à protéger, les auth-constraint et user-data-constraint.
    • web-resource-collection : détermine les ressources protégéess par un pattern url (ici les ressources /faces/admin/*). Détermine également les méthodes qui ont accès aux ressources (ici GET et POST).
    • auth-constraint : détermine les rôlessqui auront accès aux ressources.
    • user-data-constraint : détermine le mode de communication entre le client et le serveur. Il existe 3 valeurs possibles NONE pour aucune contrainte de communication, INTEGRAL où l’intégrité du message doit être préservée et CONFIDENTIAL où le message ne doit pas pouvoir être lu par un tiers (dans les faits INTEGRAL et CONFIDENTIAL sont souvents gérés par un même protocole) .


<security-constraint>
 <display-name>Admin Pages</display-name>

<web-resource-collection>
 <web-resource-name>Protected Admin Area</web-resource-name>
 <description></description>
 <url-pattern>/faces/admin/*</url-pattern>
 <http-method>GET</http-method>
 <http-method>POST</http-method>
 </web-resource-collection>
 <auth-constraint>
 <role-name>admin</role-name>
 </auth-constraint>

<user-data-constraint>
 <transport-guarantee>NONE</transport-guarantee>
 </user-data-constraint>
 </security-constraint>

<security-constraint>
 <display-name>All Access</display-name>

<web-resource-collection>
 <web-resource-name>None Protected User Area</web-resource-name>
 <description></description>
 <url-pattern>/faces/users/*</url-pattern>
 <http-method>GET</http-method>
 <http-method>POST</http-method>
 </web-resource-collection>

<user-data-constraint>
 <transport-guarantee>NONE</transport-guarantee>
 </user-data-constraint>
 </security-constraint>


 

Ici on a 2 éléments security-constraint. L‘un précise que l’accès aux ressources présentes dans /faces/admin/* sera donné aux méthodes GET et POST pour les utilisateurs ayant le rôle users. L’autre précise que l’accès aux ressources dans /faces/users/* sera accessible sans authentification pour les méthodes GET et POST.

On a enfin une dernière partie nécessaire. L’élément security-role qui permet de déclarer les rôles utilisés par l’application :


<security-role>
 <role-name>users</role-name>
 </security-role>

 

Authentification form

Pour réaliser une authentification de type form, il sera nécessaire de déclarer une page de login pour rediriger l’utilisateur et une page d’erreur. On aura donc quelque chose comme :



 <login-config>
        <auth-method>FORM</auth-method>
        <realm-name>ApplicationRealm</realm-name>
        <form-login-config>
            <form-login-page>/login.xhtml</form-login-page>
            <form-error-page>/error.xhtml</form-error-page>
        </form-login-config>
    </login-config>


 

  • form-login-config : la configuration du login par form.
    • form-login-page : la page de login où l’utilisateur sera redirigé lors d’une tentative de connexion.
    • form-error-page : en cas d’erreur de login, l’utilisateur sera redirigé vers cette page.

 

Une fois la configuration dans le web.xml réalisée, il est également nécessaire de déclarer le form dans votre page login.xhtml :


<form method="post" action="j_security_check">
    <input type="text" name="j_username">
    <input type="password" name= "j_password">
</form>

NOTE : il est important de noter que les valeurs j_security_check, j_username et j_password doivent être respectées. Elles permettent au serveur de détecter que ce formulaire est utilisé pour le loggin de l’application.

 

Sécurité déclarative vs programmatique

 

Java EE 7 permet 2 types d’approche pour sécuriser une application et ses ressources. La sécurité déclarative et la sécurité programmatique.

  • déclarative : Comme son nom l’indique, il s’agit ici de déclarer les éléments en dehors du code implémenté. La configuration du web.xml fait partie de cette sécurité. Il est également possible compléter cette configuration par la déclaration d’annotations directement dans les classes à protéger.
  • programmatique : La sécurité programmatique sera mise en place directement dans le code implémenté. Elle est utile lorsque des règles métiers doivent être prises en compte dans la mise en place de la sécurité pour une ressource donnée (ex : si l’accès à une fonctionnalité de vente n’est disponible que sur une certaine plage horaire). Notons également que la sécurité programmatique peut vous permettre d’implémenter vous-mêmes vos modules d’authentification et d’autorisation.

 

 

Concernant la sécurité déclarative

On a déjà vu plus haut ce qu’il est possible de faire au niveau web.xml. Voyons quelques exemples de ce qu’il est possible de faire au niveau du code avec des annotations.

  • Protection d’une servlet : La protection d’une servlet par annotation va nécessiter les mêmes informations que pour dans unweb.xml.
    • @WebServlet : permet de déclarer la classe comme une servlet et de l’associer à un pattern url.
    • @ServletSecurity : permet de déclarer nos HttpContraint. Ici, on précise grâce à rolesAllowed que les utilisateurs ADMIN sont les seuls autorisés pour cette ressource.

 


@WebServlet(name = "declarativelyCheckingUser", urlPatterns = { "/declarativelyCheckingUser" }) 
@ServletSecurity(@HttpConstraint(transportGuarantee = TransportGuarantee.NONE, rolesAllowed = {"ADMIN" }))
public class CheckingUserWithAnnotation extends HttpServlet {

Concernant la sécurité programmatique

On peut ressortir 4 méthodes importantes à utiliser :

  • méthode login : Cette méthode réalise une authentification en recevant les identifiants et mots e passe comme paramètres. Son utilisation implique donc que le code dispose de ces informations aux préalables.

protected void processRequest(HttpServletRequest request, HttpServletResponse response)
 throws ServletException, IOException {
 response.setContentType("text/html;charset=UTF-8");
 PrintWriter out = response.getWriter();
 try {

out.println("<html>");
 out.println("<head>");
 out.println("<title>Servlet TutorialServlet</title>");
 out.println("</head>");
 out.println("<body>");
 request.login("admindmin");
 out.println("<h1> login : ok!!</h1>");
 out.println("</body>");
 out.println("</html>");
 } catch (Exception e) {
 throw new ServletException(e);
 } finally {
 request.logout();
 out.close();
 }
 }
 
  • méthode authenticate : Cette méthode demande au serveur de réaliser une authentification de type BASIC. Lors de son appel, une popup sera affichée pour demander à l’utilisateur de s’authentifier.

protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
 response.setContentType("text/html;charset=UTF-8");
 PrintWriter out = response.getWriter();
 try {
 request.authenticate(response);
 out.println("Authenticate Successful");
 } finally {
 out.close();
 }

 

  • méthode isUserInRole : Cette méthode réalise un test sur l’utilisateur connecté et vérifie s’il dispose bien du rôle passé en paramètre.
  • méthode getPrincipal : Cette méthode renvoie l’objet représentant l’utilisateur connecté pour la session courante.

 


protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
 response.setContentType("text/html;charset=UTF-8");
 PrintWriter out = response.getWriter();
 try {
 
 // on affiche une fenêtre de connexion pour l'utilisateur
 request.authenticate(response);
 
 out.println("<html>");
 out.println("<head>");
 out.println("<title>Servlet CheckingUser</title>");
 out.println("</head>");
 out.println("<body>");
 
 out.println("L'utilisateur est il dans le rôle ADMIN ? : " + (request.isUserInRole("ADMIN")?"OUI":"NON") );
 out.println("</br>");
 out.println("L'utilisateur est il dans le rôle USER ? : " + (request.isUserInRole("USER")?"OUI":"NON") );
 out.println("</br>");
 out.println("L'utilisateur est il authentifié (avant déconnexion) ? : " + (request.getUserPrincipal()!=null?"OUI":"NON") );
 out.println("</br>");
 
 request.logout();
 
 out.println("L'utilisateur est il authentifié (après déconnexion) ? : " + (request.getUserPrincipal()!=null?"OUI":"NON") );
 
 out.println("<h1> checking user : ok!!</h1>");
 out.println("</body>");
 out.println("</html>");
 } finally {
 out.close();
 }
 

 

Utilisation des credential

Qu’est-ce qu’un credential? Un credential représente l’accréditation de l’utilisateur transmise par une application Web aux services métiers appelés.

consoleglassfish_10

Lorsque l’application Web nécessite l’appel à un service externe, elle va alors générer un credential qui sera interprété par le conteneur d’EJB pour autoriser ou non l’accès.

Pour gérer l’accès à vos EJBs, on pourra notamment utiliser les annotations suivantes :

  • @DeclareRoles : Permet de déclarer les rôles qui seront utilisés pour votre EJB. Cette annotation se place généralement au niveau de la classe.

@DeclareRoles({"ADMIN", "USERS"})
public class EjbCredentialTest{
  • @RolesAllowed : Cette annotation permet de définir les rôles donnant accès à un EJB ou un service précis.


@RolesAllowed(« USERS »)
public class UneClasse{

@RolesAllowed("Admin")
public void uneMethode(int a) {

  • @PermitAll : Permet l’accès à tous les principal. Se place au niveau d’une méthode.
  • @DenyAll : Enfin c’est anotation précise qu’aucun principal ne pourra accéder à la méthode. Cela signifie que la méthode ne peut être appelée dans le contexte du conteneur Java EE.

 

Conclusion

Voilà, j’ai essayé de résumer la plupart des points-clé que j’ai pu rencontrer.

S’il y a une chose à retenir, c’est que la sécurité en Java EE est un domaine extrêmement vaste. Nous n’avons ici fait qu’effleurer la surface, par exemple nous n’avons pas abordé la mise en place des nombreux protocoles de sécurité qui existent.

L’important en tant que développeur reste d’avoir conscience de ce qui se passe sous le moteur un minimum, gardons donc en mémoire que nous avons 3 domaines : la configuration du serveur et des realms (qui est un métier en soit), la configuration de l’application (web.xml et annotations), et l’implémentation du code.

Vous pourrez trouver quelques exemples d’implémentation sur le repo github.

Sources :

49.2 Securing Enterprise Beans

JBoss Documentation

JDBC Realm and Form Based Authentication with WildFly 8.2.0.Final, Primefaces 5.1 and MySQL 5