<?php
namespace Harmonizely\Service\Panel\OAuth;
use Harmonizely\Service\Panel\OAuth\Contract\IOAuthClient;
use Harmonizely\Types\Company\OAuthProviderType;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class TwitterOAuthClient implements IOAuthClient
{
/**
* @var string
*/
private string $appKey;
/**
* @var string
*/
private string $appSecret;
/**
* @var HttpClientInterface
*/
private HttpClientInterface $httpClient;
/**
* @var UrlGeneratorInterface
*/
private UrlGeneratorInterface $urlGenerator;
/**
* @var LoggerInterface
*/
private LoggerInterface $logger;
/**
* @var CsrfTokenManagerInterface
*/
private CsrfTokenManagerInterface $csrfTokenManager;
/**
* TwitterOAuthClient constructor.
*
* @param string $appKey
* @param string $appSecret
* @param HttpClientInterface $httpClient
* @param UrlGeneratorInterface $urlGenerator
* @param LoggerInterface $logger
* @param CsrfTokenManagerInterface $csrfTokenManager
*/
public function __construct(
string $appKey,
string $appSecret,
HttpClientInterface $httpClient,
UrlGeneratorInterface $urlGenerator,
LoggerInterface $logger,
CsrfTokenManagerInterface $csrfTokenManager
)
{
$this->appKey = $appKey;
$this->appSecret = $appSecret;
$this->httpClient = $httpClient;
$this->urlGenerator = $urlGenerator;
$this->logger = $logger;
$this->csrfTokenManager = $csrfTokenManager;
}
/**
* @return string
* @throws OAuthException
* @throws ClientExceptionInterface
* @throws RedirectionExceptionInterface
* @throws ServerExceptionInterface
* @throws TransportExceptionInterface
*/
public function getAuthUrl(): string
{
$headers = $this->getAuthHeaders($this->urlGenerator->generate('login.oauth.callback', [
'provider' => OAuthProviderType::TYPE_TWITTER,
'state' => $this->csrfTokenManager->getToken('twitter')->getValue(),
], UrlGeneratorInterface::ABSOLUTE_URL));
$headers['oauth_signature'] = urlencode($this->getSign(
'https://api.twitter.com/oauth/request_token',
'POST',
$headers,
$this->appSecret . '&'
));
$authHeader = $this->buildOauthHeader($headers);
$response = $this->httpClient->request('POST', 'https://api.twitter.com/oauth/request_token', [
'headers' => [
'Authorization' => $authHeader,
],
]);
if ($response->getStatusCode() === 200) {
$result = [];
parse_str($response->getContent(), $result);
if (isset($result['oauth_token'])) {
return 'https://api.twitter.com/oauth/authenticate?oauth_token=' . $result['oauth_token'];
} else {
throw new OAuthException('Token api end point do not return oauth_token');
}
} else {
throw new OAuthException('Token api end point status != 200');
}
}
/**
* @param Request $request
* @return Profile
* @throws OAuthException
* @throws ClientExceptionInterface
* @throws DecodingExceptionInterface
* @throws RedirectionExceptionInterface
* @throws ServerExceptionInterface
* @throws TransportExceptionInterface
*/
public function getProfileAfterCallback(Request $request): Profile
{
if ($request->get('app_oauth_token')) {
return $this->getProfile($request->get('app_oauth_token'));
}
if (!$this->csrfTokenManager->isTokenValid(new CsrfToken('twitter', $request->get('state')))) {
throw new OAuthException('State is not valid');
}
$headers = $this->getAuthHeaders(null, $request->get('oauth_token'));
$headers['oauth_signature'] = urlencode($this->getSign(
'https://api.twitter.com/oauth/access_token',
'POST',
$headers,
$this->appSecret . '&'
));
$authHeader = $this->buildOauthHeader($headers);
$response = $this->httpClient->request('POST', 'https://api.twitter.com/oauth/access_token', [
'headers' => [
'Authorization' => $authHeader,
],
'body' => [
'oauth_verifier' => $request->get('oauth_verifier'),
],
]);
if ($response->getStatusCode() === 200) {
$result = [];
parse_str($response->getContent(), $result);
if (isset($result['oauth_token']) && isset($result['oauth_token_secret'])) {
$headersForProfile = $this->getAuthHeaders(null, $result['oauth_token']);
$headersForProfile['include_email'] = 'true';
$headersForProfile['oauth_signature'] = urlencode($this->getSign(
'https://api.twitter.com/1.1/account/verify_credentials.json',
'GET',
$headersForProfile,
urlencode($this->appSecret) . '&' . urlencode($result['oauth_token_secret'])
));
$authHeader = $this->buildOauthHeader($headersForProfile);
return $this->getProfile($authHeader);
} else {
throw new OAuthException('Token api end point do not return oauth_token or oauth_token_secret');
}
} else {
throw new OAuthException('Token api end point status != 200');
}
}
/**
* @param string|null $callback
* @param string|null $token
* @return array
*/
private function getAuthHeaders(?string $callback = null, ?string $token = null): array
{
$headers = [
'oauth_consumer_key' => $this->appKey,
'oauth_nonce' => md5(time()),
'oauth_signature_method' => 'HMAC-SHA1',
'oauth_timestamp' => time(),
'oauth_version' => '1.0',
];
if ($callback) {
$headers['oauth_callback'] = urlencode($callback);
}
if ($token) {
$headers['oauth_token'] = urlencode($token);
}
return $headers;
}
/**
* @param array $headers
* @return string
*/
private function buildOauthHeader(array $headers): string
{
$result = 'OAuth realm="",';
$data = [];
foreach ($headers as $key => $value) {
$data[] = $key . '="' . $value . '"';
}
return $result . implode(',', $data);
}
/**
* @param string $baseUri
* @param string $method
* @param array $params
* @param string $secretKey
* @return string
*/
private function getSign(string $baseUri, string $method, array $params, string $secretKey): string
{
$urlParams = [];
ksort($params);
foreach ($params as $key => $value) {
$urlParams[] = "$key=" . $value;
}
$str = $method . "&" . urlencode($baseUri) . '&' . urlencode(implode('&', $urlParams));
return base64_encode(hash_hmac('sha1', $str, $secretKey, true));
}
/**
* @param string $authHeader
* @return Profile
* @throws ClientExceptionInterface
* @throws DecodingExceptionInterface
* @throws OAuthException
* @throws RedirectionExceptionInterface
* @throws ServerExceptionInterface
* @throws TransportExceptionInterface
*/
private function getProfile(string $authHeader): Profile
{
// $this->logger->error('authHeader', [
// 'authHeader' => $authHeader,
// ]);
$responseProfile = $this->httpClient->request('GET', 'https://api.twitter.com/1.1/account/verify_credentials.json', [
'headers' => [
'Authorization' => $authHeader,
],
'query' => [
'include_email' => 'true',
],
]);
if ($responseProfile->getStatusCode() === 200) {
$resultProfile = $responseProfile->toArray();
return new Profile(OAuthProviderType::TYPE_TWITTER, $resultProfile['id'], $resultProfile['name'], $resultProfile['email']);
} else {
$this->logger->error('getProfile', [
'code' => $responseProfile->getStatusCode(),
'responseProfile' => $responseProfile->getContent(false),
]);
throw new OAuthException('Profile api end point status != 200');
}
}
}