diff --git a/api/assets/AppAsset.php b/api/assets/AppAsset.php deleted file mode 100644 index e7e722f..0000000 --- a/api/assets/AppAsset.php +++ /dev/null @@ -1,29 +0,0 @@ - - * @since 2.0 - */ -class AppAsset extends AssetBundle -{ - public $basePath = '@webroot'; - public $baseUrl = '@web'; - public $css = [ - 'css/site.css', - ]; - public $js = [ - ]; - public $depends = [ - 'yii\web\YiiAsset', - 'yii\bootstrap\BootstrapAsset', - ]; -} diff --git a/api/controllers/OauthController.php b/api/controllers/OauthController.php index 3aa937c..c730cf1 100644 --- a/api/controllers/OauthController.php +++ b/api/controllers/OauthController.php @@ -4,7 +4,9 @@ namespace api\controllers; use common\components\oauth\Exception\AcceptRequiredException; use common\components\oauth\Exception\AccessDeniedException; use common\models\OauthClient; +use common\models\OauthScope; use League\OAuth2\Server\Exception\OAuthException; +use League\OAuth2\Server\Grant\RefreshTokenGrant; use Yii; use yii\filters\AccessControl; use yii\helpers\ArrayHelper; @@ -17,7 +19,7 @@ class OauthController extends Controller { 'class' => AccessControl::class, 'rules' => [ [ - 'actions' => ['validate'], + 'actions' => ['validate', 'issue-token'], 'allow' => true, ], [ @@ -32,8 +34,9 @@ class OauthController extends Controller { public function verbs() { return [ - 'validate' => ['GET'], - 'complete' => ['POST'], + 'validate' => ['GET'], + 'complete' => ['POST'], + 'issue-token' => ['POST'], ]; } @@ -141,20 +144,28 @@ class OauthController extends Controller { } /** - * Метод выполняется сервером приложения, которому был выдан auth_token. + * Метод выполняется сервером приложения, которому был выдан auth_token иди refresh_token. * * Входными данными является стандартный список GET параметров по стандарту oAuth: * $_GET = [ * client_id, * client_secret, * redirect_uri, - * code|refresh_token, + * code, + * grant_type, + * ] + * для запроса grant_type = authentication_code. + * $_GET = [ + * client_id, + * client_secret, + * refresh_token, * grant_type, * ] * * @return array */ public function actionIssueToken() { + $this->attachRefreshTokenGrantIfNeedle(); try { $response = $this->getServer()->issueAccessToken(); } catch (OAuthException $e) { @@ -168,6 +179,28 @@ class OauthController extends Controller { return $response; } + private function attachRefreshTokenGrantIfNeedle() { + $grantType = Yii::$app->request->post('grant_type'); + if ($grantType === 'authorization_code' && Yii::$app->request->post('code')) { + $authCode = Yii::$app->request->post('code'); + $codeModel = $this->getServer()->getAuthCodeStorage()->get($authCode); + if ($codeModel === null) { + return; + } + + $scopes = $codeModel->getScopes(); + if (array_search(OauthScope::OFFLINE_ACCESS, array_keys($scopes)) === false) { + return; + } + } elseif ($grantType === 'refresh_token') { + // Это валидный кейс + } else { + return; + } + + $this->getServer()->addGrantType(new RefreshTokenGrant()); + } + /** * @param array $queryParams * @param OauthClient $clientModel diff --git a/api/messages/.gitkeep b/api/messages/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/api/views/layouts/main.php b/api/views/layouts/main.php deleted file mode 100644 index a4da544..0000000 --- a/api/views/layouts/main.php +++ /dev/null @@ -1,77 +0,0 @@ - -beginPage() ?> - - - - - - - <?= Html::encode($this->title) ?> - head() ?> - - -beginBody() ?> - -
- 'My Company', - 'brandUrl' => Yii::$app->homeUrl, - 'options' => [ - 'class' => 'navbar-inverse navbar-fixed-top', - ], - ]); - $menuItems = [ - ['label' => 'Home', 'url' => ['site/index']], - ['label' => 'About', 'url' => ['site/about']], - ['label' => 'Contact', 'url' => ['site/contact']], - ]; - if (Yii::$app->user->isGuest) { - $menuItems[] = ['label' => 'Signup', 'url' => ['site/signup']]; - $menuItems[] = ['label' => 'Login', 'url' => ['site/login']]; - } else { - $menuItems[] = [ - 'label' => 'Logout (' . Yii::$app->user->identity->username . ')', - 'url' => ['site/logout'], - 'linkOptions' => ['data-method' => 'post'] - ]; - } - echo Nav::widget([ - 'options' => ['class' => 'navbar-nav navbar-right'], - 'items' => $menuItems, - ]); - NavBar::end(); - ?> - -
- isset($this->params['breadcrumbs']) ? $this->params['breadcrumbs'] : [], - ]) ?> - -
-
- - - -endBody() ?> - - -endPage() ?> diff --git a/api/views/site/about.php b/api/views/site/about.php deleted file mode 100644 index 8eb0764..0000000 --- a/api/views/site/about.php +++ /dev/null @@ -1,16 +0,0 @@ -title = 'About'; -$this->params['breadcrumbs'][] = $this->title; -?> -
-

title) ?>

- -

This is the About page. You may modify the following file to customize its content:

- - -
diff --git a/api/views/site/contact.php b/api/views/site/contact.php deleted file mode 100644 index be11983..0000000 --- a/api/views/site/contact.php +++ /dev/null @@ -1,45 +0,0 @@ -title = 'Contact'; -$this->params['breadcrumbs'][] = $this->title; -?> -
-

title) ?>

- -

- If you have business inquiries or other questions, please fill out the following form to contact us. Thank you. -

- -
-
- 'contact-form']); ?> - - field($model, 'name') ?> - - field($model, 'email') ?> - - field($model, 'subject') ?> - - field($model, 'body')->textArea(['rows' => 6]) ?> - - field($model, 'verifyCode')->widget(Captcha::className(), [ - 'template' => '
{image}
{input}
', - ]) ?> - -
- 'btn btn-primary', 'name' => 'contact-button']) ?> -
- - -
-
- -
diff --git a/api/views/site/error.php b/api/views/site/error.php deleted file mode 100644 index 0ba2574..0000000 --- a/api/views/site/error.php +++ /dev/null @@ -1,27 +0,0 @@ -title = $name; -?> -
- -

