Retour

Envoyer des SMS avec Ruby on Rails pour la double authentification

Enregistrer
Temps de lecture: 6 min Vues: 4 Niveau: Intermédiaire à Confirmé
environ 4 heures par Franci-lobbie LALANE
Envoyer des SMS avec Ruby on Rails pour la double authentification
Tags:
#ruby #2fa #SMS #authentication #security #two-factor-authentication

Implémenter une 2FA par SMS avec Ruby on Rails

La double authentification par SMS, ce n'est pas forcément la feature la plus excitante à coder. Pourtant, dès qu'un projet Rails arrive en production, c'est souvent l'une des premières briques de sécurité qu'on ajoute.
Un simple mot de passe ne suffit plus vraiment aujourd'hui. Fuites de données, phishing, mots de passe recyclés... les risques sont partout. Ajouter une deuxième étape de validation devient vite indispensable.
Dans cet article, on va voir comment mettre en place une 2FA par SMS avec Ruby et Rails, de manière propre et maintenable.
On va couvrir deux cas très courants :
  • une webapp Rails classique (sessions + cookies)
  • un backend Rails API consommé par une application mobile Flutter
L'objectif n'est pas de vendre un provider SMS, mais plutôt de comprendre comment structurer une implémentation solide côté Rails.

Pourquoi utiliser le SMS pour la 2FA

Soyons honnêtes : le SMS n'est pas la méthode de 2FA la plus sécurisée du monde. Les attaques par SIM swap existent et certains services sensibles préfèrent d'autres approches.
Mais dans la pratique, pour beaucoup de produits web, le SMS reste un excellent compromis.
Pourquoi ?
  • c'est simple pour l'utilisateur
  • aucune application supplémentaire à installer
  • mise en place rapide
  • amélioration immédiate de la sécurité
Pour beaucoup de projets Rails, surtout en B2C, c'est souvent la première marche vers une authentification plus robuste.

Architecture globale d'une 2FA

Que tu développes une webapp ou une API mobile, le principe reste exactement le même.
  1. l'utilisateur se connecte avec son login et son mot de passe
  2. le backend génère un code temporaire
  3. ce code est envoyé par SMS
  4. l'utilisateur saisit ce code
  5. le backend vérifie la validité du code
La différence se situe ensuite dans la validation finale :
  • une session Rails dans une webapp
  • un token (JWT ou autre) dans une API
Le coeur du système reste identique.

Choisir un provider SMS

La plupart des providers offrent aujourd'hui des API HTTP très simples à utiliser.
Ce qu'on attend surtout :
  • une API fiable
  • une bonne délivrabilité
  • une facturation claire
Les providers les plus utilisés avec Ruby :
  • Twilio
  • OVH SMS
  • Vonage (anciennement Nexmo)
Dans cet article on prendra Twilio comme exemple, mais l'architecture fonctionne avec n'importe quel service.

Isoler l'envoi de SMS dans un service

Une règle simple : ne jamais appeler directement un provider SMS depuis un controller.
Créer un service dédié permet :
  • de simplifier les tests
  • de remplacer facilement le provider
  • de garder des controllers lisibles
Exemple :
# app/services/sms_sender.rb
class SmsSender
  def self.send(to:, message:)
    client = Twilio::REST::Client.new(
      ENV["TWILIO_ACCOUNT_SID"],
      ENV["TWILIO_AUTH_TOKEN"]
    )

    client.messages.create(
      from: ENV["TWILIO_PHONE_NUMBER"],
      to: to,
      body: message
    )
  end
end

Ce service ne contient volontairement aucune logique métier.
Son rôle est simple : envoyer un SMS.

Générer et stocker un code de vérification

Le coeur de la 2FA est le code temporaire.
Une implémentation simple fonctionne très bien dans la majorité des cas :
  • un code numérique court
  • une durée de validité limitée
  • un seul code actif
Modèle ActiveRecord
# app/models/two_factor_code.rb
class TwoFactorCode < ApplicationRecord
  belongs_to :user

  before_create :generate_code

  def expired?
    created_at < 10.minutes.ago
  end

  private

  def generate_code
    self.code = rand(100_000..999_999)
  end
end

Quelques bonnes pratiques :
  • éviter de stocker ces codes trop longtemps
  • ne jamais logger leur valeur
  • éventuellement hasher le code si le niveau de sécurité l'exige

