app/Helpers/Recaptcha.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;
}
}