Newer
Older
TillQliro / Model / Security / Jwt.php
@Jonas Jonsson Jonas Jonsson on 2 Apr 2024 5 KB Initial
<?php
/**
 * Copyright © Qliro AB. All rights reserved.
 * See LICENSE.txt for license details.
 */

namespace Qliro\QliroOne\Model\Security;

use Magento\Framework\Serialize\Serializer\Json;

/**
 * JWT-compatible token handling class
 */
class Jwt
{
    /**
     * @var array
     */
    private $supportedMethods = [
        'HS256' => 'sha256',
        'HS384' => 'sha384',
        'HS512' => 'sha512',
    ];

    /**
     * @var \Magento\Framework\Serialize\Serializer\Json
     */
    private $json;

    /**
     * Inject dependencies
     *
     * @param \Magento\Framework\Serialize\Serializer\Json $json
     */
    public function __construct(
        Json $json
    ) {
        $this->json = $json;
    }

    /**
     * Decode a JWT string into an array
     *
     * @param string $jwt
     * @param string|null $secretKey
     * @param bool $verify Don't skip verification process
     * @return array
     * @throws \UnexpectedValueException
     * @throws \DomainException
     */
    public function decode($jwt, $secretKey = null, $verify = true)
    {
        $tokenSegments = explode('.', $jwt);

        if (count($tokenSegments) != 3) {
            throw new \UnexpectedValueException('Wrong number of segments.');
        }

        list($encodedHead, $encodedBody, $encryption) = $tokenSegments;

        if (null === ($header = $this->jsonDecode($this->safeDecode($encodedHead)))) {
            throw new \UnexpectedValueException('Invalid segment encoding.');
        }

        if (null === $payload = $this->jsonDecode($this->safeDecode($encodedBody))) {
            throw new \UnexpectedValueException('Invalid segment encoding.');
        }

        $sig = $this->safeDecode($encryption);

        if ($verify) {
            if (empty($header['alg'])) {
                throw new \DomainException('Empty algorithm');
            }

            if ($sig != $this->sign("$encodedHead.$encodedBody", $secretKey, $header['alg'])) {
                throw new \UnexpectedValueException('Signature verification failed.');
            }
        }

        return $payload;
    }

    /**
     * Convert and sign a payload into a JWT string
     *
     * @param array $payload
     * @param string $secretKey
     * @param string $algorithm
     * @return string
     */
    public function encode($payload, $secretKey, $algorithm = 'HS256')
    {
        $header = ['typ' => 'JWT', 'alg' => $algorithm];
        $segments = [];
        $segments[] = $this->safeEncode($this->jsonEncode($header));
        $segments[] = $this->safeEncode($this->jsonEncode($payload));
        $signingInput = implode('.', $segments);
        $signature = $this->sign($signingInput, $secretKey, $algorithm);
        $segments[] = $this->safeEncode($signature);

        return implode('.', $segments);
    }

    /**
     * Sign a string with a given key and algorithm
     *
     * @param string $message
     * @param string $secretKey
     * @param string $algorithm
     * @return string
     * @throws \DomainException
     */
    private function sign($message, $secretKey, $algorithm = 'HS256')
    {
        if (!isset($this->supportedMethods[$algorithm])) {
            throw new \DomainException('Algorithm is not supported.');
        }

        return \hash_hmac($this->supportedMethods[$algorithm], $message, $secretKey, true);
    }

    /**
     * Decode a JSON string into a PHP object.
     *
     * @param string $input JSON string
     * @return array
     * @throws \DomainException Provided string was invalid JSON
     */
    private function jsonDecode($input)
    {
        try {
            $data = $this->json->unserialize($input);
        } catch (\InvalidArgumentException $exception) {
            throw new \DomainException('Unknown JSON decode error.');
        }

        if ($data === null && $input !== 'null') {
            throw new \DomainException('Null result with non-null input.');
        }

        return $data;
    }

    /**
     * Encode an array into a JSON string
     *
     * @param array $input
     * @return string
     * @throws \DomainException
     */
    private function jsonEncode($input)
    {
        try {
            $json = $this->json->serialize($input);
        } catch (\InvalidArgumentException $exception) {
            throw new \DomainException('Unknown JSON encode error.');
        }

        if ($json === 'null' && $input !== null) {
            throw new \DomainException('Null result with non-null input');
        }

        return $json;
    }

    /**
     * Decode a string with URL-safe Base64
     *
     * @param string $input
     * @return string
     */
    private function safeDecode($input)
    {
        $remainder = strlen($input) % 4;
        if ($remainder) {
            $padlen = 4 - $remainder;
            $input .= str_repeat('=', $padlen);
        }

        return \base64_decode(strtr($input, '-_', '+/'));
    }

    /**
     * Encode a string with URL-safe Base64
     *
     * @param string $input
     * @return string
     */
    private function safeEncode($input)
    {
        return str_replace('=', '', strtr(\base64_encode($input), '+/', '-_'));
    }
}