Sécurité des applications Jakarta EE

  1. Objectif du TP
  2. Création du projet
  3. Protection des pages correspondant à un modèle d'URL
  4. Optionnel : Configurer des utilisateurs dans Payara
  5. Pas de protection pour les pages retournées en réponse à une requête POST
  6. Initialisation de la base de données des utilisateurs
  7. Configuration pour l'entrepôt d'identité, mécanisme d'authentification "basic"
  8. Écriture de la page de login, mécanisme d'authentification "custom form"
  9. Déconnexion
  10. Page d'erreur
  11. Login initié par l'utilisateur
  12. https

Objectif du TP :

Réserver l'accès à certaines des pages de l'application à des utilisateurs privilégiés, en utilisant l'authentification par le container.

L'application se repose sur le serveur d'application pour filtrer l'accès aux pages de l'application. Jakarta EE fournit une API pour la sécurité, qui simplifie l'authentification des utilisateurs ; ce TP utilise cette API.

Support de cours


Création du projet

Créez un nouveau projet "tpsecurite" de type Java Web, puis Web Application, avec utilisation de JSF, comme dans le TP sur PRG.

Refaites tout ce que vous avez déjà fait pour les TPs précédents :


Protection des pages correspondant à un modèle d'URL

Vous allez créer 3 répertoires pour les pages Web. admin sera réservé aux administrateurs, connect sera réservé aux utilisateurs connectés et ouvert sera ouvert à tous les utilisateurs, même ceux qui ne se sont pas authentifiés. Sous le répertoire racine des pages Web de votre application, créez ces 3 répertoires (clic droit sur "Web Pages" > New > Folder).

Ajoutez aussi une page index.xhtml à la racine du projet. Mettez cette page comme page "welcome" du projet (dans web.xml).

Ajoutez une page JSF index.xhtml dans chacun de ces répertoires. Dans la page située sous admin écrivez le texte "Réservé aux administrateurs", dans la page située sous connect écrivez "Réservé aux utilisateurs authentifiés" et dans la page située sous ouvert écrivez le texte "Ouvert à tous".

Dans ces 3 pages, ajoutez un lien vers cette page index.xhtml située à la racine du projet. Tous les liens de cet exercice seront de type <h:link>.

Dans la page "welcome" du projet ajoutez 3 liens vers les 2 pages protégées et la page ouvert à tous.

Testez le projet en passant d'une page à l'autre en utilisant les liens.

Faites un commit Git et un push sur GitHub. N'oubliez pas de le faire après avoir testé et validé chaque étape importante de ce TP (au moins à la fin de chaque question).

Modifiez le fichier web.xml pour que l'accès aux pages placées sous ce répertoire soient protégées par mot de passe. Les pages ne seront accessibles que par les utilisateurs qui ont le rôle "admin". Créez aussi un rôle "user" que vous allez utiliser pour tester les utilisateurs authentifiés qui n'ont pas le rôle "admin" (role-name a pour valeur "**" dans web.xml).

Testez à nouveau le projet (faites d'abord un "clean and build" pour redéployer l'application). Est-ce que les pages sont protégées ? Comment ?

Commit et push. N'oubliez pas pour la suite.

Correction

Lignes à ajouter à web.xml


Optionnel : Configurer des utilisateurs dans Payara

Il est possible de configurer des utilisateurs d'une façon statique dans Payara. C'est la configuration par défaut pour l'authentification des utilisateurs. Dans les exercices à suivre vous indiquerez un autre mécanisme d'authentification ("basic" ou "custom form") qui utilise les informations sur les utilisateurs enregistrées dans une base de données.

C'est un mécanisme basique qu'il n'est pas recommandé d'utiliser mais qui peut être utile pour faire des tests rapides. Nous verrons une autre façon plus sûre et plus souple de configurer les utilisateurs dans la suite de ce TP.

Pour cette configuration basique utilisez la console d'administration de Payara. Vous l'avez déjà utilisée pour l'installation des logiciels, au début de ce cours.

Dans la console d'administration ("realm" peut être traduit par "domaine" ; il peut exister plusieurs domaines avec des utilisateurs différents pour chaque domaine),

  1. Configurations > server-config > Security > Realms > file
  2. Clic sur « Manage Users ». Clic sur New. Ajouter 2 utilisateurs, leur mot de passe et leurs groupes (qui correspondent à leurs rôle). Créez un utilisateur qui a le groupe "admin" et un autre qui a le groupe "user".
  3. Il faut relancer Payara pour que les utilisateurs soient pris en compte.

