CSteipp has uploaded a new change for review. https://gerrit.wikimedia.org/r/68001
Change subject: WIP commit of some basic classes for /initial call ...................................................................... WIP commit of some basic classes for /initial call Not functional, but just so everyone can start adding to some common classes. Change-Id: I395e7e07eab652d96216af2cafe42a0c26db4ec7 --- A backend/OAuthClientRequestHandler.php M backend/OAuthUtils.php A backend/requestHandler/OAuthClientRequestHandler.php A backend/store/OAuthStore.php A frontend/special/SpecialOAuth.php A lib/OAuthClient.php A lib/OAuthClientRequest.php A lib/OAuthSignature.php 8 files changed, 650 insertions(+), 0 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/OAuth refs/changes/01/68001/1 diff --git a/backend/OAuthClientRequestHandler.php b/backend/OAuthClientRequestHandler.php new file mode 100644 index 0000000..9060386 --- /dev/null +++ b/backend/OAuthClientRequestHandler.php @@ -0,0 +1,140 @@ +<?php + +abstract class OAuthClientRequestHandler { + + // Type of OAuth Request (initiate, authorize, token) + protected $requestType; + + // OAuth datastore + protected $store; + + public static function newHandler( $type, OAuthStore $store ) { + $validTypes = array( 'initiate', 'authorize', 'token' ); + if ( !in_array( $type, $validTypes ) ) { + return null; + } + $this->requestType = $type; + $this->store = $store; + $class = 'OAuthClientRequestHandler' . ucfirst( $type ); + return new $class; + } + + + /** + * + * + */ + public function verifyRequest( OAuthClientRequest $request ) { + + // test sanity + if ( !$request->consumerKey || !$request->signature || !$request->signatureMethod ) { + throw new MWOAuthException( 'missing required parameters' ); + } + + if ( $request->version !== false && $request->version != '1.0' ) { + throw new MWOAuthException( 'only OAuth 1.0 is supported at this time' ); + } + + // Check request signature + $sigVerify = $request->getSignatureClass(); + $sigString = $request->getSignatureString( $this->getSignatureParameters() ); + if ( !$sigVerify->verify( $sigString, $this->store->getClientFromKey( $request->consumerKey ) ) ) { + throw new MWOAuthException( 'invalid-signature' ); + } + + // Verify freshness and nonce + if ( time() > $request->timestamp + ( 60 * 5 ) ) { + throw new MWOAuthException( 'timestamp-too-old' ); + } + + if ( $this->store->seenNonce( $request->nonce, $request->consumerKey ) ) { + throw new MWOAuthException( 'nonce-used' ); + } + + } +} + + +class OAuthClientRequestHandlerInitiate { + + public function process( OAuthClientRequest $request ) { + + $this->verifyRequest( $request ); + + $client = $this->store->getClientFromKey( $request->consumerKey ); + if ( !$client || !$client->isValid() || $client->isBlocked() ) { + throw new MWOAuthException( 'invalid-client' ); + } + + if ( !$client->isValidCallback() ) { + throw new MWOAuthException( 'invalid-callback' ); + } + + // register new oauth_token + secret for authorization + $token = OAuthToken::newForAuthorization(); + $this->store->saveToken( $token ); + + return array( + 'oauth_token' => $token->token, + 'oauth_token_secret' => $token->secret, + 'oauth_callback_confirmed' => true + ); + + } + + protected function getSignatureParameters() { + return array( 'oauth_consumer_key', + 'oauth_signature_method', + 'oauth_timestamp', + 'oauth_nonce', + 'oauth_callback' + ); + } + +} + +class OAuthClientRequestHandlerAuthorize { + + +} + +class OAuthClientRequestHandlerToken { + + + public function process( OAuthClientRequest $request ) { + $this->verifyRequest( $request ); + + $client = $this->store->getClientFromKey( $request->consumerKey ); + if ( !$client || !$client->isValid() || $client->isBlocked() ) { + throw new MWOAuthException( 'invalid-client' ); + } + + // createa an authorized token + $token = OAuthToken::newAuthorizedToken( $request->consumerKey, $request->token ); + + if ( !$token ) { + throw new MWOAuthException( 'invalid-request' ); + } + + $this->store->saveToken( $token ); + + return array( + 'oauth_token' => $token->token, + 'oauth_token_secret' => $token->secret, + 'oauth_callback_confirmed' => true + ); + + } + + + protected function getSignatureParameters() { + array( 'oauth_consumer_key', + 'oauth_token', + 'oauth_signature_method', + 'oauth_timestamp', + 'oauth_nonce', + 'oauth_verifier', + 'oauth_callback' + ); + } +} diff --git a/backend/OAuthUtils.php b/backend/OAuthUtils.php index 31141b9..7acfee4 100644 --- a/backend/OAuthUtils.php +++ b/backend/OAuthUtils.php @@ -4,4 +4,15 @@ */ class OAuthUtils { + public static function getHeaderParams( $header ) { + $params = array(); + if ( preg_match_all('/([a-z_-]*)=(:?"([^"]*)"|([^,]*))/', $header, $matches ) ) { + foreach ( $matches[1] as $ndx => $param ) { + // Note: rawurldecode in php 5.3+ should be fine here + $params[$param] = rawurldecode( empty( $matches[3][$ndx] ) ? $matches[4][$ndx] : $matches[3][$ndx] ); + } + } + return $params; + } + } diff --git a/backend/requestHandler/OAuthClientRequestHandler.php b/backend/requestHandler/OAuthClientRequestHandler.php new file mode 100644 index 0000000..c72e15b --- /dev/null +++ b/backend/requestHandler/OAuthClientRequestHandler.php @@ -0,0 +1,146 @@ +<?php + +// Handle a special page or api request + +abstract class OAuthClientRequestHandler { + + // Type of OAuth Request (initiate, authorize, token) + protected $requestType; + + // OAuth datastore + protected $store; + + abstract public function process( OAuthClientRequest $request ); + abstract protected function getSignatureParameters(); + + public static function newHandler( $type, $store ) { + $validTypes = array( 'initiate', 'authorize', 'token' ); + if ( !in_array( $type, $validTypes ) ) { + return null; + } + $this->requestType = $type; + $this->store = $store; + $class = 'OAuthClientRequestHandler' . ucfirst( $type ); + return new $class; + } + + + /** + * + * + */ + public function verifyRequest( OAuthClientRequest $request ) { + + // test sanity + if ( !$request->consumerKey || !$request->signature || !$request->signatureMethod ) { + throw new MWOAuthException( 'missing required parameters' ); + } + + if ( $request->version !== false && $request->version != '1.0' ) { + throw new MWOAuthException( 'only OAuth 1.0 is supported at this time' ); + } + + // Check request signature + $sigVerify = $request->getSignatureClass(); + $sigString = $request->getSignatureString( $this->getSignatureParameters() ); + if ( !$sigVerify->verify( $sigString, $this->store->getClientFromKey( $request->consumerKey ) ) ) { + throw new MWOAuthException( 'invalid-signature' ); + } + + // Verify freshness and nonce + if ( time() > $request->timestamp + ( 60 * 5 ) ) { + throw new MWOAuthException( 'timestamp-too-old' ); + } + + if ( $this->store->seenNonce( $request->nonce, $request->consumerKey ) ) { + throw new MWOAuthException( 'nonce-used' ); + } + + } +} + + +class OAuthClientRequestHandlerInitiate extends OAuthClientRequestHandler { + + public function process( OAuthClientRequest $request ) { + + $this->verifyRequest( $request ); + + $client = $this->store->getClientFromKey( $request->consumerKey ); + if ( !$client || !$client->isValid() || $client->isBlocked() ) { + throw new MWOAuthException( 'invalid-client' ); + } + + if ( !$client->isValidCallback() ) { + throw new MWOAuthException( 'invalid-callback' ); + } + + // register new oauth_token + secret for authorization + $token = OAuthToken::newForAuthorization(); + $this->store->saveToken( $token ); + + return array( + 'oauth_token' => $token->token, + 'oauth_token_secret' => $token->secret, + 'oauth_callback_confirmed' => true + ); + + } + + protected function getSignatureParameters() { + return array( 'oauth_consumer_key', + 'oauth_signature_method', + 'oauth_timestamp', + 'oauth_nonce', + 'oauth_callback' + ); + } + +} + +class OAuthClientRequestHandlerAuthorize extends OAuthClientRequestHandler { + + // this may not be the best idea? + +} + +class OAuthClientRequestHandlerToken extends OAuthClientRequestHandler { + + + public function process( OAuthClientRequest $request ) { + $this->verifyRequest( $request ); + + $client = $this->store->getClientFromKey( $request->consumerKey ); + if ( !$client || !$client->isValid() || $client->isBlocked() ) { + throw new MWOAuthException( 'invalid-client' ); + } + + // createa an authorized token + $token = OAuthToken::newAuthorizedToken( $request->consumerKey, $request->token ); + + if ( !$token ) { + throw new MWOAuthException( 'invalid-request' ); + } + + $this->store->saveToken( $token ); + + return array( + 'oauth_token' => $token->token, + 'oauth_token_secret' => $token->secret, + 'oauth_callback_confirmed' => true + ); + + } + + + protected function getSignatureParameters() { + array( 'oauth_consumer_key', + 'oauth_token', + 'oauth_signature_method', + 'oauth_timestamp', + 'oauth_nonce', + 'oauth_verifier', + 'oauth_callback' + ); + } +} diff --git a/backend/store/OAuthStore.php b/backend/store/OAuthStore.php new file mode 100644 index 0000000..a7b2ce6 --- /dev/null +++ b/backend/store/OAuthStore.php @@ -0,0 +1,40 @@ +<?php + + +abstract class OAuthStore { + + // ObjectCache for Tokens and Nonces + protected $cache; + + // Persistant storage (DB most likely) for logging/audit + protected $logging; + + public function __construct( ObjectCache $cache, $log ) { + $this->cache = $cache; + $this->loggging = $log; + } + + public static function getStore( ) { + global $wgOAuthStorage; + + $datastore = $wgOAuthStorage['data']; + $cachestore = $wgOAuthStorage['cache']; + $logstore = $wgOAuthStorage['log']; + $cache = new $cachestore(); + $log = new $logstore(); + return new $datastore( $cache, $log ); + } + + abstract public function getClientFromKey( $key); + + //abstract public function getPublicKeyForClient( $client ); + + abstract public function saveToken( $token ); + +} + +class OAuthStoreMysql extends OAuthStore { + + + +} diff --git a/frontend/special/SpecialOAuth.php b/frontend/special/SpecialOAuth.php new file mode 100644 index 0000000..e3e1051 --- /dev/null +++ b/frontend/special/SpecialOAuth.php @@ -0,0 +1,59 @@ +<?php + +class SpecialOAuth extends UnlistedSpecialPage { + + function __construct() { + parent::__construct( 'OAuth' ); + } + + public function execute( $subpage ) { + global $wgOAuthStorageType; + + $format = $request->getVal( 'format', 'json' ); + + if ( !in_array( $subpage, array( 'initiate', 'authorize', 'token' ) ) ) { + $this->showError( 'oauth-client-invalidrequest', $format ); + } + + try { + $out = $this->getOutput(); + $request = $this->getRequest(); + + $store = OAuthStore::getStore( $wgOAuthStorageType ); + $clientRequest = new OAuthClientRequest( $request, $subpage ); + $oauthClientHandler = OAuthClientRequestHandler::newHandler( $subpage, $store ); + $oauthClientHandler->verifyRequest( $clientRequest ); + + if ( !$oauthClientHandler || !$clientRequest->isValid() ) { + $this->showError( 'oauth-client-invalidrequest', $format ); + } + + $response = $oauthClientHandler->process( $clientRequest ); + + $this->showResponse( $response, $request->getVal( 'format', 'json' ) ); + + } catch ( MWOAuthException $exception ) { + $this->showError( $exception->getMessage(), $format ); + } + } + + /** + * + * + * @param string $message message key to return to the user + * @param string $format the format of the response: json, xml, or html + */ + private function showError( $message, $format ) { + + } + + /** + * + * + * @param array $response values to give back to the client + * @param string $format the format of the response: json, xml, or html + */ + private function showResponse( array $response, $format ) { + + } +} diff --git a/lib/OAuthClient.php b/lib/OAuthClient.php new file mode 100644 index 0000000..2f6367e --- /dev/null +++ b/lib/OAuthClient.php @@ -0,0 +1,48 @@ +<?php + + +class OAuthClient { + + const DELETED_NAME = 1; // Assuming we will want to have this available + const SUPPRESSED_NAME = 2; + + protected $consumerKey; + protected $hmacSecret, $rsaPublicKey; + protected $authorized, $blocked, $deleted; + + protected $owner; + + public function __construct( $consumerKey, $hmacSecret, $rsaPublicKey, $blocked, $authorized ) { + $this->consumerKey = $consumerKey; + $this->hmacSecret = $hmacSecret; + $this->rsaPublicKey = $rsaPublicKey; + $this->authorized = $authorized; + $this->blocked = $blocked; + $this->authorized = $authorized; + } + + + public function isValid() { + return $authorized && !$this->isDeleted() && !$this->isSuppressed(); + } + + public function isBlocked() { + return ( $blocked == 1 ); + } + + public function isDeleted() { + return ( $deleted & DELETED_NAME ); + } + + public function isSuppressed() { + return ( $deleted & SUPPRESSED_NAME ); + } + + public function getPublicKey() { + return $this->rsaPublicKey; + } + + public function getHmacKey() { + return $this->hmacSecret; + } +} diff --git a/lib/OAuthClientRequest.php b/lib/OAuthClientRequest.php new file mode 100644 index 0000000..dd6a440 --- /dev/null +++ b/lib/OAuthClientRequest.php @@ -0,0 +1,126 @@ +<?php + +class OAuthClientRequest { + + // Stage of the OAuth protocol handshake where this request came from + // initiate, authorize, token, api + public $type; + + //(optional) OAuth realm + public $realm; + + //oauth_consumer_key (rand string) + public $consumerKey; + + //oauth_signature_method ["HMAC-SHA1" or "RSA-SHA1"] + public $signatureMethod; + + //oauth_timestamp (unix timestamp) + public $timestamp; + + //oauth_nonce (random string to prevent replay) + public $nonce; + + //oauth_callback (encoded url) + public $callback; + + //oauth_signature (encoded string) + public $signature; + + + public $http_method; + public $http_url; + + + // This should be reusable from a special page, API, or unit test request + public function __construct( $request, $type ) { + $this->type = $type; + + if ( $request instanceof WebRequest ) { + $params = $this->getParamsFromHeader( $request->getHeader( 'Authorize' ) ); + $params['http_method'] = $request->getMethod(); + $params['http_url'] = $request->getRequestURL(); //not sure about this.. + } else { + $params = $request; + } + + if ( is_array( $params ) ) { + $this->realm = $params['realm']; + $this->consumerKey = $params['oauth_consumer_key']; + $this->signatureMethod = $params['oauth_signature_method']; + $this->timestamp = $params['oauth_timestamp']; + $this->nonce = $params['oauth_nonce']; + $this->callback = $params['oauth_callback']; + $this->signature = $params['oauth_signature']; + + $this->http_method = $params['http_method']; + } else { + wfDebug(); + $this->valid = false; + return null; + } + } + + /** + * Split up and decode the Authorization Header + * + * @param string $header the HTTP header + */ + protected function getParamsFromHeader( $header ) { + $header_parameters = null; + if ( $header && substr( $header, 0, 6) == 'OAuth ' ) { + $header_parameters = OAuthUtils::getHeaderParams( $header ); + } + return $header_parameters; + } + + + public function setValid( $valid ) { + $this->valid = $valid; + } + + public function isValid() { + return $this->valid; + } + + + public function getSignatureClassName() { + if ( $this->signatureMethod === 'HMAC-SHA1' ) { + return new OAuthSignatureHMAC; + } elseif ( $this->signatureMethod === 'RSA-SHA1' ) { + return new OAuthSignatureRSA; + } else { + throw new MWOAuthException( 'invalid-signature-type' ); + } + } + + /** + * Returns the string that was signed (or should have been signed) for this request + * + * @param array $paramList an array of strings, listing the params that should have been + * included and signed for this request type. + * @return string the string to sign or chech signature. + */ + public function getSignatureString( $paramList ) { + $parts = array( + strtoupper( $this->http_method ), + $this->http_url, + $this->getSignableParameters( $paramList ) + ); + + // rawurlencode should be fine in php 5.3+ + $parts = array_map( 'rawurlencode', $parts ); + + return implode('&', $parts); + } + + + protected function getSignableParameters( $paramList ) { + $pairs = array(); + foreach ( $paramList as $param ) { + $pairs[] = rawurlencode( $param ) . '=' rawurlencode( $this->$param ); + } + return implode('&', $pairs); + } + +} diff --git a/lib/OAuthSignature.php b/lib/OAuthSignature.php new file mode 100644 index 0000000..c35ec47 --- /dev/null +++ b/lib/OAuthSignature.php @@ -0,0 +1,80 @@ +<?php + + +abstract class OAuthSignature { + + protected $request; + + public function __construct( $request ) { + $this->request = $request; + } + + // Stuff that must be signed.. or we could just check all parameters and fail if the sig doesn't match + public getParamsSignedForRequest() { + + switch ( $this->request->type ) { + case 'initiate': + return array( 'oauth_consumer_key', 'oauth_signature_method', 'oauth_timestamp', 'oauth_nonce', 'oauth_callback' ); + + case 'token': + return array( 'oauth_consumer_key', 'oauth_token', 'oauth_signature_method', 'oauth_timestamp', 'oauth_nonce', 'oauth_verifier', 'oauth_callback' ); + + case 'api': + // todo + + default: + throw new MWOAuthException( 'invalid-request-type' ); + } + + } + + abstract public function verify( $sigString, OAuthClient $client ); +} + + +class OAuthSignatureRSA extends OAuthSignature { + + public function verify( $sigString, OAuthClient $client ) { + + $clientPublicKey = $client->getPublicKey(); + $keyid = openssl_get_publickey( $clientPublicKey ); + $ok = openssl_verify( + $sigString, + base64_decode( $request->signature ), + $keyid + ); + + openssl_free_key( $keyid ); + + if ( $ok !== 1 ) { + wfDebug( ); + throw new MWOAuthException( 'invalid-signature' ); + } + + + } + +} + + +class OAuthSignatureHMAC extends OAuthSignature { + + public function verify( $sigString, OAuthClient $client ) { + + $clientSecret = $client->getHmacKey(); + $signature = hash_hmac( + 'sha1', + $sigString, + $clientSecret, + true + ); + + if ( base64_encode( $signature ) !== $this->signature ) { + wfDebug( ); + throw new MWOAuthException( 'invalid-signature' ); + } + + + } + +} -- To view, visit https://gerrit.wikimedia.org/r/68001 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I395e7e07eab652d96216af2cafe42a0c26db4ec7 Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/extensions/OAuth Gerrit-Branch: master Gerrit-Owner: CSteipp <cste...@wikimedia.org> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits