diff --git a/.env-dist b/.env-dist index 1c79be3..9ef67e1 100644 --- a/.env-dist +++ b/.env-dist @@ -7,8 +7,8 @@ EMAILS_RENDERER_HOST=http://emails-renderer:3000 ## Security params JWT_USER_SECRET= -JWT_PUBLIC_KEY= -JWT_PRIVATE_KEY= +JWT_PUBLIC_KEY_PATH= +JWT_PRIVATE_KEY_PATH= ## External services RECAPTCHA_PUBLIC= diff --git a/api/components/User/Component.php b/api/components/User/Component.php index 30b5357..f93635a 100644 --- a/api/components/User/Component.php +++ b/api/components/User/Component.php @@ -11,6 +11,7 @@ use Emarref\Jwt\Algorithm\AlgorithmInterface; use Emarref\Jwt\Algorithm\Hs256; use Emarref\Jwt\Algorithm\Rs256; use Emarref\Jwt\Claim; +use Emarref\Jwt\Encryption\EncryptionInterface; use Emarref\Jwt\Encryption\Factory as EncryptionFactory; use Emarref\Jwt\Exception\VerificationException; use Emarref\Jwt\HeaderParameter\Custom; @@ -46,14 +47,18 @@ class Component extends YiiUserComponent { public $secret; - public $publicKey; + public $publicKeyPath; - public $privateKey; + public $privateKeyPath; public $expirationTimeout = 'PT1H'; public $sessionTimeout = 'P7D'; + private $publicKey; + + private $privateKey; + /** * @var Token[] */ @@ -62,16 +67,28 @@ class Component extends YiiUserComponent { public function init() { parent::init(); Assert::notEmpty($this->secret, 'secret must be specified'); - Assert::notEmpty($this->publicKey, 'public key must be specified'); - Assert::notEmpty($this->privateKey, 'private key must be specified'); + Assert::notEmpty($this->publicKeyPath, 'public key path must be specified'); + Assert::notEmpty($this->privateKeyPath, 'private key path must be specified'); + } - if (!($this->publicKey = file_get_contents($this->publicKey))) { - throw new InvalidConfigException('invalid public key'); + public function getPublicKey() { + if (empty($this->publicKey)) { + if (!($this->publicKey = file_get_contents($this->publicKeyPath))) { + throw new InvalidConfigException('invalid public key path'); + } } - if (!($this->privateKey = file_get_contents($this->privateKey))) { - throw new InvalidConfigException('invalid private key'); + return $this->publicKey; + } + + public function getPrivateKey() { + if (empty($this->privateKey)) { + if (!($this->privateKey = file_get_contents($this->privateKeyPath))) { + throw new InvalidConfigException('invalid private key path'); + } } + + return $this->privateKey; } public function findIdentityByAccessToken($accessToken): ?IdentityInterface { @@ -153,16 +170,9 @@ class Component extends YiiUserComponent { throw new VerificationException('Incorrect token encoding', 0, $e); } - $algorithm = $this->getAlgorithm(); $version = $notVerifiedToken->getHeader()->findParameterByName('v'); - if ($version === null) { - $algorithm = new Hs256($this->secret); - } - - $encryption = EncryptionFactory::create($algorithm); - if ($version !== null) { - $encryption->setPublicKey($this->publicKey); - } + $version = $version ? $version->getValue() : null; + $encryption = $this->getEncryption($version); $context = new VerificationContext($encryption); $context->setSubject(self::JWT_SUBJECT_PREFIX); @@ -234,8 +244,19 @@ class Component extends YiiUserComponent { return new Rs256(); } + public function getEncryption(?int $version): EncryptionInterface { + $algorithm = $version ? new Rs256() : new Hs256($this->secret); + $encryption = EncryptionFactory::create($algorithm); + + if ($version) { + $encryption->setPublicKey($this->getPublicKey())->setPrivateKey($this->getPrivateKey()); + } + + return $encryption; + } + protected function serializeToken(Token $token): string { - $encryption = EncryptionFactory::create($this->getAlgorithm())->setPrivateKey($this->privateKey); + $encryption = $this->getEncryption(1); return (new Jwt())->serialize($token, $encryption); } diff --git a/api/config/config.php b/api/config/config.php index 82ffbba..8fdec47 100644 --- a/api/config/config.php +++ b/api/config/config.php @@ -11,8 +11,8 @@ return [ 'user' => [ 'class' => api\components\User\Component::class, 'secret' => getenv('JWT_USER_SECRET'), - 'publicKey' => getenv('JWT_PUBLIC_KEY') ?: '/data/certs/public.crt', - 'privateKey' => getenv('JWT_PRIVATE_KEY') ?: '/data/certs/private.key', + 'publicKeyPath' => getenv('JWT_PUBLIC_KEY') ?: 'data/certs/public.crt', + 'privateKeyPath' => getenv('JWT_PRIVATE_KEY') ?: 'data/certs/private.key', ], 'log' => [ 'traceLevel' => YII_DEBUG ? 3 : 0, diff --git a/api/tests/unit/components/User/ComponentTest.php b/api/tests/unit/components/User/ComponentTest.php index 177f457..607b494 100644 --- a/api/tests/unit/components/User/ComponentTest.php +++ b/api/tests/unit/components/User/ComponentTest.php @@ -189,8 +189,8 @@ class ComponentTest extends TestCase { 'enableSession' => false, 'loginUrl' => null, 'secret' => 'secret', - 'publicKey' => 'data/certs/public.crt', - 'privateKey' => 'data/certs/private.key', + 'publicKeyPath' => 'data/certs/public.crt', + 'privateKeyPath' => 'data/certs/private.key', ]; } diff --git a/api/tests/unit/models/authentication/LogoutFormTest.php b/api/tests/unit/models/authentication/LogoutFormTest.php index 1c94729..b13124d 100644 --- a/api/tests/unit/models/authentication/LogoutFormTest.php +++ b/api/tests/unit/models/authentication/LogoutFormTest.php @@ -63,8 +63,8 @@ class LogoutFormTest extends TestCase { 'enableSession' => false, 'loginUrl' => null, 'secret' => 'secret', - 'publicKey' => 'data/certs/public.crt', - 'privateKey' => 'data/certs/private.key', + 'publicKeyPath' => 'data/certs/public.crt', + 'privateKeyPath' => 'data/certs/private.key', ]; } diff --git a/api/tests/unit/modules/accounts/models/ChangePasswordFormTest.php b/api/tests/unit/modules/accounts/models/ChangePasswordFormTest.php index 487a25e..f1fefca 100644 --- a/api/tests/unit/modules/accounts/models/ChangePasswordFormTest.php +++ b/api/tests/unit/modules/accounts/models/ChangePasswordFormTest.php @@ -61,8 +61,8 @@ class ChangePasswordFormTest extends TestCase { 'enableSession' => false, 'loginUrl' => null, 'secret' => 'secret', - 'publicKey' => 'data/certs/public.crt', - 'privateKey' => 'data/certs/private.key', + 'publicKeyPath' => 'data/certs/public.crt', + 'privateKeyPath' => 'data/certs/private.key', ]]); $component->shouldNotReceive('terminateSessions'); @@ -123,8 +123,8 @@ class ChangePasswordFormTest extends TestCase { 'enableSession' => false, 'loginUrl' => null, 'secret' => 'secret', - 'publicKey' => 'data/certs/public.crt', - 'privateKey' => 'data/certs/private.key', + 'publicKeyPath' => 'data/certs/public.crt', + 'privateKeyPath' => 'data/certs/private.key', ]]); $component->shouldReceive('terminateSessions')->once()->withArgs([$account, Component::KEEP_CURRENT_SESSION]); diff --git a/api/tests/unit/modules/accounts/models/EnableTwoFactorAuthFormTest.php b/api/tests/unit/modules/accounts/models/EnableTwoFactorAuthFormTest.php index d1e1a60..c70c98f 100644 --- a/api/tests/unit/modules/accounts/models/EnableTwoFactorAuthFormTest.php +++ b/api/tests/unit/modules/accounts/models/EnableTwoFactorAuthFormTest.php @@ -24,8 +24,8 @@ class EnableTwoFactorAuthFormTest extends TestCase { 'enableSession' => false, 'loginUrl' => null, 'secret' => 'secret', - 'publicKey' => 'data/certs/public.crt', - 'privateKey' => 'data/certs/private.key', + 'publicKeyPath' => 'data/certs/public.crt', + 'privateKeyPath' => 'data/certs/private.key', ]]); $component->shouldReceive('terminateSessions')->withArgs([$account, Component::KEEP_CURRENT_SESSION]); diff --git a/common/tests/unit/rbac/rules/AccountOwnerTest.php b/common/tests/unit/rbac/rules/AccountOwnerTest.php index db74e6b..5894e0c 100644 --- a/common/tests/unit/rbac/rules/AccountOwnerTest.php +++ b/common/tests/unit/rbac/rules/AccountOwnerTest.php @@ -15,8 +15,8 @@ class AccountOwnerTest extends TestCase { public function testIdentityIsNull() { $component = mock(Component::class . '[findIdentityByAccessToken]', [[ 'secret' => 'secret', - 'publicKey' => 'data/certs/public.crt', - 'privateKey' => 'data/certs/private.key', + 'publicKeyPath' => 'data/certs/public.crt', + 'privateKeyPath' => 'data/certs/private.key', ]]); $component->shouldDeferMissing(); $component->shouldReceive('findIdentityByAccessToken')->andReturn(null); @@ -40,8 +40,8 @@ class AccountOwnerTest extends TestCase { $component = mock(Component::class . '[findIdentityByAccessToken]', [[ 'secret' => 'secret', - 'publicKey' => 'data/certs/public.crt', - 'privateKey' => 'data/certs/private.key', + 'publicKeyPath' => 'data/certs/public.crt', + 'privateKeyPath' => 'data/certs/private.key', ]]); $component->shouldDeferMissing(); $component->shouldReceive('findIdentityByAccessToken')->withArgs(['token'])->andReturn($identity); diff --git a/common/tests/unit/rbac/rules/OauthClientOwnerTest.php b/common/tests/unit/rbac/rules/OauthClientOwnerTest.php index 2e3a0b1..7148c3b 100644 --- a/common/tests/unit/rbac/rules/OauthClientOwnerTest.php +++ b/common/tests/unit/rbac/rules/OauthClientOwnerTest.php @@ -36,8 +36,8 @@ class OauthClientOwnerTest extends TestCase { /** @var Component|\Mockery\MockInterface $component */ $component = mock(Component::class . '[findIdentityByAccessToken]', [[ 'secret' => 'secret', - 'publicKey' => 'data/certs/public.crt', - 'privateKey' => 'data/certs/private.key', + 'publicKeyPath' => 'data/certs/public.crt', + 'privateKeyPath' => 'data/certs/private.key', ]]); $component->shouldDeferMissing(); $component->shouldReceive('findIdentityByAccessToken')->withArgs(['token'])->andReturn($identity);