jenkins-bot has submitted this change and it was merged. Change subject: Add OAuth library ......................................................................
Add OAuth library Add an OAuth client library extracted from https://code.google.com/p/oauth/ and an OAuth client extracted from https://github.com/Stype/mwoauth-php. Change-Id: Id8222bbe9960942f94fd67d7fbc3290cb0105245 --- A src/OAuth/Client.php A src/OAuth/ClientConfig.php A src/OAuth/Consumer.php A src/OAuth/Exception.php A src/OAuth/Request.php A src/OAuth/SignatureMethod.php A src/OAuth/SignatureMethod/HmacSha1.php A src/OAuth/SignatureMethod/Plaintext.php A src/OAuth/SignatureMethod/RsaSha1.php A src/OAuth/Token.php A src/OAuth/Util.php A tests/OAuth/ConsumerTest.php A tests/OAuth/RequestTest.php A tests/OAuth/SignatureMethod/HmacSha1Test.php A tests/OAuth/SignatureMethod/PlaintextTest.php A tests/OAuth/SignatureMethod/RsaSha1Test.php A tests/OAuth/TokenTest.php 17 files changed, 2,516 insertions(+), 0 deletions(-) Approvals: CSteipp: Looks good to me, approved jenkins-bot: Verified diff --git a/src/OAuth/Client.php b/src/OAuth/Client.php new file mode 100644 index 0000000..133bfbe --- /dev/null +++ b/src/OAuth/Client.php @@ -0,0 +1,358 @@ +<?php +/** + * @section LICENSE + * This file is part of Wikimedia Slim application library + * + * Wikimedia Slim application library is free software: you can + * redistribute it and/or modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * Wikimedia Slim application library is distributed in the hope that it + * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with Wikimedia Grants Review application. If not, see + * <http://www.gnu.org/licenses/>. + * + * @file + * @copyright © 2015 Chris Steipp, Wikimedia Foundation and contributors. + */ + +namespace Wikimedia\Slimapp\OAuth; + +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; +use Wikimedia\Slimapp\OAuth\SignatureMethod\HmacSha1; +use Exception; + +/** + * MediaWiki OAuth client. + */ +class Client implements LoggerAwareInterface { + + /** + * @var LoggerInterface $logger + */ + protected $logger; + + /** + * @var ClientConfig $config + */ + private $config; + + /** + * Any extra params in the call that need to be signed + * @var array $extraParams + */ + private $extraParams = array(); + + /** + * url, defaults to oob + * @var string $callbackUrl + */ + private $callbackUrl = 'oob'; + + /** + * Track the last random nonce generated by the OAuth lib, used to verify + * /identity response isn't a replay + * @var string $lastNonce + */ + private $lastNonce; + + /** + * @param ClientConfig $config + * @param LoggerInterface $logger + */ + function __construct( + ClientConfig $config, + LoggerInterface $logger = null + ) { + $this->config = $config; + $this->logger = $logger ?: new NullLogger(); + } + + /** + * @param LoggerInterface $logger + */ + public function setLogger( LoggerInterface $logger ) { + $this->logger = $logger; + } + + /** + * @param string $url + * @param string $key + * @param string $secret + * @return MediaWiki + */ + public static function newFromKeyAndSecret( $url, $key, $secret ) { + $config = new ClientConfig( $url, true, true ); + $config->setConsumer( new Consumer( $key, $secret ) ); + return new static( $config ); + } + + /** + * @param string $key + * @param string $value + */ + public function setExtraParam( $key, $value ) { + $this->extraParams[$key] = $value; + } + + /** + * @param array $params + */ + public function setExtraParams( array $params ) { + $this->extraParams = $params; + } + + /** + * @param string $url + */ + public function setCallback( $url ) { + $this->callbackUrl = $url; + } + + /** + * First part of 3-legged OAuth, get the request Token. + * Redirect your authorizing users to the redirect url, and keep + * track of the request token since you need to pass it into complete() + * + * @return array (redirect, request/temp token) + */ + public function initiate() { + $initUrl = $this->config->endpointURL . + '/initiate&format=json&oauth_callback=' . + urlencode( $this->callbackUrl ); + $data = $this->makeOAuthCall( null, $initUrl ); + $return = json_decode( $data ); + if ( $return->oauth_callback_confirmed !== 'true' ) { + throw new Exception( "Callback wasn't confirmed" ); + } + $requestToken = new Token( $return->key, $return->secret ); + $url = $this->config->redirURL ?: + $this->config->endpointURL . "/authorize&"; + $url .= "oauth_token={$requestToken->key}&oauth_consumer_key={$this->config->consumer->key}"; + return array( $url, $requestToken ); + } + + /** + * The final leg of the OAuth handshake. Exchange the request Token from + * initiate() and the verification code that the user submitted back to you + * for an access token, which you'll use for all API calls. + * + * @param Token $requestToken Authorization code sent to the callback url + * @param string Temp/request token obtained from initiate, or null if this + * object was used and the token is already set. + * @return Token The access token + */ + public function complete( Token $requestToken, $verifyCode ) { + $tokenUrl = $this->config->endpointURL . '/token&format=json'; + $this->setExtraParam( 'oauth_verifier', $verifyCode ); + $data = $this->makeOAuthCall( $requestToken, $tokenUrl ); + $return = json_decode( $data ); + $accessToken = new Token( $return->key, $return->secret ); + // Cleanup after ourselves + $this->setExtraParams = array(); + return $accessToken; + } + + /** + * Optional step. This call the MediaWiki specific /identify method, which + * returns a signed statement of the authorizing user's identity. Use this + * if you are authenticating users in your application, and you need to + * know their username, groups, rights, etc in MediaWiki. + * + * @param Token $accessToken Access token from complete() + * @return object containing attributes of the user + */ + public function identify( Token $accessToken ) { + $identifyUrl = $this->config->endpointURL . '/identify'; + $data = $this->makeOAuthCall( $accessToken, $identifyUrl ); + $identity = $this->decodeJWT( $data, $this->config->consumer->secret ); + if ( !$this->validateJWT( + $identity, + $this->config->consumer->key, + $this->config->canonicalServerUrl, + $this->lastNonce + ) ) { + throw new Exception( "JWT didn't validate" ); + } + return $identity; + } + + /** + * Make a signed request to MediaWiki + * + * @param Token $token additional token to use in signature, besides + * the consumer token. In most cases, this will be the access token you + * got from complete(), but we set it to the request token when + * finishing the handshake. + * @param string $url URL to call + * @param bool $isPost true if this should be a POST request + * @param array $postFields POST parameters, only if $isPost is also true + * @return string Body from the curl request + */ + public function makeOAuthCall( + /*Token*/ $token, $url, $isPost = false, array $postFields = null + ) { + $params = array(); + // Get any params from the url + if ( strpos( $url, '?' ) ) { + $parsed = parse_url( $url ); + parse_str( $parsed['query'], $params ); + } + $params += $this->extraParams; + if ( $isPost && $postFields ) { + $params += $postFields; + } + $method = $isPost ? 'POST' : 'GET'; + $req = Request::fromConsumerAndToken( + $this->config->consumer, + $token, + $method, + $url, + $params + ); + $req->signRequest( + new HmacSha1(), + $this->config->consumer, + $token + ); + $this->lastNonce = $req->getParameter( 'oauth_nonce' ); + return $this->makeCurlCall( + $url, + $req->toHeader(), + $isPost, + $postFields, + $this->config + ); + } + + /** + * @param string $url + * @param array $headers + * @param bool $isPost + * @param array $postFields + * @return string + */ + private function makeCurlCall( + $url, $headers, $isPost, array $postFields = null + ) { + $ch = curl_init(); + curl_setopt( $ch, CURLOPT_URL, (string) $url ); + curl_setopt( $ch, CURLOPT_HEADER, 0 ); + curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 ); + curl_setopt( $ch, CURLOPT_HTTPHEADER, array( $headers ) ); + if ( $isPost ) { + curl_setopt( $ch, CURLOPT_POST, true ); + curl_setopt( $ch, CURLOPT_POSTFIELDS, http_build_query( $postFields ) ); + } + if ( $this->config->useSSL ) { + curl_setopt( $ch, CURLOPT_PORT, 443 ); + } + if ( $this->config->verifySSL ) { + curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, true ); + curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, 2 ); + } else { + curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false ); + curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, 0 ); + } + $data = curl_exec( $ch ); + if ( !$data ) { + throw new Exception( 'Curl error: ' . curl_error( $ch ) ); + } + return $data; + } + + /** + * @param string $JWT Json web token + * @param string $secret + * @return object + */ + private function decodeJWT( $JWT, $secret ) { + list( $headb64, $bodyb64, $sigb64 ) = explode( '.', $JWT ); + $header = json_decode( $this->urlsafeB64Decode( $headb64 ) ); + $payload = json_decode( $this->urlsafeB64Decode( $bodyb64 ) ); + $sig = $this->urlsafeB64Decode( $sigb64 ); + // MediaWiki will only use sha256 hmac (HS256) for now. This check + // makes sure an attacker doesn't return a JWT with 'none' signature + // type. + $expectSig = hash_hmac( + 'sha256', "{$headb64}.{$bodyb64}", $secret, true + ); + if ( $header->alg !== 'HS256' || !$this->compareHash( $sig, $expectSig ) ) { + throw new Exception( "Invalid JWT signature from /identify." ); + } + return $payload; + } + + /** + * @param object $identity + * @param string $consumerKey + * @param string $expectedConnonicalServer + * @param string $nonce + */ + protected function validateJWT( + $identity, $consumerKey, $expectedConnonicalServer, $nonce + ) { + // Verify the issuer is who we expect (server sends $wgCanonicalServer) + if ( $identity->iss !== $expectedConnonicalServer ) { + $this->logger->info( + "Invalid issuer '{$identity->iss}': expected '{$expectedConnonicalServer}'" ); + return false; + } + // Verify we are the intended audience + if ( $identity->aud !== $consumerKey ) { + $this->logger->info( "Invalid audience '{$identity->aud}': expected '{$consumerKey}'" ); + return false; + } + // Verify we are within the time limits of the token. Issued at (iat) + // should be in the past, Expiration (exp) should be in the future. + $now = time(); + if ( $identity->iat > $now || $identity->exp < $now ) { + $this->logger->info( + "Invalid times issued='{$identity->iat}', " . + "expires='{$identity->exp}', now='{$now}'" + ); + return false; + } + // Verify we haven't seen this nonce before, which would indicate a replay attack + if ( $identity->nonce !== $nonce ) { + $this->logger->info( "Invalid nonce '{$identity->nonce}': expected '{$nonce}'" ); + return false; + } + return true; + } + + /** + * @param string $input + * @return string + */ + private function urlsafeB64Decode( $input ) { + $remainder = strlen( $input ) % 4; + if ( $remainder ) { + $padlen = 4 - $remainder; + $input .= str_repeat( '=', $padlen ); + } + return base64_decode( strtr( $input, '-_', '+/' ) ); + } + + /** + * Constant time comparison + * @param string $hash1 + * @param string $hash2 + * @return bool + */ + private function compareHash( $hash1, $hash2 ) { + $result = strlen( $hash1 ) ^ strlen( $hash2 ); + $len = min( strlen( $hash1 ), strlen( $hash2 ) ) - 1; + for ( $i = 0; $i < $len; $i++ ) { + $result |= ord( $hash1{$i} ) ^ ord( $hash2{$i} ); + } + return $result == 0; + } +} diff --git a/src/OAuth/ClientConfig.php b/src/OAuth/ClientConfig.php new file mode 100644 index 0000000..f9f143a --- /dev/null +++ b/src/OAuth/ClientConfig.php @@ -0,0 +1,99 @@ +<?php +/** + * @section LICENSE + * This file is part of Wikimedia Slim application library + * + * Wikimedia Slim application library is free software: you can + * redistribute it and/or modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * Wikimedia Slim application library is distributed in the hope that it + * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with Wikimedia Grants Review application. If not, see + * <http://www.gnu.org/licenses/>. + * + * @file + * @copyright © 2015 Chris Steipp, Wikimedia Foundation and contributors. + */ + +namespace Wikimedia\Slimapp\OAuth; + +/** + * MediaWiki OAuth client configuration + */ +class ClientConfig { + /** + * Url to the OAuth special page + * @var string $endpointURL + */ + public $endpointURL; + + /** + * Canonical server url, used to check /identify's iss. + * A default value will be created based on the provided $endpointURL. + * @var string $canonicalServerUrl + */ + public $canonicalServerUrl; + + /** + * Url that the user is sent to. Can be different from $endpointURL to + * play nice with MobileFrontend, etc. + * @var string|null $redirURL + */ + public $redirURL = null; + + /** + * Use https when calling the server. + * @var bool $useSSL + */ + public $useSSL; + + /** + * If you're testing against a server with self-signed certificates, you + * can turn this off but don't do this in production. + * @var bool $verifySSL + */ + public $verifySSL; + + /** + * @var Consumer|null $consumer + */ + public $consumer = null; + + /** + * @param string $url OAuth endpoint URL + * @param bool $verifySSL + */ + function __construct( $url, $verifySSL = true ) { + $this->endpointURL = $url; + $this->verifySSL = $verifySSL; + + $parts = parse_url( $url ); + $this->useSSL = $parts['scheme'] === 'https'; + $this->canonicalServerUrl = "{$parts['scheme']}://{$parts['host']}" . + ( isset( $parts['port'] ) ? ':' . $parts['port'] : '' ); + } + + /** + * @param string $redirURL + * @return ClientConfig Self, for method chaining + */ + public function setRedirUrl( $redirURL ) { + $this->redirURL = $redirURL; + return $this; + } + + /** + * @param Consumer $consumer + * @return ClientConfig Self, for method chaining + */ + public function setConsumer( Consumer $consumer ) { + $this->consumer = $consumer; + return $this; + } +} diff --git a/src/OAuth/Consumer.php b/src/OAuth/Consumer.php new file mode 100644 index 0000000..54310f5 --- /dev/null +++ b/src/OAuth/Consumer.php @@ -0,0 +1,53 @@ +<?php +/** + * @section LICENSE + * Copyright (c) 2007 Andy Smith + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to + * whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @file + */ + +namespace Wikimedia\Slimapp\OAuth; + +/** + * Data type that represents the identity of the Consumer via its shared + * secret with the ServiceProvider. + */ +class Consumer { + /** + * @var string $key + */ + public $key; + + /** + * @var string $secret + */ + public $secret; + + function __construct( $key, $secret ) { + $this->key = $key; + $this->secret = $secret; + } + + function __toString() { + return __CLASS__ . "[key={$this->key},secret={$this->secret}]"; + } +} diff --git a/src/OAuth/Exception.php b/src/OAuth/Exception.php new file mode 100644 index 0000000..bc09241 --- /dev/null +++ b/src/OAuth/Exception.php @@ -0,0 +1,35 @@ +<?php +/** + * @section LICENSE + * Copyright (c) 2007 Andy Smith + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to + * whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @file + */ + +namespace Wikimedia\Slimapp\OAuth; + +/** + * Generic exception class + */ +class Exception extends \Exception { + // pass +} diff --git a/src/OAuth/Request.php b/src/OAuth/Request.php new file mode 100644 index 0000000..00e7a7a --- /dev/null +++ b/src/OAuth/Request.php @@ -0,0 +1,337 @@ +<?php +/** + * @section LICENSE + * Copyright (c) 2007 Andy Smith + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to + * whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @file + */ + +namespace Wikimedia\Slimapp\OAuth; + +/** + * An OAuth request + */ +class Request { + /** + * @var array $parameters + */ + protected $parameters; + + /** + * @var string $method + */ + protected $method; + + /** + * @var string $url + */ + protected $url; + + /** + * @var string $version + */ + public static $version = '1.0'; + + /** + * Used for tests. + * @var string $POST_INPUT + */ + public static $POST_INPUT = 'php://input'; + + /** + * @param string $method + * @param string $url + * @param array $parameters + */ + function __construct( $method, $url, $parameters = null ) { + $parameters = $parameters ?: array(); + $parameters = array_merge( + Util::parseParameters( parse_url( $url, PHP_URL_QUERY ) ), + $parameters + ); + $this->parameters = $parameters; + $this->method = $method; + $this->url = $url; + } + + + /** + * Attempt to build up a request from what was passed to the server + * + * @param string $method + * @param string $url + * @param array $params + * @return Request + */ + public static function fromRequest( + $method = null, + $url = null, + array $params = null + ) { + $scheme = ( !isset( $_SERVER['HTTPS'] ) || $_SERVER['HTTPS'] != 'on' ) ? + 'http' : 'https'; + $url = ( $url ?: $scheme ) . + '://' . $_SERVER['SERVER_NAME'] . + ':' . + $_SERVER['SERVER_PORT'] . + $_SERVER['REQUEST_URI']; + $method = $method ?: $_SERVER['REQUEST_METHOD']; + + // We weren't handed any params, so let's find the ones relevant + // to this request. If you run XML-RPC or similar you should use this + // to provide your own parsed parameter-list + if ( !$params ) { + // Find request headers + $headers = Util::getHeaders(); + + // Parse the query-string to find GET params + $params = Util::parseParameters( $_SERVER['QUERY_STRING'] ); + + // It's a POST request of the proper content-type, so parse POST + // params and add those overriding any duplicates from GET + if ( $method === 'POST' && + isset( $headers['Content-Type'] ) && + strstr( $headers['Content-Type'], + 'application/x-www-form-urlencoded' + ) + ) { + $post_data = Util::parseParameters( + file_get_contents( self::$POST_INPUT ) + ); + $params = array_merge( $params, $post_data ); + } + + // We have a Authorization-header with OAuth data. Parse the header + // and add those overriding any duplicates from GET or POST + if ( isset( $headers['Authorization'] ) && + substr( $headers['Authorization'], 0, 6 ) === 'OAuth ' + ) { + $header_params = Util::splitHeader( $headers['Authorization'] ); + $params = array_merge( $params, $header_params ); + } + } + + return new Request( $method, $url, $params ); + } + + /** + * @param Consumer $consumer + * @param Token|null $token + * @param string $method + * @param string $url + * @param array $parameters + * @return Request + */ + public static function fromConsumerAndToken( + Consumer $consumer, + /*Token*/ $token = null, + $method, + $url, + array $parameters = null + ) { + $parameters = $parameters ?: array(); + $defaults = array( + 'oauth_version' => static::$version, + 'oauth_nonce' => md5( microtime() . mt_rand() ), + 'oauth_timestamp' => time(), + 'oauth_consumer_key' => $consumer->key, + ); + if ( $token ) { + $defaults['oauth_token'] = $token->key; + } + $parameters = array_merge( $defaults, $parameters ); + + return new self( $method, $url, $parameters ); + } + + public function setParameter( $name, $value, $allow_duplicates = true ) { + if ( $allow_duplicates && isset( $this->parameters[$name] ) ) { + // We have already added parameter(s) with this name, so add to + // the list + if ( is_scalar( $this->parameters[$name] ) ) { + // This is the first duplicate, so transform scalar (string) + // into an array so we can add the duplicates + $this->parameters[$name] = array( $this->parameters[$name] ); + } + + $this->parameters[$name][] = $value; + } else { + $this->parameters[$name] = $value; + } + } + + /** + * @param string $name + * @return mixed + */ + public function getParameter( $name ) { + return isset( $this->parameters[$name] ) ? + $this->parameters[$name] : null; + } + + /** + * @return array + */ + public function getParameters() { + return $this->parameters; + } + + /** + * @param string $name + */ + public function unsetParameter( $name ) { + unset( $this->parameters[$name] ); + } + + /** + * The request parameters, sorted and concatenated into a normalized string. + * @return string + */ + public function getSignableParameters() { + // Grab all parameters + $params = $this->parameters; + + // Remove oauth_signature if present + // Ref: Spec: 9.1.1 ("The oauth_signature parameter MUST be excluded.") + if ( isset( $params['oauth_signature'] ) ) { + unset( $params['oauth_signature'] ); + } + + return Util::buildHttpQuery( $params ); + } + + /** + * Returns the base string of this request + * + * The base string defined as the method, the url + * and the parameters (normalized), each urlencoded + * and the concated with &. + */ + public function getSignatureBaseString() { + $parts = array( + $this->getNormalizedMethod(), + $this->getNormalizedUrl(), + $this->getSignableParameters() + ); + + $parts = Util::urlencode( $parts ); + + return implode( '&', $parts ); + } + + /** + * @return string + */ + public function getNormalizedMethod() { + return strtoupper( $this->method ); + } + + /** + * Parses the url and rebuilds it to be scheme://host/path + * @return string + */ + public function getNormalizedUrl() { + $parts = parse_url( $this->url ); + + $scheme = isset( $parts['scheme'] ) ? $parts['scheme'] : 'http'; + $port = isset( $parts['port'] ) ? + $parts['port'] : ( $scheme === 'https' ? '443' : '80' ); + $host = isset( $parts['host'] ) ? strtolower( $parts['host'] ) : ''; + $path = isset( $parts['path'] ) ? $parts['path'] : ''; + + if ( ( $scheme === 'https' && $port != '443' ) || + ( $scheme === 'http' && $port != '80' ) + ) { + $host = "{$host}:{$port}"; + } + return "{$scheme}://{$host}{$path}"; + } + + /** + * Builds a url usable for a GET request + */ + public function toUrl() { + $post_data = $this->toPostData(); + $out = $this->getNormalizedUrl(); + if ( $post_data ) { + $out .= '?' . $post_data; + } + return $out; + } + + /** + * Builds the data one would send in a POST request + */ + public function toPostData() { + return Util::buildHttpQuery( $this->parameters ); + } + + /** + * Builds the Authorization: header + */ + public function toHeader( $realm = null ) { + $first = true; + if ( $realm ) { + $out = 'Authorization: OAuth realm="' . + Util::urlencode( $realm ) . '"'; + $first = false; + } else { + $out = 'Authorization: OAuth'; + } + + foreach ( $this->parameters as $k => $v ) { + if ( substr( $k, 0, 5 ) !== 'oauth' ) { + continue; + } + if ( is_array( $v ) ) { + throw new Exception( 'Arrays not supported in headers' ); + } + $out .= ( $first ) ? ' ' : ','; + $out .= Util::urlencode( $k ) . '="' . Util::urlencode( $v ) . '"'; + $first = false; + } + return $out; + } + + public function __toString() { + return $this->toUrl(); + } + + public function signRequest( $signature_method, $consumer, $token ) { + $this->setParameter( + 'oauth_signature_method', + $signature_method->getName(), + false + ); + $signature = $this->buildSignature( + $signature_method, $consumer, $token + ); + $this->setParameter( 'oauth_signature', $signature, false ); + } + + public function buildSignature( $signature_method, $consumer, $token ) { + $signature = $signature_method->buildSignature( + $this, $consumer, $token + ); + return $signature; + } +} diff --git a/src/OAuth/SignatureMethod.php b/src/OAuth/SignatureMethod.php new file mode 100644 index 0000000..1717f91 --- /dev/null +++ b/src/OAuth/SignatureMethod.php @@ -0,0 +1,91 @@ +<?php +/** + * @section LICENSE + * Copyright (c) 2007 Andy Smith + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to + * whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @file + */ + +namespace Wikimedia\Slimapp\OAuth; + +/** + * A class for implementing a Signature Method + * See section 9 ("Signing Requests") in the spec + */ +abstract class SignatureMethod { + /** + * Needs to return the name of the Signature Method (ie HMAC-SHA1) + * @return string + */ + abstract public function getName(); + + /** + * Build up the signature + * NOTE: The output of this function MUST NOT be urlencoded. + * the encoding is handled in OAuthRequest when the final + * request is serialized + * @param Request $request + * @param Consumer $consumer + * @param Token $token + * @return string + */ + abstract public function buildSignature( + Request $request, + Consumer $consumer, + Token $token = null + ); + + /** + * Verifies that a given signature is correct + * @param Request $request + * @param Consumer $consumer + * @param Token|null $token + * @param string $signature + * @return bool + */ + public function checkSignature( + Request $request, + Consumer $consumer, + /*Token*/ $token, + $signature + ) { + $built = $this->buildSignature( $request, $consumer, $token ); + + // Check for zero length, although unlikely here + if ( strlen( $built ) === 0 || strlen( $signature ) === 0 ) { + return false; + } + + if ( strlen( $built ) !== strlen( $signature ) ) { + return false; + } + + // Avoid a timing leak with a (hopefully) time insensitive compare + $result = 0; + $len = strlen( $signature ); + for ( $i = 0; $i < $len; $i++ ) { + $result |= ord( $built[$i] ) ^ ord( $signature[$i] ); + } + + return $result == 0; + } +} diff --git a/src/OAuth/SignatureMethod/HmacSha1.php b/src/OAuth/SignatureMethod/HmacSha1.php new file mode 100644 index 0000000..43bc0e6 --- /dev/null +++ b/src/OAuth/SignatureMethod/HmacSha1.php @@ -0,0 +1,67 @@ +<?php +/** + * @section LICENSE + * Copyright (c) 2007 Andy Smith + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to + * whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @file + */ + +namespace Wikimedia\Slimapp\OAuth\SignatureMethod; + +use Wikimedia\Slimapp\OAuth\Consumer; +use Wikimedia\Slimapp\OAuth\Request; +use Wikimedia\Slimapp\OAuth\SignatureMethod; +use Wikimedia\Slimapp\OAuth\Token; +use Wikimedia\Slimapp\OAuth\Util; + +/** + * The HMAC-SHA1 signature method uses the HMAC-SHA1 signature algorithm as + * defined in [RFC2104] where the Signature Base String is the text and the + * key is the concatenated values (each first encoded per Parameter Encoding) + * of the Consumer Secret and Token Secret, separated by an '&' character + * (ASCII code 38) even if empty. + * - Chapter 9.2 ("HMAC-SHA1") + */ +class HmacSha1 extends SignatureMethod { + function getName() { + return 'HMAC-SHA1'; + } + + public function buildSignature( + Request $request, + Consumer $consumer, + Token $token = null + ) { + $base_string = $request->getSignatureBaseString(); + $request->base_string = $base_string; + + $key_parts = array( + $consumer->secret, + $token ? $token->secret : '' + ); + + $key_parts = Util::urlencode( $key_parts ); + $key = implode( '&', $key_parts ); + + return base64_encode( hash_hmac( 'sha1', $base_string, $key, true ) ); + } +} diff --git a/src/OAuth/SignatureMethod/Plaintext.php b/src/OAuth/SignatureMethod/Plaintext.php new file mode 100644 index 0000000..580aa28 --- /dev/null +++ b/src/OAuth/SignatureMethod/Plaintext.php @@ -0,0 +1,72 @@ +<?php +/** + * @section LICENSE + * Copyright (c) 2007 Andy Smith + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to + * whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @file + */ + +namespace Wikimedia\Slimapp\OAuth\SignatureMethod; + +use Wikimedia\Slimapp\OAuth\Consumer; +use Wikimedia\Slimapp\OAuth\Request; +use Wikimedia\Slimapp\OAuth\SignatureMethod; +use Wikimedia\Slimapp\OAuth\Token; +use Wikimedia\Slimapp\OAuth\Util; + +/** + * The PLAINTEXT method does not provide any security protection and SHOULD + * only be used over a secure channel such as HTTPS. It does not use the + * Signature Base String. + * - Chapter 9.4 ("PLAINTEXT") + */ +class Plaintext extends SignatureMethod { + public function getName() { + return 'PLAINTEXT'; + } + + /** + * oauth_signature is set to the concatenated encoded values of the Consumer + * Secret and Token Secret, separated by a '&' character (ASCII code 38), + * even if either secret is empty. The result MUST be encoded again. + * - Chapter 9.4.1 ("Generating Signatures") + * + * Please note that the second encoding MUST NOT happen in the + * SignatureMethod, as Request handles this! + */ + public function buildSignature( + Request $request, + Consumer $consumer, + Token $token = null + ) { + $key_parts = array( + $consumer->secret, + $token ? $token->secret : '' + ); + + $key_parts = Util::urlencode( $key_parts ); + $key = implode( '&', $key_parts ); + $request->base_string = $key; + + return $key; + } +} diff --git a/src/OAuth/SignatureMethod/RsaSha1.php b/src/OAuth/SignatureMethod/RsaSha1.php new file mode 100644 index 0000000..c0cce0f --- /dev/null +++ b/src/OAuth/SignatureMethod/RsaSha1.php @@ -0,0 +1,110 @@ +<?php +/** + * @section LICENSE + * Copyright (c) 2007 Andy Smith + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to + * whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @file + */ +namespace Wikimedia\Slimapp\OAuth\SignatureMethod; + +use Wikimedia\Slimapp\OAuth\Consumer; +use Wikimedia\Slimapp\OAuth\Request; +use Wikimedia\Slimapp\OAuth\SignatureMethod; +use Wikimedia\Slimapp\OAuth\Token; +use Wikimedia\Slimapp\OAuth\Util; + +/** + * The RSA-SHA1 signature method uses the RSASSA-PKCS1-v1_5 signature + * algorithm as defined in [RFC3447] section 8.2 (more simply known as + * PKCS#1), using SHA-1 as the hash function for EMSA-PKCS1-v1_5. It is + * assumed that the Consumer has provided its RSA public key in a verified way + * to the Service Provider, in a manner which is beyond the scope of this + * specification. + * - Chapter 9.3 ("RSA-SHA1") + */ +abstract class RsaSha1 extends SignatureMethod { + public function getName() { + return "RSA-SHA1"; + } + + // Up to the SP to implement this lookup of keys. Possible ideas are: + // (1) do a lookup in a table of trusted certs keyed off of consumer + // (2) fetch via http using a url provided by the requester + // (3) some sort of specific discovery code based on request + // + // Either way should return a string representation of the certificate + abstract protected function fetchPublicCert( Request $request ); + + // Up to the SP to implement this lookup of keys. Possible ideas are: + // (1) do a lookup in a table of trusted certs keyed off of consumer + // + // Either way should return a string representation of the certificate + abstract protected function fetchPrivateCert( Request $request ); + + public function buildSignature( + Request $request, + Consumer $consumer, + Token $token = null + ) { + $base_string = $request->getSignatureBaseString(); + $request->base_string = $base_string; + + // Fetch the private key cert based on the request + $cert = $this->fetchPrivateCert( $request ); + + // Pull the private key ID from the certificate + $privatekeyid = openssl_get_privatekey( $cert ); + + // Sign using the key + $ok = openssl_sign( $base_string, $signature, $privatekeyid ); + + // Release the key resource + openssl_free_key( $privatekeyid ); + + return base64_encode( $signature ); + } + + public function checkSignature( + Request $request, + Consumer $consumer, + /*Token*/ $token, + $signature + ) { + $decoded_sig = base64_decode( $signature ); + + $base_string = $request->getSignatureBaseString(); + + // Fetch the public key cert based on the request + $cert = $this->fetchPublicCert( $request ); + + // Pull the public key ID from the certificate + $publickeyid = openssl_get_publickey( $cert ); + + // Check the computed signature against the one passed in the query + $ok = openssl_verify( $base_string, $decoded_sig, $publickeyid ); + + // Release the key resource + openssl_free_key( $publickeyid ); + + return $ok == 1; + } +} diff --git a/src/OAuth/Token.php b/src/OAuth/Token.php new file mode 100644 index 0000000..438955d --- /dev/null +++ b/src/OAuth/Token.php @@ -0,0 +1,68 @@ +<?php +/** + * @section LICENSE + * Copyright (c) 2007 Andy Smith + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to + * whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @file + */ + +namespace Wikimedia\Slimapp\OAuth; + +/** + * Data type that represents an End User via either and access or requst + * token. + */ +class Token { + /** + * @var string $key + */ + public $key; + + /** + * @var string $secret + */ + public $secret; + + /** + * @param string key The token + * @param string secret The token secret + */ + function __construct( $key, $secret ) { + $this->key = $key; + $this->secret = $secret; + } + + /** + * Generate the basic string serialization of a token that a server + * would respond to request_token and access_token calls with + * + * @return string + */ + function toString() { + return 'oauth_token=' . Util::urlencode( $this->key ) . + '&oauth_token_secret=' . Util::urlencode( $this->secret ); + } + + function __toString() { + return $this->toString(); + } +} diff --git a/src/OAuth/Util.php b/src/OAuth/Util.php new file mode 100644 index 0000000..f8047a8 --- /dev/null +++ b/src/OAuth/Util.php @@ -0,0 +1,215 @@ +<?php +/** + * @section LICENSE + * Copyright (c) 2007 Andy Smith + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to + * whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @file + */ + +namespace Wikimedia\Slimapp\OAuth; + +class Util { + + /** + * @param string $input + * @return string + */ + public static function urlencode( $input ) { + if ( is_array( $input ) ) { + return array_map( __METHOD__, $input ); + + } elseif ( is_scalar( $input ) ) { + return rawurlencode( $input ); + + } else { + return ''; + } + } + + /** + * @param string $string + * @return string + */ + public static function urldecode( $string ) { + return urldecode( $string ); + } + + /** + * Utility function for turning the Authorization: header into parameters, + * has to do some unescaping Can filter out any non-oauth parameters if + * needed (default behaviour) + * + * @param string $header + * @param bool $oauthOnly + * @return array + */ + public static function splitHeader( $header, $oauthOnly = true ) { + $re = '/(' . ( $oauthOnly ? 'oauth_' : '' ) . + '[a-z_-]*)=(:?"([^"]*)"|([^,]*))/'; + $params = array(); + if ( preg_match_all( $re, $header, $m ) ) { + foreach ( $m[1] as $i => $h ) { + $params[$h] = static::urldecode( + empty( $m[3][$i] ) ? $m[4][$i] : $m[3][$i] + ); + } + if ( isset( $params['realm'] ) ) { + unset( $params['realm'] ); + } + } + return $params; + } + + /** + * @return array + */ + public static function getHeaders() { + if ( function_exists( 'apache_request_headers' ) ) { + // we need this to get the actual Authorization: header + // because apache tends to tell us it doesn't exist + $headers = apache_request_headers(); + + // sanitize the output of apache_request_headers because + // we always want the keys to be Cased-Like-This and arh() + // returns the headers in the same case as they are in the + // request + $out = array(); + foreach ( $headers as $key => $value ) { + $key = str_replace( + ' ', '-', + ucwords( strtolower( str_replace( '-', ' ', $key ) ) ) + ); + $out[$key] = $value; + } + } else { + // otherwise we don't have apache and are just going to have to + // hope that $_SERVER actually contains what we need + $out = array(); + if ( isset( $_SERVER['CONTENT_TYPE'] ) ) { + $out['Content-Type'] = $_SERVER['CONTENT_TYPE']; + } + if ( isset( $_ENV['CONTENT_TYPE'] ) ) { + $out['Content-Type'] = $_ENV['CONTENT_TYPE']; + } + + foreach ( $_SERVER as $key => $value ) { + if ( substr( $key, 0, 5 ) == 'HTTP_' ) { + // this is chaos, basically it is just there to capitalize + // the first letter of every word that is not an initial + // HTTP and strip HTTP_ prefix + // Code from przemek + $key = str_replace( + ' ', '-', + ucwords( strtolower( + str_replace( '_', ' ', substr( $key, 5 ) ) + ) ) + ); + $out[$key] = $value; + } + } + } + return $out; + } + + /** + * This function takes a input like a=b&a=c&d=e and returns the parsed + * parameters like this array('a' => array('b','c'), 'd' => 'e') + * + * @param string $input + * @return array + */ + public static function parseParameters( $input ) { + if ( !isset( $input ) || !$input ) { + return array(); + } + + $pairs = explode( '&', $input ); + + $parsed = array(); + foreach ( $pairs as $pair ) { + $split = explode( '=', $pair, 2 ); + $parameter = static::urldecode( $split[0] ); + $value = isset( $split[1] ) ? static::urldecode( $split[1] ) : ''; + + if ( isset( $parsed[$parameter] ) ) { + // We have already recieved parameter(s) with this name, so + // add to the list of parameters with this name + + if ( is_scalar( $parsed[$parameter] ) ) { + // This is the first duplicate, so transform scalar + // (string) into an array so we can add the duplicates + $parsed[$parameter] = array( $parsed[$parameter] ); + } + + $parsed[$parameter][] = $value; + } else { + $parsed[$parameter] = $value; + } + } + return $parsed; + } + + /** + * @param array $params + * @return string + */ + public static function buildHttpQuery( array $params ) { + if ( !$params ) { + return ''; + } + + // Urlencode both keys and values + $keys = static::urlencode( array_keys( $params ) ); + $values = static::urlencode( array_values( $params ) ); + $params = array_combine( $keys, $values ); + + // Parameters are sorted by name, using lexicographical byte value + // ordering. Ref: Spec: 9.1.1 (1) + uksort( $params, 'strcmp' ); + + $pairs = array(); + foreach ( $params as $parameter => $value ) { + if ( is_array( $value ) ) { + // If two or more parameters share the same name, they are + // sorted by their value + // Ref: Spec: 9.1.1 (1) + sort( $value, SORT_STRING ); + foreach ( $value as $duplicate_value ) { + $pairs[] = "{$parameter}={$duplicate_value}"; + } + } else { + $pairs[] = "{$parameter}={$value}"; + } + } + // For each parameter, the name is separated from the corresponding + // value by an '=' character (ASCII code 61) + // Each name-value pair is separated by an '&' character (ASCII code 38) + return implode( '&', $pairs ); + } + + /** + * Disallow construction of utility class. + */ + private function __construct() { + // no-op + } +} diff --git a/tests/OAuth/ConsumerTest.php b/tests/OAuth/ConsumerTest.php new file mode 100644 index 0000000..8e6c0f8 --- /dev/null +++ b/tests/OAuth/ConsumerTest.php @@ -0,0 +1,41 @@ +<?php +/** + * @section LICENSE + * Copyright (c) 2007 Andy Smith + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to + * whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @file + */ + +namespace Wikimedia\Slimapp\OAuth; + +/** + * @coversDefaultClass \Wikimedia\Slimapp\OAuth\Consumer + */ +class ConsumerTest extends \PHPUnit_Framework_TestCase { + public function testConvertToString() { + $consumer = new Consumer( 'key', 'secret' ); + $this->assertEquals( + 'Wikimedia\Slimapp\OAuth\Consumer[key=key,secret=secret]', + (string) $consumer + ); + } +} diff --git a/tests/OAuth/RequestTest.php b/tests/OAuth/RequestTest.php new file mode 100644 index 0000000..144ccd6 --- /dev/null +++ b/tests/OAuth/RequestTest.php @@ -0,0 +1,524 @@ +<?php +/** + * @section LICENSE + * Copyright (c) 2007 Andy Smith + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to + * whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @file + */ + +namespace Wikimedia\Slimapp\OAuth; + +use Wikimedia\Slimapp\OAuth\SignatureMethod\HmacSha1; +use Wikimedia\Slimapp\OAuth\SignatureMethod\Plaintext; + +/** + * @coversDefaultClass \Wikimedia\Slimapp\OAuth\Request + */ +class RequestTest extends \PHPUnit_Framework_TestCase { + + public function testCanGetSingleParameter() { + $request = new Request( '', '', array( 'test'=>'foo' ) ); + $this->assertEquals( 'foo', $request->getParameter( 'test' ), + 'Failed to read back parameter' + ); + + $request = new Request( + '', '', array( 'test' => array( 'foo', 'bar' ) ) + ); + $this->assertEquals( array( 'foo', 'bar' ), + $request->getParameter( 'test' ), 'Failed to read back parameter' ); + + $request = new Request( + '', '', array( 'test' => 'foo', 'bar' => 'baz' ) + ); + $this->assertEquals( 'foo', $request->getParameter( 'test' ), + 'Failed to read back parameter' + ); + $this->assertEquals( 'baz', $request->getParameter( 'bar' ), + 'Failed to read back parameter' + ); + } + + public function testGetAllParameters() { + // Yes, a awesomely boring test.. But if this doesn't work, the other + // tests is unreliable + $request = new Request( '', '', array( 'test' => 'foo' ) ); + $this->assertEquals( array( 'test'=>'foo' ), $request->getParameters(), + 'Failed to read back parameters' + ); + + $request = new Request( + '', '', array( 'test' => 'foo', 'bar' => 'baz' ) + ); + $this->assertEquals( array( 'test' => 'foo', 'bar' => 'baz' ), + $request->getParameters(), 'Failed to read back parameters' ); + + $request = new Request( + '', '', array( 'test' => array( 'foo', 'bar' ) ) + ); + $this->assertEquals( array( 'test' => array( 'foo', 'bar' ) ), + $request->getParameters(), 'Failed to read back parameters' ); + } + + public function testSetParameters() { + $request = new Request( '', '' ); + $this->assertEquals( null, $request->getParameter( 'test' ), + 'Failed to assert that non-existing parameter is null' ); + + $request->setParameter( 'test', 'foo' ); + $this->assertEquals( 'foo', $request->getParameter( 'test' ), + 'Failed to set single-entry parameter' ); + + $request->setParameter( 'test', 'bar' ); + $this->assertEquals( array( 'foo', 'bar' ), + $request->getParameter( 'test' ), + 'Failed to set single-entry parameter' + ); + + $request->setParameter( 'test', 'bar', false ); + $this->assertEquals( 'bar', $request->getParameter( 'test' ), + 'Failed to set single-entry parameter' + ); + } + + public function testUnsetParameter() { + $request = new Request( '', '' ); + $this->assertEquals( null, $request->getParameter( 'test' ) ); + + $request->setParameter( 'test', 'foo' ); + $this->assertEquals( 'foo', $request->getParameter( 'test' ) ); + + $request->unsetParameter( 'test' ); + $this->assertEquals( null, $request->getParameter( 'test' ), + 'Failed to unset parameter' + ); + } + + public function testCreateRequestFromConsumerAndToken() { + $cons = new Consumer( 'key', 'kd94hf93k423kf44' ); + $token = new Token( 'token', 'pfkkdhi9sl3r4s00' ); + + $request = Request::fromConsumerAndToken( + $cons, $token, 'POST', 'http://example.com' + ); + $this->assertEquals( 'POST', $request->getNormalizedMethod() ); + $this->assertEquals( 'http://example.com', $request->getNormalizedUrl() ); + $this->assertEquals( '1.0', $request->getParameter( 'oauth_version' ) ); + $this->assertEquals( $cons->key, $request->getParameter( 'oauth_consumer_key' ) ); + $this->assertEquals( $token->key, $request->getParameter( 'oauth_token' ) ); + $this->assertEquals( time(), $request->getParameter( 'oauth_timestamp' ) ); + $this->assertRegExp( '/[0-9a-f]{32}/', $request->getParameter( 'oauth_nonce' ) ); + // We don't know what the nonce will be, except it'll be md5 and hence + // 32 hexa digits + + $request = Request::fromConsumerAndToken( $cons, $token, 'POST', + 'http://example.com', array( 'oauth_nonce'=>'foo' ) ); + $this->assertEquals( 'foo', $request->getParameter( 'oauth_nonce' ) ); + + $request = Request::fromConsumerAndToken( $cons, null, 'POST', + 'http://example.com', array( 'oauth_nonce'=>'foo' ) ); + $this->assertNull( $request->getParameter( 'oauth_token' ) ); + + // Test that parameters given in the $http_url instead of in the + // $parameters-parameter will still be picked up + $request = Request::fromConsumerAndToken( $cons, $token, 'POST', + 'http://example.com/?foo=bar' ); + $this->assertEquals( 'http://example.com/', $request->getNormalizedUrl() ); + $this->assertEquals( 'bar', $request->getParameter( 'foo' ) ); + } + + public function testBuildRequestFromPost() { + static::buildRequest( + 'POST', 'http://testbed/test', 'foo=bar&baz=blargh' ); + $this->assertEquals( array( 'foo'=>'bar','baz'=>'blargh' ), + Request::fromRequest()->getParameters(), + 'Failed to parse POST parameters' ); + } + + public function testBuildRequestFromGet() { + static::buildRequest( 'GET', 'http://testbed/test?foo=bar&baz=blargh' ); + $this->assertEquals( array( 'foo'=>'bar','baz'=>'blargh' ), + Request::fromRequest()->getParameters(), + 'Failed to parse GET parameters' ); + } + + public function testBuildRequestFromHeader() { + $test_header = 'OAuth realm="",oauth_foo=bar,oauth_baz="bla,rgh"'; + static::buildRequest( 'POST', 'http://testbed/test', '', $test_header ); + $this->assertEquals( + array( 'oauth_foo'=>'bar','oauth_baz'=>'bla,rgh' ), + Request::fromRequest()->getParameters(), + 'Failed to split auth-header correctly' ); + } + + public function testHasProperParameterPriority() { + $test_header = 'OAuth realm="",oauth_foo=header'; + static::buildRequest( 'POST', 'http://testbed/test?oauth_foo=get', + 'oauth_foo=post', $test_header ); + $this->assertEquals( 'header', + Request::fromRequest()->getParameter( 'oauth_foo' ), + 'Loaded parameters in with the wrong priorities' ); + + static::buildRequest( 'POST', 'http://testbed/test?oauth_foo=get', + 'oauth_foo=post' ); + $this->assertEquals( 'post', + Request::fromRequest()->getParameter( 'oauth_foo' ), + 'Loaded parameters in with the wrong priorities' ); + + static::buildRequest( 'POST', 'http://testbed/test?oauth_foo=get' ); + $this->assertEquals( 'get', + Request::fromRequest()->getParameter( 'oauth_foo' ), + 'Loaded parameters in with the wrong priorities' ); + } + + public function testNormalizeHttpMethod() { + static::buildRequest( 'POST', 'http://testbed/test' ); + $this->assertEquals( 'POST', + Request::fromRequest()->getNormalizedMethod(), + 'Failed to normalize HTTP method: POST' ); + + static::buildRequest( 'post', 'http://testbed/test' ); + $this->assertEquals( 'POST', + Request::fromRequest()->getNormalizedMethod(), + 'Failed to normalize HTTP method: post' ); + + static::buildRequest( 'GET', 'http://testbed/test' ); + $this->assertEquals( 'GET', + Request::fromRequest()->getNormalizedMethod(), + 'Failed to normalize HTTP method: GET' ); + + static::buildRequest( 'PUT', 'http://testbed/test' ); + $this->assertEquals( 'PUT', + Request::fromRequest()->getNormalizedMethod(), + 'Failed to normalize HTTP method: PUT' ); + } + + public function testNormalizeParameters() { + // This is mostly repeats of OAuthUtilTest::testParseParameters + // & OAuthUtilTest::TestBuildHttpQuery + + // Tests taken from + // http://wiki.oauth.net/TestCases ( "Normalize Request Parameters" ) + static::buildRequest( 'POST', 'http://testbed/test', 'name' ); + $this->assertEquals( 'name=', + Request::fromRequest()->getSignableParameters() ); + + static::buildRequest( 'POST', 'http://testbed/test', 'a=b' ); + $this->assertEquals( 'a=b', + Request::fromRequest()->getSignableParameters() ); + + static::buildRequest( 'POST', 'http://testbed/test', 'a=b&c=d' ); + $this->assertEquals( 'a=b&c=d', + Request::fromRequest()->getSignableParameters() ); + + static::buildRequest( 'POST', 'http://testbed/test', 'a=x%21y&a=x+y' ); + $this->assertEquals( 'a=x%20y&a=x%21y', + Request::fromRequest()->getSignableParameters() ); + + static::buildRequest( 'POST', 'http://testbed/test', 'x%21y=a&x=a' ); + $this->assertEquals( 'x=a&x%21y=a', + Request::fromRequest()->getSignableParameters() ); + + static::buildRequest( 'POST', + 'http://testbed/test', 'a=1&c=hi there&f=25&f=50&f=a&z=p&z=t' ); + $this->assertEquals( 'a=1&c=hi%20there&f=25&f=50&f=a&z=p&z=t', + Request::fromRequest()->getSignableParameters() ); + } + + public function testNormalizeHttpUrl() { + static::buildRequest( 'POST', 'http://example.com' ); + $this->assertEquals( 'http://example.com', + Request::fromRequest()->getNormalizedUrl() ); + + static::buildRequest( 'POST', 'https://example.com' ); + $this->assertEquals( 'https://example.com', + Request::fromRequest()->getNormalizedUrl() ); + + // Tests that http on !80 and https on !443 keeps the port + static::buildRequest( 'POST', 'http://example.com:8080' ); + $this->assertEquals( 'http://example.com:8080', + Request::fromRequest()->getNormalizedUrl() ); + + static::buildRequest( 'POST', 'https://example.com:80' ); + $this->assertEquals( 'https://example.com:80', + Request::fromRequest()->getNormalizedUrl() ); + + static::buildRequest( 'POST', 'http://example.com:443' ); + $this->assertEquals( 'http://example.com:443', + Request::fromRequest()->getNormalizedUrl() ); + + static::buildRequest( 'POST', 'http://Example.COM' ); + $this->assertEquals( 'http://example.com', + Request::fromRequest()->getNormalizedUrl() ); + + // Emulate silly behavior by some clients, where there Host header + // includes the port + static::buildRequest( 'POST', 'http://example.com' ); + $_SERVER['HTTP_HOST'] = $_SERVER['HTTP_HOST'] . ':' . + $_SERVER['SERVER_PORT']; + $this->assertEquals( 'http://example.com', + Request::fromRequest()->getNormalizedUrl() ); + } + + public function testBuildPostData() { + static::buildRequest( 'POST', 'http://example.com' ); + $this->assertEquals( '', Request::fromRequest()->toPostData() ); + + static::buildRequest( 'POST', 'http://example.com', 'foo=bar' ); + $this->assertEquals( 'foo=bar', Request::fromRequest()->toPostData() ); + + static::buildRequest( 'GET', 'http://example.com?foo=bar' ); + $this->assertEquals( 'foo=bar', Request::fromRequest()->toPostData() ); + } + + public function testBuildUrl() { + static::buildRequest( 'POST', 'http://example.com' ); + $this->assertEquals( 'http://example.com', + Request::fromRequest()->toUrl() ); + + static::buildRequest( 'POST', 'http://example.com', 'foo=bar' ); + $this->assertEquals( 'http://example.com?foo=bar', + Request::fromRequest()->toUrl() ); + + static::buildRequest( 'GET', 'http://example.com?foo=bar' ); + $this->assertEquals( 'http://example.com?foo=bar', + Request::fromRequest()->toUrl() ); + } + + public function testConvertToString() { + static::buildRequest( 'POST', 'http://example.com' ); + $this->assertEquals( 'http://example.com', + (string)Request::fromRequest() ); + + static::buildRequest( 'POST', 'http://example.com', 'foo=bar' ); + $this->assertEquals( 'http://example.com?foo=bar', + (string)Request::fromRequest() ); + + static::buildRequest( 'GET', 'http://example.com?foo=bar' ); + $this->assertEquals( 'http://example.com?foo=bar', + (string)Request::fromRequest() ); + } + + public function testBuildHeader() { + static::buildRequest( 'POST', 'http://example.com' ); + $this->assertEquals( 'Authorization: OAuth', + Request::fromRequest()->toHeader() ); + $this->assertEquals( 'Authorization: OAuth realm="test"', + Request::fromRequest()->toHeader( 'test' ) ); + + static::buildRequest( 'POST', 'http://example.com', 'foo=bar' ); + $this->assertEquals( 'Authorization: OAuth', + Request::fromRequest()->toHeader() ); + $this->assertEquals( 'Authorization: OAuth realm="test"', + Request::fromRequest()->toHeader( 'test' ) ); + + static::buildRequest( 'POST', 'http://example.com', 'oauth_test=foo' ); + $this->assertEquals( 'Authorization: OAuth oauth_test="foo"', + Request::fromRequest()->toHeader() ); + $this->assertEquals( + 'Authorization: OAuth realm="test",oauth_test="foo"', + Request::fromRequest()->toHeader( 'test' ) ); + + // Is headers supposted to be Urlencoded. More to the point: + // Should it be baz = bla,rgh or baz = bla%2Crgh ?? + // - morten.fangel + static::buildRequest( 'POST', 'http://example.com', + '', 'OAuth realm="",oauth_foo=bar,oauth_baz="bla,rgh"' ); + $this->assertEquals( + 'Authorization: OAuth oauth_foo="bar",oauth_baz="bla%2Crgh"', + Request::fromRequest()->toHeader() ); + $this->assertEquals( + 'Authorization: OAuth realm="test",oauth_foo="bar",oauth_baz="bla%2Crgh"', + Request::fromRequest()->toHeader( 'test' ) ); + } + + public function testWontBuildHeaderWithArrayInput() { + $this->setExpectedException( 'Wikimedia\Slimapp\OAuth\Exception' ); + static::buildRequest( 'POST', 'http://example.com', + 'oauth_foo=bar&oauth_foo=baz' ); + Request::fromRequest()->toHeader(); + } + + public function testBuildBaseString() { + static::buildRequest( 'POST', 'http://testbed/test', 'n=v' ); + $this->assertEquals( + 'POST&http%3A%2F%2Ftestbed%2Ftest&n%3Dv', + Request::fromRequest()->getSignatureBaseString() + ); + + static::buildRequest( 'POST', 'http://testbed/test', 'n=v&n=v2' ); + $this->assertEquals( 'POST&http%3A%2F%2Ftestbed%2Ftest&n%3Dv%26n%3Dv2', + Request::fromRequest()->getSignatureBaseString() ); + + static::buildRequest( 'GET', 'http://example.com?n=v' ); + $this->assertEquals( 'GET&http%3A%2F%2Fexample.com&n%3Dv', + Request::fromRequest()->getSignatureBaseString() ); + + $params = 'oauth_version=1.0&oauth_consumer_key=dpf43f3p2l4k3l03' + . '&oauth_timestamp=1191242090' + . '&oauth_nonce=hsu94j3884jdopsl' + . '&oauth_signature_method=PLAINTEXT&oauth_signature=ignored'; + static::buildRequest( 'POST', 'https://photos.example.net/request_token', $params ); + $this->assertEquals( + 'POST&https%3A%2F%2Fphotos.example.net%2Frequest_token&oauth_' + . 'consumer_key%3Ddpf43f3p2l4k3l03%26oauth_nonce%3Dhsu94j3884j' + . 'dopsl%26oauth_signature_method%3DPLAINTEXT%26oauth_timestam' + . 'p%3D1191242090%26oauth_version%3D1.0', + Request::fromRequest()->getSignatureBaseString() ); + + $params = 'file=vacation.jpg&size=original&oauth_version=1.0'; + $params .= '&oauth_consumer_key=dpf43f3p2l4k3l03'; + $params .= '&oauth_token=nnch734d00sl2jdk&oauth_timestamp=1191242096'; + $params .= '&oauth_nonce=kllo9940pd9333jh'; + $params .= '&oauth_signature=ignored&oauth_signature_method=HMAC-SHA1'; + static::buildRequest( 'GET', 'http://photos.example.net/photos?'.$params ); + $this->assertEquals( + 'GET&http%3A%2F%2Fphotos.example.net%2Fphotos&file%3Dvacation' + . '.jpg%26oauth_consumer_key%3Ddpf43f3p2l4k3l03%26oauth_nonce%' + . '3Dkllo9940pd9333jh%26oauth_signature_method%3DHMAC-SHA1%26o' + . 'auth_timestamp%3D1191242096%26oauth_token%3Dnnch734d00sl2jd' + . 'k%26oauth_version%3D1.0%26size%3Doriginal', + Request::fromRequest()->getSignatureBaseString() ); + } + + public function testBuildSignature() { + $params = 'file=vacation.jpg&size=original&oauth_version=1.0' + . '&oauth_consumer_key=dpf43f3p2l4k3l03' + . '&oauth_token=nnch734d00sl2jdk' + . '&oauth_timestamp=1191242096&oauth_nonce=kllo9940pd9333jh' + . '&oauth_signature=ignored&oauth_signature_method=HMAC-SHA1'; + static::buildRequest( 'GET', 'http://photos.example.net/photos?'.$params ); + $r = Request::fromRequest(); + + $cons = new Consumer( 'key', 'kd94hf93k423kf44' ); + $token = new Token( 'token', 'pfkkdhi9sl3r4s00' ); + + $hmac = new HmacSha1(); + $plaintext = new Plaintext(); + + $this->assertEquals( 'tR3+Ty81lMeYAr/Fid0kMTYa/WM=', + $r->buildSignature( $hmac, $cons, $token ) ); + + $this->assertEquals( 'kd94hf93k423kf44&pfkkdhi9sl3r4s00', + $r->buildSignature( $plaintext, $cons, $token ) ); + } + + public function testSign() { + $params = 'file=vacation.jpg&size=original&oauth_version=1.0' + . '&oauth_consumer_key=dpf43f3p2l4k3l03' + . '&oauth_token=nnch734d00sl2jdk' + . '&oauth_timestamp=1191242096&oauth_nonce=kllo9940pd9333jh' + . '&oauth_signature=__ignored__&oauth_signature_method=HMAC-SHA1'; + static::buildRequest( 'GET', + 'http://photos.example.net/photos?'.$params ); + $r = Request::fromRequest(); + + $cons = new Consumer( 'key', 'kd94hf93k423kf44' ); + $token = new Token( 'token', 'pfkkdhi9sl3r4s00' ); + + $hmac = new HmacSha1(); + $plaintext = new Plaintext(); + + // We need to test both what the parameter is, and how the serialized + // request is.. + + $r->signRequest( $hmac, $cons, $token ); + $this->assertEquals( 'HMAC-SHA1', + $r->getParameter( 'oauth_signature_method' ) ); + $this->assertEquals( 'tR3+Ty81lMeYAr/Fid0kMTYa/WM=', + $r->getParameter( 'oauth_signature' ) ); + $expectedPostdata = 'file=vacation.jpg' + . '&oauth_consumer_key=dpf43f3p2l4k3l03' + . '&oauth_nonce=kllo9940pd9333jh' + . '&oauth_signature=tR3%2BTy81lMeYAr%2FFid0kMTYa%2FWM%3D' + . '&oauth_signature_method=HMAC-SHA1' + . '&oauth_timestamp=1191242096&oauth_token=nnch734d00sl2jdk' + . '&oauth_version=1.0&size=original'; + $this->assertEquals( $expectedPostdata, $r->toPostData() ); + + $r->signRequest( $plaintext, $cons, $token ); + $this->assertEquals( 'PLAINTEXT', + $r->getParameter( 'oauth_signature_method' ) ); + $this->assertEquals( 'kd94hf93k423kf44&pfkkdhi9sl3r4s00', + $r->getParameter( 'oauth_signature' ) ); + $expectedPostdata = 'file=vacation.jpg' + . '&oauth_consumer_key=dpf43f3p2l4k3l03' + . '&oauth_nonce=kllo9940pd9333jh&' + . 'oauth_signature=kd94hf93k423kf44%26pfkkdhi9sl3r4s00' + . '&oauth_signature_method=PLAINTEXT' + . '&oauth_timestamp=1191242096&oauth_token=nnch734d00sl2jdk' + . '&oauth_version=1.0&size=original'; + $this->assertEquals( $expectedPostdata, $r->toPostData() ); + + } + + /** + * Populates $_{SERVER,GET,POST} and whatever environment-variables needed + * to test everything. + * + * @param string $method GET or POST + * @param string $uri What URI is the request to ( eg http://example.com/foo?bar=baz ) + * @param string $post_data What should the post-data be + * @param string $auth_header What to set the Authorization header to + */ + public static function buildRequest( + $method, $uri, $post_data = '', $auth_header = '' + ) { + $_SERVER = array(); + $_POST = array(); + $_GET = array(); + + $method = strtoupper( $method ); + + $parts = parse_url( $uri ); + + $scheme = $parts['scheme']; + $port = isset( $parts['port'] ) && $parts['port'] ? + $parts['port'] : ( $scheme === 'https' ? '443' : '80' ); + $host = $parts['host']; + $path = isset( $parts['path'] ) ? $parts['path'] : null; + $query = isset( $parts['query'] ) ? $parts['query'] : null; + + if ( $scheme == 'https' ) { + $_SERVER['HTTPS'] = 'on'; + } + + $_SERVER['REQUEST_METHOD'] = $method; + $_SERVER['HTTP_HOST'] = $host; + $_SERVER['SERVER_NAME'] = $host; + $_SERVER['SERVER_PORT'] = $port; + $_SERVER['SCRIPT_NAME'] = $path; + $_SERVER['REQUEST_URI'] = $path . '?' . $query; + $_SERVER['QUERY_STRING'] = $query . ''; + parse_str( $query, $_GET ); + + if ( $method == 'POST' ) { + $_SERVER['HTTP_CONTENT_TYPE'] = 'application/x-www-form-urlencoded'; + $_POST = parse_str( $post_data ); + Request::$POST_INPUT = 'data:application/x-www-form-urlencoded,'.$post_data; + } + + if ( $auth_header != '' ) { + $_SERVER['HTTP_AUTHORIZATION'] = $auth_header; + } + } +} diff --git a/tests/OAuth/SignatureMethod/HmacSha1Test.php b/tests/OAuth/SignatureMethod/HmacSha1Test.php new file mode 100644 index 0000000..a9e9f22 --- /dev/null +++ b/tests/OAuth/SignatureMethod/HmacSha1Test.php @@ -0,0 +1,116 @@ +<?php +/** + * @section LICENSE + * Copyright (c) 2007 Andy Smith + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to + * whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @file + */ + +namespace Wikimedia\Slimapp\OAuth\SignatureMethod; + +use Wikimedia\Slimapp\OAuth\Consumer; +use Wikimedia\Slimapp\OAuth\Request; +use Wikimedia\Slimapp\OAuth\Token; + +/** + * @coversDefaultClass \Wikimedia\Slimapp\OAuth\SignatureMethod\HmacSha1 + */ +class HmacSha1Test extends \PHPUnit_Framework_TestCase { + private $method; + + public function setUp() { + $this->method = new HmacSha1(); + } + + public function testIdentifyAsHmacSha1() { + $this->assertEquals( 'HMAC-SHA1', $this->method->getName() ); + } + + public function testBuildSignature() { + // Tests taken from http://wiki.oauth.net/TestCases section 9.2 ( "HMAC-SHA1" ) + $request = $this->mockRequest( 'bs' ); + $consumer = new Consumer( '__unused__', 'cs' ); + $token = null; + $this->assertEquals( 'egQqG5AJep5sJ7anhXju1unge2I=', + $this->method->buildSignature( $request, $consumer, $token ) ); + + $request = $this->mockRequest( 'bs' ); + $consumer = new Consumer( '__unused__', 'cs' ); + $token = new Token( '__unused__', 'ts' ); + $this->assertEquals( 'VZVjXceV7JgPq/dOTnNmEfO0Fv8=', + $this->method->buildSignature( $request, $consumer, $token ) ); + + $request = $this->mockRequest( + 'GET&http%3A%2F%2Fphotos.example.net%2Fphotos&file%3Dvacation.jpg%26' + . 'oauth_consumer_key%3Ddpf43f3p2l4k3l03%26' + . 'oauth_nonce%3Dkllo9940pd9333jh%26' + . 'oauth_signature_method%3DHMAC-SHA1%26' + . 'oauth_timestamp%3D1191242096%26' + . 'oauth_token%3Dnnch734d00sl2jdk%26' + . 'oauth_version%3D1.0%26size%3Doriginal' ); + $consumer = new Consumer( '__unused__', 'kd94hf93k423kf44' ); + $token = new Token( '__unused__', 'pfkkdhi9sl3r4s00' ); + $this->assertEquals( 'tR3+Ty81lMeYAr/Fid0kMTYa/WM=', + $this->method->buildSignature( $request, $consumer, $token ) ); + } + + public function testVerifySignature() { + // Tests taken from http://wiki.oauth.net/TestCases section 9.2 + // ( "HMAC-SHA1" ) + $request = $this->mockRequest( 'bs' ); + $consumer = new Consumer( '__unused__', 'cs' ); + $token = null; + $signature = 'egQqG5AJep5sJ7anhXju1unge2I='; + $this->assertTrue( $this->method->checkSignature( + $request, $consumer, $token, $signature ) ); + + $request = $this->mockRequest( 'bs' ); + $consumer = new Consumer( '__unused__', 'cs' ); + $token = new Token( '__unused__', 'ts' ); + $signature = 'VZVjXceV7JgPq/dOTnNmEfO0Fv8='; + $this->assertTrue( $this->method->checkSignature( + $request, $consumer, $token, $signature ) ); + + $request = $this->mockRequest( + 'GET&http%3A%2F%2Fphotos.example.net%2Fphotos&file%3Dvacation.jpg%26' + . 'oauth_consumer_key%3Ddpf43f3p2l4k3l03%26' + . 'oauth_nonce%3Dkllo9940pd9333jh%26' + . 'oauth_signature_method%3DHMAC-SHA1%26' + . 'oauth_timestamp%3D1191242096%26' + . 'oauth_token%3Dnnch734d00sl2jdk%26' + . 'oauth_version%3D1.0%26size%3Doriginal' ); + $consumer = new Consumer( '__unused__', 'kd94hf93k423kf44' ); + $token = new Token( '__unused__', 'pfkkdhi9sl3r4s00' ); + $signature = 'tR3+Ty81lMeYAr/Fid0kMTYa/WM='; + $this->assertTrue( $this->method->checkSignature( + $request, $consumer, $token, $signature ) ); + } + + protected function mockRequest( $baseStr ) { + $stub = $this->getMockBuilder( 'Wikimedia\Slimapp\OAuth\request' ) + ->disableOriginalConstructor() + ->getMock(); + $stub->method( 'getSignatureBaseString' ) + ->willReturn( $baseStr ); + return $stub; + } +} diff --git a/tests/OAuth/SignatureMethod/PlaintextTest.php b/tests/OAuth/SignatureMethod/PlaintextTest.php new file mode 100644 index 0000000..137b6a7 --- /dev/null +++ b/tests/OAuth/SignatureMethod/PlaintextTest.php @@ -0,0 +1,128 @@ +<?php +/** + * @section LICENSE + * Copyright (c) 2007 Andy Smith + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to + * whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @file + */ + +namespace Wikimedia\Slimapp\OAuth\SignatureMethod; + +use Wikimedia\Slimapp\OAuth\Consumer; +use Wikimedia\Slimapp\OAuth\Request; +use Wikimedia\Slimapp\OAuth\Token; + +/** + * @coversDefaultClass \Wikimedia\Slimapp\OAuth\SignatureMethod\Plaintext + */ +class PlaintextTest extends \PHPUnit_Framework_TestCase { + private $method; + + public function setUp() { + $this->method = new Plaintext(); + } + + public function testIdentifyAsPlaintext() { + $this->assertEquals( 'PLAINTEXT', $this->method->getName() ); + } + + public function testBuildSignature() { + // Tests based on from http://wiki.oauth.net/TestCases section 9.2 ( "HMAC-SHA1" ) + $request = $this->mockRequest( '__unused__' ); + $consumer = new Consumer( '__unused__', 'cs' ); + $token = null; + $this->assertEquals( 'cs&', $this->method->buildSignature( $request, $consumer, $token ) ); + + $request = $this->mockRequest( '__unused__' ); + $consumer = new Consumer( '__unused__', 'cs' ); + $token = new Token( '__unused__', 'ts' ); + $this->assertEquals( 'cs&ts', $this->method->buildSignature( $request, $consumer, $token ) ); + + $request = $this->mockRequest( '__unused__' ); + $consumer = new Consumer( '__unused__', 'kd94hf93k423kf44' ); + $token = new Token( '__unused__', 'pfkkdhi9sl3r4s00' ); + $this->assertEquals( 'kd94hf93k423kf44&pfkkdhi9sl3r4s00', + $this->method->buildSignature( $request, $consumer, $token ) ); + + // Tests taken from Chapter 9.4.1 ( "Generating Signature" ) from the spec + $request = $this->mockRequest( '__unused__' ); + $consumer = new Consumer( '__unused__', 'djr9rjt0jd78jf88' ); + $token = new Token( '__unused__', 'jjd999tj88uiths3' ); + $this->assertEquals( 'djr9rjt0jd78jf88&jjd999tj88uiths3', + $this->method->buildSignature( $request, $consumer, $token ) ); + + $request = $this->mockRequest( '__unused__' ); + $consumer = new Consumer( '__unused__', 'djr9rjt0jd78jf88' ); + $token = new Token( '__unused__', 'jjd99$tj88uiths3' ); + $this->assertEquals( 'djr9rjt0jd78jf88&jjd99%24tj88uiths3', + $this->method->buildSignature( $request, $consumer, $token ) ); + } + + public function testVerifySignature() { + // Tests based on from http://wiki.oauth.net/TestCases section 9.2 ( "HMAC-SHA1" ) + $request = $this->mockRequest( '__unused__' ); + $consumer = new Consumer( '__unused__', 'cs' ); + $token = null; + $signature = 'cs&'; + $this->assertTrue( $this->method->checkSignature( + $request, $consumer, $token, $signature ) ); + + $request = $this->mockRequest( '__unused__' ); + $consumer = new Consumer( '__unused__', 'cs' ); + $token = new Token( '__unused__', 'ts' ); + $signature = 'cs&ts'; + $this->assertTrue( $this->method->checkSignature( + $request, $consumer, $token, $signature ) ); + + $request = $this->mockRequest( '__unused__' ); + $consumer = new Consumer( '__unused__', 'kd94hf93k423kf44' ); + $token = new Token( '__unused__', 'pfkkdhi9sl3r4s00' ); + $signature = 'kd94hf93k423kf44&pfkkdhi9sl3r4s00'; + $this->assertTrue( $this->method->checkSignature( + $request, $consumer, $token, $signature ) ); + + // Tests taken from Chapter 9.4.1 ( "Generating Signature" ) from the + // spec + $request = $this->mockRequest( '__unused__' ); + $consumer = new Consumer( '__unused__', 'djr9rjt0jd78jf88' ); + $token = new Token( '__unused__', 'jjd999tj88uiths3' ); + $signature = 'djr9rjt0jd78jf88&jjd999tj88uiths3'; + $this->assertTrue( $this->method->checkSignature( + $request, $consumer, $token, $signature ) ); + + $request = $this->mockRequest( '__unused__' ); + $consumer = new Consumer( '__unused__', 'djr9rjt0jd78jf88' ); + $token = new Token( '__unused__', 'jjd99$tj88uiths3' ); + $signature = 'djr9rjt0jd78jf88&jjd99%24tj88uiths3'; + $this->assertTrue( $this->method->checkSignature( + $request, $consumer, $token, $signature ) ); + } + + protected function mockRequest( $baseStr ) { + $stub = $this->getMockBuilder( 'Wikimedia\Slimapp\OAuth\request' ) + ->disableOriginalConstructor() + ->getMock(); + $stub->method( 'getSignatureBaseString' ) + ->willReturn( $baseStr ); + return $stub; + } +} diff --git a/tests/OAuth/SignatureMethod/RsaSha1Test.php b/tests/OAuth/SignatureMethod/RsaSha1Test.php new file mode 100644 index 0000000..c1da1ba --- /dev/null +++ b/tests/OAuth/SignatureMethod/RsaSha1Test.php @@ -0,0 +1,144 @@ +<?php +/** + * @section LICENSE + * Copyright (c) 2007 Andy Smith + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to + * whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @file + */ + +namespace Wikimedia\Slimapp\OAuth\SignatureMethod; + +use Wikimedia\Slimapp\OAuth\Consumer; +use Wikimedia\Slimapp\OAuth\Request; +use Wikimedia\Slimapp\OAuth\Token; + +/** + * @coversDefaultClass \Wikimedia\Slimapp\OAuth\SignatureMethod\RsaSha1 + */ +class RsaSha1Test extends \PHPUnit_Framework_TestCase { + private $method; + + public function setUp() { + $cert = <<<EOD +-----BEGIN PRIVATE KEY----- +MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALRiMLAh9iimur8V +A7qVvdqxevEuUkW4K+2KdMXmnQbG9Aa7k7eBjK1S+0LYmVjPKlJGNXHDGuy5Fw/d +7rjVJ0BLB+ubPK8iA/Tw3hLQgXMRRGRXXCn8ikfuQfjUS1uZSatdLB81mydBETlJ +hI6GH4twrbDJCR2Bwy/XWXgqgGRzAgMBAAECgYBYWVtleUzavkbrPjy0T5FMou8H +X9u2AC2ry8vD/l7cqedtwMPp9k7TubgNFo+NGvKsl2ynyprOZR1xjQ7WgrgVB+mm +uScOM/5HVceFuGRDhYTCObE+y1kxRloNYXnx3ei1zbeYLPCHdhxRYW7T0qcynNmw +rn05/KO2RLjgQNalsQJBANeA3Q4Nugqy4QBUCEC09SqylT2K9FrrItqL2QKc9v0Z +zO2uwllCbg0dwpVuYPYXYvikNHHg+aCWF+VXsb9rpPsCQQDWR9TT4ORdzoj+Nccn +qkMsDmzt0EfNaAOwHOmVJ2RVBspPcxt5iN4HI7HNeG6U5YsFBb+/GZbgfBT3kpNG +WPTpAkBI+gFhjfJvRw38n3g/+UeAkwMI2TJQS4n8+hid0uus3/zOjDySH3XHCUno +cn1xOJAyZODBo47E+67R4jV1/gzbAkEAklJaspRPXP877NssM5nAZMU0/O/NGCZ+ +3jPgDUno6WbJn5cqm8MqWhW1xGkImgRk+fkDBquiq4gPiT898jusgQJAd5Zrr6Q8 +AO/0isr/3aa6O6NLQxISLKcPDk2NOccAfS/xOtfOz4sJYM3+Bs4Io9+dZGSDCA54 +Lw03eHTNQghS0A== +-----END PRIVATE KEY----- +EOD; + $pubcert = <<<EOD +-----BEGIN CERTIFICATE----- +MIIBpjCCAQ+gAwIBAgIBATANBgkqhkiG9w0BAQUFADAZMRcwFQYDVQQDDA5UZXN0 +IFByaW5jaXBhbDAeFw03MDAxMDEwODAwMDBaFw0zODEyMzEwODAwMDBaMBkxFzAV +BgNVBAMMDlRlc3QgUHJpbmNpcGFsMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB +gQC0YjCwIfYoprq/FQO6lb3asXrxLlJFuCvtinTF5p0GxvQGu5O3gYytUvtC2JlY +zypSRjVxwxrsuRcP3e641SdASwfrmzyvIgP08N4S0IFzEURkV1wp/IpH7kH41Etb +mUmrXSwfNZsnQRE5SYSOhh+LcK2wyQkdgcMv11l4KoBkcwIDAQABMA0GCSqGSIb3 +DQEBBQUAA4GBAGZLPEuJ5SiJ2ryq+CmEGOXfvlTtEL2nuGtr9PewxkgnOjZpUy+d +4TvuXJbNQc8f4AMWL/tO9w0Fk80rWKp9ea8/df4qMq5qlFWlx6yOLQxumNOmECKb +WpkUQDIDJEoFUzKMVuJf4KO/FJ345+BNLGgbJ6WujreoM1X/gYfdnJ/J +-----END CERTIFICATE----- +EOD; + + $this->method = $this->getMockBuilder( + 'Wikimedia\Slimapp\OAuth\SignatureMethod\RsaSha1' ) + ->setMethods( array( 'fetchPrivateCert', 'fetchPublicCert' ) ) + ->getMock(); + + $this->method->method( 'fetchPrivateCert' ) + ->willReturn( $cert ); + $this->method->method( 'fetchPublicCert' ) + ->willReturn( $pubcert ); + } + + public function testIdentifyAsRsaSha1() { + $this->assertEquals( 'RSA-SHA1', $this->method->getName() ); + } + + public function testBuildSignature() { + if ( !function_exists( 'openssl_get_privatekey' ) ) { + $this->markTestSkipped( + 'OpenSSL not available, can\'t test RSA-SHA1 functionality' + ); + } + + // Tests taken from http://wiki.oauth.net/TestCases section 9.3 + // ("RSA-SHA1") + $request = $this->mockRequest( + // @codingStandardsIgnoreStart Line exceeds 100 characters + 'GET&http%3A%2F%2Fphotos.example.net%2Fphotos&file%3Dvacaction.jpg%26oauth_consumer_key%3Ddpf43f3p2l4k3l03%26oauth_nonce%3D13917289812797014437%26oauth_signature_method%3DRSA-SHA1%26oauth_timestamp%3D1196666512%26oauth_version%3D1.0%26size%3Doriginal' + // @codingStandardsIgnoreEnd + ); + $consumer = new Consumer( 'dpf43f3p2l4k3l03', '__unused__' ); + $token = null; + // @codingStandardsIgnoreStart Line exceeds 100 characters + $signature = 'jvTp/wX1TYtByB1m+Pbyo0lnCOLIsyGCH7wke8AUs3BpnwZJtAuEJkvQL2/9n4s5wUmUl4aCI4BwpraNx4RtEXMe5qg5T1LVTGliMRpKasKsW//e+RinhejgCuzoH26dyF8iY2ZZ/5D1ilgeijhV/vBka5twt399mXwaYdCwFYE='; + // @codingStandardsIgnoreEnd + $this->assertEquals( $signature, + $this->method->buildSignature( $request, $consumer, $token ) + ); + } + + public function testVerifySignature() { + if ( !function_exists( 'openssl_get_privatekey' ) ) { + $this->markTestSkipped( + 'OpenSSL not available, can\'t test RSA-SHA1 functionality' + ); + } + + // Tests taken from http://wiki.oauth.net/TestCases section 9.3 + // ("RSA-SHA1") + $request = $this->mockRequest( + // @codingStandardsIgnoreStart Line exceeds 100 characters + 'GET&http%3A%2F%2Fphotos.example.net%2Fphotos&file%3Dvacaction.jpg%26oauth_consumer_key%3Ddpf43f3p2l4k3l03%26oauth_nonce%3D13917289812797014437%26oauth_signature_method%3DRSA-SHA1%26oauth_timestamp%3D1196666512%26oauth_version%3D1.0%26size%3Doriginal' + // @codingStandardsIgnoreEnd + ); + $consumer = new Consumer( 'dpf43f3p2l4k3l03', '__unused__' ); + $token = null; + // @codingStandardsIgnoreStart Line exceeds 100 characters + $signature = 'jvTp/wX1TYtByB1m+Pbyo0lnCOLIsyGCH7wke8AUs3BpnwZJtAuEJkvQL2/9n4s5wUmUl4aCI4BwpraNx4RtEXMe5qg5T1LVTGliMRpKasKsW//e+RinhejgCuzoH26dyF8iY2ZZ/5D1ilgeijhV/vBka5twt399mXwaYdCwFYE='; + // @codingStandardsIgnoreEnd + $this->assertTrue( $this->method->checkSignature( + $request, $consumer, $token, $signature + ) ); + } + + protected function mockRequest( $baseStr ) { + $stub = $this->getMockBuilder( 'Wikimedia\Slimapp\OAuth\request' ) + ->disableOriginalConstructor() + ->getMock(); + $stub->method( 'getSignatureBaseString' ) + ->willReturn( $baseStr ); + return $stub; + } +} diff --git a/tests/OAuth/TokenTest.php b/tests/OAuth/TokenTest.php new file mode 100644 index 0000000..eecde55 --- /dev/null +++ b/tests/OAuth/TokenTest.php @@ -0,0 +1,58 @@ +<?php +/** + * @section LICENSE + * Copyright (c) 2007 Andy Smith + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to + * whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @file + */ + +namespace Wikimedia\Slimapp\OAuth; + +/** + * @coversDefaultClass \Wikimedia\Slimapp\OAuth\Token + */ +class TokenTest extends \PHPUnit_Framework_TestCase { + public function testSerialize() { + $token = new Token( 'token', 'secret' ); + $this->assertEquals( + 'oauth_token=token&oauth_token_secret=secret', $token->toString() + ); + + $token = new Token( 'token&', 'secret%' ); + $this->assertEquals( + 'oauth_token=token%26&oauth_token_secret=secret%25', + $token->toString() + ); + } + public function testConvertToString() { + $token = new Token( 'token', 'secret' ); + $this->assertEquals( + 'oauth_token=token&oauth_token_secret=secret', (string)$token + ); + + $token = new Token( 'token&', 'secret%' ); + $this->assertEquals( + 'oauth_token=token%26&oauth_token_secret=secret%25', + (string)$token + ); + } +} -- To view, visit https://gerrit.wikimedia.org/r/234917 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: Id8222bbe9960942f94fd67d7fbc3290cb0105245 Gerrit-PatchSet: 6 Gerrit-Project: wikimedia/slimapp Gerrit-Branch: master Gerrit-Owner: BryanDavis <bda...@wikimedia.org> Gerrit-Reviewer: BryanDavis <bda...@wikimedia.org> Gerrit-Reviewer: CSteipp <cste...@wikimedia.org> Gerrit-Reviewer: Niharika29 <niharikakohl...@gmail.com> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits