diff --git a/api/components/Tokens/Component.php b/api/components/Tokens/Component.php index 7294a74..c5025a2 100644 --- a/api/components/Tokens/Component.php +++ b/api/components/Tokens/Component.php @@ -36,7 +36,7 @@ class Component extends BaseComponent { public $privateKeyPass; /** - * @var string|\Defuse\Crypto\Key + * @var string */ public $encryptionKey; diff --git a/api/components/User/Component.php b/api/components/User/Component.php index bf00f9c..966601d 100644 --- a/api/components/User/Component.php +++ b/api/components/User/Component.php @@ -5,6 +5,8 @@ namespace api\components\User; use common\models\Account; use common\models\AccountSession; +use common\models\OauthClient; +use Webmozart\Assert\Assert; use yii\web\User as YiiUserComponent; /** @@ -78,6 +80,15 @@ class Component extends YiiUserComponent { } if (!($mode & self::KEEP_MINECRAFT_SESSIONS)) { + /** @var \common\models\OauthSession|null $minecraftSession */ + $minecraftSession = $account->getSessions() + ->andWhere(['client_id' => OauthClient::UNAUTHORIZED_MINECRAFT_GAME_LAUNCHER]) + ->one(); + if ($minecraftSession !== null) { + $minecraftSession->revoked_at = time(); + Assert::true($minecraftSession->save()); + } + foreach ($account->minecraftAccessKeys as $minecraftAccessKey) { $minecraftAccessKey->delete(); } diff --git a/api/components/User/JwtIdentity.php b/api/components/User/JwtIdentity.php index 34ee296..9fe214f 100644 --- a/api/components/User/JwtIdentity.php +++ b/api/components/User/JwtIdentity.php @@ -6,6 +6,8 @@ namespace api\components\User; use api\components\Tokens\TokenReader; use Carbon\Carbon; use common\models\Account; +use common\models\OauthClient; +use common\models\OauthSession; use Exception; use Lcobucci\JWT\Token; use Lcobucci\JWT\ValidationData; @@ -50,9 +52,25 @@ class JwtIdentity implements IdentityInterface { throw new UnauthorizedHttpException('Incorrect token'); } + $tokenReader = new TokenReader($token); + $accountId = $tokenReader->getAccountId(); + $iat = $token->getClaim('iat'); + if ($tokenReader->getMinecraftClientToken() !== null && self::isRevoked($accountId, OauthClient::UNAUTHORIZED_MINECRAFT_GAME_LAUNCHER, $iat)) { + throw new UnauthorizedHttpException('Token has been revoked'); + } + + if ($tokenReader->getClientId() !== null && self::isRevoked($accountId, $tokenReader->getClientId(), $iat)) { + throw new UnauthorizedHttpException('Token has been revoked'); + } + return new self($token); } + private static function isRevoked(int $accountId, string $clientId, int $iat): bool { + $session = OauthSession::findOne(['account_id' => $accountId, 'client_id' => $clientId]); + return $session !== null && $session->revoked_at !== null && $session->revoked_at > $iat; + } + public function getToken(): Token { return $this->token; } diff --git a/api/modules/authserver/models/AuthenticationForm.php b/api/modules/authserver/models/AuthenticationForm.php index 1792a0a..8e37372 100644 --- a/api/modules/authserver/models/AuthenticationForm.php +++ b/api/modules/authserver/models/AuthenticationForm.php @@ -85,6 +85,7 @@ class AuthenticationForm extends ApiForm { $account = $loginForm->getAccount(); $token = Yii::$app->tokensFactory->createForMinecraftAccount($account, $this->clientToken); $dataModel = new AuthenticateData($account, (string)$token, $this->clientToken); + // TODO: issue session in the oauth_sessions Authserver::info("User with id = {$account->id}, username = '{$account->username}' and email = '{$account->email}' successfully logged in."); diff --git a/api/tests/unit/components/User/ComponentTest.php b/api/tests/unit/components/User/ComponentTest.php index 1a2fea5..04d8a53 100644 --- a/api/tests/unit/components/User/ComponentTest.php +++ b/api/tests/unit/components/User/ComponentTest.php @@ -88,6 +88,7 @@ class ComponentTest extends TestCase { $component->terminateSessions($account, Component::KEEP_SITE_SESSIONS); $this->assertEmpty($account->getMinecraftAccessKeys()->all()); $this->assertNotEmpty($account->getSessions()->all()); + // TODO: write test about invalidating new minecraft access tokens based on JWT // All sessions should be removed except the current one $component->terminateSessions($account, Component::KEEP_CURRENT_SESSION); diff --git a/api/tests/unit/components/User/JwtIdentityTest.php b/api/tests/unit/components/User/JwtIdentityTest.php index cc91a25..3428ccd 100644 --- a/api/tests/unit/components/User/JwtIdentityTest.php +++ b/api/tests/unit/components/User/JwtIdentityTest.php @@ -7,6 +7,8 @@ use api\components\User\JwtIdentity; use api\tests\unit\TestCase; use Carbon\Carbon; use common\tests\fixtures\AccountFixture; +use common\tests\fixtures\OauthClientFixture; +use common\tests\fixtures\OauthSessionFixture; use yii\web\UnauthorizedHttpException; class JwtIdentityTest extends TestCase { @@ -14,6 +16,8 @@ class JwtIdentityTest extends TestCase { public function _fixtures(): array { return [ 'accounts' => AccountFixture::class, + 'oauthClients' => OauthClientFixture::class, + 'oauthSessions' => OauthSessionFixture::class, ]; } @@ -46,6 +50,14 @@ class JwtIdentityTest extends TestCase { 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1NjQ2MTc3NDIsImV4cCI6MTU2NDYxNDE0Miwic3ViIjoiZWx5fDEifQ._6hj6XUSmSLibgT9ZE1Pokf4oI9r-d6tEc1z2J-fBlr1710Qiso5yNcXqb3Z_xy7Qtemyq8jOlOZA8DvmkVBrg', 'Incorrect token', ]; + yield 'revoked by oauth client' => [ + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudF9pbmZvLG1pbmVjcmFmdF9zZXJ2ZXJfc2Vzc2lvbiIsImlhdCI6MTU2NDYxMDUwMCwic3ViIjoiZWx5fDEiLCJhdWQiOiJjbGllbnR8dGxhdW5jaGVyIn0.YzUzvnREEoQPu8CvU6WLdysUU0bC_xzigQPs2LK1su38uysSYgSbPzNOZYkQnvcmVLehHY-ON44x-oA8Os-9ZA', + 'Token has been revoked', + ]; + yield 'revoked by unauthorized minecraft launcher' => [ + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJlbHktc2NvcGVzIjoibWluZWNyYWZ0X3NlcnZlcl9zZXNzaW9uIiwiZWx5LWNsaWVudC10b2tlbiI6IllBTVhneTBBcEI5Z2dUL1VYNjNJaTdKcGtNd2ZwTmxaaE8yVVVEeEZ3YTFmZ2g4dksyN0RtV25vN2xqbk1pWWJwQ1VuS09YVnR2V1YrVVg1dWRQVVFsK04xY3BBZlJBL2ErZW1BZz09IiwiaWF0IjoxNTY0NjEwNTAwLCJzdWIiOiJlbHl8MSJ9.mxFgf4M1QSG4_Zd3sGoJUx9L9_XbjHd4T8-CWIVzmSPp2_9OHjq-CIFEwSwlfoz3QGN7NV0TpC8-PfRvjd93eQ', + 'Token has been revoked', + ]; yield 'invalid signature' => [ 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1NjQ2MTA1NDIsImV4cCI6MTU2NDYxNDE0Miwic3ViIjoiZWx5fDEifQ.yth31f2PyhUkYSfBlizzUXWIgOvxxk8gNP-js0z8g1OT5rig40FPTIkgsZRctAwAAlj6QoIWW7-hxLTcSb2vmw', 'Incorrect token', diff --git a/common/models/Account.php b/common/models/Account.php index fc227b8..9cf7ef0 100644 --- a/common/models/Account.php +++ b/common/models/Account.php @@ -14,33 +14,33 @@ use const common\LATEST_RULES_VERSION; /** * Fields: - * @property integer $id + * @property int $id * @property string $uuid * @property string $username * @property string $email * @property string $password_hash - * @property integer $password_hash_strategy + * @property int $password_hash_strategy * @property string $lang - * @property integer $status - * @property integer $rules_agreement_version + * @property int $status + * @property int $rules_agreement_version * @property string $registration_ip * @property string $otp_secret - * @property integer $is_otp_enabled - * @property integer $created_at - * @property integer $updated_at - * @property integer $password_changed_at + * @property int $is_otp_enabled + * @property int $created_at + * @property int $updated_at + * @property int $password_changed_at * * Getters-setters: * @property-write string $password plain user's password * @property-read string $profileLink link to the user's Ely.by profile * * Relations: - * @property EmailActivation[] $emailActivations - * @property OauthSession[] $oauthSessions - * @property OauthClient[] $oauthClients - * @property UsernameHistory[] $usernameHistory - * @property AccountSession[] $sessions - * @property MinecraftAccessKey[] $minecraftAccessKeys + * @property-read EmailActivation[] $emailActivations + * @property-read OauthSession[] $oauthSessions + * @property-read OauthClient[] $oauthClients + * @property-read UsernameHistory[] $usernameHistory + * @property-read AccountSession[] $sessions + * @property-read MinecraftAccessKey[] $minecraftAccessKeys * * Behaviors: * @mixin TimestampBehavior diff --git a/common/models/OauthClient.php b/common/models/OauthClient.php index cad84a0..260272f 100644 --- a/common/models/OauthClient.php +++ b/common/models/OauthClient.php @@ -31,6 +31,12 @@ class OauthClient extends ActiveRecord { public const TYPE_APPLICATION = 'application'; public const TYPE_MINECRAFT_SERVER = 'minecraft-server'; + public const TYPE_MINECRAFT_GAME_LAUNCHER = 'minecraft-game-launcher'; + + /** + * Abstract oauth_client, used to + */ + public const UNAUTHORIZED_MINECRAFT_GAME_LAUNCHER = 'unauthorized_minecraft_game_launcher'; public static function tableName(): string { return 'oauth_clients'; diff --git a/common/models/OauthSession.php b/common/models/OauthSession.php index aaa5c7e..bca5167 100644 --- a/common/models/OauthSession.php +++ b/common/models/OauthSession.php @@ -10,11 +10,12 @@ use yii\db\ActiveRecord; /** * Fields: - * @property int $account_id - * @property string $client_id - * @property int $legacy_id - * @property array $scopes - * @property integer $created_at + * @property int $account_id + * @property string $client_id + * @property int|null $legacy_id + * @property array $scopes + * @property int $created_at + * @property int|null $revoked_at * * Relations: * @property-read OauthClient $client @@ -58,6 +59,7 @@ class OauthSession extends ActiveRecord { * @return array of refresh tokens (ids) */ public function getLegacyRefreshTokens(): array { + // TODO: it seems that this method isn't used anywhere if ($this->legacy_id === null) { return []; } diff --git a/common/tests/fixtures/data/oauth-sessions.php b/common/tests/fixtures/data/oauth-sessions.php index 5828c20..70023bc 100644 --- a/common/tests/fixtures/data/oauth-sessions.php +++ b/common/tests/fixtures/data/oauth-sessions.php @@ -6,6 +6,23 @@ return [ 'legacy_id' => 1, 'scopes' => null, 'created_at' => 1479944472, + 'revoked_at' => null, + ], + 'revoked-tlauncher' => [ + 'account_id' => 1, + 'client_id' => 'tlauncher', + 'legacy_id' => null, + 'scopes' => null, + 'created_at' => Carbon\Carbon::create(2019, 8, 1, 0, 0, 0, 'Europe/Minsk')->unix(), + 'revoked_at' => Carbon\Carbon::create(2019, 8, 1, 1, 2, 0, 'Europe/Minsk')->unix(), + ], + 'revoked-minecraft-game-launchers' => [ + 'account_id' => 1, + 'client_id' => common\models\OauthClient::UNAUTHORIZED_MINECRAFT_GAME_LAUNCHER, + 'legacy_id' => null, + 'scopes' => null, + 'created_at' => Carbon\Carbon::create(2019, 8, 1, 0, 0, 0, 'Europe/Minsk')->unix(), + 'revoked_at' => Carbon\Carbon::create(2019, 8, 1, 1, 2, 0, 'Europe/Minsk')->unix(), ], 'banned-account-session' => [ 'account_id' => 10, @@ -13,6 +30,7 @@ return [ 'legacy_id' => 2, 'scopes' => null, 'created_at' => 1481421663, + 'revoked_at' => null, ], 'deleted-client-session' => [ 'account_id' => 1, @@ -20,6 +38,7 @@ return [ 'legacy_id' => 3, 'scopes' => null, 'created_at' => 1519510065, + 'revoked_at' => null, ], 'actual-deleted-client-session' => [ 'account_id' => 2, @@ -27,5 +46,6 @@ return [ 'legacy_id' => 4, 'scopes' => null, 'created_at' => 1519511568, + 'revoked_at' => null, ], ]; diff --git a/console/migrations/m190914_181236_rework_oauth_related_tables.php b/console/migrations/m190914_181236_rework_oauth_related_tables.php index 2cb68ac..bf26038 100644 --- a/console/migrations/m190914_181236_rework_oauth_related_tables.php +++ b/console/migrations/m190914_181236_rework_oauth_related_tables.php @@ -34,9 +34,21 @@ class m190914_181236_rework_oauth_related_tables extends Migration { $this->addForeignKey('FK_oauth_session_to_account', 'oauth_sessions', 'account_id', 'accounts', 'id', 'CASCADE', 'CASCADE'); $this->addForeignKey('FK_oauth_session_to_oauth_client', 'oauth_sessions', 'client_id', 'oauth_clients', 'id', 'CASCADE', 'CASCADE'); $this->addColumn('oauth_sessions', 'scopes', $this->json()->toString('scopes') . ' AFTER `legacy_id`'); + $this->addColumn('oauth_sessions', 'revoked_at', $this->integer(11)->unsigned() . ' AFTER `created_at`'); + + $this->insert('oauth_clients', [ + 'id' => 'unauthorized_minecraft_game_launcher', + 'secret' => 'there_is_no_secret', + 'type' => 'minecraft-game-launcher', + 'name' => 'Unauthorized Minecraft game launcher', + 'created_at' => time(), + ]); } public function safeDown() { + $this->delete('oauth_clients', ['id' => 'unauthorized_minecraft_game_launcher']); + + $this->dropColumn('oauth_sessions', 'revoked_at'); $this->dropColumn('oauth_sessions', 'scopes'); $this->dropForeignKey('FK_oauth_session_to_oauth_client', 'oauth_sessions'); $this->dropForeignKey('FK_oauth_session_to_account', 'oauth_sessions');