summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'OAuth/src/Backend/MWOAuthDataStore.php')
-rw-r--r--OAuth/src/Backend/MWOAuthDataStore.php257
1 files changed, 257 insertions, 0 deletions
diff --git a/OAuth/src/Backend/MWOAuthDataStore.php b/OAuth/src/Backend/MWOAuthDataStore.php
new file mode 100644
index 00000000..76c33522
--- /dev/null
+++ b/OAuth/src/Backend/MWOAuthDataStore.php
@@ -0,0 +1,257 @@
+<?php
+
+namespace MediaWiki\Extensions\OAuth\Backend;
+
+use MediaWiki\Extensions\OAuth\Lib\OAuthConsumer;
+use MediaWiki\Extensions\OAuth\Lib\OAuthDataStore;
+use MediaWiki\Logger\LoggerFactory;
+use Wikimedia\Rdbms\DBConnRef;
+
+class MWOAuthDataStore extends OAuthDataStore {
+ /** @var DBConnRef DB for the consumer/grant registry */
+ protected $centralReplica;
+
+ /** @var DBConnRef|null Master DB for repeated lookup in case of replication lag problems;
+ * null if there is no separate master and replica DB
+ */
+ protected $centralMaster;
+
+ /** @var \BagOStuff Cache for Tokens and Nonces */
+ protected $cache;
+
+ /** @var \Psr\Log\LoggerInterface */
+ protected $logger;
+
+ /**
+ * @param DBConnRef $centralReplica Central DB replica
+ * @param DBConnRef|null $centralMaster Central DB master (if different)
+ * @param \BagOStuff $cache
+ */
+ public function __construct( DBConnRef $centralReplica, $centralMaster, \BagOStuff $cache ) {
+ if ( $centralMaster !== null && !( $centralMaster instanceof DBConnRef ) ) {
+ throw new \InvalidArgumentException(
+ __METHOD__ . ': $centralMaster must be a DB or null'
+ );
+ }
+ $this->centralReplica = $centralReplica;
+ $this->centralMaster = $centralMaster;
+ $this->cache = $cache;
+ $this->logger = LoggerFactory::getInstance( 'OAuth' );
+ }
+
+ /**
+ * Get an MWOAuthConsumer from the consumer's key
+ *
+ * @param string $consumerKey the string value of the Consumer's key
+ * @return Consumer|bool
+ */
+ public function lookup_consumer( $consumerKey ) {
+ return Consumer::newFromKey( $this->centralReplica, $consumerKey );
+ }
+
+ /**
+ * Get either a request or access token from the data store
+ *
+ * @param OAuthConsumer|Consumer $consumer
+ * @param string $token_type
+ * @param string $token String the token
+ * @throws MWOAuthException
+ * @return MWOAuthToken
+ */
+ public function lookup_token( $consumer, $token_type, $token ) {
+ $this->logger->debug( __METHOD__ . ": Looking up $token_type token '$token'" );
+
+ if ( $token_type === 'request' ) {
+ $returnToken = $this->cache->get( Utils::getCacheKey(
+ 'token',
+ $consumer->key,
+ $token_type,
+ $token
+ ) );
+ if ( $returnToken === '**USED**' ) {
+ throw new MWOAuthException( 'mwoauthdatastore-request-token-already-used', [
+ \Message::rawParam( \Linker::makeExternalLink(
+ 'https://www.mediawiki.org/wiki/Help:OAuth/Errors#E009',
+ 'E009',
+ true
+ ) )
+ ] );
+ }
+ if ( $token === null || !( $returnToken instanceof MWOAuthToken ) ) {
+ throw new MWOAuthException( 'mwoauthdatastore-request-token-not-found', [
+ \Message::rawParam( \Linker::makeExternalLink(
+ 'https://www.mediawiki.org/wiki/Help:OAuth/Errors#E004',
+ 'E004',
+ true
+ ) )
+ ] );
+ }
+ } elseif ( $token_type === 'access' ) {
+ $cmra = ConsumerAcceptance::newFromToken( $this->centralReplica, $token );
+ if ( !$cmra && $this->centralMaster ) {
+ // try master in case there is replication lag T124942
+ $cmra = ConsumerAcceptance::newFromToken( $this->centralMaster, $token );
+ }
+ if ( !$cmra ) {
+ throw new MWOAuthException( 'mwoauthdatastore-access-token-not-found' );
+ }
+
+ // Ensure the cmra's consumer matches the expected consumer (T103023)
+ $mwconsumer = ( $consumer instanceof Consumer )
+ ? $consumer : $this->lookup_consumer( $consumer->key );
+ if ( !$mwconsumer || $mwconsumer->getId() !== $cmra->getConsumerId() ) {
+ throw new MWOAuthException( 'mwoauthdatastore-access-token-not-found' );
+ }
+
+ $secret = Utils::hmacDBSecret( $cmra->getAccessSecret() );
+ $returnToken = new MWOAuthToken( $cmra->getAccessToken(), $secret );
+ } else {
+ throw new MWOAuthException( 'mwoauthdatastore-invalid-token-type' );
+ }
+
+ return $returnToken;
+ }
+
+ /**
+ * Check that nonce has not been seen before. Add it on check, so we don't repeat it.
+ * Note, timestamp has already been checked, so this should be a fresh nonce.
+ *
+ * @param Consumer|OAuthConsumer $consumer
+ * @param string $token
+ * @param string $nonce
+ * @param int $timestamp
+ * @return bool
+ */
+ public function lookup_nonce( $consumer, $token, $nonce, $timestamp ) {
+ $key = Utils::getCacheKey( 'nonce', $consumer->key, $token, $nonce );
+ // Do an add for the key associated with this nonce to check if it was already used.
+ // Set timeout 5 minutes in the future of the timestamp as OAuthServer does. Use the
+ // timestamp so the client can also expire their nonce records after 5 mins.
+ if ( !$this->cache->add( $key, 1, $timestamp + 300 ) ) {
+ $this->logger->info( "$key exists, so nonce has been used by this consumer+token" );
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Helper function to generate and return an MWOAuthToken. MWOAuthToken can be used as a
+ * request or access token.
+ * TODO: put in Utils?
+ * @return MWOAuthToken
+ */
+ public static function newToken() {
+ return new MWOAuthToken(
+ \MWCryptRand::generateHex( 32 ),
+ \MWCryptRand::generateHex( 32 )
+ );
+ }
+
+ /**
+ * Generate a new token (attached to this consumer), save it in the cache, and return it
+ *
+ * @param Consumer|OAuthConsumer $consumer
+ * @param string $callback
+ * @return MWOAuthToken
+ */
+ public function new_request_token( $consumer, $callback = 'oob' ) {
+ $token = self::newToken();
+ $cacheConsumerKey = Utils::getCacheKey( 'consumer', 'request', $token->key );
+ $cacheTokenKey = Utils::getCacheKey(
+ 'token', $consumer->key, 'request', $token->key
+ );
+ $cacheCallbackKey = Utils::getCacheKey(
+ 'callback', $consumer->key, 'request', $token->key
+ );
+ $this->cache->add( $cacheConsumerKey, $consumer->key, 600 ); // 10 minutes. Kindof arbitray.
+ $this->cache->add( $cacheTokenKey, $token, 600 ); // 10 minutes. Kindof arbitray.
+ $this->cache->add( $cacheCallbackKey, $callback, 600 ); // 10 minutes. Kindof arbitray.
+ $this->logger->debug( __METHOD__ .
+ ": New request token {$token->key} for {$consumer->key} with callback {$callback}" );
+ return $token;
+ }
+
+ /**
+ * Return a consumer key associated with the given request token.
+ *
+ * @param MWOAuthToken $requestToken the request token
+ * @return string|false the consumer key or false if nothing is stored for the request token
+ */
+ public function getConsumerKey( $requestToken ) {
+ $cacheKey = Utils::getCacheKey( 'consumer', 'request', $requestToken );
+ $consumerKey = $this->cache->get( $cacheKey );
+ return $consumerKey;
+ }
+
+ /**
+ * Return a stored callback URL parameter given by the consumer in /initiate.
+ * It throws an exception if callback URL parameter does not exist in the cache.
+ * A stored callback URL parameter is deleted from the cache once read for the first
+ * time.
+ *
+ * @param string $consumerKey the consumer key
+ * @param string $requestKey original request key from /initiate
+ * @throws MWOAuthException
+ * @return string|false the stored callback URL parameter
+ */
+ public function getCallbackUrl( $consumerKey, $requestKey ) {
+ $cacheKey = Utils::getCacheKey( 'callback', $consumerKey, 'request', $requestKey );
+ $callback = $this->cache->get( $cacheKey );
+ if ( $callback === null || !is_string( $callback ) ) {
+ throw new MWOAuthException( 'mwoauthdatastore-callback-not-found' );
+ }
+ $this->cache->delete( $cacheKey );
+ return $callback;
+ }
+
+ /**
+ * Return a new access token attached to this consumer for the user associated with this
+ * token if the request token is authorized. Should also invalidate the request token.
+ *
+ * @param MWOAuthToken $token the request token that started this
+ * @param Consumer $consumer
+ * @param int|null $verifier
+ * @throws MWOAuthException
+ * @return MWOAuthToken the access token
+ */
+ public function new_access_token( $token, $consumer, $verifier = null ) {
+ $this->logger->debug( __METHOD__ .
+ ": Getting new access token for token {$token->key}, consumer {$consumer->key}" );
+
+ if ( !$token->getVerifyCode() || !$token->getAccessKey() ) {
+ throw new MWOAuthException( 'mwoauthdatastore-bad-token' );
+ } elseif ( $token->getVerifyCode() !== $verifier ) {
+ throw new MWOAuthException( 'mwoauthdatastore-bad-verifier' );
+ }
+
+ $cacheKey = Utils::getCacheKey( 'token',
+ $consumer->getConsumerKey(), 'request', $token->key );
+ $accessToken = $this->lookup_token( $consumer, 'access', $token->getAccessKey() );
+ $this->cache->set( $cacheKey, '**USED**', 600 );
+ $this->logger->debug( __METHOD__ .
+ ": New access token {$accessToken->key} for {$consumer->key}" );
+ return $accessToken;
+ }
+
+ /**
+ * Update a request token. The token probably already exists, but had another attribute added.
+ *
+ * @param MWOAuthToken $token the token to store
+ * @param Consumer|OAuthConsumer $consumer
+ */
+ public function updateRequestToken( $token, $consumer ) {
+ $cacheKey = Utils::getCacheKey( 'token', $consumer->key, 'request', $token->key );
+ $this->cache->set( $cacheKey, $token, 600 ); // 10 more minutes. Kindof arbitray.
+ }
+
+ /**
+ * Return the string representing the Consumer's public RSA key
+ *
+ * @param string $consumerKey the string value of the Consumer's key
+ * @return string|null
+ */
+ public function getRSAKey( $consumerKey ) {
+ $cmr = Consumer::newFromKey( $this->centralReplica, $consumerKey );
+ return $cmr ? $cmr->getRsaKey() : null;
+ }
+}