2019-08-01 14:47:12 +05:30
|
|
|
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
namespace api\components\Tokens;
|
|
|
|
|
2019-08-02 05:59:20 +05:30
|
|
|
use Carbon\Carbon;
|
2019-08-01 14:47:12 +05:30
|
|
|
use Exception;
|
|
|
|
use Lcobucci\JWT\Builder;
|
|
|
|
use Lcobucci\JWT\Parser;
|
|
|
|
use Lcobucci\JWT\Token;
|
2019-08-02 21:46:34 +05:30
|
|
|
use Webmozart\Assert\Assert;
|
2019-08-01 14:47:12 +05:30
|
|
|
use yii\base\Component as BaseComponent;
|
|
|
|
|
|
|
|
class Component extends BaseComponent {
|
|
|
|
|
|
|
|
private const PREFERRED_ALGORITHM = 'ES256';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
public $hmacKey;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
public $publicKeyPath;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
public $privateKeyPath;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var string|null
|
|
|
|
*/
|
|
|
|
public $privateKeyPass;
|
|
|
|
|
2019-12-04 23:40:15 +05:30
|
|
|
/**
|
2019-12-10 04:08:09 +05:30
|
|
|
* @var string
|
2019-12-04 23:40:15 +05:30
|
|
|
*/
|
|
|
|
public $encryptionKey;
|
|
|
|
|
2019-08-01 14:47:12 +05:30
|
|
|
/**
|
|
|
|
* @var AlgorithmsManager|null
|
|
|
|
*/
|
|
|
|
private $algorithmManager;
|
|
|
|
|
2019-08-02 21:46:34 +05:30
|
|
|
public function init(): void {
|
|
|
|
parent::init();
|
|
|
|
Assert::notEmpty($this->hmacKey, 'hmacKey must be set');
|
|
|
|
Assert::notEmpty($this->privateKeyPath, 'privateKeyPath must be set');
|
|
|
|
Assert::notEmpty($this->publicKeyPath, 'publicKeyPath must be set');
|
2019-12-04 23:40:15 +05:30
|
|
|
Assert::notEmpty($this->encryptionKey, 'encryptionKey must be set');
|
2019-08-02 21:46:34 +05:30
|
|
|
}
|
|
|
|
|
2019-08-01 14:47:12 +05:30
|
|
|
public function create(array $payloads = [], array $headers = []): Token {
|
2019-08-02 05:59:20 +05:30
|
|
|
$now = Carbon::now();
|
2019-12-06 21:01:04 +05:30
|
|
|
$builder = (new Builder())->issuedAt($now->getTimestamp());
|
|
|
|
if (isset($payloads['exp'])) {
|
|
|
|
$builder->expiresAt($payloads['exp']);
|
|
|
|
}
|
|
|
|
|
2019-08-01 14:47:12 +05:30
|
|
|
foreach ($payloads as $claim => $value) {
|
2019-12-04 23:40:15 +05:30
|
|
|
$builder->withClaim($claim, $this->prepareValue($value));
|
2019-08-01 14:47:12 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($headers as $claim => $value) {
|
2019-12-04 23:40:15 +05:30
|
|
|
$builder->withHeader($claim, $this->prepareValue($value));
|
2019-08-01 14:47:12 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
/** @noinspection PhpUnhandledExceptionInspection */
|
|
|
|
$algorithm = $this->getAlgorithmManager()->get(self::PREFERRED_ALGORITHM);
|
|
|
|
|
|
|
|
return $builder->getToken($algorithm->getSigner(), $algorithm->getPrivateKey());
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param string $jwt
|
|
|
|
*
|
|
|
|
* @return Token
|
|
|
|
* @throws \InvalidArgumentException
|
|
|
|
*/
|
|
|
|
public function parse(string $jwt): Token {
|
|
|
|
return (new Parser())->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) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-05 22:07:46 +05:30
|
|
|
public function encryptValue(string $rawValue): string {
|
|
|
|
/** @noinspection PhpUnhandledExceptionInspection */
|
|
|
|
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
|
2019-12-06 17:07:51 +05:30
|
|
|
$cipher = $this->base64UrlEncode($nonce . sodium_crypto_secretbox($rawValue, $nonce, $this->encryptionKey));
|
2019-12-05 22:07:46 +05:30
|
|
|
sodium_memzero($rawValue);
|
|
|
|
|
|
|
|
return $cipher;
|
|
|
|
}
|
|
|
|
|
2019-12-04 23:40:15 +05:30
|
|
|
public function decryptValue(string $encryptedValue): string {
|
2019-12-06 17:07:51 +05:30
|
|
|
$decoded = $this->base64UrlDecode($encryptedValue);
|
2019-12-05 22:07:46 +05:30
|
|
|
Assert::true($decoded !== false, 'passed value has an invalid base64 encoding');
|
|
|
|
Assert::true(mb_strlen($decoded, '8bit') >= (SODIUM_CRYPTO_SECRETBOX_NONCEBYTES + SODIUM_CRYPTO_SECRETBOX_MACBYTES));
|
|
|
|
$nonce = mb_substr($decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit');
|
|
|
|
$cipherText = mb_substr($decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit');
|
|
|
|
|
|
|
|
$rawValue = sodium_crypto_secretbox_open($cipherText, $nonce, $this->encryptionKey);
|
|
|
|
Assert::true($rawValue !== false);
|
|
|
|
sodium_memzero($cipherText);
|
|
|
|
|
|
|
|
return $rawValue;
|
2019-12-04 23:40:15 +05:30
|
|
|
}
|
|
|
|
|
2019-08-01 14:47:12 +05:30
|
|
|
private function getAlgorithmManager(): AlgorithmsManager {
|
|
|
|
if ($this->algorithmManager === null) {
|
|
|
|
$this->algorithmManager = new AlgorithmsManager([
|
|
|
|
new Algorithms\HS256($this->hmacKey),
|
|
|
|
new Algorithms\ES256(
|
|
|
|
"file://{$this->privateKeyPath}",
|
|
|
|
$this->privateKeyPass,
|
|
|
|
"file://{$this->publicKeyPath}"
|
|
|
|
),
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->algorithmManager;
|
|
|
|
}
|
|
|
|
|
2019-12-04 23:40:15 +05:30
|
|
|
private function prepareValue($value) {
|
|
|
|
if ($value instanceof EncryptedValue) {
|
2019-12-05 22:07:46 +05:30
|
|
|
return $this->encryptValue($value->getValue());
|
2019-12-04 23:40:15 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
return $value;
|
|
|
|
}
|
|
|
|
|
2019-12-06 17:07:51 +05:30
|
|
|
private function base64UrlEncode(string $rawValue): string {
|
|
|
|
return rtrim(strtr(base64_encode($rawValue), '+/', '-_'), '=');
|
|
|
|
}
|
|
|
|
|
|
|
|
private function base64UrlDecode(string $encodedValue): string {
|
|
|
|
return base64_decode(str_pad(strtr($encodedValue, '-_', '+/'), strlen($encodedValue) % 4, '=', STR_PAD_RIGHT));
|
|
|
|
}
|
|
|
|
|
2019-08-01 14:47:12 +05:30
|
|
|
}
|