https://d226lax1qjow5r.cloudfront.net/blog/blogposts/otp-based-jwt-authentication-in-spring-boot-with-vonage-verify-api/jwt-authentication_spring-boot_verify-api.png

Authentification par OTP (JWT) dans Spring Boot avec l'API Verify de Vonage

Publié le November 17, 2022

Temps de lecture : 7 minutes

Cet article a été mis à jour en mars 2025

Vue d'ensemble

L'expérience utilisateur (UX) sans faille est un facteur déterminant pour la croissance des produits. L'authentification fait partie intégrante d'une bonne UX, notamment dans les applications bancaires ou FinTech.

Imaginez que vous ayez à créer une page de connexion pour une banque où le client utilise un numéro de téléphone et un mot de passe à usage unique (OTP) pour s'authentifier. Éliminer la nécessité de se souvenir d'un mot de passe tout en offrant la sécurité améliorerait l'expérience de l'utilisateur.

Voyons comment créer une authentification JWT (JSON Web Token) basée sur l'OTP en Java. Nous utiliserons le cadre Spring Boot et l'API Verify de Vonage.

Pour en savoir plus sur JWT ici.

Compte API Vonage

Avant de commencer à utiliser l'API, nous aurons besoin d'un Account API Vonage.

Vonage API dashboard

Une fois que nous avons notre clé API et le secret APInous les utiliserons pour la fonction Verify API.

Mise en place

Nous allons créer une nouvelle application Spring et importer le SDK du serveur Java de Vonage pour pouvoir utiliser les API de Vonage dans notre application. Nous ajoutons le code ci-dessous à notre fichier build.gradle ou au fichier POM.

Gradle

Ajoutez ce qui suit au fichier build.gradle le texte suivant.

repositories {
    mavenCentral()
}

dependencies {
    implementation 'com.vonage:server-sdk:8.20.1'
}

Maven

Ajoutez ce qui suit au fichier POM de notre projet.

<dependency>
    <groupid>com.vonage</groupid>
    <artifactid>server-sdk</artifactid>
    <version>8.20.1</version>
</dependency>

Aperçu de la mise en œuvre

Maintenant que nous avons terminé la configuration, nous pouvons nous plonger dans le développement. Cette authentification par OTP avec JWT peut être réalisée en trois étapes :

  1. Création de JWT

  2. Filtrer les demandes des utilisateurs

  3. Accéder aux fonctionnalités de l'API

Création de JWT

Nous utiliserons un mécanisme d'autorisation basé sur un jeton dans lequel, une fois l'utilisateur authentifié avec succès, nous générerons un nouveau jeton avec une période d'expiration et le renverrons à l'utilisateur.

L'utilisateur devra transmettre ce jeton à chaque demande pour prouver son identité et accéder ensuite aux applications nécessitant une autorisation.

Filtrer les demandes des utilisateurs

Chaque fois que nous recevons un jeton dans la requête, nous devons le vérifier et indiquer à Spring Security que l'utilisateur est autorisé et peut accéder aux points de terminaison restreints. Nous aurons besoin d'un filtre pour chaque requête.

Accéder aux fonctionnalités de l'API

Nous utiliserons trois routes API différentes pour compléter l'authentification. Chacune d'entre elles a un objectif unique :

  • Obtenir le numéro de téléphone de l'utilisateur pour envoyer l'OTP

  • Verify the OTP and return the JWT to the user (Vérifier l'OTP et renvoyer le JWT à l'utilisateur)

  • Point final général pour les essais

Voyons comment chacun de ces éléments peut être mis en œuvre séparément.

Gestion du cryptage des JWT

JWT est une combinaison de deux méthodes de cryptage différentes créées à l'aide de JWS et JWE, qui peuvent être cryptées à l'aide d'une clé symétrique. SECRET_KEY avec une charge utile.

La charge utile peut contenir des données sensibles que nous pouvons utiliser pour valider l'utilisateur (par exemple, la date d'expiration, les coordonnées de l'utilisateur, etc.)

En utilisant la même SECRET_KEYnous pouvons décrypter le jeton pour obtenir la charge utile et l'utiliser en cas de besoin.

Pour gérer les JWT, nous utiliserons le paquetage io.jsonwebtoken::jjwt paquetage.

dependencies {
    implementation 'io.jsonwebtoken:jjwt:0.12.6'
}

Créer une classe JWTUtils sous le paquetage util et ajoutez le code suivant.

package com.example.vonage.auth.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.cglib.core.internal.Function;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Component
public class JWTUtil {
	private final String SECRET_KEY = "secret key";

	public String extractIdentifier(String token) {
    	return extractClaim(token, Claims::getSubject);
	}