Envoyer le code par SMS

Quand la 2FA est déclenchée, on crée un code et on l'envoie.
code = user.two_factor_codes.create!

SmsSender.send(
  to: user.phone_number,
  message: "Votre code de verification est #{code.code}"
)

Dans une vraie application, cet envoi doit passer par un ActiveJob pour ne pas bloquer la requête HTTP.

Cas 1 : Webapp Rails classique

Dans une application Rails traditionnelle, le flow ressemble à ça :
  1. login + mot de passe valide
  2. génération du code
  3. envoi du SMS
  4. redirection vers l'écran 2FA
  5. validation du code
  6. activation complète de la session
Controller de verification
class TwoFactorController < ApplicationController
  def create
    record = current_user.two_factor_codes.last

    if record&.code == params[:code].to_i && !record.expired?
      session[:two_factor_passed] = true
      redirect_to dashboard_path
    else
      flash[:alert] = "Code invalide ou expire"
      render :new
    end
  end
end

Tant que two_factor_passed n'est pas présent dans la session, l'utilisateur n'a pas accès aux pages sensibles.

Cas 2 : Backend Rails API pour mobile

Pour une API consommée par Flutter ou une autre app mobile, la logique est la même mais l'interface change.
Versionner les endpoints
Une API mobile peut rester longtemps sans mise à jour.
Le versionnement est donc essentiel.
POST /api/v1/auth/2fa/send
POST /api/v1/auth/2fa/verify

Routes
namespace :api do
  namespace :v1 do
    namespace :auth do
      post "2fa/send",   to: "two_factor#send_code"
      post "2fa/verify", to: "two_factor#verify"
    end
  end
end

Controller API
class Api::V1::Auth::TwoFactorController < Api::V1::BaseController
  def send_code
    code = current_user.two_factor_codes.create!

    SmsSender.send(
      to: current_user.phone_number,
      message: "Votre code est #{code.code}"
    )

    render json: { success: true }
  end

  def verify
    record = current_user.two_factor_codes.last

    if record&.code == params[:code].to_i && !record.expired?
      token = JwtEncoder.encode(user_id: current_user.id)
      render json: { success: true, token: token }
    else
      render json: { success: false }, status: :unauthorized
    end
  end
end

L'application Flutter se contente d'appeler ces endpoints.
Toute la logique de sécurité reste côté backend.

Points importants a ne pas oublier

Rate limiting
Sans limite, un utilisateur pourrait demander des dizaines de SMS en quelques minutes.
Il faut donc ajouter :
  • un delai minimum entre deux envois
  • un nombre maximum de codes par jour
Envoi asynchrone avec ActiveJob
Ne jamais envoyer un SMS dans une requête synchrone.
Créer un job dédié :
class SendTwoFactorSmsJob < ApplicationJob
  queue_as :default

  def perform(user_id, code)
    user = User.find(user_id)

    SmsSender.send(
      to: user.phone_number,
      message: "Votre code est #{code}"
    )
  end
end

Puis :
SendTwoFactorSmsJob.perform_later(user.id, code.code)

Nettoyer les anciens codes
Un job périodique peut supprimer les codes expirés afin d'éviter d'accumuler des données inutiles.
Ne jamais logger les codes
Cela peut sembler évident, mais c'est une erreur fréquente.
Les codes 2FA ne doivent jamais apparaître dans les logs.

Aller plus loin

Une architecture propre permet ensuite d'ajouter facilement d'autres méthodes de vérification :
  • TOTP (Google Authenticator)
  • email fallback
  • passkeys
  • WebAuthn
L'idée est de garder un système évolutif.

Conclusion

La 2FA par SMS n'est pas parfaite, mais elle reste une excellente première couche de sécurité.
Avec Ruby on Rails, on dispose de tous les outils nécessaires pour construire une implémentation propre :
  • services clairs
  • jobs asynchrones
  • API versionnée
Que tu travailles sur une webapp Rails ou une API pour mobile, les principes restent exactement les mêmes.
Et si tu prends le temps de bien penser ton système de 2FA aujourd'hui, tu t'éviteras beaucoup de problèmes demain.

Happy conding!

Laissez un commentaire

Se connecterpour laisser un commentaire.