Testez les accès aux pages avec les utilisateurs que vous venez de créer. Comme les données de la session sont conservées dans des cookies, il faut supprimer les cookies de votre navigateur après avoir rempli les données dans le formulaire qui protège les pages. Pour éviter de le faire, vous pouvez changer de navigateur entre 2 tests et/ou utiliser le mode "private" de votre navigateur ("fenêtre de navigation privée" par exemple dans Chrome), en changeant de fenêtre privée entre chaque test (les cookies ne sont pas gardés par ce type de fenêtre).

Supprimez les 2 utilisateurs quand vous en avez terminé avec cette question.


Pas de protection pour les pages retournées en réponse à une requête POST

Dans la page "welcome" placée à la racine de l'application, ajoutez un lien vers la page réservée aux administrateurs avec un composant JSF qui génère une requête POST, suivi d'un forward interne pour retourner la page protégée (<h:commandLink>).

Est-ce que la page est protégée en passant par ce lien ?

Correction

Page index.xhtml


Initialisation de la base de données des utilisateurs

Nous allons voir maintenant une autre façon de définir les utilisateurs d'une application, bien plus souple (pas besoin de relancer le serveur quand on ajoute des utilisateurs) et bien plus sure.

Les utilisateurs seront enregistrés dans une base de données.

Commencez par créer la base de données MySQL qui sera utilisée par ce TP. Nommez-la, par exemple, "tpsecurite".

Vous pourriez créer les tables et les données directement dans le gestionnaire de base de données. Dans cet exercice, vous allez voir comment faire créer au démarrage de l'application les tables et les données utilisées pour authentifier les utilisateurs. Vous avez déjà initialisé des données dans le TP sur les comptes bancaires ; cette fois-ci vous allez aussi créer les tables. Dans une application réelle, ces tables et ces données existeront déjà et cette étape ne sera pas nécessaire.

Commencez par créer une source données dans le serveur d'application comme vous l'avez fait dans l'installation des logiciels (avec la base de données tpsecurite à la place de la base de données customer). Cette source de données aura comme nom jndi "jdbc/tpsecurite".

Comme pour le TP sur JPA, créez une classe Init de type EJB singleton pour créer les tables et y ajouter des données. L'EJB devra être exécuté automatiquement au démarrage de l'application.

La structure des tables sera la plus simple possible. Toutes les colonnes sont de type varchar ; la taille est écrite entre parenthèses. Voici la structure :

Vous allez écrire une classe utilitaire util.HashMdp qui servira à crypter des mots de passe. Cette classe est un bean CDI que vous pourrez injecter dans vos classes si vous en avez besoin. Vous éviterez ainsi d'avoir à répéter le même code à plusieurs endroits de votre code.

Pour cette initialisation on peut utiliser JPA (avec EntityManager.createNativeQuery pour la création des tables) ou JDBC. La correction utilise JDBC (une petite révision...) car JPA pose un problème si on veut tester si les tables existent déjà. On n'a rien à faire pour créer les tables si la source de données est en mode "create-table" dans persistence.xml.

Comme la structure des tables est minimaliste c'est plus simple qu'avec JPA pour ajouter des données. Il serait possible d'ajouter d'autres informations dans ces tables ; par exemple des id non significatifs générés par le SGBD comme clé primaire, un email ou une adresse pour la table utilisateur.

La méthode d'initialisation devra tester si les tables ou les données existent déjà avant d'ajouter les tables ou les données.

Testez en lançant l'application. Lancez-la 2 fois pour voir si les données ne sont pas dupliquées. En fait il suffit de modifier du code pour que le déploiement soit effectué automatiquement et donc pour lancer l'exécution de la méthode init() de l'EJB Init.

Allez voir les données enregistrées dans les tables login et login_groupe. Remarquez que les 2 mots de passe cryptés ne sont pas égaux alors qu'ils correspondent tous les deux à "toto". En effet, l'algorithme de cryptage "sale" les valeurs cryptées pour compliquer la tâche des pirates informatiques.

Correction

EJB singleton


Configuration pour l'entrepôt d'identité, mécanisme d'authentification "basic"

