Changement de mot de passe par l'utilisateur en JSF

Dans la continuité de l'article traitant de la configuration d'un realm JDBC dans GlassFish, je vous propose à présent de réaliser un formulaire JSF destiné aux utilisateurs d'un application web sécurisée et leur permettant de changer leur mot de passe dont l'empreinte MD5 est stockée dans une base de données MySQL.
L'accès aux données d'authentification des utilisateurs dans la base de données sera effectué en JDBC.

La plateforme technique utilisée pour rédiger cet article est constituée du JDK version 1.6, du serveur GlassFish version 3.1.2 qui inclut la librairie d'implémentation JSF Mojarra version 2.1.6 et du serveur de base de données MySQL version 5.6.

Le scénario de changement de mot de passe

Le scénario de changement de mot de passe que nous allons suivre dans cet article est le suivant :
  1. L'utilisateur saisit son mot de passe actuel et à deux reprises son nouveau mot de passe, la 2ème fois servant à vérifier qu'il ne s'est pas trompé dans sa saisie. Il soumet sa demande de changement de passe à l'application web.
  2. L'application web effectue les tâches suivantes :
    1. Elle contrôle que le nouveau mot de passe saisi la 1ère fois est identique à celui saisi la 2ème fois pour confirmation.
    2. Elle s'assure que le nouveau mot de passe est différent du mot de passe actuel.
    3. Elle génère l'empreinte MD5 du mot de passe actuel et vérifie qu'elle est identique à celle du mot de passe stockée en base de données.
    4. Elle met à jour l'empreinte MD5 du nouveau mot de passe en base de données.
    5. Elle confirme enfin à l'utilisateur que son mot de passe a été changé.

L'application web de démonstration demo_changepwd

Pour illustrer la fonctionnalité de changement de mot de passe décrite dans le scénario précédent, nous allons réaliser une application web constituée des ressources suivantes :

  • 1 vue JSF / Facelet nommée change_password.xhtml contenant le formulaire web de changement de mot de passe.
  • 1 managed bean nommé AuthenticationBean dont les propriétés correspondent aux champs du formulaire de la vue change_password.xhtml et comprenant une méthode publique exécutée en réponse à l'action du formulaire et chargée de contrôler la saisie et de mettre à jour le nouveau mot de passe en base de données. D'autres propriétés et méthodes privées viendront compléter le managed bean pour interroger et mettre à jour la table des mots de passe et pour hacher en MD5 les mots de passe.

A noter que cette application de démonstration n'inclut pas de mécanisme d'authentification préalable à la fonctionnalité de changement de mot de passe. J'invite les lecteurs à se référer aux articles indiqués au paragraphe Articles connexes pour les aider à mettre en oeuvre un mécanisme d'authentification dans GlassFish.

L'accès JDBC à la base de données

Le choix de lire et surtout de modifier le mot de passe dans la base de données en passant directement par l'API JDBC est motivé par le fait que de cette manière-là, on s'assure que le nouveau mot de passe est mis à jour sans délais dans la base de données.
En effet, le mot de passe est également lu en JDBC par le mécanisme d'authentification de GlassFish. Or, si l'on met à jour le mot de passe en passant par un framework de persistence gérant sa propre mémoire cache de données (JPA/EJB, Hibernate...), on ne maîtrise pas le moment où le mot de passe sera effectivement modifié dans la base de données.

Cette solution ne nous empêche pas au sein d'une même application web, de gérer aussi les accès en base de données des entités métier en s'appuyant cette fois-ci, sur un framework de persistence avec les avantages qu'il apporte.

Base de données utilisée

Nous n'accéderons dans l'application demo_changepwd qu'à la table USERS contenant le mot de passe des utilisateurs. Cette table est celle décrite au paragraphe Création des tables d'authentification dans MySQL de l'article Configurer un realm MySQL dans GlassFish. Elle est constituée des colonnes USER_LOGIN et PASSWORD.

L'empreinte du mot de passe est stockée après hachage en MD5.

Configuration du Serveur Web

Nous déploierons notre application de démonstration dans le serveur GlassFish.

