mirror of
https://github.com/elyby/accounts.git
synced 2024-09-18 01:25:35 +05:30
Fix revokation validation. Add additional tests cases
This commit is contained in:
parent
016a193263
commit
d27070630c
@ -81,7 +81,7 @@ class Component extends YiiUserComponent {
|
|||||||
|
|
||||||
if (!($mode & self::KEEP_MINECRAFT_SESSIONS)) {
|
if (!($mode & self::KEEP_MINECRAFT_SESSIONS)) {
|
||||||
/** @var \common\models\OauthSession|null $minecraftSession */
|
/** @var \common\models\OauthSession|null $minecraftSession */
|
||||||
$minecraftSession = $account->getSessions()
|
$minecraftSession = $account->getOauthSessions()
|
||||||
->andWhere(['client_id' => OauthClient::UNAUTHORIZED_MINECRAFT_GAME_LAUNCHER])
|
->andWhere(['client_id' => OauthClient::UNAUTHORIZED_MINECRAFT_GAME_LAUNCHER])
|
||||||
->one();
|
->one();
|
||||||
if ($minecraftSession !== null) {
|
if ($minecraftSession !== null) {
|
||||||
|
@ -54,23 +54,24 @@ class JwtIdentity implements IdentityInterface {
|
|||||||
|
|
||||||
$tokenReader = new TokenReader($token);
|
$tokenReader = new TokenReader($token);
|
||||||
$accountId = $tokenReader->getAccountId();
|
$accountId = $tokenReader->getAccountId();
|
||||||
|
if ($accountId !== null) {
|
||||||
$iat = $token->getClaim('iat');
|
$iat = $token->getClaim('iat');
|
||||||
if ($tokenReader->getMinecraftClientToken() !== null && self::isRevoked($accountId, OauthClient::UNAUTHORIZED_MINECRAFT_GAME_LAUNCHER, $iat)) {
|
if ($tokenReader->getMinecraftClientToken() !== null
|
||||||
|
&& self::isRevoked($accountId, OauthClient::UNAUTHORIZED_MINECRAFT_GAME_LAUNCHER, $iat)
|
||||||
|
) {
|
||||||
throw new UnauthorizedHttpException('Token has been revoked');
|
throw new UnauthorizedHttpException('Token has been revoked');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($tokenReader->getClientId() !== null && self::isRevoked($accountId, $tokenReader->getClientId(), $iat)) {
|
if ($tokenReader->getClientId() !== null
|
||||||
|
&& self::isRevoked($accountId, $tokenReader->getClientId(), $iat)
|
||||||
|
) {
|
||||||
throw new UnauthorizedHttpException('Token has been revoked');
|
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;
|
||||||
}
|
}
|
||||||
@ -100,6 +101,11 @@ class JwtIdentity implements IdentityInterface {
|
|||||||
throw new NotSupportedException('This method used for cookie auth, except we using Bearer auth');
|
throw new NotSupportedException('This method used for cookie auth, except we using Bearer auth');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
// @codeCoverageIgnoreEnd
|
// @codeCoverageIgnoreEnd
|
||||||
|
|
||||||
private function getReader(): TokenReader {
|
private function getReader(): TokenReader {
|
||||||
|
@ -9,8 +9,12 @@ use api\modules\authserver\exceptions\ForbiddenOperationException;
|
|||||||
use api\modules\authserver\Module as Authserver;
|
use api\modules\authserver\Module as Authserver;
|
||||||
use api\modules\authserver\validators\ClientTokenValidator;
|
use api\modules\authserver\validators\ClientTokenValidator;
|
||||||
use api\modules\authserver\validators\RequiredValidator;
|
use api\modules\authserver\validators\RequiredValidator;
|
||||||
|
use api\rbac\Permissions as P;
|
||||||
use common\helpers\Error as E;
|
use common\helpers\Error as E;
|
||||||
use common\models\Account;
|
use common\models\Account;
|
||||||
|
use common\models\OauthClient;
|
||||||
|
use common\models\OauthSession;
|
||||||
|
use Webmozart\Assert\Assert;
|
||||||
use Yii;
|
use Yii;
|
||||||
|
|
||||||
class AuthenticationForm extends ApiForm {
|
class AuthenticationForm extends ApiForm {
|
||||||
@ -85,7 +89,17 @@ 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
|
/** @var OauthSession|null $minecraftOauthSession */
|
||||||
|
$hasMinecraftOauthSession = $account->getOauthSessions()
|
||||||
|
->andWhere(['client_id' => OauthClient::UNAUTHORIZED_MINECRAFT_GAME_LAUNCHER])
|
||||||
|
->exists();
|
||||||
|
if ($hasMinecraftOauthSession === false) {
|
||||||
|
$minecraftOauthSession = new OauthSession();
|
||||||
|
$minecraftOauthSession->account_id = $account->id;
|
||||||
|
$minecraftOauthSession->client_id = OauthClient::UNAUTHORIZED_MINECRAFT_GAME_LAUNCHER;
|
||||||
|
$minecraftOauthSession->scopes = [P::MINECRAFT_SERVER_SESSION];
|
||||||
|
Assert::true($minecraftOauthSession->save());
|
||||||
|
}
|
||||||
|
|
||||||
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.");
|
||||||
|
|
||||||
|
@ -10,6 +10,9 @@ use api\modules\authserver\validators\AccessTokenValidator;
|
|||||||
use api\modules\authserver\validators\RequiredValidator;
|
use api\modules\authserver\validators\RequiredValidator;
|
||||||
use common\models\Account;
|
use common\models\Account;
|
||||||
use common\models\MinecraftAccessKey;
|
use common\models\MinecraftAccessKey;
|
||||||
|
use common\models\OauthClient;
|
||||||
|
use common\models\OauthSession;
|
||||||
|
use Webmozart\Assert\Assert;
|
||||||
use Yii;
|
use Yii;
|
||||||
|
|
||||||
class RefreshTokenForm extends ApiForm {
|
class RefreshTokenForm extends ApiForm {
|
||||||
@ -68,6 +71,19 @@ class RefreshTokenForm extends ApiForm {
|
|||||||
|
|
||||||
$token = Yii::$app->tokensFactory->createForMinecraftAccount($account, $this->clientToken);
|
$token = Yii::$app->tokensFactory->createForMinecraftAccount($account, $this->clientToken);
|
||||||
|
|
||||||
|
// TODO: This behavior duplicates with the AuthenticationForm. Need to find a way to avoid duplication.
|
||||||
|
/** @var OauthSession|null $minecraftOauthSession */
|
||||||
|
$hasMinecraftOauthSession = $account->getOauthSessions()
|
||||||
|
->andWhere(['client_id' => OauthClient::UNAUTHORIZED_MINECRAFT_GAME_LAUNCHER])
|
||||||
|
->exists();
|
||||||
|
if ($hasMinecraftOauthSession === false) {
|
||||||
|
$minecraftOauthSession = new OauthSession();
|
||||||
|
$minecraftOauthSession->account_id = $account->id;
|
||||||
|
$minecraftOauthSession->client_id = OauthClient::UNAUTHORIZED_MINECRAFT_GAME_LAUNCHER;
|
||||||
|
$minecraftOauthSession->scopes = [P::MINECRAFT_SERVER_SESSION];
|
||||||
|
Assert::true($minecraftOauthSession->save());
|
||||||
|
}
|
||||||
|
|
||||||
return new AuthenticateData($account, (string)$token, $this->clientToken);
|
return new AuthenticateData($account, (string)$token, $this->clientToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,7 +20,6 @@ use Yii;
|
|||||||
|
|
||||||
class OauthProcess {
|
class OauthProcess {
|
||||||
|
|
||||||
// TODO: merge this with PublicScopesRepository
|
|
||||||
private const INTERNAL_PERMISSIONS_TO_PUBLIC_SCOPES = [
|
private const INTERNAL_PERMISSIONS_TO_PUBLIC_SCOPES = [
|
||||||
P::OBTAIN_OWN_ACCOUNT_INFO => 'account_info',
|
P::OBTAIN_OWN_ACCOUNT_INFO => 'account_info',
|
||||||
P::OBTAIN_ACCOUNT_EMAIL => 'account_email',
|
P::OBTAIN_ACCOUNT_EMAIL => 'account_email',
|
||||||
@ -325,12 +324,7 @@ class OauthProcess {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private function createAcceptRequiredException(): OAuthServerException {
|
private function createAcceptRequiredException(): OAuthServerException {
|
||||||
return new OAuthServerException(
|
return new OAuthServerException('Client must accept authentication request.', 0, 'accept_required', 401);
|
||||||
'Client must accept authentication request.',
|
|
||||||
0,
|
|
||||||
'accept_required',
|
|
||||||
401
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getScopesList(AuthorizationRequest $request): array {
|
private function getScopesList(AuthorizationRequest $request): array {
|
||||||
|
@ -9,9 +9,12 @@ use api\components\User\LegacyOAuth2Identity;
|
|||||||
use api\tests\unit\TestCase;
|
use api\tests\unit\TestCase;
|
||||||
use common\models\Account;
|
use common\models\Account;
|
||||||
use common\models\AccountSession;
|
use common\models\AccountSession;
|
||||||
|
use common\models\OauthClient;
|
||||||
use common\tests\fixtures\AccountFixture;
|
use common\tests\fixtures\AccountFixture;
|
||||||
use common\tests\fixtures\AccountSessionFixture;
|
use common\tests\fixtures\AccountSessionFixture;
|
||||||
use common\tests\fixtures\MinecraftAccessKeyFixture;
|
use common\tests\fixtures\MinecraftAccessKeyFixture;
|
||||||
|
use common\tests\fixtures\OauthClientFixture;
|
||||||
|
use common\tests\fixtures\OauthSessionFixture;
|
||||||
use Lcobucci\JWT\Claim\Basic;
|
use Lcobucci\JWT\Claim\Basic;
|
||||||
use Lcobucci\JWT\Token;
|
use Lcobucci\JWT\Token;
|
||||||
|
|
||||||
@ -32,6 +35,8 @@ class ComponentTest extends TestCase {
|
|||||||
'accounts' => AccountFixture::class,
|
'accounts' => AccountFixture::class,
|
||||||
'sessions' => AccountSessionFixture::class,
|
'sessions' => AccountSessionFixture::class,
|
||||||
'minecraftSessions' => MinecraftAccessKeyFixture::class,
|
'minecraftSessions' => MinecraftAccessKeyFixture::class,
|
||||||
|
'oauthClients' => OauthClientFixture::class,
|
||||||
|
'oauthSessions' => OauthSessionFixture::class,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,7 +93,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
|
$this->assertEqualsWithDelta(time(), $account->getOauthSessions()->andWhere(['client_id' => OauthClient::UNAUTHORIZED_MINECRAFT_GAME_LAUNCHER])->one()->revoked_at, 5);
|
||||||
|
|
||||||
// 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);
|
||||||
|
@ -5,14 +5,12 @@ namespace codeception\api\unit\models\authentication;
|
|||||||
|
|
||||||
use api\models\authentication\RefreshTokenForm;
|
use api\models\authentication\RefreshTokenForm;
|
||||||
use api\tests\unit\TestCase;
|
use api\tests\unit\TestCase;
|
||||||
use Codeception\Specify;
|
|
||||||
use common\models\AccountSession;
|
use common\models\AccountSession;
|
||||||
use common\tests\fixtures\AccountSessionFixture;
|
use common\tests\fixtures\AccountSessionFixture;
|
||||||
use Yii;
|
use Yii;
|
||||||
use yii\web\Request;
|
use yii\web\Request;
|
||||||
|
|
||||||
class RefreshTokenFormTest extends TestCase {
|
class RefreshTokenFormTest extends TestCase {
|
||||||
use Specify;
|
|
||||||
|
|
||||||
public function _fixtures(): array {
|
public function _fixtures(): array {
|
||||||
return [
|
return [
|
||||||
@ -21,9 +19,8 @@ class RefreshTokenFormTest extends TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function testRenew() {
|
public function testRenew() {
|
||||||
/** @var Request|\Mockery\MockInterface $request */
|
$request = $this->createPartialMock(Request::class, ['getUserIP']);
|
||||||
$request = mock(Request::class . '[getUserIP]')->makePartial();
|
$request->method('getUserIP')->willReturn('10.1.2.3');
|
||||||
$request->shouldReceive('getUserIP')->andReturn('10.1.2.3');
|
|
||||||
Yii::$app->set('request', $request);
|
Yii::$app->set('request', $request);
|
||||||
|
|
||||||
$model = new RefreshTokenForm();
|
$model = new RefreshTokenForm();
|
||||||
|
@ -6,7 +6,10 @@ namespace codeception\api\unit\modules\authserver\models;
|
|||||||
use api\modules\authserver\exceptions\ForbiddenOperationException;
|
use api\modules\authserver\exceptions\ForbiddenOperationException;
|
||||||
use api\modules\authserver\models\AuthenticationForm;
|
use api\modules\authserver\models\AuthenticationForm;
|
||||||
use api\tests\unit\TestCase;
|
use api\tests\unit\TestCase;
|
||||||
|
use common\models\OauthClient;
|
||||||
|
use common\models\OauthSession;
|
||||||
use common\tests\fixtures\AccountFixture;
|
use common\tests\fixtures\AccountFixture;
|
||||||
|
use common\tests\fixtures\OauthClientFixture;
|
||||||
use Ramsey\Uuid\Uuid;
|
use Ramsey\Uuid\Uuid;
|
||||||
|
|
||||||
class AuthenticationFormTest extends TestCase {
|
class AuthenticationFormTest extends TestCase {
|
||||||
@ -14,6 +17,7 @@ class AuthenticationFormTest extends TestCase {
|
|||||||
public function _fixtures(): array {
|
public function _fixtures(): array {
|
||||||
return [
|
return [
|
||||||
'accounts' => AccountFixture::class,
|
'accounts' => AccountFixture::class,
|
||||||
|
'oauthClients' => OauthClientFixture::class,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,14 +32,18 @@ class AuthenticationFormTest extends TestCase {
|
|||||||
$this->assertSame('df936908-b2e1-544d-96f8-2977ec213022', $result['selectedProfile']['id']);
|
$this->assertSame('df936908-b2e1-544d-96f8-2977ec213022', $result['selectedProfile']['id']);
|
||||||
$this->assertSame('Admin', $result['selectedProfile']['name']);
|
$this->assertSame('Admin', $result['selectedProfile']['name']);
|
||||||
$this->assertFalse($result['selectedProfile']['legacy']);
|
$this->assertFalse($result['selectedProfile']['legacy']);
|
||||||
|
$this->assertTrue(OauthSession::find()->andWhere([
|
||||||
|
'account_id' => 1,
|
||||||
|
'client_id' => OauthClient::UNAUTHORIZED_MINECRAFT_GAME_LAUNCHER,
|
||||||
|
])->exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @dataProvider getInvalidCredentialsCases
|
* @dataProvider getInvalidCredentialsCases
|
||||||
*/
|
*/
|
||||||
public function testAuthenticateByWrongNicknamePass(string $expectedFieldError, string $login, string $password) {
|
public function testAuthenticateByWrongNicknamePass(string $expectedExceptionMessage, string $login, string $password) {
|
||||||
$this->expectException(ForbiddenOperationException::class);
|
$this->expectException(ForbiddenOperationException::class);
|
||||||
$this->expectExceptionMessage("Invalid credentials. Invalid {$expectedFieldError} or password.");
|
$this->expectExceptionMessage($expectedExceptionMessage);
|
||||||
|
|
||||||
$authForm = new AuthenticationForm();
|
$authForm = new AuthenticationForm();
|
||||||
$authForm->username = $login;
|
$authForm->username = $login;
|
||||||
@ -45,19 +53,10 @@ class AuthenticationFormTest extends TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function getInvalidCredentialsCases() {
|
public function getInvalidCredentialsCases() {
|
||||||
yield ['nickname', 'wrong-username', 'wrong-password'];
|
yield ['Invalid credentials. Invalid nickname or password.', 'wrong-username', 'wrong-password'];
|
||||||
yield ['email', 'wrong-email@ely.by', 'wrong-password'];
|
yield ['Invalid credentials. Invalid email or password.', 'wrong-email@ely.by', 'wrong-password'];
|
||||||
}
|
yield ['This account has been suspended.', 'Banned', 'password_0'];
|
||||||
|
yield ['Account protected with two factor auth.', 'AccountWithEnabledOtp', 'password_0'];
|
||||||
public function testAuthenticateByValidCredentialsIntoBlockedAccount() {
|
|
||||||
$this->expectException(ForbiddenOperationException::class);
|
|
||||||
$this->expectExceptionMessage('This account has been suspended.');
|
|
||||||
|
|
||||||
$authForm = new AuthenticationForm();
|
|
||||||
$authForm->username = 'Banned';
|
|
||||||
$authForm->password = 'password_0';
|
|
||||||
$authForm->clientToken = Uuid::uuid4()->toString();
|
|
||||||
$authForm->authenticate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
13
common/tests/fixtures/data/oauth-clients.php
vendored
13
common/tests/fixtures/data/oauth-clients.php
vendored
@ -14,6 +14,19 @@ return [
|
|||||||
'is_deleted' => 0,
|
'is_deleted' => 0,
|
||||||
'created_at' => 1455309271,
|
'created_at' => 1455309271,
|
||||||
],
|
],
|
||||||
|
'unauthorizedMinecraftGameLauncher' => [
|
||||||
|
'id' => 'unauthorized_minecraft_game_launcher',
|
||||||
|
'secret' => 'there_is_no_secret',
|
||||||
|
'type' => 'minecraft-game-launcher',
|
||||||
|
'name' => 'Unauthorized Minecraft game launcher',
|
||||||
|
'description' => '',
|
||||||
|
'redirect_uri' => null,
|
||||||
|
'website_url' => null,
|
||||||
|
'minecraft_server_ip' => null,
|
||||||
|
'is_trusted' => false,
|
||||||
|
'is_deleted' => false,
|
||||||
|
'created_at' => 1576003878,
|
||||||
|
],
|
||||||
'tlauncher' => [
|
'tlauncher' => [
|
||||||
'id' => 'tlauncher',
|
'id' => 'tlauncher',
|
||||||
'secret' => 'HsX-xXzdGiz3mcsqeEvrKHF47sqiaX94',
|
'secret' => 'HsX-xXzdGiz3mcsqeEvrKHF47sqiaX94',
|
||||||
|
Loading…
Reference in New Issue
Block a user