andydefer/laravel-totp

Laravel TOTP package for two-factor authentication with polymorphic support, recovery codes and QR Code generation.

Maintainers

Package info

github.com/andydefer/laravel-totp

pkg:composer/andydefer/laravel-totp

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.0 2026-06-20 02:25 UTC

This package is auto-updated.

Last update: 2026-06-20 02:28:37 UTC


README

Package de double authentification TOTP (RFC 6238) pour Laravel avec support polymorphique

Latest Version Total Downloads PHP Version License

Un package Laravel pour la double authentification (2FA) avec TOTP (RFC 6238), support polymorphique, codes de récupération et génération de QR Code.

📋 Table des matières

✨ Fonctionnalités

  • Polymorphisme - Attachez TOTP à n'importe quel modèle (User, Admin, etc.) sans que le modèle n'ait à déclarer de relation
  • TOTP Standard - RFC 6238 compatible (SHA1, 6 chiffres, période de 30s)
  • Génération de secrets - Secrets Base32 (32 caractères)
  • Génération de QR Code - Compatible Google Authenticator, Authy, Microsoft Authenticator
  • Codes de récupération - 10 codes de secours de 8 caractères (hashés en SHA256)
  • Fenêtre de tolérance - Tolérance de +/- 1 période (30s) pour les latences réseau
  • Activation/Désactivation - Gestion complète du cycle de vie TOTP
  • Vérification - Vérification des codes avec comparaison sécurisée (hash_equals)
  • Support des métadonnées - Stockez des données supplémentaires au format JSON
  • Suppression douce - SoftDeletes pour une suppression sécurisée
  • Tests complets - Couverture complète des tests d'intégration (27 tests)

🚀 Prérequis

  • PHP 8.2 ou supérieur
  • Laravel 12.0, 13.0, 14.0 ou 15.0
  • Extension GD (pour la génération de QR Code)

📦 Installation

Installez le package via Composer :

composer require andydefer/laravel-totp

Publier les migrations

php artisan vendor:publish --tag=Totp-migrations

Exécuter les migrations

php artisan migrate

⚙️ Configuration

Le package est automatiquement découvert par Laravel. Aucune configuration supplémentaire n'est requise.

Si vous devez personnaliser le Service Provider, ajoutez-le manuellement dans config/app.php :

'providers' => [
    // ...
    AndyDefer\LaravelTotp\TotpServiceProvider::class,
],

Dépendances optionnelles

Pour la génération de QR Code, le package utilise endroid/qr-code. Assurez-vous que l'extension GD est activée :

# Vérifier que GD est installé
php -m | grep gd

# Sur Ubuntu/Debian
sudo apt-get install php-gd

# Sur macOS avec Homebrew
brew install php-gd

🏗️ Structure du package

laravel-totp/
├── src/
│   ├── TotpServiceProvider.php
│   ├── Models/
│   │   └── TotpSecret.php          # Seul modèle avec relations polymorphiques
│   ├── Services/
│   │   ├── TotpService.php         # Service principal
│   │   ├── TotpGenerator.php       # Génération TOTP
│   │   └── QrCodeGenerator.php     # Génération QR Code
│   ├── ValueObjects/
│   │   └── TotpSecretVO.php
│   └── Exceptions/
│       └── TotpException.php
├── database/
│   └── migrations/
│       └── create_totp_secrets_table.php
└── tests/
    ├── Integration/
    │   └── Services/
    │       └── TotpServiceIntegrationTest.php
    ├── Fixtures/
    └── IntegrationTestCase.php

⚠️ Important : Aucune relation dans les modèles consommateurs

Contrairement à d'autres packages, le modèle consommateur (User, Admin, etc.) n'a pas besoin de déclarer de relation. Seul le modèle TotpSecret contient les relations polymorphiques.

// ✅ Le modèle User reste propre - AUCUNE RELATION !
class User extends Model
{
    protected $fillable = ['name', 'email', 'password'];
    // AUCUNE méthode totpSecret() ici !
}

📖 Utilisation

Configurer TOTP

use AndyDefer\LaravelTotp\Services\TotpService;

class TwoFactorController extends Controller
{
    public function __construct(
        private readonly TotpService $totpService,
    ) {}

    public function setup(Request $request)
    {
        $user = $request->user();

        $setup = $this->totpService->setup($user);

        return response()->json([
            'secret' => $setup['secret'],
            'qr_code' => base64_encode($setup['qr_code']),
            'qr_code_uri' => $setup['qr_code_uri'],
            'recovery_codes' => $setup['recovery_codes'],
        ]);
    }
}

Réponse :

