From 6aab2592b477bc6c9b31ee3c7807abdc5a3d817f Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Mon, 23 Jan 2017 02:07:29 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=B2=D0=BA=D0=BB=D1=8E=D1=87=D0=B5=D0=BD=D0=B8=D1=8F?= =?UTF-8?q?/=D0=BE=D1=82=D0=BA=D0=BB=D1=8E=D1=87=D0=B5=D0=BD=D0=B8=D1=8F?= =?UTF-8?q?=20OTP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/controllers/TwoFactorAuthController.php | 14 ++- api/models/profile/TwoFactorAuthForm.php | 4 +- common/helpers/Error.php | 4 +- .../api/_pages/TwoFactorAuthRoute.php | 17 +++- .../functional/TwoFactorAuthDisableCest.php | 61 ++++++++++++ .../functional/TwoFactorAuthEnableCest.php | 61 ++++++++++++ .../models/profile/TwoFactorAuthFormTest.php | 98 +++++++++++++++++++ .../common/fixtures/data/accounts.php | 30 ++++++ 8 files changed, 281 insertions(+), 8 deletions(-) create mode 100644 tests/codeception/api/functional/TwoFactorAuthDisableCest.php create mode 100644 tests/codeception/api/functional/TwoFactorAuthEnableCest.php diff --git a/api/controllers/TwoFactorAuthController.php b/api/controllers/TwoFactorAuthController.php index b6a50ee..6b3b925 100644 --- a/api/controllers/TwoFactorAuthController.php +++ b/api/controllers/TwoFactorAuthController.php @@ -17,16 +17,22 @@ class TwoFactorAuthController extends Controller { 'class' => AccessControl::class, 'rules' => [ [ + 'allow' => true, 'class' => ActiveUserRule::class, - 'actions' => [ - 'credentials', - ], ], ], ], ]); } + public function verbs() { + return [ + 'credentials' => ['GET'], + 'activate' => ['POST'], + 'disable' => ['DELETE'], + ]; + } + public function actionCredentials() { $account = Yii::$app->user->identity; $model = new TwoFactorAuthForm($account); @@ -37,6 +43,7 @@ class TwoFactorAuthController extends Controller { public function actionActivate() { $account = Yii::$app->user->identity; $model = new TwoFactorAuthForm($account, ['scenario' => TwoFactorAuthForm::SCENARIO_ACTIVATE]); + $model->load(Yii::$app->request->post()); if (!$model->activate()) { return [ 'success' => false, @@ -52,6 +59,7 @@ class TwoFactorAuthController extends Controller { public function actionDisable() { $account = Yii::$app->user->identity; $model = new TwoFactorAuthForm($account, ['scenario' => TwoFactorAuthForm::SCENARIO_DISABLE]); + $model->load(Yii::$app->request->getBodyParams()); if (!$model->disable()) { return [ 'success' => false, diff --git a/api/models/profile/TwoFactorAuthForm.php b/api/models/profile/TwoFactorAuthForm.php index 398cf33..907e686 100644 --- a/api/models/profile/TwoFactorAuthForm.php +++ b/api/models/profile/TwoFactorAuthForm.php @@ -61,7 +61,7 @@ class TwoFactorAuthForm extends ApiForm { } public function activate(): bool { - if (!$this->validate()) { + if ($this->scenario !== self::SCENARIO_ACTIVATE || !$this->validate()) { return false; } @@ -75,7 +75,7 @@ class TwoFactorAuthForm extends ApiForm { } public function disable(): bool { - if (!$this->validate()) { + if ($this->scenario !== self::SCENARIO_DISABLE || !$this->validate()) { return false; } diff --git a/common/helpers/Error.php b/common/helpers/Error.php index 8425256..3baa1b8 100644 --- a/common/helpers/Error.php +++ b/common/helpers/Error.php @@ -54,8 +54,8 @@ final class Error { const SUBJECT_REQUIRED = 'error.subject_required'; const MESSAGE_REQUIRED = 'error.message_required'; - const OTP_TOKEN_REQUIRED = 'error.otp_token_required'; - const OTP_TOKEN_INCORRECT = 'error.otp_token_incorrect'; + const OTP_TOKEN_REQUIRED = 'error.token_required'; + const OTP_TOKEN_INCORRECT = 'error.token_incorrect'; const OTP_ALREADY_ENABLED = 'error.otp_already_enabled'; const OTP_NOT_ENABLED = 'error.otp_not_enabled'; diff --git a/tests/codeception/api/_pages/TwoFactorAuthRoute.php b/tests/codeception/api/_pages/TwoFactorAuthRoute.php index 32ffeaa..ed677bb 100644 --- a/tests/codeception/api/_pages/TwoFactorAuthRoute.php +++ b/tests/codeception/api/_pages/TwoFactorAuthRoute.php @@ -8,9 +8,24 @@ use yii\codeception\BasePage; */ class TwoFactorAuthRoute extends BasePage { + public $route = '/two-factor-auth'; + public function credentials() { - $this->route = '/two-factor-auth'; $this->actor->sendGET($this->getUrl()); } + public function enable($token = null, $password = null) { + $this->actor->sendPOST($this->getUrl(), [ + 'token' => $token, + 'password' => $password, + ]); + } + + public function disable($token = null, $password = null) { + $this->actor->sendDELETE($this->getUrl(), [ + 'token' => $token, + 'password' => $password, + ]); + } + } diff --git a/tests/codeception/api/functional/TwoFactorAuthDisableCest.php b/tests/codeception/api/functional/TwoFactorAuthDisableCest.php new file mode 100644 index 0000000..974857e --- /dev/null +++ b/tests/codeception/api/functional/TwoFactorAuthDisableCest.php @@ -0,0 +1,61 @@ +route = new TwoFactorAuthRoute($I); + } + + public function testFails(FunctionalTester $I) { + $I->loggedInAsActiveAccount('AccountWithEnabledOtp', 'password_0'); + + $this->route->disable(); + $I->canSeeResponseContainsJson([ + 'success' => false, + 'errors' => [ + 'token' => 'error.token_required', + 'password' => 'error.password_required', + ], + ]); + + $this->route->disable('123456', 'invalid_password'); + $I->canSeeResponseContainsJson([ + 'success' => false, + 'errors' => [ + 'token' => 'error.token_incorrect', + 'password' => 'error.password_incorrect', + ], + ]); + + $I->loggedInAsActiveAccount('AccountWithOtpSecret', 'password_0'); + $this->route->disable('123456', 'invalid_password'); + $I->canSeeResponseContainsJson([ + 'success' => false, + 'errors' => [ + 'account' => 'error.otp_not_enabled', + ], + ]); + } + + public function testSuccessEnable(FunctionalTester $I) { + $I->loggedInAsActiveAccount('AccountWithEnabledOtp', 'password_0'); + $totp = new TOTP(null, 'secret-secret-secret'); + $this->route->disable($totp->now(), 'password_0'); + $I->canSeeResponseCodeIs(200); + $I->canSeeResponseIsJson(); + $I->canSeeResponseContainsJson([ + 'success' => true, + ]); + } + +} diff --git a/tests/codeception/api/functional/TwoFactorAuthEnableCest.php b/tests/codeception/api/functional/TwoFactorAuthEnableCest.php new file mode 100644 index 0000000..efec01f --- /dev/null +++ b/tests/codeception/api/functional/TwoFactorAuthEnableCest.php @@ -0,0 +1,61 @@ +route = new TwoFactorAuthRoute($I); + } + + public function testFails(FunctionalTester $I) { + $I->loggedInAsActiveAccount('AccountWithOtpSecret', 'password_0'); + + $this->route->enable(); + $I->canSeeResponseContainsJson([ + 'success' => false, + 'errors' => [ + 'token' => 'error.token_required', + 'password' => 'error.password_required', + ], + ]); + + $this->route->enable('123456', 'invalid_password'); + $I->canSeeResponseContainsJson([ + 'success' => false, + 'errors' => [ + 'token' => 'error.token_incorrect', + 'password' => 'error.password_incorrect', + ], + ]); + + $I->loggedInAsActiveAccount('AccountWithEnabledOtp', 'password_0'); + $this->route->enable('123456', 'invalid_password'); + $I->canSeeResponseContainsJson([ + 'success' => false, + 'errors' => [ + 'account' => 'error.otp_already_enabled', + ], + ]); + } + + public function testSuccessEnable(FunctionalTester $I) { + $I->loggedInAsActiveAccount('AccountWithOtpSecret', 'password_0'); + $totp = new TOTP(null, 'some otp secret value'); + $this->route->enable($totp->now(), 'password_0'); + $I->canSeeResponseCodeIs(200); + $I->canSeeResponseIsJson(); + $I->canSeeResponseContainsJson([ + 'success' => true, + ]); + } + +} diff --git a/tests/codeception/api/unit/models/profile/TwoFactorAuthFormTest.php b/tests/codeception/api/unit/models/profile/TwoFactorAuthFormTest.php index 37587da..a160805 100644 --- a/tests/codeception/api/unit/models/profile/TwoFactorAuthFormTest.php +++ b/tests/codeception/api/unit/models/profile/TwoFactorAuthFormTest.php @@ -2,7 +2,9 @@ namespace tests\codeception\api\unit\models\profile; use api\models\profile\TwoFactorAuthForm; +use common\helpers\Error as E; use common\models\Account; +use OTPHP\TOTP; use tests\codeception\api\unit\TestCase; class TwoFactorAuthFormTest extends TestCase { @@ -64,4 +66,100 @@ class TwoFactorAuthFormTest extends TestCase { $this->assertEquals('some valid totp secret value', $result['secret']); } + public function testActivate() { + /** @var Account|\PHPUnit_Framework_MockObject_MockObject $account */ + $account = $this->getMockBuilder(Account::class) + ->setMethods(['save']) + ->getMock(); + + $account->expects($this->once()) + ->method('save') + ->willReturn(true); + + $account->is_otp_enabled = false; + $account->otp_secret = 'mock secret'; + + /** @var TwoFactorAuthForm|\PHPUnit_Framework_MockObject_MockObject $model */ + $model = $this->getMockBuilder(TwoFactorAuthForm::class) + ->setMethods(['validate']) + ->setConstructorArgs([$account, ['scenario' => TwoFactorAuthForm::SCENARIO_ACTIVATE]]) + ->getMock(); + + $model->expects($this->once()) + ->method('validate') + ->willReturn(true); + + $this->assertTrue($model->activate()); + $this->assertTrue($account->is_otp_enabled); + } + + public function testDisable() { + /** @var Account|\PHPUnit_Framework_MockObject_MockObject $account */ + $account = $this->getMockBuilder(Account::class) + ->setMethods(['save']) + ->getMock(); + + $account->expects($this->once()) + ->method('save') + ->willReturn(true); + + $account->is_otp_enabled = true; + $account->otp_secret = 'mock secret'; + + /** @var TwoFactorAuthForm|\PHPUnit_Framework_MockObject_MockObject $model */ + $model = $this->getMockBuilder(TwoFactorAuthForm::class) + ->setMethods(['validate']) + ->setConstructorArgs([$account, ['scenario' => TwoFactorAuthForm::SCENARIO_DISABLE]]) + ->getMock(); + + $model->expects($this->once()) + ->method('validate') + ->willReturn(true); + + $this->assertTrue($model->disable()); + $this->assertNull($account->otp_secret); + $this->assertFalse($account->is_otp_enabled); + } + + public function testValidateOtpDisabled() { + $account = new Account(); + $account->is_otp_enabled = true; + $model = new TwoFactorAuthForm($account); + $model->validateOtpDisabled('account'); + $this->assertEquals([E::OTP_ALREADY_ENABLED], $model->getErrors('account')); + + $account = new Account(); + $account->is_otp_enabled = false; + $model = new TwoFactorAuthForm($account); + $model->validateOtpDisabled('account'); + $this->assertEmpty($model->getErrors('account')); + } + + public function testValidateOtpEnabled() { + $account = new Account(); + $account->is_otp_enabled = false; + $model = new TwoFactorAuthForm($account); + $model->validateOtpEnabled('account'); + $this->assertEquals([E::OTP_NOT_ENABLED], $model->getErrors('account')); + + $account = new Account(); + $account->is_otp_enabled = true; + $model = new TwoFactorAuthForm($account); + $model->validateOtpEnabled('account'); + $this->assertEmpty($model->getErrors('account')); + } + + public function testGetTotp() { + $account = new Account(); + $account->otp_secret = 'mock secret'; + $account->email = 'check@this.email'; + + $model = new TwoFactorAuthForm($account); + $totp = $model->getTotp(); + $this->assertInstanceOf(TOTP::class, $totp); + $this->assertEquals('check@this.email', $totp->getLabel()); + $this->assertEquals('mock secret', $totp->getSecret()); + $this->assertEquals('Ely.by', $totp->getIssuer()); + } + } diff --git a/tests/codeception/common/fixtures/data/accounts.php b/tests/codeception/common/fixtures/data/accounts.php index 33db104..c8c79d3 100644 --- a/tests/codeception/common/fixtures/data/accounts.php +++ b/tests/codeception/common/fixtures/data/accounts.php @@ -146,4 +146,34 @@ return [ 'created_at' => 1474404139, 'updated_at' => 1474404149, ], + 'account-with-otp-secret' => [ + 'id' => 12, + 'uuid' => '9e9dcd11-2322-46dc-a992-e822a422726e', + 'username' => 'AccountWithOtpSecret', + 'email' => 'sava-galkin@mail.ru', + 'password_hash' => '$2y$13$2rYkap5T6jG8z/mMK8a3Ou6aZxJcmAaTha6FEuujvHEmybSHRzW5e', # password_0 + 'password_hash_strategy' => \common\models\Account::PASS_HASH_STRATEGY_YII2, + 'lang' => 'ru', + 'status' => \common\models\Account::STATUS_ACTIVE, + 'rules_agreement_version' => \common\LATEST_RULES_VERSION, + 'otp_secret' => 'some otp secret value', + 'is_otp_enabled' => false, + 'created_at' => 1485124615, + 'updated_at' => 1485124615, + ], + 'account-with-enabled-otp' => [ + 'id' => 13, + 'uuid' => '15d0afa7-a2bb-44d3-9f31-964cbccc6043', + 'username' => 'AccountWithEnabledOtp', + 'email' => 'otp@gmail.com', + 'password_hash' => '$2y$13$2rYkap5T6jG8z/mMK8a3Ou6aZxJcmAaTha6FEuujvHEmybSHRzW5e', # password_0 + 'password_hash_strategy' => \common\models\Account::PASS_HASH_STRATEGY_YII2, + 'lang' => 'ru', + 'status' => \common\models\Account::STATUS_ACTIVE, + 'rules_agreement_version' => \common\LATEST_RULES_VERSION, + 'otp_secret' => 'secret-secret-secret', + 'is_otp_enabled' => true, + 'created_at' => 1485124685, + 'updated_at' => 1485124685, + ], ];