Replace captcha package with own implementation

This commit is contained in:
2026-04-20 21:36:02 +02:00
parent 361b511c3e
commit 8ae9eaaadb
17 changed files with 212 additions and 156 deletions

View File

@@ -5,7 +5,7 @@ namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Downloads;
use App\Models\Episode;
use GrantHolle\Altcha\Rules\ValidAltcha;
use App\Rules\ValidCaptcha;
use Illuminate\Http\Request;
class DownloadApiController extends Controller
@@ -17,7 +17,7 @@ class DownloadApiController extends Controller
{
$validated = $request->validate([
'episode_id' => ['required'],
'captcha' => ['required', new ValidAltcha],
'captcha' => ['required', new ValidCaptcha],
]);
$episode = Episode::where('id', $request->input('episode_id'))

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Http\Controllers\Auth;
use AltchaOrg\Altcha\Algorithm\Pbkdf2;
use AltchaOrg\Altcha\Altcha;
use AltchaOrg\Altcha\CreateChallengeOptions;
use App\Http\Controllers\Controller;
class CaptchaController extends Controller
{
public function create(): array
{
$pbkdf2 = new Pbkdf2;
$altcha = new Altcha(
hmacSignatureSecret: config('captcha.hmac_key'),
);
// Create challenge
$challenge = $altcha->createChallenge(new CreateChallengeOptions(
algorithm: $pbkdf2,
cost: 5000,
counter: random_int(5000, 10000),
expiresAt: time() + 600,
));
return get_object_vars($challenge);
}
}

View File

@@ -4,7 +4,7 @@ namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use GrantHolle\Altcha\Rules\ValidAltcha;
use App\Rules\ValidCaptcha;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
@@ -25,7 +25,7 @@ class RegisteredUserController extends Controller
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
'password' => ['required', 'confirmed', Rules\Password::defaults()],
'altcha' => ['required', new ValidAltcha],
'altcha' => ['required', new ValidCaptcha],
]);
$user = User::create([

View File

@@ -3,7 +3,7 @@
namespace App\Http\Controllers;
use App\Models\Contact;
use GrantHolle\Altcha\Rules\ValidAltcha;
use App\Rules\ValidCaptcha;
use Illuminate\Http\Request;
class ContactController extends Controller
@@ -26,7 +26,7 @@ class ContactController extends Controller
'email' => 'required|max:50',
'message' => 'required|max:1000',
'subject' => 'required|max:50',
'altcha' => ['required', new ValidAltcha],
'altcha' => ['required', new ValidCaptcha],
]);
$contact = new Contact;

View File

@@ -2,7 +2,7 @@
namespace App\Http\Requests\Auth;
use GrantHolle\Altcha\Rules\ValidAltcha;
use App\Rules\ValidCaptcha;
use Illuminate\Auth\Events\Lockout;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
@@ -30,7 +30,7 @@ class LoginRequest extends FormRequest
return [
'email' => ['required', 'string', 'email'],
'password' => ['required', 'string'],
'altcha' => ['required', new ValidAltcha],
'altcha' => ['required', new ValidCaptcha],
];
}

129
app/Rules/ValidCaptcha.php Normal file
View File

@@ -0,0 +1,129 @@
<?php
namespace App\Rules;
use AltchaOrg\Altcha\Algorithm\Pbkdf2;
use AltchaOrg\Altcha\Altcha;
use AltchaOrg\Altcha\Challenge;
use AltchaOrg\Altcha\ChallengeParameters;
use AltchaOrg\Altcha\Payload;
use AltchaOrg\Altcha\Solution;
use AltchaOrg\Altcha\VerifySolutionOptions;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
/**
* Validation rule to verify captcha solution.
*/
class ValidCaptcha implements ValidationRule
{
/**
* Altcha instance.
*/
protected Altcha $altcha;
/**
* Pbkdf2 algorithm instance.
*/
protected Pbkdf2 $pbkdf2;
/**
* Class constructor.
*/
public function __construct()
{
$this->pbkdf2 = new Pbkdf2;
$this->altcha = new Altcha(
hmacSignatureSecret: config('captcha.hmac_key'),
);
}
/**
* Parse payload and return the decoded data as an array.
*/
private function parsePayload(string $value): ?array
{
$decoded = base64_decode($value, true);
if ($decoded === false) {
return null;
}
$payload = json_decode($decoded, true);
if (! is_array($payload)) {
return null;
}
return $payload;
}
/**
* Verify if payload has required fields.
*/
private function verifyFields(array $payload): bool
{
if (! isset($payload['challenge'], $payload['solution'])) {
return false;
}
if (! is_array($payload['challenge']) || ! is_array($payload['solution'])) {
return false;
}
return true;
}
/**
* Create a Challenge object from challenge data.
*/
private function createChallenge(array $challengeData): Challenge
{
return new Challenge(
ChallengeParameters::fromArray($challengeData['parameters'] ?? []),
$challengeData['signature'] ?? null,
);
}
/**
* Create a Solution object from solution data.
*/
private function createSolution(array $solutionData): Solution
{
return new Solution(
counter: (int) ($solutionData['counter'] ?? 0),
derivedKey: (string) ($solutionData['derivedKey'] ?? ''),
);
}
/**
* Run the validation rule.
*
* @param \Closure(string, ?string=): \Illuminate\Translation\PotentiallyTranslatedString $fail
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$payload = $this->parsePayload($value);
if (! $payload) {
$fail('Invalid captcha.');
return;
}
if (! $this->verifyFields($payload)) {
$fail('Invalid captcha.');
return;
}
$challenge = $this->createChallenge($payload['challenge']);
$solution = $this->createSolution($payload['solution']);
$result = $this->altcha->verifySolution(new VerifySolutionOptions(
algorithm: $this->pbkdf2,
payload: new Payload($challenge, $solution),
));
if (! $result->verified) {
$fail('Invalid captcha.');
}
}
}