{
    "secret": "JBSWY3DPEHPK3PXP",
    "qr_code": "iVBORw0KGgoAAAANSUhEUgAA...",
    "qr_code_uri": "otpauth://totp/Laravel:john@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Laravel&algorithm=SHA1&digits=6&period=30",
    "recovery_codes": [
        "X7K9M2P4", "R3T8W6Q1", "F5N2V9L3",
        "C1H7A4E8", "Y6B0U3S9", "D2J5R8M1",
        "W4P7K0T6", "G9E3F1V8", "L2N5Q7C4", "S8U1B6X9"
    ]
}

Activer TOTP après vérification

public function enable(Request $request)
{
    $user = $request->user();

    $request->validate([
        'code' => 'required|string|size:6',
    ]);

    try {
        $enabled = $this->totpService->verifyAndEnable(
            authenticatable: $user,
            code: $request->code,
            window: 1, // Tolérance de +/- 1 période (30s)
        );

        if ($enabled) {
            return response()->json([
                'message' => 'TOTP enabled successfully',
                'verified_at' => now()->toDateTimeString(),
            ]);
        }

        return response()->json([
            'message' => 'Invalid TOTP code',
        ], 400);
    } catch (TotpException $e) {
        return response()->json([
            'message' => $e->getMessage(),
        ], 400);
    }
}

Vérifier un code TOTP

public function verifyLogin(Request $request)
{
    $user = User::where('email', $request->email)->firstOrFail();

    $request->validate([
        'code' => 'required|string|size:6',
    ]);

    try {
        $valid = $this->totpService->verify(
            authenticatable: $user,
            code: $request->code,
            window: 1,
        );

        if ($valid) {
            auth()->login($user);
            
            return response()->json([
                'message' => 'Login successful',
            ]);
        }

        return response()->json([
            'message' => 'Invalid TOTP code',
        ], 400);
    } catch (TotpException $e) {
        return response()->json([
            'message' => $e->getMessage(),
        ], 400);
    }
}

Vérifier un code de récupération

public function recover(Request $request)
{
    $user = User::where('email', $request->email)->firstOrFail();

    $request->validate([
        'recovery_code' => 'required|string',
    ]);

    try {
        $valid = $this->totpService->verifyRecoveryCode(
            authenticatable: $user,
            code: $request->recovery_code,
        );

        if ($valid) {
            auth()->login($user);
            
            return response()->json([
                'message' => 'Recovery successful',
            ]);
        }

        return response()->json([
            'message' => 'Invalid recovery code',
        ], 400);
    } catch (TotpException $e) {
        return response()->json([
            'message' => $e->getMessage(),
        ], 400);
    }
}

Désactiver TOTP

public function disable(Request $request)
{
    $user = $request->user();

    try {
        $this->totpService->disable($user);

        return response()->json([
            'message' => 'TOTP disabled successfully',
        ]);
    } catch (TotpException $e) {
        return response()->json([
            'message' => $e->getMessage(),
        ], 400);
    }
}

Régénérer les codes de récupération

public function regenerateRecoveryCodes(Request $request)
{
    $user = $request->user();

    try {
        $recoveryCodes = $this->totpService->regenerateRecoveryCodes($user);

        return response()->json([
            'recovery_codes' => $recoveryCodes->toArray(),
        ]);
    } catch (TotpException $e) {
        return response()->json([
            'message' => $e->getMessage(),
        ], 400);
    }
}

Vérifier l'état de TOTP

public function status(Request $request)
{
    $user = $request->user();

    return response()->json([
        'enabled' => $this->totpService->isEnabled($user),
        'verified' => $this->totpService->isVerified($user),
        'remaining_recovery_codes' => count(
            $this->totpService->getRemainingRecoveryCodes($user)
        ),
    ]);
}

📚 Référence de l'API

TotpService

Méthode Description Retourne Exception
setup(Model $authenticatable) Configurer TOTP (génère secret, QR Code, codes récupération) array{secret, qr_code, qr_code_uri, recovery_codes} -
verifyAndEnable(Model $authenticatable, string $code, int $window = 1) Vérifier le code et activer TOTP bool TotpException
enable(Model $authenticatable, string $secret, array $recoveryCodes) Activer TOTP avec un secret existant TotpSecret -
disable(Model $authenticatable) Désactiver TOTP bool TotpException
verify(Model $authenticatable, string $code, int $window = 1) Vérifier un code TOTP bool TotpException
verifyRecoveryCode(Model $authenticatable, string $code) Vérifier un code de récupération bool TotpException
markAsVerified(Model $authenticatable) Marquer TOTP comme vérifié void -
isEnabled(Model $authenticatable) Vérifier si TOTP est activé bool -
isVerified(Model $authenticatable) Vérifier si TOTP est vérifié bool -
getRemainingRecoveryCodes(Model $authenticatable) Récupérer les codes de récupération restants StringTypedCollection -
regenerateRecoveryCodes(Model $authenticatable) Régénérer les codes de récupération StringTypedCollection -
getSecret(Model $authenticatable) Récupérer le secret ?string -

TotpGenerator

