mirror of
https://github.com/elyby/accounts.git
synced 2024-11-06 08:11:24 +05:30
Introduce revokation mechanism
This commit is contained in:
parent
ba7fad84a0
commit
016a193263
@ -36,7 +36,7 @@ class Component extends BaseComponent {
|
|||||||
public $privateKeyPass;
|
public $privateKeyPass;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string|\Defuse\Crypto\Key
|
* @var string
|
||||||
*/
|
*/
|
||||||
public $encryptionKey;
|
public $encryptionKey;
|
||||||
|
|
||||||
|
@ -5,6 +5,8 @@ namespace api\components\User;
|
|||||||
|
|
||||||
use common\models\Account;
|
use common\models\Account;
|
||||||
use common\models\AccountSession;
|
use common\models\AccountSession;
|
||||||
|
use common\models\OauthClient;
|
||||||
|
use Webmozart\Assert\Assert;
|
||||||
use yii\web\User as YiiUserComponent;
|
use yii\web\User as YiiUserComponent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -78,6 +80,15 @@ class Component extends YiiUserComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!($mode & self::KEEP_MINECRAFT_SESSIONS)) {
|
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) {
|
foreach ($account->minecraftAccessKeys as $minecraftAccessKey) {
|
||||||
$minecraftAccessKey->delete();
|
$minecraftAccessKey->delete();
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,8 @@ namespace api\components\User;
|
|||||||
use api\components\Tokens\TokenReader;
|
use api\components\Tokens\TokenReader;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use common\models\Account;
|
use common\models\Account;
|
||||||
|
use common\models\OauthClient;
|
||||||
|
use common\models\OauthSession;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Lcobucci\JWT\Token;
|
use Lcobucci\JWT\Token;
|
||||||
use Lcobucci\JWT\ValidationData;
|
use Lcobucci\JWT\ValidationData;
|
||||||
@ -50,9 +52,25 @@ class JwtIdentity implements IdentityInterface {
|
|||||||
throw new UnauthorizedHttpException('Incorrect token');
|
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);
|
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 {
|
public function getToken(): Token {
|
||||||
return $this->token;
|
return $this->token;
|
||||||
}
|
}
|
||||||
|
@ -85,6 +85,7 @@ class AuthenticationForm extends ApiForm {
|
|||||||
$account = $loginForm->getAccount();
|
$account = $loginForm->getAccount();
|
||||||
$token = Yii::$app->tokensFactory->createForMinecraftAccount($account, $this->clientToken);
|
$token = Yii::$app->tokensFactory->createForMinecraftAccount($account, $this->clientToken);
|
||||||
$dataModel = new AuthenticateData($account, (string)$token, $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.");
|
Authserver::info("User with id = {$account->id}, username = '{$account->username}' and email = '{$account->email}' successfully logged in.");
|
||||||
|
|
||||||
|
@ -88,6 +88,7 @@ class ComponentTest extends TestCase {
|
|||||||
$component->terminateSessions($account, Component::KEEP_SITE_SESSIONS);
|
$component->terminateSessions($account, Component::KEEP_SITE_SESSIONS);
|
||||||
$this->assertEmpty($account->getMinecraftAccessKeys()->all());
|
$this->assertEmpty($account->getMinecraftAccessKeys()->all());
|
||||||
$this->assertNotEmpty($account->getSessions()->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
|
// All sessions should be removed except the current one
|
||||||
$component->terminateSessions($account, Component::KEEP_CURRENT_SESSION);
|
$component->terminateSessions($account, Component::KEEP_CURRENT_SESSION);
|
||||||
|
@ -7,6 +7,8 @@ use api\components\User\JwtIdentity;
|
|||||||
use api\tests\unit\TestCase;
|
use api\tests\unit\TestCase;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use common\tests\fixtures\AccountFixture;
|
use common\tests\fixtures\AccountFixture;
|
||||||
|
use common\tests\fixtures\OauthClientFixture;
|
||||||
|
use common\tests\fixtures\OauthSessionFixture;
|
||||||
use yii\web\UnauthorizedHttpException;
|
use yii\web\UnauthorizedHttpException;
|
||||||
|
|
||||||
class JwtIdentityTest extends TestCase {
|
class JwtIdentityTest extends TestCase {
|
||||||
@ -14,6 +16,8 @@ class JwtIdentityTest extends TestCase {
|
|||||||
public function _fixtures(): array {
|
public function _fixtures(): array {
|
||||||
return [
|
return [
|
||||||
'accounts' => AccountFixture::class,
|
'accounts' => AccountFixture::class,
|
||||||
|
'oauthClients' => OauthClientFixture::class,
|
||||||
|
'oauthSessions' => OauthSessionFixture::class,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,6 +50,14 @@ class JwtIdentityTest extends TestCase {
|
|||||||
'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1NjQ2MTc3NDIsImV4cCI6MTU2NDYxNDE0Miwic3ViIjoiZWx5fDEifQ._6hj6XUSmSLibgT9ZE1Pokf4oI9r-d6tEc1z2J-fBlr1710Qiso5yNcXqb3Z_xy7Qtemyq8jOlOZA8DvmkVBrg',
|
'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1NjQ2MTc3NDIsImV4cCI6MTU2NDYxNDE0Miwic3ViIjoiZWx5fDEifQ._6hj6XUSmSLibgT9ZE1Pokf4oI9r-d6tEc1z2J-fBlr1710Qiso5yNcXqb3Z_xy7Qtemyq8jOlOZA8DvmkVBrg',
|
||||||
'Incorrect token',
|
'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' => [
|
yield 'invalid signature' => [
|
||||||
'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1NjQ2MTA1NDIsImV4cCI6MTU2NDYxNDE0Miwic3ViIjoiZWx5fDEifQ.yth31f2PyhUkYSfBlizzUXWIgOvxxk8gNP-js0z8g1OT5rig40FPTIkgsZRctAwAAlj6QoIWW7-hxLTcSb2vmw',
|
'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1NjQ2MTA1NDIsImV4cCI6MTU2NDYxNDE0Miwic3ViIjoiZWx5fDEifQ.yth31f2PyhUkYSfBlizzUXWIgOvxxk8gNP-js0z8g1OT5rig40FPTIkgsZRctAwAAlj6QoIWW7-hxLTcSb2vmw',
|
||||||
'Incorrect token',
|
'Incorrect token',
|
||||||
|
@ -14,33 +14,33 @@ use const common\LATEST_RULES_VERSION;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Fields:
|
* Fields:
|
||||||
* @property integer $id
|
* @property int $id
|
||||||
* @property string $uuid
|
* @property string $uuid
|
||||||
* @property string $username
|
* @property string $username
|
||||||
* @property string $email
|
* @property string $email
|
||||||
* @property string $password_hash
|
* @property string $password_hash
|
||||||
* @property integer $password_hash_strategy
|
* @property int $password_hash_strategy
|
||||||
* @property string $lang
|
* @property string $lang
|
||||||
* @property integer $status
|
* @property int $status
|
||||||
* @property integer $rules_agreement_version
|
* @property int $rules_agreement_version
|
||||||
* @property string $registration_ip
|
* @property string $registration_ip
|
||||||
* @property string $otp_secret
|
* @property string $otp_secret
|
||||||
* @property integer $is_otp_enabled
|
* @property int $is_otp_enabled
|
||||||
* @property integer $created_at
|
* @property int $created_at
|
||||||
* @property integer $updated_at
|
* @property int $updated_at
|
||||||
* @property integer $password_changed_at
|
* @property int $password_changed_at
|
||||||
*
|
*
|
||||||
* Getters-setters:
|
* Getters-setters:
|
||||||
* @property-write string $password plain user's password
|
* @property-write string $password plain user's password
|
||||||
* @property-read string $profileLink link to the user's Ely.by profile
|
* @property-read string $profileLink link to the user's Ely.by profile
|
||||||
*
|
*
|
||||||
* Relations:
|
* Relations:
|
||||||
* @property EmailActivation[] $emailActivations
|
* @property-read EmailActivation[] $emailActivations
|
||||||
* @property OauthSession[] $oauthSessions
|
* @property-read OauthSession[] $oauthSessions
|
||||||
* @property OauthClient[] $oauthClients
|
* @property-read OauthClient[] $oauthClients
|
||||||
* @property UsernameHistory[] $usernameHistory
|
* @property-read UsernameHistory[] $usernameHistory
|
||||||
* @property AccountSession[] $sessions
|
* @property-read AccountSession[] $sessions
|
||||||
* @property MinecraftAccessKey[] $minecraftAccessKeys
|
* @property-read MinecraftAccessKey[] $minecraftAccessKeys
|
||||||
*
|
*
|
||||||
* Behaviors:
|
* Behaviors:
|
||||||
* @mixin TimestampBehavior
|
* @mixin TimestampBehavior
|
||||||
|
@ -31,6 +31,12 @@ class OauthClient extends ActiveRecord {
|
|||||||
|
|
||||||
public const TYPE_APPLICATION = 'application';
|
public const TYPE_APPLICATION = 'application';
|
||||||
public const TYPE_MINECRAFT_SERVER = 'minecraft-server';
|
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 {
|
public static function tableName(): string {
|
||||||
return 'oauth_clients';
|
return 'oauth_clients';
|
||||||
|
@ -12,9 +12,10 @@ use yii\db\ActiveRecord;
|
|||||||
* Fields:
|
* Fields:
|
||||||
* @property int $account_id
|
* @property int $account_id
|
||||||
* @property string $client_id
|
* @property string $client_id
|
||||||
* @property int $legacy_id
|
* @property int|null $legacy_id
|
||||||
* @property array $scopes
|
* @property array $scopes
|
||||||
* @property integer $created_at
|
* @property int $created_at
|
||||||
|
* @property int|null $revoked_at
|
||||||
*
|
*
|
||||||
* Relations:
|
* Relations:
|
||||||
* @property-read OauthClient $client
|
* @property-read OauthClient $client
|
||||||
@ -58,6 +59,7 @@ class OauthSession extends ActiveRecord {
|
|||||||
* @return array of refresh tokens (ids)
|
* @return array of refresh tokens (ids)
|
||||||
*/
|
*/
|
||||||
public function getLegacyRefreshTokens(): array {
|
public function getLegacyRefreshTokens(): array {
|
||||||
|
// TODO: it seems that this method isn't used anywhere
|
||||||
if ($this->legacy_id === null) {
|
if ($this->legacy_id === null) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
20
common/tests/fixtures/data/oauth-sessions.php
vendored
20
common/tests/fixtures/data/oauth-sessions.php
vendored
@ -6,6 +6,23 @@ return [
|
|||||||
'legacy_id' => 1,
|
'legacy_id' => 1,
|
||||||
'scopes' => null,
|
'scopes' => null,
|
||||||
'created_at' => 1479944472,
|
'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' => [
|
'banned-account-session' => [
|
||||||
'account_id' => 10,
|
'account_id' => 10,
|
||||||
@ -13,6 +30,7 @@ return [
|
|||||||
'legacy_id' => 2,
|
'legacy_id' => 2,
|
||||||
'scopes' => null,
|
'scopes' => null,
|
||||||
'created_at' => 1481421663,
|
'created_at' => 1481421663,
|
||||||
|
'revoked_at' => null,
|
||||||
],
|
],
|
||||||
'deleted-client-session' => [
|
'deleted-client-session' => [
|
||||||
'account_id' => 1,
|
'account_id' => 1,
|
||||||
@ -20,6 +38,7 @@ return [
|
|||||||
'legacy_id' => 3,
|
'legacy_id' => 3,
|
||||||
'scopes' => null,
|
'scopes' => null,
|
||||||
'created_at' => 1519510065,
|
'created_at' => 1519510065,
|
||||||
|
'revoked_at' => null,
|
||||||
],
|
],
|
||||||
'actual-deleted-client-session' => [
|
'actual-deleted-client-session' => [
|
||||||
'account_id' => 2,
|
'account_id' => 2,
|
||||||
@ -27,5 +46,6 @@ return [
|
|||||||
'legacy_id' => 4,
|
'legacy_id' => 4,
|
||||||
'scopes' => null,
|
'scopes' => null,
|
||||||
'created_at' => 1519511568,
|
'created_at' => 1519511568,
|
||||||
|
'revoked_at' => null,
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
@ -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_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->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', '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() {
|
public function safeDown() {
|
||||||
|
$this->delete('oauth_clients', ['id' => 'unauthorized_minecraft_game_launcher']);
|
||||||
|
|
||||||
|
$this->dropColumn('oauth_sessions', 'revoked_at');
|
||||||
$this->dropColumn('oauth_sessions', 'scopes');
|
$this->dropColumn('oauth_sessions', 'scopes');
|
||||||
$this->dropForeignKey('FK_oauth_session_to_oauth_client', 'oauth_sessions');
|
$this->dropForeignKey('FK_oauth_session_to_oauth_client', 'oauth_sessions');
|
||||||
$this->dropForeignKey('FK_oauth_session_to_account', 'oauth_sessions');
|
$this->dropForeignKey('FK_oauth_session_to_account', 'oauth_sessions');
|
||||||
|
Loading…
Reference in New Issue
Block a user