Pour accéder à la base de données contenant la table USERS, nous devons déclarer dans GlassFish un pool de connexion JDBC à la base de données ainsi qu'une ressource JNDI associée que nous invoquerons dans le code Java.

Pour cela, je vous invite à suivre la procédure décrite aux paragraphes Creation d'un pool de connexion JDBC à la base de données MySQL et Creation d'une ressource JDBC pour le pool de connexion testPool de l'article Configurer un realm MySQL dans GlassFish

L'intérêt de se connecter à la base de données en passant par une ressource JDBC est principalement de rendre l'application web indépendante de la source de données qu'elle utilise. L'identifiant, le mot de passe, le nom de la base de données et le SGBD sont configurés dans le pool de connexion JDBC au niveau du conteneur de servlets et peuvent évoluer sans impacter l'application web, dès lors que l'URL JNDI de la ressource JDBC reste inchangée.

Accès à la table des mots de passe depuis le Managed Bean

Il est nécessaire d'accéder à la base données à la fois en lecture (contrôle du mot de passe courant) et en écriture (mise à jour du nouveau mot de passe).

Pour cela, nous déclarons dans le managed bean une propriété privée nommée datasource instanciée par injection de la ressource JNDI jdbc/test configurée pour accéder à la base de données MySQL, et cela grâce à l'annotation @Resource(name="jdbc/test").

La source de données étant disponible à travers la propriété datasource, la lecture du mot de passe actuel peut alors intervenir dans la méthode private Boolean isPasswordValid() qui retourne true si l'empreinte du mot de passe courant (propriété currentPassword) obtenue après hachage MD5, est conforme à celle stockée dans la table USERS pour l'identifiant de connexion LOGIN_NAME égal à la propriété loginName.

Le code de la méthode isPasswordValid() est le suivant :

    private Boolean isPasswordValid(Connection connection) throws SQLException {
        // Querying the user's password in the table 'users'...
        PreparedStatement statement = connection.prepareStatement(
                   "select password from users where login_name = ?"); 
        statement.setString(1, this.loginName);
        ResultSet result =  statement.executeQuery();
        if (result.next()) {
            String dbCryptedPassword = result.getString("password");
            statement.close();
 
            String cryptedCurrentPassword = this.getCryptedPassword(this.currentPassword);
 
            //Logging crypted password values to be compared... 
            Logger.getLogger(AuthenticationBean.class.getName()).log(Level.INFO,
                    "Current password once encrypted for {0}: {1}", new Object[]{this.loginName, cryptedCurrentPassword});
            Logger.getLogger(AuthenticationBean.class.getName()).log(Level.INFO,
                    "Password in DB for {0}: {1}", new Object[]{this.loginName, dbCryptedPassword});
 
            //Both current and DB passwords are the same?
            if (cryptedCurrentPassword.equals(dbCryptedPassword))
                return true;
            else return false;
        } 
        else {
            Logger.getLogger(AuthenticationBean.class.getName()).log(Level.WARNING,
                    "User not found in the table USERS for the login name ''{0}''!", this.loginName);        
            return false;
        }
    }
Code 1 : méthode AuthenticationBean.isPasswordValid() de l'application demo_changepwd

Vous noterez à la lecture de ce code que le hashage du mot de passe courant est assuré par la méthode privée getCryptedPassword() du managed bean, dont le code vous sera présenté plus loin dans cet article.

Concernant la mise à jour du nouveau mot de passe dans la table USERS, elle est assurée par la méthode privée void updatePassword().
Son code est le suivant :

    private void updatePassword(Connection connection) throws SQLException {
        // Updating the user's crypted password in the table 'users'...
        PreparedStatement statement = connection.prepareStatement(
                   "update users set password = ? where login_name = ?"); 
        statement.setString(1, this.getCryptedPassword(this.newPassword));
        statement.setString(2, this.loginName);
        statement.executeUpdate();
        statement.close();
    }
Code 2 : méthode AuthenticationBean.updatePassword() de l'application demo_changepwd

La vue change_password.xhtml

Le formulaire de changement de mot de passe est créé dans la vue JSF / Facelet change_password.xhtml avec la balise <h:form/>.
Les champs du formulaire sont au nombre de 4 : le premier pour saisir l'identifiant de connexion, les 3 suivants pour saisir le mot de passe actuel et le nouveau mot de passe (double saisie du nouveau mot de passe pour confirmation).

