承接 andydefer/laravel-otp 相关项目开发

从需求分析到上线部署,全程专人跟进,保证项目质量与交付效率

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

andydefer/laravel-otp

Composer 安装命令:

composer require andydefer/laravel-otp

包简介

Laravel package providing OTP (One-Time Password) management with Repository pattern, DTOs, Enums, and Value Objects.

README 文档

README

Système de mots de passe à usage unique (OTP) polymorphique pour applications Laravel

Latest Version Total Downloads PHP Version License

Un package Laravel complet pour gérer des mots de passe à usage unique (OTP) polymorphiques avec le pattern Repository, des DTOs, des Value Objects, le support TOTP (RFC 6238), des codes de récupération et une gestion des tentatives et du rate limiting.

📋 Table des matières

✨ Fonctionnalités

  • Polymorphisme - OTPs attachables à n'importe quel modèle (User, Admin, etc.)
  • Types d'OTP flexibles - Utilisez les types prédéfinis ou créez vos propres types via PurposeVO
  • 6 types prédéfinis - Authentification, Réinitialisation, Vérification email/téléphone, 2FA, Transaction
  • TOTP Support - RFC 6238 compatible avec génération de secrets Base32
  • Codes de récupération - Génération de codes de secours (configurables)
  • Gestion des tentatives - Limitez les tentatives par OTP (configurable par type)
  • Expiration automatique - TTL configurable par type d'OTP
  • Rate Limiting - Limitez le nombre d'OTP par période (15 min par défaut)
  • DTO Pattern - OtpRecord et OtpFilterRecord pour un typage fort
  • Value Objects - PurposeVO pour une gestion flexible des types
  • Repository Pattern - Séparation propre de la logique d'accès aux données
  • Multi-bases de données - Support SQLite, MySQL, PostgreSQL pour les filtres JSON
  • 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

🚀 Prérequis

  • PHP 8.2 ou supérieur
  • Laravel 12.0, 13.0, 14.0 ou 15.0

📦 Installation

Installez le package via Composer :

composer require andydefer/laravel-otp

Publier les migrations

php artisan vendor:publish --tag=Otp-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\LaravelOtp\OtpServiceProvider::class,
],

🏗️ Structure du package

laravel-otp/
├── src/
│   ├── OtpServiceProvider.php
│   ├── Models/
│   │   └── Otp.php
│   ├── Records/
│   │   ├── OtpRecord.php
│   │   └── OtpFilterRecord.php
│   ├── Repositories/
│   │   └── OtpRepository.php
│   ├── Services/
│   │   ├── OtpService.php
│   │   └── OtpGenerator.php
│   └── ValueObjects/
│       └── PurposeVO.php
├── database/
│   └── migrations/
│       └── create_otps_table.php
└── tests/
    ├── Integration/
    │   └── Services/
    │       └── OtpServiceIntegrationTest.php
    ├── Fixtures/
    └── IntegrationTestCase.php

📖 Utilisation

Créer un OTP avec un type prédéfini

Le package fournit des constantes prédéfinies via la classe PurposeVO :

use AndyDefer\LaravelOtp\Services\OtpService;
use AndyDefer\LaravelOtp\ValueObjects\PurposeVO;

class AuthController extends Controller
{
    public function sendOtp(OtpService $otpService)
    {
        $user = auth()->user();

        // Créer un PurposeVO pour l'authentification
        $purpose = new PurposeVO(
            value: 'authentication',
            label: 'Authentification',
            ttl: 300,
            maxAttempts: 3
        );

        $otp = $otpService->create(
            identifier: $user,
            purpose: $purpose
        );

        // Envoyer le code par SMS ou email
        Notification::send($user, new OtpNotification($otp->code));

        return response()->json([
            'message' => 'OTP envoyé',
            'expires_in' => $otp->expires_at->diffInSeconds(now())
        ]);
    }
}

Créer un OTP avec un type personnalisé

Vous pouvez créer vos propres types d'OTP :

$customPurpose = new PurposeVO(
    value: 'api_access',
    label: 'Accès API',
    ttl: 180,        // 3 minutes
    maxAttempts: 3
);

$otp = $otpService->create($user, $customPurpose);

Créer un OTP avec un code personnalisé

$purpose = new PurposeVO('password_reset', 'Réinitialisation', 900, 5);

$otp = $otpService->create(
    identifier: $user,
    purpose: $purpose,
    code: 'SECURE-CODE-123' // Code personnalisé
);

Créer un OTP avec un TTL personnalisé

$purpose = new PurposeVO('two_factor', 'Double authentification', 120, 3);

