pragmarx/recovery
Generate customizable recovery/backup codes for 2FA account recovery. Create arrays, JSON, or (Laravel) Collections, and tune how many codes to make plus blocks and characters per block for your preferred format and length.
Installation
composer require pragmarx/recovery
Add to config/auth.php under guards (if using Laravel's auth):
'web' => [
'driver' => 'session',
'provider' => 'users',
'recovery' => true, // Enable recovery codes
],
Publish Config
php artisan vendor:publish --provider="Pragmarx\Recovery\RecoveryServiceProvider"
Configure config/recovery.php (default: 10 codes, 6 digits, 30-day expiry).
First Use Case: Generate Codes
use Pragmarx\Recovery\Recovery;
$user = auth()->user();
$codes = Recovery::generate($user); // Returns array of codes
Store the hashed codes in user.recovery_codes (migration provided in package).
database/migrations/xxxx_create_recovery_codes_table.phpvendor/pragmarx/recovery/src/RecoveryServiceProvider.phpvendor/pragmarx/recovery/src/Facades/Recovery.phpconfig/recovery.phpGenerate Codes (Admin/Onboarding)
$codes = Recovery::generate($user);
// Send via email/SMS (e.g., using Laravel Notifications)
Validate Codes (Login/2FA Fallback)
if (Recovery::attempt($user, $input['code'])) {
auth()->login($user, true); // Auto-login on success
}
Rotate Codes (Security)
Recovery::rotate($user); // Invalidates all existing codes
auth:web guard with recovery enabled.public function login(Request $request) {
$credentials = $request->only(['email', 'password']);
if (!auth()->attempt($credentials)) {
return back()->withErrors(['email' => 'Invalid credentials']);
}
// Check for 2FA (e.g., using `laravel-2fa`)
if (auth()->user()->has2faEnabled()) {
return redirect()->route('verify.2fa');
}
// Fallback to recovery codes
return view('auth.recovery');
}
Pragmarx\Recovery\Recovery::storeCodes() to use a different DB table or cache.config/recovery.php to change length/digits:
'code' => [
'length' => 8,
'digits' => true,
],
recovery.codes.generated or recovery.code.attempted:
Recovery::generate($user)->each(fn($code) => event(new CodeGenerated($user, $code)));
Code Expiry
Recovery::rotate($user) or update config/recovery.php:
'expiry' => 90, // Days
user.recovery_codes table for expires_at timestamps.Rate Limiting
use Illuminate\Cache\RateLimiting\Limit;
RateLimiter::for('recovery_attempts', function (Request $request) {
return Limit::perMinute(5)->by($request->ip());
});
Code Reuse
Pragmarx\Recovery\Recovery::attempt():
$code = RecoveryCode::where('code', $code)
->where('user_id', $user->id)
->whereNull('used_at')
->first();
Check Generated Codes
dd(Recovery::getCodes($user)); // Returns unhashed codes (for testing)
Verify Hashing
hash_hmac(). Override in config/recovery.php:
'hash_algorithm' => 'sha256',
Log Attempts
.env:
DB_LOG_QUERIES=true
user.recovery_attempts table for failed attempts.web). Use Recovery::guard('api')->generate($user) for multi-guard setups.User model has recoveryCodes() relationship:
public function recoveryCodes() {
return $this->hasMany(RecoveryCode::class);
}
Custom Code Generator
Pragmarx\Recovery\Generators\CodeGenerator:
class CustomGenerator extends CodeGenerator {
public function generate(): string {
return Str::random(8); // Custom logic
}
}
AppServiceProvider:
Recovery::setGenerator(new CustomGenerator());
Multi-Factor Recovery
laravel-2fa:
if (TwoFactor::verifyCode($user, $input['code'])) {
// Success
} elseif (Recovery::attempt($user, $input['recovery_code'])) {
// Fallback
}
Audit Logging
Pragmarx\Recovery\RecoveryCode model to log events:
protected static function boot() {
parent::boot();
static::created(fn($code) => Log::info("Recovery code generated for {$code->user->email}"));
}
How can I help you explore Laravel packages today?