mirror of
https://github.com/elyby/accounts.git
synced 2024-11-26 16:52:02 +05:30
Добавлен роут и логика для обновления access_token по refresh_token'у
This commit is contained in:
parent
cb038c897b
commit
1945a7baec
@ -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();
|
||||||
|
42
api/components/User/RenewResult.php
Normal file
42
api/components/User/RenewResult.php
Normal 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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
58
api/models/authentication/RefreshTokenForm.php
Normal file
58
api/models/authentication/RefreshTokenForm.php
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
32
tests/codeception/api/functional/RefreshTokenCest.php
Normal file
32
tests/codeception/api/functional/RefreshTokenCest.php
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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])))
|
||||||
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user