// Le TTL du purpose est utilisé par défaut
$otp = $otpService->create($user, $purpose);

// Ou surcharge avec un TTL personnalisé
$otp = $otpService->create(
    identifier: $user,
    purpose: $purpose,
    code: null,
    ttl: 60 // 1 minute
);

Vérifier un OTP

$purpose = new PurposeVO('authentication', 'Authentification', 300, 3);

$valid = $otpService->verify(
    identifier: $user,
    code: $request->input('code'),
    purpose: $purpose
);

if ($valid) {
    // OTP valide, connecter l'utilisateur
    return response()->json(['message' => 'Authentification réussie']);
}

return response()->json(['message' => 'Code invalide ou expiré'], 400);

Vérifier un OTP sans le marquer comme utilisé

// Permet de vérifier un OTP sans le consommer
// Utile pour les pré-vérifications
$valid = $otpService->verify(
    identifier: $user,
    code: $code,
    purpose: $purpose,
    markAsUsed: false
);

Invalider un OTP

// Invalide tous les OTPs d'un type pour un identifiant
$purpose = new PurposeVO('authentication', 'Authentification', 300, 3);
$otpService->invalidate($user, $purpose);

Récupérer tous les OTPs

// Récupère tous les OTPs (valides et invalides) pour un identifiant et un type
$purpose = new PurposeVO('authentication', 'Authentification', 300, 3);
$otps = $otpService->getAllFor($user, $purpose);

Récupérer les OTPs valides

// Récupère uniquement les OTPs valides (non expirés et non utilisés)
$purpose = new PurposeVO('authentication', 'Authentification', 300, 3);
$validOtps = $otpService->getValidFor($user, $purpose);

Compter les OTPs

$purpose = new PurposeVO('authentication', 'Authentification', 300, 3);

// Compter tous les OTPs
$count = $otpService->countFor($user, $purpose);

// Compter les OTPs valides
$validCount = $otpService->countValidFor($user, $purpose);

// Compter les OTPs expirés
$expiredCount = $otpService->countExpiredFor($user, $purpose);

Supprimer les OTPs expirés

// Supprime tous les OTPs expirés de la base de données
$deletedCount = $otpService->deleteExpired();

Gestion des tentatives

$purpose = new PurposeVO('authentication', 'Authentification', 300, 3);

// Incrémenter les tentatives pour un OTP
$otpService->incrementAttempts($user, $code, $purpose);

// Récupérer le nombre de tentatives
$attempts = $otpService->getAttempts($user, $code, $purpose);

TOTP (Time-based OTP)

// 1. Générer un secret pour l'utilisateur
$secret = $otpService->generateSecret();

// 2. Stocker le secret chez l'utilisateur
$user->otp_secret = $secret;
$user->save();

// 3. Créer un OTP basé sur TOTP
$purpose = new PurposeVO('two_factor', 'Double authentification', 120, 3);

$otp = $otpService->createWithTOTP(
    identifier: $user,
    purpose: $purpose,
    secret: $secret
);

// Le code est généré automatiquement selon RFC 6238
// Code valide pendant 30 secondes (intervalle TOTP standard)

Codes de récupération

// Générer 10 codes de récupération de 8 caractères
$recoveryCodes = $otpService->generateRecoveryCodes(
    identifier: $user,
    count: 10,
    length: 8
);

// Stocker les codes hashés chez l'utilisateur
$user->recovery_codes = array_map('hash', $recoveryCodes);
$user->save();

// Afficher les codes à l'utilisateur
return response()->json(['recovery_codes' => $recoveryCodes]);

Rate Limiting

$purpose = new PurposeVO('authentication', 'Authentification', 300, 3);

// Vérifier si l'utilisateur a dépassé la limite
$isRateLimited = $otpService->isRateLimited(
    identifier: $user,
    purpose: $purpose,
    limit: 5,    // 5 tentatives
    window: now()->subMinutes(15)  // sur 15 minutes
);

if ($isRateLimited) {
    return response()->json([
        'message' => 'Trop de tentatives. Veuillez réessayer plus tard.'
    ], 429);
}

📚 Référence de l'API

OtpService

