application = $application; $this->client = $client ?: HttpFactory::getHttp($options); $this->input = $input ?: $application->getInput(); $this->options = $options; $this->version = $version; } /** * Method to form the oauth flow. * * @return array|null The access token. * * @since 1.0 * @throws \DomainException */ public function authenticate() { // Already got some credentials stored? if ($this->token) { $response = $this->verifyCredentials(); if ($response) { return $this->token; } $this->token = null; } // Check for callback. if (strcmp($this->version, '1.0a') === 0) { $verifier = $this->input->get('oauth_verifier'); } else { $verifier = $this->input->get('oauth_token'); } if (!empty($verifier)) { $session = $this->application->getSession(); // Get token form session. $this->token = [ 'key' => $session->get('oauth_token.key'), 'secret' => $session->get('oauth_token.secret'), ]; // Verify the returned request token. if (strcmp($this->token['key'], $this->input->get('oauth_token')) !== 0) { throw new \DomainException('Bad session!'); } // Set token verifier for 1.0a. if (strcmp($this->version, '1.0a') === 0) { $this->token['verifier'] = $this->input->get('oauth_verifier'); } // Generate access token. $this->generateAccessToken(); // Return the access token. return $this->token; } // Generate a request token. $this->generateRequestToken(); // Authenticate the user and authorise the app. $this->authorise(); } /** * Method used to get a request token. * * @return void * * @since 1.1.2 * @throws \DomainException */ private function generateRequestToken() { $parameters = []; // Set the callback URL. if ($this->getOption('callback')) { $parameters['oauth_callback'] = $this->getOption('callback'); } // Make an OAuth request for the Request Token. $response = $this->oauthRequest($this->getOption('requestTokenURL'), 'POST', $parameters); parse_str($response->body, $params); if (strcmp($this->version, '1.0a') === 0 && strcmp($params['oauth_callback_confirmed'], 'true') !== 0) { throw new \DomainException('Bad request token!'); } // Save the request token. $this->token = ['key' => $params['oauth_token'], 'secret' => $params['oauth_token_secret']]; // Save the request token in session $session = $this->application->getSession(); $session->set('oauth_token.key', $this->token['key']); $session->set('oauth_token.secret', $this->token['secret']); } /** * Method used to authorise the application. * * @return void * * @since 1.1.2 */ private function authorise() { $url = $this->getOption('authoriseURL') . '?oauth_token=' . $this->token['key']; if ($this->getOption('scope')) { $scope = \is_array($this->getOption('scope')) ? implode(' ', $this->getOption('scope')) : $this->getOption('scope'); $url .= '&scope=' . urlencode($scope); } if ($this->getOption('sendheaders')) { $this->application->redirect($url); } } /** * Method used to get an access token. * * @return void * * @since 1.1.2 */ private function generateAccessToken() { // Set the parameters. $parameters = [ 'oauth_token' => $this->token['key'], ]; if (strcmp($this->version, '1.0a') === 0) { $parameters = array_merge($parameters, ['oauth_verifier' => $this->token['verifier']]); } // Make an OAuth request for the Access Token. $response = $this->oauthRequest($this->getOption('accessTokenURL'), 'POST', $parameters); parse_str($response->body, $params); // Save the access token. $this->token = ['key' => $params['oauth_token'], 'secret' => $params['oauth_token_secret']]; } /** * Method used to make an OAuth request. * * @param string $url The request URL. * @param string $method The request method. * @param array $parameters Array containing request parameters. * @param mixed $data The POST request data. * @param array $headers An array of name-value pairs to include in the header of the request * * @return \Joomla\Http\Response * * @since 1.0 * @throws \DomainException */ public function oauthRequest($url, $method, $parameters, $data = [], $headers = []) { // Set the parameters. $defaults = [ 'oauth_consumer_key' => $this->getOption('consumer_key'), 'oauth_signature_method' => 'HMAC-SHA1', 'oauth_version' => '1.0', 'oauth_nonce' => $this->generateNonce(), 'oauth_timestamp' => time(), ]; $parameters = array_merge($parameters, $defaults); // Do not encode multipart parameters. Do not include $data in the signature if $data is not array. if (isset($headers['Content-Type']) && strpos($headers['Content-Type'], 'multipart/form-data') !== false || !\is_array($data)) { $oauthHeaders = $parameters; } else { // Use all parameters for the signature. $oauthHeaders = array_merge($parameters, $data); } // Sign the request. $oauthHeaders = $this->signRequest($url, $method, $oauthHeaders); // Get parameters for the Authorisation header. if (\is_array($data)) { $oauthHeaders = array_diff_key($oauthHeaders, $data); } // Send the request. switch ($method) { case 'GET': $url = $this->toUrl($url, $data); $response = $this->client->get($url, ['Authorization' => $this->createHeader($oauthHeaders)]); break; case 'POST': $headers = array_merge($headers, ['Authorization' => $this->createHeader($oauthHeaders)]); $response = $this->client->post($url, $data, $headers); break; case 'PUT': $headers = array_merge($headers, ['Authorization' => $this->createHeader($oauthHeaders)]); $response = $this->client->put($url, $data, $headers); break; case 'DELETE': $headers = array_merge($headers, ['Authorization' => $this->createHeader($oauthHeaders)]); $response = $this->client->delete($url, $headers); break; } // Validate the response code. $this->validateResponse($url, $response); return $response; } /** * Method to validate a response. * * @param string $url The request URL. * @param Response $response The response to validate. * * @return void * * @since 1.0 * @throws \DomainException */ abstract public function validateResponse($url, $response); /** * Method used to create the header for the POST request. * * @param array $parameters Array containing request parameters. * * @return string The header. * * @since 1.1.2 */ private function createHeader(array $parameters): string { $header = 'OAuth '; foreach ($parameters as $key => $value) { if (!strcmp($header, 'OAuth ')) { $header .= $key . '="' . $this->safeEncode($value) . '"'; } else { $header .= ', ' . $key . '="' . $value . '"'; } } return $header; } /** * Method to create the URL formed string with the parameters. * * @param string $url The request URL. * @param array $parameters Array containing request parameters. * * @return string The formed URL. * * @since 1.0 */ public function toUrl($url, $parameters) { $uri = new Uri($url); $uri->setQuery($parameters); return (string) $uri; } /** * Method used to sign requests. * * @param string $url The URL to sign. * @param string $method The request method. * @param array $parameters Array containing request parameters. * * @return array The array containing the request parameters, including signature. * * @since 1.1.2 */ private function signRequest(string $url, string $method, array $parameters): array { // Create the signature base string. $base = $this->baseString($url, $method, $parameters); $parameters['oauth_signature'] = $this->safeEncode( base64_encode( hash_hmac('sha1', $base, $this->prepareSigningKey(), true) ) ); return $parameters; } /** * Prepare the signature base string. * * @param string $url The URL to sign. * @param string $method The request method. * @param array $parameters Array containing request parameters. * * @return string The base string. * * @since 1.1.2 */ private function baseString(string $url, string $method, array $parameters): string { // Sort the parameters alphabetically uksort($parameters, 'strcmp'); // Encode parameters. foreach ($parameters as $key => $value) { $key = $this->safeEncode($key); if (\is_array($value)) { foreach ($value as $k => $v) { $v = $this->safeEncode($v); $kv[] = "{$key}={$v}"; } } else { $value = $this->safeEncode($value); $kv[] = "{$key}={$value}"; } } // Form the parameter string. $params = implode('&', $kv); // Signature base string elements. $base = [ $method, $url, $params, ]; // Return the base string. return implode('&', $this->safeEncode($base)); } /** * Encodes the string or array passed in a way compatible with OAuth. * If an array is passed each array value will will be encoded. * * @param mixed $data The scalar or array to encode. * * @return string $data encoded in a way compatible with OAuth. * * @since 1.0 */ public function safeEncode($data) { if (\is_array($data)) { return array_map([$this, 'safeEncode'], $data); } if (is_scalar($data)) { return str_ireplace( ['+', '%7E'], [' ', '~'], rawurlencode($data) ); } return ''; } /** * Method used to generate the current nonce. * * @return string The current nonce. * * @since 1.0 */ public static function generateNonce() { // The md5s look nicer than numbers. return md5(microtime() . random_bytes(16)); } /** * Prepares the OAuth signing key. * * @return string The prepared signing key. * * @since 1.1.2 */ private function prepareSigningKey(): string { return $this->safeEncode($this->getOption('consumer_secret')) . '&' . $this->safeEncode(($this->token) ? $this->token['secret'] : ''); } /** * Returns an HTTP 200 OK response code and a representation of the requesting user if authentication was successful; * returns a 401 status code and an error message if not. * * @return array The decoded JSON response * * @since 1.0 */ abstract public function verifyCredentials(); /** * Get an option from the OAuth1 Client instance. * * @param string $key The name of the option to get * @param mixed $default Optional default value if the option does not exist * * @return mixed The option value * * @since 1.0 */ public function getOption($key, $default = null) { return $this->options[$key] ?? $default; } /** * Set an option for the OAuth1 Client instance. * * @param string $key The name of the option to set * @param mixed $value The option value to set * * @return $this * * @since 1.0 */ public function setOption($key, $value) { $this->options[$key] = $value; return $this; } /** * Get the oauth token key or secret. * * @return array The oauth token key and secret. * * @since 1.0 */ public function getToken() { return $this->token; } /** * Set the oauth token. * * @param array $token The access token key and secret. * * @return $this * * @since 1.0 */ public function setToken($token) { $this->token = $token; return $this; } }