Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
33.57% |
48 / 143 |
|
27.27% |
3 / 11 |
CRAP | |
0.00% |
0 / 1 |
MWOAuthServer | |
33.57% |
48 / 143 |
|
27.27% |
3 / 11 |
394.17 | |
0.00% |
0 / 1 |
getConsumerKey | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
fetch_request_token | |
0.00% |
0 / 33 |
|
0.00% |
0 / 1 |
20 | |||
checkCallback | |
79.07% |
34 / 43 |
|
0.00% |
0 / 1 |
13.32 | |||
looseSchemeMatch | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getOrNull | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
componentMatches | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
4 | |||
fetch_access_token | |
0.00% |
0 / 37 |
|
0.00% |
0 / 1 |
30 | |||
verify_request | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
checkSourceIP | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
authorize | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getCurrentAuthorization | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\OAuth\Backend; |
4 | |
5 | use MediaWiki\Extension\OAuth\Lib\OAuthServer; |
6 | use MediaWiki\Linker\Linker; |
7 | use MediaWiki\SpecialPage\SpecialPage; |
8 | use MediaWiki\User\User; |
9 | use Message; |
10 | |
11 | class MWOAuthServer extends OAuthServer { |
12 | /** @var MWOAuthDataStore */ |
13 | protected $data_store; |
14 | |
15 | /** |
16 | * Return a consumer key associated with the given request token. |
17 | * |
18 | * @param string $requestToken |
19 | * @return string|false the consumer key or false if nothing is stored for the request token |
20 | */ |
21 | public function getConsumerKey( $requestToken ) { |
22 | return $this->data_store->getConsumerKey( $requestToken ); |
23 | } |
24 | |
25 | /** |
26 | * Process a request_token request returns the request token on success. This |
27 | * also checks the IP restriction, which the OAuthServer method did not. |
28 | * |
29 | * @param MWOAuthRequest &$request |
30 | * @return MWOAuthToken |
31 | * @throws MWOAuthException |
32 | */ |
33 | public function fetch_request_token( &$request ) { |
34 | $this->get_version( $request ); |
35 | |
36 | /** @var Consumer $consumer */ |
37 | $consumer = $this->get_consumer( $request ); |
38 | |
39 | // Consumer must not be owner-only |
40 | if ( $consumer->getOwnerOnly() ) { |
41 | throw new MWOAuthException( 'mwoauthserver-consumer-owner-only', [ |
42 | 'consumer_name' => $consumer->getName(), |
43 | 'update_url' => SpecialPage::getTitleFor( |
44 | 'OAuthConsumerRegistration', 'update/' . $consumer->getConsumerKey() |
45 | ), |
46 | Message::rawParam( Linker::makeExternalLink( |
47 | 'https://www.mediawiki.org/wiki/Help:OAuth/Errors#E010', |
48 | 'E010', |
49 | true |
50 | ) ), |
51 | 'consumer' => $consumer->getConsumerKey(), |
52 | ] ); |
53 | } |
54 | |
55 | // Consumer must have a key for us to verify |
56 | if ( !$consumer->getSecretKey() && !$consumer->getRsaKey() ) { |
57 | throw new MWOAuthException( 'mwoauthserver-consumer-no-secret', [ |
58 | Message::rawParam( Linker::makeExternalLink( |
59 | 'https://www.mediawiki.org/wiki/Help:OAuth/Errors#E011', |
60 | 'E011', |
61 | true |
62 | ) ), |
63 | 'consumer' => $consumer->getConsumerKey(), |
64 | 'consumer_name' => $consumer->getName(), |
65 | ] ); |
66 | } |
67 | |
68 | $this->checkSourceIP( $consumer, $request ); |
69 | |
70 | // no token required for the initial token request |
71 | $token = null; |
72 | |
73 | $this->check_signature( $request, $consumer, $token ); |
74 | |
75 | $callback = $request->get_parameter( 'oauth_callback' ); |
76 | |
77 | $this->checkCallback( $consumer, $callback ); |
78 | |
79 | $new_token = $this->data_store->new_request_token( $consumer, $callback ); |
80 | $new_token->oauth_callback_confirmed = 'true'; |
81 | return $new_token; |
82 | } |
83 | |
84 | /** |
85 | * Ensure the callback is "oob" or that the registered callback is a valid |
86 | * prefix of the supplied callback. It throws an exception if callback is |
87 | * invalid. |
88 | * |
89 | * In MediaWiki, we require the callback to be established at |
90 | * registration. OAuth 1.0a (rfc5849, section 2.1) specifies that |
91 | * oauth_callback is required for the temporary credentials, and "If the |
92 | * client is unable to receive callbacks or a callback URI has been |
93 | * established via other means, the parameter value MUST be set to "oob" |
94 | * (case sensitive), to indicate an out-of-band configuration." Otherwise, |
95 | * client can provide a callback and the configured callback must be |
96 | * a prefix of the supplied callback. The matching performed here is based |
97 | * on parsed URL components rather than strict string matching. Protocol |
98 | * upgrades from http to https are also allowed, and the registered callback |
99 | * can be made to match any port number, by specifying port 1. (This is |
100 | * less secure, and only meant for demo consumers for local development.) |
101 | * |
102 | * @param Consumer $consumer |
103 | * @param string $callback |
104 | * @return void |
105 | * @throws MWOAuthException |
106 | */ |
107 | private function checkCallback( $consumer, $callback ) { |
108 | if ( !$consumer->getCallbackIsPrefix() ) { |
109 | if ( $callback !== 'oob' ) { |
110 | throw new MWOAuthException( 'mwoauth-callback-not-oob', [ |
111 | 'consumer' => $consumer->getConsumerKey(), |
112 | 'consumer_name' => $consumer->getName(), |
113 | 'callback_url' => $callback, |
114 | ] ); |
115 | } |
116 | |
117 | return; |
118 | } |
119 | |
120 | if ( !$callback ) { |
121 | throw new MWOAuthException( 'mwoauth-callback-not-oob-or-prefix', [ |
122 | 'consumer' => $consumer->getConsumerKey(), |
123 | 'consumer_name' => $consumer->getName(), |
124 | ] ); |
125 | } |
126 | if ( $callback === 'oob' ) { |
127 | return; |
128 | } |
129 | |
130 | $reqCallback = wfParseUrl( $callback ); |
131 | if ( $reqCallback === false ) { |
132 | throw new MWOAuthException( 'mwoauth-callback-not-oob-or-prefix', [ |
133 | 'consumer' => $consumer->getConsumerKey(), |
134 | 'consumer_name' => $consumer->getName(), |
135 | 'callback_url' => $callback, |
136 | ] ); |
137 | } |
138 | |
139 | $knownCallback = wfParseUrl( $consumer->getCallbackUrl() ); |
140 | $exactPath = array_key_exists( 'query', $knownCallback ); |
141 | |
142 | $match = |
143 | // Protocol can be upgraded from http to https |
144 | self::looseSchemeMatch( $knownCallback['scheme'], $reqCallback['scheme'] ) && |
145 | // Host must match exactly |
146 | $knownCallback['host'] === $reqCallback['host'] && |
147 | // Port must be either missing from both or an exact match, |
148 | // unless the registered callback allows any port, which is specified |
149 | // by using port 1. |
150 | ( static::getOrNull( 'port', $knownCallback ) === 1 || |
151 | static::getOrNull( 'port', $knownCallback ) === |
152 | static::getOrNull( 'port', $reqCallback ) |
153 | ) && |
154 | // Path must be an exact match if query is provided in the |
155 | // registered callback. Otherwise it must be a prefix match if |
156 | // provided in the registered callback or anything if no path was |
157 | // included in the registered callback at all. |
158 | static::componentMatches( 'path', $knownCallback, $reqCallback, $exactPath ) && |
159 | // Query string must be aprefix match if provided in the |
160 | // registered callback. |
161 | static::componentMatches( 'query', $knownCallback, $reqCallback ); |
162 | |
163 | if ( !$match ) { |
164 | throw new MWOAuthException( 'mwoauth-callback-not-oob-or-prefix', [ |
165 | 'consumer' => $consumer->getConsumerKey(), |
166 | 'consumer_name' => $consumer->getName(), |
167 | 'callback_url' => $callback, |
168 | 'consumer_callback_prefix' => $consumer->getCallbackUrl(), |
169 | ] ); |
170 | } |
171 | } |
172 | |
173 | /** |
174 | * Compare URL schemes for a match. |
175 | * |
176 | * Allows 'https' to match an expected 'http' value. |
177 | * |
178 | * @param string $want |
179 | * @param string $got |
180 | * @return bool |
181 | */ |
182 | private static function looseSchemeMatch( $want, $got ) { |
183 | if ( $want === 'http' ) { |
184 | return in_array( $got, [ 'http', 'https' ], true ); |
185 | } else { |
186 | return $want === $got; |
187 | } |
188 | } |
189 | |
190 | /** |
191 | * Get a named value from an array or return null if the key does not |
192 | * exist. |
193 | * |
194 | * @param string $key |
195 | * @param array $arr |
196 | * @return mixed |
197 | */ |
198 | private static function getOrNull( $key, $arr ) { |
199 | return array_key_exists( $key, $arr ) ? $arr[$key] : null; |
200 | } |
201 | |
202 | /** |
203 | * Check that a callback URL component matches the expected value. |
204 | * |
205 | * @param string $part URL component name |
206 | * @param array $expect Expected URL components |
207 | * @param array $got Posted URl components |
208 | * @param bool $exact Perform exact match instead of prefix match |
209 | * @return bool |
210 | */ |
211 | private static function componentMatches( |
212 | $part, $expect, $got, $exact = false |
213 | ) { |
214 | if ( !array_key_exists( $part, $expect ) ) { |
215 | // Anything in the request is ok if we do not have the URL part in |
216 | // the expected values |
217 | $match = true; |
218 | } elseif ( !array_key_exists( $part, $got ) ) { |
219 | $match = false; |
220 | } elseif ( $exact ) { |
221 | $match = $expect[$part] === $got[$part]; |
222 | } else { |
223 | $want = (string)$expect[$part]; |
224 | $have = (string)$got[$part]; |
225 | $match = strpos( $have, $want ) === 0; |
226 | } |
227 | return $match; |
228 | } |
229 | |
230 | /** |
231 | * process an access_token request |
232 | * returns the access token on success |
233 | * |
234 | * @param MWOAuthRequest &$request |
235 | * @return MWOAuthToken |
236 | * @throws MWOAuthException |
237 | */ |
238 | public function fetch_access_token( &$request ) { |
239 | $this->get_version( $request ); |
240 | |
241 | /** @var Consumer $consumer */ |
242 | $consumer = $this->get_consumer( $request ); |
243 | |
244 | // Consumer must not be owner-only |
245 | if ( $consumer->getOwnerOnly() ) { |
246 | throw new MWOAuthException( 'mwoauthserver-consumer-owner-only', [ |
247 | 'consumer_name' => $consumer->getName(), |
248 | 'update_url' => SpecialPage::getTitleFor( |
249 | 'OAuthConsumerRegistration', 'update/' . $consumer->getConsumerKey() |
250 | ), |
251 | Message::rawParam( Linker::makeExternalLink( |
252 | 'https://www.mediawiki.org/wiki/Help:OAuth/Errors#E010', |
253 | 'E010', |
254 | true |
255 | ) ), |
256 | 'consumer' => $consumer->getConsumerKey(), |
257 | ] ); |
258 | } |
259 | |
260 | // Consumer must have a key for us to verify |
261 | if ( !$consumer->getSecretKey() && !$consumer->getRsaKey() ) { |
262 | throw new MWOAuthException( 'mwoauthserver-consumer-no-secret', [ |
263 | Message::rawParam( Linker::makeExternalLink( |
264 | 'https://www.mediawiki.org/wiki/Help:OAuth/Errors#E011', |
265 | 'E011', |
266 | true |
267 | ) ), |
268 | 'consumer' => $consumer->getConsumerKey(), |
269 | 'consumer_name' => $consumer->getName(), |
270 | ] ); |
271 | } |
272 | |
273 | $this->checkSourceIP( $consumer, $request ); |
274 | |
275 | // requires authorized request token |
276 | /** @var MWOAuthToken $token */ |
277 | $token = $this->get_token( $request, $consumer, 'request' ); |
278 | |
279 | if ( !$token->secret ) { |
280 | // This token has a blank secret.. something is wrong |
281 | throw new MWOAuthException( 'mwoauthdatastore-bad-token', [ |
282 | 'consumer' => $consumer->getConsumerKey(), |
283 | 'consumer_name' => $consumer->getName(), |
284 | 'token' => $token->key, |
285 | ] ); |
286 | } |
287 | |
288 | $this->check_signature( $request, $consumer, $token ); |
289 | |
290 | // Rev A change |
291 | $verifier = $request->get_parameter( 'oauth_verifier' ); |
292 | $this->logger->debug( __METHOD__ . ": verify code is '$verifier'" ); |
293 | return $this->data_store->new_access_token( $token, $consumer, $verifier ); |
294 | } |
295 | |
296 | /** |
297 | * Wrap the call to the parent function and check that the source IP of |
298 | * the request is allowed by this consumer's restrictions. |
299 | * @param MWOAuthRequest &$request |
300 | * @return array |
301 | */ |
302 | public function verify_request( &$request ) { |
303 | [ $consumer, $token ] = parent::verify_request( $request ); |
304 | $this->checkSourceIP( $consumer, $request ); |
305 | return [ $consumer, $token ]; |
306 | } |
307 | |
308 | /** |
309 | * Ensure the request comes from an approved IP address, if IP restriction has been |
310 | * setup by the Consumer. It throws an exception if IP address is invalid. |
311 | * |
312 | * @param Consumer $consumer |
313 | * @param MWOAuthRequest $request |
314 | * @throws MWOAuthException |
315 | */ |
316 | private function checkSourceIP( $consumer, $request ) { |
317 | $restrictions = $consumer->getRestrictions(); |
318 | if ( !$restrictions->checkIP( $request->getSourceIP() ) ) { |
319 | throw new MWOAuthException( 'mwoauthdatastore-bad-source-ip', [ |
320 | 'consumer' => $consumer->getConsumerKey(), |
321 | 'consumer_name' => $consumer->getName(), |
322 | 'request_ip' => $request->getSourceIP(), |
323 | ] ); |
324 | } |
325 | } |
326 | |
327 | /** |
328 | * @deprecated User MWOAuthConsumer::authorize(...) |
329 | * |
330 | * @param string $consumerKey |
331 | * @param string $requestTokenKey |
332 | * @param User $mwUser |
333 | * @param bool $update |
334 | * @return string |
335 | */ |
336 | public function authorize( $consumerKey, $requestTokenKey, User $mwUser, $update ) { |
337 | $dbr = Utils::getCentralDB( DB_REPLICA ); |
338 | $consumer = Consumer::newFromKey( $dbr, $consumerKey ); |
339 | return $consumer->authorize( $mwUser, $update, $consumer->getGrants(), $requestTokenKey ); |
340 | } |
341 | |
342 | /** |
343 | * Attempts to find an authorization by this user for this consumer. Since a user can |
344 | * accept a consumer multiple times (once for "*" and once for each specific wiki), |
345 | * there can several access tokens per-wiki (with varying grants) for a consumer. |
346 | * This will choose the most wiki-specific access token. The precedence is: |
347 | * a) The acceptance for wiki X if the consumer is applicable only to wiki X |
348 | * b) The acceptance for wiki $wikiId (if the consumer is applicable to it) |
349 | * c) The acceptance for wikis "*" (all wikis) |
350 | * |
351 | * Users might want more grants on some wikis than on "*". Note that the reverse would not |
352 | * make sense, since the consumer could just use the "*" acceptance if it has more grants. |
353 | * |
354 | * @param User $mwUser (local wiki user) User who may or may not have authorizations |
355 | * @param Consumer $consumer |
356 | * @param string $wikiId |
357 | * @throws MWOAuthException |
358 | * @return ConsumerAcceptance |
359 | * @deprecated Use MWOAuthConsumer::getCurrentAuthorization(...) |
360 | */ |
361 | public function getCurrentAuthorization( User $mwUser, $consumer, $wikiId ) { |
362 | wfDeprecated( __METHOD__ ); |
363 | return $consumer->getCurrentAuthorization( $mwUser, $wikiId ); |
364 | } |
365 | } |