Добавлен роут и логика для обновления access_token по refresh_token'у

This commit is contained in:
ErickSkrauch 2016-05-31 01:03:30 +03:00
parent cb038c897b
commit 1945a7baec
9 changed files with 258 additions and 3 deletions

View File

@ -1,6 +1,7 @@
<?php <?php
namespace api\components\User; namespace api\components\User;
use api\models\AccountIdentity;
use common\models\AccountSession; use common\models\AccountSession;
use Emarref\Jwt\Algorithm\Hs256; use Emarref\Jwt\Algorithm\Hs256;
use Emarref\Jwt\Claim; use Emarref\Jwt\Claim;
@ -64,6 +65,30 @@ class Component extends YiiUserComponent {
return $result; return $result;
} }
public function renew(AccountSession $session) {
$account = $session->account;
$transaction = Yii::$app->db->beginTransaction();
try {
$identity = new AccountIdentity($account->attributes);
$jwt = $this->getJWT($identity);
$result = new RenewResult($identity, $jwt);
$session->setIp(Yii::$app->request->userIP);
$session->last_refreshed_at = time();
if (!$session->save()) {
throw new ErrorException('Cannot update session info');
}
$transaction->commit();
} catch (ErrorException $e) {
$transaction->rollBack();
throw $e;
}
return $result;
}
public function getJWT(IdentityInterface $identity) { public function getJWT(IdentityInterface $identity) {
$jwt = new Jwt(); $jwt = new Jwt();
$token = new Token(); $token = new Token();

View File

@ -0,0 +1,42 @@
<?php
namespace api\components\User;
use Yii;
use yii\web\IdentityInterface;
class RenewResult {
/**
* @var IdentityInterface
*/
private $identity;
/**
* @var string
*/
private $jwt;
public function __construct(IdentityInterface $identity, string $jwt) {
$this->identity = $identity;
$this->jwt = $jwt;
}
public function getIdentity() : IdentityInterface {
return $this->identity;
}
public function getJwt() : string {
return $this->jwt;
}
public function getAsResponse() {
/** @var Component $component */
$component = Yii::$app->user;
return [
'access_token' => $this->getJwt(),
'expires_in' => $component->expirationTimeout,
];
}
}

View File

@ -4,6 +4,7 @@ namespace api\controllers;
use api\models\authentication\ForgotPasswordForm; use api\models\authentication\ForgotPasswordForm;
use api\models\authentication\LoginForm; use api\models\authentication\LoginForm;
use api\models\authentication\RecoverPasswordForm; use api\models\authentication\RecoverPasswordForm;
use api\models\authentication\RefreshTokenForm;
use common\helpers\StringHelper; use common\helpers\StringHelper;
use Yii; use Yii;
use yii\filters\AccessControl; use yii\filters\AccessControl;
@ -14,13 +15,13 @@ class AuthenticationController extends Controller {
public function behaviors() { public function behaviors() {
return ArrayHelper::merge(parent::behaviors(), [ return ArrayHelper::merge(parent::behaviors(), [
'authenticator' => [ 'authenticator' => [
'except' => ['login', 'forgot-password', 'recover-password'], 'except' => ['login', 'forgot-password', 'recover-password', 'refresh-token'],
], ],
'access' => [ 'access' => [
'class' => AccessControl::class, 'class' => AccessControl::class,
'rules' => [ 'rules' => [
[ [
'actions' => ['login', 'forgot-password', 'recover-password'], 'actions' => ['login', 'forgot-password', 'recover-password', 'refresh-token'],
'allow' => true, 'allow' => true,
'roles' => ['?'], 'roles' => ['?'],
], ],
@ -34,6 +35,7 @@ class AuthenticationController extends Controller {
'login' => ['POST'], 'login' => ['POST'],
'forgot-password' => ['POST'], 'forgot-password' => ['POST'],
'recover-password' => ['POST'], 'recover-password' => ['POST'],
'refresh-token' => ['POST'],
]; ];
} }
@ -109,4 +111,19 @@ class AuthenticationController extends Controller {
], $result->getAsResponse()); ], $result->getAsResponse());
} }
public function actionRefreshToken() {
$model = new RefreshTokenForm();
$model->load(Yii::$app->request->post());
if (($result = $model->renew()) === false) {
return [
'success' => false,
'errors' => $this->normalizeModelErrors($model->getErrors()),
];
}
return array_merge([
'success' => true,
], $result->getAsResponse());
}
} }

View File

@ -0,0 +1,58 @@
<?php
namespace api\models\authentication;
use api\models\base\ApiForm;
use common\models\AccountSession;
use Yii;
class RefreshTokenForm extends ApiForm {
public $refresh_token;
/**
* @var AccountSession|null
*/
private $session;
public function rules() {
return [
['refresh_token', 'required'],
['refresh_token', 'validateRefreshToken'],
];
}
public function validateRefreshToken() {
if (!$this->hasErrors()) {
/** @var AccountSession|null $token */
if ($this->getSession() === null) {
$this->addError('refresh_token', 'error.refresh_token_not_exist');
}
}
}
/**
* @return \api\components\User\RenewResult|bool
*/
public function renew() {
if (!$this->validate()) {
return false;
}
/** @var \api\components\User\Component $component */
$component = Yii::$app->user;
return $component->renew($this->getSession());
}
/**
* @return AccountSession|null
*/
public function getSession() {
if ($this->session === null) {
$this->session = AccountSession::findOne(['refresh_token' => $this->refresh_token]);
}
return $this->session;
}
}

View File

@ -12,7 +12,7 @@ use yii\db\ActiveRecord;
* @property string $refresh_token * @property string $refresh_token
* @property integer $last_used_ip * @property integer $last_used_ip
* @property integer $created_at * @property integer $created_at
* @property integer $last_refreshed * @property integer $last_refreshed_at
* *
* Отношения: * Отношения:
* @property Account $account * @property Account $account

View File

@ -38,4 +38,11 @@ class AuthenticationRoute extends BasePage {
]); ]);
} }
public function refreshToken($refreshToken = null) {
$this->route = ['authentication/refresh-token'];
$this->actor->sendPOST($this->getUrl(), [
'refresh_token' => $refreshToken,
]);
}
} }