Créez une classe Config dans les sous-paquetage config du paquetage de base de l'application. Elle est annotée pour la portée application de CDI. Une annotation CDI va lui permettre d'être découverte au déploiement par CDI (indispensable pour l'API de sécurité).

package xx.xxxx.xxxx.config;

import jakarta.enterprise.context.ApplicationScoped;

/**
 * Configuration de l'application pour la sécurité.
 */
@ApplicationScoped
public class Config {
  
}

Dans cette classe vous allez configurer l'entrepôt d'identité qui va servir à authentifier les utilisateurs.

Il suffit de donner les ordres SQL qui vont retourner le mot de passe et les groupes associés à un login donné.

Vous pouvez faire un test rapide en utilisant l'authentification "basic" par un formulaire généré par le navigateur. Cette authentification ne doit pas être utilisée normalement car elle génère des problèmes, par exemple pour faire des logout.

Pour ce test avec "basic", il suffit d'ajouter cette annotation à la classe Config (vous pouvez donner n'importe quel nom au realm) :
@BasicAuthenticationMechanismDefinition(realmName="user-realm")

Lancez l'application et testez les accès autorisés pour l'utilisateur "toto". Pour tester pour l'utilisateur "admin" utilisez un autre navigateur ou bien utilisez le mode "private" de votre navigateur ("fenêtre de navigation privée" par exemple dans Chrome), en changeant de fenêtre privée entre chaque test (les cookies ne sont pas gardés par ce type de fenêtre).

Dans la suite du TP vous allez utiliser un autre type d'authentification, plus sûr et qui permettra d'ajouter un bouton pour déconnecter les utilisateurs de la session (logout). Enlevez la ligne @BasicAuthenticationMechanismDefinition(realmName="user-realm")

Correction

Classe Config


Écriture de la page de login, mécanisme d'authentification "custom form"

Dans le mécanisme d'authentification "basic", le formulaire de login, qui demande le nom et le mot de passe de l'utilisateur", et son traitement sont fournis par le navigateur. Le plus souvent vous utiliserez votre propre formulaire de login et vous voudrez personnaliser son traitement. L'API de sécurité de Jakarta EE vous fournit tous les éléments pour cela.

Vous allez écrire une page JSF de login. Elle contient un formulaire qui demande le nom de login et le mot de passe de l'utilisateur. Au bas de cette page ajoutez un lien vers la page "welcome" de l'application pour le cas où l'utilisateur changerait d'avis.

Le backing bean qui traite le formulaire utilise la méthode SecurityContext.authenticate qui a 3 paramètres : la requête, la réponse et des paramètres qui contiennent en particulier les "credentials" qui vont servir à authentifier l'utilisateur. Pour notre cas les "credentials" sont le nom de login et le mot de passe. Attention à bien importer les bons types ; par exemple jakarta.security.enterprise.SecurityContext. et jakarta.security.enterprise.AuthenticationStatus.SEND_CONTINUE.

Il reste à configurer l'application pour que le container utilise cette page pour authentifier les utilisateurs. Pour cela annotez la classe Config avec @CustomFormAuthenticationMechanismDefinition.

Testez.

Correction

Classe Config
Page de login
Backing bean


Déconnexion

Dans la page "welcome" ajoutez un bouton (balise <h:commandButton>) qui permet de sortir de la session. Associez-le à une méthode logout() du backing bean LoginBean.

Pour vérifier plus facilement, ajoutez à la fin de la page "welcome" de l'application une zone (délimitée par une balise <h:panelGroup>) qui n'est affichée que si l'utilisateur est authentifié et qui affiche "Administrateur" si l'utilisateur est administrateur. Mettez le bouton qui permet de sortir de l'application dans cette zone.

Cette information et ce bouton pourrait être mis dans un template pour être affichés sur toutes les pages.

Correction

Logout


Pages d'erreur

Si l'utilisateur veut accéder à une page qu'il n'a pas le droit de voir une vilaine erreur 403 est affichée. Écrivez une page noauth.xhtml pour afficher un message plus agréable et qui a lien vers la page de "welcome" de l'application.

Il suffit alors d'ajouter une balise <error-page> dans le fichier web.xml pour le code d'erreur 403.

Correction

Page d'erreur


Login initié par utilisateur

Jusqu'à maintenant l'utilisateur devait s'authentifier lorsqu'il voulait accéder à une page protégée.

Vous allez ajouter un bouton qui permet à l'utilisateur de s'authentifier pour toute la session, même s'il n'accède pas à une page protégée. Ce bouton ne s'affiche que si l'utilisateur ne s'est pas déjà authentifié.

Après l'authentification, la page index.xhtml est affichée.

Correction

Page de login
Bouton vers la page de login
Backing bean


HTTPS

Les navigateurs n'acceptent plus les URL en "http" pour les sites non locaux qu'après avoir lancé un avertissement.

