Skip to content

Commit

Permalink
Change 2FA service to generate the secret on our own and use an exter…
Browse files Browse the repository at this point in the history
…nal QR service to display the image
  • Loading branch information
DaneEveritt committed Jun 22, 2019
1 parent 2db7928 commit 092e7e7
Show file tree
Hide file tree
Showing 5 changed files with 44 additions and 186 deletions.
4 changes: 3 additions & 1 deletion app/Http/Controllers/Base/SecurityController.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,10 @@ public function index(Request $request)
*/
public function generateTotp(Request $request)
{
$totpData = $this->twoFactorSetupService->handle($request->user());

return response()->json([
'qrImage' => $this->twoFactorSetupService->handle($request->user()),
'qrImage' => 'https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=' . $totpData,
]);
}

Expand Down
42 changes: 22 additions & 20 deletions app/Services/Users/TwoFactorSetupService.php
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/

namespace Pterodactyl\Services\Users;

use Exception;
use RuntimeException;
use Pterodactyl\Models\User;
use PragmaRX\Google2FAQRCode\Google2FA;
use Illuminate\Contracts\Encryption\Encrypter;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
use Illuminate\Contracts\Config\Repository as ConfigRepository;

class TwoFactorSetupService
{
const VALID_BASE32_CHARACTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';

/**
* @var \Illuminate\Contracts\Config\Repository
*/
Expand All @@ -27,11 +23,6 @@ class TwoFactorSetupService
*/
private $encrypter;

/**
* @var PragmaRX\Google2FAQRCode\Google2FA
*/
private $google2FA;

/**
* @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface
*/
Expand All @@ -42,24 +33,22 @@ class TwoFactorSetupService
*
* @param \Illuminate\Contracts\Config\Repository $config
* @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
* @param PragmaRX\Google2FAQRCode\Google2FA $google2FA
* @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository
*/
public function __construct(
ConfigRepository $config,
Encrypter $encrypter,
Google2FA $google2FA,
UserRepositoryInterface $repository
) {
$this->config = $config;
$this->encrypter = $encrypter;
$this->google2FA = $google2FA;
$this->repository = $repository;
}

/**
* Generate a 2FA token and store it in the database before returning the
* QR code image.
* QR code URL. This URL will need to be attached to a QR generating service in
* order to function.
*
* @param \Pterodactyl\Models\User $user
* @return string
Expand All @@ -69,13 +58,26 @@ public function __construct(
*/
public function handle(User $user): string
{
$secret = $this->google2FA->generateSecretKey($this->config->get('pterodactyl.auth.2fa.bytes'));
$image = $this->google2FA->getQRCodeInline($this->config->get('app.name'), $user->email, $secret);
$secret = '';
try {
for ($i = 0; $i < $this->config->get('pterodactyl.auth.2fa.bytes', 16); $i++) {
$secret .= substr(self::VALID_BASE32_CHARACTERS, random_int(0, 31), 1);
}
} catch (Exception $exception) {
throw new RuntimeException($exception->getMessage(), 0, $exception);
}

$this->repository->withoutFreshModel()->update($user->id, [
'totp_secret' => $this->encrypter->encrypt($secret),
]);

return $image;
$company = $this->config->get('app.name');

return sprintf(
'otpauth://totp/%1$s:%2$s?secret=%3$s&issuer=%1$s',
rawurlencode($company),
rawurlencode($user->email),
rawurlencode($secret)
);
}
}
1 change: 0 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
"matriphe/iso-639": "^1.2",
"nesbot/carbon": "^1.22",
"pragmarx/google2fa": "^5.0",
"pragmarx/google2fa-qrcode": "^1.0.3",
"predis/predis": "^1.1",
"prologue/alerts": "^0.4",
"ramsey/uuid": "^3.7",
Expand Down
151 changes: 1 addition & 150 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 18 additions & 14 deletions tests/Unit/Services/Users/TwoFactorSetupServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
use Mockery as m;
use Tests\TestCase;
use Pterodactyl\Models\User;
use PragmaRX\Google2FAQRCode\Google2FA;
use Illuminate\Contracts\Config\Repository;
use Illuminate\Contracts\Encryption\Encrypter;
use Pterodactyl\Services\Users\TwoFactorSetupService;
Expand All @@ -23,11 +22,6 @@ class TwoFactorSetupServiceTest extends TestCase
*/
private $encrypter;

/**
* @var PragmaRX\Google2FAQRCode\Google2FA|\Mockery\Mock
*/
private $google2FA;

/**
* @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface|\Mockery\Mock
*/
Expand All @@ -42,7 +36,6 @@ public function setUp()

$this->config = m::mock(Repository::class);
$this->encrypter = m::mock(Encrypter::class);
$this->google2FA = m::mock(Google2FA::class);
$this->repository = m::mock(UserRepositoryInterface::class);
}

Expand All @@ -53,16 +46,27 @@ public function testSecretAndImageAreReturned()
{
$model = factory(User::class)->make();

$this->config->shouldReceive('get')->with('pterodactyl.auth.2fa.bytes')->once()->andReturn(32);
$this->google2FA->shouldReceive('generateSecretKey')->with(32)->once()->andReturn('secretKey');
$this->config->shouldReceive('get')->with('app.name')->once()->andReturn('CompanyName');
$this->google2FA->shouldReceive('getQRCodeInline')->with('CompanyName', $model->email, 'secretKey')->once()->andReturn('http://url.com');
$this->encrypter->shouldReceive('encrypt')->with('secretKey')->once()->andReturn('encryptedSecret');
$this->config->shouldReceive('get')->with('pterodactyl.auth.2fa.bytes', 16)->andReturn(32);
$this->config->shouldReceive('get')->with('app.name')->andReturn('Company Name');
$this->encrypter->shouldReceive('encrypt')
->with(m::on(function ($value) {
return preg_match('/([A-Z234567]{32})/', $value) !== false;
}))
->once()
->andReturn('encryptedSecret');

$this->repository->shouldReceive('withoutFreshModel->update')->with($model->id, ['totp_secret' => 'encryptedSecret'])->once()->andReturnNull();

$response = $this->getService()->handle($model);
$this->assertNotEmpty($response);
$this->assertSame('http://url.com', $response);

$companyName = preg_quote(rawurlencode('Company Name'));
$email = preg_quote(rawurlencode($model->email));

$this->assertRegExp(
'/otpauth:\/\/totp\/' . $companyName . ':' . $email . '\?secret=([A-Z234567]{32})&issuer=' . $companyName . '/',
$response
);
}

/**
Expand All @@ -72,6 +76,6 @@ public function testSecretAndImageAreReturned()
*/
private function getService(): TwoFactorSetupService
{
return new TwoFactorSetupService($this->config, $this->encrypter, $this->google2FA, $this->repository);
return new TwoFactorSetupService($this->config, $this->encrypter, $this->repository);
}
}

0 comments on commit 092e7e7

Please sign in to comment.