mirror of
https://github.com/elyby/accounts.git
synced 2024-11-30 10:42:16 +05:30
В форму входа внедрена проверка на наличие включённой OTP авторизации
This commit is contained in:
parent
6aab2592b4
commit
a2e1e9a805
@ -3,6 +3,7 @@ namespace api\models\authentication;
|
|||||||
|
|
||||||
use api\models\AccountIdentity;
|
use api\models\AccountIdentity;
|
||||||
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\models\Account;
|
use common\models\Account;
|
||||||
@ -16,6 +17,7 @@ class LoginForm extends ApiForm {
|
|||||||
|
|
||||||
public $login;
|
public $login;
|
||||||
public $password;
|
public $password;
|
||||||
|
public $token;
|
||||||
public $rememberMe = false;
|
public $rememberMe = false;
|
||||||
|
|
||||||
public function rules() {
|
public function rules() {
|
||||||
@ -28,6 +30,11 @@ class LoginForm extends ApiForm {
|
|||||||
}, 'message' => E::PASSWORD_REQUIRED],
|
}, 'message' => E::PASSWORD_REQUIRED],
|
||||||
['password', 'validatePassword'],
|
['password', 'validatePassword'],
|
||||||
|
|
||||||
|
['token', 'required', 'when' => function(self $model) {
|
||||||
|
return !$model->hasErrors() && $model->getAccount()->is_otp_enabled;
|
||||||
|
}, 'message' => E::OTP_TOKEN_REQUIRED],
|
||||||
|
['token', 'validateTotpToken'],
|
||||||
|
|
||||||
['login', 'validateActivity'],
|
['login', 'validateActivity'],
|
||||||
|
|
||||||
['rememberMe', 'boolean'],
|
['rememberMe', 'boolean'],
|
||||||
@ -51,6 +58,22 @@ class LoginForm extends ApiForm {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function validateTotpToken($attribute) {
|
||||||
|
if ($this->hasErrors()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$account = $this->getAccount();
|
||||||
|
if (!$account->is_otp_enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$validator = new TotpValidator(['account' => $account]);
|
||||||
|
if (!$validator->validate($this->token, $error)) {
|
||||||
|
$this->addError($attribute, $error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function validateActivity($attribute) {
|
public function validateActivity($attribute) {
|
||||||
if (!$this->hasErrors()) {
|
if (!$this->hasErrors()) {
|
||||||
$account = $this->getAccount();
|
$account = $this->getAccount();
|
||||||
|
@ -8,15 +8,23 @@ use yii\codeception\BasePage;
|
|||||||
*/
|
*/
|
||||||
class AuthenticationRoute extends BasePage {
|
class AuthenticationRoute extends BasePage {
|
||||||
|
|
||||||
public function login($login = '', $password = '', $rememberMe = false) {
|
/**
|
||||||
|
* @param string $login
|
||||||
|
* @param string $password
|
||||||
|
* @param string|bool|null $rememberMeOrToken
|
||||||
|
* @param bool $rememberMe
|
||||||
|
*/
|
||||||
|
public function login($login = '', $password = '', $rememberMeOrToken = null, $rememberMe = false) {
|
||||||
$this->route = ['authentication/login'];
|
$this->route = ['authentication/login'];
|
||||||
$params = [
|
$params = [
|
||||||
'login' => $login,
|
'login' => $login,
|
||||||
'password' => $password,
|
'password' => $password,
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($rememberMe) {
|
if ((is_bool($rememberMeOrToken) && $rememberMeOrToken) || $rememberMe) {
|
||||||
$params['rememberMe'] = 1;
|
$params['rememberMe'] = 1;
|
||||||
|
} elseif ($rememberMeOrToken !== null) {
|
||||||
|
$params['token'] = $rememberMeOrToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->actor->sendPOST($this->getUrl(), $params);
|
$this->actor->sendPOST($this->getUrl(), $params);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace tests\codeception\api;
|
namespace tests\codeception\api;
|
||||||
|
|
||||||
|
use OTPHP\TOTP;
|
||||||
use tests\codeception\api\_pages\AuthenticationRoute;
|
use tests\codeception\api\_pages\AuthenticationRoute;
|
||||||
|
|
||||||
class LoginCest {
|
class LoginCest {
|
||||||
@ -103,6 +104,56 @@ class LoginCest {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testLoginToken(FunctionalTester $I) {
|
||||||
|
$route = new AuthenticationRoute($I);
|
||||||
|
|
||||||
|
$I->wantTo('see token don\'t have errors if email, username or token not set');
|
||||||
|
$route->login();
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'success' => false,
|
||||||
|
]);
|
||||||
|
$I->cantSeeResponseJsonMatchesJsonPath('$.errors.token');
|
||||||
|
|
||||||
|
$I->wantTo('see token don\'t have errors if username not exists in database');
|
||||||
|
$route->login('non-exist-username', 'random-password');
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'success' => false,
|
||||||
|
]);
|
||||||
|
$I->cantSeeResponseJsonMatchesJsonPath('$.errors.token');
|
||||||
|
|
||||||
|
$I->wantTo('see token don\'t has errors if email not exists in database');
|
||||||
|
$route->login('not-exist@user.com', 'random-password');
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'success' => false,
|
||||||
|
]);
|
||||||
|
$I->cantSeeResponseJsonMatchesJsonPath('$.errors.token');
|
||||||
|
|
||||||
|
$I->wantTo('see token don\'t has errors if email correct, but password wrong');
|
||||||
|
$route->login('not-exist@user.com', 'random-password');
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'success' => false,
|
||||||
|
]);
|
||||||
|
$I->cantSeeResponseJsonMatchesJsonPath('$.errors.token');
|
||||||
|
|
||||||
|
$I->wantTo('see error.token_required if username and password correct, but account have enable otp');
|
||||||
|
$route->login('AccountWithEnabledOtp', 'password_0');
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'success' => false,
|
||||||
|
'errors' => [
|
||||||
|
'token' => 'error.token_required',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$I->wantTo('see error.token_incorrect if username and password correct, but token wrong');
|
||||||
|
$route->login('AccountWithEnabledOtp', 'password_0', '123456');
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'success' => false,
|
||||||
|
'errors' => [
|
||||||
|
'token' => 'error.token_incorrect',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public function testLoginByUsernameCorrect(FunctionalTester $I) {
|
public function testLoginByUsernameCorrect(FunctionalTester $I) {
|
||||||
$route = new AuthenticationRoute($I);
|
$route = new AuthenticationRoute($I);
|
||||||
|
|
||||||
@ -151,4 +202,16 @@ class LoginCest {
|
|||||||
$I->canSeeAuthCredentials(true);
|
$I->canSeeAuthCredentials(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testLoginByAccountWithOtp(FunctionalTester $I) {
|
||||||
|
$route = new AuthenticationRoute($I);
|
||||||
|
|
||||||
|
$I->wantTo('login into account with enabled otp');
|
||||||
|
$route->login('AccountWithEnabledOtp', 'password_0', (new TOTP(null, 'secret-secret-secret'))->now());
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'success' => true,
|
||||||
|
]);
|
||||||
|
$I->cantSeeResponseJsonMatchesJsonPath('$.errors');
|
||||||
|
$I->canSeeAuthCredentials(false);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ use api\models\AccountIdentity;
|
|||||||
use api\models\authentication\LoginForm;
|
use api\models\authentication\LoginForm;
|
||||||
use Codeception\Specify;
|
use Codeception\Specify;
|
||||||
use common\models\Account;
|
use common\models\Account;
|
||||||
|
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;
|
||||||
|
|
||||||
@ -38,7 +39,7 @@ class LoginFormTest extends TestCase {
|
|||||||
'account' => null,
|
'account' => null,
|
||||||
]);
|
]);
|
||||||
$model->validateLogin('login');
|
$model->validateLogin('login');
|
||||||
expect($model->getErrors('login'))->equals(['error.login_not_exist']);
|
$this->assertEquals(['error.login_not_exist'], $model->getErrors('login'));
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->specify('no errors if login exists', function () {
|
$this->specify('no errors if login exists', function () {
|
||||||
@ -47,7 +48,7 @@ class LoginFormTest extends TestCase {
|
|||||||
'account' => new AccountIdentity(),
|
'account' => new AccountIdentity(),
|
||||||
]);
|
]);
|
||||||
$model->validateLogin('login');
|
$model->validateLogin('login');
|
||||||
expect($model->getErrors('login'))->isEmpty();
|
$this->assertEmpty($model->getErrors('login'));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,7 +59,7 @@ class LoginFormTest extends TestCase {
|
|||||||
'account' => new AccountIdentity(['password' => '12345678']),
|
'account' => new AccountIdentity(['password' => '12345678']),
|
||||||
]);
|
]);
|
||||||
$model->validatePassword('password');
|
$model->validatePassword('password');
|
||||||
expect($model->getErrors('password'))->equals(['error.password_incorrect']);
|
$this->assertEquals(['error.password_incorrect'], $model->getErrors('password'));
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->specify('no errors if password valid', function () {
|
$this->specify('no errors if password valid', function () {
|
||||||
@ -67,7 +68,35 @@ class LoginFormTest extends TestCase {
|
|||||||
'account' => new AccountIdentity(['password' => '12345678']),
|
'account' => new AccountIdentity(['password' => '12345678']),
|
||||||
]);
|
]);
|
||||||
$model->validatePassword('password');
|
$model->validatePassword('password');
|
||||||
expect($model->getErrors('password'))->isEmpty();
|
$this->assertEmpty($model->getErrors('password'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateTotpToken() {
|
||||||
|
$account = new AccountIdentity(['password' => '12345678']);
|
||||||
|
$account->password = '12345678';
|
||||||
|
$account->is_otp_enabled = true;
|
||||||
|
$account->otp_secret = 'mock secret';
|
||||||
|
|
||||||
|
$this->specify('error.token_incorrect if totp invalid', function() use ($account) {
|
||||||
|
$model = $this->createModel([
|
||||||
|
'password' => '12345678',
|
||||||
|
'token' => '321123',
|
||||||
|
'account' => $account,
|
||||||
|
]);
|
||||||
|
$model->validateTotpToken('token');
|
||||||
|
$this->assertEquals(['error.token_incorrect'], $model->getErrors('token'));
|
||||||
|
});
|
||||||
|
|
||||||
|
$totp = new TOTP(null, 'mock secret');
|
||||||
|
$this->specify('no errors if password valid', function() use ($account, $totp) {
|
||||||
|
$model = $this->createModel([
|
||||||
|
'password' => '12345678',
|
||||||
|
'token' => $totp->now(),
|
||||||
|
'account' => $account,
|
||||||
|
]);
|
||||||
|
$model->validateTotpToken('token');
|
||||||
|
$this->assertEmpty($model->getErrors('token'));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,7 +106,7 @@ class LoginFormTest extends TestCase {
|
|||||||
'account' => new AccountIdentity(['status' => Account::STATUS_REGISTERED]),
|
'account' => new AccountIdentity(['status' => Account::STATUS_REGISTERED]),
|
||||||
]);
|
]);
|
||||||
$model->validateActivity('login');
|
$model->validateActivity('login');
|
||||||
expect($model->getErrors('login'))->equals(['error.account_not_activated']);
|
$this->assertEquals(['error.account_not_activated'], $model->getErrors('login'));
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->specify('error.account_banned if account has banned status', function () {
|
$this->specify('error.account_banned if account has banned status', function () {
|
||||||
@ -85,7 +114,7 @@ class LoginFormTest extends TestCase {
|
|||||||
'account' => new AccountIdentity(['status' => Account::STATUS_BANNED]),
|
'account' => new AccountIdentity(['status' => Account::STATUS_BANNED]),
|
||||||
]);
|
]);
|
||||||
$model->validateActivity('login');
|
$model->validateActivity('login');
|
||||||
expect($model->getErrors('login'))->equals(['error.account_banned']);
|
$this->assertEquals(['error.account_banned'], $model->getErrors('login'));
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->specify('no errors if account active', function () {
|
$this->specify('no errors if account active', function () {
|
||||||
@ -93,12 +122,11 @@ class LoginFormTest extends TestCase {
|
|||||||
'account' => new AccountIdentity(['status' => Account::STATUS_ACTIVE]),
|
'account' => new AccountIdentity(['status' => Account::STATUS_ACTIVE]),
|
||||||
]);
|
]);
|
||||||
$model->validateActivity('login');
|
$model->validateActivity('login');
|
||||||
expect($model->getErrors('login'))->isEmpty();
|
$this->assertEmpty($model->getErrors('login'));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testLogin() {
|
public function testLogin() {
|
||||||
$this->specify('user should be able to login with correct username and password', function () {
|
|
||||||
$model = $this->createModel([
|
$model = $this->createModel([
|
||||||
'login' => 'erickskrauch',
|
'login' => 'erickskrauch',
|
||||||
'password' => '12345678',
|
'password' => '12345678',
|
||||||
@ -108,21 +136,22 @@ class LoginFormTest extends TestCase {
|
|||||||
'status' => Account::STATUS_ACTIVE,
|
'status' => Account::STATUS_ACTIVE,
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
expect('model should login user', $model->login())->isInstanceOf(LoginResult::class);
|
$this->assertInstanceOf(LoginResult::class, $model->login(), 'model should login user');
|
||||||
expect('error message should not be set', $model->errors)->isEmpty();
|
$this->assertEmpty($model->getErrors(), 'error message should not be set');
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testLoginWithRehashing() {
|
public function testLoginWithRehashing() {
|
||||||
$this->specify('user, that login using account with old pass hash strategy should update it automatically', function () {
|
|
||||||
$model = new LoginForm([
|
$model = new LoginForm([
|
||||||
'login' => $this->tester->grabFixture('accounts', 'user-with-old-password-type')['username'],
|
'login' => $this->tester->grabFixture('accounts', 'user-with-old-password-type')['username'],
|
||||||
'password' => '12345678',
|
'password' => '12345678',
|
||||||
]);
|
]);
|
||||||
expect($model->login())->isInstanceOf(LoginResult::class);
|
$this->assertInstanceOf(LoginResult::class, $model->login());
|
||||||
expect($model->errors)->isEmpty();
|
$this->assertEmpty($model->getErrors());
|
||||||
expect($model->getAccount()->password_hash_strategy)->equals(Account::PASS_HASH_STRATEGY_YII2);
|
$this->assertEquals(
|
||||||
});
|
Account::PASS_HASH_STRATEGY_YII2,
|
||||||
|
$model->getAccount()->password_hash_strategy,
|
||||||
|
'user, that login using account with old pass hash strategy should update it automatically'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user