	public Date extractExpiration(String token){
    	return extractClaim(token, Claims::getExpiration);
	}

	public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
    	final Claims claims = extractAllClaims(token);
    	return claimsResolver.apply(claims);
	}

	private Claims extractAllClaims(String token){
    	return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
	}

	private Boolean isTokenExpired(String token){
    	return extractExpiration(token).before(new Date());
	}

	public String generateToken(String details) {
    	Map<String, Object> claims = new HashMap<>();
    	return createToken(claims, details);
	}

	private String createToken(Map<String, Object> claims, String subject) {
    	long time = System.currentTimeMillis();
    	long expiry = Duration.ofDays(10).toMillis();
    	return Jwts.builder()
            	.setClaims(claims)
            	.setSubject(subject)
            	.setIssuedAt(new Date(time))
            	.setExpiration(new Date(time + expiry))
            	.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
            	.compact();
	}

	public Boolean validateToken(String token, String identifier) {
    	final String phoneNumber = extractIdentifier(token);
    	return phoneNumber.equals(identifier) && !isTokenExpired(token);
	}
}

Il contient toute la logique relative au JWT, qui peut être utilisée pour créer un nouveau jeton, valider le jeton et obtenir des identifiants à partir du jeton pour valider l'utilisateur, etc.

Nous remplaçons ici le SECRET_KEY par n'importe quel secret partagé et nous y accédons à partir des fichiers de propriétés de l'application plutôt que de le coder en dur.

De même, lors de la génération du jeton, nous transmettons le paramètre détails (numéro de téléphone) sous forme de chaîne de caractères, mais vous pouvez stocker n'importe quel objet et l'extraire.

La validation est extrêmement simple : nous stockons simplement le numéro de téléphone dans le jeton, qui sera utilisé à chaque fois pour l'autorisation.

Dans la méthode validateToken nous comparons les numéros de téléphone et vérifions si le jeton a expiré.

La date d'expiration du jeton est de dix jours Duration.ofDays(10).toMillis() à compter de la création du jeton.

Ajouter un filtre pour authentifier un utilisateur et définir le contexte

Chaque fois que nous recevons une requête nécessitant une autorisation, nous devons valider le jeton et définir le contexte dans lequel l'utilisateur est authentifié.

Nous allons ajouter un nouveau filtre qui étendra le filtre OncePerRequestFilter- comme son nom l'indique ; ce filtre sera exécuté une fois pour chaque demande.

Créer une nouvelle classe Java nommée JWTFilter dans le paquetage filters et ajoutez le code suivant.

Il contient la logique permettant d'extraire le jeton de la demande et de le valider. S'il est autorisé, il définit le contexte dans lequel l'utilisateur est authentifié.

package com.example.vonage.auth.filters;

import com.example.vonage.auth.utils.JWTUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class JWTFilter extends OncePerRequestFilter {
    @Autowired
    JWTUtil jwtUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        final String authorizationHeader = request.getHeader("Authorization");
        String jwt = null, identifier = null;
        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer")) {
            jwt = authorizationHeader.substring(7);
            identifier = jwtUtil.extractIdentifier(jwt);
        }
        if (identifier != null && jwtUtil.validateToken(jwt, identifier) && SecurityContextHolder.getContext().getAuthentication() == null) {
            AuthenticationFilter apiToken = new AuthenticationFilter("abc", "xyz", AuthorityUtils.createAuthorityList());
            SecurityContextHolder.getContext().setAuthentication(apiToken);
        }

        filterChain.doFilter(request, response);
    }
}

Pour chaque requête, nous récupérons l'en-tête Authorization et nous en extrayons le jeton après le texte BearerC'est pourquoi nous obtenons la sous-chaîne après les sept premiers caractères (y compris un espace après le mot Bearer).

Une fois que nous avons le jeton, nous extrayons l identifiant à des fins de validation.

Pour la validation, nous testons si l'utilisateur est déjà authentifié ou non, ce qui ne sera pas le cas puisque nous disposons de la fonction SANS ÉTAT et si l'identifiant est le même et si le jeton n'a pas expiré. La session est sans état parce que nous ne maintenons aucune session étant donné qu'il s'agit d'une autorisation basée sur un jeton. Ce paramètre sera mis à jour dans la sécurité de printemps.

Parce que nous n'avons utilisé que le numéro de téléphone comme identifiant, il n'y a pas de vérification croisée. Nous pouvons renforcer la sécurité en stockant l'adresse électronique de l'utilisateur ou tout autre objet unique, puis en utilisant le numéro de téléphone pour l'extraire de la base de données et procéder à une vérification.

