Current File : /var/www/vinorea/modules/ps_accounts/src/Service/OAuth2/OAuth2Service.php
<?php
/**
 * Copyright since 2007 PrestaShop SA and Contributors
 * PrestaShop is an International Registered Trademark & Property of PrestaShop SA
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Academic Free License version 3.0
 * that is bundled with this package in the file LICENSE.md.
 * It is also available through the world-wide-web at this URL:
 * https://opensource.org/licenses/AFL-3.0
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@prestashop.com so we can send you a copy immediately.
 *
 * @author    PrestaShop SA and Contributors <contact@prestashop.com>
 * @copyright Since 2007 PrestaShop SA and Contributors
 * @license   https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0
 */

namespace PrestaShop\Module\PsAccounts\Service\OAuth2;

use PrestaShop\Module\PsAccounts\Http\Client\ClientConfig;
use PrestaShop\Module\PsAccounts\Http\Client\Curl\Client as HttpClient;
use PrestaShop\Module\PsAccounts\Http\Client\Factory;
use PrestaShop\Module\PsAccounts\Http\Client\Request;
use PrestaShop\Module\PsAccounts\Http\Client\Response;
use PrestaShop\Module\PsAccounts\Service\OAuth2\Resource\AccessToken;
use PrestaShop\Module\PsAccounts\Service\OAuth2\Resource\UserInfo;
use PrestaShop\Module\PsAccounts\Service\OAuth2\Resource\WellKnown;
use PrestaShop\Module\PsAccounts\Vendor\Ramsey\Uuid\Uuid;

class OAuth2Service
{
    /**
     * cached openid-configuration ttl (24 Hours)
     */
    const OPENID_CONFIGURATION_CACHE_TTL = 60 * 60 * 24;

    /**
     * cached openid-configuration filename
     */
    const OPENID_CONFIGURATION_JSON = 'openid-configuration.json';

    /**
     * cached JWKS (JSON Web Key Set) filename
     */
    const JWKS_JSON = 'jwks.json';

    /**
     * @var HttpClient
     */
    private $httpClient;

    /**
     * @var array
     */
    protected $clientConfig;

    /**
     * @var WellKnown
     */
    private $wellKnown;

    /**
     * @var CachedFile
     */
    private $cachedWellKnown;

    /**
     * @var CachedFile
     */
    private $cachedJwks;

    /**
     * @var OAuth2Client
     */
    private $oAuth2Client;

    /**
     * @var string[]
     */
    protected $defaultScopes = [
        'openid',
        'offline_access',
    ];

    /**
     * @param array $config
     * @param OAuth2Client $oAuth2Client
     * @param string $cacheDir
     *
     * @throws \Exception
     */
    public function __construct(
        array $config,
        OAuth2Client $oAuth2Client,
        $cacheDir
    ) {
        $this->clientConfig = array_merge([
            ClientConfig::NAME => static::class,
            ClientConfig::HEADERS => $this->getHeaders(),
        ], $config);

        $this->oAuth2Client = $oAuth2Client;

        $this->cachedWellKnown = new CachedFile(
            $cacheDir . '/' . self::OPENID_CONFIGURATION_JSON,
            self::OPENID_CONFIGURATION_CACHE_TTL
        );
        $this->cachedJwks = new CachedFile(
            $cacheDir . '/' . self::JWKS_JSON
        );
    }

    /**
     * @return HttpClient
     */
    public function getHttpClient()
    {
        if (null === $this->httpClient) {
            $this->httpClient = (new Factory())->create($this->clientConfig);
        }

        return $this->httpClient;
    }

    /**
     * @param HttpClient $httpClient
     *
     * @return void
     */
    public function setHttpClient(HttpClient $httpClient)
    {
        $this->httpClient = $httpClient;
    }

    /**
     * @param array $additionalHeaders
     *
     * @return array
     */
    private function getHeaders($additionalHeaders = [])
    {
        return array_merge([
            'Accept' => 'application/json',
            'X-Module-Version' => \Ps_accounts::VERSION,
            'X-Prestashop-Version' => _PS_VERSION_,
            'X-Request-ID' => Uuid::uuid4()->toString(),
        ], $additionalHeaders);
    }

    /**
     * @return WellKnown
     *
     * @throws OAuth2Exception
     */
    public function getWellKnown()
    {
        /* @phpstan-ignore-next-line */
        if (!isset($this->wellKnown) || $this->cachedWellKnown->isExpired()) {
            $this->wellKnown = new WellKnown(json_decode($this->getWellKnownFromCache(), true));
        }

        return $this->wellKnown;
    }

