giftcon_dev/app/Services/Admin/AdminOtpService.php
2026-02-04 16:55:00 +09:00

100 lines
3.4 KiB
PHP

<?php
namespace App\Services\Admin;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Str;
class AdminOtpService
{
public function startChallenge(int $adminUserId, string $phoneE164, string $ip, string $ua): array
{
$challengeId = Str::random(40);
$otp = str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT);
$prefix = (string) config('admin.redis_prefix', 'admin:2fa:');
$key = $prefix . 'challenge:' . $challengeId;
$otpHashKey = (string) config('admin.otp_hash_key', '');
$otpHash = hash_hmac('sha256', $otp, $otpHashKey);
$ttl = (int) config('admin.otp_ttl', 300);
$cooldown = (int) config('admin.otp_resend_cooldown', 30);
Redis::hmset($key, [
'admin_user_id' => (string) $adminUserId,
'otp_hash' => $otpHash,
'attempts' => '0',
'resend_count' => '0',
'resend_after' => (string) (time() + $cooldown),
'ip' => $ip,
'ua' => mb_substr($ua, 0, 250),
]);
Redis::expire($key, $ttl);
return [
'challenge_id' => $challengeId,
'otp' => $otp, // DB 저장 금지, “발송에만” 사용
];
}
public function canResend(string $challengeId): array
{
$key = (string) config('admin.redis_prefix', 'admin:2fa:') . 'challenge:' . $challengeId;
$resendAfter = (int) (Redis::hget($key, 'resend_after') ?: 0);
if ($resendAfter > time()) {
return ['ok' => false, 'wait' => $resendAfter - time()];
}
return ['ok' => true, 'wait' => 0];
}
public function resend(string $challengeId): ?string
{
$key = (string) config('admin.redis_prefix', 'admin:2fa:') . 'challenge:' . $challengeId;
if (!Redis::exists($key)) return null;
$cooldown = (int) config('admin.otp_resend_cooldown', 30);
$otpHashKey = (string) config('admin.otp_hash_key', '');
$otp = str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT);
$otpHash = hash_hmac('sha256', $otp, $otpHashKey);
Redis::hset($key, 'otp_hash', $otpHash);
Redis::hincrby($key, 'resend_count', 1);
Redis::hset($key, 'resend_after', (string) (time() + $cooldown));
return $otp;
}
public function verify(string $challengeId, string $otpInput): array
{
$key = (string) config('admin.redis_prefix', 'admin:2fa:') . 'challenge:' . $challengeId;
if (!Redis::exists($key)) {
return ['ok' => false, 'reason' => 'expired'];
}
$maxAttempts = (int) config('admin.otp_max_attempts', 5);
$attempts = (int) (Redis::hget($key, 'attempts') ?: 0);
if ($attempts >= $maxAttempts) {
return ['ok' => false, 'reason' => 'locked'];
}
$otpHashKey = (string) config('admin.otp_hash_key', '');
$expected = (string) Redis::hget($key, 'otp_hash');
$given = hash_hmac('sha256', trim($otpInput), $otpHashKey);
if (!hash_equals($expected, $given)) {
Redis::hincrby($key, 'attempts', 1);
$left = max(0, $maxAttempts - ($attempts + 1));
return ['ok' => false, 'reason' => 'mismatch', 'left' => $left];
}
$adminUserId = (int) (Redis::hget($key, 'admin_user_id') ?: 0);
Redis::del($key);
return ['ok' => true, 'admin_user_id' => $adminUserId];
}
}