La saisie de l'identifiant de connexion n'est pas nécessaire dans une application web incluant un mécanisme d'authentification et peut être seulement affiché pour information dans le formulaire à partir de la propriété remoteUser de l'objet request (classe javax.servlet.http.HttpServletRequest).

Le bouton de validation du formulaire est créé avec le composant <h:commandeButton/> dont l'action déclenche la méthode AuthenticationBean.changePassword().

Les messages de l'application sont affichés par le composant <h:messages/>.

Le code de la vue change_password.xhtml est le suivant

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html">
    <h:head>
        <title>Changing password</title>
    </h:head>
    <h:body>
        <h3>Change your password</h3>
        <h:messages/>
        <h:form>
            <h:outputLabel for="login_name">Login Name:</h:outputLabel><br/>
            <h:inputText id="login_name" label="Login name" required="true" value="#{authenticationBean.loginName}"/><br/>
            <h:outputLabel for="current_password">Current password:</h:outputLabel><br/>
            <h:inputSecret id="current_password" label="Current password" required="true" value="#{authenticationBean.currentPassword}" /><br/>
            <h:outputLabel for="new_password">New password:</h:outputLabel><br/>
            <h:inputSecret id="new_password" label="New password" required="true" value="#{authenticationBean.newPassword}" /><br/>
            <h:outputLabel for="new_password_confirmation">New password for confirmation:</h:outputLabel><br/>
            <h:inputSecret id="new_password_confirmation" label="New password for confirmation" required="true" value="#{authenticationBean.newPasswordConfirmation}" /><br/>
            <h:commandButton value="Submit" action="#{authenticationBean.changePassword}"/>
        </h:form>
    </h:body>
</html>
Code 3 : vue Facelet change_password.xhtml de l'application demo_changepwd

Logique applicative

Action exécutée à la validation du formulaire

A la validation du formulaire, la méthode changePassword() du managed bean est exécutée.
Elle réalise les contrôles sur les données saisies comme indiqué au paragraphe Le scénario de changement de mot de passe pour les points 2.a et 2.b.

Ensuite, elle appelle la méthode privée isPasswordValid() pour authentifier l'utilisateur en comparant le mot de passe actuel avec celui enregistré dans la table USERS (voir détail de la méthode isPasswordValid() au paragraphe Accès à la table des mots de passe depuis le Managed Bean).

Enfin, si l'utilisateur est bien authentifié, alors le nouveau mot de passe est mis à jour dans la table USERS par appel de la méthode privée updatePassword() également décrite au paragraphe Accès à la table des mots de passe depuis le Managed Bean.

Le code source de la méthode changePassword() est le suivant :

    public String changePassword() throws SQLException {
        FacesContext context = FacesContext.getCurrentInstance();
 
        //1)Checking whether both new password and password confirmation are the same or not
        if (!this.newPassword.equals(this.newPasswordConfirmation))
            context.addMessage(null, new FacesMessage("The new password doesn't match with the new password confirmation! Try again."));
 
        //2)Checking whether the new password is equal to the current one or not
        else if (this.newPassword.equals(this.currentPassword))
            context.addMessage(null, new FacesMessage("The current password and the new one can't be the same! Try again."));
        else {
            Connection connection=null;
            try {
                if(this.dataSource==null) throw new SQLException("Can't get data source");
 
                connection = this.dataSource.getConnection();
                if(connection==null) throw new SQLException("Can't get database connection");
 
                //3)Does the password in the form match with the one in the database?
                if (!this.isPasswordValid(connection))
                    context.addMessage(null, new FacesMessage("The current password is invalid! Try again."));
                else
                {
                    // Password update in the database
                    this.updatePassword(connection);
                    context.addMessage(null, new FacesMessage("Password successfully updated for '" + this.loginName +"'."));
                }
            } finally {
                if (connection!=null) connection.close();
            }
        }
        return "change_password";
    }
Code 4 : méthode changePassword() de l'application demo_changepwd

Hachage MD5 du mot de passe

