mirror of
https://github.com/elyby/accounts.git
synced 2025-02-24 03:37:34 +05:30
Upgrade project to PHP 8.3, add PHPStan, upgrade almost every dependency (#36)
* start updating to PHP 8.3
* taking off!
Co-authored-by: ErickSkrauch <erickskrauch@yandex.ru>
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
* dropped this
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
* migrate to symfonymailer
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
* this is so stupid 😭
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
* ah, free, at last.
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
* oh, Gabriel.
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
* now dawns thy reckoning.
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
* and thy gore shall GLISTEN before the temples of man.
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
* creature of steel.
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
* my gratitude upon thee for my freedom.
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
* but the crimes thy kind has committed against humanity
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
* Upgrade PHP-CS-Fixer and do fix the codebase
* First review round (maybe I have broken something)
* are NOT forgotten.
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
* Enable parallel PHP-CS-Fixer runner
* PHPStan level 1
* PHPStan level 2
* PHPStan level 3
* PHPStan level 4
* PHPStan level 5
* Levels 6 and 7 takes too much effort. Generate a baseline and fix them eventually
* Resolve TODO's related to the php-mock
* Drastically reduce baseline size with the Rector
* More code modernization with help of the Rector
* Update GitLab CI
---------
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
Co-authored-by: ErickSkrauch <erickskrauch@yandex.ru>
This commit is contained in:
parent
625250b367
commit
57d492da8a
14
.gitignore
vendored
14
.gitignore
vendored
@ -2,25 +2,23 @@
|
||||
.idea
|
||||
|
||||
# Composer
|
||||
composer.phar
|
||||
/vendor
|
||||
|
||||
# Mac DS_Store Files
|
||||
.DS_Store
|
||||
|
||||
# npm debug
|
||||
npm-debug*
|
||||
|
||||
# Docker and related files
|
||||
/docker-compose.yml
|
||||
/docker-compose.override.yml
|
||||
/.env
|
||||
|
||||
# id_rsa
|
||||
/id_rsa
|
||||
|
||||
# PHP-CS-Fixer
|
||||
.php_cs
|
||||
.php_cs.cache
|
||||
.php-cs-fixer.php
|
||||
.php-cs-fixer.cache
|
||||
|
||||
# PHPStan
|
||||
phpstan.neon
|
||||
|
||||
# Codeception
|
||||
codeception.yml
|
||||
|
@ -1,4 +1,4 @@
|
||||
image: edbizarro/gitlab-ci-pipeline-php:7.4-alpine
|
||||
image: thecodingmachine/php:8.3-v4-cli
|
||||
|
||||
stages:
|
||||
- prepare
|
||||
@ -46,9 +46,8 @@ variables:
|
||||
# Steps to extend #
|
||||
###################
|
||||
|
||||
.vendorCache:
|
||||
cache:
|
||||
key: backend-deps
|
||||
.vendorCache: &vendorCache
|
||||
key: composer
|
||||
paths:
|
||||
- vendor
|
||||
policy: pull
|
||||
@ -59,9 +58,8 @@ variables:
|
||||
|
||||
Composer:
|
||||
stage: prepare
|
||||
extends:
|
||||
- .vendorCache
|
||||
cache:
|
||||
<<: *vendorCache
|
||||
policy: pull-push
|
||||
before_script:
|
||||
- sudo composer self-update --2
|
||||
@ -76,15 +74,20 @@ Composer:
|
||||
|
||||
PHP-CS-Fixer:
|
||||
stage: testing
|
||||
extends:
|
||||
- .vendorCache
|
||||
cache:
|
||||
- *vendorCache
|
||||
- key: php-cs-fixer-$CI_COMMIT_REF_SLUG
|
||||
fallback_keys:
|
||||
- php-cs-fixer-$CI_DEFAULT_BRANCH
|
||||
paths:
|
||||
- .php-cs-fixer.cache
|
||||
when: always
|
||||
script:
|
||||
- vendor/bin/php-cs-fixer fix -v --dry-run
|
||||
|
||||
Codeception:
|
||||
stage: testing
|
||||
extends:
|
||||
- .vendorCache
|
||||
cache: *vendorCache
|
||||
services:
|
||||
- name: redis:4.0.10-alpine
|
||||
alias: redis
|
||||
@ -114,6 +117,24 @@ Codeception:
|
||||
- wait-for-it "${DB_HOST}:3306" -s -t 0 -- php yii migrate/up --interactive=0
|
||||
- vendor/bin/codecept run
|
||||
|
||||
PHPStan:
|
||||
stage: testing
|
||||
image: $PHP_BUILD_IMAGE
|
||||
cache:
|
||||
- *vendorCache
|
||||
- key: phpstan-$CI_COMMIT_REF_SLUG
|
||||
fallback_keys:
|
||||
- phpstan-$CI_DEFAULT_BRANCH
|
||||
paths:
|
||||
- .phpstan
|
||||
when: on_success
|
||||
before_script:
|
||||
- |
|
||||
echo -e "includes: [phpstan.dist.neon]\nparameters:\n tmpDir: .phpstan\n reportUnmatchedIgnoredErrors: false" > phpstan.neon
|
||||
script:
|
||||
- vendor/bin/codecept build
|
||||
- vendor/bin/phpstan analyse --no-progress --memory-limit 2G
|
||||
|
||||
###############
|
||||
# Build stage #
|
||||
###############
|
||||
|
28
.php-cs-fixer.dist.php
Normal file
28
.php-cs-fixer.dist.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Do not adjust this file for your environment manually.
|
||||
* Copy it as .php-cs-fixer.php (without .dist part) and then adjust.
|
||||
*/
|
||||
|
||||
$finder = PhpCsFixer\Finder::create()
|
||||
->in(__DIR__)
|
||||
->exclude('data')
|
||||
->exclude('docker')
|
||||
->exclude('frontend')
|
||||
->notPath('common/emails/views')
|
||||
->notPath('common/mail/layouts')
|
||||
->notPath('#.*/runtime#')
|
||||
->notPath('autocompletion.php')
|
||||
->exclude('_output')
|
||||
->exclude('_generated')
|
||||
// Remove the line below if your host OS is Windows, because it'll try to fix file, that should be executed
|
||||
// on Linux environment
|
||||
->name('yii');
|
||||
|
||||
return Ely\CS\Config::create([
|
||||
'self_accessor' => false,
|
||||
])
|
||||
->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect())
|
||||
->setFinder($finder);
|
17
.php_cs.dist
17
.php_cs.dist
@ -1,17 +0,0 @@
|
||||
<?php
|
||||
$finder = \PhpCsFixer\Finder::create()
|
||||
->in(__DIR__)
|
||||
->exclude('data')
|
||||
->exclude('docker')
|
||||
->exclude('frontend')
|
||||
->notPath('common/emails/views')
|
||||
->notPath('common/mail/layouts')
|
||||
->notPath('/.*\/runtime/')
|
||||
->notPath('autocompletion.php')
|
||||
->notPath('/.*\/tests\/_output/')
|
||||
->notPath('/.*\/tests\/_support\/_generated/')
|
||||
->name('yii');
|
||||
|
||||
return \Ely\CS\Config::create([
|
||||
'self_accessor' => false,
|
||||
])->setFinder($finder);
|
@ -1,4 +1,4 @@
|
||||
FROM php:7.4.33-fpm-alpine3.16 AS app
|
||||
FROM php:8.3.12-fpm-alpine3.20 AS app
|
||||
|
||||
# bash needed to support wait-for-it script
|
||||
RUN apk add --update --no-cache git bash patch openssh dcron \
|
||||
@ -6,7 +6,7 @@ RUN apk add --update --no-cache git bash patch openssh dcron \
|
||||
-o /usr/local/bin/install-php-extensions \
|
||||
https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions \
|
||||
&& chmod +x /usr/local/bin/install-php-extensions \
|
||||
&& install-php-extensions @composer zip pdo_mysql intl pcntl opcache imagick xdebug-^2 \
|
||||
&& install-php-extensions @composer zip pdo_mysql intl pcntl opcache imagick xdebug \
|
||||
# Create cron directory
|
||||
&& mkdir -p /etc/cron.d \
|
||||
# Install wait-for-it script
|
||||
@ -16,7 +16,6 @@ RUN apk add --update --no-cache git bash patch openssh dcron \
|
||||
# Feature: https://docs.docker.com/develop/develop-images/build_enhancements/#new-docker-build-secret-information
|
||||
# Track issues: https://github.com/docker/compose/issues/6358, https://github.com/compose-spec/compose-spec/issues/81
|
||||
|
||||
COPY ./patches /var/www/html/patches/
|
||||
COPY ./composer.* /var/www/html/
|
||||
|
||||
ARG build_env=prod
|
||||
|
@ -3,7 +3,7 @@ actor_suffix: Tester
|
||||
bootstrap: _bootstrap.php
|
||||
paths:
|
||||
tests: tests
|
||||
log: tests/_output
|
||||
output: tests/_output
|
||||
data: tests/_data
|
||||
helpers: tests/_support
|
||||
settings:
|
||||
|
@ -18,7 +18,7 @@ class ErrorHandler extends \yii\web\ErrorHandler {
|
||||
return parent::convertExceptionToArray($exception);
|
||||
}
|
||||
|
||||
public function logException($exception) {
|
||||
public function logException($exception): void {
|
||||
if ($exception instanceof AuthserverException) {
|
||||
Yii::error($exception, AuthserverException::class . ':' . $exception->getName());
|
||||
} elseif ($exception instanceof SessionServerException) {
|
||||
|
@ -8,12 +8,9 @@ use DateInterval;
|
||||
use League\OAuth2\Server\AuthorizationServer;
|
||||
use yii\base\Component as BaseComponent;
|
||||
|
||||
class Component extends BaseComponent {
|
||||
final class Component extends BaseComponent {
|
||||
|
||||
/**
|
||||
* @var AuthorizationServer
|
||||
*/
|
||||
private $_authServer;
|
||||
private ?AuthorizationServer $_authServer = null;
|
||||
|
||||
public function getAuthServer(): AuthorizationServer {
|
||||
if ($this->_authServer === null) {
|
||||
@ -39,7 +36,7 @@ class Component extends BaseComponent {
|
||||
new Repositories\EmptyScopeRepository(),
|
||||
new Keys\EmptyKey(),
|
||||
'', // Omit the key because we use our own encryption mechanism
|
||||
new ResponseTypes\BearerTokenResponse()
|
||||
new ResponseTypes\BearerTokenResponse(),
|
||||
);
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
$authCodeGrant = new Grants\AuthCodeGrant($authCodesRepo, $refreshTokensRepo, new DateInterval('PT10M'));
|
||||
|
@ -25,7 +25,7 @@ trait CryptTrait {
|
||||
protected function decrypt($encryptedData): string {
|
||||
try {
|
||||
return Yii::$app->tokens->decryptValue($encryptedData);
|
||||
} catch (SodiumException | RangeException $e) {
|
||||
} catch (SodiumException|RangeException $e) {
|
||||
throw new LogicException($e->getMessage(), 0, $e);
|
||||
}
|
||||
}
|
||||
|
@ -9,12 +9,12 @@ use League\OAuth2\Server\Entities\Traits\EntityTrait;
|
||||
use League\OAuth2\Server\Entities\Traits\TokenEntityTrait;
|
||||
use Yii;
|
||||
|
||||
class AccessTokenEntity implements AccessTokenEntityInterface {
|
||||
final class AccessTokenEntity implements AccessTokenEntityInterface {
|
||||
use EntityTrait;
|
||||
use TokenEntityTrait;
|
||||
|
||||
public function __toString(): string {
|
||||
return (string)Yii::$app->tokensFactory->createForOAuthClient($this);
|
||||
public function toString(): string {
|
||||
return Yii::$app->tokensFactory->createForOAuthClient($this)->toString();
|
||||
}
|
||||
|
||||
public function setPrivateKey(CryptKeyInterface $privateKey): void {
|
||||
|
@ -12,15 +12,18 @@ class ClientEntity implements ClientEntityInterface {
|
||||
use ClientTrait;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
* @param non-empty-string $id
|
||||
* @param string|string[] $redirectUri
|
||||
*/
|
||||
private $isTrusted;
|
||||
|
||||
public function __construct(string $id, string $name, $redirectUri, bool $isTrusted) {
|
||||
public function __construct(
|
||||
string $id,
|
||||
string $name,
|
||||
string|array $redirectUri,
|
||||
private readonly bool $isTrusted,
|
||||
) {
|
||||
$this->identifier = $id;
|
||||
$this->name = $name;
|
||||
$this->redirectUri = $redirectUri;
|
||||
$this->isTrusted = $isTrusted;
|
||||
}
|
||||
|
||||
public function isConfidential(): bool {
|
||||
|
@ -10,7 +10,7 @@ class UserEntity implements UserEntityInterface {
|
||||
use EntityTrait;
|
||||
|
||||
public function __construct(int $id) {
|
||||
$this->identifier = $id;
|
||||
$this->identifier = (string)$id;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2\Events;
|
||||
|
||||
use League\Event\AbstractEvent;
|
||||
use League\OAuth2\Server\EventEmitting\AbstractEvent;
|
||||
|
||||
class RequestedRefreshToken extends AbstractEvent {
|
||||
|
||||
|
@ -9,7 +9,9 @@ use api\components\OAuth2\Repositories\PublicScopeRepository;
|
||||
use DateInterval;
|
||||
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
|
||||
use League\OAuth2\Server\Entities\ClientEntityInterface;
|
||||
use League\OAuth2\Server\Entities\ScopeEntityInterface;
|
||||
use League\OAuth2\Server\Exception\OAuthServerException;
|
||||
use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException;
|
||||
use League\OAuth2\Server\Grant\AuthCodeGrant as BaseAuthCodeGrant;
|
||||
use League\OAuth2\Server\RequestEvent;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
@ -22,22 +24,22 @@ class AuthCodeGrant extends BaseAuthCodeGrant {
|
||||
* @param DateInterval $accessTokenTTL
|
||||
* @param ClientEntityInterface $client
|
||||
* @param string|null $userIdentifier
|
||||
* @param \League\OAuth2\Server\Entities\ScopeEntityInterface[] $scopes
|
||||
* @param ScopeEntityInterface[] $scopes
|
||||
*
|
||||
* @return AccessTokenEntityInterface
|
||||
* @throws \League\OAuth2\Server\Exception\OAuthServerException
|
||||
* @throws \League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException
|
||||
* @throws OAuthServerException
|
||||
* @throws UniqueTokenIdentifierConstraintViolationException
|
||||
*/
|
||||
protected function issueAccessToken(
|
||||
DateInterval $accessTokenTTL,
|
||||
ClientEntityInterface $client,
|
||||
$userIdentifier,
|
||||
array $scopes = []
|
||||
?string $userIdentifier,
|
||||
array $scopes = [],
|
||||
): AccessTokenEntityInterface {
|
||||
foreach ($scopes as $i => $scope) {
|
||||
if ($scope->getIdentifier() === PublicScopeRepository::OFFLINE_ACCESS) {
|
||||
unset($scopes[$i]);
|
||||
$this->getEmitter()->emit(new RequestedRefreshToken());
|
||||
$this->getEmitter()->emit(new RequestedRefreshToken('refresh_token_requested'));
|
||||
}
|
||||
}
|
||||
|
||||
@ -47,7 +49,7 @@ class AuthCodeGrant extends BaseAuthCodeGrant {
|
||||
protected function validateRedirectUri(
|
||||
string $redirectUri,
|
||||
ClientEntityInterface $client,
|
||||
ServerRequestInterface $request
|
||||
ServerRequestInterface $request,
|
||||
): void {
|
||||
$allowedRedirectUris = (array)$client->getRedirectUri();
|
||||
foreach ($allowedRedirectUris as $allowedRedirectUri) {
|
||||
|
@ -5,15 +5,17 @@ namespace api\components\OAuth2\Grants;
|
||||
|
||||
use api\components\OAuth2\CryptTrait;
|
||||
use api\components\Tokens\TokenReader;
|
||||
use Carbon\Carbon;
|
||||
use Carbon\FactoryImmutable;
|
||||
use common\models\OauthSession;
|
||||
use InvalidArgumentException;
|
||||
use Lcobucci\JWT\ValidationData;
|
||||
use Lcobucci\JWT\Validation\Constraint\LooseValidAt;
|
||||
use Lcobucci\JWT\Validation\Validator;
|
||||
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
|
||||
use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
|
||||
use League\OAuth2\Server\Exception\OAuthServerException;
|
||||
use League\OAuth2\Server\Grant\RefreshTokenGrant as BaseRefreshTokenGrant;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Throwable;
|
||||
use Yii;
|
||||
|
||||
class RefreshTokenGrant extends BaseRefreshTokenGrant {
|
||||
@ -30,7 +32,7 @@ class RefreshTokenGrant extends BaseRefreshTokenGrant {
|
||||
* @return array
|
||||
* @throws OAuthServerException
|
||||
*/
|
||||
protected function validateOldRefreshToken(ServerRequestInterface $request, $clientId): array {
|
||||
protected function validateOldRefreshToken(ServerRequestInterface $request, string $clientId): array {
|
||||
$refreshToken = $this->getRequestParameter('refresh_token', $request);
|
||||
if ($refreshToken !== null && mb_strlen($refreshToken) === 40) {
|
||||
return $this->validateLegacyRefreshToken($refreshToken);
|
||||
@ -41,7 +43,7 @@ class RefreshTokenGrant extends BaseRefreshTokenGrant {
|
||||
|
||||
/**
|
||||
* Currently we're not rotating refresh tokens.
|
||||
* So we overriding this method to always return null, which means,
|
||||
* So we're overriding this method to always return null, which means,
|
||||
* that refresh_token will not be issued.
|
||||
*
|
||||
* @param AccessTokenEntityInterface $accessToken
|
||||
@ -67,8 +69,8 @@ class RefreshTokenGrant extends BaseRefreshTokenGrant {
|
||||
[
|
||||
'access_token_id' => $accessTokenId,
|
||||
'session_id' => $sessionId,
|
||||
] = json_decode($result, true, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (\Exception $e) {
|
||||
] = json_decode((string)$result, true, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (Throwable $e) {
|
||||
throw OAuthServerException::invalidRefreshToken('Cannot decrypt the refresh token', $e);
|
||||
}
|
||||
|
||||
@ -89,8 +91,14 @@ class RefreshTokenGrant extends BaseRefreshTokenGrant {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $jwt
|
||||
* @return array
|
||||
* @return array{
|
||||
* client_id: string,
|
||||
* refresh_token_id?: string,
|
||||
* access_token_id?: string,
|
||||
* scopes: list<string>|null,
|
||||
* user_id: string|null,
|
||||
* expire_time: int|null,
|
||||
* }
|
||||
* @throws OAuthServerException
|
||||
*/
|
||||
private function validateAccessToken(string $jwt): array {
|
||||
@ -104,7 +112,7 @@ class RefreshTokenGrant extends BaseRefreshTokenGrant {
|
||||
throw OAuthServerException::invalidRefreshToken('Cannot decrypt the refresh token');
|
||||
}
|
||||
|
||||
if (!$token->validate(new ValidationData(Carbon::now()->getTimestamp()))) {
|
||||
if (!(new Validator())->validate($token, new LooseValidAt(FactoryImmutable::getDefaultInstance()))) {
|
||||
throw OAuthServerException::invalidRefreshToken('Token has expired');
|
||||
}
|
||||
|
||||
|
@ -15,4 +15,8 @@ class EmptyKey implements CryptKeyInterface {
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getKeyContents(): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -22,11 +22,11 @@ class AccessTokenRepository implements AccessTokenRepositoryInterface {
|
||||
public function getNewToken(
|
||||
ClientEntityInterface $clientEntity,
|
||||
array $scopes,
|
||||
$userIdentifier = null
|
||||
$userIdentifier = null,
|
||||
): AccessTokenEntityInterface {
|
||||
$accessToken = new AccessTokenEntity();
|
||||
$accessToken->setClient($clientEntity);
|
||||
array_map([$accessToken, 'addScope'], $scopes);
|
||||
array_map($accessToken->addScope(...), $scopes);
|
||||
if ($userIdentifier !== null) {
|
||||
$accessToken->setUserIdentifier($userIdentifier);
|
||||
}
|
||||
|
@ -21,8 +21,9 @@ class EmptyScopeRepository implements ScopeRepositoryInterface {
|
||||
public function finalizeScopes(
|
||||
array $scopes,
|
||||
$grantType,
|
||||
ClientEntityInterface $client,
|
||||
$userIdentifier = null
|
||||
ClientEntityInterface $clientEntity,
|
||||
$userIdentifier = null,
|
||||
?string $authCodeId = null,
|
||||
): array {
|
||||
return $scopes;
|
||||
}
|
||||
|
@ -10,11 +10,10 @@ use League\OAuth2\Server\Entities\ClientEntityInterface;
|
||||
use League\OAuth2\Server\Entities\ScopeEntityInterface;
|
||||
use League\OAuth2\Server\Exception\OAuthServerException;
|
||||
use League\OAuth2\Server\Repositories\ScopeRepositoryInterface;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
class InternalScopeRepository implements ScopeRepositoryInterface {
|
||||
|
||||
private const ALLOWED_SCOPES = [
|
||||
private const array ALLOWED_SCOPES = [
|
||||
P::CHANGE_ACCOUNT_USERNAME,
|
||||
P::CHANGE_ACCOUNT_PASSWORD,
|
||||
P::BLOCK_ACCOUNT,
|
||||
@ -22,7 +21,7 @@ class InternalScopeRepository implements ScopeRepositoryInterface {
|
||||
P::ESCAPE_IDENTITY_VERIFICATION,
|
||||
];
|
||||
|
||||
private const PUBLIC_SCOPES_TO_INTERNAL_PERMISSIONS = [
|
||||
private const array PUBLIC_SCOPES_TO_INTERNAL_PERMISSIONS = [
|
||||
'internal_account_info' => P::OBTAIN_EXTENDED_ACCOUNT_INFO,
|
||||
];
|
||||
|
||||
@ -35,21 +34,23 @@ class InternalScopeRepository implements ScopeRepositoryInterface {
|
||||
return new ScopeEntity($identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws OAuthServerException
|
||||
*/
|
||||
public function finalizeScopes(
|
||||
array $scopes,
|
||||
$grantType,
|
||||
ClientEntityInterface $client,
|
||||
$userIdentifier = null
|
||||
ClientEntityInterface $clientEntity,
|
||||
$userIdentifier = null,
|
||||
?string $authCodeId = null,
|
||||
): array {
|
||||
/** @var ClientEntity $client */
|
||||
Assert::isInstanceOf($client, ClientEntity::class);
|
||||
|
||||
if (empty($scopes)) {
|
||||
return $scopes;
|
||||
}
|
||||
|
||||
/** @var ClientEntity $clientEntity */
|
||||
// Right now we have no available scopes for the client_credentials grant
|
||||
if (!$client->isTrusted()) {
|
||||
if (!$clientEntity->isTrusted()) {
|
||||
throw OAuthServerException::invalidScope($scopes[0]->getIdentifier());
|
||||
}
|
||||
|
||||
|
@ -11,18 +11,18 @@ use League\OAuth2\Server\Repositories\ScopeRepositoryInterface;
|
||||
|
||||
class PublicScopeRepository implements ScopeRepositoryInterface {
|
||||
|
||||
public const OFFLINE_ACCESS = 'offline_access';
|
||||
public const CHANGE_SKIN = 'change_skin';
|
||||
public const string OFFLINE_ACCESS = 'offline_access';
|
||||
public const string CHANGE_SKIN = 'change_skin';
|
||||
|
||||
private const ACCOUNT_INFO = 'account_info';
|
||||
private const ACCOUNT_EMAIL = 'account_email';
|
||||
private const string ACCOUNT_INFO = 'account_info';
|
||||
private const string ACCOUNT_EMAIL = 'account_email';
|
||||
|
||||
private const PUBLIC_SCOPES_TO_INTERNAL_PERMISSIONS = [
|
||||
private const array PUBLIC_SCOPES_TO_INTERNAL_PERMISSIONS = [
|
||||
self::ACCOUNT_INFO => P::OBTAIN_OWN_ACCOUNT_INFO,
|
||||
self::ACCOUNT_EMAIL => P::OBTAIN_ACCOUNT_EMAIL,
|
||||
];
|
||||
|
||||
private const ALLOWED_SCOPES = [
|
||||
private const array ALLOWED_SCOPES = [
|
||||
P::OBTAIN_OWN_ACCOUNT_INFO,
|
||||
P::OBTAIN_ACCOUNT_EMAIL,
|
||||
P::MINECRAFT_SERVER_SESSION,
|
||||
@ -43,7 +43,8 @@ class PublicScopeRepository implements ScopeRepositoryInterface {
|
||||
array $scopes,
|
||||
$grantType,
|
||||
ClientEntityInterface $clientEntity,
|
||||
$userIdentifier = null
|
||||
$userIdentifier = null,
|
||||
?string $authCodeId = null,
|
||||
): array {
|
||||
return $scopes;
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ class Component extends \yii\base\Component {
|
||||
|
||||
public $secret;
|
||||
|
||||
public function init() {
|
||||
public function init(): void {
|
||||
if ($this->public === null) {
|
||||
throw new InvalidConfigException('Public is required');
|
||||
}
|
||||
|
@ -12,30 +12,27 @@ use yii\di\Instance;
|
||||
|
||||
class Validator extends \yii\validators\Validator {
|
||||
|
||||
private const SITE_VERIFY_URL = 'https://recaptcha.net/recaptcha/api/siteverify';
|
||||
private const string SITE_VERIFY_URL = 'https://recaptcha.net/recaptcha/api/siteverify';
|
||||
|
||||
private const REPEAT_LIMIT = 3;
|
||||
private const REPEAT_TIMEOUT = 1;
|
||||
private const int REPEAT_LIMIT = 3;
|
||||
private const int REPEAT_TIMEOUT = 1;
|
||||
|
||||
public $skipOnEmpty = false;
|
||||
|
||||
public $message = E::CAPTCHA_INVALID;
|
||||
|
||||
public $requiredMessage = E::CAPTCHA_REQUIRED;
|
||||
public string $requiredMessage = E::CAPTCHA_REQUIRED;
|
||||
|
||||
/**
|
||||
* @var Component|string
|
||||
*/
|
||||
public $component = 'reCaptcha';
|
||||
public Component|string $component = 'reCaptcha';
|
||||
|
||||
private $client;
|
||||
|
||||
public function __construct(ClientInterface $client, array $config = []) {
|
||||
public function __construct(
|
||||
private readonly ClientInterface $client,
|
||||
array $config = [],
|
||||
) {
|
||||
parent::__construct($config);
|
||||
$this->client = $client;
|
||||
}
|
||||
|
||||
public function init() {
|
||||
public function init(): void {
|
||||
parent::init();
|
||||
$this->component = Instance::ensure($this->component, Component::class);
|
||||
}
|
||||
@ -43,7 +40,7 @@ class Validator extends \yii\validators\Validator {
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected function validateValue($value) {
|
||||
protected function validateValue($value): ?array {
|
||||
if (empty($value)) {
|
||||
return [$this->requiredMessage, []];
|
||||
}
|
||||
@ -53,7 +50,7 @@ class Validator extends \yii\validators\Validator {
|
||||
$isSuccess = true;
|
||||
try {
|
||||
$response = $this->performRequest($value);
|
||||
} catch (ConnectException | ServerException $e) {
|
||||
} catch (ConnectException|ServerException $e) {
|
||||
if (++$repeats >= self::REPEAT_LIMIT) {
|
||||
throw $e;
|
||||
}
|
||||
@ -77,9 +74,7 @@ class Validator extends \yii\validators\Validator {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $value
|
||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
protected function performRequest(string $value): ResponseInterface {
|
||||
return $this->client->request('POST', self::SITE_VERIFY_URL, [
|
||||
|
@ -6,22 +6,20 @@ namespace api\components\Tokens\Algorithms;
|
||||
use Lcobucci\JWT\Signer;
|
||||
use Lcobucci\JWT\Signer\Ecdsa\Sha256;
|
||||
use Lcobucci\JWT\Signer\Key;
|
||||
use Lcobucci\JWT\Signer\Key\InMemory;
|
||||
|
||||
final class ES256 implements AlgorithmInterface {
|
||||
|
||||
private string $privateKeyPath;
|
||||
|
||||
private ?string $privateKeyPass;
|
||||
|
||||
private ?Key $privateKey = null;
|
||||
|
||||
private ?Key $publicKey = null;
|
||||
|
||||
private Sha256 $signer;
|
||||
private readonly Sha256 $signer;
|
||||
|
||||
public function __construct(string $privateKeyPath, ?string $privateKeyPass = null) {
|
||||
$this->privateKeyPath = $privateKeyPath;
|
||||
$this->privateKeyPass = $privateKeyPass;
|
||||
public function __construct(
|
||||
private readonly string $privateKeyPath,
|
||||
private readonly ?string $privateKeyPass = null,
|
||||
) {
|
||||
$this->signer = new Sha256();
|
||||
}
|
||||
|
||||
@ -31,7 +29,7 @@ final class ES256 implements AlgorithmInterface {
|
||||
|
||||
public function getPrivateKey(): Key {
|
||||
if ($this->privateKey === null) {
|
||||
$this->privateKey = new Key($this->privateKeyPath, $this->privateKeyPass);
|
||||
$this->privateKey = InMemory::plainText($this->privateKeyPath, $this->privateKeyPass ?? '');
|
||||
}
|
||||
|
||||
return $this->privateKey;
|
||||
@ -40,9 +38,9 @@ final class ES256 implements AlgorithmInterface {
|
||||
public function getPublicKey(): Key {
|
||||
if ($this->publicKey === null) {
|
||||
$privateKey = $this->getPrivateKey();
|
||||
$privateKeyOpenSSL = openssl_pkey_get_private($privateKey->getContent(), $privateKey->getPassphrase() ?? '');
|
||||
$privateKeyOpenSSL = openssl_pkey_get_private($privateKey->contents(), $privateKey->passphrase());
|
||||
$publicPem = openssl_pkey_get_details($privateKeyOpenSSL)['key'];
|
||||
$this->publicKey = new Key($publicPem);
|
||||
$this->publicKey = InMemory::plainText($publicPem);
|
||||
}
|
||||
|
||||
return $this->publicKey;
|
||||
|
@ -6,21 +6,18 @@ namespace api\components\Tokens\Algorithms;
|
||||
use Lcobucci\JWT\Signer;
|
||||
use Lcobucci\JWT\Signer\Hmac\Sha256;
|
||||
use Lcobucci\JWT\Signer\Key;
|
||||
use Lcobucci\JWT\Signer\Key\InMemory;
|
||||
|
||||
class HS256 implements AlgorithmInterface {
|
||||
final class HS256 implements AlgorithmInterface {
|
||||
|
||||
private ?InMemory $loadedKey = null;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* @param non-empty-string $key
|
||||
*/
|
||||
private $key;
|
||||
|
||||
/**
|
||||
* @var Key|null
|
||||
*/
|
||||
private $loadedKey;
|
||||
|
||||
public function __construct(string $key) {
|
||||
$this->key = $key;
|
||||
public function __construct(
|
||||
private readonly string $key,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getSigner(): Signer {
|
||||
@ -37,7 +34,7 @@ class HS256 implements AlgorithmInterface {
|
||||
|
||||
private function loadKey(): Key {
|
||||
if ($this->loadedKey === null) {
|
||||
$this->loadedKey = new Key($this->key);
|
||||
$this->loadedKey = InMemory::plainText($this->key);
|
||||
}
|
||||
|
||||
return $this->loadedKey;
|
||||
|
@ -22,11 +22,11 @@ final class AlgorithmsManager {
|
||||
* @param AlgorithmInterface[] $algorithms
|
||||
*/
|
||||
public function __construct(array $algorithms = []) {
|
||||
array_map([$this, 'add'], $algorithms);
|
||||
array_map($this->add(...), $algorithms);
|
||||
}
|
||||
|
||||
public function add(AlgorithmInterface $algorithm): self {
|
||||
$id = $algorithm->getSigner()->getAlgorithmId();
|
||||
$id = $algorithm->getSigner()->algorithmId();
|
||||
Assert::keyNotExists($this->algorithms, $id, 'passed algorithm is already exists');
|
||||
$this->algorithms[$id] = $algorithm;
|
||||
|
||||
|
@ -5,31 +5,40 @@ namespace api\components\Tokens;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use Lcobucci\JWT\Builder;
|
||||
use Lcobucci\JWT\Parser;
|
||||
use InvalidArgumentException;
|
||||
use Lcobucci\JWT\Encoding\ChainedFormatter;
|
||||
use Lcobucci\JWT\Encoding\JoseEncoder;
|
||||
use Lcobucci\JWT\Token;
|
||||
use Lcobucci\JWT\Token\Builder;
|
||||
use Lcobucci\JWT\Token\Parser;
|
||||
use Lcobucci\JWT\Token\RegisteredClaims;
|
||||
use Lcobucci\JWT\UnencryptedToken;
|
||||
use Lcobucci\JWT\Validation\Constraint\SignedWith;
|
||||
use Lcobucci\JWT\Validation\Validator;
|
||||
use ParagonIE\ConstantTime\Base64UrlSafe;
|
||||
use RangeException;
|
||||
use SodiumException;
|
||||
use Webmozart\Assert\Assert;
|
||||
use yii\base\Component as BaseComponent;
|
||||
|
||||
class Component extends BaseComponent {
|
||||
|
||||
private const PREFERRED_ALGORITHM = 'ES256';
|
||||
private const string PREFERRED_ALGORITHM = 'ES256';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $privateKeyPath;
|
||||
public string $privateKeyPath;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
public $privateKeyPass;
|
||||
public ?string $privateKeyPass = null;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $encryptionKey;
|
||||
public string $encryptionKey;
|
||||
|
||||
private ?AlgorithmsManager $algorithmManager = null;
|
||||
|
||||
@ -39,19 +48,49 @@ class Component extends BaseComponent {
|
||||
Assert::notEmpty($this->encryptionKey, 'encryptionKey must be set');
|
||||
}
|
||||
|
||||
public function create(array $payloads = [], array $headers = []): Token {
|
||||
/**
|
||||
* @param array{
|
||||
* sub?: string,
|
||||
* jti?: string,
|
||||
* iat?: \DateTimeImmutable,
|
||||
* nbf?: \DateTimeImmutable,
|
||||
* exp?: \DateTimeImmutable,
|
||||
* } $payloads
|
||||
* @param array $headers
|
||||
*
|
||||
* @throws \api\components\Tokens\AlgorithmIsNotDefinedException
|
||||
*/
|
||||
public function create(array $payloads = [], array $headers = []): UnencryptedToken {
|
||||
$now = Carbon::now();
|
||||
$builder = (new Builder())->issuedAt($now->getTimestamp());
|
||||
$builder = (new Builder(new JoseEncoder(), ChainedFormatter::default()))->issuedAt($now->toDateTimeImmutable());
|
||||
if (isset($payloads['sub'])) {
|
||||
$builder = $builder->relatedTo($payloads['sub']);
|
||||
}
|
||||
|
||||
if (isset($payloads['jti'])) {
|
||||
$builder = $builder->identifiedBy($payloads['jti']);
|
||||
}
|
||||
|
||||
if (isset($payloads['iat'])) {
|
||||
$builder = $builder->issuedAt($payloads['iat']);
|
||||
}
|
||||
|
||||
if (isset($payloads['nbf'])) {
|
||||
$builder = $builder->canOnlyBeUsedAfter($payloads['nbf']);
|
||||
}
|
||||
|
||||
if (isset($payloads['exp'])) {
|
||||
$builder->expiresAt($payloads['exp']);
|
||||
$builder = $builder->expiresAt($payloads['exp']);
|
||||
}
|
||||
|
||||
foreach ($payloads as $claim => $value) {
|
||||
$builder->withClaim($claim, $this->prepareValue($value));
|
||||
if (!in_array($claim, RegisteredClaims::ALL, true)) { // Registered claims are handled by the if-chain above
|
||||
$builder = $builder->withClaim($claim, $this->prepareValue($value));
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($headers as $claim => $value) {
|
||||
$builder->withHeader($claim, $this->prepareValue($value));
|
||||
$builder = $builder->withHeader($claim, $this->prepareValue($value));
|
||||
}
|
||||
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
@ -64,17 +103,17 @@ class Component extends BaseComponent {
|
||||
* @param string $jwt
|
||||
*
|
||||
* @return Token
|
||||
* @throws \InvalidArgumentException
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function parse(string $jwt): Token {
|
||||
return (new Parser())->parse($jwt);
|
||||
return (new Parser(new JoseEncoder()))->parse($jwt);
|
||||
}
|
||||
|
||||
public function verify(Token $token): bool {
|
||||
try {
|
||||
$algorithm = $this->getAlgorithmManager()->get($token->getHeader('alg'));
|
||||
return $token->verify($algorithm->getSigner(), $algorithm->getPublicKey());
|
||||
} catch (Exception $e) {
|
||||
$algorithm = $this->getAlgorithmManager()->get($token->headers()->get('alg'));
|
||||
return (new Validator())->validate($token, new SignedWith($algorithm->getSigner(), $algorithm->getPublicKey()));
|
||||
} catch (Exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -92,8 +131,8 @@ class Component extends BaseComponent {
|
||||
* @param string $encryptedValue
|
||||
*
|
||||
* @return string
|
||||
* @throws \SodiumException
|
||||
* @throws \RangeException
|
||||
* @throws SodiumException
|
||||
* @throws RangeException
|
||||
*/
|
||||
public function decryptValue(string $encryptedValue): string {
|
||||
$decoded = Base64UrlSafe::decode($encryptedValue);
|
||||
@ -109,7 +148,7 @@ class Component extends BaseComponent {
|
||||
}
|
||||
|
||||
public function getPublicKey(): string {
|
||||
return $this->getAlgorithmManager()->get(self::PREFERRED_ALGORITHM)->getPublicKey()->getContent();
|
||||
return $this->getAlgorithmManager()->get(self::PREFERRED_ALGORITHM)->getPublicKey()->contents();
|
||||
}
|
||||
|
||||
private function getAlgorithmManager(): AlgorithmsManager {
|
||||
@ -122,9 +161,9 @@ class Component extends BaseComponent {
|
||||
return $this->algorithmManager;
|
||||
}
|
||||
|
||||
private function prepareValue($value) {
|
||||
private function prepareValue(EncryptedValue|string $value): string {
|
||||
if ($value instanceof EncryptedValue) {
|
||||
return $this->encryptValue($value->getValue());
|
||||
return $this->encryptValue($value->value);
|
||||
}
|
||||
|
||||
return $value;
|
||||
|
@ -3,19 +3,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace api\components\Tokens;
|
||||
|
||||
class EncryptedValue {
|
||||
final readonly class EncryptedValue {
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $value;
|
||||
|
||||
public function __construct(string $value) {
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
public function getValue(): string {
|
||||
return $this->value;
|
||||
public function __construct(public string $value) {
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -3,19 +3,18 @@ declare(strict_types=1);
|
||||
|
||||
namespace api\components\Tokens;
|
||||
|
||||
use Lcobucci\JWT\Token;
|
||||
use Lcobucci\JWT\UnencryptedToken;
|
||||
use Yii;
|
||||
|
||||
final class TokenReader {
|
||||
final readonly class TokenReader {
|
||||
|
||||
private Token $token;
|
||||
|
||||
public function __construct(Token $token) {
|
||||
$this->token = $token;
|
||||
public function __construct(
|
||||
private UnencryptedToken $token,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getAccountId(): ?int {
|
||||
$sub = $this->token->getClaim('sub', false);
|
||||
$sub = $this->token->claims()->get('sub', false);
|
||||
if ($sub === false) {
|
||||
return null;
|
||||
}
|
||||
@ -24,36 +23,36 @@ final class TokenReader {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int)mb_substr($sub, mb_strlen(TokensFactory::SUB_ACCOUNT_PREFIX));
|
||||
return (int)mb_substr((string)$sub, mb_strlen(TokensFactory::SUB_ACCOUNT_PREFIX));
|
||||
}
|
||||
|
||||
public function getClientId(): ?string {
|
||||
return $this->token->getClaim('client_id', false) ?: null;
|
||||
return $this->token->claims()->get('client_id', false) ?: null;
|
||||
}
|
||||
|
||||
public function getScopes(): ?array {
|
||||
$scopes = $this->token->getClaim('scope', false);
|
||||
$scopes = $this->token->claims()->get('scope', false);
|
||||
if ($scopes !== false) {
|
||||
return explode(' ', $scopes);
|
||||
return explode(' ', (string)$scopes);
|
||||
}
|
||||
|
||||
// Handle legacy tokens, which used "ely-scopes" claim and was delimited with comma
|
||||
$scopes = $this->token->getClaim('ely-scopes', false);
|
||||
$scopes = $this->token->claims()->get('ely-scopes', false);
|
||||
if ($scopes === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return explode(',', $scopes);
|
||||
return explode(',', (string)$scopes);
|
||||
}
|
||||
|
||||
public function getMinecraftClientToken(): ?string {
|
||||
$encodedClientToken = $this->token->getClaim('ely-client-token', false);
|
||||
$encodedClientToken = $this->token->claims()->get('ely-client-token', false);
|
||||
if ($encodedClientToken === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* It really might throw an exception but we have not seen any case of such exception yet
|
||||
* It really might throw an exception, but we have not seen any case of such exception yet
|
||||
* @noinspection PhpUnhandledExceptionInspection
|
||||
*/
|
||||
return Yii::$app->tokens->decryptValue($encodedClientToken);
|
||||
|
@ -10,6 +10,7 @@ use common\models\Account;
|
||||
use common\models\AccountSession;
|
||||
use DateTime;
|
||||
use Lcobucci\JWT\Token;
|
||||
use Lcobucci\JWT\UnencryptedToken;
|
||||
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
|
||||
use League\OAuth2\Server\Entities\ScopeEntityInterface;
|
||||
use Yii;
|
||||
@ -17,54 +18,52 @@ use yii\base\Component;
|
||||
|
||||
class TokensFactory extends Component {
|
||||
|
||||
public const SUB_ACCOUNT_PREFIX = 'ely|';
|
||||
public const string SUB_ACCOUNT_PREFIX = 'ely|';
|
||||
|
||||
public function createForWebAccount(Account $account, AccountSession $session = null): Token {
|
||||
public function createForWebAccount(Account $account, AccountSession $session = null): UnencryptedToken {
|
||||
$payloads = [
|
||||
'sub' => $this->buildSub($account->id),
|
||||
'exp' => Carbon::now()->addHour()->getTimestamp(),
|
||||
'exp' => Carbon::now()->addHour()->toDateTimeImmutable(),
|
||||
'scope' => $this->prepareScopes([R::ACCOUNTS_WEB_USER]),
|
||||
];
|
||||
if ($session === null) {
|
||||
// If we don't remember a session, the token should live longer
|
||||
// so that the session doesn't end while working with the account
|
||||
$payloads['exp'] = Carbon::now()->addDays(7)->getTimestamp();
|
||||
$payloads['exp'] = Carbon::now()->addDays(7)->toDateTimeImmutable();
|
||||
} else {
|
||||
$payloads['jti'] = $session->id;
|
||||
$payloads['jti'] = (string)$session->id;
|
||||
}
|
||||
|
||||
return Yii::$app->tokens->create($payloads);
|
||||
}
|
||||
|
||||
public function createForOAuthClient(AccessTokenEntityInterface $accessToken): Token {
|
||||
public function createForOAuthClient(AccessTokenEntityInterface $accessToken): UnencryptedToken {
|
||||
$payloads = [
|
||||
'client_id' => $accessToken->getClient()->getIdentifier(),
|
||||
'scope' => $this->prepareScopes($accessToken->getScopes()),
|
||||
];
|
||||
if ($accessToken->getExpiryDateTime() > new DateTime()) {
|
||||
$payloads['exp'] = $accessToken->getExpiryDateTime()->getTimestamp();
|
||||
$payloads['exp'] = $accessToken->getExpiryDateTime();
|
||||
}
|
||||
|
||||
if ($accessToken->getUserIdentifier() !== null) {
|
||||
$payloads['sub'] = $this->buildSub($accessToken->getUserIdentifier());
|
||||
$payloads['sub'] = $this->buildSub((int)$accessToken->getUserIdentifier());
|
||||
}
|
||||
|
||||
return Yii::$app->tokens->create($payloads);
|
||||
}
|
||||
|
||||
public function createForMinecraftAccount(Account $account, string $clientToken): Token {
|
||||
public function createForMinecraftAccount(Account $account, string $clientToken): UnencryptedToken {
|
||||
return Yii::$app->tokens->create([
|
||||
'scope' => $this->prepareScopes([P::OBTAIN_OWN_ACCOUNT_INFO, P::MINECRAFT_SERVER_SESSION]),
|
||||
'ely-client-token' => new EncryptedValue($clientToken),
|
||||
'sub' => $this->buildSub($account->id),
|
||||
'exp' => Carbon::now()->addDays(2)->getTimestamp(),
|
||||
'exp' => Carbon::now()->addDays(2)->toDateTimeImmutable(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ScopeEntityInterface[]|string[] $scopes
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function prepareScopes(array $scopes): string {
|
||||
return implode(' ', array_map(function($scope): string {
|
||||
|
@ -18,13 +18,13 @@ use yii\web\User as YiiUserComponent;
|
||||
*/
|
||||
class Component extends YiiUserComponent {
|
||||
|
||||
public const KEEP_MINECRAFT_SESSIONS = 1;
|
||||
public const KEEP_SITE_SESSIONS = 2;
|
||||
public const KEEP_CURRENT_SESSION = 4;
|
||||
public const int KEEP_MINECRAFT_SESSIONS = 1;
|
||||
public const int KEEP_SITE_SESSIONS = 2;
|
||||
public const int KEEP_CURRENT_SESSION = 4;
|
||||
|
||||
public $enableSession = false;
|
||||
|
||||
public $loginUrl = null;
|
||||
public $loginUrl;
|
||||
|
||||
/**
|
||||
* We don't use the standard web authorization mechanism via cookies.
|
||||
@ -57,12 +57,13 @@ class Component extends YiiUserComponent {
|
||||
return null;
|
||||
}
|
||||
|
||||
$sessionId = $identity->getToken()->getClaim('jti', false);
|
||||
if ($sessionId === false) {
|
||||
/** @var int|null $sessionId */
|
||||
$sessionId = $identity->getToken()->claims()->get('jti');
|
||||
if ($sessionId === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return AccountSession::findOne($sessionId);
|
||||
return AccountSession::findOne(['id' => (int)$sessionId]);
|
||||
}
|
||||
|
||||
public function terminateSessions(Account $account, int $mode = 0): void {
|
||||
|
@ -5,57 +5,54 @@ namespace api\components\User;
|
||||
|
||||
use api\components\Tokens\TokenReader;
|
||||
use Carbon\Carbon;
|
||||
use Carbon\FactoryImmutable;
|
||||
use common\models\Account;
|
||||
use common\models\OauthClient;
|
||||
use common\models\OauthSession;
|
||||
use DateTimeImmutable;
|
||||
use Exception;
|
||||
use Lcobucci\JWT\Token;
|
||||
use Lcobucci\JWT\ValidationData;
|
||||
use Lcobucci\JWT\UnencryptedToken;
|
||||
use Lcobucci\JWT\Validation\Constraint\LooseValidAt;
|
||||
use Lcobucci\JWT\Validation\Validator;
|
||||
use Yii;
|
||||
use yii\base\NotSupportedException;
|
||||
use yii\web\UnauthorizedHttpException;
|
||||
|
||||
class JwtIdentity implements IdentityInterface {
|
||||
|
||||
/**
|
||||
* @var Token
|
||||
*/
|
||||
private $token;
|
||||
private ?TokenReader $reader = null;
|
||||
|
||||
/**
|
||||
* @var TokenReader|null
|
||||
*/
|
||||
private $reader;
|
||||
|
||||
private function __construct(Token $token) {
|
||||
$this->token = $token;
|
||||
private function __construct(
|
||||
private readonly UnencryptedToken $token,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function findIdentityByAccessToken($rawToken, $type = null): IdentityInterface {
|
||||
public static function findIdentityByAccessToken($token, $type = null): IdentityInterface {
|
||||
try {
|
||||
$token = Yii::$app->tokens->parse($rawToken);
|
||||
$parsedToken = Yii::$app->tokens->parse($token);
|
||||
} catch (Exception $e) {
|
||||
Yii::error($e);
|
||||
throw new UnauthorizedHttpException('Incorrect token');
|
||||
}
|
||||
|
||||
if (!Yii::$app->tokens->verify($token)) {
|
||||
if (!Yii::$app->tokens->verify($parsedToken)) {
|
||||
throw new UnauthorizedHttpException('Incorrect token');
|
||||
}
|
||||
|
||||
$now = Carbon::now();
|
||||
if ($token->isExpired($now)) {
|
||||
if ($parsedToken->isExpired($now)) {
|
||||
throw new UnauthorizedHttpException('Token expired');
|
||||
}
|
||||
|
||||
if (!$token->validate(new ValidationData($now->getTimestamp()))) {
|
||||
if (!(new Validator())->validate($parsedToken, new LooseValidAt(FactoryImmutable::getDefaultInstance()))) {
|
||||
throw new UnauthorizedHttpException('Incorrect token');
|
||||
}
|
||||
|
||||
$tokenReader = new TokenReader($token);
|
||||
$tokenReader = new TokenReader($parsedToken);
|
||||
$accountId = $tokenReader->getAccountId();
|
||||
if ($accountId !== null) {
|
||||
$iat = $token->getClaim('iat');
|
||||
/** @var DateTimeImmutable $iat */
|
||||
$iat = $parsedToken->claims()->get('iat');
|
||||
if ($tokenReader->getMinecraftClientToken() !== null
|
||||
&& self::isRevoked($accountId, OauthClient::UNAUTHORIZED_MINECRAFT_GAME_LAUNCHER, $iat)
|
||||
) {
|
||||
@ -69,10 +66,10 @@ class JwtIdentity implements IdentityInterface {
|
||||
}
|
||||
}
|
||||
|
||||
return new self($token);
|
||||
return new self($parsedToken);
|
||||
}
|
||||
|
||||
public function getToken(): Token {
|
||||
public function getToken(): UnencryptedToken {
|
||||
return $this->token;
|
||||
}
|
||||
|
||||
@ -85,10 +82,10 @@ class JwtIdentity implements IdentityInterface {
|
||||
}
|
||||
|
||||
public function getId(): string {
|
||||
return (string)$this->token;
|
||||
return $this->token->toString();
|
||||
}
|
||||
|
||||
// @codeCoverageIgnoreStart
|
||||
/** @codeCoverageIgnoreStart */
|
||||
public function getAuthKey() {
|
||||
throw new NotSupportedException('This method used for cookie auth, except we using Bearer auth');
|
||||
}
|
||||
@ -97,17 +94,19 @@ class JwtIdentity implements IdentityInterface {
|
||||
throw new NotSupportedException('This method used for cookie auth, except we using Bearer auth');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NotSupportedException
|
||||
*/
|
||||
public static function findIdentity($id) {
|
||||
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 {
|
||||
private static function isRevoked(int $accountId, string $clientId, DateTimeImmutable $iat): bool {
|
||||
$session = OauthSession::findOne(['account_id' => $accountId, 'client_id' => $clientId]);
|
||||
return $session !== null && $session->revoked_at !== null && $session->revoked_at > $iat;
|
||||
return $session !== null && $session->revoked_at !== null && $session->revoked_at > $iat->getTimestamp();
|
||||
}
|
||||
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
/** @codeCoverageIgnoreEnd */
|
||||
private function getReader(): TokenReader {
|
||||
if ($this->reader === null) {
|
||||
$this->reader = new TokenReader($this->token);
|
||||
|
@ -10,33 +10,21 @@ use Yii;
|
||||
use yii\base\NotSupportedException;
|
||||
use yii\web\UnauthorizedHttpException;
|
||||
|
||||
class LegacyOAuth2Identity implements IdentityInterface {
|
||||
readonly class LegacyOAuth2Identity implements IdentityInterface {
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* @param string[] $scopes
|
||||
*/
|
||||
private $accessToken;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $sessionId;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private $scopes;
|
||||
|
||||
private function __construct(string $accessToken, int $sessionId, array $scopes) {
|
||||
$this->accessToken = $accessToken;
|
||||
$this->sessionId = $sessionId;
|
||||
$this->scopes = $scopes;
|
||||
private function __construct(
|
||||
private string $accessToken,
|
||||
private int $sessionId,
|
||||
private array $scopes,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
* @throws UnauthorizedHttpException
|
||||
* @return IdentityInterface
|
||||
*/
|
||||
public static function findIdentityByAccessToken($token, $type = null): IdentityInterface {
|
||||
$tokenParams = self::findRecordOnLegacyStorage($token);
|
||||
@ -48,16 +36,11 @@ class LegacyOAuth2Identity implements IdentityInterface {
|
||||
throw new UnauthorizedHttpException('Token expired');
|
||||
}
|
||||
|
||||
return new static($token, $tokenParams['session_id'], $tokenParams['scopes']);
|
||||
return new self($token, $tokenParams['session_id'], $tokenParams['scopes']);
|
||||
}
|
||||
|
||||
public function getAccount(): ?Account {
|
||||
$session = $this->getSession();
|
||||
if ($session === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $session->account;
|
||||
return $this->getSession()?->account;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -71,7 +54,7 @@ class LegacyOAuth2Identity implements IdentityInterface {
|
||||
return $this->accessToken;
|
||||
}
|
||||
|
||||
// @codeCoverageIgnoreStart
|
||||
/** @codeCoverageIgnoreStart */
|
||||
public function getAuthKey() {
|
||||
throw new NotSupportedException('This method used for cookie auth, except we using Bearer auth');
|
||||
}
|
||||
@ -84,8 +67,7 @@ class LegacyOAuth2Identity implements IdentityInterface {
|
||||
throw new NotSupportedException('This method used for cookie auth, except we using Bearer auth');
|
||||
}
|
||||
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
/** @codeCoverageIgnoreEnd */
|
||||
private static function findRecordOnLegacyStorage(string $accessToken): ?array {
|
||||
$record = Yii::$app->redis->get("oauth:access:tokens:{$accessToken}");
|
||||
if ($record === null) {
|
||||
@ -93,8 +75,8 @@ class LegacyOAuth2Identity implements IdentityInterface {
|
||||
}
|
||||
|
||||
try {
|
||||
$data = json_decode($record, true, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (Exception $e) {
|
||||
$data = json_decode((string)$record, true, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (Exception) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -16,15 +16,12 @@ return [
|
||||
],
|
||||
'container' => [
|
||||
'singletons' => [
|
||||
api\components\ReCaptcha\Validator::class => function() {
|
||||
return new class(new GuzzleHttp\Client()) extends api\components\ReCaptcha\Validator {
|
||||
protected function validateValue($value) {
|
||||
api\components\ReCaptcha\Validator::class => fn(): api\components\ReCaptcha\Validator => new class(new GuzzleHttp\Client()) extends api\components\ReCaptcha\Validator {
|
||||
protected function validateValue($value): ?array {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
},
|
||||
common\components\SkinsSystemApi::class => function() {
|
||||
return new class('http://chrly.ely.by') extends common\components\SkinsSystemApi {
|
||||
common\components\SkinsSystemApi::class => fn(): common\components\SkinsSystemApi => new class('http://chrly.ely.by') extends common\components\SkinsSystemApi {
|
||||
public function textures(string $username): ?array {
|
||||
return [
|
||||
'SKIN' => [
|
||||
@ -103,7 +100,6 @@ return [
|
||||
public function getSignatureVerificationKey(string $format = 'pem'): string {
|
||||
return "-----BEGIN PUBLIC KEY-----\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANbUpVCZkMKpfvYZ08W3lumdAaYxLBnm\nUDlzHBQH3DpYef5WCO32TDU6feIJ58A0lAywgtZ4wwi2dGHOz/1hAvcCAwEAAQ==\n-----END PUBLIC KEY-----";
|
||||
}
|
||||
};
|
||||
},
|
||||
],
|
||||
],
|
||||
|
@ -42,7 +42,7 @@ return [
|
||||
'traceLevel' => YII_DEBUG ? 3 : 0,
|
||||
'targets' => [
|
||||
[
|
||||
'class' => mito\sentry\Target::class,
|
||||
'class' => nohnaimer\sentry\Target::class,
|
||||
'levels' => ['error', 'warning'],
|
||||
'except' => [
|
||||
'legacy-authserver',
|
||||
|
@ -48,7 +48,7 @@ class AuthenticationController extends Controller {
|
||||
];
|
||||
}
|
||||
|
||||
public function actionLogin() {
|
||||
public function actionLogin(): array {
|
||||
$model = new LoginForm();
|
||||
$model->load(Yii::$app->request->post());
|
||||
if (($result = $model->login()) === null) {
|
||||
@ -69,7 +69,7 @@ class AuthenticationController extends Controller {
|
||||
], $result->formatAsOAuth2Response());
|
||||
}
|
||||
|
||||
public function actionLogout() {
|
||||
public function actionLogout(): array {
|
||||
$form = new LogoutForm();
|
||||
$form->logout();
|
||||
|
||||
@ -78,7 +78,7 @@ class AuthenticationController extends Controller {
|
||||
];
|
||||
}
|
||||
|
||||
public function actionForgotPassword() {
|
||||
public function actionForgotPassword(): array {
|
||||
$model = new ForgotPasswordForm();
|
||||
$model->load(Yii::$app->request->post());
|
||||
if ($model->forgotPassword() === false) {
|
||||
@ -106,14 +106,14 @@ class AuthenticationController extends Controller {
|
||||
],
|
||||
];
|
||||
|
||||
if (strpos($model->login, '@') === false) {
|
||||
if (!str_contains((string)$model->login, '@')) {
|
||||
$response['data']['emailMask'] = StringHelper::getEmailMask($model->getAccount()->email);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function actionRecoverPassword() {
|
||||
public function actionRecoverPassword(): array {
|
||||
$model = new RecoverPasswordForm();
|
||||
$model->load(Yii::$app->request->post());
|
||||
if (($result = $model->recoverPassword()) === null) {
|
||||
@ -128,7 +128,7 @@ class AuthenticationController extends Controller {
|
||||
], $result->formatAsOAuth2Response());
|
||||
}
|
||||
|
||||
public function actionRefreshToken() {
|
||||
public function actionRefreshToken(): array {
|
||||
$model = new RefreshTokenForm();
|
||||
$model->load(Yii::$app->request->post());
|
||||
if (($result = $model->renew()) === null) {
|
||||
|
@ -25,7 +25,7 @@ class Controller extends \yii\rest\Controller {
|
||||
// XML and rate limiter is not necessary
|
||||
unset(
|
||||
$parentBehaviors['contentNegotiator']['formats']['application/xml'],
|
||||
$parentBehaviors['rateLimiter']
|
||||
$parentBehaviors['rateLimiter'],
|
||||
);
|
||||
|
||||
return $parentBehaviors;
|
||||
|
@ -21,7 +21,7 @@ class FeedbackController extends Controller {
|
||||
];
|
||||
}
|
||||
|
||||
public function actionIndex() {
|
||||
public function actionIndex(): array {
|
||||
$model = new FeedbackForm();
|
||||
$model->load(Yii::$app->request->post());
|
||||
if (!$model->sendMessage()) {
|
||||
|
@ -27,7 +27,7 @@ class OptionsController extends Controller {
|
||||
];
|
||||
}
|
||||
|
||||
public function actionIndex() {
|
||||
public function actionIndex(): array {
|
||||
return [
|
||||
'reCaptchaPublicKey' => Yii::$app->reCaptcha->public,
|
||||
];
|
||||
|
@ -37,7 +37,7 @@ class SignupController extends Controller {
|
||||
];
|
||||
}
|
||||
|
||||
public function actionIndex() {
|
||||
public function actionIndex(): array {
|
||||
$model = new RegistrationForm();
|
||||
$model->load(Yii::$app->request->post());
|
||||
if (!$model->signup()) {
|
||||
@ -52,7 +52,7 @@ class SignupController extends Controller {
|
||||
];
|
||||
}
|
||||
|
||||
public function actionRepeatMessage() {
|
||||
public function actionRepeatMessage(): array {
|
||||
$model = new RepeatAccountActivationForm();
|
||||
$model->load(Yii::$app->request->post());
|
||||
if (!$model->sendRepeatMessage()) {
|
||||
@ -77,7 +77,7 @@ class SignupController extends Controller {
|
||||
];
|
||||
}
|
||||
|
||||
public function actionConfirm() {
|
||||
public function actionConfirm(): array {
|
||||
$model = new ConfirmEmailForm();
|
||||
$model->load(Yii::$app->request->post());
|
||||
if (!($result = $model->confirm())) {
|
||||
|
@ -48,7 +48,7 @@ final class LogMetricsToStatsd implements BootstrapInterface {
|
||||
|
||||
private function getPrefix(ActionEvent $event): ?string {
|
||||
$action = $event->action;
|
||||
switch (get_class($action)) {
|
||||
switch ($action::class) {
|
||||
case actions\AcceptRulesAction::class: return 'accounts.acceptRules';
|
||||
case actions\ChangeEmailAction::class: return 'accounts.changeEmail';
|
||||
case actions\ChangeLanguageAction::class: return 'accounts.switchLanguage';
|
||||
|
@ -28,6 +28,7 @@ final class MockDataResponse implements BootstrapInterface {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var \yii\web\Response $response */
|
||||
$response = $event->action->controller->response;
|
||||
$response->format = Response::FORMAT_JSON;
|
||||
$response->data = $result;
|
||||
@ -38,6 +39,7 @@ final class MockDataResponse implements BootstrapInterface {
|
||||
|
||||
private function getResponse(ActionEvent $event): ?array {
|
||||
$action = $event->action;
|
||||
/** @var \yii\web\Controller $controller */
|
||||
$controller = $action->controller;
|
||||
$request = $controller->request;
|
||||
if ($controller instanceof SignupController && $action->id === 'index') {
|
||||
|
@ -12,13 +12,13 @@ use yii\base\InvalidConfigException;
|
||||
|
||||
class FeedbackForm extends ApiForm {
|
||||
|
||||
public $subject;
|
||||
public mixed $subject = null;
|
||||
|
||||
public $email;
|
||||
public mixed $email = null;
|
||||
|
||||
public $type;
|
||||
public mixed $type = null;
|
||||
|
||||
public $message;
|
||||
public mixed $message = null;
|
||||
|
||||
public function rules(): array {
|
||||
return [
|
||||
@ -31,27 +31,30 @@ class FeedbackForm extends ApiForm {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidConfigException
|
||||
*/
|
||||
public function sendMessage(): bool {
|
||||
if (!$this->validate()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var \yii\swiftmailer\Mailer $mailer */
|
||||
/** @var \yii\symfonymailer\Mailer $mailer */
|
||||
$mailer = Yii::$app->mailer;
|
||||
$supportEmail = Yii::$app->params['supportEmail'];
|
||||
if (!$supportEmail) {
|
||||
throw new InvalidConfigException('Please specify supportEmail value in app params');
|
||||
throw new InvalidConfigException('Please specify supportEmail value in the app params');
|
||||
}
|
||||
|
||||
$account = $this->getAccount();
|
||||
/** @var \yii\swiftmailer\Message $message */
|
||||
/** @var \yii\symfonymailer\Message $message */
|
||||
$message = $mailer->compose('@common/emails/views/feedback', [
|
||||
'model' => $this,
|
||||
'account' => $account,
|
||||
]);
|
||||
$message
|
||||
->setTo($supportEmail)
|
||||
->setFrom([$this->email => $account ? $account->username : $this->email])
|
||||
->setFrom([$this->email => $account?->username ?? $this->email])
|
||||
->setSubject($this->subject);
|
||||
|
||||
Assert::true($message->send(), 'Unable send feedback email.');
|
||||
|
@ -3,26 +3,18 @@ declare(strict_types=1);
|
||||
|
||||
namespace api\models\authentication;
|
||||
|
||||
use Lcobucci\JWT\Token;
|
||||
use DateTimeImmutable;
|
||||
use Lcobucci\JWT\UnencryptedToken;
|
||||
|
||||
class AuthenticationResult {
|
||||
final readonly class AuthenticationResult {
|
||||
|
||||
/**
|
||||
* @var Token
|
||||
*/
|
||||
private $token;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $refreshToken;
|
||||
|
||||
public function __construct(Token $token, string $refreshToken = null) {
|
||||
$this->token = $token;
|
||||
$this->refreshToken = $refreshToken;
|
||||
public function __construct(
|
||||
private UnencryptedToken $token,
|
||||
private ?string $refreshToken = null,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getToken(): Token {
|
||||
public function getToken(): UnencryptedToken {
|
||||
return $this->token;
|
||||
}
|
||||
|
||||
@ -31,9 +23,11 @@ class AuthenticationResult {
|
||||
}
|
||||
|
||||
public function formatAsOAuth2Response(): array {
|
||||
/** @var DateTimeImmutable $expiresAt */
|
||||
$expiresAt = $this->token->claims()->get('exp');
|
||||
$response = [
|
||||
'access_token' => (string)$this->token,
|
||||
'expires_in' => $this->token->getClaim('exp') - time(),
|
||||
'access_token' => $this->token->toString(),
|
||||
'expires_in' => $expiresAt->getTimestamp() - (new DateTimeImmutable())->getTimestamp(),
|
||||
];
|
||||
|
||||
$refreshToken = $this->refreshToken;
|
||||
|
@ -16,9 +16,9 @@ use yii\base\ErrorException;
|
||||
|
||||
class ForgotPasswordForm extends ApiForm {
|
||||
|
||||
public $captcha;
|
||||
public mixed $captcha = null;
|
||||
|
||||
public $login;
|
||||
public mixed $login = null;
|
||||
|
||||
public function rules(): array {
|
||||
return [
|
||||
@ -90,6 +90,7 @@ class ForgotPasswordForm extends ApiForm {
|
||||
return null;
|
||||
}
|
||||
|
||||
// @phpstan-ignore return.type
|
||||
return $account->getEmailActivations()->withType(EmailActivation::TYPE_FORGOT_PASSWORD_KEY)->one();
|
||||
}
|
||||
|
||||
|
@ -13,39 +13,23 @@ use Yii;
|
||||
|
||||
class LoginForm extends ApiForm {
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $login;
|
||||
public mixed $login = null;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $password;
|
||||
public mixed $password = null;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
public $totp;
|
||||
public mixed $totp = null;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public $rememberMe = false;
|
||||
public mixed $rememberMe = false;
|
||||
|
||||
public function rules(): array {
|
||||
return [
|
||||
['login', 'required', 'message' => E::LOGIN_REQUIRED],
|
||||
['login', 'validateLogin'],
|
||||
|
||||
['password', 'required', 'when' => function(self $model): bool {
|
||||
return !$model->hasErrors();
|
||||
}, 'message' => E::PASSWORD_REQUIRED],
|
||||
['password', 'required', 'when' => fn(self $model): bool => !$model->hasErrors(), 'message' => E::PASSWORD_REQUIRED],
|
||||
['password', 'validatePassword'],
|
||||
|
||||
['totp', 'required', 'when' => function(self $model): bool {
|
||||
return !$model->hasErrors() && $model->getAccount()->is_otp_enabled;
|
||||
}, 'message' => E::TOTP_REQUIRED],
|
||||
['totp', 'required', 'when' => fn(self $model): bool => !$model->hasErrors() && $model->getAccount()->is_otp_enabled, 'message' => E::TOTP_REQUIRED],
|
||||
['totp', 'validateTotp'],
|
||||
|
||||
['login', 'validateActivity'],
|
||||
@ -81,7 +65,6 @@ class LoginForm extends ApiForm {
|
||||
}
|
||||
|
||||
$validator = new TotpValidator(['account' => $account]);
|
||||
$validator->window = 1;
|
||||
$validator->validateAttribute($this, $attribute);
|
||||
}
|
||||
|
||||
@ -99,6 +82,7 @@ class LoginForm extends ApiForm {
|
||||
}
|
||||
}
|
||||
|
||||
/** @noinspection PhpIncompatibleReturnTypeInspection */
|
||||
public function getAccount(): ?Account {
|
||||
return Account::find()->andWhereLogin($this->login)->one();
|
||||
}
|
||||
@ -130,7 +114,7 @@ class LoginForm extends ApiForm {
|
||||
|
||||
$transaction->commit();
|
||||
|
||||
return new AuthenticationResult($token, $session ? $session->refresh_token : null);
|
||||
return new AuthenticationResult($token, $session?->refresh_token);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ class RegistrationForm extends ApiForm {
|
||||
];
|
||||
}
|
||||
|
||||
public function validatePasswordAndRePasswordMatch($attribute) {
|
||||
public function validatePasswordAndRePasswordMatch($attribute): void {
|
||||
if (!$this->hasErrors()) {
|
||||
if ($this->password !== $this->rePassword) {
|
||||
$this->addError($attribute, E::RE_PASSWORD_DOES_NOT_MATCH);
|
||||
@ -64,7 +64,7 @@ class RegistrationForm extends ApiForm {
|
||||
|
||||
public function signup() {
|
||||
if (!$this->validate() && !$this->canContinue($this->getFirstErrors())) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
$transaction = Yii::$app->db->beginTransaction();
|
||||
|
@ -16,11 +16,9 @@ use Yii;
|
||||
|
||||
class RepeatAccountActivationForm extends ApiForm {
|
||||
|
||||
public $captcha;
|
||||
public mixed $captcha = null;
|
||||
|
||||
public $email;
|
||||
|
||||
private $emailActivation;
|
||||
public mixed $email = null;
|
||||
|
||||
public function rules(): array {
|
||||
return [
|
||||
@ -74,8 +72,6 @@ class RepeatAccountActivationForm extends ApiForm {
|
||||
$activation->key = UserFriendlyRandomKey::make();
|
||||
Assert::true($activation->save(), 'Unable save email-activation model.');
|
||||
|
||||
$this->emailActivation = $activation;
|
||||
|
||||
Yii::$app->queue->push(SendRegistrationEmail::createFromConfirmation($activation));
|
||||
|
||||
$transaction->commit();
|
||||
@ -90,6 +86,7 @@ class RepeatAccountActivationForm extends ApiForm {
|
||||
}
|
||||
|
||||
public function getActivation(): ?RegistrationConfirmation {
|
||||
// @phpstan-ignore return.type
|
||||
return $this->getAccount()
|
||||
->getEmailActivations()
|
||||
->withType(EmailActivation::TYPE_REGISTRATION_EMAIL_CONFIRMATION)
|
||||
|
@ -7,11 +7,11 @@ use common\models\Account;
|
||||
|
||||
class BaseAccountForm extends ApiForm {
|
||||
|
||||
private Account $account;
|
||||
|
||||
public function __construct(Account $account, array $config = []) {
|
||||
public function __construct(
|
||||
private readonly Account $account,
|
||||
array $config = [],
|
||||
) {
|
||||
parent::__construct($config);
|
||||
$this->account = $account;
|
||||
}
|
||||
|
||||
public function getAccount(): Account {
|
||||
|
@ -8,8 +8,11 @@ use common\helpers\Error as E;
|
||||
class EmailVerificationAction extends BaseAccountAction {
|
||||
|
||||
/**
|
||||
* @param SendEmailVerificationForm|AccountActionForm $model
|
||||
* @return array
|
||||
* @param SendEmailVerificationForm $model
|
||||
*
|
||||
* @return array{
|
||||
* canRepeatIn?: int,
|
||||
* }
|
||||
*/
|
||||
public function getFailedResultData(AccountActionForm $model): array {
|
||||
$emailError = $model->getFirstError('email');
|
||||
|
@ -29,9 +29,10 @@ class ChangePasswordForm extends AccountActionForm {
|
||||
['newPassword', PasswordValidator::class],
|
||||
['newRePassword', 'validatePasswordAndRePasswordMatch'],
|
||||
['logoutAll', 'boolean'],
|
||||
['password', PasswordRequiredValidator::class, 'account' => $this->getAccount(), 'when' => function() {
|
||||
return !$this->hasErrors();
|
||||
}],
|
||||
['password', PasswordRequiredValidator::class,
|
||||
'account' => $this->getAccount(),
|
||||
'when' => fn(): bool => !$this->hasErrors(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -16,9 +16,7 @@ class ChangeUsernameForm extends AccountActionForm {
|
||||
|
||||
public function rules(): array {
|
||||
return [
|
||||
['username', UsernameValidator::class, 'accountCallback' => function() {
|
||||
return $this->getAccount()->id;
|
||||
}],
|
||||
['username', UsernameValidator::class, 'accountCallback' => fn() => $this->getAccount()->id],
|
||||
['password', PasswordRequiredValidator::class, 'account' => $this->getAccount()],
|
||||
];
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ final class DeleteAccountForm extends AccountActionForm {
|
||||
Assert::true($account->save(), 'Cannot delete account');
|
||||
|
||||
// Schedule complete account erasing
|
||||
Yii::$app->queue->delay($account->getDeleteAt()->diffInRealSeconds())->push(new DeleteAccount($account->id));
|
||||
Yii::$app->queue->delay($account->getDeleteAt()->diffInUTCSeconds(null, true))->push(new DeleteAccount($account->id));
|
||||
|
||||
$transaction->commit();
|
||||
|
||||
|
@ -18,7 +18,7 @@ class EnableTwoFactorAuthForm extends AccountActionForm {
|
||||
return [
|
||||
['account', 'validateOtpDisabled'],
|
||||
['totp', 'required', 'message' => E::TOTP_REQUIRED],
|
||||
['totp', TotpValidator::class, 'account' => $this->getAccount(), 'window' => 2],
|
||||
['totp', TotpValidator::class, 'account' => $this->getAccount()],
|
||||
['password', PasswordRequiredValidator::class, 'account' => $this->getAccount()],
|
||||
];
|
||||
}
|
||||
|
@ -77,14 +77,15 @@ class SendEmailVerificationForm extends AccountActionForm {
|
||||
* Including checking for the confirmation of the new E-mail type, because when you go to this step,
|
||||
* the activation of the previous step is removed.
|
||||
*
|
||||
* @return CurrentEmailConfirmation|\common\models\confirmations\NewEmailConfirmation
|
||||
* @return \common\models\confirmations\CurrentEmailConfirmation|\common\models\confirmations\NewEmailConfirmation
|
||||
*/
|
||||
public function getEmailActivation(): ?EmailActivation {
|
||||
// @phpstan-ignore return.type
|
||||
return $this->getAccount()
|
||||
->getEmailActivations()
|
||||
->withType(
|
||||
EmailActivation::TYPE_CURRENT_EMAIL_CONFIRMATION,
|
||||
EmailActivation::TYPE_NEW_EMAIL_CONFIRMATION
|
||||
EmailActivation::TYPE_NEW_EMAIL_CONFIRMATION,
|
||||
)
|
||||
->one();
|
||||
}
|
||||
|
@ -17,6 +17,13 @@ use Webmozart\Assert\Assert;
|
||||
|
||||
class TwoFactorAuthInfo extends BaseAccountForm {
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* qr: string,
|
||||
* uri: string,
|
||||
* secret: string,
|
||||
* }
|
||||
*/
|
||||
public function getCredentials(): array {
|
||||
if (empty($this->getAccount()->otp_secret)) {
|
||||
$this->setOtpSecret();
|
||||
|
@ -32,11 +32,11 @@ class Module extends BaseModule implements BootstrapInterface {
|
||||
], false);
|
||||
}
|
||||
|
||||
public static function info($message) {
|
||||
public static function info($message): void {
|
||||
Yii::info($message, 'legacy-authserver');
|
||||
}
|
||||
|
||||
public static function error($message) {
|
||||
public static function error($message): void {
|
||||
Yii::info($message, 'legacy-authserver');
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,7 @@ use api\controllers\Controller;
|
||||
class IndexController extends Controller {
|
||||
|
||||
// TODO: симулировать для этого модуля обработчик 404 ошибок, как был в фалконе
|
||||
public function notFoundAction() {
|
||||
public function notFoundAction(): void {
|
||||
/*return $this->response
|
||||
->setStatusCode(404, 'Not Found')
|
||||
->setContent('Page not found. Check our <a href="http://docs.ely.by">documentation site</a>.');*/
|
||||
|
@ -5,21 +5,14 @@ namespace api\modules\authserver\models;
|
||||
|
||||
use common\models\Account;
|
||||
|
||||
final class AuthenticateData {
|
||||
final readonly class AuthenticateData {
|
||||
|
||||
private Account $account;
|
||||
|
||||
private string $accessToken;
|
||||
|
||||
private string $clientToken;
|
||||
|
||||
private bool $requestUser;
|
||||
|
||||
public function __construct(Account $account, string $accessToken, string $clientToken, bool $requestUser) {
|
||||
$this->account = $account;
|
||||
$this->accessToken = $accessToken;
|
||||
$this->clientToken = $clientToken;
|
||||
$this->requestUser = $requestUser;
|
||||
public function __construct(
|
||||
private Account $account,
|
||||
private string $accessToken,
|
||||
private string $clientToken,
|
||||
private bool $requestUser,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getResponseData(bool $includeAvailableProfiles = false): array {
|
||||
|
@ -17,6 +17,7 @@ use common\models\OauthSession;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Webmozart\Assert\Assert;
|
||||
use Yii;
|
||||
use yii\db\Exception;
|
||||
|
||||
class AuthenticationForm extends ApiForm {
|
||||
|
||||
@ -50,8 +51,8 @@ class AuthenticationForm extends ApiForm {
|
||||
|
||||
/**
|
||||
* @return AuthenticateData
|
||||
* @throws \api\modules\authserver\exceptions\IllegalArgumentException
|
||||
* @throws \api\modules\authserver\exceptions\ForbiddenOperationException
|
||||
* @throws ForbiddenOperationException
|
||||
* @throws Exception
|
||||
*/
|
||||
public function authenticate(): AuthenticateData {
|
||||
// This validating method will throw an exception in case when validation will not pass successfully
|
||||
@ -61,7 +62,7 @@ class AuthenticationForm extends ApiForm {
|
||||
|
||||
// The previous authorization server implementation used the nickname field instead of username,
|
||||
// so we keep such behavior
|
||||
$attribute = strpos($this->username, '@') === false ? 'nickname' : 'email';
|
||||
$attribute = !str_contains($this->username, '@') ? 'nickname' : 'email';
|
||||
|
||||
$password = $this->password;
|
||||
$totp = null;
|
||||
@ -113,7 +114,7 @@ class AuthenticationForm extends ApiForm {
|
||||
$account = $loginForm->getAccount();
|
||||
$clientToken = $this->clientToken ?: Uuid::uuid4()->toString();
|
||||
$token = Yii::$app->tokensFactory->createForMinecraftAccount($account, $clientToken);
|
||||
$dataModel = new AuthenticateData($account, (string)$token, $clientToken, (bool)$this->requestUser);
|
||||
$dataModel = new AuthenticateData($account, $token->toString(), $clientToken, (bool)$this->requestUser);
|
||||
/** @var OauthSession|null $minecraftOauthSession */
|
||||
$minecraftOauthSession = $account->getOauthSessions()
|
||||
->andWhere(['client_id' => OauthClient::UNAUTHORIZED_MINECRAFT_GAME_LAUNCHER])
|
||||
|
@ -75,7 +75,7 @@ class RefreshTokenForm extends ApiForm {
|
||||
$minecraftOauthSession->last_used_at = time();
|
||||
Assert::true($minecraftOauthSession->save());
|
||||
|
||||
return new AuthenticateData($account, (string)$token, $this->clientToken, (bool)$this->requestUser);
|
||||
return new AuthenticateData($account, $token->toString(), $this->clientToken, (bool)$this->requestUser);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ class SignoutForm extends ApiForm {
|
||||
|
||||
// The previous authorization server implementation used the nickname field instead of username,
|
||||
// so we keep such behavior
|
||||
$attribute = strpos($this->username, '@') === false ? 'nickname' : 'email';
|
||||
$attribute = !str_contains($this->username, '@') ? 'nickname' : 'email';
|
||||
|
||||
throw new ForbiddenOperationException("Invalid credentials. Invalid {$attribute} or password.");
|
||||
}
|
||||
|
@ -13,8 +13,8 @@ use yii\validators\Validator;
|
||||
|
||||
class AccessTokenValidator extends Validator {
|
||||
|
||||
private const INVALID_TOKEN = 'Invalid token.';
|
||||
private const TOKEN_EXPIRED = 'Token expired.';
|
||||
private const string INVALID_TOKEN = 'Invalid token.';
|
||||
private const string TOKEN_EXPIRED = 'Token expired.';
|
||||
|
||||
public bool $verifyExpiration = true;
|
||||
|
||||
@ -27,7 +27,7 @@ class AccessTokenValidator extends Validator {
|
||||
protected function validateValue($value): ?array {
|
||||
try {
|
||||
$token = Yii::$app->tokens->parse($value);
|
||||
} catch (Exception $e) {
|
||||
} catch (Exception) {
|
||||
throw new ForbiddenOperationException(self::INVALID_TOKEN);
|
||||
}
|
||||
|
||||
|
@ -10,7 +10,7 @@ class Module extends \yii\base\Module implements BootstrapInterface {
|
||||
/**
|
||||
* @param \yii\base\Application $app the application currently running
|
||||
*/
|
||||
public function bootstrap($app) {
|
||||
public function bootstrap($app): void {
|
||||
$app->getUrlManager()->addRules([
|
||||
'/internal/<controller>/<accountId>/<action>' => "{$this->id}/<controller>/<action>",
|
||||
], false);
|
||||
|
@ -20,11 +20,9 @@ class AccountsController extends Controller {
|
||||
'actions' => ['info'],
|
||||
'allow' => true,
|
||||
'roles' => [P::OBTAIN_EXTENDED_ACCOUNT_INFO],
|
||||
'roleParams' => function() {
|
||||
return [
|
||||
'roleParams' => fn(): array => [
|
||||
'accountId' => 0,
|
||||
];
|
||||
},
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
@ -37,7 +35,7 @@ class AccountsController extends Controller {
|
||||
];
|
||||
}
|
||||
|
||||
public function actionInfo(int $id = null, string $username = null, string $uuid = null) {
|
||||
public function actionInfo(int $id = null, string $username = null, string $uuid = null): array {
|
||||
if ($id !== null) {
|
||||
$account = Account::findOne($id);
|
||||
} elseif ($username !== null) {
|
||||
|
@ -3,10 +3,10 @@ namespace api\modules\internal\helpers;
|
||||
|
||||
final class Error {
|
||||
|
||||
public const ACCOUNT_ALREADY_BANNED = 'error.account_already_banned';
|
||||
public const ACCOUNT_NOT_BANNED = 'error.account_not_banned';
|
||||
public const string ACCOUNT_ALREADY_BANNED = 'error.account_already_banned';
|
||||
public const string ACCOUNT_NOT_BANNED = 'error.account_not_banned';
|
||||
|
||||
public const ACCOUNT_ALREADY_DELETED = 'error.account_already_deleted';
|
||||
public const ACCOUNT_NOT_DELETED = 'error.account_not_deleted';
|
||||
public const string ACCOUNT_ALREADY_DELETED = 'error.account_already_deleted';
|
||||
public const string ACCOUNT_NOT_DELETED = 'error.account_not_deleted';
|
||||
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ class ApiController extends Controller {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/** @var Account|null $record */
|
||||
/** @var Account|null $account */
|
||||
$account = Account::findOne(['username' => $username]);
|
||||
}
|
||||
|
||||
@ -64,7 +64,7 @@ class ApiController extends Controller {
|
||||
public function actionUsernamesByUuid(string $uuid) {
|
||||
try {
|
||||
$uuid = Uuid::fromString($uuid)->toString();
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
} catch (\InvalidArgumentException) {
|
||||
return $this->illegalArgumentResponse('Invalid uuid format.');
|
||||
}
|
||||
|
||||
|
@ -27,11 +27,9 @@ class AuthorizationController extends Controller {
|
||||
'allow' => true,
|
||||
'actions' => ['complete'],
|
||||
'roles' => [P::COMPLETE_OAUTH_FLOW],
|
||||
'roleParams' => function() {
|
||||
return [
|
||||
'roleParams' => fn(): array => [
|
||||
'accountId' => Yii::$app->user->identity->getAccount()->id,
|
||||
];
|
||||
},
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
@ -38,7 +38,7 @@ class ClientsController extends Controller {
|
||||
'actions' => ['update', 'delete', 'reset'],
|
||||
'allow' => true,
|
||||
'permissions' => [P::MANAGE_OAUTH_CLIENTS],
|
||||
'roleParams' => fn() => [
|
||||
'roleParams' => fn(): array => [
|
||||
'clientId' => Yii::$app->request->get('clientId'),
|
||||
],
|
||||
],
|
||||
@ -46,7 +46,7 @@ class ClientsController extends Controller {
|
||||
'actions' => ['get'],
|
||||
'allow' => true,
|
||||
'permissions' => [P::VIEW_OAUTH_CLIENTS],
|
||||
'roleParams' => fn() => [
|
||||
'roleParams' => fn(): array => [
|
||||
'clientId' => Yii::$app->request->get('clientId'),
|
||||
],
|
||||
],
|
||||
@ -54,7 +54,7 @@ class ClientsController extends Controller {
|
||||
'actions' => ['get-per-account'],
|
||||
'allow' => true,
|
||||
'permissions' => [P::VIEW_OAUTH_CLIENTS],
|
||||
'roleParams' => fn() => [
|
||||
'roleParams' => fn(): array => [
|
||||
'accountId' => Yii::$app->request->get('accountId'),
|
||||
],
|
||||
],
|
||||
@ -62,7 +62,7 @@ class ClientsController extends Controller {
|
||||
'actions' => ['get-authorized-clients', 'revoke-client'],
|
||||
'allow' => true,
|
||||
'permissions' => [P::MANAGE_OAUTH_SESSIONS],
|
||||
'roleParams' => fn() => [
|
||||
'roleParams' => fn(): array => [
|
||||
'accountId' => Yii::$app->request->get('accountId'),
|
||||
],
|
||||
],
|
||||
|
@ -27,7 +27,7 @@ class IdentityController extends Controller {
|
||||
$account = $identity->getAccount();
|
||||
if ($account === null) {
|
||||
Yii::$app->sentry->captureMessage('Unexpected lack of account', [
|
||||
'identityType' => get_class($identity),
|
||||
'identityType' => $identity::class,
|
||||
'userId' => $identity->getId(),
|
||||
'assignedPermissions' => $identity->getAssignedPermissions(),
|
||||
], [
|
||||
|
@ -6,14 +6,12 @@ use yii\base\Exception;
|
||||
|
||||
class UnsupportedOauthClientType extends Exception implements OauthException {
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $type;
|
||||
|
||||
public function __construct(string $type, int $code = 0, Throwable $previous = null) {
|
||||
public function __construct(
|
||||
private readonly string $type,
|
||||
int $code = 0,
|
||||
Throwable $previous = null,
|
||||
) {
|
||||
parent::__construct('Unsupported oauth client type', $code, $previous);
|
||||
$this->type = $type;
|
||||
}
|
||||
|
||||
public function getType(): string {
|
||||
|
@ -9,7 +9,7 @@ use common\models\Account;
|
||||
|
||||
class IdentityInfo extends BaseAccountForm {
|
||||
|
||||
private $model;
|
||||
private readonly AccountInfo $model;
|
||||
|
||||
public function __construct(Account $account, array $config = []) {
|
||||
parent::__construct($account, $config);
|
||||
|
@ -12,10 +12,7 @@ use yii\helpers\Inflector;
|
||||
|
||||
class OauthClientForm {
|
||||
|
||||
/**
|
||||
* @var OauthClient
|
||||
*/
|
||||
private $client;
|
||||
private readonly OauthClient $client;
|
||||
|
||||
public function __construct(OauthClient $client) {
|
||||
if ($client->type === null) {
|
||||
|
@ -15,23 +15,20 @@ class OauthClientFormFactory {
|
||||
* @throws UnsupportedOauthClientType
|
||||
*/
|
||||
public static function create(OauthClient $client): OauthClientTypeForm {
|
||||
switch ($client->type) {
|
||||
case OauthClient::TYPE_APPLICATION:
|
||||
return new ApplicationType([
|
||||
return match ($client->type) {
|
||||
OauthClient::TYPE_APPLICATION => new ApplicationType([
|
||||
'name' => $client->name,
|
||||
'websiteUrl' => $client->website_url,
|
||||
'description' => $client->description,
|
||||
'redirectUri' => $client->redirect_uri,
|
||||
]);
|
||||
case OauthClient::TYPE_MINECRAFT_SERVER:
|
||||
return new MinecraftServerType([
|
||||
]),
|
||||
OauthClient::TYPE_MINECRAFT_SERVER => new MinecraftServerType([
|
||||
'name' => $client->name,
|
||||
'websiteUrl' => $client->website_url,
|
||||
'minecraftServerIp' => $client->minecraft_server_ip,
|
||||
]);
|
||||
}
|
||||
|
||||
throw new UnsupportedOauthClientType($client->type);
|
||||
]),
|
||||
default => throw new UnsupportedOauthClientType($client->type),
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -13,22 +13,19 @@ use GuzzleHttp\Psr7\Response;
|
||||
use League\OAuth2\Server\AuthorizationServer;
|
||||
use League\OAuth2\Server\Entities\ScopeEntityInterface;
|
||||
use League\OAuth2\Server\Exception\OAuthServerException;
|
||||
use League\OAuth2\Server\RequestTypes\AuthorizationRequest;
|
||||
use League\OAuth2\Server\RequestTypes\AuthorizationRequestInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Webmozart\Assert\Assert;
|
||||
use Yii;
|
||||
|
||||
class OauthProcess {
|
||||
|
||||
private const INTERNAL_PERMISSIONS_TO_PUBLIC_SCOPES = [
|
||||
private const array INTERNAL_PERMISSIONS_TO_PUBLIC_SCOPES = [
|
||||
P::OBTAIN_OWN_ACCOUNT_INFO => 'account_info',
|
||||
P::OBTAIN_ACCOUNT_EMAIL => 'account_email',
|
||||
];
|
||||
|
||||
private AuthorizationServer $server;
|
||||
|
||||
public function __construct(AuthorizationServer $server) {
|
||||
$this->server = $server;
|
||||
public function __construct(private readonly AuthorizationServer $server) {
|
||||
}
|
||||
|
||||
/**
|
||||
@ -96,17 +93,12 @@ class OauthProcess {
|
||||
$canBeAutoApproved = $this->canBeAutoApproved($account, $client, $authRequest);
|
||||
$acceptParam = ((array)$request->getParsedBody())['accept'] ?? null;
|
||||
if ($acceptParam === null && !$canBeAutoApproved) {
|
||||
Yii::$app->statsd->inc('oauth.complete.approve_required');
|
||||
throw $this->createAcceptRequiredException();
|
||||
}
|
||||
|
||||
Yii::$app->statsd->inc('oauth.complete.approve_required');
|
||||
|
||||
if ($acceptParam === null && $canBeAutoApproved) {
|
||||
$approved = true;
|
||||
} else {
|
||||
$approved = in_array($acceptParam, [1, '1', true, 'true'], true);
|
||||
}
|
||||
|
||||
// At this point if the $acceptParam is an empty, then the application can be auto approved
|
||||
$approved = $acceptParam === null || in_array($acceptParam, [1, '1', true, 'true'], true);
|
||||
if ($approved) {
|
||||
$this->storeOauthSession($account, $client, $authRequest);
|
||||
}
|
||||
@ -163,7 +155,7 @@ class OauthProcess {
|
||||
Yii::$app->statsd->inc("oauth.issueToken_{$grantType}.attempt");
|
||||
|
||||
$shouldIssueRefreshToken = false;
|
||||
$this->server->getEmitter()->addOneTimeListener(RequestedRefreshToken::class, function() use (&$shouldIssueRefreshToken) {
|
||||
$this->server->getEmitter()->subscribeOnceTo(RequestedRefreshToken::class, function() use (&$shouldIssueRefreshToken): void {
|
||||
$shouldIssueRefreshToken = true;
|
||||
});
|
||||
|
||||
@ -207,14 +199,8 @@ class OauthProcess {
|
||||
/**
|
||||
* The method checks whether the current user can be automatically authorized for the specified client
|
||||
* without requesting access to the necessary list of scopes
|
||||
*
|
||||
* @param Account $account
|
||||
* @param OauthClient $client
|
||||
* @param AuthorizationRequest $request
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function canBeAutoApproved(Account $account, OauthClient $client, AuthorizationRequest $request): bool {
|
||||
private function canBeAutoApproved(Account $account, OauthClient $client, AuthorizationRequestInterface $request): bool {
|
||||
if ($client->is_trusted) {
|
||||
return true;
|
||||
}
|
||||
@ -231,7 +217,7 @@ class OauthProcess {
|
||||
return empty(array_diff($this->getScopesList($request), $session->getScopes()));
|
||||
}
|
||||
|
||||
private function storeOauthSession(Account $account, OauthClient $client, AuthorizationRequest $request): void {
|
||||
private function storeOauthSession(Account $account, OauthClient $client, AuthorizationRequestInterface $request): void {
|
||||
$session = $this->findOauthSession($account, $client);
|
||||
if ($session === null) {
|
||||
$session = new OauthSession();
|
||||
@ -301,7 +287,7 @@ class OauthProcess {
|
||||
];
|
||||
|
||||
if ($e->hasRedirect()) {
|
||||
$response['redirectUri'] = $e->getRedirectUri();
|
||||
$response['redirectUri'] = $e->getRedirectUri() . http_build_query($e->getPayload());
|
||||
}
|
||||
|
||||
if ($e->getHttpStatusCode() !== 200) {
|
||||
@ -345,12 +331,11 @@ class OauthProcess {
|
||||
return new OAuthServerException('Client must accept authentication request.', 0, 'accept_required', 401);
|
||||
}
|
||||
|
||||
private function getScopesList(AuthorizationRequest $request): array {
|
||||
return array_values(array_map(function(ScopeEntityInterface $scope): string {
|
||||
return $scope->getIdentifier();
|
||||
}, $request->getScopes()));
|
||||
private function getScopesList(AuthorizationRequestInterface $request): array {
|
||||
return array_values(array_map(fn(ScopeEntityInterface $scope): string => $scope->getIdentifier(), $request->getScopes()));
|
||||
}
|
||||
|
||||
/** @noinspection PhpIncompatibleReturnTypeInspection */
|
||||
private function findOauthSession(Account $account, OauthClient $client): ?OauthSession {
|
||||
return $account->getOauthSessions()->andWhere(['client_id' => $client->id])->one();
|
||||
}
|
||||
|
@ -9,11 +9,11 @@ class Module extends \yii\base\Module {
|
||||
|
||||
public $defaultRoute = 'session';
|
||||
|
||||
public static function info($message) {
|
||||
public static function info($message): void {
|
||||
Yii::info($message, 'session');
|
||||
}
|
||||
|
||||
public static function error($message) {
|
||||
public static function error($message): void {
|
||||
Yii::info($message, 'session');
|
||||
}
|
||||
|
||||
|
@ -95,7 +95,7 @@ class SessionController extends Controller {
|
||||
$hasJoinedForm = new HasJoinedForm($protocol);
|
||||
try {
|
||||
$hasJoinedForm->hasJoined();
|
||||
} catch (ForbiddenOperationException $e) {
|
||||
} catch (ForbiddenOperationException) {
|
||||
return 'NO';
|
||||
} catch (SessionServerException $e) {
|
||||
Yii::$app->response->statusCode = $e->statusCode;
|
||||
@ -116,7 +116,7 @@ class SessionController extends Controller {
|
||||
public function actionProfile(string $uuid, string $unsigned = null): ?array {
|
||||
try {
|
||||
$uuid = Uuid::fromString($uuid)->toString();
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
} catch (\InvalidArgumentException) {
|
||||
throw new IllegalArgumentException('Invalid uuid format.');
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,7 @@ class RateLimiter extends \yii\filters\RateLimiter {
|
||||
|
||||
private $server;
|
||||
|
||||
public function init() {
|
||||
public function init(): void {
|
||||
parent::init();
|
||||
if ($this->authserverDomain === null) {
|
||||
throw new InvalidConfigException('authserverDomain param is required');
|
||||
@ -30,10 +30,10 @@ class RateLimiter extends \yii\filters\RateLimiter {
|
||||
*/
|
||||
public function beforeAction($action) {
|
||||
$this->checkRateLimit(
|
||||
null,
|
||||
null, // @phpstan-ignore argument.type (at this moment we don't have any specific identity, so pass null (yea, it's hacky))
|
||||
$this->request ?: Yii::$app->getRequest(),
|
||||
$this->response ?: Yii::$app->getResponse(),
|
||||
$action
|
||||
$action,
|
||||
);
|
||||
|
||||
return true;
|
||||
@ -43,8 +43,8 @@ class RateLimiter extends \yii\filters\RateLimiter {
|
||||
* @inheritdoc
|
||||
* @throws TooManyRequestsHttpException
|
||||
*/
|
||||
public function checkRateLimit($user, $request, $response, $action) {
|
||||
if (parse_url($request->getHostInfo(), PHP_URL_HOST) === $this->authserverDomain) {
|
||||
public function checkRateLimit($user, $request, $response, $action): void {
|
||||
if (parse_url((string)$request->getHostInfo(), PHP_URL_HOST) === $this->authserverDomain) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -66,11 +66,7 @@ class RateLimiter extends \yii\filters\RateLimiter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @return OauthClient|null
|
||||
*/
|
||||
protected function getServer(Request $request) {
|
||||
protected function getServer(Request $request): ?OauthClient {
|
||||
$serverId = $request->get('server_id');
|
||||
if ($serverId === null) {
|
||||
$this->server = false;
|
||||
@ -78,7 +74,6 @@ class RateLimiter extends \yii\filters\RateLimiter {
|
||||
}
|
||||
|
||||
if ($this->server === null) {
|
||||
/** @var OauthClient|null $server */
|
||||
$this->server = OauthClient::findOne($serverId);
|
||||
// TODO: убедится, что это сервер
|
||||
if ($this->server === null) {
|
||||
@ -93,7 +88,7 @@ class RateLimiter extends \yii\filters\RateLimiter {
|
||||
return $this->server;
|
||||
}
|
||||
|
||||
protected function buildKey($ip): string {
|
||||
protected function buildKey(string $ip): string {
|
||||
return 'sessionserver:ratelimit:' . $ip;
|
||||
}
|
||||
|
||||
|
@ -14,14 +14,11 @@ use yii\base\Model;
|
||||
|
||||
class HasJoinedForm extends Model {
|
||||
|
||||
/**
|
||||
* @var HasJoinedInterface
|
||||
*/
|
||||
private $protocol;
|
||||
|
||||
public function __construct(HasJoinedInterface $protocol, array $config = []) {
|
||||
public function __construct(
|
||||
private readonly HasJoinedInterface $protocol,
|
||||
array $config = [],
|
||||
) {
|
||||
parent::__construct($config);
|
||||
$this->protocol = $protocol;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -9,7 +9,6 @@ use api\modules\session\models\protocols\JoinInterface;
|
||||
use api\modules\session\Module as Session;
|
||||
use api\modules\session\validators\RequiredValidator;
|
||||
use api\rbac\Permissions as P;
|
||||
use Closure;
|
||||
use common\helpers\StringHelper;
|
||||
use common\models\Account;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
@ -20,35 +19,32 @@ use yii\web\UnauthorizedHttpException;
|
||||
|
||||
class JoinForm extends Model {
|
||||
|
||||
public $accessToken;
|
||||
public mixed $accessToken = null;
|
||||
|
||||
public $selectedProfile;
|
||||
public mixed $selectedProfile = null;
|
||||
|
||||
public $serverId;
|
||||
public mixed $serverId = null;
|
||||
|
||||
/**
|
||||
* @var Account|null
|
||||
*/
|
||||
private $account;
|
||||
private ?Account $account = null;
|
||||
|
||||
/**
|
||||
* @var JoinInterface
|
||||
*/
|
||||
private $protocol;
|
||||
|
||||
public function __construct(JoinInterface $protocol, array $config = []) {
|
||||
public function __construct(
|
||||
private readonly JoinInterface $protocol,
|
||||
array $config = [],
|
||||
) {
|
||||
parent::__construct($config);
|
||||
$this->protocol = $protocol;
|
||||
$this->accessToken = $protocol->getAccessToken();
|
||||
$this->selectedProfile = $protocol->getSelectedProfile();
|
||||
$this->serverId = $protocol->getServerId();
|
||||
$this->accessToken = $this->protocol->getAccessToken();
|
||||
$this->selectedProfile = $this->protocol->getSelectedProfile();
|
||||
$this->serverId = $this->protocol->getServerId();
|
||||
}
|
||||
|
||||
public function rules(): array {
|
||||
return [
|
||||
[['accessToken', 'serverId'], RequiredValidator::class],
|
||||
[['accessToken', 'selectedProfile'], Closure::fromCallable([$this, 'validateUuid'])],
|
||||
[['accessToken'], Closure::fromCallable([$this, 'validateAccessToken'])],
|
||||
[['accessToken', 'selectedProfile'], $this->validateUuid(...)],
|
||||
[['accessToken'], $this->validateAccessToken(...)],
|
||||
];
|
||||
}
|
||||
|
||||
@ -147,7 +143,7 @@ class JoinForm extends Model {
|
||||
throw new ForbiddenOperationException('Wrong selected_profile.');
|
||||
}
|
||||
|
||||
if (!$isUuid && mb_strtolower($account->username) !== mb_strtolower($selectedProfile)) {
|
||||
if (!$isUuid && mb_strtolower($account->username) !== mb_strtolower((string)$selectedProfile)) {
|
||||
Session::error("User with access_token = '{$accessToken}' trying to join with identity = '{$selectedProfile}', but access_token issued to account with username = '{$account->username}'.");
|
||||
Yii::$app->statsd->inc('sessionserver.join.fail_username_mismatch');
|
||||
|
||||
|
@ -1,36 +1,35 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\modules\session\models;
|
||||
|
||||
use common\models\Account;
|
||||
use Yii;
|
||||
|
||||
class SessionModel {
|
||||
final readonly class SessionModel {
|
||||
|
||||
private const KEY_TIME = 120; // 2 min
|
||||
private const int KEY_TIME = 120; // 2 min
|
||||
|
||||
public $username;
|
||||
|
||||
public $serverId;
|
||||
|
||||
public function __construct(string $username, string $serverId) {
|
||||
$this->username = $username;
|
||||
$this->serverId = $serverId;
|
||||
public function __construct(
|
||||
public string $username,
|
||||
public string $serverId,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function find(string $username, string $serverId): ?self {
|
||||
$key = static::buildKey($username, $serverId);
|
||||
$key = self::buildKey($username, $serverId);
|
||||
$result = Yii::$app->redis->get($key);
|
||||
if (!$result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = json_decode($result, true);
|
||||
$data = json_decode((string)$result, true);
|
||||
|
||||
return new static($data['username'], $data['serverId']);
|
||||
return new self($data['username'], $data['serverId']);
|
||||
}
|
||||
|
||||
public function save() {
|
||||
$key = static::buildKey($this->username, $this->serverId);
|
||||
public function save(): mixed {
|
||||
$key = self::buildKey($this->username, $this->serverId);
|
||||
$data = json_encode([
|
||||
'username' => $this->username,
|
||||
'serverId' => $this->serverId,
|
||||
@ -39,15 +38,15 @@ class SessionModel {
|
||||
return Yii::$app->redis->setex($key, self::KEY_TIME, $data);
|
||||
}
|
||||
|
||||
public function delete() {
|
||||
return Yii::$app->redis->del(static::buildKey($this->username, $this->serverId));
|
||||
public function delete(): mixed {
|
||||
return Yii::$app->redis->del(self::buildKey($this->username, $this->serverId));
|
||||
}
|
||||
|
||||
public function getAccount(): ?Account {
|
||||
return Account::findOne(['username' => $this->username]);
|
||||
}
|
||||
|
||||
protected static function buildKey($username, $serverId): string {
|
||||
protected static function buildKey(string $username, string $serverId): string {
|
||||
return md5('minecraft:join-server:' . mb_strtolower($username) . ':' . $serverId);
|
||||
}
|
||||
|
||||
|
@ -3,9 +3,9 @@ namespace api\modules\session\models\protocols;
|
||||
|
||||
abstract class BaseHasJoined implements HasJoinedInterface {
|
||||
|
||||
private $username;
|
||||
private readonly string $username;
|
||||
|
||||
private $serverId;
|
||||
private readonly string $serverId;
|
||||
|
||||
public function __construct(string $username, string $serverId) {
|
||||
$this->username = trim($username);
|
||||
@ -21,11 +21,7 @@ abstract class BaseHasJoined implements HasJoinedInterface {
|
||||
}
|
||||
|
||||
public function validate(): bool {
|
||||
return !$this->isEmpty($this->username) && !$this->isEmpty($this->serverId);
|
||||
}
|
||||
|
||||
private function isEmpty($value): bool {
|
||||
return $value === null || $value === '';
|
||||
return $this->username !== '' && $this->serverId !== '';
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -3,15 +3,15 @@ namespace api\modules\session\models\protocols;
|
||||
|
||||
class LegacyJoin extends BaseJoin {
|
||||
|
||||
private $user;
|
||||
private readonly string $user;
|
||||
|
||||
private $sessionId;
|
||||
private string $sessionId;
|
||||
|
||||
private $serverId;
|
||||
private readonly string $serverId;
|
||||
|
||||
private $accessToken;
|
||||
|
||||
private $uuid;
|
||||
private ?string $uuid = null;
|
||||
|
||||
public function __construct(string $user, string $sessionId, string $serverId) {
|
||||
$this->user = trim($user);
|
||||
@ -46,7 +46,7 @@ class LegacyJoin extends BaseJoin {
|
||||
* Split by ':' to take into account authorization in modern launchers and login to an legacy version of the game.
|
||||
* The sessionId is passed on as "token:{accessToken}:{uuid}", so it needs to be processed
|
||||
*/
|
||||
private function parseSessionId(string $sessionId) {
|
||||
private function parseSessionId(string $sessionId): void {
|
||||
$parts = explode(':', $sessionId);
|
||||
if (count($parts) === 3) {
|
||||
$this->accessToken = $parts[1];
|
||||
|
@ -3,11 +3,11 @@ namespace api\modules\session\models\protocols;
|
||||
|
||||
class ModernJoin extends BaseJoin {
|
||||
|
||||
private $accessToken;
|
||||
private readonly string $accessToken;
|
||||
|
||||
private $selectedProfile;
|
||||
private readonly string $selectedProfile;
|
||||
|
||||
private $serverId;
|
||||
private readonly string $serverId;
|
||||
|
||||
public function __construct(string $accessToken, string $selectedProfile, string $serverId) {
|
||||
$this->accessToken = trim($accessToken);
|
||||
|
@ -11,10 +11,9 @@ class RequiredValidator extends \yii\validators\RequiredValidator {
|
||||
|
||||
/**
|
||||
* @param string $value
|
||||
* @return null
|
||||
* @throws \api\modules\session\exceptions\SessionServerException
|
||||
*/
|
||||
protected function validateValue($value) {
|
||||
protected function validateValue($value): ?array {
|
||||
if (parent::validateValue($value) !== null) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
|
@ -16,16 +16,14 @@ class Manager extends PhpManager {
|
||||
* In Yii2, the mechanism of recursive permissions checking requires that the array
|
||||
* with permissions must be indexed by the keys of these permissions.
|
||||
*
|
||||
* @param string $accessToken
|
||||
* @return string[]
|
||||
* @return array<string, \yii\rbac\Assignment>
|
||||
*/
|
||||
public function getAssignments($accessToken): array {
|
||||
public function getAssignments($userId): array {
|
||||
$identity = Yii::$app->user->getIdentity();
|
||||
if ($identity === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
/** @noinspection NullPointerExceptionInspection */
|
||||
$rawPermissions = $identity->getAssignedPermissions();
|
||||
$result = [];
|
||||
foreach ($rawPermissions as $name) {
|
||||
|
@ -6,42 +6,42 @@ namespace api\rbac;
|
||||
final class Permissions {
|
||||
|
||||
// Top level Controller permissions
|
||||
public const OBTAIN_ACCOUNT_INFO = 'obtain_account_info';
|
||||
public const CHANGE_ACCOUNT_LANGUAGE = 'change_account_language';
|
||||
public const CHANGE_ACCOUNT_USERNAME = 'change_account_username';
|
||||
public const CHANGE_ACCOUNT_PASSWORD = 'change_account_password';
|
||||
public const CHANGE_ACCOUNT_EMAIL = 'change_account_email';
|
||||
public const MANAGE_TWO_FACTOR_AUTH = 'manage_two_factor_auth';
|
||||
public const DELETE_ACCOUNT = 'delete_account';
|
||||
public const RESTORE_ACCOUNT = 'restore_account';
|
||||
public const BLOCK_ACCOUNT = 'block_account';
|
||||
public const COMPLETE_OAUTH_FLOW = 'complete_oauth_flow';
|
||||
public const MANAGE_OAUTH_SESSIONS = 'manage_oauth_sessions';
|
||||
public const CREATE_OAUTH_CLIENTS = 'create_oauth_clients';
|
||||
public const VIEW_OAUTH_CLIENTS = 'view_oauth_clients';
|
||||
public const MANAGE_OAUTH_CLIENTS = 'manage_oauth_clients';
|
||||
public const string OBTAIN_ACCOUNT_INFO = 'obtain_account_info';
|
||||
public const string CHANGE_ACCOUNT_LANGUAGE = 'change_account_language';
|
||||
public const string CHANGE_ACCOUNT_USERNAME = 'change_account_username';
|
||||
public const string CHANGE_ACCOUNT_PASSWORD = 'change_account_password';
|
||||
public const string CHANGE_ACCOUNT_EMAIL = 'change_account_email';
|
||||
public const string MANAGE_TWO_FACTOR_AUTH = 'manage_two_factor_auth';
|
||||
public const string DELETE_ACCOUNT = 'delete_account';
|
||||
public const string RESTORE_ACCOUNT = 'restore_account';
|
||||
public const string BLOCK_ACCOUNT = 'block_account';
|
||||
public const string COMPLETE_OAUTH_FLOW = 'complete_oauth_flow';
|
||||
public const string MANAGE_OAUTH_SESSIONS = 'manage_oauth_sessions';
|
||||
public const string CREATE_OAUTH_CLIENTS = 'create_oauth_clients';
|
||||
public const string VIEW_OAUTH_CLIENTS = 'view_oauth_clients';
|
||||
public const string MANAGE_OAUTH_CLIENTS = 'manage_oauth_clients';
|
||||
|
||||
// Personal level controller permissions
|
||||
public const OBTAIN_OWN_ACCOUNT_INFO = 'obtain_own_account_info';
|
||||
public const OBTAIN_OWN_EXTENDED_ACCOUNT_INFO = 'obtain_own_extended_account_info';
|
||||
public const CHANGE_OWN_ACCOUNT_LANGUAGE = 'change_own_account_language';
|
||||
public const ACCEPT_NEW_PROJECT_RULES = 'accept_new_project_rules';
|
||||
public const CHANGE_OWN_ACCOUNT_USERNAME = 'change_own_account_username';
|
||||
public const CHANGE_OWN_ACCOUNT_PASSWORD = 'change_own_account_password';
|
||||
public const CHANGE_OWN_ACCOUNT_EMAIL = 'change_own_account_email';
|
||||
public const MANAGE_OWN_TWO_FACTOR_AUTH = 'manage_own_two_factor_auth';
|
||||
public const DELETE_OWN_ACCOUNT = 'delete_own_account';
|
||||
public const RESTORE_OWN_ACCOUNT = 'restore_own_account';
|
||||
public const MINECRAFT_SERVER_SESSION = 'minecraft_server_session';
|
||||
public const MANAGE_OWN_OAUTH_SESSIONS = 'manage_own_oauth_sessions';
|
||||
public const VIEW_OWN_OAUTH_CLIENTS = 'view_own_oauth_clients';
|
||||
public const MANAGE_OWN_OAUTH_CLIENTS = 'manage_own_oauth_clients';
|
||||
public const string OBTAIN_OWN_ACCOUNT_INFO = 'obtain_own_account_info';
|
||||
public const string OBTAIN_OWN_EXTENDED_ACCOUNT_INFO = 'obtain_own_extended_account_info';
|
||||
public const string CHANGE_OWN_ACCOUNT_LANGUAGE = 'change_own_account_language';
|
||||
public const string ACCEPT_NEW_PROJECT_RULES = 'accept_new_project_rules';
|
||||
public const string CHANGE_OWN_ACCOUNT_USERNAME = 'change_own_account_username';
|
||||
public const string CHANGE_OWN_ACCOUNT_PASSWORD = 'change_own_account_password';
|
||||
public const string CHANGE_OWN_ACCOUNT_EMAIL = 'change_own_account_email';
|
||||
public const string MANAGE_OWN_TWO_FACTOR_AUTH = 'manage_own_two_factor_auth';
|
||||
public const string DELETE_OWN_ACCOUNT = 'delete_own_account';
|
||||
public const string RESTORE_OWN_ACCOUNT = 'restore_own_account';
|
||||
public const string MINECRAFT_SERVER_SESSION = 'minecraft_server_session';
|
||||
public const string MANAGE_OWN_OAUTH_SESSIONS = 'manage_own_oauth_sessions';
|
||||
public const string VIEW_OWN_OAUTH_CLIENTS = 'view_own_oauth_clients';
|
||||
public const string MANAGE_OWN_OAUTH_CLIENTS = 'manage_own_oauth_clients';
|
||||
|
||||
// Data permissions
|
||||
public const OBTAIN_ACCOUNT_EMAIL = 'obtain_account_email';
|
||||
public const OBTAIN_EXTENDED_ACCOUNT_INFO = 'obtain_account_extended_info';
|
||||
public const string OBTAIN_ACCOUNT_EMAIL = 'obtain_account_email';
|
||||
public const string OBTAIN_EXTENDED_ACCOUNT_INFO = 'obtain_account_extended_info';
|
||||
|
||||
// Service permissions
|
||||
public const ESCAPE_IDENTITY_VERIFICATION = 'escape_identity_verification';
|
||||
public const string ESCAPE_IDENTITY_VERIFICATION = 'escape_identity_verification';
|
||||
|
||||
}
|
||||
|
@ -5,6 +5,6 @@ namespace api\rbac;
|
||||
|
||||
final class Roles {
|
||||
|
||||
public const ACCOUNTS_WEB_USER = 'accounts_web_user';
|
||||
public const string ACCOUNTS_WEB_USER = 'accounts_web_user';
|
||||
|
||||
}
|
||||
|
@ -36,19 +36,6 @@ class RequestParser implements RequestParserInterface {
|
||||
$parser = Yii::createObject(JsonParser::class);
|
||||
$parser->throwException = false;
|
||||
$result = $parser->parse($rawBody, $contentType);
|
||||
if (is_string($result)) {
|
||||
Yii::$app->sentry->captureMessage('Received an empty $result from the parser', [
|
||||
'inputText' => $rawBody,
|
||||
'inputTextLength' => mb_strlen($rawBody),
|
||||
'outputText' => $result,
|
||||
'contentType' => $contentType,
|
||||
], [
|
||||
'level' => 'warning',
|
||||
]);
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!empty($result)) {
|
||||
return $result;
|
||||
}
|
||||
|
@ -3,11 +3,11 @@ namespace api\tests\_pages;
|
||||
|
||||
class AccountsRoute extends BasePage {
|
||||
|
||||
public function get(int $accountId) {
|
||||
public function get(int $accountId): void {
|
||||
$this->getActor()->sendGET("/api/v1/accounts/{$accountId}");
|
||||
}
|
||||
|
||||
public function changePassword(int $accountId, $currentPassword = null, $newPassword = null, $newRePassword = null) {
|
||||
public function changePassword(int $accountId, $currentPassword = null, $newPassword = null, $newRePassword = null): void {
|
||||
$this->getActor()->sendPOST("/api/v1/accounts/{$accountId}/password", [
|
||||
'password' => $currentPassword,
|
||||
'newPassword' => $newPassword,
|
||||
@ -15,65 +15,65 @@ class AccountsRoute extends BasePage {
|
||||
]);
|
||||
}
|
||||
|
||||
public function changeUsername(int $accountId, $currentPassword = null, $newUsername = null) {
|
||||
public function changeUsername(int $accountId, $currentPassword = null, $newUsername = null): void {
|
||||
$this->getActor()->sendPOST("/api/v1/accounts/{$accountId}/username", [
|
||||
'password' => $currentPassword,
|
||||
'username' => $newUsername,
|
||||
]);
|
||||
}
|
||||
|
||||
public function changeEmailInitialize(int $accountId, $password = '') {
|
||||
public function changeEmailInitialize(int $accountId, $password = ''): void {
|
||||
$this->getActor()->sendPOST("/api/v1/accounts/{$accountId}/email-verification", [
|
||||
'password' => $password,
|
||||
]);
|
||||
}
|
||||
|
||||
public function changeEmailSubmitNewEmail(int $accountId, $key = null, $email = null) {
|
||||
public function changeEmailSubmitNewEmail(int $accountId, $key = null, $email = null): void {
|
||||
$this->getActor()->sendPOST("/api/v1/accounts/{$accountId}/new-email-verification", [
|
||||
'key' => $key,
|
||||
'email' => $email,
|
||||
]);
|
||||
}
|
||||
|
||||
public function changeEmail(int $accountId, $key = null) {
|
||||
public function changeEmail(int $accountId, $key = null): void {
|
||||
$this->getActor()->sendPOST("/api/v1/accounts/{$accountId}/email", [
|
||||
'key' => $key,
|
||||
]);
|
||||
}
|
||||
|
||||
public function changeLanguage(int $accountId, $lang = null) {
|
||||
public function changeLanguage(int $accountId, $lang = null): void {
|
||||
$this->getActor()->sendPOST("/api/v1/accounts/{$accountId}/language", [
|
||||
'lang' => $lang,
|
||||
]);
|
||||
}
|
||||
|
||||
public function acceptRules(int $accountId) {
|
||||
public function acceptRules(int $accountId): void {
|
||||
$this->getActor()->sendPOST("/api/v1/accounts/{$accountId}/rules");
|
||||
}
|
||||
|
||||
public function getTwoFactorAuthCredentials(int $accountId) {
|
||||
public function getTwoFactorAuthCredentials(int $accountId): void {
|
||||
$this->getActor()->sendGET("/api/v1/accounts/{$accountId}/two-factor-auth");
|
||||
}
|
||||
|
||||
public function enableTwoFactorAuth(int $accountId, $totp = null, $password = null) {
|
||||
public function enableTwoFactorAuth(int $accountId, $totp = null, $password = null): void {
|
||||
$this->getActor()->sendPOST("/api/v1/accounts/{$accountId}/two-factor-auth", [
|
||||
'totp' => $totp,
|
||||
'password' => $password,
|
||||
]);
|
||||
}
|
||||
|
||||
public function disableTwoFactorAuth(int $accountId, $totp = null, $password = null) {
|
||||
public function disableTwoFactorAuth(int $accountId, $totp = null, $password = null): void {
|
||||
$this->getActor()->sendDELETE("/api/v1/accounts/{$accountId}/two-factor-auth", [
|
||||
'totp' => $totp,
|
||||
'password' => $password,
|
||||
]);
|
||||
}
|
||||
|
||||
public function ban(int $accountId) {
|
||||
public function ban(int $accountId): void {
|
||||
$this->getActor()->sendPOST("/api/v1/accounts/{$accountId}/ban");
|
||||
}
|
||||
|
||||
public function pardon(int $accountId) {
|
||||
public function pardon(int $accountId): void {
|
||||
$this->getActor()->sendDELETE("/api/v1/accounts/{$accountId}/ban");
|
||||
}
|
||||
|
||||
|
@ -6,10 +6,10 @@ class AuthenticationRoute extends BasePage {
|
||||
/**
|
||||
* @param string $login
|
||||
* @param string $password
|
||||
* @param string|bool|null $rememberMeOrToken
|
||||
* @param bool|string|null $rememberMeOrToken
|
||||
* @param bool $rememberMe
|
||||
*/
|
||||
public function login($login = '', $password = '', $rememberMeOrToken = null, $rememberMe = false) {
|
||||
public function login(string $login = '', string $password = '', bool|string|null $rememberMeOrToken = null, bool $rememberMe = false): void {
|
||||
$params = [
|
||||
'login' => $login,
|
||||
'password' => $password,
|
||||
@ -24,14 +24,14 @@ class AuthenticationRoute extends BasePage {
|
||||
$this->getActor()->sendPOST('/api/authentication/login', $params);
|
||||
}
|
||||
|
||||
public function forgotPassword($login = null, $token = null) {
|
||||
public function forgotPassword($login = null, $token = null): void {
|
||||
$this->getActor()->sendPOST('/api/authentication/forgot-password', [
|
||||
'login' => $login,
|
||||
'totp' => $token,
|
||||
]);
|
||||
}
|
||||
|
||||
public function recoverPassword($key = null, $newPassword = null, $newRePassword = null) {
|
||||
public function recoverPassword($key = null, $newPassword = null, $newRePassword = null): void {
|
||||
$this->getActor()->sendPOST('/api/authentication/recover-password', [
|
||||
'key' => $key,
|
||||
'newPassword' => $newPassword,
|
||||
@ -39,7 +39,7 @@ class AuthenticationRoute extends BasePage {
|
||||
]);
|
||||
}
|
||||
|
||||
public function refreshToken($refreshToken = null) {
|
||||
public function refreshToken($refreshToken = null): void {
|
||||
$this->getActor()->sendPOST('/api/authentication/refresh-token', [
|
||||
'refresh_token' => $refreshToken,
|
||||
]);
|
||||
|
@ -5,13 +5,9 @@ use api\tests\FunctionalTester;
|
||||
|
||||
class BasePage {
|
||||
|
||||
/**
|
||||
* @var FunctionalTester
|
||||
*/
|
||||
private $actor;
|
||||
|
||||
public function __construct(FunctionalTester $I) {
|
||||
$this->actor = $I;
|
||||
public function __construct(
|
||||
private readonly FunctionalTester $actor,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getActor(): FunctionalTester {
|
||||
|
@ -3,7 +3,7 @@ namespace api\tests\_pages;
|
||||
|
||||
class IdentityInfoRoute extends BasePage {
|
||||
|
||||
public function info() {
|
||||
public function info(): void {
|
||||
$this->getActor()->sendGET('/api/account/v1/info');
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,7 @@ namespace api\tests\_pages;
|
||||
|
||||
class InternalRoute extends BasePage {
|
||||
|
||||
public function info(string $param, string $value) {
|
||||
public function info(string $param, string $value): void {
|
||||
$this->getActor()->sendGET('/api/internal/accounts/info', [$param => $value]);
|
||||
}
|
||||
|
||||
|
@ -3,12 +3,12 @@ namespace api\tests\_pages;
|
||||
|
||||
class MojangApiRoute extends BasePage {
|
||||
|
||||
public function usernameToUuid($username, $at = null) {
|
||||
public function usernameToUuid($username, $at = null): void {
|
||||
$params = $at === null ? [] : ['at' => $at];
|
||||
$this->getActor()->sendGET("/api/mojang/profiles/{$username}", $params);
|
||||
}
|
||||
|
||||
public function usernamesByUuid($uuid) {
|
||||
public function usernamesByUuid($uuid): void {
|
||||
$this->getActor()->sendGET("/api/mojang/profiles/{$uuid}/names");
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,7 @@ namespace api\tests\_pages;
|
||||
|
||||
class OptionsRoute extends BasePage {
|
||||
|
||||
public function get() {
|
||||
public function get(): void {
|
||||
$this->getActor()->sendGET('/api/options');
|
||||
}
|
||||
|
||||
|
@ -3,23 +3,23 @@ namespace api\tests\_pages;
|
||||
|
||||
class SessionServerRoute extends BasePage {
|
||||
|
||||
public function join($params) {
|
||||
public function join($params): void {
|
||||
$this->getActor()->sendPOST('/api/minecraft/session/join', $params);
|
||||
}
|
||||
|
||||
public function joinLegacy(array $params) {
|
||||
public function joinLegacy(array $params): void {
|
||||
$this->getActor()->sendGET('/api/minecraft/session/legacy/join', $params);
|
||||
}
|
||||
|
||||
public function hasJoined(array $params) {
|
||||
public function hasJoined(array $params): void {
|
||||
$this->getActor()->sendGET('/api/minecraft/session/hasJoined', $params);
|
||||
}
|
||||
|
||||
public function hasJoinedLegacy(array $params) {
|
||||
public function hasJoinedLegacy(array $params): void {
|
||||
$this->getActor()->sendGET('/api/minecraft/session/legacy/hasJoined', $params);
|
||||
}
|
||||
|
||||
public function profile(string $profileUuid, bool $signed = false) {
|
||||
public function profile(string $profileUuid, bool $signed = false): void {
|
||||
$url = "/api/minecraft/session/profile/{$profileUuid}";
|
||||
if ($signed) {
|
||||
$url .= '?unsigned=false';
|
||||
|
@ -3,15 +3,15 @@ namespace api\tests\_pages;
|
||||
|
||||
class SignupRoute extends BasePage {
|
||||
|
||||
public function register(array $registrationData) {
|
||||
public function register(array $registrationData): void {
|
||||
$this->getActor()->sendPOST('/api/signup', $registrationData);
|
||||
}
|
||||
|
||||
public function sendRepeatMessage($email = '') {
|
||||
public function sendRepeatMessage($email = ''): void {
|
||||
$this->getActor()->sendPOST('/api/signup/repeat-message', ['email' => $email]);
|
||||
}
|
||||
|
||||
public function confirm($key = '') {
|
||||
public function confirm($key = ''): void {
|
||||
$this->getActor()->sendPOST('/api/signup/confirm', [
|
||||
'key' => $key,
|
||||
]);
|
||||
|
@ -12,21 +12,21 @@ use Yii;
|
||||
class FunctionalTester extends Actor {
|
||||
use FunctionalTesterActions;
|
||||
|
||||
public function amAuthenticated(string $asUsername = 'admin') { // Do not declare type
|
||||
/** @var Account $account */
|
||||
public function amAuthenticated(string $asUsername = 'admin'): mixed {
|
||||
/** @var Account|null $account */
|
||||
$account = Account::findOne(['username' => $asUsername]);
|
||||
if ($account === null) {
|
||||
throw new InvalidArgumentException("Cannot find account with username \"{$asUsername}\"");
|
||||
}
|
||||
|
||||
$token = Yii::$app->tokensFactory->createForWebAccount($account);
|
||||
$this->amBearerAuthenticated((string)$token);
|
||||
$this->amBearerAuthenticated($token->toString());
|
||||
|
||||
return $account->id;
|
||||
}
|
||||
|
||||
public function notLoggedIn(): void {
|
||||
$this->haveHttpHeader('Authorization', null);
|
||||
$this->haveHttpHeader('Authorization', '');
|
||||
Yii::$app->user->logout();
|
||||
}
|
||||
|
||||
|
@ -4,5 +4,4 @@ use common\config\ConfigLoader;
|
||||
use yii\helpers\ArrayHelper;
|
||||
|
||||
return ArrayHelper::merge(ConfigLoader::load('api'), [
|
||||
|
||||
]);
|
||||
|
@ -4,5 +4,4 @@ use common\config\ConfigLoader;
|
||||
use yii\helpers\ArrayHelper;
|
||||
|
||||
return ArrayHelper::merge(ConfigLoader::load('api'), [
|
||||
|
||||
]);
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user