Commit 2d4ca7d6 authored by Mark Harding's avatar Mark Harding

(feat): backend for captcha - front#646

1 merge request!455Backend for captcha - front#646
Pipeline #114883762 passed with stages
in 7 minutes and 56 seconds
File added
......@@ -45,9 +45,9 @@ class register implements Interfaces\Api, Interfaces\ApiIgnorePam
}
try {
$captcha = Core\Di\Di::_()->get('Security\ReCaptcha');
$captcha->setAnswer($_POST['captcha']);
if (isset($_POST['captcha']) && !$captcha->validate()) {
$captcha = Core\Di\Di::_()->get('Captcha\Manager');
if (isset($_POST['captcha']) && !$captcha->verifyFromClientJson($_POST['captcha'])) {
throw new \Exception('Captcha failed');
}
......
<?php
/**
* API for returning a captcha
*/
namespace Minds\Controllers\api\v2;
use Minds\Api\Factory;
use Minds\Common\Cookie;
use Minds\Core\Di\Di;
use Minds\Core\Config;
use Minds\Core\Session;
use Minds\Interfaces;
class captcha implements Interfaces\Api
{
public function get($pages)
{
$captchaManager = Di::_()->get('Captcha\Manager');
$captcha = $captchaManager->build();
return Factory::response(
$captcha->export()
);
}
public function post($pages)
{
return Factory::response([]);
}
public function put($pages)
{
return Factory::response([]);
}
public function delete($pages)
{
return Factory::response([]);
}
}
<?php
/**
* Captcha Model
*/
namespace Minds\Core\Captcha;
use Minds\Traits\MagicAttributes;
class Captcha
{
use MagicAttributes;
/** @var string */
private $jwtToken;
/** @var string */
private $clientText;
/** @var string */
private $base64Image;
/**
* Export the captcha
* @param array $extras
* @return array
*/
public function export(array $extras = []): array
{
return [
'jwt_token' => $this->jwtToken,
'base64_image' => $this->base64Image,
];
}
}
<?php
namespace Minds\Core\Captcha;
class ImageGenerator
{
/** @var int */
protected $width = 250;
/** @var int */
protected $height = 100;
/** @var string */
protected $text;
/**
* Set the width
* @param int $width
* @return self
*/
public function setWidth(int $width): self
{
$this->width = $width;
return $this;
}
/**
* Set the height
* @param int $height
* @return self
*/
public function setHeight(int $height): self
{
$this->height = $height;
return $this;
}
/**
* Set the text to output
* @param string $text
* @return self
*/
public function setText(string $text): self
{
$this->text = $text;
return $this;
}
/**
* Outputs the captcha image
* @return string
*/
public function build(): string
{
$image = imagecreatetruecolor($this->width, $this->height);
// Slight grey background
$backgroundColor = imagecolorallocate($image, 240, 240, 240);
// Builds the image background
imagefilledrectangle($image, 0, 0, $this->width, $this->height, $backgroundColor);
// Set the line thickness
imagesetthickness($image, 3);
// Dark grey lines
$lineColor = imagecolorallocate($image, 74, 74, 74);
$numberOfLines = rand(4, 10);
for ($i = 0; $i < $numberOfLines; $i++) {
imagesetthickness($image, rand(1, 3));
imageline($image, 0, rand() % $this->height, $this->width, rand() % $this->height, $lineColor);
}
for ($i = 0; $i< $this->width * 4; $i++) {
$pixelColor = imagecolorallocate($image, rand(0, 255), rand(0, 255), rand(0, 255));
imagesetpixel($image, rand() % $this->width, rand() % $this->height, $pixelColor);
}
$font = __MINDS_ROOT__ . '/Assets/fonts/Roboto-Medium.ttf';
$angle = rand(-6, 6);
$size = rand($this->height * 0.25, $this->height * 0.55);
$x = 10;
$y = ($this->height / 2) + ($size / 2);
$color = imagecolorallocate($image, 64, 64, 64);
// Write the text to the image
imagettftext($image, $size, $angle, $x, $y, $color, $font, $this->text);
ob_start();
imagepng($image);
$imagedata = ob_get_clean();
$base64 = base64_encode($imagedata);
imagedestroy($image);
return "data:image/png;base64,$base64";
}
}
<?php
namespace Minds\Core\Captcha;
use Minds\Common\Jwt;
use Minds\Core\Config;
use Minds\Core\Di\Di;
class Manager
{
/** @var ImageGenerator */
private $imageGenerator;
/** @var JWT */
private $jwt;
/** @var string */
private $secret;
public function __construct($imageGenerator = null, $jwt = null, $config = null)
{
$this->imageGenerator = $imageGenerator ?? new ImageGenerator;
$this->jwt = $jwt ?? new Jwt();
$config = $config ?? Di::_()->get('Config');
$this->secret = $config->get('captcha') ? $config->get('captcha')['jwt_secret'] : 'todo';
$this->jwt->setKey($this->secret);
}
/**
* Verify from client json
* @param string $json
* @return bool
*/
public function verifyFromClientJson(string $json): bool
{
$data = json_decode($json, true);
$captcha = new Captcha();
$captcha->setJwtToken($data['jwtToken'])
->setClientText($data['clientText']);
return $this->verify($captcha);
}
/**
* Verify if a captcha is valid
* @param Captcha $captcha
* @return bool
*/
public function verify(Captcha $captcha): bool
{
$jwtToken = $captcha->getJwtToken();
$decodedJwtToken = $this->jwt->decode($jwtToken);
$salt = $decodedJwtToken['salt'];
$hash = $decodedJwtToken['public_hash'];
// This is what the client has said the captcha image has
$clientText = $captcha->getClientText();
// Now convert this back to our hash
$clientHash = $this->buildCaptchaHash($clientText, $salt);
return $clientHash === $hash;
}
/**
* Output the captcha
* @param string $forcedText
* @return void
*/
public function build(string $forcedText = ''): Captcha
{
$text = $forcedText ?: $this->getRandomText(6);
$now = time();
$expires = $now + 300; // Captcha are good for 5 minutes
$salt = $this->jwt->randomString();
$jwtToken = $this->jwt
->setKey($this->secret)
->encode([
'public_hash' => $this->buildCaptchaHash($text, $salt),
'salt' => $salt,
], $expires, $now);
$image = $this->imageGenerator
->setText($text)
->build();
$captcha = new Captcha();
$captcha->setBase64Image($image)
->setJwtToken($jwtToken);
return $captcha;
}
/**
* Get the random text
* @param int $length
* @return sdtring
*/
protected function getRandomText(int $length): string
{
$chars = array_merge(
range(1, 9),
range('A', 'N'),
range('P', 'Z'), // We don't want O's or 0s
range('a', 'n'),
range('p', 'z')
);
shuffle($chars);
$text="";
for ($i = 0; $i < $length; $i++) {
$text .= $chars[array_rand($chars)];
}
return $text;
}
/**
* Return hash based on text and salt with a secret
* @param string $text
* @param string $salt
* @return string
*/
protected function buildCaptchaHash(string $text, string $salt): string
{
return hash('sha1', $text . $this->secret . $salt);
}
}
<?php
/**
* Captcha Module
*/
namespace Minds\Core\Captcha;
use Minds\Interfaces\ModuleInterface;
class Module implements ModuleInterface
{
/**
* OnInit.
*/
public function onInit()
{
$provider = new Provider();
$provider->register();
}
}
<?php
/**
* Minds Captcha Provider.
*/
namespace Minds\Core\Captcha;
use Minds\Core\Di\Provider as DiProvider;
class Provider extends DiProvider
{
public function register()
{
$this->di->bind('Captcha\Manager', function ($di) {
return new Manager();
}, ['useFactory' => true]);
}
}
......@@ -33,6 +33,7 @@ class Minds extends base
VideoChat\Module::class,
Feeds\Module::class,
Front\Module::class,
Captcha\Module::class,
];
/**
......
<?php
namespace Spec\Minds\Core\Captcha;
use Minds\Core\Captcha\ImageGenerator;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class ImageGeneratorSpec extends ObjectBehavior
{
public function it_is_initializable()
{
$this->shouldHaveType(ImageGenerator::class);
}
}
<?php
namespace Spec\Minds\Core\Captcha;
use Minds\Core\Captcha\Manager;
use Minds\Core\Captcha\Captcha as CaptchaModel;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class ManagerSpec extends ObjectBehavior
{
public function it_is_initializable()
{
$this->shouldHaveType(Manager::class);
}
public function it_should_build_a_captcha()
{
$captcha = $this->build();
$jwtToken = $captcha->getJwtToken();
$jwtToken->shouldBeString();
}
public function it_should_verify_a_captcha()
{
$captcha = $this->build('abfu21')->getWrappedObject();
$captcha->setClientText('abfu21');
$this->verify($captcha)
->shouldBe(true);
}
public function it_should_verify_a_captcha_with_fail()
{
$captcha = $this->build();
$captcha->setClientText('abfu21');
$this->verify($captcha)
->shouldBe(false);
}
}
......@@ -626,3 +626,8 @@ $CONFIG->set('unleash', [
'pollingIntervalSeconds' => 300,
'metricsIntervalSeconds' => 15
]);
$CONFIG->set('captcha', [
'jwt_secret' => '{{site-secret}}',
]);
Please register or to comment