238 lines
5.4 KiB
PHP
238 lines
5.4 KiB
PHP
<?php
|
|
|
|
namespace App\Helpers;
|
|
|
|
use App\Exceptions\Entities\AuthorizationException;
|
|
use Cache;
|
|
use GuzzleHttp\Client;
|
|
use Request;
|
|
use Throwable;
|
|
|
|
class Recaptcha
|
|
{
|
|
private string $userEmail;
|
|
|
|
private bool $solved = false;
|
|
|
|
/**
|
|
* Increment amount of login tries for ip and login
|
|
*/
|
|
public function incrementCaptchaAmounts(): void
|
|
{
|
|
if (!$this->captchaEnabled()) {
|
|
return;
|
|
}
|
|
|
|
$cacheKey = $this->getCaptchaCacheKey();
|
|
|
|
if (!Cache::store('octane')->has($cacheKey)) {
|
|
Cache::store('octane')->put($cacheKey, 1, config('recaptcha.ttl'));
|
|
} else {
|
|
Cache::store('octane')->increment($cacheKey);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Shows captcha status from config
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function captchaEnabled(): bool
|
|
{
|
|
return config('recaptcha.enabled');
|
|
}
|
|
|
|
/**
|
|
* Returns unique for ip and user login key
|
|
* @return string
|
|
*/
|
|
private function getCaptchaCacheKey(): string
|
|
{
|
|
$ip = Request::ip();
|
|
$email = $this->userEmail;
|
|
return "AUTH_RECAPTCHA_LIMITER_{$ip}_{$email}_ATTEMPTS";
|
|
}
|
|
|
|
/**
|
|
* Forget about tries of user to login
|
|
*/
|
|
public function clearCaptchaAmounts(): void
|
|
{
|
|
if (!$this->captchaEnabled()) {
|
|
return;
|
|
}
|
|
|
|
Cache::store('octane')->forget($this->getCaptchaCacheKey());
|
|
}
|
|
|
|
/**
|
|
* @param array $credentials
|
|
* @throws AuthorizationException
|
|
*/
|
|
public function check(array $credentials): void
|
|
{
|
|
if ($this->isBanned()) {
|
|
$this->incrementBanAmounts();
|
|
throw new AuthorizationException(AuthorizationException::ERROR_TYPE_BANNED);
|
|
}
|
|
|
|
$this->userEmail = $credentials['email'];
|
|
|
|
if (isset($credentials['recaptcha'])) {
|
|
$this->solve($credentials['recaptcha']);
|
|
}
|
|
|
|
if ($this->needsCaptcha()) {
|
|
$this->incrementBanAmounts();
|
|
throw new AuthorizationException(AuthorizationException::ERROR_TYPE_CAPTCHA);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Tests if user on ban list
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function isBanned(): bool
|
|
{
|
|
if (!$this->banEnabled()) {
|
|
return false;
|
|
}
|
|
|
|
$cacheKey = $this->getBanCacheKey();
|
|
|
|
$banData = Cache::store('octane')->get($cacheKey, null);
|
|
|
|
if ($banData === null || !isset($banData['amounts'], $banData['time'])) {
|
|
return false;
|
|
}
|
|
|
|
if ($banData['amounts'] < config('recaptcha.ban_attempts')) {
|
|
return false;
|
|
}
|
|
|
|
if ($banData['time'] + config('recaptcha.rate_limiter_ttl') < time()) {
|
|
Cache::store('octane')->forget($cacheKey);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Shows ban limiter status from config
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function banEnabled(): bool
|
|
{
|
|
return $this->captchaEnabled() && config('recaptcha.rate_limiter_enabled');
|
|
}
|
|
|
|
/**
|
|
* Returns unique for ip key
|
|
*
|
|
* @return string
|
|
*/
|
|
private function getBanCacheKey(): string
|
|
{
|
|
$ip = Request::ip();
|
|
return "AUTH_RATE_LIMITER_{$ip}";
|
|
}
|
|
|
|
/**
|
|
* Increment amount of tries for ip
|
|
*/
|
|
private function incrementBanAmounts(): void
|
|
{
|
|
if (!$this->banEnabled()) {
|
|
return;
|
|
}
|
|
|
|
$cacheKey = $this->getBanCacheKey();
|
|
|
|
$banData = Cache::store('octane')->get($cacheKey);
|
|
|
|
if ($banData === null || !isset($banData['amounts'])) {
|
|
$banData = ['amounts' => 1, 'time' => time()];
|
|
} else {
|
|
$banData['amounts']++;
|
|
}
|
|
|
|
Cache::store('octane')->put($cacheKey, $banData, config('recaptcha.rate_limiter_ttl'));
|
|
}
|
|
|
|
/**
|
|
* Sends request to google with captcha token for verify
|
|
*
|
|
* @param string $captchaToken
|
|
*/
|
|
private function solve(string $captchaToken = ''): void
|
|
{
|
|
if (!$this->captchaEnabled()) {
|
|
return;
|
|
}
|
|
|
|
if ($this->solved) {
|
|
return;
|
|
}
|
|
|
|
$response = (new Client())->post(config('recaptcha.google_url'), [
|
|
'form_params' => [
|
|
'secret' => config('recaptcha.secret_key'),
|
|
'response' => $captchaToken,
|
|
],
|
|
]);
|
|
|
|
if ($response->getStatusCode() !== 200) {
|
|
return;
|
|
}
|
|
|
|
$response = $response->getBody();
|
|
|
|
if ($response === null) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$data = json_decode($response, true, 512, JSON_THROW_ON_ERROR | JSON_THROW_ON_ERROR);
|
|
} catch (Throwable $throwable) {
|
|
return;
|
|
}
|
|
|
|
if (isset($data['success']) && $data['success'] === true) {
|
|
$this->solved = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Tests if we need to show captcha to user
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function needsCaptcha(): bool
|
|
{
|
|
if (!$this->captchaEnabled()) {
|
|
return false;
|
|
}
|
|
|
|
if ($this->solved) {
|
|
return false;
|
|
}
|
|
|
|
$cacheKey = $this->getCaptchaCacheKey();
|
|
|
|
$attempts = Cache::store('octane')->get($cacheKey, null);
|
|
|
|
if ($attempts === null) {
|
|
return false;
|
|
}
|
|
|
|
if ($attempts <= config('recaptcha.failed_attempts')) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|