View File

@ -0,0 +1,32 @@
<?php
namespace codeception\api\functional;
use tests\codeception\api\_pages\AuthenticationRoute;
use tests\codeception\api\FunctionalTester;
class RefreshTokenCest {
public function testRefreshInvalidToken(FunctionalTester $I) {
$route = new AuthenticationRoute($I);
$I->wantTo('get error.refresh_token_not_exist if passed token is invalid');
$route->refreshToken('invalid-token');
$I->canSeeResponseCodeIs(200);
$I->canSeeResponseContainsJson([
'success' => false,
'errors' => [
'refresh_token' => 'error.refresh_token_not_exist',
],
]);
}
public function testRefreshToken(FunctionalTester $I) {
$route = new AuthenticationRoute($I);
$I->wantTo('get new access_token by my refresh_token');
$route->refreshToken('SOutIr6Seeaii3uqMVy3Wan8sKFVFrNz');
$I->canSeeResponseCodeIs(200);
$I->canSeeAuthCredentials(false);
}
}

View File

@ -3,6 +3,7 @@ namespace codeception\api\unit\components\User;
use api\components\User\Component; use api\components\User\Component;
use api\components\User\LoginResult; use api\components\User\LoginResult;
use api\components\User\RenewResult;
use api\models\AccountIdentity; use api\models\AccountIdentity;
use Codeception\Specify; use Codeception\Specify;
use common\models\AccountSession; use common\models\AccountSession;
@ -75,6 +76,24 @@ class ComponentTest extends DbTestCase {
}); });
} }
public function testRenew() {
$this->specify('success get RenewResult object', function() {
/** @var AccountSession $session */
$session = AccountSession::findOne($this->sessions['admin']['id']);
$callTime = time();
$usedRemoteAddr = $_SERVER['REMOTE_ADDR'] ?? null;
$_SERVER['REMOTE_ADDR'] = '192.168.0.1';
$result = $this->component->renew($session);
expect($result)->isInstanceOf(RenewResult::class);
expect(is_string($result->getJwt()))->true();
expect($result->getIdentity()->getId())->equals($session->account_id);
$session->refresh();
expect($session->last_refreshed_at)->greaterOrEquals($callTime);
expect($session->getReadableIp())->equals($_SERVER['REMOTE_ADDR']);
$_SERVER['REMOTE_ADDR'] = $usedRemoteAddr;
});
}
public function testGetJWT() { public function testGetJWT() {
$this->specify('get string, contained jwt token', function() { $this->specify('get string, contained jwt token', function() {
expect($this->component->getJWT(new AccountIdentity(['id' => 1]))) expect($this->component->getJWT(new AccountIdentity(['id' => 1])))

View File

@ -0,0 +1,55 @@
<?php
namespace codeception\api\unit\models\authentication;
use api\components\User\RenewResult;
use api\models\authentication\RefreshTokenForm;
use Codeception\Specify;
use common\models\AccountSession;
use tests\codeception\api\unit\DbTestCase;
use tests\codeception\common\fixtures\AccountSessionFixture;
/**
* @property AccountSessionFixture $sessions
*/
class RefreshTokenFormTest extends DbTestCase {
use Specify;
public function fixtures() {
return [
'sessions' => AccountSessionFixture::class,
];
}
public function testValidateRefreshToken() {
$this->specify('error.refresh_token_not_exist if passed token not exists', function() {
/** @var RefreshTokenForm $model */
$model = new class extends RefreshTokenForm {
public function getSession() {
return null;
}
};
$model->validateRefreshToken();
expect($model->getErrors('refresh_token'))->equals(['error.refresh_token_not_exist']);
});
$this->specify('no errors if token exists', function() {
/** @var RefreshTokenForm $model */
$model = new class extends RefreshTokenForm {
public function getSession() {
return new AccountSession();
}
};
$model->validateRefreshToken();
expect($model->getErrors('refresh_token'))->isEmpty();
});
}
public function testRenew() {
$this->specify('success renew token', function() {
$model = new RefreshTokenForm();
$model->refresh_token = $this->sessions['admin']['refresh_token'];
expect($model->renew())->isInstanceOf(RenewResult::class);
});
}
}