617 lines
18 KiB
PHP
617 lines
18 KiB
PHP
<?php
|
|
/*!
|
|
* Hybridauth
|
|
* https://hybridauth.github.io | https://github.com/hybridauth/hybridauth
|
|
* (c) 2017 Hybridauth authors | https://hybridauth.github.io/license.html
|
|
*/
|
|
|
|
namespace Hybridauth\Adapter;
|
|
|
|
use Hybridauth\Exception\Exception;
|
|
use Hybridauth\Exception\InvalidApplicationCredentialsException;
|
|
use Hybridauth\Exception\AuthorizationDeniedException;
|
|
use Hybridauth\Exception\InvalidOauthTokenException;
|
|
use Hybridauth\Exception\InvalidAccessTokenException;
|
|
use Hybridauth\Data;
|
|
use Hybridauth\HttpClient;
|
|
use Hybridauth\Thirdparty\OAuth\OAuthConsumer;
|
|
use Hybridauth\Thirdparty\OAuth\OAuthRequest;
|
|
use Hybridauth\Thirdparty\OAuth\OAuthSignatureMethodHMACSHA1;
|
|
use Hybridauth\Thirdparty\OAuth\OAuthUtil;
|
|
|
|
/**
|
|
* This class can be used to simplify the authorization flow of OAuth 1 based service providers.
|
|
*
|
|
* Subclasses (i.e., providers adapters) can either use the already provided methods or override
|
|
* them when necessary.
|
|
*/
|
|
abstract class OAuth1 extends AbstractAdapter implements AdapterInterface
|
|
{
|
|
/**
|
|
* Base URL to provider API
|
|
*
|
|
* This var will be used to build urls when sending signed requests
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $apiBaseUrl = '';
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected $authorizeUrl = '';
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected $requestTokenUrl = '';
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected $accessTokenUrl = '';
|
|
|
|
/**
|
|
* IPD API Documentation
|
|
*
|
|
* OPTIONAL.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $apiDocumentation = '';
|
|
|
|
/**
|
|
* OAuth Version
|
|
*
|
|
* '1.0' OAuth Core 1.0
|
|
* '1.0a' OAuth Core 1.0 Revision A
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $oauth1Version = '1.0a';
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected $consumerKey = null;
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected $consumerSecret = null;
|
|
|
|
/**
|
|
* @var object
|
|
*/
|
|
protected $OAuthConsumer = null;
|
|
|
|
/**
|
|
* @var object
|
|
*/
|
|
protected $sha1Method = null;
|
|
|
|
/**
|
|
* @var object
|
|
*/
|
|
protected $consumerToken = null;
|
|
|
|
/**
|
|
* Authorization Url Parameters
|
|
*
|
|
* @var bool
|
|
*/
|
|
protected $AuthorizeUrlParameters = [];
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected $requestTokenMethod = 'POST';
|
|
|
|
/**
|
|
* @var array
|
|
*/
|
|
protected $requestTokenParameters = [];
|
|
|
|
/**
|
|
* @var array
|
|
*/
|
|
protected $requestTokenHeaders = [];
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected $tokenExchangeMethod = 'POST';
|
|
|
|
/**
|
|
* @var array
|
|
*/
|
|
protected $tokenExchangeParameters = [];
|
|
|
|
/**
|
|
* @var array
|
|
*/
|
|
protected $tokenExchangeHeaders = [];
|
|
|
|
/**
|
|
* @var array
|
|
*/
|
|
protected $apiRequestParameters = [];
|
|
|
|
/**
|
|
* @var array
|
|
*/
|
|
protected $apiRequestHeaders = [];
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
protected function configure()
|
|
{
|
|
$this->consumerKey = $this->config->filter('keys')->get('id') ?: $this->config->filter('keys')->get('key');
|
|
$this->consumerSecret = $this->config->filter('keys')->get('secret');
|
|
|
|
if (!$this->consumerKey || !$this->consumerSecret) {
|
|
throw new InvalidApplicationCredentialsException(
|
|
'Your application id is required in order to connect to ' . $this->providerId
|
|
);
|
|
}
|
|
|
|
if ($this->config->exists('tokens')) {
|
|
$this->setAccessToken($this->config->get('tokens'));
|
|
}
|
|
|
|
$this->setCallback($this->config->get('callback'));
|
|
$this->setApiEndpoints($this->config->get('endpoints'));
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
protected function initialize()
|
|
{
|
|
/**
|
|
* Set up OAuth Signature and Consumer
|
|
*
|
|
* OAuth Core: All Token requests and Protected Resources requests MUST be signed
|
|
* by the Consumer and verified by the Service Provider.
|
|
*
|
|
* The protocol defines three signature methods: HMAC-SHA1, RSA-SHA1, and PLAINTEXT..
|
|
*
|
|
* The Consumer declares a signature method in the oauth_signature_method parameter..
|
|
*
|
|
* http://oauth.net/core/1.0a/#signing_process
|
|
*/
|
|
$this->sha1Method = new OAuthSignatureMethodHMACSHA1();
|
|
|
|
$this->OAuthConsumer = new OAuthConsumer(
|
|
$this->consumerKey,
|
|
$this->consumerSecret
|
|
);
|
|
|
|
if ($this->getStoredData('request_token')) {
|
|
$this->consumerToken = new OAuthConsumer(
|
|
$this->getStoredData('request_token'),
|
|
$this->getStoredData('request_token_secret')
|
|
);
|
|
}
|
|
|
|
if ($this->getStoredData('access_token')) {
|
|
$this->consumerToken = new OAuthConsumer(
|
|
$this->getStoredData('access_token'),
|
|
$this->getStoredData('access_token_secret')
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function authenticate()
|
|
{
|
|
$this->logger->info(sprintf('%s::authenticate()', get_class($this)));
|
|
|
|
if ($this->isConnected()) {
|
|
return true;
|
|
}
|
|
|
|
try {
|
|
if (!$this->getStoredData('request_token')) {
|
|
// Start a new flow.
|
|
$this->authenticateBegin();
|
|
} elseif (empty($_GET['oauth_token']) && empty($_GET['denied'])) {
|
|
// A previous authentication was not finished, and this request is not finishing it.
|
|
$this->authenticateBegin();
|
|
} else {
|
|
// Finish a flow.
|
|
$this->authenticateFinish();
|
|
}
|
|
} catch (Exception $exception) {
|
|
$this->clearStoredData();
|
|
|
|
throw $exception;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function isConnected()
|
|
{
|
|
return (bool)$this->getStoredData('access_token');
|
|
}
|
|
|
|
/**
|
|
* Initiate the authorization protocol
|
|
*
|
|
* 1. Obtaining an Unauthorized Request Token
|
|
* 2. Build Authorization URL for Authorization Request and redirect the user-agent to the
|
|
* Authorization Server.
|
|
*/
|
|
protected function authenticateBegin()
|
|
{
|
|
$response = $this->requestAuthToken();
|
|
|
|
$this->validateAuthTokenRequest($response);
|
|
|
|
$authUrl = $this->getAuthorizeUrl();
|
|
|
|
$this->logger->debug(sprintf('%s::authenticateBegin(), redirecting user to:', get_class($this)), [$authUrl]);
|
|
|
|
HttpClient\Util::redirect($authUrl);
|
|
}
|
|
|
|
/**
|
|
* Finalize the authorization process
|
|
*
|
|
* @throws AuthorizationDeniedException
|
|
* @throws \Hybridauth\Exception\HttpClientFailureException
|
|
* @throws \Hybridauth\Exception\HttpRequestFailedException
|
|
* @throws InvalidAccessTokenException
|
|
* @throws InvalidOauthTokenException
|
|
*/
|
|
protected function authenticateFinish()
|
|
{
|
|
$this->logger->debug(
|
|
sprintf('%s::authenticateFinish(), callback url:', get_class($this)),
|
|
[HttpClient\Util::getCurrentUrl(true)]
|
|
);
|
|
|
|
$denied = filter_input(INPUT_GET, 'denied');
|
|
$oauth_problem = filter_input(INPUT_GET, 'oauth_problem');
|
|
$oauth_token = filter_input(INPUT_GET, 'oauth_token');
|
|
$oauth_verifier = filter_input(INPUT_GET, 'oauth_verifier');
|
|
|
|
if ($denied) {
|
|
throw new AuthorizationDeniedException(
|
|
'User denied access request. Provider returned a denied token: ' . htmlentities($denied)
|
|
);
|
|
}
|
|
|
|
if ($oauth_problem) {
|
|
throw new InvalidOauthTokenException(
|
|
'Provider returned an error. oauth_problem: ' . htmlentities($oauth_problem)
|
|
);
|
|
}
|
|
|
|
if (!$oauth_token) {
|
|
throw new InvalidOauthTokenException(
|
|
'Expecting a non-null oauth_token to continue the authorization flow.'
|
|
);
|
|
}
|
|
|
|
$response = $this->exchangeAuthTokenForAccessToken($oauth_token, $oauth_verifier);
|
|
|
|
$this->validateAccessTokenExchange($response);
|
|
|
|
$this->initialize();
|
|
}
|
|
|
|
/**
|
|
* Build Authorization URL for Authorization Request
|
|
*
|
|
* @param array $parameters
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function getAuthorizeUrl($parameters = [])
|
|
{
|
|
$this->AuthorizeUrlParameters = !empty($parameters)
|
|
? $parameters
|
|
: array_replace(
|
|
(array)$this->AuthorizeUrlParameters,
|
|
(array)$this->config->get('authorize_url_parameters')
|
|
);
|
|
|
|
$this->AuthorizeUrlParameters['oauth_token'] = $this->getStoredData('request_token');
|
|
|
|
return $this->authorizeUrl . '?' . http_build_query($this->AuthorizeUrlParameters, '', '&');
|
|
}
|
|
|
|
/**
|
|
* Unauthorized Request Token
|
|
*
|
|
* OAuth Core: The Consumer obtains an unauthorized Request Token by asking the Service Provider
|
|
* to issue a Token. The Request Token's sole purpose is to receive User approval and can only
|
|
* be used to obtain an Access Token.
|
|
*
|
|
* http://oauth.net/core/1.0/#auth_step1
|
|
* 6.1.1. Consumer Obtains a Request Token
|
|
*
|
|
* @return string Raw Provider API response
|
|
* @throws \Hybridauth\Exception\HttpClientFailureException
|
|
* @throws \Hybridauth\Exception\HttpRequestFailedException
|
|
*/
|
|
protected function requestAuthToken()
|
|
{
|
|
/**
|
|
* OAuth Core 1.0 Revision A: oauth_callback: An absolute URL to which the Service Provider will redirect
|
|
* the User back when the Obtaining User Authorization step is completed.
|
|
*
|
|
* http://oauth.net/core/1.0a/#auth_step1
|
|
*/
|
|
if ('1.0a' == $this->oauth1Version) {
|
|
$this->requestTokenParameters['oauth_callback'] = $this->callback;
|
|
}
|
|
|
|
$response = $this->oauthRequest(
|
|
$this->requestTokenUrl,
|
|
$this->requestTokenMethod,
|
|
$this->requestTokenParameters,
|
|
$this->requestTokenHeaders
|
|
);
|
|
|
|
return $response;
|
|
}
|
|
|
|
/**
|
|
* Validate Unauthorized Request Token Response
|
|
*
|
|
* OAuth Core: The Service Provider verifies the signature and Consumer Key. If successful,
|
|
* it generates a Request Token and Token Secret and returns them to the Consumer in the HTTP
|
|
* response body.
|
|
*
|
|
* http://oauth.net/core/1.0/#auth_step1
|
|
* 6.1.2. Service Provider Issues an Unauthorized Request Token
|
|
*
|
|
* @param string $response
|
|
*
|
|
* @return \Hybridauth\Data\Collection
|
|
* @throws InvalidOauthTokenException
|
|
*/
|
|
protected function validateAuthTokenRequest($response)
|
|
{
|
|
/**
|
|
* The response contains the following parameters:
|
|
*
|
|
* - oauth_token The Request Token.
|
|
* - oauth_token_secret The Token Secret.
|
|
* - oauth_callback_confirmed MUST be present and set to true.
|
|
*
|
|
* http://oauth.net/core/1.0/#auth_step1
|
|
* 6.1.2. Service Provider Issues an Unauthorized Request Token
|
|
*
|
|
* Example of a successful response:
|
|
*
|
|
* HTTP/1.1 200 OK
|
|
* Content-Type: text/html; charset=utf-8
|
|
* Cache-Control: no-store
|
|
* Pragma: no-cache
|
|
*
|
|
* oauth_token=80359084-clg1DEtxQF3wstTcyUdHF3wsdHM&oauth_token_secret=OIF07hPmJB:P
|
|
* 6qiHTi1znz6qiH3tTcyUdHnz6qiH3tTcyUdH3xW3wsDvV08e&example_parameter=example_value
|
|
*
|
|
* OAuthUtil::parse_parameters will attempt to decode the raw response into an array.
|
|
*/
|
|
$tokens = OAuthUtil::parse_parameters($response);
|
|
|
|
$collection = new Data\Collection($tokens);
|
|
|
|
if (!$collection->exists('oauth_token')) {
|
|
throw new InvalidOauthTokenException(
|
|
'Provider returned no oauth_token: ' . htmlentities($response)
|
|
);
|
|
}
|
|
|
|
$this->consumerToken = new OAuthConsumer(
|
|
$tokens['oauth_token'],
|
|
$tokens['oauth_token_secret']
|
|
);
|
|
|
|
$this->storeData('request_token', $tokens['oauth_token']);
|
|
$this->storeData('request_token_secret', $tokens['oauth_token_secret']);
|
|
|
|
return $collection;
|
|
}
|
|
|
|
/**
|
|
* Requests an Access Token
|
|
*
|
|
* OAuth Core: The Request Token and Token Secret MUST be exchanged for an Access Token and Token Secret.
|
|
*
|
|
* http://oauth.net/core/1.0a/#auth_step3
|
|
* 6.3.1. Consumer Requests an Access Token
|
|
*
|
|
* @param string $oauth_token
|
|
* @param string $oauth_verifier
|
|
*
|
|
* @return string Raw Provider API response
|
|
* @throws \Hybridauth\Exception\HttpClientFailureException
|
|
* @throws \Hybridauth\Exception\HttpRequestFailedException
|
|
*/
|
|
protected function exchangeAuthTokenForAccessToken($oauth_token, $oauth_verifier = '')
|
|
{
|
|
$this->tokenExchangeParameters['oauth_token'] = $oauth_token;
|
|
|
|
/**
|
|
* OAuth Core 1.0 Revision A: oauth_verifier: The verification code received from the Service Provider
|
|
* in the "Service Provider Directs the User Back to the Consumer" step.
|
|
*
|
|
* http://oauth.net/core/1.0a/#auth_step3
|
|
*/
|
|
if ('1.0a' == $this->oauth1Version) {
|
|
$this->tokenExchangeParameters['oauth_verifier'] = $oauth_verifier;
|
|
}
|
|
|
|
$response = $this->oauthRequest(
|
|
$this->accessTokenUrl,
|
|
$this->tokenExchangeMethod,
|
|
$this->tokenExchangeParameters,
|
|
$this->tokenExchangeHeaders
|
|
);
|
|
|
|
return $response;
|
|
}
|
|
|
|
/**
|
|
* Validate Access Token Response
|
|
*
|
|
* OAuth Core: If successful, the Service Provider generates an Access Token and Token Secret and returns
|
|
* them in the HTTP response body.
|
|
*
|
|
* The Access Token and Token Secret are stored by the Consumer and used when signing Protected Resources requests.
|
|
*
|
|
* http://oauth.net/core/1.0a/#auth_step3
|
|
* 6.3.2. Service Provider Grants an Access Token
|
|
*
|
|
* @param string $response
|
|
*
|
|
* @return \Hybridauth\Data\Collection
|
|
* @throws InvalidAccessTokenException
|
|
*/
|
|
protected function validateAccessTokenExchange($response)
|
|
{
|
|
/**
|
|
* The response contains the following parameters:
|
|
*
|
|
* - oauth_token The Access Token.
|
|
* - oauth_token_secret The Token Secret.
|
|
*
|
|
* http://oauth.net/core/1.0/#auth_step3
|
|
* 6.3.2. Service Provider Grants an Access Token
|
|
*
|
|
* Example of a successful response:
|
|
*
|
|
* HTTP/1.1 200 OK
|
|
* Content-Type: text/html; charset=utf-8
|
|
* Cache-Control: no-store
|
|
* Pragma: no-cache
|
|
*
|
|
* oauth_token=sHeLU7Far428zj8PzlWR75&oauth_token_secret=fXb30rzoG&oauth_callback_confirmed=true
|
|
*
|
|
* OAuthUtil::parse_parameters will attempt to decode the raw response into an array.
|
|
*/
|
|
$tokens = OAuthUtil::parse_parameters($response);
|
|
|
|
$collection = new Data\Collection($tokens);
|
|
|
|
if (!$collection->exists('oauth_token')) {
|
|
throw new InvalidAccessTokenException(
|
|
'Provider returned no access_token: ' . htmlentities($response)
|
|
);
|
|
}
|
|
|
|
$this->consumerToken = new OAuthConsumer(
|
|
$collection->get('oauth_token'),
|
|
$collection->get('oauth_token_secret')
|
|
);
|
|
|
|
$this->storeData('access_token', $collection->get('oauth_token'));
|
|
$this->storeData('access_token_secret', $collection->get('oauth_token_secret'));
|
|
|
|
$this->deleteStoredData('request_token');
|
|
$this->deleteStoredData('request_token_secret');
|
|
|
|
return $collection;
|
|
}
|
|
|
|
/**
|
|
* Send a signed request to provider API
|
|
*
|
|
* Note: Since the specifics of error responses is beyond the scope of RFC6749 and OAuth specifications,
|
|
* Hybridauth will consider any HTTP status code that is different than '200 OK' as an ERROR.
|
|
*
|
|
* @param string $url
|
|
* @param string $method
|
|
* @param array $parameters
|
|
* @param array $headers
|
|
* @param bool $multipart
|
|
*
|
|
* @return mixed
|
|
* @throws \Hybridauth\Exception\HttpClientFailureException
|
|
* @throws \Hybridauth\Exception\HttpRequestFailedException
|
|
*/
|
|
public function apiRequest($url, $method = 'GET', $parameters = [], $headers = [], $multipart = false)
|
|
{
|
|
// refresh tokens if needed
|
|
$this->maintainToken();
|
|
|
|
if (strrpos($url, 'http://') !== 0 && strrpos($url, 'https://') !== 0) {
|
|
$url = rtrim($this->apiBaseUrl, '/') . '/' . ltrim($url, '/');
|
|
}
|
|
|
|
$parameters = array_replace($this->apiRequestParameters, (array)$parameters);
|
|
|
|
$headers = array_replace($this->apiRequestHeaders, (array)$headers);
|
|
|
|
$response = $this->oauthRequest($url, $method, $parameters, $headers, $multipart);
|
|
|
|
$response = (new Data\Parser())->parse($response);
|
|
|
|
return $response;
|
|
}
|
|
|
|
/**
|
|
* Setup and Send a Signed Oauth Request
|
|
*
|
|
* This method uses OAuth Library.
|
|
*
|
|
* @param string $uri
|
|
* @param string $method
|
|
* @param array $parameters
|
|
* @param array $headers
|
|
* @param bool $multipart
|
|
*
|
|
* @return string Raw Provider API response
|
|
* @throws \Hybridauth\Exception\HttpClientFailureException
|
|
* @throws \Hybridauth\Exception\HttpRequestFailedException
|
|
*/
|
|
protected function oauthRequest($uri, $method = 'GET', $parameters = [], $headers = [], $multipart = false)
|
|
{
|
|
$signing_parameters = $parameters;
|
|
if ($multipart) {
|
|
$signing_parameters = [];
|
|
}
|
|
|
|
$request = OAuthRequest::from_consumer_and_token(
|
|
$this->OAuthConsumer,
|
|
$this->consumerToken,
|
|
$method,
|
|
$uri,
|
|
$signing_parameters
|
|
);
|
|
|
|
$request->sign_request(
|
|
$this->sha1Method,
|
|
$this->OAuthConsumer,
|
|
$this->consumerToken
|
|
);
|
|
|
|
$uri = $request->get_normalized_http_url();
|
|
$headers = array_replace($request->to_header(), (array)$headers);
|
|
|
|
$response = $this->httpClient->request(
|
|
$uri,
|
|
$method,
|
|
$parameters,
|
|
$headers,
|
|
$multipart
|
|
);
|
|
|
|
$this->validateApiResponse('Signed API request to ' . $uri . ' has returned an error');
|
|
|
|
return $response;
|
|
}
|
|
}
|