Skip to content

Validation de courriels avec jeton JWT

La validation de courriels lors de l'inscription est une pratique courante et permet de régulariser l'accès à une application. Le processus s'exécute normalement de la façon suivante:

  1. L'utilisateur s'inscrit auprès de l'application
  2. Le compte est créé dans un état non vérifié
  3. Le serveur crée un lien de confirmation et l'envoie à l'utilisateur
  4. L'utilisateur reçoit le courriel et clique sur le lien pour confirmer le courriel
  5. Le serveur reçoit la demande de validation de courriel et la valide si elle est valide
  6. Le compte utilisateur est maintenant dans un état vérifié
  7. L'utilisateur est redirigé vers la page de connexion de l'application

Spring ne nous offre pas de mécanisme automatisé pour faire le tout, il faut donc créer et gérer toute cette logique.

Ici on se concentrera sur les étapes 3 et 5 principalement.

La création du lien et la validation de ce dernier lorsque l'utilisateur clique sur le lien de confirmation se fera à l'aide de jetons JWT.

Modèle Utilisateur

Il faudra bien entendu ajouter une propriété à notre modèle Utilisateur pour conserver en note le statut de vérification du courriel. Dans mon cas j'utiliserai la propriété isVerified de type booléen.

Les jetons JWT (Json Web Token)

Un jeton JWT est à la base un objet JSON, qui a été signé avec une clé de chiffrement. Normalement le serveur est le seul détenteur de la clé pour déchiffrer le jeton, ce qui nous assure l'authenticité de ce dernier.

Pour signer les jetons JWT que l'on crée, il nous faudra une paire de clé RSA.

On peut le faire en ligne de commande avec l'utilitaire OpenSSL

Pour installer OpenSSL et générer les clés sur windows voir l'annexe D

  • Localisez la clé publique et rennomez la public.pem
  • Localisez la clé privée et rennomez la private.pem
  • Placez les deux clés dans le dossier /ressources/certs

Attention

À partir de ce point, les notes de cours assument qu'une paire de clé de chiffrement est disponible dans le dossier /resources/certs

Il nous faudra également avoir une façon simple de faire référence à ces clés. On pourra le faire avec un peu de configuration et une référence vers le fichier application.properties.

  • Créer un enregistrement record et lui ajouter le décorateur @ConfigurationProperties avec un préfixe approprié.
@ConfigurationProperties(prefix = "rsa")
public record RsaKeyProperties(RSAPublicKey rsaPublicKey, RSAPrivateKey rsaPrivateKey) {}

Pour que cette configuration soit reconnue, il faudra ajouter un décorateur à la classe principale de l'application (celle qui contient la méthode main)

@EnableConfigurationProperties({RsaKeyProperties.class})
@SpringBootApplication
public class DemoAuthApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoAuthApplication.class, args);
    }

}

Ainsi, je peux maintenant ajouter la configuration vers les fichiers appropriés dans application.properties et utiliser l'enregistrement ailleurs dans l'applicaiton pour accéder aux propriétées.

rsa.rsa-private-key=classpath:certs/private.pem
rsa.rsa-public-key=classpath:certs/public.pem

Dépendences pour les JWT

Il faudra ajouter trois (3) dépendances pour nous faciliter la tâche.

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.13.0</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.13.0</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.13.0</version>
    <scope>runtime</scope>
</dependency>

Création d'un utilitaire JWT

On peut maintenant créer un service qui contiendra toutes les méthodes pertinentes pour gérer les jetons JWT. Il pourra faire référence à l'enregistrement créé au préalable pour utiliser les clés.

@Service
public class JWTUtilsService {
    private final RsaKeyProperties rsaKeyProperties;

    public JWTUtilsService(RsaKeyProperties rsaKeyProperties) {
        this.rsaKeyProperties = rsaKeyProperties;
    }
}

