Dans la Silicon Valley, la méthode d'authentification par défaut est l'e-mail et le mot de passe, optionnellement améliorée par Google Sign-In. Cette hypothèse échoue spectaculairement en Afrique, où plus de 300 millions de personnes utilisent WhatsApp quotidiennement mais beaucoup n'ont pas d'adresse e-mail personnelle. Un étudiant à Abidjan, un commerçant à Lagos, un enseignant à Nairobi -- ils communiquent par WhatsApp, paient par mobile money et s'identifient par numéro de téléphone.
Construire FLIN sans authentification WhatsApp serait comme construire un framework web américain sans Google Sign-In. Cela fonctionnerait techniquement, mais ignorerait comment le public cible vit réellement.
FLIN fournit WhatsApp OTP comme méthode d'authentification intégrée. Trois fonctions -- whatsapp_send_otp(), otp_generate(), et le schéma de vérification standard -- gèrent l'ensemble du flux. Pas de SDK Twilio. Pas de service d'authentification tiers. Pas de surprises de facturation par message.
Le flux d'authentification WhatsApp
WhatsApp OTP utilise un flux en 3 étapes pour les nouveaux utilisateurs et un flux en 2 étapes pour les utilisateurs de retour :
Nouvel utilisateur :
1. Entrer le numéro de téléphone -> Envoyer OTP via WhatsApp
2. Entrer le code OTP -> Vérifier le code
3. Compléter le profil (nom, e-mail, avatar)
4. Créer le compte -> Tableau de bord
Utilisateur de retour :
1. Entrer le numéro de téléphone -> Envoyer OTP via WhatsApp
2. Entrer le code OTP -> Vérifier le code -> Tableau de bord (étape 3 sautée)L'insight clé est que le numéro de téléphone est l'identité. Si le téléphone est déjà enregistré, l'utilisateur est connecté après la vérification OTP. Sinon, il complète un formulaire de profil et un compte est créé.
Étape 1 : Envoyer l'OTP
flin// app/auth/send-whatsapp-otp.flin
layout = "auth"
waPhone = session.waPhone || ""
otpSent = false
fn processSendWhatsappOtp() {
if waPhone != "" {
code = otp_generate(6)
result = whatsapp_send_otp(waPhone, code)
if result.success {
session.waOtpCode = code
session.waOtpPhone = waPhone
otpSent = true
}
}
}
processSendWhatsappOtp()
{if otpSent}
<h2>Entrez le code envoyé sur votre WhatsApp</h2>
<p>Nous avons envoyé un code à 6 chiffres au {waPhone}</p>
<input class="otp-input" type="text" bind={codeInput}
maxlength="6" autocomplete="one-time-code" inputmode="numeric">
<button click={
session.waOtpInput = codeInput;
location.href = "/auth/verify-whatsapp-otp"
}>
Vérifier le code
</button>
{else}
<p>Une erreur s'est produite. Veuillez réessayer.</p>
<a href="/login">Retour à la connexion</a>
{/if}La fonction otp_generate(6) crée un code aléatoire à 6 chiffres cryptographiquement sécurisé. La fonction whatsapp_send_otp() l'envoie via l'API WhatsApp Business.
Étape 2 : Vérifier l'OTP
flin// app/auth/verify-whatsapp-otp.flin
layout = "auth"
otpInput = session.waOtpInput || ""
otpCode = session.waOtpCode || ""
otpPhone = session.waOtpPhone || ""
// Effacer les données sensibles immédiatement
session.waOtpInput = none
session.waOtpCode = none
verifyOk = false
isNewUser = false
fn processVerifyWhatsappOtp() {
if otpInput != "" && otpCode != "" && otpPhone != "" {
if otpInput == otpCode {
existing = User.where(phone == otpPhone && role == "User").first
if existing != none {
// Utilisateur de retour -- connecter directement
session.user = existing.email
session.userName = existing.name || existing.firstName || existing.email
session.userId = to_text(existing.id)
session.waOtpPhone = none
verifyOk = true
} else {
// Nouvel utilisateur -- complétion de profil nécessaire
session.waVerifiedPhone = otpPhone
session.waOtpPhone = none
isNewUser = true
verifyOk = true
}
}
}
}
processVerifyWhatsappOtp()
{if verifyOk && !isNewUser}
<h2>Bon retour !</h2>
<script>setTimeout(function() { window.location.href = "/tasks"; }, 1000);</script>
{else if verifyOk && isNewUser}
<h2>Téléphone vérifié ! Configurons votre profil.</h2>
<script>setTimeout(function() { window.location.href = "/auth/whatsapp-complete-profile"; }, 1200);</script>
{else}
<h2>Code invalide</h2>
<p>Le code que vous avez saisi est incorrect ou a expiré.</p>
<a href="/login">Réessayer</a>
{/if}Le schéma de sécurité critique : les données OTP sont effacées de la session immédiatement après lecture. Le code existe dans le stockage de session pour le minimum de temps possible.
Étape 3 : Complétion du profil (nouveaux utilisateurs uniquement)
flin// app/auth/whatsapp-complete-profile.flin
layout = "auth"
waPhone = session.waVerifiedPhone || ""
errorKey = session.waCreateError || ""
session.waCreateError = none
{if waPhone == ""}
<script>window.location.href = "/login";</script>
{else}
<h2>Complétez votre profil</h2>
<form method="POST" action="/auth/whatsapp-create-account" enctype="multipart/form-data">
<input type="text" name="firstName" placeholder="Prénom" required>
<input type="text" name="lastName" placeholder="Nom de famille">
<input type="email" name="email" placeholder="Adresse e-mail" required>
<input type="file" name="avatar" accept="image/*">
<input type="text" name="occupation" placeholder="Profession">
<input type="text" name="country" placeholder="Pays">
{if errorKey != ""}
<p class="error">{t(errorKey)}</p>
{/if}
<button type="submit">Créer le compte</button>
</form>
{/if}flin// app/auth/whatsapp-create-account.flin
route POST {
validate {
firstName: text @required @minLength(1)
email: text @required @email
lastName: text
occupation: text
country: text
avatar: file @max_size("5MB")
}
waPhone = session.waVerifiedPhone || ""
if waPhone == "" { redirect("/login") }
// Vérifier l'unicité de l'e-mail
existingEmail = User.where(email == body.email && role == "User").first
if existingEmail != none {
session.waCreateError = "error.email_taken"
redirect("/auth/whatsapp-complete-profile")
}
// Vérifier l'unicité du téléphone
existingPhone = User.where(phone == waPhone && role == "User").first
if existingPhone != none {
session.user = existingPhone.email
session.userName = existingPhone.name
session.userId = to_text(existingPhone.id)
session.waVerifiedPhone = none
redirect("/tasks")
}
avatarPath = ""
if body.avatar != none {
avatarPath = save_file(body.avatar, ".flindb/avatars/")
}
fullName = to_text(body.firstName) + " " + to_text(body.lastName || "")
newUser = User {
email: body.email,
name: fullName,
firstName: body.firstName,
lastName: body.lastName || "",
phone: waPhone,
provider: "WhatsApp",
avatar: avatarPath,
occupation: body.occupation || "",
country: body.country || "",
emailVerified: false
}
save newUser
session.waVerifiedPhone = none
session.user = newUser.email
session.userName = fullName
session.userId = to_text(newUser.id)
redirect("/tasks")
}La fonction whatsapp_send_otp()
La fonction intégrée gère l'intégration de l'API WhatsApp Business :
flinresult = whatsapp_send_otp(phone_number, code)
// result.success -> true/false
// result.error -> message d'erreur en cas d'échecEn coulisses, FLIN envoie l'OTP via l'API WhatsApp Business en utilisant un modèle de message pré-approuvé. Le modèle est configuré une fois dans le WhatsApp Business Manager et référencé par le runtime :
rustpub async fn send_whatsapp_otp(
phone: &str,
code: &str,
) -> Result<SendResult, WhatsAppError> {
let api_url = env::var("WHATSAPP_API_URL")?;
let token = env::var("WHATSAPP_TOKEN")?;
let template_name = env::var("WHATSAPP_OTP_TEMPLATE")
.unwrap_or_else(|_| "otp_verification".to_string());
let payload = json!({
"messaging_product": "whatsapp",
"to": normalize_phone(phone),
"type": "template",
"template": {
"name": template_name,
"language": { "code": "en" },
"components": [{
"type": "body",
"parameters": [{
"type": "text",
"text": code
}]
}]
}
});
let response = reqwest::Client::new()
.post(&api_url)
.bearer_auth(&token)
.json(&payload)
.send()
.await?;
if response.status().is_success() {
Ok(SendResult { success: true, error: None })
} else {
let error = response.text().await.unwrap_or_default();
Ok(SendResult { success: false, error: Some(error) })
}
}Normalisation des numéros de téléphone
Les numéros de téléphone africains se présentent sous de nombreux formats : +225 07 08 09 10, 00225 0708091010, 07 08 09 10, 225708091010. FLIN normalise tous ces formats au format E.164 (+2250708091010) avant l'envoi :
rustfn normalize_phone(phone: &str) -> String {
// Supprimer les espaces, tirets, parenthèses
let digits: String = phone.chars()
.filter(|c| c.is_ascii_digit() || *c == '+')
.collect();
if digits.starts_with('+') {
digits
} else if digits.starts_with("00") {
format!("+{}", &digits[2..])
} else if digits.len() == 10 {
// Supposer un format local -- indicatif pays nécessaire depuis la config
let country_code = env::var("DEFAULT_PHONE_COUNTRY").unwrap_or("225".into());
format!("+{}{}", country_code, digits)
} else {
format!("+{}", digits)
}
}Pourquoi WhatsApp OTP pour l'Afrique
La décision d'intégrer WhatsApp OTP dans FLIN a été motivée par la réalité du marché :
Pénétration de WhatsApp. En Côte d'Ivoire, au Nigeria, au Kenya, au Ghana, en Afrique du Sud et dans la majeure partie de l'Afrique subsaharienne, WhatsApp est la plateforme de messagerie par défaut. La plupart des gens consultent WhatsApp avant de consulter leur e-mail.
Rareté de l'e-mail. De nombreux internautes africains, surtout en dehors des grandes villes, n'ont pas d'adresse e-mail personnelle. Exiger une inscription par e-mail les exclut.
Identité basée sur le téléphone. Le mobile money (MTN Mobile Money, Orange Money, Wave, M-Pesa) utilise les numéros de téléphone comme identifiants. Les services gouvernementaux acceptent de plus en plus la vérification par téléphone. Un numéro de téléphone est la forme la plus universelle d'identité numérique en Afrique.
Coûts des SMS. Les OTP par SMS sont chers en Afrique (0,03-0,10 USD par message) et peu fiables selon les opérateurs. Les messages WhatsApp coûtent une fraction de ce prix via l'API Business et sont livrés de manière fiable sur les connexions data.
Confiance. Les utilisateurs font confiance aux messages reçus sur WhatsApp. Un code de vérification reçu sur WhatsApp semble légitime. Un code reçu par SMS pourrait ressembler à du spam.
En faisant de WhatsApp OTP une fonctionnalité intégrée, FLIN se positionne comme un framework qui comprend son marché principal. Un développeur à Abidjan construisant une application pour les utilisateurs ouest-africains peut ajouter l'authentification par téléphone en quelques minutes, pas en quelques jours.
Considérations de sécurité
WhatsApp OTP a des considérations de sécurité spécifiques :
Expiration du code. Les codes OTP de FLIN sont valides pendant 10 minutes. Après cela, les données de session sont invalidées et un nouveau code doit être demandé.
Limitation du débit. Le point de terminaison d'envoi OTP doit être limité en débit pour empêcher les abus. Le garde guard rate_limit(3, 300) (3 requêtes par 5 minutes) est recommandé pour les points de terminaison OTP.
Longueur du code. Le code à 6 chiffres fournit 1 million de combinaisons possibles. Combiné avec la limitation du débit (5 tentatives par session), la force brute est impraticable.
Hygiène de session. Les données OTP sont effacées de la session immédiatement après utilisation. Le code, le numéro de téléphone et l'entrée de vérification ne sont jamais stockés plus longtemps que nécessaire.
Le WhatsApp OTP de FLIN n'est pas un wrapper autour d'un service d'authentification tiers. C'est une méthode d'authentification de première classe intégrée au langage, avec les mêmes garanties de gestion de session, de validation et de sécurité que l'authentification par e-mail/mot de passe.
Dans le prochain article, nous couvrons les validateurs de corps de requête -- comment les blocs validate de FLIN appliquent la sécurité de type, les contraintes et les règles métier sur les données entrantes avant que votre code de gestionnaire ne s'exécute.
Ceci est la partie 112 de la série « Comment nous avons construit FLIN », documentant comment un CEO à Abidjan et un CTO IA ont conçu et construit un langage de programmation à partir de zéro.
Navigation de la série : - [111] OAuth2 et authentification sociale - [112] Authentification WhatsApp OTP pour l'Afrique (vous êtes ici) - [113] Validateurs de corps de requête - [114] 75 tests de sécurité : comment nous avons tout vérifié