first commit
This commit is contained in:
53
app/Services/Auth/PasswordManager.php
Normal file
53
app/Services/Auth/PasswordManager.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Auth;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Support\PasswordPolicy;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
/**
|
||||
* Управление сменой пароля с ведением истории (меры ИАФ.3).
|
||||
*/
|
||||
class PasswordManager
|
||||
{
|
||||
/**
|
||||
* Установить новый пароль: сохранить старый в историю, обновить метку
|
||||
* времени смены и подрезать историю до лимита.
|
||||
*/
|
||||
public function change(User $user, string $newPassword): void
|
||||
{
|
||||
DB::transaction(function () use ($user, $newPassword): void {
|
||||
// Сохраняем текущий пароль в историю до перезаписи.
|
||||
if ($user->password) {
|
||||
$user->passwordHistories()->create([
|
||||
'password_hash' => $user->password,
|
||||
'created_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
$user->forceFill([
|
||||
'password' => Hash::make($newPassword),
|
||||
'password_changed_at' => now(),
|
||||
])->save();
|
||||
|
||||
$this->trimHistory($user);
|
||||
});
|
||||
}
|
||||
|
||||
private function trimHistory(User $user): void
|
||||
{
|
||||
$limit = PasswordPolicy::historyLimit();
|
||||
|
||||
$ids = $user->passwordHistories()
|
||||
->orderByDesc('created_at')
|
||||
->skip($limit)
|
||||
->take(PHP_INT_MAX)
|
||||
->pluck('id');
|
||||
|
||||
if ($ids->isNotEmpty()) {
|
||||
$user->passwordHistories()->whereIn('id', $ids)->delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
85
app/Services/Auth/TwoFactorService.php
Normal file
85
app/Services/Auth/TwoFactorService.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Auth;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
use PragmaRX\Google2FA\Google2FA;
|
||||
|
||||
/**
|
||||
* Многофакторная аутентификация по TOTP (мера ИАФ.4).
|
||||
*
|
||||
* Секрет и резервные коды хранятся в зашифрованном виде (ЗНИ) — поля помечены
|
||||
* как encrypted в модели User. Резервные коды дополнительно хешируются.
|
||||
*/
|
||||
class TwoFactorService
|
||||
{
|
||||
public function __construct(private readonly Google2FA $engine) {}
|
||||
|
||||
public function generateSecret(): string
|
||||
{
|
||||
return $this->engine->generateSecretKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* URI для QR-кода (otpauth://) — добавляется в приложение-аутентификатор.
|
||||
*/
|
||||
public function provisioningUri(User $user, string $secret): string
|
||||
{
|
||||
return $this->engine->getQRCodeUrl(
|
||||
(string) config('app.name'),
|
||||
(string) $user->email,
|
||||
$secret,
|
||||
);
|
||||
}
|
||||
|
||||
public function verify(string $secret, string $code): bool
|
||||
{
|
||||
// verifyKey возвращает позицию окна (int) при успехе либо false.
|
||||
return $this->engine->verifyKey($secret, $code) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Сгенерировать набор одноразовых резервных кодов.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function generateRecoveryCodes(): array
|
||||
{
|
||||
$count = (int) config('security.mfa.recovery_codes');
|
||||
|
||||
return Collection::times($count, fn () => Str::upper(Str::random(5).'-'.Str::random(5)))
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $codes
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function hashRecoveryCodes(array $codes): array
|
||||
{
|
||||
return array_map(static fn (string $code) => Hash::make($code), $codes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить и «погасить» резервный код. Возвращает новый список хешей или
|
||||
* null, если код не подошёл.
|
||||
*
|
||||
* @param array<int, string> $hashedCodes
|
||||
* @return array<int, string>|null
|
||||
*/
|
||||
public function consumeRecoveryCode(array $hashedCodes, string $input): ?array
|
||||
{
|
||||
foreach ($hashedCodes as $index => $hash) {
|
||||
if (Hash::check($input, $hash)) {
|
||||
unset($hashedCodes[$index]);
|
||||
|
||||
return array_values($hashedCodes);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user