Cleanup User Component, update tests

This commit is contained in:
ErickSkrauch 2019-07-26 17:04:57 +03:00
parent 445c234360
commit 4c2a9cc172
7 changed files with 172 additions and 155 deletions

View File

@ -1,4 +1,6 @@
<?php <?php
declare(strict_types=1);
namespace api\components\User; namespace api\components\User;
use api\exceptions\ThisShouldNotHappenException; use api\exceptions\ThisShouldNotHappenException;
@ -11,6 +13,7 @@ use Emarref\Jwt\Algorithm\AlgorithmInterface;
use Emarref\Jwt\Algorithm\Hs256; use Emarref\Jwt\Algorithm\Hs256;
use Emarref\Jwt\Algorithm\Rs256; use Emarref\Jwt\Algorithm\Rs256;
use Emarref\Jwt\Claim; use Emarref\Jwt\Claim;
use Emarref\Jwt\Encryption\Asymmetric as AsymmetricEncryption;
use Emarref\Jwt\Encryption\EncryptionInterface; use Emarref\Jwt\Encryption\EncryptionInterface;
use Emarref\Jwt\Encryption\Factory as EncryptionFactory; use Emarref\Jwt\Encryption\Factory as EncryptionFactory;
use Emarref\Jwt\Exception\VerificationException; use Emarref\Jwt\Exception\VerificationException;
@ -18,6 +21,7 @@ use Emarref\Jwt\HeaderParameter\Custom;
use Emarref\Jwt\Token; use Emarref\Jwt\Token;
use Emarref\Jwt\Verification\Context as VerificationContext; use Emarref\Jwt\Verification\Context as VerificationContext;
use Exception; use Exception;
use InvalidArgumentException;
use Webmozart\Assert\Assert; use Webmozart\Assert\Assert;
use Yii; use Yii;
use yii\base\InvalidConfigException; use yii\base\InvalidConfigException;
@ -39,6 +43,8 @@ class Component extends YiiUserComponent {
public const JWT_SUBJECT_PREFIX = 'ely|'; public const JWT_SUBJECT_PREFIX = 'ely|';
private const LATEST_JWT_VERSION = 1;
public $enableSession = false; public $enableSession = false;
public $loginUrl = null; public $loginUrl = null;
@ -71,26 +77,6 @@ class Component extends YiiUserComponent {
Assert::notEmpty($this->privateKeyPath, 'private key path must be specified'); Assert::notEmpty($this->privateKeyPath, 'private key path must be specified');
} }
public function getPublicKey() {
if (empty($this->publicKey)) {
if (!($this->publicKey = file_get_contents($this->publicKeyPath))) {
throw new InvalidConfigException('invalid public key path');
}
}
return $this->publicKey;
}
public function getPrivateKey() {
if (empty($this->privateKey)) {
if (!($this->privateKey = file_get_contents($this->privateKeyPath))) {
throw new InvalidConfigException('invalid private key path');
}
}
return $this->privateKey;
}
public function findIdentityByAccessToken($accessToken): ?IdentityInterface { public function findIdentityByAccessToken($accessToken): ?IdentityInterface {
if (empty($accessToken)) { if (empty($accessToken)) {
return null; return null;
@ -109,29 +95,17 @@ class Component extends YiiUserComponent {
return null; return null;
} }
public function createJwtAuthenticationToken(Account $account, bool $rememberMe): AuthenticationResult { public function createJwtAuthenticationToken(Account $account, AccountSession $session = null): Token {
$ip = Yii::$app->request->userIP;
$token = $this->createToken($account); $token = $this->createToken($account);
if ($rememberMe) { if ($session !== null) {
$session = new AccountSession();
$session->account_id = $account->id;
$session->setIp($ip);
$session->generateRefreshToken();
if (!$session->save()) {
throw new ThisShouldNotHappenException('Cannot save account session model');
}
$token->addClaim(new Claim\JwtId($session->id)); $token->addClaim(new Claim\JwtId($session->id));
} else { } else {
$session = null;
// If we don't remember a session, the token should live longer // If we don't remember a session, the token should live longer
// so that the session doesn't end while working with the account // so that the session doesn't end while working with the account
$token->addClaim(new Claim\Expiration((new DateTime())->add(new DateInterval($this->sessionTimeout)))); $token->addClaim(new Claim\Expiration((new DateTime())->add(new DateInterval($this->sessionTimeout))));
} }
$jwt = $this->serializeToken($token); return $token;
return new AuthenticationResult($account, $jwt, $session);
} }
public function renewJwtAuthenticationToken(AccountSession $session): AuthenticationResult { public function renewJwtAuthenticationToken(AccountSession $session): AuthenticationResult {
@ -155,6 +129,13 @@ class Component extends YiiUserComponent {
return $result; return $result;
} }
public function serializeToken(Token $token): string {
$encryption = $this->getEncryptionForVersion(self::LATEST_JWT_VERSION);
$this->prepareEncryptionForEncoding($encryption);
return (new Jwt())->serialize($token, $encryption);
}
/** /**
* @param string $jwtString * @param string $jwtString
* @return Token * @return Token
@ -170,9 +151,10 @@ class Component extends YiiUserComponent {
throw new VerificationException('Incorrect token encoding', 0, $e); throw new VerificationException('Incorrect token encoding', 0, $e);
} }
$version = $notVerifiedToken->getHeader()->findParameterByName('v'); $versionHeader = $notVerifiedToken->getHeader()->findParameterByName('v');
$version = $version ? $version->getValue() : null; $version = $versionHeader ? $versionHeader->getValue() : 0;
$encryption = $this->getEncryption($version); $encryption = $this->getEncryptionForVersion($version);
$this->prepareEncryptionForDecoding($encryption);
$context = new VerificationContext($encryption); $context = new VerificationContext($encryption);
$context->setSubject(self::JWT_SUBJECT_PREFIX); $context->setSubject(self::JWT_SUBJECT_PREFIX);
@ -240,28 +222,27 @@ class Component extends YiiUserComponent {
} }
} }
public function getAlgorithm(): AlgorithmInterface { private function getPublicKey() {
return new Rs256(); if (empty($this->publicKey)) {
} if (!($this->publicKey = file_get_contents($this->publicKeyPath))) {
throw new InvalidConfigException('invalid public key path');
public function getEncryption(?int $version): EncryptionInterface { }
$algorithm = $version ? new Rs256() : new Hs256($this->secret);
$encryption = EncryptionFactory::create($algorithm);
if ($version) {
$encryption->setPublicKey($this->getPublicKey())->setPrivateKey($this->getPrivateKey());
} }
return $encryption; return $this->publicKey;
} }
protected function serializeToken(Token $token): string { private function getPrivateKey() {
$encryption = $this->getEncryption(1); if (empty($this->privateKey)) {
if (!($this->privateKey = file_get_contents($this->privateKeyPath))) {
throw new InvalidConfigException('invalid private key path');
}
}
return (new Jwt())->serialize($token, $encryption); return $this->privateKey;
} }
protected function createToken(Account $account): Token { private function createToken(Account $account): Token {
$token = new Token(); $token = new Token();
$token->addHeader(new Custom('v', 1)); $token->addHeader(new Custom('v', 1));
foreach ($this->getClaims($account) as $claim) { foreach ($this->getClaims($account) as $claim) {
@ -275,7 +256,7 @@ class Component extends YiiUserComponent {
* @param Account $account * @param Account $account
* @return Claim\AbstractClaim[] * @return Claim\AbstractClaim[]
*/ */
protected function getClaims(Account $account): array { private function getClaims(Account $account): array {
$currentTime = new DateTime(); $currentTime = new DateTime();
return [ return [
@ -286,7 +267,22 @@ class Component extends YiiUserComponent {
]; ];
} }
private function getBearerToken() { private function getEncryptionForVersion(int $version): EncryptionInterface {
return EncryptionFactory::create($this->getAlgorithm($version ?? 0));
}
private function getAlgorithm(int $version): AlgorithmInterface {
switch ($version) {
case 0:
return new Hs256($this->secret);
case 1:
return new Rs256();
}
throw new InvalidArgumentException('Unsupported token version');
}
private function getBearerToken(): ?string {
$authHeader = Yii::$app->request->getHeaders()->get('Authorization'); $authHeader = Yii::$app->request->getHeaders()->get('Authorization');
if ($authHeader === null || !preg_match('/^Bearer\s+(.*?)$/', $authHeader, $matches)) { if ($authHeader === null || !preg_match('/^Bearer\s+(.*?)$/', $authHeader, $matches)) {
return null; return null;
@ -295,4 +291,16 @@ class Component extends YiiUserComponent {
return $matches[1]; return $matches[1];
} }
private function prepareEncryptionForEncoding(EncryptionInterface $encryption): void {
if ($encryption instanceof AsymmetricEncryption) {
$encryption->setPrivateKey($this->getPrivateKey());
}
}
private function prepareEncryptionForDecoding(EncryptionInterface $encryption) {
if ($encryption instanceof AsymmetricEncryption) {
$encryption->setPublicKey($this->getPublicKey());
}
}
} }

View File

@ -4,12 +4,14 @@ declare(strict_types=1);
namespace api\models\authentication; namespace api\models\authentication;
use api\aop\annotations\CollectModelMetrics; use api\aop\annotations\CollectModelMetrics;
use api\components\User\AuthenticationResult;
use api\models\base\ApiForm; use api\models\base\ApiForm;
use api\validators\EmailActivationKeyValidator; use api\validators\EmailActivationKeyValidator;
use common\models\Account; use common\models\Account;
use common\models\AccountSession;
use common\models\EmailActivation; use common\models\EmailActivation;
use Webmozart\Assert\Assert;
use Yii; use Yii;
use yii\base\ErrorException;
class ConfirmEmailForm extends ApiForm { class ConfirmEmailForm extends ApiForm {
@ -23,8 +25,8 @@ class ConfirmEmailForm extends ApiForm {
/** /**
* @CollectModelMetrics(prefix="signup.confirmEmail") * @CollectModelMetrics(prefix="signup.confirmEmail")
* @return \api\components\User\AuthenticationResult|bool * @return AuthenticationResult|bool
* @throws ErrorException * @throws \Throwable
*/ */
public function confirm() { public function confirm() {
if (!$this->validate()) { if (!$this->validate()) {
@ -37,17 +39,22 @@ class ConfirmEmailForm extends ApiForm {
$confirmModel = $this->key; $confirmModel = $this->key;
$account = $confirmModel->account; $account = $confirmModel->account;
$account->status = Account::STATUS_ACTIVE; $account->status = Account::STATUS_ACTIVE;
if (!$confirmModel->delete()) { Assert::notSame($confirmModel->delete(), false, 'Unable remove activation key.');
throw new ErrorException('Unable remove activation key.');
}
if (!$account->save()) { Assert::true($account->save(), 'Unable activate user account.');
throw new ErrorException('Unable activate user account.');
} $session = new AccountSession();
$session->account_id = $account->id;
$session->setIp(Yii::$app->request->userIP);
$session->generateRefreshToken();
Assert::true($session->save(), 'Cannot save account session model');
$token = Yii::$app->user->createJwtAuthenticationToken($account, $session);
$jwt = Yii::$app->user->serializeToken($token);
$transaction->commit(); $transaction->commit();
return Yii::$app->user->createJwtAuthenticationToken($account, true); return new AuthenticationResult($account, $jwt, $session);
} }
} }

View File

@ -1,12 +1,17 @@
<?php <?php
declare(strict_types=1);
namespace api\models\authentication; namespace api\models\authentication;
use api\aop\annotations\CollectModelMetrics; use api\aop\annotations\CollectModelMetrics;
use api\components\User\AuthenticationResult;
use api\models\base\ApiForm; use api\models\base\ApiForm;
use api\traits\AccountFinder; use api\traits\AccountFinder;
use api\validators\TotpValidator; use api\validators\TotpValidator;
use common\helpers\Error as E; use common\helpers\Error as E;
use common\models\Account; use common\models\Account;
use common\models\AccountSession;
use Webmozart\Assert\Assert;
use Yii; use Yii;
class LoginForm extends ApiForm { class LoginForm extends ApiForm {
@ -41,15 +46,13 @@ class LoginForm extends ApiForm {
]; ];
} }
public function validateLogin($attribute) { public function validateLogin(string $attribute): void {
if (!$this->hasErrors()) { if (!$this->hasErrors() && $this->getAccount() === null) {
if ($this->getAccount() === null) { $this->addError($attribute, E::LOGIN_NOT_EXIST);
$this->addError($attribute, E::LOGIN_NOT_EXIST);
}
} }
} }
public function validatePassword($attribute) { public function validatePassword(string $attribute): void {
if (!$this->hasErrors()) { if (!$this->hasErrors()) {
$account = $this->getAccount(); $account = $this->getAccount();
if ($account === null || !$account->validatePassword($this->password)) { if ($account === null || !$account->validatePassword($this->password)) {
@ -58,11 +61,12 @@ class LoginForm extends ApiForm {
} }
} }
public function validateTotp($attribute) { public function validateTotp(string $attribute): void {
if ($this->hasErrors()) { if ($this->hasErrors()) {
return; return;
} }
/** @var Account $account */
$account = $this->getAccount(); $account = $this->getAccount();
if (!$account->is_otp_enabled) { if (!$account->is_otp_enabled) {
return; return;
@ -73,8 +77,9 @@ class LoginForm extends ApiForm {
$validator->validateAttribute($this, $attribute); $validator->validateAttribute($this, $attribute);
} }
public function validateActivity($attribute) { public function validateActivity(string $attribute): void {
if (!$this->hasErrors()) { if (!$this->hasErrors()) {
/** @var Account $account */
$account = $this->getAccount(); $account = $this->getAccount();
if ($account->status === Account::STATUS_BANNED) { if ($account->status === Account::STATUS_BANNED) {
$this->addError($attribute, E::ACCOUNT_BANNED); $this->addError($attribute, E::ACCOUNT_BANNED);
@ -92,20 +97,37 @@ class LoginForm extends ApiForm {
/** /**
* @CollectModelMetrics(prefix="authentication.login") * @CollectModelMetrics(prefix="authentication.login")
* @return \api\components\User\AuthenticationResult|bool * @return AuthenticationResult|bool
*/ */
public function login() { public function login() {
if (!$this->validate()) { if (!$this->validate()) {
return false; return false;
} }
$transaction = Yii::$app->db->beginTransaction();
/** @var Account $account */
$account = $this->getAccount(); $account = $this->getAccount();
if ($account->password_hash_strategy !== Account::PASS_HASH_STRATEGY_YII2) { if ($account->password_hash_strategy !== Account::PASS_HASH_STRATEGY_YII2) {
$account->setPassword($this->password); $account->setPassword($this->password);
$account->save(); Assert::true($account->save(), 'Unable to upgrade user\'s password');
} }
return Yii::$app->user->createJwtAuthenticationToken($account, $this->rememberMe); $session = null;
if ($this->rememberMe) {
$session = new AccountSession();
$session->account_id = $account->id;
$session->setIp(Yii::$app->request->userIP);
$session->generateRefreshToken();
Assert::true($session->save(), 'Cannot save account session model');
}
$token = Yii::$app->user->createJwtAuthenticationToken($account, $session);
$jwt = Yii::$app->user->serializeToken($token);
$transaction->commit();
return new AuthenticationResult($account, $jwt, $session);
} }
} }

View File

@ -1,4 +1,6 @@
<?php <?php
declare(strict_types=1);
namespace api\models\authentication; namespace api\models\authentication;
use api\aop\annotations\CollectModelMetrics; use api\aop\annotations\CollectModelMetrics;
@ -7,8 +9,8 @@ use api\validators\EmailActivationKeyValidator;
use common\helpers\Error as E; use common\helpers\Error as E;
use common\models\EmailActivation; use common\models\EmailActivation;
use common\validators\PasswordValidator; use common\validators\PasswordValidator;
use Webmozart\Assert\Assert;
use Yii; use Yii;
use yii\base\ErrorException;
class RecoverPasswordForm extends ApiForm { class RecoverPasswordForm extends ApiForm {
@ -18,7 +20,7 @@ class RecoverPasswordForm extends ApiForm {
public $newRePassword; public $newRePassword;
public function rules() { public function rules(): array {
return [ return [
['key', EmailActivationKeyValidator::class, 'type' => EmailActivation::TYPE_FORGOT_PASSWORD_KEY], ['key', EmailActivationKeyValidator::class, 'type' => EmailActivation::TYPE_FORGOT_PASSWORD_KEY],
['newPassword', 'required', 'message' => E::NEW_PASSWORD_REQUIRED], ['newPassword', 'required', 'message' => E::NEW_PASSWORD_REQUIRED],
@ -28,18 +30,16 @@ class RecoverPasswordForm extends ApiForm {
]; ];
} }
public function validatePasswordAndRePasswordMatch($attribute) { public function validatePasswordAndRePasswordMatch(string $attribute): void {
if (!$this->hasErrors()) { if (!$this->hasErrors() && $this->newPassword !== $this->newRePassword) {
if ($this->newPassword !== $this->newRePassword) { $this->addError($attribute, E::NEW_RE_PASSWORD_DOES_NOT_MATCH);
$this->addError($attribute, E::NEW_RE_PASSWORD_DOES_NOT_MATCH);
}
} }
} }
/** /**
* @CollectModelMetrics(prefix="authentication.recoverPassword") * @CollectModelMetrics(prefix="authentication.recoverPassword")
* @return \api\components\User\AuthenticationResult|bool * @return \api\components\User\AuthenticationResult|bool
* @throws ErrorException * @throws \Throwable
*/ */
public function recoverPassword() { public function recoverPassword() {
if (!$this->validate()) { if (!$this->validate()) {
@ -52,17 +52,16 @@ class RecoverPasswordForm extends ApiForm {
$confirmModel = $this->key; $confirmModel = $this->key;
$account = $confirmModel->account; $account = $confirmModel->account;
$account->password = $this->newPassword; $account->password = $this->newPassword;
if (!$confirmModel->delete()) { Assert::notSame($confirmModel->delete(), false, 'Unable remove activation key.');
throw new ErrorException('Unable remove activation key.');
}
if (!$account->save(false)) { Assert::true($account->save(), 'Unable activate user account.');
throw new ErrorException('Unable activate user account.');
} $token = Yii::$app->user->createJwtAuthenticationToken($account);
$jwt = Yii::$app->user->serializeToken($token);
$transaction->commit(); $transaction->commit();
return Yii::$app->user->createJwtAuthenticationToken($account, false); return new \api\components\User\AuthenticationResult($account, $jwt, null);
} }
} }

View File

@ -19,8 +19,9 @@ class FunctionalTester extends Actor {
throw new InvalidArgumentException("Cannot find account for username \"{$asUsername}\""); throw new InvalidArgumentException("Cannot find account for username \"{$asUsername}\"");
} }
$result = Yii::$app->user->createJwtAuthenticationToken($account, false); $token = Yii::$app->user->createJwtAuthenticationToken($account);
$this->amBearerAuthenticated($result->getJwt()); $jwt = Yii::$app->user->serializeToken($token);
$this->amBearerAuthenticated($jwt);
return $account->id; return $account->id;
} }

View File

@ -1,24 +1,22 @@
<?php <?php
declare(strict_types=1);
namespace codeception\api\unit\components\User; namespace codeception\api\unit\components\User;
use api\components\User\AuthenticationResult;
use api\components\User\Component; use api\components\User\Component;
use api\components\User\Identity; use api\components\User\Identity;
use api\tests\unit\TestCase; use api\tests\unit\TestCase;
use common\models\Account; use common\models\Account;
use common\models\AccountSession; use common\models\AccountSession;
use common\tests\_support\ProtectedCaller;
use common\tests\fixtures\AccountFixture; use common\tests\fixtures\AccountFixture;
use common\tests\fixtures\AccountSessionFixture; use common\tests\fixtures\AccountSessionFixture;
use common\tests\fixtures\MinecraftAccessKeyFixture; use common\tests\fixtures\MinecraftAccessKeyFixture;
use Emarref\Jwt\Claim; use Emarref\Jwt\Claim;
use Emarref\Jwt\Jwt; use Emarref\Jwt\Jwt;
use Emarref\Jwt\Token;
use Yii; use Yii;
use yii\web\Request; use yii\web\Request;
class ComponentTest extends TestCase { class ComponentTest extends TestCase {
use ProtectedCaller;
/** /**
* @var Component|\PHPUnit\Framework\MockObject\MockObject * @var Component|\PHPUnit\Framework\MockObject\MockObject
@ -41,42 +39,24 @@ class ComponentTest extends TestCase {
public function testCreateJwtAuthenticationToken() { public function testCreateJwtAuthenticationToken() {
$this->mockRequest(); $this->mockRequest();
// Token without session
$account = new Account(['id' => 1]); $account = new Account(['id' => 1]);
$result = $this->component->createJwtAuthenticationToken($account, false); $token = $this->component->createJwtAuthenticationToken($account);
$this->assertInstanceOf(AuthenticationResult::class, $result); $payloads = $token->getPayload();
$this->assertNull($result->getSession()); $this->assertEqualsWithDelta(time(), $payloads->findClaimByName('iat')->getValue(), 3);
$this->assertSame($account, $result->getAccount());
$payloads = (new Jwt())->deserialize($result->getJwt())->getPayload();
/** @noinspection NullPointerExceptionInspection */
$this->assertEqualsWithDelta(time(), $payloads->findClaimByName(Claim\IssuedAt::NAME)->getValue(), 3);
/** @noinspection SummerTimeUnsafeTimeManipulationInspection */
/** @noinspection NullPointerExceptionInspection */
$this->assertEqualsWithDelta(time() + 60 * 60 * 24 * 7, $payloads->findClaimByName('exp')->getValue(), 3); $this->assertEqualsWithDelta(time() + 60 * 60 * 24 * 7, $payloads->findClaimByName('exp')->getValue(), 3);
/** @noinspection NullPointerExceptionInspection */
$this->assertSame('ely|1', $payloads->findClaimByName('sub')->getValue()); $this->assertSame('ely|1', $payloads->findClaimByName('sub')->getValue());
/** @noinspection NullPointerExceptionInspection */
$this->assertSame('accounts_web_user', $payloads->findClaimByName('ely-scopes')->getValue()); $this->assertSame('accounts_web_user', $payloads->findClaimByName('ely-scopes')->getValue());
$this->assertNull($payloads->findClaimByName('jti')); $this->assertNull($payloads->findClaimByName('jti'));
/** @var Account $account */ $session = new AccountSession(['id' => 2]);
$account = $this->tester->grabFixture('accounts', 'admin'); $token = $this->component->createJwtAuthenticationToken($account, $session);
$result = $this->component->createJwtAuthenticationToken($account, true); $payloads = $token->getPayload();
$this->assertInstanceOf(AuthenticationResult::class, $result); $this->assertEqualsWithDelta(time(), $payloads->findClaimByName('iat')->getValue(), 3);
$this->assertInstanceOf(AccountSession::class, $result->getSession());
$this->assertSame($account, $result->getAccount());
/** @noinspection NullPointerExceptionInspection */
$this->assertTrue($result->getSession()->refresh());
$payloads = (new Jwt())->deserialize($result->getJwt())->getPayload();
/** @noinspection NullPointerExceptionInspection */
$this->assertEqualsWithDelta(time(), $payloads->findClaimByName(Claim\IssuedAt::NAME)->getValue(), 3);
/** @noinspection NullPointerExceptionInspection */
$this->assertEqualsWithDelta(time() + 3600, $payloads->findClaimByName('exp')->getValue(), 3); $this->assertEqualsWithDelta(time() + 3600, $payloads->findClaimByName('exp')->getValue(), 3);
/** @noinspection NullPointerExceptionInspection */
$this->assertSame('ely|1', $payloads->findClaimByName('sub')->getValue()); $this->assertSame('ely|1', $payloads->findClaimByName('sub')->getValue());
/** @noinspection NullPointerExceptionInspection */
$this->assertSame('accounts_web_user', $payloads->findClaimByName('ely-scopes')->getValue()); $this->assertSame('accounts_web_user', $payloads->findClaimByName('ely-scopes')->getValue());
/** @noinspection NullPointerExceptionInspection */ $this->assertSame(2, $payloads->findClaimByName('jti')->getValue());
$this->assertSame($result->getSession()->id, $payloads->findClaimByName('jti')->getValue());
} }
public function testRenewJwtAuthenticationToken() { public function testRenewJwtAuthenticationToken() {
@ -105,15 +85,19 @@ class ComponentTest extends TestCase {
public function testParseToken() { public function testParseToken() {
$this->mockRequest(); $this->mockRequest();
$token = $this->callProtected($this->component, 'createToken', new Account(['id' => 1])); $account = new Account(['id' => 1]);
$jwt = $this->callProtected($this->component, 'serializeToken', $token); $token = $this->component->createJwtAuthenticationToken($account);
$this->assertInstanceOf(Token::class, $this->component->parseToken($jwt), 'success get RenewResult object'); $jwt = $this->component->serializeToken($token);
$this->component->parseToken($jwt);
} }
public function testGetActiveSession() { public function testGetActiveSession() {
/** @var Account $account */
$account = $this->tester->grabFixture('accounts', 'admin'); $account = $this->tester->grabFixture('accounts', 'admin');
$result = $this->component->createJwtAuthenticationToken($account, true); /** @var AccountSession $session */
$this->component->logout(); $session = $this->tester->grabFixture('sessions', 'admin');
$token = $this->component->createJwtAuthenticationToken($account, $session);
$jwt = $this->component->serializeToken($token);
/** @var Component|\PHPUnit\Framework\MockObject\MockObject $component */ /** @var Component|\PHPUnit\Framework\MockObject\MockObject $component */
$component = $this->getMockBuilder(Component::class) $component = $this->getMockBuilder(Component::class)
@ -125,39 +109,42 @@ class ComponentTest extends TestCase {
->method('getIsGuest') ->method('getIsGuest')
->willReturn(false); ->willReturn(false);
$this->mockAuthorizationHeader($result->getJwt()); $this->mockAuthorizationHeader($jwt);
$session = $component->getActiveSession(); $foundSession = $component->getActiveSession();
$this->assertInstanceOf(AccountSession::class, $session); $this->assertInstanceOf(AccountSession::class, $foundSession);
/** @noinspection NullPointerExceptionInspection */ $this->assertSame($session->id, $foundSession->id);
$this->assertSame($session->id, $result->getSession()->id);
} }
public function testTerminateSessions() { public function testTerminateSessions() {
/** @var AccountSession $session */ /** @var AccountSession $session */
$session = AccountSession::findOne($this->tester->grabFixture('sessions', 'admin2')['id']); $session = $this->tester->grabFixture('sessions', 'admin2');
/** @var Component|\Mockery\MockInterface $component */ /** @var Component|\Mockery\MockInterface $component */
$component = mock(Component::class . '[getActiveSession]', [$this->getComponentConfig()])->shouldDeferMissing(); $component = mock(Component::class . '[getActiveSession]', [$this->getComponentConfig()])->makePartial();
$component->shouldReceive('getActiveSession')->times(1)->andReturn($session); $component->shouldReceive('getActiveSession')->times(1)->andReturn($session);
/** @var Account $account */ /** @var Account $account */
$account = $this->tester->grabFixture('accounts', 'admin'); $account = $this->tester->grabFixture('accounts', 'admin');
$component->createJwtAuthenticationToken($account, true); $component->createJwtAuthenticationToken($account);
// Dry run: no sessions should be removed
$component->terminateSessions($account, Component::KEEP_MINECRAFT_SESSIONS | Component::KEEP_SITE_SESSIONS); $component->terminateSessions($account, Component::KEEP_MINECRAFT_SESSIONS | Component::KEEP_SITE_SESSIONS);
$this->assertNotEmpty($account->getMinecraftAccessKeys()->all()); $this->assertNotEmpty($account->getMinecraftAccessKeys()->all());
$this->assertNotEmpty($account->getSessions()->all()); $this->assertNotEmpty($account->getSessions()->all());
// All Minecraft sessions should be removed. Web sessions should be kept
$component->terminateSessions($account, Component::KEEP_SITE_SESSIONS); $component->terminateSessions($account, Component::KEEP_SITE_SESSIONS);
$this->assertEmpty($account->getMinecraftAccessKeys()->all()); $this->assertEmpty($account->getMinecraftAccessKeys()->all());
$this->assertNotEmpty($account->getSessions()->all()); $this->assertNotEmpty($account->getSessions()->all());
// All sessions should be removed except the current one
$component->terminateSessions($account, Component::KEEP_CURRENT_SESSION); $component->terminateSessions($account, Component::KEEP_CURRENT_SESSION);
$sessions = $account->getSessions()->all(); $sessions = $account->getSessions()->all();
$this->assertCount(1, $sessions); $this->assertCount(1, $sessions);
$this->assertSame($session->id, $sessions[0]->id); $this->assertSame($session->id, $sessions[0]->id);
// With no arguments each and every session should be removed
$component->terminateSessions($account); $component->terminateSessions($account);
$this->assertEmpty($account->getSessions()->all()); $this->assertEmpty($account->getSessions()->all());
$this->assertEmpty($account->getMinecraftAccessKeys()->all()); $this->assertEmpty($account->getMinecraftAccessKeys()->all());
@ -165,7 +152,7 @@ class ComponentTest extends TestCase {
private function mockRequest($userIP = '127.0.0.1') { private function mockRequest($userIP = '127.0.0.1') {
/** @var Request|\Mockery\MockInterface $request */ /** @var Request|\Mockery\MockInterface $request */
$request = mock(Request::class . '[getHostInfo,getUserIP]')->shouldDeferMissing(); $request = mock(Request::class . '[getHostInfo,getUserIP]')->makePartial();
$request->shouldReceive('getHostInfo')->andReturn('http://localhost'); $request->shouldReceive('getHostInfo')->andReturn('http://localhost');
$request->shouldReceive('getUserIP')->andReturn($userIP); $request->shouldReceive('getUserIP')->andReturn($userIP);

View File

@ -1,19 +1,15 @@
<?php <?php
declare(strict_types=1);
namespace codeception\api\unit\models; namespace codeception\api\unit\models;
use api\components\User\Jwt;
use api\components\User\JwtIdentity; use api\components\User\JwtIdentity;
use api\tests\unit\TestCase; use api\tests\unit\TestCase;
use common\tests\_support\ProtectedCaller;
use common\tests\fixtures\AccountFixture; use common\tests\fixtures\AccountFixture;
use Emarref\Jwt\Claim; use Emarref\Jwt\Claim\Expiration as ExpirationClaim;
use Emarref\Jwt\Encryption\Factory as EncryptionFactory;
use Emarref\Jwt\HeaderParameter\Custom;
use Emarref\Jwt\Token;
use Yii; use Yii;
class JwtIdentityTest extends TestCase { class JwtIdentityTest extends TestCase {
use ProtectedCaller;
public function _fixtures(): array { public function _fixtures(): array {
return [ return [
@ -33,13 +29,7 @@ class JwtIdentityTest extends TestCase {
* @expectedExceptionMessage Token expired * @expectedExceptionMessage Token expired
*/ */
public function testFindIdentityByAccessTokenWithExpiredToken() { public function testFindIdentityByAccessTokenWithExpiredToken() {
$token = new Token(); $expiredToken = $this->generateToken(time() - 3600);
$token->addHeader(new Custom('v', 1));
$token->addClaim(new Claim\IssuedAt(1464593193));
$token->addClaim(new Claim\Expiration(1464596793));
$token->addClaim(new Claim\Subject('ely|' . $this->tester->grabFixture('accounts', 'admin')['id']));
$expiredToken = (new Jwt())->serialize($token, EncryptionFactory::create(Yii::$app->user->getAlgorithm())->setPrivateKey(Yii::$app->user->privateKey));
JwtIdentity::findIdentityByAccessToken($expiredToken); JwtIdentity::findIdentityByAccessToken($expiredToken);
} }
@ -51,14 +41,17 @@ class JwtIdentityTest extends TestCase {
JwtIdentity::findIdentityByAccessToken(''); JwtIdentity::findIdentityByAccessToken('');
} }
protected function generateToken() { private function generateToken(int $expiresAt = null): string {
/** @var \api\components\User\Component $component */ /** @var \api\components\User\Component $component */
$component = Yii::$app->user; $component = Yii::$app->user;
/** @var \common\models\Account $account */ /** @var \common\models\Account $account */
$account = $this->tester->grabFixture('accounts', 'admin'); $account = $this->tester->grabFixture('accounts', 'admin');
$token = $this->callProtected($component, 'createToken', $account); $token = $component->createJwtAuthenticationToken($account);
if ($expiresAt !== null) {
$token->addClaim(new ExpirationClaim($expiresAt));
}
return $this->callProtected($component, 'serializeToken', $token); return $component->serializeToken($token);
} }
} }