Pour notre utilisation, on pourra créer un jeton qui aura comme sujet (subject) le courriel de l'utilisateur. Ainsi, lorsqu'on décodera le jeton, on pourra facilement récupérer le courriel, trouver l'utilisateur dans la base de données et modifier l'attribut de vérification du courriel.

Créer un jeton

  • Pour créer un jeton JWT on utilisera le builder de la classe Jwts
// Ajouter des paramètres au besoin
public String genererToken(String email) {

    return  Jwts.builder()            
            .subject(email)
            .issuedAt()
            .expiration()
            .signWith(rsaKeyProperties.rsaPrivateKey()) // Utilisation de la clé configurée dans rsaKeyProperties
            .compact();
}

Les différents paramètres du builder devront contenir

  • subject() : L'identifiant de l'utilisateur
  • issuedAt() : Doit être le moment immédiat. Doit être fourni comme un objet java.util.Date
  • expiration() : Le moment où le jeton expirera. On veut généralement lui donner une durée de vie assez courte. Doit être fourni comme un objet java.util.Date
  • signWith() : Doit recevoir la clée privée créée
  • compact() complète le jeton et le transforme en String. C'est l'équivalent de build() dans un builder traditionnel.

Décoder un jeton

public String validerJetonEtExtraireCourriel(String token) {
    try {
        JwtParser parser = Jwts.parser()
                .verifyWith(rsaKeyProperties.rsaPublicKey())
                .build();

        Jws<Claims> claims = parser.parseSignedClaims(token);
        // Récupérer le sujet, dans l'exemple précédent c'est le courriel.          
        return claims.getPayload().getSubject();

    } catch (io.jsonwebtoken.ExpiredJwtException e) {
        throw new RuntimeException("Jeton expiré", e);
    } catch (io.jsonwebtoken.security.SignatureException e) {
        throw new RuntimeException("Signature du jeton invalide", e);
    } catch (io.jsonwebtoken.JwtException e) {
        throw new RuntimeException("Jeton invalide", e);
    }
}
Ajouter de l'information au jeton

Il est possible d'ajouter plus d'informations à un jeton JWT. Pour ce faire, on utilisera claim() dans le builder et on donnera en paramètre une paire clé-valeur. Par exemple:

claim("age", 32)

Pour récupérer la valeur:

Integer age = claims.getPayload().getSubject().get("age");

Envoyer un courriel avec Java

Pour débuter, ajoutons la dépendence Maven pour gérer l'envoi de courriel

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-mail</artifactId>
</dependency>

Il faudra ensuite configurer les informations du serveur SMTP pour envoyer les courriels. Dans le fichier application.properties. Dans cet exemple j'utilise Mailtrap pour simuler l'envoi de courriels.

spring.mail.host=sandbox.smtp.mailtrap.io
spring.mail.port=2525
spring.mail.username=
spring.mail.password=
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
  • Pour envoyer un courriel dont le corps est écrit en HTML
//Extrait de UserService.java

private JavaMailSender emailSender;

public UtilisateurService(JavaMailSender emailSender) {
    // Autres paramètres du constructeur
    this.emailSender = emailSender;
}

private void envoyerCourriel(String courriel) throws MessagingException {    

    MimeMessage message = emailSender.createMimeMessage();
    MimeMessageHelper helper = new MimeMessageHelper(message, "UTF-8");

    String text = /* Écrire le corps du courriel en HTML */

    helper.setTo(courriel);
    helper.setFrom("[email protected]");
    helper.setSubject("Confirmez votre courriel");
    helper.setText(text, true);
    emailSender.send(message);
}

Pour un courriel de validation

  • Pour le courriel de validation, on voudra envoyer un lien qui redirige vers une méthode d'action qui permettra de changer l'attribut de validation de l'utilisateur.

  • À ce lien, on ajoutera un jeton JWT contenant comme sujet le courriel de l'utilisateur à valider.

    • http://localhost:8080/valider?token={jeton généré}
  • Lors de la validation, on décode le jeton JWT, on retrouve le courriel et l'utilisateur et on indique que son courriel est validé.