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
|
||||
namespace api\components\User;
|
||||
|
||||
use api\models\AccountIdentity;
|
||||
use common\models\AccountSession;
|
||||
use Emarref\Jwt\Algorithm\Hs256;
|
||||
use Emarref\Jwt\Claim;
|
||||
@ -64,6 +65,30 @@ class Component extends YiiUserComponent {
|
||||
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) {
|
||||
$jwt = new Jwt();
|
||||
$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\LoginForm;
|
||||
use api\models\authentication\RecoverPasswordForm;
|
||||
use api\models\authentication\RefreshTokenForm;
|
||||
use common\helpers\StringHelper;
|
||||
use Yii;
|
||||
use yii\filters\AccessControl;
|
||||
@ -14,13 +15,13 @@ class AuthenticationController extends Controller {
|
||||
public function behaviors() {
|
||||
return ArrayHelper::merge(parent::behaviors(), [
|
||||
'authenticator' => [
|
||||
'except' => ['login', 'forgot-password', 'recover-password'],
|
||||
'except' => ['login', 'forgot-password', 'recover-password', 'refresh-token'],
|
||||
],
|
||||
'access' => [
|
||||
'class' => AccessControl::class,
|
||||
'rules' => [
|
||||
[
|
||||
'actions' => ['login', 'forgot-password', 'recover-password'],
|
||||
'actions' => ['login', 'forgot-password', 'recover-password', 'refresh-token'],
|
||||
'allow' => true,
|
||||
'roles' => ['?'],
|
||||
],
|
||||
@ -34,6 +35,7 @@ class AuthenticationController extends Controller {
|
||||
'login' => ['POST'],
|
||||
'forgot-password' => ['POST'],
|
||||
'recover-password' => ['POST'],
|
||||
'refresh-token' => ['POST'],
|
||||
];
|
||||
}
|
||||
|
||||
@ -109,4 +111,19 @@ class AuthenticationController extends Controller {
|
||||
], $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 integer $last_used_ip
|
||||
* @property integer $created_at
|
||||
* @property integer $last_refreshed
|
||||
* @property integer $last_refreshed_at
|
||||
*
|
||||
* Отношения:
|
||||
* @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\LoginResult;
|
||||
use api\components\User\RenewResult;
|
||||
use api\models\AccountIdentity;
|
||||
use Codeception\Specify;
|
||||
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() {
|
||||
$this->specify('get string, contained jwt token', function() {
|
||||
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