Méthode Description Retourne
generateSecret(int $length = 32) Générer un secret Base32 string
generateCode(string $secret, ?int $timestamp = null, int $digits = 6, int $period = 30) Générer un code TOTP string
generateCodesWithWindow(string $secret, int $digits = 6, int $period = 30, int $window = 1, ?int $timestamp = null) Générer les codes avec fenêtre de tolérance array
generateRecoveryCodes(int $count = 10, int $length = 8) Générer des codes de récupération array
hashRecoveryCodes(array $codes) Hasher les codes de récupération (SHA256) array
verifyRecoveryCode(string $code, array $hashedCodes) Vérifier un code de récupération bool

QrCodeGenerator

Méthode Description Retourne
generate(string $account, string $secret, string $issuer, int $digits = 6, int $period = 30, string $algorithm = 'SHA1') Générer un QR Code PNG string
generateFromUri(string $uri) Générer un QR Code à partir d'une URI string
generateDataUri(string $uri) Générer une Data URI du QR Code string
buildUri(string $account, string $secret, string $issuer, int $digits = 6, int $period = 30, string $algorithm = 'SHA1') Construire l'URI TOTP string

🎯 Value Objects

Le package supporte les Value Objects suivants :

Value Object Description Exemple
TotpSecretVO Secret TOTP avec métadonnées new TotpSecretVO('JBSWY3...', true, [...], $verifiedAt)
DateTimeVO Date/heure new DateTimeVO('2024-01-01 12:00:00')
StrictDataObject Métadonnées typées StrictDataObject::from(['ip' => '127.0.0.1'])

Accesseurs dans le modèle TotpSecret

$totpSecret = TotpSecret::find(1);

// Accès sous forme de Value Objects
$secretVO = $totpSecret->getSecret();         // TotpSecretVO
$recoveryCodes = $totpSecret->getRecoveryCodes(); // array<string>
$verifiedAt = $totpSecret->getVerifiedAt();   // DateTimeVO
$createdAt = $totpSecret->getCreatedAt();     // DateTimeVO
$updatedAt = $totpSecret->getUpdatedAt();     // DateTimeVO
$metadata = $totpSecret->getMetadata();       // StrictDataObject

// Méthodes utilitaires
$totpSecret->isEnabled();     // bool
$totpSecret->isVerified();    // bool
$totpSecret->countRecoveryCodes();  // int
$totpSecret->hasRecoveryCodes();    // bool

// Relation polymorphique
$authenticatable = $totpSecret->authenticatable;  // User, Admin, etc.

📝 Structure de la base de données

CREATE TABLE totp_secrets (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    authenticatable_type VARCHAR(255) NOT NULL,  -- Type de l'identifiant (morph)
    authenticatable_id BIGINT UNSIGNED NOT NULL, -- ID de l'identifiant
    secret VARCHAR(255) NOT NULL UNIQUE,         -- Secret Base32
    is_enabled BOOLEAN DEFAULT FALSE,            -- TOTP activé ?
    recovery_codes JSON NULL,                    -- Codes de récupération hashés
    verified_at TIMESTAMP NULL,                  -- Date de première vérification
    metadata JSON NULL,                          -- Métadonnées
    created_at TIMESTAMP NULL,
    updated_at TIMESTAMP NULL,
    deleted_at TIMESTAMP NULL,
    
    INDEX idx_authenticatable (authenticatable_type, authenticatable_id),
    INDEX idx_secret (secret),
    INDEX idx_is_enabled (is_enabled)
);

Structure du champ recovery_codes (JSON)

[
    "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8",
    "b109f3bbbc244eb82441917ed06d618b9008dd09b3befd1b5e07394c706a8bb1",
    "..."
]

Les codes de récupération sont hashés en SHA256 avant stockage.

🔧 Exceptions possibles

Exception Message Quand ?
TotpException::totpNotEnabled() TOTP is not enabled for this user. verify() appelé mais TOTP pas activé
TotpException::secretNotFound() TOTP secret not found. verify() appelé mais secret inexistant
TotpException::invalidCode() Invalid TOTP code. Code invalide
TotpException::invalidRecoveryCode() Invalid recovery code. Code de récupération invalide
TotpException::alreadyEnabled() TOTP is already enabled for this user. Activation déjà faite
TotpException::alreadyDisabled() TOTP is already disabled for this user. Désactivation déjà faite
TotpException::maxAttemptsExceeded() Maximum TOTP attempts exceeded. Trop de tentatives
TotpException::setupFailed() TOTP setup failed. Échec de la configuration

👨‍💻 Auteur

Andy Kani

⭐ Support

Si vous trouvez ce package utile, n'hésitez pas à lui donner une ⭐ sur GitHub !

🙏 Remerciements

  • Framework Laravel
  • Tous les contributeurs et utilisateurs de ce package

Construit avec ❤️ pour la communauté Laravel