Реализован метод для запроса информации для активации двухфакторной аутентификации

Добавлен валидатор для TOTP кодов
This commit is contained in:
ErickSkrauch
2017-01-21 01:54:30 +03:00
parent bb1fd1a960
commit 3b9ef7ea70
10 changed files with 351 additions and 0 deletions

View File

@ -0,0 +1,37 @@
<?php
namespace api\controllers;
use api\filters\ActiveUserRule;
use api\models\profile\TwoFactorAuthForm;
use Yii;
use yii\filters\AccessControl;
use yii\helpers\ArrayHelper;
class TwoFactorAuthController extends Controller {
public $defaultAction = 'credentials';
public function behaviors() {
return ArrayHelper::merge(parent::behaviors(), [
'access' => [
'class' => AccessControl::class,
'rules' => [
[
'class' => ActiveUserRule::class,
'actions' => [
'credentials',
],
],
],
],
]);
}
public function actionCredentials() {
$account = Yii::$app->user->identity;
$model = new TwoFactorAuthForm($account);
return $model->getCredentials();
}
}

View File

@ -0,0 +1,95 @@
<?php
namespace api\models\profile;
use api\models\base\ApiForm;
use api\validators\TotpValidator;
use api\validators\PasswordRequiredValidator;
use BaconQrCode\Common\ErrorCorrectionLevel;
use BaconQrCode\Encoder\Encoder;
use BaconQrCode\Renderer\Color\Rgb;
use BaconQrCode\Renderer\Image\Svg;
use BaconQrCode\Writer;
use Base32\Base32;
use common\components\Qr\ElyDecorator;
use common\helpers\Error as E;
use common\models\Account;
use OTPHP\TOTP;
use yii\base\ErrorException;
class TwoFactorAuthForm extends ApiForm {
const SCENARIO_ENABLE = 'enable';
const SCENARIO_DISABLE = 'disable';
public $token;
public $password;
/**
* @var Account
*/
private $account;
public function __construct(Account $account, array $config = []) {
$this->account = $account;
parent::__construct($config);
}
public function rules() {
$on = [self::SCENARIO_ENABLE, self::SCENARIO_DISABLE];
return [
['token', 'required', 'message' => E::OTP_TOKEN_REQUIRED, 'on' => $on],
['token', TotpValidator::class, 'account' => $this->account, 'window' => 30, 'on' => $on],
['password', PasswordRequiredValidator::class, 'account' => $this->account, 'on' => $on],
];
}
public function getCredentials(): array {
if (empty($this->account->otp_secret)) {
$this->setOtpSecret();
}
$provisioningUri = $this->getTotp()->getProvisioningUri();
return [
'qr' => base64_encode($this->drawQrCode($provisioningUri)),
'uri' => $provisioningUri,
'secret' => $this->account->otp_secret,
];
}
public function getAccount(): Account {
return $this->account;
}
/**
* @return TOTP
*/
public function getTotp(): TOTP {
$totp = new TOTP($this->account->email, $this->account->otp_secret);
$totp->setIssuer('Ely.by');
return $totp;
}
public function drawQrCode(string $content): string {
$renderer = new Svg();
$renderer->setHeight(256);
$renderer->setWidth(256);
$renderer->setForegroundColor(new Rgb(32, 126, 92));
$renderer->setMargin(0);
$renderer->addDecorator(new ElyDecorator());
$writer = new Writer($renderer);
return $writer->writeString($content, Encoder::DEFAULT_BYTE_MODE_ECODING, ErrorCorrectionLevel::H);
}
protected function setOtpSecret(): void {
$this->account->otp_secret = trim(Base32::encode(random_bytes(32)), '=');
if (!$this->account->save()) {
throw new ErrorException('Cannot set account otp_secret');
}
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace api\validators;
use common\helpers\Error as E;
use common\models\Account;
use OTPHP\TOTP;
use Yii;
use yii\base\InvalidConfigException;
use yii\validators\Validator;
class TotpValidator extends Validator {
/**
* @var Account
*/
public $account;
/**
* @var int|null Задаёт окно, в промежуток которого будет проверяться код.
* Позволяет избежать ситуации, когда пользователь ввёл код в последнюю секунду
* его существования и пока шёл запрос, тот протух.
*/
public $window;
public $skipOnEmpty = false;
public function init() {
parent::init();
if ($this->account === null) {
$this->account = Yii::$app->user->identity;
}
if (!$this->account instanceof Account) {
throw new InvalidConfigException('account should be instance of ' . Account::class);
}
if (empty($this->account->otp_secret)) {
throw new InvalidConfigException('account should have not empty otp_secret');
}
}
protected function validateValue($value) {
$totp = new TOTP(null, $this->account->otp_secret);
if (!$totp->verify((string)$value, null, $this->window)) {
return [E::OTP_TOKEN_INCORRECT, []];
}
return null;
}
}