    /**
     * @param bool $forceRefresh
     *
     * @return string
     *
     * @throws OAuth2Exception
     */
    protected function getWellKnownFromCache($forceRefresh = false)
    {
        if ($this->cachedWellKnown->isExpired() || $forceRefresh) {
            $wellKnown = $this->fetchWellKnown();

            $this->cachedWellKnown->write(
                json_encode($wellKnown, JSON_UNESCAPED_SLASHES)
            );
        }

        return (string) $this->cachedWellKnown->read();
    }

    /**
     * @return array
     *
     * @throws OAuth2Exception
     */
    protected function fetchWellKnown()
    {
        //$response = $this->getHttpClient()->get($this->getOpenIdConfigurationUri());
        $response = $this->getHttpClient()->get('/.well-known/openid-configuration');

        if (!$response->isSuccessful) {
            throw new OAuth2Exception($this->getResponseErrorMsg($response, 'Unable to get openid-configuration'));
        }

        return $response->body;
    }

    /**
     * @param bool $forceRefresh
     *
     * @return array
     *
     * @throws OAuth2Exception
     */
    public function getJwks($forceRefresh = false)
    {
        if ($this->cachedJwks->isExpired() || $forceRefresh) {
            $response = $this->getHttpClient()->get($this->getWellKnown()->jwks_uri);

            if (!$response->isSuccessful) {
                throw new OAuth2Exception($this->getResponseErrorMsg($response, 'Unable to get JWKS'));
            }

            $this->cachedJwks->write(
                json_encode($response->body, JSON_UNESCAPED_SLASHES)
            );
        }

        return json_decode($this->cachedJwks->read(), true);
    }

    /**
     * @param array $scope
     * @param array $audience
     *
     * @return AccessToken access token
     *
     * @throws OAuth2Exception
     */
    public function getAccessTokenByClientCredentials(array $scope = [], array $audience = [])
    {
        $this->assertClientExists();

        $response = $this->getHttpClient()->post(
            $this->getWellKnown()->token_endpoint,
            [
                Request::FORM => [
                    'grant_type' => 'client_credentials',
                    'client_id' => $this->oAuth2Client->getClientId(),
                    'client_secret' => $this->oAuth2Client->getClientSecret(),
                    'scope' => implode(' ', $scope),
                    'audience' => implode(' ', $audience),
                    'redirect_uri' => $this->getOAuth2Client()->getRedirectUri(),
                ],
            ]
        );

        if (!$response->isSuccessful) {
            throw new OAuth2Exception($this->getResponseErrorMsg($response, 'Unable to get access token'));
        }

        return new AccessToken($response->body);
    }

    /**
     * @param string $state
     * @param string|null $pkceCode
     * @param string $pkceMethod
     * @param string $uiLocales
     * @param string $acrValues
     * @param string $prompt
     *
     * @return string authorization flow uri
     *
     * @throws OAuth2Exception
     */
    public function getAuthorizationUri(
        $state,
        $pkceCode = null,
        $pkceMethod = 'S256',
        $uiLocales = 'fr',
        $acrValues = 'prompt:login',
        $prompt = 'none'
    ) {
        $this->assertClientExists();

        return $this->getWellKnown()->authorization_endpoint . '?' .
            http_build_query(array_merge([
                'ui_locales' => $uiLocales,
                'state' => $state,
                'scope' => implode(' ', $this->defaultScopes),
                'response_type' => 'code',
                'approval_prompt' => 'auto',
                'redirect_uri' => $this->getOAuth2Client()->getRedirectUri(),
                'client_id' => $this->oAuth2Client->getClientId(),
                'acr_values' => $acrValues,
                'prompt' => $prompt,
            ], $pkceCode ? [
                'code_challenge' => trim(strtr(base64_encode(hash('sha256', $pkceCode, true)), '+/', '-_'), '='),
                'code_challenge_method' => $pkceMethod,
            ] : []));
    }

    /**
     * @param int $length
     *
     * @return string
     */
    public function getRandomState($length = 32)
    {
        /* @phpstan-ignore-next-line */
        return bin2hex(random_bytes((int) ($length / 2)));
    }

