mirror of
https://github.com/elyby/accounts.git
synced 2024-11-30 10:42:16 +05:30
Логика уничтожения активных сессий вынесена в компонент User
Теперь при смене пароля и включении двухфакторной аутентификации также очищаются и сессии Minecraft
This commit is contained in:
parent
7bf8260331
commit
689919fc17
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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()) {
|
||||||
|
@ -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 {
|
||||||
|
@ -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();
|
||||||
|
@ -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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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'])
|
||||||
|
Loading…
Reference in New Issue
Block a user