title) ?>

- -
- -
- -

- The above error occurred while the Web server was processing your request. -

-

- Please contact us if you think this is a server error. Thank you. -

- -
diff --git a/api/views/site/index.php b/api/views/site/index.php deleted file mode 100644 index f780610..0000000 --- a/api/views/site/index.php +++ /dev/null @@ -1,53 +0,0 @@ -title = 'My Yii Application'; -?> -
- -
-

Congratulations!

- -

You have successfully created your Yii-powered application.

- -

Get started with Yii

-
- -
- -
-
-

Heading

- -

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et - dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip - ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu - fugiat nulla pariatur.

- -

Yii Documentation »

-
-
-

Heading

- -

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et - dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip - ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu - fugiat nulla pariatur.

- -

Yii Forum »

-
-
-

Heading

- -

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et - dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip - ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu - fugiat nulla pariatur.

- -

Yii Extensions »

-
-
- -
-
diff --git a/api/views/site/requestPasswordResetToken.php b/api/views/site/requestPasswordResetToken.php deleted file mode 100644 index a687530..0000000 --- a/api/views/site/requestPasswordResetToken.php +++ /dev/null @@ -1,31 +0,0 @@ -title = 'Request password reset'; -$this->params['breadcrumbs'][] = $this->title; -?> -
-

title) ?>

- -

Please fill out your email. A link to reset password will be sent there.

