mirror of
https://github.com/elyby/accounts.git
synced 2025-05-31 14:11:46 +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:
@@ -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 {
|
||||
|
Reference in New Issue
Block a user