Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 118 |
|
0.00% |
0 / 11 |
CRAP | |
0.00% |
0 / 1 |
MWOAuthDataStore | |
0.00% |
0 / 118 |
|
0.00% |
0 / 11 |
1056 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
lookup_consumer | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
lookup_token | |
0.00% |
0 / 45 |
|
0.00% |
0 / 1 |
182 | |||
lookup_nonce | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
newToken | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
new_request_token | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
2 | |||
getConsumerKey | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getCallbackUrl | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
new_access_token | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
20 | |||
updateRequestToken | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getRSAKey | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\OAuth\Backend; |
4 | |
5 | use BagOStuff; |
6 | use InvalidArgumentException; |
7 | use MediaWiki\Extension\OAuth\Lib\OAuthConsumer; |
8 | use MediaWiki\Extension\OAuth\Lib\OAuthDataStore; |
9 | use MediaWiki\Linker\Linker; |
10 | use MediaWiki\Logger\LoggerFactory; |
11 | use Message; |
12 | use MWCryptRand; |
13 | use Psr\Log\LoggerInterface; |
14 | use Wikimedia\Rdbms\IDatabase; |
15 | |
16 | class MWOAuthDataStore extends OAuthDataStore { |
17 | /** @var IDatabase DB for the consumer/grant registry */ |
18 | protected $centralReplica; |
19 | |
20 | /** @var IDatabase|null Primary DB for repeated lookup in case of replication lag problems; |
21 | * null if there is no separate primary DB and replica DB |
22 | */ |
23 | protected $centralPrimary; |
24 | |
25 | /** @var BagOStuff Cache for tokens */ |
26 | protected $tokenCache; |
27 | |
28 | /** @var BagOStuff Cache for nonces */ |
29 | protected $nonceCache; |
30 | |
31 | /** @var LoggerInterface */ |
32 | protected $logger; |
33 | |
34 | /** |
35 | * @param IDatabase $centralReplica Central DB replica |
36 | * @param IDatabase|null $centralPrimary Central DB primary (if different) |
37 | * @param BagOStuff $tokenCache |
38 | * @param BagOStuff $nonceCache |
39 | */ |
40 | public function __construct( |
41 | IDatabase $centralReplica, |
42 | $centralPrimary, |
43 | BagOStuff $tokenCache, |
44 | BagOStuff $nonceCache |
45 | ) { |
46 | if ( $centralPrimary !== null && !( $centralPrimary instanceof IDatabase ) ) { |
47 | throw new InvalidArgumentException( |
48 | __METHOD__ . ': $centralPrimary must be a DB or null' |
49 | ); |
50 | } |
51 | $this->centralReplica = $centralReplica; |
52 | $this->centralPrimary = $centralPrimary; |
53 | $this->tokenCache = $tokenCache; |
54 | $this->nonceCache = $nonceCache; |
55 | $this->logger = LoggerFactory::getInstance( 'OAuth' ); |
56 | } |
57 | |
58 | /** |
59 | * Get an MWOAuthConsumer from the consumer's key |
60 | * |
61 | * @param string $consumerKey the string value of the Consumer's key |
62 | * @return Consumer|false |
63 | */ |
64 | public function lookup_consumer( $consumerKey ) { |
65 | return Consumer::newFromKey( $this->centralReplica, $consumerKey ); |
66 | } |
67 | |
68 | /** |
69 | * Get either a request or access token from the data store |
70 | * |
71 | * @param OAuthConsumer|Consumer $consumer |
72 | * @param string $token_type |
73 | * @param string $token String the token |
74 | * @throws MWOAuthException |
75 | * @return MWOAuthToken |
76 | */ |
77 | public function lookup_token( $consumer, $token_type, $token ) { |
78 | $this->logger->debug( __METHOD__ . ": Looking up $token_type token '$token'" ); |
79 | |
80 | if ( $token_type === 'request' ) { |
81 | $returnToken = $this->tokenCache->get( Utils::getCacheKey( |
82 | 'token', |
83 | $consumer->key, |
84 | $token_type, |
85 | $token |
86 | ) ); |
87 | if ( $returnToken === '**USED**' ) { |
88 | throw new MWOAuthException( 'mwoauthdatastore-request-token-already-used', [ |
89 | Message::rawParam( Linker::makeExternalLink( |
90 | 'https://www.mediawiki.org/wiki/Help:OAuth/Errors#E009', |
91 | 'E009', |
92 | true |
93 | ) ), |
94 | 'consumer' => $consumer->key, |
95 | ] ); |
96 | } |
97 | if ( $token === null || !( $returnToken instanceof MWOAuthToken ) ) { |
98 | throw new MWOAuthException( 'mwoauthdatastore-request-token-not-found', [ |
99 | Message::rawParam( Linker::makeExternalLink( |
100 | 'https://www.mediawiki.org/wiki/Help:OAuth/Errors#E004', |
101 | 'E004', |
102 | true |
103 | ) ), |
104 | 'consumer' => $consumer->key, |
105 | ] ); |
106 | } |
107 | } elseif ( $token_type === 'access' ) { |
108 | $cmra = ConsumerAcceptance::newFromToken( $this->centralReplica, $token ); |
109 | if ( !$cmra && $this->centralPrimary ) { |
110 | // try primary database in case there is replication lag T124942 |
111 | $cmra = ConsumerAcceptance::newFromToken( $this->centralPrimary, $token ); |
112 | } |
113 | if ( !$cmra ) { |
114 | throw new MWOAuthException( 'mwoauthdatastore-access-token-not-found' ); |
115 | } |
116 | |
117 | // Ensure the cmra's consumer matches the expected consumer (T103023) |
118 | $mwconsumer = ( $consumer instanceof Consumer ) |
119 | ? $consumer : $this->lookup_consumer( $consumer->key ); |
120 | if ( !$mwconsumer || $mwconsumer->getId() !== $cmra->getConsumerId() ) { |
121 | throw new MWOAuthException( 'mwoauthdatastore-access-token-not-found', [ |
122 | 'consumer' => $mwconsumer ? $mwconsumer->getConsumerKey() : '', |
123 | 'cmra_id' => $cmra->getId(), |
124 | ] ); |
125 | } |
126 | |
127 | $secret = Utils::hmacDBSecret( $cmra->getAccessSecret() ); |
128 | $returnToken = new MWOAuthToken( $cmra->getAccessToken(), $secret ); |
129 | } else { |
130 | throw new MWOAuthException( 'mwoauthdatastore-invalid-token-type', [ |
131 | 'token_type' => $token_type, |
132 | ] ); |
133 | } |
134 | |
135 | return $returnToken; |
136 | } |
137 | |
138 | /** |
139 | * Check that nonce has not been seen before. Add it on check, so we don't repeat it. |
140 | * Note, timestamp has already been checked, so this should be a fresh nonce. |
141 | * |
142 | * @param Consumer|OAuthConsumer $consumer |
143 | * @param string $token |
144 | * @param string $nonce |
145 | * @param int $timestamp |
146 | * @return bool |
147 | */ |
148 | public function lookup_nonce( $consumer, $token, $nonce, $timestamp ) { |
149 | $key = Utils::getCacheKey( 'nonce', $consumer->key, $token, $nonce ); |
150 | // Do an add for the key associated with this nonce to check if it was already used. |
151 | // Set timeout 5 minutes in the future of the timestamp as OAuthServer does. Use the |
152 | // timestamp so the client can also expire their nonce records after 5 mins. |
153 | if ( !$this->nonceCache->add( $key, 1, $timestamp + 300 ) ) { |
154 | // T308861 |
155 | $key = preg_replace( |
156 | "/(oauth_token_secret\=\w+:)/", |
157 | "oauth_token_secret=[REDACTED]:", |
158 | $key ); |
159 | $this->logger->info( '{key} exists, so nonce has been used by this consumer+token', |
160 | [ 'key' => $key, 'consumer' => $consumer->key, 'oauth_timestamp' => $timestamp ] ); |
161 | return true; |
162 | } |
163 | return false; |
164 | } |
165 | |
166 | /** |
167 | * Helper function to generate and return an MWOAuthToken. MWOAuthToken can be used as a |
168 | * request or access token. |
169 | * TODO: put in Utils? |
170 | * @return MWOAuthToken |
171 | */ |
172 | public static function newToken() { |
173 | return new MWOAuthToken( |
174 | MWCryptRand::generateHex( 32 ), |
175 | MWCryptRand::generateHex( 32 ) |
176 | ); |
177 | } |
178 | |
179 | /** |
180 | * Generate a new token (attached to this consumer), save it in the cache, and return it |
181 | * |
182 | * @param Consumer|OAuthConsumer $consumer |
183 | * @param string $callback |
184 | * @return MWOAuthToken |
185 | */ |
186 | public function new_request_token( $consumer, $callback = 'oob' ) { |
187 | $token = self::newToken(); |
188 | $cacheConsumerKey = Utils::getCacheKey( 'consumer', 'request', $token->key ); |
189 | $cacheTokenKey = Utils::getCacheKey( |
190 | 'token', $consumer->key, 'request', $token->key |
191 | ); |
192 | $cacheCallbackKey = Utils::getCacheKey( |
193 | 'callback', $consumer->key, 'request', $token->key |
194 | ); |
195 | |
196 | // 600s == 10 minutes. Kind of arbitrary. |
197 | $this->tokenCache->add( $cacheConsumerKey, $consumer->key, 600 ); |
198 | $this->tokenCache->add( $cacheTokenKey, $token, 600 ); |
199 | $this->tokenCache->add( $cacheCallbackKey, $callback, 600 ); |
200 | $this->logger->debug( __METHOD__ . |
201 | ": New request token {$token->key} for {$consumer->key} with callback {$callback}" ); |
202 | return $token; |
203 | } |
204 | |
205 | /** |
206 | * Return a consumer key associated with the given request token. |
207 | * |
208 | * @param string $requestToken |
209 | * @return string|false the consumer key or false if nothing is stored for the request token |
210 | */ |
211 | public function getConsumerKey( $requestToken ) { |
212 | $cacheKey = Utils::getCacheKey( 'consumer', 'request', $requestToken ); |
213 | return $this->tokenCache->get( $cacheKey ); |
214 | } |
215 | |
216 | /** |
217 | * Return a stored callback URL parameter given by the consumer in /initiate. |
218 | * It throws an exception if callback URL parameter does not exist in the cache. |
219 | * A stored callback URL parameter is deleted from the cache once read for the first |
220 | * time. |
221 | * |
222 | * @param string $consumerKey |
223 | * @param string $requestKey original request key from /initiate |
224 | * @throws MWOAuthException |
225 | * @return string|false the stored callback URL parameter |
226 | */ |
227 | public function getCallbackUrl( $consumerKey, $requestKey ) { |
228 | $cacheKey = Utils::getCacheKey( 'callback', $consumerKey, 'request', $requestKey ); |
229 | $callback = $this->tokenCache->get( $cacheKey ); |
230 | if ( $callback === null || !is_string( $callback ) ) { |
231 | throw new MWOAuthException( 'mwoauthdatastore-callback-not-found', [ |
232 | 'consumer' => $consumerKey, |
233 | ] ); |
234 | } |
235 | $this->tokenCache->delete( $cacheKey ); |
236 | return $callback; |
237 | } |
238 | |
239 | /** |
240 | * Return a new access token attached to this consumer for the user associated with this |
241 | * token if the request token is authorized. Should also invalidate the request token. |
242 | * |
243 | * @param MWOAuthToken $token the request token that started this |
244 | * @param Consumer $consumer |
245 | * @param int|null $verifier |
246 | * @throws MWOAuthException |
247 | * @return MWOAuthToken the access token |
248 | */ |
249 | public function new_access_token( $token, $consumer, $verifier = null ) { |
250 | $this->logger->debug( __METHOD__ . |
251 | ": Getting new access token for token {$token->key}, consumer {$consumer->key}" ); |
252 | |
253 | if ( !$token->getVerifyCode() || !$token->getAccessKey() ) { |
254 | throw new MWOAuthException( 'mwoauthdatastore-bad-token', [ |
255 | 'consumer' => $consumer->getConsumerKey(), |
256 | 'consumer_name' => $consumer->getName(), |
257 | 'token' => $token->key, |
258 | ] ); |
259 | } elseif ( $token->getVerifyCode() !== $verifier ) { |
260 | throw new MWOAuthException( 'mwoauthdatastore-bad-verifier', [ |
261 | 'consumer' => $consumer->getConsumerKey(), |
262 | 'consumer_name' => $consumer->getName(), |
263 | 'token' => $token->key, |
264 | ] ); |
265 | } |
266 | |
267 | $cacheKey = Utils::getCacheKey( 'token', |
268 | $consumer->getConsumerKey(), 'request', $token->key ); |
269 | $accessToken = $this->lookup_token( $consumer, 'access', $token->getAccessKey() ); |
270 | $this->tokenCache->set( $cacheKey, '**USED**', 600 ); |
271 | $this->logger->debug( __METHOD__ . |
272 | ": New access token {$accessToken->key} for {$consumer->key}" ); |
273 | return $accessToken; |
274 | } |
275 | |
276 | /** |
277 | * Update a request token. The token probably already exists, but had another attribute added. |
278 | * |
279 | * @param MWOAuthToken $token the token to store |
280 | * @param Consumer|OAuthConsumer $consumer |
281 | */ |
282 | public function updateRequestToken( $token, $consumer ) { |
283 | $cacheKey = Utils::getCacheKey( 'token', $consumer->key, 'request', $token->key ); |
284 | // 10 more minutes. Kind of arbitrary. |
285 | $this->tokenCache->set( $cacheKey, $token, 600 ); |
286 | } |
287 | |
288 | /** |
289 | * Return the string representing the Consumer's public RSA key |
290 | * |
291 | * @param string $consumerKey the string value of the Consumer's key |
292 | * @return string|null |
293 | */ |
294 | public function getRSAKey( $consumerKey ) { |
295 | $cmr = Consumer::newFromKey( $this->centralReplica, $consumerKey ); |
296 | return $cmr ? $cmr->getRsaKey() : null; |
297 | } |
298 | } |