Логика уничтожения активных сессий вынесена в компонент User

Теперь при смене пароля и включении двухфакторной аутентификации также очищаются и сессии Minecraft
This commit is contained in:
ErickSkrauch 2017-02-23 02:01:32 +03:00
parent 7bf8260331
commit 689919fc17
6 changed files with 105 additions and 26 deletions

View File

@ -27,6 +27,11 @@ use yii\web\User as YiiUserComponent;
*/ */
class Component extends YiiUserComponent { class Component extends YiiUserComponent {
const TERMINATE_MINECRAFT_SESSIONS = 1;
const TERMINATE_SITE_SESSIONS = 2;
const DO_NOT_TERMINATE_CURRENT_SESSION = 4;
const TERMINATE_ALL = self::TERMINATE_MINECRAFT_SESSIONS | self::TERMINATE_SITE_SESSIONS;
public $enableSession = false; public $enableSession = false;
public $loginUrl = null; public $loginUrl = null;
@ -184,6 +189,24 @@ class Component extends YiiUserComponent {
return AccountSession::findOne($sessionId->getValue()); return AccountSession::findOne($sessionId->getValue());
} }
public function terminateSessions(int $mode = self::TERMINATE_ALL | self::DO_NOT_TERMINATE_CURRENT_SESSION): void {
$identity = $this->getIdentity();
$activeSession = ($mode & self::DO_NOT_TERMINATE_CURRENT_SESSION) ? $this->getActiveSession() : null;
if ($mode & self::TERMINATE_SITE_SESSIONS) {
foreach ($identity->sessions as $session) {
if ($activeSession === null || $activeSession->id !== $session->id) {
$session->delete();
}
}
}
if ($mode & self::TERMINATE_MINECRAFT_SESSIONS) {
foreach ($identity->minecraftAccessKeys as $minecraftAccessKey) {
$minecraftAccessKey->delete();
}
}
}
public function getAlgorithm() : AlgorithmInterface { public function getAlgorithm() : AlgorithmInterface {
return new Hs256($this->secret); return new Hs256($this->secret);
} }

View File

@ -66,15 +66,7 @@ class ChangePasswordForm extends ApiForm {
$account->setPassword($this->newPassword); $account->setPassword($this->newPassword);
if ($this->logoutAll) { if ($this->logoutAll) {
/** @var \api\components\User\Component $userComponent */ Yii::$app->user->terminateSessions();
$userComponent = Yii::$app->user;
$sessions = $account->sessions;
$activeSession = $userComponent->getActiveSession();
foreach ($sessions as $session) {
if (!$activeSession || $activeSession->id !== $session->id) {
$session->delete();
}
}
} }
if (!$account->save()) { if (!$account->save()) {

View File

@ -14,6 +14,7 @@ use common\components\Qr\ElyDecorator;
use common\helpers\Error as E; use common\helpers\Error as E;
use common\models\Account; use common\models\Account;
use OTPHP\TOTP; use OTPHP\TOTP;
use Yii;
use yii\base\ErrorException; use yii\base\ErrorException;
class TwoFactorAuthForm extends ApiForm { class TwoFactorAuthForm extends ApiForm {
@ -73,12 +74,18 @@ class TwoFactorAuthForm extends ApiForm {
return false; return false;
} }
$transaction = Yii::$app->db->beginTransaction();
$account = $this->account; $account = $this->account;
$account->is_otp_enabled = true; $account->is_otp_enabled = true;
if (!$account->save()) { if (!$account->save()) {
throw new ErrorException('Cannot enable otp for account'); throw new ErrorException('Cannot enable otp for account');
} }
Yii::$app->user->terminateSessions();
$transaction->commit();
return true; return true;
} }
@ -142,6 +149,7 @@ class TwoFactorAuthForm extends ApiForm {
* строка составляет 160% от исходной. Поэтому, генерируя исходный приватный ключ, мы должны обеспечить * строка составляет 160% от исходной. Поэтому, генерируя исходный приватный ключ, мы должны обеспечить
* ему такую длину, чтобы 160% его длины было равно запрошенному значению * ему такую длину, чтобы 160% его длины было равно запрошенному значению
* *
* @param int $length
* @throws ErrorException * @throws ErrorException
*/ */
protected function setOtpSecret(int $length = 24): void { protected function setOtpSecret(int $length = 24): void {

View File

@ -15,6 +15,7 @@ use tests\codeception\api\unit\TestCase;
use tests\codeception\common\_support\ProtectedCaller; use tests\codeception\common\_support\ProtectedCaller;
use tests\codeception\common\fixtures\AccountFixture; use tests\codeception\common\fixtures\AccountFixture;
use tests\codeception\common\fixtures\AccountSessionFixture; use tests\codeception\common\fixtures\AccountSessionFixture;
use tests\codeception\common\fixtures\MinecraftAccessKeyFixture;
use Yii; use Yii;
use yii\web\Request; use yii\web\Request;
@ -36,6 +37,7 @@ class ComponentTest extends TestCase {
return [ return [
'accounts' => AccountFixture::class, 'accounts' => AccountFixture::class,
'sessions' => AccountSessionFixture::class, 'sessions' => AccountSessionFixture::class,
'minecraftSessions' => MinecraftAccessKeyFixture::class,
]; ];
} }
@ -166,6 +168,43 @@ class ComponentTest extends TestCase {
}); });
} }
public function testTerminateSessions() {
/** @var AccountSession $session */
$session = AccountSession::findOne($this->tester->grabFixture('sessions', 'admin2')['id']);
/** @var Component|\PHPUnit_Framework_MockObject_MockObject $component */
$component = $this->getMockBuilder(Component::class)
->setMethods(['getActiveSession'])
->setConstructorArgs([$this->getComponentArguments()])
->getMock();
$component
->expects($this->exactly(1))
->method('getActiveSession')
->willReturn($session);
/** @var AccountIdentity $identity */
$identity = AccountIdentity::findOne($this->tester->grabFixture('accounts', 'admin')['id']);
$component->login($identity, true);
$component->terminateSessions(0);
$this->assertNotEmpty($identity->getMinecraftAccessKeys()->all());
$this->assertNotEmpty($identity->getSessions()->all());
$component->terminateSessions(Component::TERMINATE_MINECRAFT_SESSIONS);
$this->assertEmpty($identity->getMinecraftAccessKeys()->all());
$this->assertNotEmpty($identity->getSessions()->all());
$component->terminateSessions(Component::TERMINATE_SITE_SESSIONS | Component::DO_NOT_TERMINATE_CURRENT_SESSION);
$sessions = $identity->getSessions()->all();
$this->assertEquals(1, count($sessions));
$this->assertTrue($sessions[0]->id === $session->id);
$component->terminateSessions(Component::TERMINATE_ALL);
$this->assertEmpty($identity->getSessions()->all());
$this->assertEmpty($identity->getMinecraftAccessKeys()->all());
}
public function testSerializeToken() { public function testSerializeToken() {
$this->specify('get string, contained jwt token', function() { $this->specify('get string, contained jwt token', function() {
$token = new Token(); $token = new Token();

View File

@ -97,7 +97,7 @@ class ChangePasswordFormTest extends TestCase {
public function testChangePasswordWithLogout() { public function testChangePasswordWithLogout() {
/** @var Component|\PHPUnit_Framework_MockObject_MockObject $component */ /** @var Component|\PHPUnit_Framework_MockObject_MockObject $component */
$component = $this->getMockBuilder(Component::class) $component = $this->getMockBuilder(Component::class)
->setMethods(['getActiveSession']) ->setMethods(['getActiveSession', 'terminateSessions'])
->setConstructorArgs([[ ->setConstructorArgs([[
'identityClass' => AccountIdentity::class, 'identityClass' => AccountIdentity::class,
'enableSession' => false, 'enableSession' => false,
@ -114,25 +114,22 @@ class ChangePasswordFormTest extends TestCase {
->method('getActiveSession') ->method('getActiveSession')
->will($this->returnValue($session)); ->will($this->returnValue($session));
$component
->expects($this->once())
->method('terminateSessions');
Yii::$app->set('user', $component); Yii::$app->set('user', $component);
$this->specify('change password with removing all session, except current', function() use ($session) { /** @var Account $account */
/** @var Account $account */ $account = $this->tester->grabFixture('accounts', 'admin');
$account = Account::findOne($this->tester->grabFixture('accounts', 'admin')['id']); $model = new ChangePasswordForm($account, [
'password' => 'password_0',
'newPassword' => 'my-new-password',
'newRePassword' => 'my-new-password',
'logoutAll' => true,
]);
$model = new ChangePasswordForm($account, [ $this->assertTrue($model->changePassword());
'password' => 'password_0',
'newPassword' => 'my-new-password',
'newRePassword' => 'my-new-password',
'logoutAll' => true,
]);
expect($model->changePassword())->true();
/** @var AccountSession[] $sessions */
$sessions = $account->getSessions()->all();
expect(count($sessions))->equals(1);
expect($sessions[0]->id)->equals($session->id);
});
} }
} }

View File

@ -1,12 +1,15 @@
<?php <?php
namespace tests\codeception\api\unit\models\profile; namespace tests\codeception\api\unit\models\profile;
use api\components\User\Component;
use api\models\AccountIdentity;
use api\models\profile\TwoFactorAuthForm; use api\models\profile\TwoFactorAuthForm;
use common\helpers\Error as E; use common\helpers\Error as E;
use common\models\Account; use common\models\Account;
use OTPHP\TOTP; use OTPHP\TOTP;
use tests\codeception\api\unit\TestCase; use tests\codeception\api\unit\TestCase;
use tests\codeception\common\_support\ProtectedCaller; use tests\codeception\common\_support\ProtectedCaller;
use Yii;
class TwoFactorAuthFormTest extends TestCase { class TwoFactorAuthFormTest extends TestCase {
use ProtectedCaller; use ProtectedCaller;
@ -69,6 +72,23 @@ class TwoFactorAuthFormTest extends TestCase {
} }
public function testActivate() { public function testActivate() {
/** @var Component|\PHPUnit_Framework_MockObject_MockObject $component */
$component = $this->getMockBuilder(Component::class)
->setMethods(['terminateSessions'])
->setConstructorArgs([[
'identityClass' => AccountIdentity::class,
'enableSession' => false,
'loginUrl' => null,
'secret' => 'secret',
]])
->getMock();
$component
->expects($this->once())
->method('terminateSessions');
Yii::$app->set('user', $component);
/** @var Account|\PHPUnit_Framework_MockObject_MockObject $account */ /** @var Account|\PHPUnit_Framework_MockObject_MockObject $account */
$account = $this->getMockBuilder(Account::class) $account = $this->getMockBuilder(Account::class)
->setMethods(['save']) ->setMethods(['save'])