feat: popup quảng cáo, đặt lại mật khẩu bằng token 80 ký tự, 2FA email, vá lỗ hổng bảo mật
- Popup/banner quảng cáo cấu hình từ admin (loại, link, lịch hiển thị) - Đặt lại mật khẩu: token 80 ký tự kèm check digit Luhn thay cho token+email lộ trong URL, chỉ lưu bản băm, một lần dùng, hết hạn 60 phút - Xác thực 2 lớp qua email khi đăng nhập (mã 6 số, ghi nhớ thiết bị 90 ngày) - ApiAuthCheck chặn ngay tài khoản bị khoá dù JWT còn hạn (trước đây dùng được tới hết TTL) - Handler trả 404 thay vì 500 khi ModelNotFoundException không bọc HttpException - Test ClientTamperingTest: xác nhận IDOR/giả mạo dữ liệu từ client đều bị chặn Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
e888a6ed5d
commit
12d5b4951c
@@ -8,14 +8,16 @@ use App\Mail\Customer\PasswordResetLink;
|
||||
use App\Mail\Customer\VerifyAccount as VerifyAccountMail;
|
||||
use App\Mail\Customer\Welcome as WelcomeMail;
|
||||
use App\Models\Customer;
|
||||
use App\Models\CustomerTrustedDevice;
|
||||
use App\Models\RefreshToken;
|
||||
use App\Models\Setting;
|
||||
use App\Repositories\Api\Client\ClientRepositoryInterface;
|
||||
use App\Support\PasswordResetToken;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
@@ -39,15 +41,17 @@ class ClientAuthService
|
||||
/**
|
||||
* Xác thực khách hàng và tạo access + refresh token
|
||||
*/
|
||||
public function authenticateClient(array $credentials): array
|
||||
public function verifyCredentials(array $credentials): Customer
|
||||
{
|
||||
if (!$token = Auth::guard('client')->attempt($credentials)) {
|
||||
$guard = Auth::guard('client');
|
||||
|
||||
if (!$guard->validate($credentials)) {
|
||||
throw ValidationException::withMessages([
|
||||
'email' => ['Thông tin đăng nhập không chính xác.'],
|
||||
]);
|
||||
}
|
||||
|
||||
$client = Auth::guard('client')->user();
|
||||
$client = $guard->getLastAttempted();
|
||||
|
||||
if ((int)$client->status !== 1) {
|
||||
$statusLabel = match ((int)$client->status) {
|
||||
@@ -68,12 +72,18 @@ class ClientAuthService
|
||||
]);
|
||||
}
|
||||
|
||||
$refreshToken = $this->createRefreshToken($client->id);
|
||||
return $client;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{client: Customer, token: string, refresh_token: string}
|
||||
*/
|
||||
public function issueTokens(Customer $client): array
|
||||
{
|
||||
return [
|
||||
'client' => $client,
|
||||
'token' => $token,
|
||||
'refresh_token' => $refreshToken,
|
||||
'token' => JWTAuth::fromUser($client),
|
||||
'refresh_token' => $this->createRefreshToken($client->id),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -357,21 +367,14 @@ class ClientAuthService
|
||||
return null;
|
||||
}
|
||||
|
||||
// Broker tự throttle theo email (auth.passwords.users.throttle).
|
||||
Password::broker('users')->sendResetLink(
|
||||
['email' => $customer->email],
|
||||
function (Customer $customer, string $token) {
|
||||
$url = rtrim(config('app.frontend_url'), '/') . '/dat-lai-mat-khau?' . http_build_query([
|
||||
'token' => $token,
|
||||
'email' => $customer->email,
|
||||
]);
|
||||
$token = PasswordResetToken::issue($customer);
|
||||
|
||||
Mail::to($customer->email)->queue(new PasswordResetLink([
|
||||
'fullname' => $customer->fullname,
|
||||
'reset_url' => $url,
|
||||
]));
|
||||
},
|
||||
);
|
||||
if ($token !== null) {
|
||||
Mail::to($customer->email)->queue(new PasswordResetLink([
|
||||
'fullname' => $customer->fullname,
|
||||
'reset_url' => rtrim(config('app.frontend_url'), '/') . '/dat-lai-mat-khau?t=' . $token,
|
||||
]));
|
||||
}
|
||||
|
||||
return $customer;
|
||||
}
|
||||
@@ -380,24 +383,25 @@ class ClientAuthService
|
||||
* Đặt lại mật khẩu bằng token trong email. Thành công thì thu hồi toàn bộ refresh token
|
||||
* để các phiên cũ (có thể của kẻ gian) phải đăng nhập lại.
|
||||
*/
|
||||
public function resetPassword(array $data): ?Customer
|
||||
public function resetPassword(string $token, string $password): Customer
|
||||
{
|
||||
$resetCustomer = null;
|
||||
$customer = PasswordResetToken::findCustomer($token);
|
||||
|
||||
$status = Password::broker('users')->reset($data, function (Customer $customer, string $password) use (&$resetCustomer) {
|
||||
$customer->password = Hash::make($password);
|
||||
$customer->save();
|
||||
RefreshToken::where('customer_id', $customer->id)->delete();
|
||||
$resetCustomer = $customer;
|
||||
});
|
||||
|
||||
if ($status !== Password::PASSWORD_RESET) {
|
||||
if (!$customer) {
|
||||
throw ValidationException::withMessages([
|
||||
'token' => ['Link đặt lại mật khẩu không hợp lệ hoặc đã hết hạn.'],
|
||||
]);
|
||||
}
|
||||
|
||||
return $resetCustomer;
|
||||
DB::transaction(function () use ($customer, $password) {
|
||||
$customer->password = Hash::make($password);
|
||||
$customer->save();
|
||||
PasswordResetToken::revokeFor($customer);
|
||||
RefreshToken::where('customer_id', $customer->id)->delete();
|
||||
CustomerTrustedDevice::where('customer_id', $customer->id)->delete();
|
||||
});
|
||||
|
||||
return $customer;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -446,5 +450,6 @@ class ClientAuthService
|
||||
|
||||
// Access token cũ tự vô hiệu nhờ claim `pv`; thu hồi luôn refresh token để mọi phiên phải đăng nhập lại.
|
||||
RefreshToken::where('customer_id', $client->id)->delete();
|
||||
CustomerTrustedDevice::where('customer_id', $client->id)->delete();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user