Méthode Description Retourne
create(Model $identifier, PurposeVO $purpose, ?string $code = null, ?int $ttl = null) Créer un OTP Model
createWithTOTP(Model $identifier, PurposeVO $purpose, string $secret, ?int $ttl = null) Créer un OTP TOTP Model
verify(Model $identifier, string $code, PurposeVO $purpose, bool $markAsUsed = true) Vérifier un OTP bool
findValid(Model $identifier, string $code, PurposeVO $purpose) Trouver un OTP valide ?Model
invalidate(Model $identifier, PurposeVO $purpose) Invalider tous les OTPs d'un type void
getAllFor(Model $identifier, PurposeVO $purpose) Récupérer tous les OTPs Collection
getValidFor(Model $identifier, PurposeVO $purpose) Récupérer les OTPs valides Collection
deleteExpired() Supprimer les OTPs expirés int
getAttempts(Model $identifier, string $code, PurposeVO $purpose) Récupérer les tentatives int
incrementAttempts(Model $identifier, string $code, PurposeVO $purpose) Incrémenter les tentatives void
countFor(Model $identifier, PurposeVO $purpose) Compter les OTPs int
countValidFor(Model $identifier, PurposeVO $purpose) Compter les OTPs valides int
countExpiredFor(Model $identifier, PurposeVO $purpose) Compter les OTPs expirés int
generateRecoveryCodes(Model $identifier, int $count = 10, int $length = 8) Générer des codes de récupération array
generateSecret() Générer un secret TOTP string
isRateLimited(Model $identifier, PurposeVO $purpose, int $limit = 5, ?CarbonInterface $window = null) Vérifier le rate limiting bool

OtpRepository

Méthode Description Retourne
incrementAttempts(int $id) Incrémenter les tentatives d'un OTP Otp
markAsUsed(int $id) Marquer un OTP comme utilisé Otp
delete(int $id) Supprimer un OTP (hérité) bool
find(int $id) Trouver un OTP par ID (hérité) ?Otp
count(AbstractRecord $criteria) Compter les OTPs (hérité) int

OtpGenerator

Méthode Description Retourne
generate(int $length = 6, bool $numeric = true) Générer un code OTP string
generateTOTP(string $secret, int $digits = 6) Générer un code TOTP string
generateSecret(int $length = 32) Générer un secret Base32 string
generateRecoveryCodes(int $count = 10, int $length = 8) Générer des codes de récupération array

PurposeVO

Méthode Description Retourne
__construct(string $value, ?string $label, ?int $ttl, ?int $maxAttempts) Créer un PurposeVO -
getValue() Récupérer les données StrictDataObject
getLabel() Récupérer le label ?string
getTtl() Récupérer le TTL ?int
getMaxAttempts() Récupérer les tentatives max ?int
__toString() Convertir en JSON string

🎯 Value Objects

Le package supporte les Value Objects suivants :

Value Object Description Exemple
PurposeVO Type d'OTP avec label, TTL et maxAttempts new PurposeVO('auth', 'Auth', 300, 3)
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 Otp

$otp = Otp::find(1);

// Accès sous forme de Value Objects
$purpose = $otp->getPurpose();        // PurposeVO
$createdAt = $otp->getCreatedAt();    // DateTimeVO
$updatedAt = $otp->getUpdatedAt();    // DateTimeVO
$expiresAt = $otp->getExpiresAt();    // DateTimeVO
$usedAt = $otp->getUsedAt();          // DateTimeVO
$deletedAt = $otp->getDeletedAt();    // DateTimeVO
$metadata = $otp->getMetadata();      // StrictDataObject

// Méthodes utilitaires
$otp->isValid();      // bool
$otp->isUsed();       // bool
$otp->isExpired();    // bool

// Relations
$identifier = $otp->identifier;  // User, Admin, etc.

📝 Structure de la base de données

CREATE TABLE otps (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    identifier_type VARCHAR(255) NOT NULL,    -- Type de l'identifiant (morph)
    identifier_id BIGINT UNSIGNED NOT NULL,   -- ID de l'identifiant
    code VARCHAR(255) NOT NULL,               -- Code OTP
    purpose JSON NOT NULL,                    -- Type d'OTP (JSON)
    expires_at TIMESTAMP NOT NULL,            -- Date d'expiration
    used_at TIMESTAMP NULL,                   -- Date d'utilisation
    attempts INT DEFAULT 0,                   -- Nombre de tentatives
    metadata JSON NULL,                       -- Métadonnées
    created_at TIMESTAMP NULL,
    updated_at TIMESTAMP NULL,
    deleted_at TIMESTAMP NULL,
    
    INDEX idx_identifier (identifier_type, identifier_id),
    INDEX idx_code (code),
    INDEX idx_expires_at (expires_at),
    INDEX idx_used_at (used_at)
);

Structure du champ purpose (JSON)

{
    "value": "authentication",
    "label": "Authentification",
    "ttl": 300,
    "maxAttempts": 3
}

🔍 Exemple complet