Le mot de passe actuel saisi dans le formulaire doit être haché en MD5 pour comparer son empreinte à celle stockée dans la table USERS.
Il en est de même pour le nouveau mot de passe qui doit être haché en MD5 avant d'être mis à jour dans la table USERS.

Ce hachage MD5 est assuré par la méthode privée getCryptedPassword() qui s'appuie d'une part sur la classe standard Java MessageDigest pour obtenir l'empreinte MD5 du mot de passe sous la forme d'un tableau d'octets et d'autre part, sur un algorithme de conversion de ce tableau d'octets en chaîne de caractères encodée en base 16 héxadécimale, pour être en phase avec l'encodage utilisé par défaut par le realm JDBC pour authentifier l'utilisateur (voir le champ Encoding disponible à la configuration d'un realm JDBC à partir de la console d'administration de GlassFish).

Ci-dessous le code source complet de la méthode getCryptedPassword() :

    private String getCryptedPassword(String notCryptedPassword) {
        MessageDigest md=null;
        try {
            md = MessageDigest.getInstance("MD5");
        } catch (NoSuchAlgorithmException ex) {
            Logger.getLogger(AuthenticationBean.class.getName()).log(Level.SEVERE, null, ex);
        }
        if (md == null)
            return notCryptedPassword;
 
        md.update(notCryptedPassword.getBytes());
 
        byte input[] = md.digest();
 
        // Convert the byte variable to hexadecimal format
        StringBuilder hexaString = new StringBuilder();
    	for (int i=0;i<input.length;i++) {
    		String hexaChar=Integer.toHexString(0xff & input[i]);
   	     	if(hexaChar.length()==1) hexaString.append('0');
   	     	hexaString.append(hexaChar);
    	}
        return hexaString.toString();
    }
Code 5 : méthode getCryptedPassword() de l'application demo_changepwd

Améliorations possibles

Règle de saisie du nouveau mot de passe

Pour renforcer la sécurité de l'application web, il pourrait être imposé à l'utilisateur de saisir son nouveau mot de passe selon des règles strictes de définition des mots de passe.
Par exemple, en lui imposant que son mot de passe ait une longueur minimale de 8 caractères, qu'il contienne à la fois des lettes en majuscule et minuscule et au minimum un chiffre etc...

Un contrôle sur le niveau de sécurité du mot de passe peut être réalisé par l'intermédiaire d'expressions régulières en implémentant les classes Java standards java.util.regex.Pattern et java.util.regex.Matcher.

Enfin, pour aller encore plus dans la stratégie de sécurité de l'application web, on pourrait aussi envisager de conserver l'historique des mots de passe de l'utilisateur dans la base de données et d'interdire la saisie d'un nouveau mot de passe déjà utilisé par le passé.

Crytpage du mot de passe en SHA-256

GlassFish supporte également l'algorithme de hachage SHA-256 pour authentifier un utilisateur à partir d'un realm JDBC.
L'application demo_changepwd peut assez facilement être adaptée pour hacher les mots de passe en SHA-256 plutôt qu'en MD5, toujours par le biais de la classe Java MessageDigest, mais en indiquant à la place l'algorithme SHA-256 comme illustré ci-dessous :
md = MessageDigest.getInstance("SHA-256");.

Gestion des exceptions Java

Pour éviter d'allourdir la lisibilité du code de l'application demo_changepwd, les exceptions Java ne sont pas interceptées par l'application et peuvent remonter jusqu'à l'utilisateur final.
Pour une déploiement en production de cette fonctionnalité de changement de mot de passe, il est nécessaire d'intercepter ces exceptions avant qu'elles ne parviennent à l'utilisateur, en combinant l'emploi des instructions Java try {...} catch {...} et la redirection vers une page d'erreur adaptée, à l'aide des balises <error-page/> du descripteur de déploiement web.xml.

Code source de l'application demo_changepwd

Le code source de l'application web demo_changepwd pré-configuré pour GlassFish et Ant peut être téléchargé sous la forme d'une archive ZIP en cliquant sur le lien ci-dessous :

Ce code source vous est mis à disposition uniquement à titre expérimental et à des fins éducatives. Vous restez par conséquent seul responsable de l'utilisation que vous en faites.

Articles connexes