- -
-
- 'request-password-reset-form']); ?> - - field($model, 'email') ?> - -
- 'btn btn-primary']) ?> -
- - -
-
-
diff --git a/api/views/site/resetPassword.php b/api/views/site/resetPassword.php deleted file mode 100644 index 6818ca9..0000000 --- a/api/views/site/resetPassword.php +++ /dev/null @@ -1,31 +0,0 @@ -title = 'Reset password'; -$this->params['breadcrumbs'][] = $this->title; -?> -
-

title) ?>

- -

Please choose your new password:

- -
-
- 'reset-password-form']); ?> - - field($model, 'password')->passwordInput() ?> - -
- 'btn btn-primary']) ?> -
- - -
-
-
diff --git a/api/views/site/signup.php b/api/views/site/signup.php deleted file mode 100644 index bbaffc5..0000000 --- a/api/views/site/signup.php +++ /dev/null @@ -1,35 +0,0 @@ -title = 'Signup'; -$this->params['breadcrumbs'][] = $this->title; -?> -
-

title) ?>

- -

Please fill out the following fields to signup:

- -
-
- 'form-signup']); ?> - - field($model, 'username') ?> - - field($model, 'email') ?> - - field($model, 'password')->passwordInput() ?> - -
- 'btn btn-primary', 'name' => 'signup-button']) ?> -
- - -
-
-
diff --git a/common/components/oauth/Component.php b/common/components/oauth/Component.php index 9fe54a3..9844817 100644 --- a/common/components/oauth/Component.php +++ b/common/components/oauth/Component.php @@ -2,6 +2,7 @@ namespace common\components\oauth; use common\components\oauth\Storage\Redis\AuthCodeStorage; +use common\components\oauth\Storage\Redis\RefreshTokenStorage; use common\components\oauth\Storage\Yii2\AccessTokenStorage; use common\components\oauth\Storage\Yii2\ClientStorage; use common\components\oauth\Storage\Yii2\ScopeStorage; @@ -43,6 +44,7 @@ class Component extends \yii\base\Component { ->setScopeStorage(new ScopeStorage()) ->setSessionStorage(new SessionStorage()) ->setAuthCodeStorage(new AuthCodeStorage()) + ->setRefreshTokenStorage(new RefreshTokenStorage()) ->setScopeDelimiter(','); $this->_authServer = $authServer; diff --git a/common/components/oauth/Entity/AccessTokenEntity.php b/common/components/oauth/Entity/AccessTokenEntity.php index 3501d82..bd70930 100644 --- a/common/components/oauth/Entity/AccessTokenEntity.php +++ b/common/components/oauth/Entity/AccessTokenEntity.php @@ -2,7 +2,7 @@ namespace common\components\oauth\Entity; use League\OAuth2\Server\Entity\EntityTrait; -use League\OAuth2\Server\Entity\SessionEntity; +use League\OAuth2\Server\Entity\SessionEntity as OriginalSessionEntity; class AccessTokenEntity extends \League\OAuth2\Server\Entity\AccessTokenEntity { use EntityTrait; @@ -17,7 +17,7 @@ class AccessTokenEntity extends \League\OAuth2\Server\Entity\AccessTokenEntity { * @inheritdoc * @return static */ - public function setSession(SessionEntity $session) { + public function setSession(OriginalSessionEntity $session) { parent::setSession($session); $this->sessionId = $session->getId(); diff --git a/common/components/oauth/Storage/Redis/AuthCodeStorage.php b/common/components/oauth/Storage/Redis/AuthCodeStorage.php index 8e75e7f..204027d 100644 --- a/common/components/oauth/Storage/Redis/AuthCodeStorage.php +++ b/common/components/oauth/Storage/Redis/AuthCodeStorage.php @@ -32,7 +32,7 @@ class AuthCodeStorage extends AbstractStorage implements AuthCodeInterface { 'id' => $result['id'], 'redirectUri' => $result['client_redirect_uri'], 'expireTime' => $result['expire_time'], - 'sessionId' => $result['sessionId'], + 'sessionId' => $result['session_id'], ]); } diff --git a/common/components/oauth/Storage/Redis/RefreshTokenStorage.php b/common/components/oauth/Storage/Redis/RefreshTokenStorage.php index 2736f78..f3ad9e0 100644 --- a/common/components/oauth/Storage/Redis/RefreshTokenStorage.php +++ b/common/components/oauth/Storage/Redis/RefreshTokenStorage.php @@ -1,5 +1,5 @@ cache[$token])) { + if (!isset($this->cache[$token])) { $this->cache[$token] = OauthAccessToken::findOne($token); } diff --git a/common/components/redis/Key.php b/common/components/redis/Key.php index 1b1f951..aaea4ff 100644 --- a/common/components/redis/Key.php +++ b/common/components/redis/Key.php @@ -20,7 +20,7 @@ class Key { } public function getValue() { - return $this->getRedis()->get(json_decode($this->key)); + return json_decode($this->getRedis()->get($this->key), true); } public function setValue($value) { diff --git a/common/models/OauthScope.php b/common/models/OauthScope.php index cfa3acc..74d3901 100644 --- a/common/models/OauthScope.php +++ b/common/models/OauthScope.php @@ -10,6 +10,9 @@ use yii\db\ActiveRecord; */ class OauthScope extends ActiveRecord { + const OFFLINE_ACCESS = 'offline_access'; + const MINECRAFT_SERVER_SESSION = 'minecraft_server_session'; + public static function tableName() { return '{{%oauth_scopes}}'; } diff --git a/common/models/OauthSession.php b/common/models/OauthSession.php index e438287..a74032c 100644 --- a/common/models/OauthSession.php +++ b/common/models/OauthSession.php @@ -25,7 +25,7 @@ class OauthSession extends ActiveRecord { return '{{%oauth_sessions}}'; } - public function getOauthAccessTokens() { + public function getAccessTokens() { return $this->hasMany(OauthAccessToken::class, ['session_id' => 'id']); } diff --git a/console/migrations/m160222_204006_add_init_scopes.php b/console/migrations/m160222_204006_add_init_scopes.php new file mode 100644 index 0000000..8201196 --- /dev/null +++ b/console/migrations/m160222_204006_add_init_scopes.php @@ -0,0 +1,18 @@ +batchInsert('{{%oauth_scopes}}', ['id'], [ + ['offline_access'], + ['minecraft_server_session'], + ]); + } + + public function safeDown() { + $this->delete('{{%oauth_scopes}}'); + } + +} diff --git a/tests/codeception/api/_pages/OauthRoute.php b/tests/codeception/api/_pages/OauthRoute.php index 4356928..0a4118a 100644 --- a/tests/codeception/api/_pages/OauthRoute.php +++ b/tests/codeception/api/_pages/OauthRoute.php @@ -18,4 +18,9 @@ class OauthRoute extends BasePage { $this->actor->sendPOST($this->getUrl($queryParams), $postParams); } + public function issueToken($postParams = []) { + $this->route = ['oauth/issue-token']; + $this->actor->sendPOST($this->getUrl(), $postParams); + } + } diff --git a/tests/codeception/api/functional/OauthAccessTokenCest.php b/tests/codeception/api/functional/OauthAccessTokenCest.php new file mode 100644 index 0000000..3d92717 --- /dev/null +++ b/tests/codeception/api/functional/OauthAccessTokenCest.php @@ -0,0 +1,99 @@ +route = new OauthRoute($I); + } + + public function testIssueTokenWithWrongArgs(FunctionalTester $I) { + $I->wantTo('check behavior on on request without any credentials'); + $this->route->issueToken(); + $I->canSeeResponseCodeIs(400); + $I->canSeeResponseContainsJson([ + 'error' => 'invalid_request', + ]); + + $I->wantTo('check behavior on passing invalid auth code'); + $this->route->issueToken($this->buildParams( + 'wrong-auth-code', + 'ely', + 'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM', + 'http://ely.by' + )); + $I->canSeeResponseCodeIs(400); + $I->canSeeResponseContainsJson([ + 'error' => 'invalid_request', + ]); + } + + public function testIssueToken(FunctionalTester $I, Scenario $scenario) { + $I = new OauthSteps($scenario); + $authCode = $I->getAuthCode(); + $this->route->issueToken($this->buildParams( + $authCode, + 'ely', + 'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM', + 'http://ely.by' + )); + $I->canSeeResponseCodeIs(200); + $I->canSeeResponseIsJson(); + $I->canSeeResponseContainsJson([ + 'token_type' => 'Bearer', + ]); + $I->canSeeResponseJsonMatchesJsonPath('$.access_token'); + $I->canSeeResponseJsonMatchesJsonPath('$.expires_in'); + } + + public function testIssueTokenWithRefreshToken(FunctionalTester $I, Scenario $scenario) { + $I = new OauthSteps($scenario); + $authCode = $I->getAuthCode(false); + $this->route->issueToken($this->buildParams( + $authCode, + 'ely', + 'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM', + 'http://ely.by' + )); + $I->canSeeResponseCodeIs(200); + $I->canSeeResponseIsJson(); + $I->canSeeResponseContainsJson([ + 'token_type' => 'Bearer', + ]); + $I->canSeeResponseJsonMatchesJsonPath('$.access_token'); + $I->canSeeResponseJsonMatchesJsonPath('$.refresh_token'); + $I->canSeeResponseJsonMatchesJsonPath('$.expires_in'); + } + + private function buildParams($code = null, $clientId = null, $clientSecret = null, $redirectUri = null) { + $params = ['grant_type' => 'authorization_code']; + if ($code !== null) { + $params['code'] = $code; + } + + if ($clientId !== null) { + $params['client_id'] = $clientId; + } + + if ($clientSecret !== null) { + $params['client_secret'] = $clientSecret; + } + + if ($redirectUri !== null) { + $params['redirect_uri'] = $redirectUri; + } + + return $params; + } + +} diff --git a/tests/codeception/api/functional/OauthCest.php b/tests/codeception/api/functional/OauthAuthCodeCest.php similarity index 99% rename from tests/codeception/api/functional/OauthCest.php rename to tests/codeception/api/functional/OauthAuthCodeCest.php index bd84ef1..34d0001 100644 --- a/tests/codeception/api/functional/OauthCest.php +++ b/tests/codeception/api/functional/OauthAuthCodeCest.php @@ -3,8 +3,9 @@ namespace tests\codeception\api; use tests\codeception\api\_pages\OauthRoute; use tests\codeception\api\functional\_steps\AccountSteps; +use Yii; -class OauthCest { +class OauthAuthCodeCest { /** * @var OauthRoute diff --git a/tests/codeception/api/functional/OauthRefreshTokenCest.php b/tests/codeception/api/functional/OauthRefreshTokenCest.php new file mode 100644 index 0000000..7068587 --- /dev/null +++ b/tests/codeception/api/functional/OauthRefreshTokenCest.php @@ -0,0 +1,99 @@ +route = new OauthRoute($I); + } + + public function testRefreshToken(FunctionalTester $I, Scenario $scenario) { + $I = new OauthSteps($scenario); + $refreshToken = $I->getRefreshToken(); + $this->route->issueToken($this->buildParams( + $refreshToken, + 'ely', + 'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM' + )); + $I->canSeeResponseCodeIs(200); + $I->canSeeResponseIsJson(); + $I->canSeeResponseContainsJson([ + 'token_type' => 'Bearer', + ]); + $I->canSeeResponseJsonMatchesJsonPath('$.access_token'); + $I->canSeeResponseJsonMatchesJsonPath('$.refresh_token'); + $I->canSeeResponseJsonMatchesJsonPath('$.expires_in'); + } + + public function testRefreshTokenWithSameScopes(FunctionalTester $I, Scenario $scenario) { + $I = new OauthSteps($scenario); + $refreshToken = $I->getRefreshToken(); + $this->route->issueToken($this->buildParams( + $refreshToken, + 'ely', + 'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM', + [OauthScope::MINECRAFT_SERVER_SESSION, OauthScope::OFFLINE_ACCESS] + )); + $I->canSeeResponseCodeIs(200); + $I->canSeeResponseIsJson(); + $I->canSeeResponseContainsJson([ + 'token_type' => 'Bearer', + ]); + $I->canSeeResponseJsonMatchesJsonPath('$.access_token'); + $I->canSeeResponseJsonMatchesJsonPath('$.refresh_token'); + $I->canSeeResponseJsonMatchesJsonPath('$.expires_in'); + } + + public function testRefreshTokenWithNewScopes(FunctionalTester $I, Scenario $scenario) { + $I = new OauthSteps($scenario); + $refreshToken = $I->getRefreshToken(); + $this->route->issueToken($this->buildParams( + $refreshToken, + 'ely', + 'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM', + [OauthScope::MINECRAFT_SERVER_SESSION, OauthScope::OFFLINE_ACCESS, 'change_skin'] + )); + $I->canSeeResponseCodeIs(400); + $I->canSeeResponseIsJson(); + $I->canSeeResponseContainsJson([ + 'error' => 'invalid_scope', + ]); + } + + private function buildParams($refreshToken = null, $clientId = null, $clientSecret = null, $scopes = []) { + $params = ['grant_type' => 'refresh_token']; + if ($refreshToken !== null) { + $params['refresh_token'] = $refreshToken; + } + + if ($clientId !== null) { + $params['client_id'] = $clientId; + } + + if ($clientSecret !== null) { + $params['client_secret'] = $clientSecret; + } + + if (!empty($scopes)) { + if (is_array($scopes)) { + $scopes = implode(',', $scopes); + } + + $params['scope'] = $scopes; + } + + return $params; + } + +} diff --git a/tests/codeception/api/functional/_steps/OauthSteps.php b/tests/codeception/api/functional/_steps/OauthSteps.php new file mode 100644 index 0000000..d303c63 --- /dev/null +++ b/tests/codeception/api/functional/_steps/OauthSteps.php @@ -0,0 +1,42 @@ +loggedInAsActiveAccount(); + $route = new OauthRoute($this); + $route->complete([ + 'client_id' => 'ely', + 'redirect_uri' => 'http://ely.by', + 'response_type' => 'code', + 'scope' => 'minecraft_server_session' . ($online ? '' : ',offline_access'), + ], ['accept' => true]); + $this->canSeeResponseJsonMatchesJsonPath('$.redirectUri'); + $response = json_decode($this->grabResponse(), true); + preg_match('/code=(\w+)/', $response['redirectUri'], $matches); + + return $matches[1]; + } + + public function getRefreshToken() { + // TODO: по идее можно напрямую сделать зпись в базу, что ускорит процесс тестирования + $authCode = $this->getAuthCode(false); + $route = new OauthRoute($this); + $route->issueToken([ + 'code' => $authCode, + 'client_id' => 'ely', + 'client_secret' => 'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM', + 'redirect_uri' => 'http://ely.by', + 'grant_type' => 'authorization_code', + ]); + + $response = json_decode($this->grabResponse(), true); + + return $response['refresh_token']; + } + +} diff --git a/tests/codeception/common/fixtures/data/oauth-scopes.php b/tests/codeception/common/fixtures/data/oauth-scopes.php index dc2ef34..6d16ab5 100644 --- a/tests/codeception/common/fixtures/data/oauth-scopes.php +++ b/tests/codeception/common/fixtures/data/oauth-scopes.php @@ -6,4 +6,7 @@ return [ 'change_skin' => [ 'id' => 'change_skin', ], + 'offline_access' => [ + 'id' => 'offline_access', + ], ];