Pour obtenir le contexte d'authentification, nous avons étendu la fonction AbstractAuthenticationToken et avons fourni une clé et un secret uniques.

AuthenticationFilter apiToken = new AuthenticationFilter("abc", "xyz", AuthorityUtils.createAuthorityList());

SecurityContextHolder.getContext().setAuthentication(apiToken);

abc et xyz peuvent être remplacés par une paire d'identifiants uniques tels que phone number et email.

Créez une nouvelle classe Java nommée AuthenticationFilter sous le paquetage filters pour obtenir le contexte authentifié.

package com.example.vonage.auth.filters;

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.Transient;
import java.util.Collection;

@Transient
public class AuthenticationFilter extends AbstractAuthenticationToken {
    private String apiKey, keySecret;

    /**
    * Creates a token with the supplied array of authorities.
    *
    * @param authorities the collection of <tt>GrantedAuthority</tt>s for the principal
    *                	represented by this authentication object.
    */
    public AuthenticationFilter(String apiKey, String keySecret, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.apiKey = apiKey;
        this.keySecret = keySecret;
        setAuthenticated(true);
    }

    @Override
    public Object getCredentials() {
        return keySecret;
    }

    @Override
    public Object getPrincipal() {
        return apiKey;
    }
}

Une fois l'autorisation obtenue, nous transmettrons la demande. filterChain.doFilter(request, response);

Notre code pour le filtre est prêt ! Nous devons maintenant l'utiliser pour chaque requête, à l'exception de la connexion.

Mettons à jour la configuration de Spring Security pour gérer la même chose.

Créez une nouvelle classe Java nommée WebSecurityConfig sous le paquetage config et ajoutez le code suivant.

package com.example.vonage.auth.config;

import com.example.vonage.auth.error.AuthError;
import com.example.vonage.auth.filters.JWTFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    AuthError authErrorHandler;

    @Autowired
    JWTFilter jwtFilter;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable()
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and().authorizeRequests(configurer ->
                configurer.antMatchers("/api/login/**").permitAll().anyRequest().authenticated()
            ).exceptionHandling().authenticationEntryPoint(authErrorHandler);
    }
}

Ici, nous avons configuré les éléments cors et csrf pour que l'API fonctionne et nous avons ajouté notre jwtFilter avant tout filtre de sécurité interne à Spring.

UsernamePasswordAuthenticationFilter.class est le premier filtre en priorité. Mais avant cela, notre filtre s'exécute et définit le contexte. Par conséquent, aucune autre vérification n'est nécessaire et la demande est traitée.

Nous avons également défini la session comme étant sans état .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) car nous utilisons un jeton pour l'autorisation.

Toutes les demandes d'authentification sont contournées par le contrôle de sécurité. "/api/login/**".

A la fin, nous avons ajouté une classe d'erreur commune pour gérer l'erreur et fournir une réponse personnalisée .exceptionHandling().authenticationEntryPoint(authErrorHandler);.

Créez une classe Java nommée AuthError sous le paquetage error et ajoutez le code suivant.

package com.example.vonage.auth.error;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.Serializable;

@Component
public class AuthError implements AuthenticationEntryPoint, Serializable {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        ObjectMapper mapper = new ObjectMapper();
        String responseMsg = mapper.writeValueAsString("Unathorized User");
        response.getWriter().write(responseMsg);
        response.setContentType("application/json");
    }
}

Définir des contrôleurs pour les itinéraires restreints et non restreints

Notre filtre et notre validation de jeton sont maintenant en place. Écoutons la requête et authentifions l'utilisateur.

Services

Créer une classe Java nommée "AuthService" dans le paquetage services paquetage.

package com.example.vonage.auth.services;

import com.example.vonage.auth.utils.JWTUtil;
import com.vonage.client.VonageClient;
import com.vonage.client.verify.CheckResponse;
import com.vonage.client.verify.VerifyResponse;
import com.vonage.client.verify.VerifyStatus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class AuthService {
    @Autowired
    JWTUtil jwtUtil;

    final VonageClient client = VonageClient.builder()
        .apiKey("7ed*****")
        .apiSecret("W**u*E*VlaWe****")
        .build();

    public String init(String identifier) {
        VerifyResponse response = client.getVerifyClient().verify("91900*******", "Vonage");
        if (response.getStatus() == VerifyStatus.OK) {
            return response.getRequestId();
        }
        else {
            return "ERROR! " + response.getStatus() + " " + response.getErrorText();
        }
    }

    public String verify(String identifier, String request_id, String otp) {
        CheckResponse response = client.getVerifyClient().check(request_id, otp);
        if (response.getStatus() == VerifyStatus.OK) {
            return jwtUtil.generateToken(identifier);
        }
        else {
            return "Verification failed: " + response.getErrorText();
        }
    }
}

AuthService aura deux méthodes : init et verify.

init accepte le numéro de téléphone comme paramètre et envoie l'OTP à ce numéro de téléphone. La réponse renvoie un request_id qui sera utilisé avec l'OTP pour la validation.

verify accepte le numéro de téléphone, l'identifiant de la demande et l'OTP comme paramètres et vérifie l'identifiant de la demande et l'OTP. Une fois la vérification effectuée, le numéro de téléphone est utilisé comme identifiant, un nouveau jeton est généré et la réponse est renvoyée.

Contrôleurs

Il est possible d'accéder à une application par le biais de points d'accès mis à la disposition de l'utilisateur. Ces points d'accès peuvent être publics, privés ou protégés. Dans notre cas, l'API de connexion sera accessible au public, tandis que toutes les autres API sont protégées et nécessitent une authentification de l'utilisateur pour y accéder.

Auth (Connexion)

Aucune autorisation n'est requise pour accéder à ces itinéraires.

Créez une classe Java nommée "Auth" dans le paquetage controllers et ajoutez le code suivant.

package com.example.vonage.auth.controllers;

import com.example.vonage.auth.services.AuthService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.constraints.NotBlank;

@Validated
@RestController
@RequestMapping("api/login")
public class Auth {

    @Autowired
    AuthService authService;

    @PostMapping(path="/init", consumes = {MediaType.APPLICATION_FORM_URLENCODED_VALUE})
    public String init(@NotBlank String identifier){
        return authService.sendOtp(identifier);
    }

    @PostMapping(path="/verify", consumes = {MediaType.APPLICATION_FORM_URLENCODED_VALUE})
    public String verify(@NotBlank String identifier, @NotBlank String request_id, @NotBlank String otp) {
        return authService.verify(identifier, request_id, otp);
    }
}

Autres (affectés)

Une autorisation est requise pour accéder à cet itinéraire.

Créez une classe Java nommée "Hello" dans le paquetage controllers et ajoutez le code suivant.

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Validated
@RestController
@RequestMapping("api/")
public class Hello {
    
    @GetMapping(path="/hello")
    public String hello(){
        return "Hello world";
    }
}

Essais

Nous avons ici trois points d'extrémité de l'API.

/api/hello sans autorisation

Si aucun en-tête Authorization n'est présent dans la requête ou qu'un jeton invalide est fourni, une erreur sera générée, unauthorized user.

Without Authorization

/api/login/init

Il accepte le numéro de téléphone au format URL codé et renvoie l'identifiant de la demande après l'envoi de l'OTP.

Init the OTP request

/api/login/verify

Il accepte le numéro de téléphone, l'identifiant de la demande et l'OTP au format codé en URL et renvoie le jeton JWT après l'envoi de l'OTP.

Verify the OTP

/api/hello avec autorisation

Dans l'en-tête de la requête, passez

Authorization : Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI5MDA0NzU4NTM0IiwiZXhwIjoxNjYzOTYxMzIwLCJpYXQiOjE2NjM5MjUzMjB9.fY2536Zuz2KNzinZvWU5CNZ6K1p_wOu0pkEbwqP8gbg

With Authorization

Conclusion

Maintenant que nous avons créé un flux de connexion en deux étapes avec le numéro de téléphone et l'OTP, notre prochaine étape pourrait être d'ajouter une authentification à deux facteurs permettant de revérifier le même utilisateur en passant un appel au numéro de téléphone à l'aide de la fonction Voice API de Vonage.

Vonage dispose d'un ensemble intéressant d API intéressantes qui peuvent être utilisées pour faire passer votre produit au niveau supérieur et offrir une meilleure expérience à l'utilisateur.

L'engagement de la communauté est toujours le bienvenu. Rejoignez Vonage sur GitHub pour des exemples de code et la Communauté Slack pour poser des questions à tout moment. Envoyez-nous un tweet et faites-nous part du problème intéressant que vous avez résolu en utilisant l'API de Vonage.

Vous pouvez également me contacter sur Twitter ou sur mon blog learnersbucket.com.

Partager:

https://a.storyblok.com/f/270183/400x533/ed2a75e7a4/prashant-yadav.png
Prashant YadavDéveloppeur en vedette Auteur

Ingénieur frontend senior basé à Mumbai, en Inde. Je travaille sur le développement fullstack avec React et Spring boot pour la Fintech la plus appréciée d'Inde. Je suis également le fondateur de learnersbucket.com où j'écris des guides de préparation aux entretiens JavaScript.