Внедрена валидация OTP в процесс восстановления пароля

This commit is contained in:
ErickSkrauch 2017-01-23 23:50:13 +03:00
parent e82b8aa8cf
commit 4695b6e724
5 changed files with 119 additions and 24 deletions

View File

@ -2,6 +2,7 @@
namespace api\models\authentication; namespace api\models\authentication;
use api\models\base\ApiForm; use api\models\base\ApiForm;
use api\validators\TotpValidator;
use common\helpers\Error as E; use common\helpers\Error as E;
use api\traits\AccountFinder; use api\traits\AccountFinder;
use common\components\UserFriendlyRandomKey; use common\components\UserFriendlyRandomKey;
@ -16,11 +17,16 @@ class ForgotPasswordForm extends ApiForm {
use AccountFinder; use AccountFinder;
public $login; public $login;
public $token;
public function rules() { public function rules() {
return [ return [
['login', 'required', 'message' => E::LOGIN_REQUIRED], ['login', 'required', 'message' => E::LOGIN_REQUIRED],
['login', 'validateLogin'], ['login', 'validateLogin'],
['token', 'required', 'when' => function(self $model) {
return !$this->hasErrors() && $model->getAccount()->is_otp_enabled;
}, 'message' => E::OTP_TOKEN_REQUIRED],
['token', 'validateTotpToken'],
['login', 'validateActivity'], ['login', 'validateActivity'],
['login', 'validateFrequency'], ['login', 'validateFrequency'],
]; ];
@ -34,6 +40,20 @@ class ForgotPasswordForm extends ApiForm {
} }
} }
public function validateTotpToken($attribute) {
if ($this->hasErrors()) {
return;
}
$account = $this->getAccount();
if (!$account->is_otp_enabled) {
return;
}
$validator = new TotpValidator(['account' => $account]);
$validator->validateAttribute($this, $attribute);
}
public function validateActivity($attribute) { public function validateActivity($attribute) {
if (!$this->hasErrors()) { if (!$this->hasErrors()) {
$account = $this->getAccount(); $account = $this->getAccount();

View File

@ -69,9 +69,7 @@ class LoginForm extends ApiForm {
} }
$validator = new TotpValidator(['account' => $account]); $validator = new TotpValidator(['account' => $account]);
if (!$validator->validate($this->token, $error)) { $validator->validateAttribute($this, $attribute);
$this->addError($attribute, $error);
}
} }
public function validateActivity($attribute) { public function validateActivity($attribute) {

View File

@ -35,10 +35,11 @@ class AuthenticationRoute extends BasePage {
$this->actor->sendPOST($this->getUrl()); $this->actor->sendPOST($this->getUrl());
} }
public function forgotPassword($login = '') { public function forgotPassword($login = null, $token = null) {
$this->route = ['authentication/forgot-password']; $this->route = ['authentication/forgot-password'];
$this->actor->sendPOST($this->getUrl(), [ $this->actor->sendPOST($this->getUrl(), [
'login' => $login, 'login' => $login,
'token' => $token,
]); ]);
} }

View File

@ -1,41 +1,87 @@
<?php <?php
namespace codeception\api\functional; namespace codeception\api\functional;
use OTPHP\TOTP;
use tests\codeception\api\_pages\AuthenticationRoute; use tests\codeception\api\_pages\AuthenticationRoute;
use tests\codeception\api\FunctionalTester; use tests\codeception\api\FunctionalTester;
class ForgotPasswordCest { class ForgotPasswordCest {
public function testForgotPasswordByEmail(FunctionalTester $I) { /**
$route = new AuthenticationRoute($I); * @var AuthenticationRoute
*/
private $route;
$I->wantTo('create new password recover request by passing email'); public function _before(FunctionalTester $I) {
$route->forgotPassword('admin@ely.by'); $this->route = new AuthenticationRoute($I);
}
public function testWrongInput(FunctionalTester $I) {
$I->wantTo('see reaction on invalid input');
$this->route->forgotPassword();
$I->canSeeResponseContainsJson([ $I->canSeeResponseContainsJson([
'success' => true, 'success' => false,
'errors' => [
'login' => 'error.login_required',
],
]); ]);
$I->canSeeResponseJsonMatchesJsonPath('$.data.canRepeatIn');
$I->canSeeResponseJsonMatchesJsonPath('$.data.repeatFrequency'); $this->route->forgotPassword('becauseimbatman!');
$I->canSeeResponseContainsJson([
'success' => false,
'errors' => [
'login' => 'error.login_not_exist',
],
]);
$this->route->forgotPassword('AccountWithEnabledOtp');
$I->canSeeResponseContainsJson([
'success' => false,
'errors' => [
'token' => 'error.token_required',
],
]);
$this->route->forgotPassword('AccountWithEnabledOtp');
$I->canSeeResponseContainsJson([
'success' => false,
'errors' => [
'token' => 'error.token_required',
],
]);
$this->route->forgotPassword('AccountWithEnabledOtp', '123456');
$I->canSeeResponseContainsJson([
'success' => false,
'errors' => [
'token' => 'error.token_incorrect',
],
]);
}
public function testForgotPasswordByEmail(FunctionalTester $I) {
$I->wantTo('create new password recover request by passing email');
$this->route->forgotPassword('admin@ely.by');
$this->assertSuccessResponse($I, false);
} }
public function testForgotPasswordByUsername(FunctionalTester $I) { public function testForgotPasswordByUsername(FunctionalTester $I) {
$route = new AuthenticationRoute($I);
$I->wantTo('create new password recover request by passing username'); $I->wantTo('create new password recover request by passing username');
$route->forgotPassword('Admin'); $this->route->forgotPassword('Admin');
$I->canSeeResponseContainsJson([ $this->assertSuccessResponse($I, true);
'success' => true, }
]);
$I->canSeeResponseJsonMatchesJsonPath('$.data.canRepeatIn'); public function testForgotPasswordByAccountWithOtp(FunctionalTester $I) {
$I->canSeeResponseJsonMatchesJsonPath('$.data.repeatFrequency'); $I->wantTo('create new password recover request by passing username and otp token');
$I->canSeeResponseJsonMatchesJsonPath('$.data.emailMask'); $totp = new TOTP(null, 'secret-secret-secret');
$this->route->forgotPassword('AccountWithEnabledOtp', $totp->now());
$this->assertSuccessResponse($I, true);
} }
public function testDataForFrequencyError(FunctionalTester $I) { public function testDataForFrequencyError(FunctionalTester $I) {
$route = new AuthenticationRoute($I);
$I->wantTo('get info about time to repeat recover password request'); $I->wantTo('get info about time to repeat recover password request');
$route->forgotPassword('Notch'); $this->route->forgotPassword('Notch');
$I->canSeeResponseContainsJson([ $I->canSeeResponseContainsJson([
'success' => false, 'success' => false,
'errors' => [ 'errors' => [
@ -46,4 +92,18 @@ class ForgotPasswordCest {
$I->canSeeResponseJsonMatchesJsonPath('$.data.repeatFrequency'); $I->canSeeResponseJsonMatchesJsonPath('$.data.repeatFrequency');
} }
/**
* @param FunctionalTester $I
*/
private function assertSuccessResponse(FunctionalTester $I, bool $expectEmailMask = false): void {
$I->canSeeResponseContainsJson([
'success' => true,
]);
$I->canSeeResponseJsonMatchesJsonPath('$.data.canRepeatIn');
$I->canSeeResponseJsonMatchesJsonPath('$.data.repeatFrequency');
if ($expectEmailMask) {
$I->canSeeResponseJsonMatchesJsonPath('$.data.emailMask');
}
}
} }

View File

@ -4,6 +4,7 @@ namespace codeception\api\unit\models\authentication;
use api\models\authentication\ForgotPasswordForm; use api\models\authentication\ForgotPasswordForm;
use Codeception\Specify; use Codeception\Specify;
use common\models\EmailActivation; use common\models\EmailActivation;
use OTPHP\TOTP;
use tests\codeception\api\unit\TestCase; use tests\codeception\api\unit\TestCase;
use tests\codeception\common\fixtures\AccountFixture; use tests\codeception\common\fixtures\AccountFixture;
use tests\codeception\common\fixtures\EmailActivationFixture; use tests\codeception\common\fixtures\EmailActivationFixture;
@ -18,7 +19,7 @@ class ForgotPasswordFormTest extends TestCase {
]; ];
} }
public function testValidateAccount() { public function testValidateLogin() {
$this->specify('error.login_not_exist if login is invalid', function() { $this->specify('error.login_not_exist if login is invalid', function() {
$model = new ForgotPasswordForm(['login' => 'unexist']); $model = new ForgotPasswordForm(['login' => 'unexist']);
$model->validateLogin('login'); $model->validateLogin('login');
@ -32,6 +33,21 @@ class ForgotPasswordFormTest extends TestCase {
}); });
} }
public function testValidateTotpToken() {
$model = new ForgotPasswordForm();
$model->login = 'AccountWithEnabledOtp';
$model->token = '123456';
$model->validateTotpToken('token');
$this->assertEquals(['error.token_incorrect'], $model->getErrors('token'));
$totp = new TOTP(null, 'secret-secret-secret');
$model = new ForgotPasswordForm();
$model->login = 'AccountWithEnabledOtp';
$model->token = $totp->now();
$model->validateTotpToken('token');
$this->assertEmpty($model->getErrors('token'));
}
public function testValidateActivity() { public function testValidateActivity() {
$this->specify('error.account_not_activated if account is not confirmed', function() { $this->specify('error.account_not_activated if account is not confirmed', function() {
$model = new ForgotPasswordForm([ $model = new ForgotPasswordForm([