<?php
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Http\Client\Auth;
use Cake\Core\Exception\Exception;
use Cake\Http\Client\Request;
use Cake\Utility\Security;
use RuntimeException;
/**
* Oauth 1 authentication strategy for Cake\Http\Client
*
* This object does not handle getting Oauth access tokens from the service
* provider. It only handles make client requests *after* you have obtained the Oauth
* tokens.
*
* Generally not directly constructed, but instead used by Cake\Http\Client
* when $options['auth']['type'] is 'oauth'
*/
class Oauth
{
/**
* Add headers for Oauth authorization.
*
* @param \Cake\Http\Client\Request $request The request object.
* @param array $credentials Authentication credentials.
* @return \Cake\Http\Client\Request The updated request.
* @throws \Cake\Core\Exception\Exception On invalid signature types.
*/
public function authentication(Request $request, array $credentials)
{
if (!isset($credentials['consumerKey'])) {
return $request;
}
if (empty($credentials['method'])) {
$credentials['method'] = 'hmac-sha1';
}
$credentials['method'] = strtoupper($credentials['method']);
$value = null;
switch ($credentials['method']) {
case 'HMAC-SHA1':
$hasKeys = isset(
$credentials['consumerSecret'],
$credentials['token'],
$credentials['tokenSecret']
);
if (!$hasKeys) {
return $request;
}
$value = $this->_hmacSha1($request, $credentials);
break;
case 'RSA-SHA1':
if (!isset($credentials['privateKey'])) {
return $request;
}
$value = $this->_rsaSha1($request, $credentials);
break;
case 'PLAINTEXT':
$hasKeys = isset(
$credentials['consumerSecret'],
$credentials['token'],
$credentials['tokenSecret']
);
if (!$hasKeys) {
return $request;
}
$value = $this->_plaintext($request, $credentials);
break;
default:
throw new Exception(sprintf('Unknown Oauth signature method %s', $credentials['method']));
}
return $request->withHeader('Authorization', $value);
}
/**
* Plaintext signing
*
* This method is **not** suitable for plain HTTP.
* You should only ever use PLAINTEXT when dealing with SSL
* services.
*
* @param \Cake\Http\Client\Request $request The request object.
* @param array $credentials Authentication credentials.
* @return string Authorization header.
*/
protected function _plaintext($request, $credentials)
{
$values = [
'oauth_version' => '1.0',
'oauth_nonce' => uniqid(),
'oauth_timestamp' => time(),
'oauth_signature_method' => 'PLAINTEXT',
'oauth_token' => $credentials['token'],
'oauth_consumer_key' => $credentials['consumerKey'],
];
if (isset($credentials['realm'])) {
$values['oauth_realm'] = $credentials['realm'];
}
$key = [$credentials['consumerSecret'], $credentials['tokenSecret']];
$key = implode('&', $key);
$values['oauth_signature'] = $key;
return $this->_buildAuth($values);
}
/**
* Use HMAC-SHA1 signing.
*
* This method is suitable for plain HTTP or HTTPS.
*
* @param \Cake\Http\Client\Request $request The request object.
* @param array $credentials Authentication credentials.
* @return string
*/
protected function _hmacSha1($request, $credentials)
{
$nonce = isset($credentials['nonce']) ? $credentials['nonce'] : uniqid();
$timestamp = isset($credentials['timestamp']) ? $credentials['timestamp'] : time();
$values = [
'oauth_version' => '1.0',
'oauth_nonce' => $nonce,
'oauth_timestamp' => $timestamp,
'oauth_signature_method' => 'HMAC-SHA1',
'oauth_token' => $credentials['token'],
'oauth_consumer_key' => $credentials['consumerKey'],
];
$baseString = $this->baseString($request, $values);
if (isset($credentials['realm'])) {
$values['oauth_realm'] = $credentials['realm'];
}
$key = [$credentials['consumerSecret'], $credentials['tokenSecret']];
$key = array_map([$this, '_encode'], $key);
$key = implode('&', $key);
$values['oauth_signature'] = base64_encode(
hash_hmac('sha1', $baseString, $key, true)
);
return $this->_buildAuth($values);
}
/**
* Use RSA-SHA1 signing.
*
* This method is suitable for plain HTTP or HTTPS.
*
* @param \Cake\Http\Client\Request $request The request object.
* @param array $credentials Authentication credentials.
* @return string
*
* @throws \RuntimeException
*/
protected function _rsaSha1($request, $credentials)
{
if (!function_exists('openssl_pkey_get_private')) {
throw new RuntimeException('RSA-SHA1 signature method requires the OpenSSL extension.');
}
$nonce = isset($credentials['nonce']) ? $credentials['nonce'] : bin2hex(Security::randomBytes(16));
$timestamp = isset($credentials['timestamp']) ? $credentials['timestamp'] : time();
$values = [
'oauth_version' => '1.0',
'oauth_nonce' => $nonce,
'oauth_timestamp' => $timestamp,
'oauth_signature_method' => 'RSA-SHA1',
'oauth_consumer_key' => $credentials['consumerKey'],
];
if (isset($credentials['consumerSecret'])) {
$values['oauth_consumer_secret'] = $credentials['consumerSecret'];
}
if (isset($credentials['token'])) {
$values['oauth_token'] = $credentials['token'];
}
if (isset($credentials['tokenSecret'])) {
$values['oauth_token_secret'] = $credentials['tokenSecret'];
}
$baseString = $this->baseString($request, $values);
if (isset($credentials['realm'])) {
$values['oauth_realm'] = $credentials['realm'];
}
if (is_resource($credentials['privateKey'])) {
$resource = $credentials['privateKey'];
$privateKey = stream_get_contents($resource);
rewind($resource);
$credentials['privateKey'] = $privateKey;
}
$credentials += [
'privateKeyPassphrase' => null,
];
if (is_resource($credentials['privateKeyPassphrase'])) {
$resource = $credentials['privateKeyPassphrase'];
$passphrase = stream_get_line($resource, 0, PHP_EOL);
rewind($resource);
$credentials['privateKeyPassphrase'] = $passphrase;
}
$privateKey = openssl_pkey_get_private($credentials['privateKey'], $credentials['privateKeyPassphrase']);
$signature = '';
openssl_sign($baseString, $signature, $privateKey);
openssl_free_key($privateKey);
$values['oauth_signature'] = base64_encode($signature);
return $this->_buildAuth($values);
}
/**
* Generate the Oauth basestring
*
* - Querystring, request data and oauth_* parameters are combined.
* - Values are sorted by name and then value.
* - Request values are concatenated and urlencoded.
* - The request URL (without querystring) is normalized.
* - The HTTP method, URL and request parameters are concatenated and returned.
*
* @param \Cake\Http\Client\Request $request The request object.
* @param array $oauthValues Oauth values.
* @return string
*/
public function baseString($request, $oauthValues)
{
$parts = [
$request->getMethod(),
$this->_normalizedUrl($request->getUri()),
$this->_normalizedParams($request, $oauthValues),
];
$parts = array_map([$this, '_encode'], $parts);
return implode('&', $parts);
}
/**
* Builds a normalized URL
*
* Section 9.1.2. of the Oauth spec
*
* @param \Psr\Http\Message\UriInterface $uri Uri object to build a normalized version of.
* @return string Normalized URL
*/
protected function _normalizedUrl($uri)
{
$out = $uri->getScheme() . '://';
$out .= strtolower($uri->getHost());
$out .= $uri->getPath();
return $out;
}
/**
* Sorts and normalizes request data and oauthValues
*
* Section 9.1.1 of Oauth spec.
*
* - URL encode keys + values.
* - Sort keys & values by byte value.
*
* @param \Cake\Http\Client\Request $request The request object.
* @param array $oauthValues Oauth values.
* @return string sorted and normalized values
*/
protected function _normalizedParams($request, $oauthValues)
{
$query = parse_url($request->getUri(), PHP_URL_QUERY);
parse_str($query, $queryArgs);
$post = [];
$body = $request->body();
if (is_string($body) && $request->getHeaderLine('content-type') === 'application/x-www-form-urlencoded') {
parse_str($body, $post);
}
if (is_array($body)) {
$post = $body;
}
$args = array_merge($queryArgs, $oauthValues, $post);
$pairs = $this->_normalizeData($args);
$data = [];
foreach ($pairs as $pair) {
$data[] = implode('=', $pair);
}
sort($data, SORT_STRING);
return implode('&', $data);
}
/**
* Recursively convert request data into the normalized form.
*
* @param array $args The arguments to normalize.
* @param string $path The current path being converted.
* @see https://tools.ietf.org/html/rfc5849#section-3.4.1.3.2
* @return array
*/
protected function _normalizeData($args, $path = '')
{
$data = [];
foreach ($args as $key => $value) {
if ($path) {
// Fold string keys with [].
// Numeric keys result in a=b&a=c. While this isn't
// standard behavior in PHP, it is common in other platforms.
if (!is_numeric($key)) {
$key = "{$path}[{$key}]";
} else {
$key = $path;
}
}
if (is_array($value)) {
uksort($value, 'strcmp');
$data = array_merge($data, $this->_normalizeData($value, $key));
} else {
$data[] = [$key, $value];
}
}
return $data;
}
/**
* Builds the Oauth Authorization header value.
*
* @param array $data The oauth_* values to build
* @return string
*/
protected function _buildAuth($data)
{
$out = 'OAuth ';
$params = [];
foreach ($data as $key => $value) {
$params[] = $key . '="' . $this->_encode($value) . '"';
}
$out .= implode(',', $params);
return $out;
}
/**
* URL Encodes a value based on rules of rfc3986
*
* @param string $value Value to encode.
* @return string
*/
protected function _encode($value)
{
return str_replace(['%7E', '+'], ['~', ' '], rawurlencode($value));
}
}
// @deprecated 3.4.0 Add backwards compat alias.
class_alias('Cake\Http\Client\Auth\Oauth', 'Cake\Network\Http\Auth\Oauth');