Tous les sites professionnels doivent avoir une adresse en "https". HTTPS est mis pour Hypertext Transfer Protocol Secure. Ce protocole nécessite d'obtenir un certificat SSL (voir fonctionnement de Secure Sockets Layer). Les données échangées entre le navigateur et le site Web sont chiffrées en utilisant ce certificat, ce qui rend plus difficile le vol ou l'interception de données.

Pour avoir un site en https, il faut

  1. Récupérer un certificat SSL.
  2. Configurer le serveur d'application pour qu'il utilise ce certificat.
  3. Ensuite, il suffit de taper dans un navigateur les mêmes URLs qu'avant (avec http) mais en remplaçant http par https.

Vous allez avoir besoin de la commande openssl. Elle n'est pas installée sous Windows. Si vous avez ce système d'exploitation il va donc falloir l'installer.

Récupération d'un certificat SSL

Pour une application en production, il faudrait récupérer un certificat signé par une autorité de certification de confiance ("trusted CA", d'après les intiales en anglais) qui va prouver l'identité du serveur dans ses échanges avec les clients. Vous pourriez par exemple utiliser un certificat de "Let's Encrypt", une autorité de certification à but non lucratif ; cet article explique comment faire.

Pour tester, il est possible d'utiliser un certificat auto-signé (https://blog.payara.fish/securing-payara-server-with-custom-ssl-certificate), et c'est ce que vous allez faire dans ce TP.

Vous allez tout d'abord créer un certificat pour un CA que vous allez créer et qui va signer le certificat du serveur (en production le CA devra être un CA de confiance connu de tous les navigateurs). Ensuite vous créerez le certificat du serveur ; pour les tests locaux, il correspondra à l'adresse "localhost".

Ouvrez un terminal pour taper des commandes. Créez un répertoire et placez-vous dedans.

Certificat du CA

Voici les étapes pour le certificat du CA (je ne détaille pas les options des commandes ; vous pouvez les trouver facilement sur le Web) :

  1. Tapez la commande pour générer la clé privée du CA et la mettre dans le fichier ca-key.pem :
    openssl genrsa -aes256 -out ca-key.pem 4096
    La commande demande une phrase secrète (un mot de passe ; il est recommandé de taper une phrase pour plus de sécurité). Retenez la bien pour la suite.
  2. Générez le certificat pour le CA à partir de la clé privée (le certificat sera mis dans le fichier ca.pem et expirera dans 365 jours) :
    openssl req -new -x509 -sha256 -days 365 -key ca-key.pem -out ca.pem
    La commande demande la phrase secrète (de l'étape précédente), un DN (Distinguished Name) composé d’un code pays (FR), état ou province (PACA), une ville (Nice), une organisation (ce que vous voulez), une unité dans l’organisation ((ce que vous voulez ou Enter pour ne rien indiquer), un nom commun (...), une adresse email (...). On peut entrer ce qu’on veut pour ces informations ; elles serviront à identifier le certificat.
    Vous pouvez examiner le contenu du certificat en tapant la commande
    openssl x509 -in ca.pem -text

Certificat autosigné (par le CA) pour le serveur

  1. Clé privée pour le certificat du serveur :
    openssl genrsa -out cert-key.pem 4096
  2. Créer une requête pour un certificat (CSR). Normalement on envoie cette requête au CA de confiance (vous pouvez mettre n'importe quoi à la place de "monordi") :
    openssl req -new -sha256 -subj "/CN=monordi" -key cert-key.pem -out cert.csr
  3. Créer un fichier extfile.cnf qui va contenir le paramètre surbjectAltNames dont la valeur désigne une adresse (un nom pour un DNS ou une adresse IP ; vous testerez en local sur localhost) qui indiquera le domaine pour lequel le certificat sera utilisé :
    echo subjectAltName=DNS:localhost > extfile.cnf
  4. Génération du certificat "authentifié" par le CA, dans le ficier cert.pem :
    openssl x509 -req -sha256 -days 3650 -in cert.csr -CA ca.pem -CAkey ca-key.pem -out cert.pem -extfile extfile.cnf -CAcreateserial
  5. Combiner le certificat et la clé privée en un seul fichier mycert.p12 :
    openssl pkcs12 -export -in cert.pem -inkey cert-key.pem -out mycert.p12 -name domain1_certificate
    Le fichier ".p12" est crypté avec un mot de passe qui est demandé pendant l'exécution de la commande. Choisissez le mot de passe que vous voulez.

Configuration de Payara

Cette configuration n'est pas standardisée. Elle dépend du serveur d'application.

Payara ne doit pas être démarré pendant ces manipulations.

  1. Importer le fichier mycert.p12 dans le keystore du domaine 1 de Payara (c'est le domaine que vous utilisez par défaut) :
    keytool -importkeystore -destkeystore C:\autresprogrammes\payara\payara-6.2023.2\payara6\glassfish\domains\domain1\config\keystore.jks -srckeystore mycert.p12 -srcstoretype PKCS12 -alias domain1_certificate
    keytool est un outil fourni avec le JDK de Java. Remplacez "C:\autresprogrammes\payara\payara-6.2023.2" par le répertoire d'installation de Payara.
    domain1_certificate est un alias qui va servir à identifier le certificat. Vous pouvez choisir un autre nom.
    La commande demande « Entrez le mot de passe du fichier des clés de destination : ». Entrez « changeit » qui est le mot de passe par défaut utilisé par Payara (en production il faudrait évidemment changer le mot de passe de Payara avec la commande "asadmin change-master-password" de Payara, et utiliser ce nouveau mot de passe pour cette commande keytool)
    La commande demande alors « Entrez le mot de passe du fichier des clé source: ». Entrez le mot de passe que vous avez utilisé pour la création du fichier "mycert.p12". Vérifiez que le fichier keystore.jks (du même répertoire que keystore.jks) a bien été modifié.
  2. Il reste maintenant à importer le certificat dans carcerts.jks (certificats de confiance de Payara) :
    keytool -importcert -trustcacerts -destkeystore C:\autresprogrammes\payara\payara-6.2023.2\payara6\glassfish\domains\domain1\config\cacerts.jks -file mycert.crt -alias domain1_certificate
    N'oubliez pas de remplacer "C:\autresprogrammes\payara\payara-6.2023.2" par le répertoire d'installation de Payara.
    La commande demande d'entrer le mot de passe du fichier des clés (entrez "changeit").
    La commande affiche les informations sur le certificat et demande si vous faites confiance à ce certificat. Répondez "oui" après avoir vérifié le certificat (au début vous allez trouver les informations que vous avez données pour le certificat, et plus loin "localhost" pour SubjectAlternative Name.
  3. Mise à jour le http listener de Payara avec la commande asadmin de Payara. Pour cela il faut démarrer Payara et lancer ensuite la commande
    asadmin set configs.config.server-config.network-config.protocols.protocol.http-listener-2.ssl.cert-nickname=domain1_certificate
    domain1_certificate est l'alias que vous avez choisi dans les commandes précédentes.
    La commande asdmin est placée dans le répertoire bin dans le répertoire d'installation de Payara (C:\autresprogrammes\payara\payara-6.2023.2\payara6\glassfish\bin par exemple).
    Dans une installation par défaut de Payara, http-listener-2 est responsable du traitement des connexions https sur le port 8181.
  4. Vérification : dans la console d'administration de Payara, Configurations > server-config > HTTP Service > HTTP Listeners > http-listener-2. On voit que la valeur du champ « Certificate NickName » a bien la valeur domain1_certificate.

Tests

Redémarrer Payara pour la prise en compte de sa configuration.

Lancez l'exécution d'un des projets des TPs, par exemple le TP sur PRG.

Si vous tapez alors cette adresse dans le navigateur,
https://localhost:8181/tpprg/formulaire_4.xhtml
il s'affiche un avertissement car le CA utilisé pour signer le certificat n'est pas connu du navigateur. Si on lui dit de continuer tout de même, le https de la barre de navigation du navigateur est barré.

Pour continuer le test jusqu'au bout, on pourrait ajouter le CA comme autorité de confiance pour tout l'ordinateur mais on va se contenter de l'ajouter pour le navigateur. Evidemment, si le certificat pour le serveur avait été obtenu d'une CA publique mondialement connue, cette dernière étape aurait été inutile.

Voici comment faire avec Chrome :

  1. Paramètres
  2. Chercher « sécurité »
  3. Sécurité navigation sécurisée et autres paramètres de sécurité
  4. Gérer les certificats d’appareils
  5. Une fenêtre « Certificats » s’ouvre. Onglet « Autorités de certification racines de confiance ».
  6. Clic sur « Importer ». Un wizard aide à l’importation. Clic sur Suivant. Parcourir les fichiers pour trouver le fichier ca.pem qui contient le certificat pour le CA (celui que vous avez créé ci-dessus).
  7. Clic sur Suivant. Installer le fichier. Chrome dit que l’importation a réussi. On peut alors voir que le certificat est bien dans la liste.

Si vous retapez l'URL du projet, tout se passe alors parfaitement, sans avertissement. https est utilisé à la place de http.


Retour