use AndyDefer\LaravelOtp\Services\OtpService;
use AndyDefer\LaravelOtp\ValueObjects\PurposeVO;

class AuthController extends Controller
{
    public function __construct(
        private readonly OtpService $otpService
    ) {}

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

        $purpose = new PurposeVO(
            value: 'authentication',
            label: 'Authentification',
            ttl: 300,
            maxAttempts: 3
        );

        // Rate limiting
        if ($this->otpService->isRateLimited($user, $purpose)) {
            return response()->json([
                'message' => 'Too many attempts. Please try again later.'
            ], 429);
        }

        $otp = $this->otpService->create($user, $purpose);

        // Envoyer par SMS ou email
        Notification::send($user, new OtpNotification($otp->code));

        return response()->json([
            'message' => 'OTP sent',
            'expires_in' => $otp->expires_at->diffInSeconds(now())
        ]);
    }

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

        $purpose = new PurposeVO(
            value: 'authentication',
            label: 'Authentification',
            ttl: 300,
            maxAttempts: 3
        );

        $valid = $this->otpService->verify(
            identifier: $user,
            code: $request->code,
            purpose: $purpose
        );

        if (!$valid) {
            $this->otpService->incrementAttempts(
                $user,
                $request->code,
                $purpose
            );

            return response()->json([
                'message' => 'Invalid or expired OTP'
            ], 400);
        }

        auth()->login($user);

        return response()->json([
            'message' => 'Authenticated successfully',
            'user' => $user
        ]);
    }

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

        // Générer un secret TOTP
        $secret = $this->otpService->generateSecret();
        $user->otp_secret = $secret;
        $user->save();

        // Générer des codes de récupération
        $recoveryCodes = $this->otpService->generateRecoveryCodes($user, 10, 8);

        return response()->json([
            'secret' => $secret,
            'recovery_codes' => $recoveryCodes,
            'qr_code_url' => $this->generateQrCode($user->email, $secret)
        ]);
    }

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

        $purpose = new PurposeVO(
            value: 'two_factor',
            label: 'Double authentification',
            ttl: 120,
            maxAttempts: 3
        );

        $otp = $this->otpService->createWithTOTP(
            identifier: $user,
            purpose: $purpose,
            secret: $user->otp_secret
        );

        $valid = $this->otpService->verify(
            identifier: $user,
            code: $request->code,
            purpose: $purpose
        );

        if (!$valid) {
            return response()->json(['message' => 'Invalid 2FA code'], 400);
        }

        $user->two_factor_enabled = true;
        $user->save();

        return response()->json(['message' => '2FA enabled successfully']);
    }

    public function customPurpose()
    {
        $user = User::find(1);

        // Créer un type d'OTP personnalisé
        $customPurpose = new PurposeVO(
            value: 'api_access',
            label: 'Accès API',
            ttl: 180,
            maxAttempts: 3
        );

        $otp = $this->otpService->create($user, $customPurpose);

        return $otp;
    }
}

🧪 Tests

Exécuter les tests

composer test

Exécuter uniquement les tests unitaires

composer test-unit

Exécuter uniquement les tests d'intégration

composer test-integration

Configuration des tests

Le package utilise orchestra/testbench pour les tests d'intégration avec une base de données SQLite en mémoire.

🔧 Développement

Style de code

./vendor/bin/pint

Analyse statique

./vendor/bin/phpstan analyse
./vendor/bin/psalm

📄 Journal des modifications

Veuillez consulter le CHANGELOG pour plus d'informations sur les modifications récentes.

🤝 Contribuer

Veuillez consulter CONTRIBUTING pour plus de détails.

Flux de développement

  1. Forkez le dépôt
  2. Créez une branche de fonctionnalité (git checkout -b feature/amazing-feature)
  3. Apportez vos modifications
  4. Exécutez les tests (composer test)
  5. Committez vos modifications (git commit -m 'Ajouter une fonctionnalité géniale')
  6. Poussez vers la branche (git push origin feature/amazing-feature)
  7. Ouvrez une Pull Request

📦 Dépendances

👨‍💻 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

andydefer/laravel-otp 适用场景与选型建议

andydefer/laravel-otp 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 0 次下载、GitHub Stars 达 0, 最近一次更新时间为 2026 年 07 月 15 日, 在 PHP 生态内属于活跃度较高的组件。

我们在过去多个企业项目中使用过 andydefer/laravel-otp 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 andydefer/laravel-otp 我们能提供哪些服务?
定制开发 / 二次开发

基于 andydefer/laravel-otp 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 0
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 0
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-07-15