    /**
     * @param int $length
     *
     * @return string
     */
    public function getRandomPkceCode($length = 64)
    {
        /* @phpstan-ignore-next-line */
        return (string) substr(strtr(base64_encode(random_bytes($length)), '+/', '-_'), 0, $length);
    }

    /**
     * @param string $code
     * @param string|null $pkceCode
     * @param array $scope
     * @param array $audience
     *
     * @return AccessToken access token
     *
     * @throws OAuth2Exception
     */
    public function getAccessTokenByAuthorizationCode(
        $code,
        $pkceCode = null,
        array $scope = [],
        array $audience = []
    ) {
        $this->assertClientExists();

        $response = $this->getHttpClient()->post(
            $this->getWellKnown()->token_endpoint,
            [
                Request::FORM => array_merge([
                    'grant_type' => 'authorization_code',
                    'client_id' => $this->oAuth2Client->getClientId(),
                    'client_secret' => $this->oAuth2Client->getClientSecret(),
                    'code' => $code,
                    'scope' => implode(' ', $scope),
                    'audience' => implode(' ', $audience),
                    'redirect_uri' => $this->getOAuth2Client()->getRedirectUri(),
                ], $pkceCode ? [
                    'code_verifier' => $pkceCode,
                ] : []),
            ]
        );

        if (!$response->isSuccessful) {
            throw new OAuth2Exception($this->getResponseErrorMsg($response, 'Unable to get access token'));
        }

        return new AccessToken($response->body);
    }

    /**
     * @param string $refreshToken
     *
     * @return AccessToken
     *
     * @throws OAuth2Exception
     */
    public function refreshAccessToken($refreshToken)
    {
        $this->assertClientExists();

        $response = $this->getHttpClient()->post(
            $this->getWellKnown()->token_endpoint,
            [
                Request::FORM => [
                    'grant_type' => 'refresh_token',
                    'client_id' => $this->oAuth2Client->getClientId(),
                    'refresh_token' => $refreshToken,
                ],
            ]
        );

        if (!$response->isSuccessful) {
            throw new OAuth2Exception($this->getResponseErrorMsg($response, 'Unable to refresh access token'));
        }

        return new AccessToken($response->body);
    }

    /**
     * @param string $accessToken
     *
     * @return UserInfo
     */
    public function getUserInfo($accessToken)
    {
        $response = $this->getHttpClient()->get(
            $this->getWellKnown()->userinfo_endpoint,
            [
                Request::HEADERS => $this->getHeaders([
                    'Authorization' => 'Bearer ' . $accessToken,
                ]),
            ]
        );

        if (!$response->isSuccessful) {
            throw new OAuth2Exception($this->getResponseErrorMsg($response, 'Unable to get user infos'));
        }

        return new UserInfo($response->body);
    }

    /**
     * @param string $postLogoutRedirectUri
     * @param string|null $idTokenHint
     *
     * @return string
     */
    public function getLogoutUri($postLogoutRedirectUri, $idTokenHint = null)
    {
        return $this->getWellKnown()->end_session_endpoint . '?' .
            http_build_query([
                'id_token_hint' => $idTokenHint,
                'post_logout_redirect_uri' => $postLogoutRedirectUri,
            ]);
    }

    /**
     * @return OAuth2Client
     */
    public function getOAuth2Client()
    {
        return $this->oAuth2Client;
    }

    /**
     * @return void
     */
    public function clearCache()
    {
        $this->cachedJwks->clear();
        $this->cachedWellKnown->clear();
    }

    /**
     * @return CachedFile
     */
    public function getCachedWellKnown()
    {
        return $this->cachedWellKnown;
    }

    /**
     * @return CachedFile
     */
    public function getCachedJwks()
    {
        return $this->cachedJwks;
    }

    /**
     * @return void
     *
     * @throws OAuth2Exception
     */
    protected function assertClientExists()
    {
        if (!$this->oAuth2Client->exists()) {
            throw new OAuth2Exception('OAuth2 client not configured');
        }
    }

    /**
     * @param Response $response
     * @param string $defaultMessage
     *
     * @return string
     */
    protected function getResponseErrorMsg(Response $response, $defaultMessage = '')
    {
        $msg = $defaultMessage;
        $body = $response->body;
        if (isset($body['error']) &&
            isset($body['error_description'])
        ) {
            $msg = $body['error'] . ': ' . $body['error_description'];
        }

        return $response->statusCode . ' - ' . $msg;
    }
}