Merge ~cjwatson/launchpad:oauthlib into launchpad:master
- Git
- lp:~cjwatson/launchpad
- oauthlib
- Merge into master
Proposed by
Colin Watson
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Colin Watson | ||||
Approved revision: | bc855e779a25db270488e87c029533772e2d95ed | ||||
Merge reported by: | Otto Co-Pilot | ||||
Merged at revision: | not available | ||||
Proposed branch: | ~cjwatson/launchpad:oauthlib | ||||
Merge into: | launchpad:master | ||||
Diff against target: |
750 lines (+44/-587) 6 files modified
dev/null (+0/-536) lib/lp/services/webapp/authentication.py (+12/-5) lib/lp/services/webapp/tests/test_authentication.py (+10/-8) lib/lp/services/webapp/tests/test_publication.py (+11/-16) lib/lp/testing/pages.py (+10/-22) setup.py (+1/-0) |
||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Tom Wardill (community) | Approve | ||
Review via email: mp+393828@code.launchpad.net |
Commit message
Port from contrib.oauth to oauthlib
Description of the change
Let's prefer code that somebody else is maintaining.
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/lib/contrib/oauth.py b/lib/contrib/oauth.py |
2 | deleted file mode 100644 |
3 | index 2d11e6c..0000000 |
4 | --- a/lib/contrib/oauth.py |
5 | +++ /dev/null |
6 | @@ -1,536 +0,0 @@ |
7 | -import base64 |
8 | -import hmac |
9 | -import random |
10 | -import time |
11 | - |
12 | -import six |
13 | -from six.moves.urllib.parse import ( |
14 | - parse_qs, |
15 | - quote, |
16 | - unquote, |
17 | - urlencode, |
18 | - urlparse, |
19 | - ) |
20 | - |
21 | - |
22 | -VERSION = '1.0' # Hi Blaine! |
23 | -HTTP_METHOD = 'GET' |
24 | -SIGNATURE_METHOD = 'PLAINTEXT' |
25 | - |
26 | -# Generic exception class |
27 | -class OAuthError(RuntimeError): |
28 | - def __init__(self, message='OAuth error occured'): |
29 | - self.message = message |
30 | - |
31 | -# optional WWW-Authenticate header (401 error) |
32 | -def build_authenticate_header(realm=''): |
33 | - return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} |
34 | - |
35 | -# url escape |
36 | -def escape(s): |
37 | - # escape '/' too |
38 | - return quote(s, safe='~') |
39 | - |
40 | -# util function: current timestamp |
41 | -# seconds since epoch (UTC) |
42 | -def generate_timestamp(): |
43 | - return int(time.time()) |
44 | - |
45 | -# util function: nonce |
46 | -# pseudorandom number |
47 | -def generate_nonce(length=8): |
48 | - return ''.join(str(random.randint(0, 9)) for i in range(length)) |
49 | - |
50 | -# OAuthConsumer is a data type that represents the identity of the Consumer |
51 | -# via its shared secret with the Service Provider. |
52 | -class OAuthConsumer(object): |
53 | - key = None |
54 | - secret = None |
55 | - |
56 | - def __init__(self, key, secret): |
57 | - self.key = key |
58 | - self.secret = secret |
59 | - |
60 | -# OAuthToken is a data type that represents an End User via either an access |
61 | -# or request token. |
62 | -class OAuthToken(object): |
63 | - # access tokens and request tokens |
64 | - key = None |
65 | - secret = None |
66 | - |
67 | - ''' |
68 | - key = the token |
69 | - secret = the token secret |
70 | - ''' |
71 | - def __init__(self, key, secret): |
72 | - self.key = key |
73 | - self.secret = secret |
74 | - |
75 | - def to_string(self): |
76 | - return urlencode({'oauth_token': self.key, 'oauth_token_secret': self.secret}) |
77 | - |
78 | - # return a token from something like: |
79 | - # oauth_token_secret=digg&oauth_token=digg |
80 | - @staticmethod |
81 | - def from_string(s): |
82 | - params = parse_qs(s, keep_blank_values=False) |
83 | - key = params['oauth_token'][0] |
84 | - secret = params['oauth_token_secret'][0] |
85 | - return OAuthToken(key, secret) |
86 | - |
87 | - def __str__(self): |
88 | - return self.to_string() |
89 | - |
90 | -# OAuthRequest represents the request and can be serialized |
91 | -class OAuthRequest(object): |
92 | - ''' |
93 | - OAuth parameters: |
94 | - - oauth_consumer_key |
95 | - - oauth_token |
96 | - - oauth_signature_method |
97 | - - oauth_signature |
98 | - - oauth_timestamp |
99 | - - oauth_nonce |
100 | - - oauth_version |
101 | - ... any additional parameters, as defined by the Service Provider. |
102 | - ''' |
103 | - parameters = None # oauth parameters |
104 | - http_method = HTTP_METHOD |
105 | - http_url = None |
106 | - version = VERSION |
107 | - |
108 | - def __init__(self, http_method=HTTP_METHOD, http_url=None, parameters=None): |
109 | - self.http_method = http_method |
110 | - self.http_url = http_url |
111 | - self.parameters = parameters or {} |
112 | - |
113 | - def set_parameter(self, parameter, value): |
114 | - self.parameters[parameter] = value |
115 | - |
116 | - def get_parameter(self, parameter): |
117 | - try: |
118 | - return self.parameters[parameter] |
119 | - except: |
120 | - raise OAuthError('Parameter not found: %s' % parameter) |
121 | - |
122 | - def _get_timestamp_nonce(self): |
123 | - return self.get_parameter('oauth_timestamp'), self.get_parameter('oauth_nonce') |
124 | - |
125 | - # get any non-oauth parameters |
126 | - def get_nonoauth_parameters(self): |
127 | - parameters = {} |
128 | - for k, v in six.iteritems(self.parameters): |
129 | - # ignore oauth parameters |
130 | - if k.find('oauth_') < 0: |
131 | - parameters[k] = v |
132 | - return parameters |
133 | - |
134 | - # serialize as a header for an HTTPAuth request |
135 | - def to_header(self, realm=''): |
136 | - auth_header = 'OAuth realm="%s"' % realm |
137 | - # add the oauth parameters |
138 | - if self.parameters: |
139 | - for k, v in six.iteritems(self.parameters): |
140 | - auth_header += ', %s="%s"' % (k, v) |
141 | - return {'Authorization': auth_header} |
142 | - |
143 | - # serialize as post data for a POST request |
144 | - def to_postdata(self): |
145 | - return '&'.join( |
146 | - '%s=%s' % (escape(str(k)), escape(str(v))) |
147 | - for k, v in six.iteritems(self.parameters)) |
148 | - |
149 | - # serialize as a url for a GET request |
150 | - def to_url(self): |
151 | - return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata()) |
152 | - |
153 | - # return a string that consists of all the parameters that need to be signed |
154 | - def get_normalized_parameters(self): |
155 | - params = self.parameters |
156 | - try: |
157 | - # exclude the signature if it exists |
158 | - del params['oauth_signature'] |
159 | - except: |
160 | - pass |
161 | - key_values = params.items() |
162 | - # sort lexicographically, first after key, then after value |
163 | - key_values.sort() |
164 | - # combine key value pairs in string and escape |
165 | - return '&'.join('%s=%s' % (escape(str(k)), escape(str(v))) for k, v in key_values) |
166 | - |
167 | - # just uppercases the http method |
168 | - def get_normalized_http_method(self): |
169 | - return self.http_method.upper() |
170 | - |
171 | - # parses the url and rebuilds it to be scheme://host/path |
172 | - def get_normalized_http_url(self): |
173 | - parts = urlparse(self.http_url) |
174 | - url_string = '%s://%s%s' % (parts[0], parts[1], parts[2]) # scheme, netloc, path |
175 | - return url_string |
176 | - |
177 | - # set the signature parameter to the result of build_signature |
178 | - def sign_request(self, signature_method, consumer, token): |
179 | - # set the signature method |
180 | - self.set_parameter('oauth_signature_method', signature_method.get_name()) |
181 | - # set the signature |
182 | - self.set_parameter('oauth_signature', self.build_signature(signature_method, consumer, token)) |
183 | - |
184 | - def build_signature(self, signature_method, consumer, token): |
185 | - # call the build signature method within the signature method |
186 | - return signature_method.build_signature(self, consumer, token) |
187 | - |
188 | - @staticmethod |
189 | - def from_request(http_method, http_url, headers=None, postdata=None, parameters=None): |
190 | - |
191 | - # let the library user override things however they'd like, if they know |
192 | - # which parameters to use then go for it, for example XMLRPC might want to |
193 | - # do this |
194 | - if parameters is not None: |
195 | - return OAuthRequest(http_method, http_url, parameters) |
196 | - |
197 | - # from the headers |
198 | - if headers is not None: |
199 | - try: |
200 | - auth_header = headers['Authorization'] |
201 | - # check that the authorization header is OAuth |
202 | - auth_header.index('OAuth') |
203 | - # get the parameters from the header |
204 | - parameters = OAuthRequest._split_header(auth_header) |
205 | - return OAuthRequest(http_method, http_url, parameters) |
206 | - except: |
207 | - raise OAuthError('Unable to parse OAuth parameters from Authorization header.') |
208 | - |
209 | - # from the parameter string (post body) |
210 | - if http_method == 'POST' and postdata is not None: |
211 | - parameters = OAuthRequest._split_url_string(postdata) |
212 | - |
213 | - # from the url string |
214 | - elif http_method == 'GET': |
215 | - param_str = urlparse(http_url).query |
216 | - parameters = OAuthRequest._split_url_string(param_str) |
217 | - |
218 | - if parameters: |
219 | - return OAuthRequest(http_method, http_url, parameters) |
220 | - |
221 | - raise OAuthError('Missing all OAuth parameters. OAuth parameters must be in the headers, post body, or url.') |
222 | - |
223 | - @staticmethod |
224 | - def from_consumer_and_token(oauth_consumer, token=None, http_method=HTTP_METHOD, http_url=None, parameters=None): |
225 | - if not parameters: |
226 | - parameters = {} |
227 | - |
228 | - defaults = { |
229 | - 'oauth_consumer_key': oauth_consumer.key, |
230 | - 'oauth_timestamp': generate_timestamp(), |
231 | - 'oauth_nonce': generate_nonce(), |
232 | - 'oauth_version': OAuthRequest.version, |
233 | - } |
234 | - |
235 | - defaults.update(parameters) |
236 | - parameters = defaults |
237 | - |
238 | - if token: |
239 | - parameters['oauth_token'] = token.key |
240 | - |
241 | - return OAuthRequest(http_method, http_url, parameters) |
242 | - |
243 | - @staticmethod |
244 | - def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD, http_url=None, parameters=None): |
245 | - if not parameters: |
246 | - parameters = {} |
247 | - |
248 | - parameters['oauth_token'] = token.key |
249 | - |
250 | - if callback: |
251 | - parameters['oauth_callback'] = escape(callback) |
252 | - |
253 | - return OAuthRequest(http_method, http_url, parameters) |
254 | - |
255 | - # util function: turn Authorization: header into parameters, has to do some unescaping |
256 | - @staticmethod |
257 | - def _split_header(header): |
258 | - params = {} |
259 | - header = header.lstrip() |
260 | - if not header.startswith('OAuth '): |
261 | - raise ValueError("not an OAuth header: %r" % header) |
262 | - header = header[6:] |
263 | - parts = header.split(',') |
264 | - for param in parts: |
265 | - # remove whitespace |
266 | - param = param.strip() |
267 | - # split key-value |
268 | - param_parts = param.split('=', 1) |
269 | - if param_parts[0] == 'realm': |
270 | - # Realm header is not an OAuth parameter according to rfc5849 |
271 | - # section 3.4.1.3.1. |
272 | - continue |
273 | - # remove quotes and unescape the value |
274 | - params[param_parts[0]] = unquote(param_parts[1].strip('\"')) |
275 | - return params |
276 | - |
277 | - # util function: turn url string into parameters, has to do some unescaping |
278 | - @staticmethod |
279 | - def _split_url_string(param_str): |
280 | - parameters = parse_qs(param_str, keep_blank_values=False) |
281 | - for k, v in six.iteritems(parameters): |
282 | - parameters[k] = unquote(v[0]) |
283 | - return parameters |
284 | - |
285 | -# OAuthServer is a worker to check a requests validity against a data store |
286 | -class OAuthServer(object): |
287 | - timestamp_threshold = 300 # in seconds, five minutes |
288 | - version = VERSION |
289 | - signature_methods = None |
290 | - data_store = None |
291 | - |
292 | - def __init__(self, data_store=None, signature_methods=None): |
293 | - self.data_store = data_store |
294 | - self.signature_methods = signature_methods or {} |
295 | - |
296 | - def set_data_store(self, oauth_data_store): |
297 | - self.data_store = oauth_data_store |
298 | - |
299 | - def get_data_store(self): |
300 | - return self.data_store |
301 | - |
302 | - def add_signature_method(self, signature_method): |
303 | - self.signature_methods[signature_method.get_name()] = signature_method |
304 | - return self.signature_methods |
305 | - |
306 | - # process a request_token request |
307 | - # returns the request token on success |
308 | - def fetch_request_token(self, oauth_request): |
309 | - try: |
310 | - # get the request token for authorization |
311 | - token = self._get_token(oauth_request, 'request') |
312 | - except: |
313 | - # no token required for the initial token request |
314 | - version = self._get_version(oauth_request) |
315 | - consumer = self._get_consumer(oauth_request) |
316 | - self._check_signature(oauth_request, consumer, None) |
317 | - # fetch a new token |
318 | - token = self.data_store.fetch_request_token(consumer) |
319 | - return token |
320 | - |
321 | - # process an access_token request |
322 | - # returns the access token on success |
323 | - def fetch_access_token(self, oauth_request): |
324 | - version = self._get_version(oauth_request) |
325 | - consumer = self._get_consumer(oauth_request) |
326 | - # get the request token |
327 | - token = self._get_token(oauth_request, 'request') |
328 | - self._check_signature(oauth_request, consumer, token) |
329 | - new_token = self.data_store.fetch_access_token(consumer, token) |
330 | - return new_token |
331 | - |
332 | - # verify an api call, checks all the parameters |
333 | - def verify_request(self, oauth_request): |
334 | - # -> consumer and token |
335 | - version = self._get_version(oauth_request) |
336 | - consumer = self._get_consumer(oauth_request) |
337 | - # get the access token |
338 | - token = self._get_token(oauth_request, 'access') |
339 | - self._check_signature(oauth_request, consumer, token) |
340 | - parameters = oauth_request.get_nonoauth_parameters() |
341 | - return consumer, token, parameters |
342 | - |
343 | - # authorize a request token |
344 | - def authorize_token(self, token, user): |
345 | - return self.data_store.authorize_request_token(token, user) |
346 | - |
347 | - # get the callback url |
348 | - def get_callback(self, oauth_request): |
349 | - return oauth_request.get_parameter('oauth_callback') |
350 | - |
351 | - # optional support for the authenticate header |
352 | - def build_authenticate_header(self, realm=''): |
353 | - return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} |
354 | - |
355 | - # verify the correct version request for this server |
356 | - def _get_version(self, oauth_request): |
357 | - try: |
358 | - version = oauth_request.get_parameter('oauth_version') |
359 | - except: |
360 | - version = VERSION |
361 | - if version and version != self.version: |
362 | - raise OAuthError('OAuth version %s not supported' % str(version)) |
363 | - return version |
364 | - |
365 | - # figure out the signature with some defaults |
366 | - def _get_signature_method(self, oauth_request): |
367 | - try: |
368 | - signature_method = oauth_request.get_parameter('oauth_signature_method') |
369 | - except: |
370 | - signature_method = SIGNATURE_METHOD |
371 | - try: |
372 | - # get the signature method object |
373 | - signature_method = self.signature_methods[signature_method] |
374 | - except: |
375 | - signature_method_names = ', '.join(self.signature_methods.keys()) |
376 | - raise OAuthError('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names)) |
377 | - |
378 | - return signature_method |
379 | - |
380 | - def _get_consumer(self, oauth_request): |
381 | - consumer_key = oauth_request.get_parameter('oauth_consumer_key') |
382 | - if not consumer_key: |
383 | - raise OAuthError('Invalid consumer key') |
384 | - consumer = self.data_store.lookup_consumer(consumer_key) |
385 | - if not consumer: |
386 | - raise OAuthError('Invalid consumer') |
387 | - return consumer |
388 | - |
389 | - # try to find the token for the provided request token key |
390 | - def _get_token(self, oauth_request, token_type='access'): |
391 | - token_field = oauth_request.get_parameter('oauth_token') |
392 | - token = self.data_store.lookup_token(token_type, token_field) |
393 | - if not token: |
394 | - raise OAuthError('Invalid %s token: %s' % (token_type, token_field)) |
395 | - return token |
396 | - |
397 | - def _check_signature(self, oauth_request, consumer, token): |
398 | - timestamp, nonce = oauth_request._get_timestamp_nonce() |
399 | - self._check_timestamp(timestamp) |
400 | - self._check_nonce(consumer, token, nonce) |
401 | - signature_method = self._get_signature_method(oauth_request) |
402 | - try: |
403 | - signature = oauth_request.get_parameter('oauth_signature') |
404 | - except: |
405 | - raise OAuthError('Missing signature') |
406 | - # attempt to construct the same signature |
407 | - built = signature_method.build_signature(oauth_request, consumer, token) |
408 | - if signature != built: |
409 | - key, base = signature_method.build_signature_base_string(oauth_request, consumer, token) |
410 | - raise OAuthError('Signature does not match. Expected: %s Got: %s Expected signature base string: %s' % (built, signature, base)) |
411 | - |
412 | - def _check_timestamp(self, timestamp): |
413 | - # verify that timestamp is recentish |
414 | - timestamp = int(timestamp) |
415 | - now = int(time.time()) |
416 | - lapsed = now - timestamp |
417 | - if lapsed > self.timestamp_threshold: |
418 | - raise OAuthError('Expired timestamp: given %d and now %s has a greater difference than threshold %d' % (timestamp, now, self.timestamp_threshold)) |
419 | - |
420 | - def _check_nonce(self, consumer, token, nonce): |
421 | - # verify that the nonce is uniqueish |
422 | - try: |
423 | - self.data_store.lookup_nonce(consumer, token, nonce) |
424 | - raise OAuthError('Nonce already used: %s' % str(nonce)) |
425 | - except: |
426 | - pass |
427 | - |
428 | -# OAuthClient is a worker to attempt to execute a request |
429 | -class OAuthClient(object): |
430 | - consumer = None |
431 | - token = None |
432 | - |
433 | - def __init__(self, oauth_consumer, oauth_token): |
434 | - self.consumer = oauth_consumer |
435 | - self.token = oauth_token |
436 | - |
437 | - def get_consumer(self): |
438 | - return self.consumer |
439 | - |
440 | - def get_token(self): |
441 | - return self.token |
442 | - |
443 | - def fetch_request_token(self, oauth_request): |
444 | - # -> OAuthToken |
445 | - raise NotImplementedError |
446 | - |
447 | - def fetch_access_token(self, oauth_request): |
448 | - # -> OAuthToken |
449 | - raise NotImplementedError |
450 | - |
451 | - def access_resource(self, oauth_request): |
452 | - # -> some protected resource |
453 | - raise NotImplementedError |
454 | - |
455 | -# OAuthDataStore is a database abstraction used to lookup consumers and tokens |
456 | -class OAuthDataStore(object): |
457 | - |
458 | - def lookup_consumer(self, key): |
459 | - # -> OAuthConsumer |
460 | - raise NotImplementedError |
461 | - |
462 | - def lookup_token(self, oauth_consumer, token_type, token_token): |
463 | - # -> OAuthToken |
464 | - raise NotImplementedError |
465 | - |
466 | - def lookup_nonce(self, oauth_consumer, oauth_token, nonce, timestamp): |
467 | - # -> OAuthToken |
468 | - raise NotImplementedError |
469 | - |
470 | - def fetch_request_token(self, oauth_consumer): |
471 | - # -> OAuthToken |
472 | - raise NotImplementedError |
473 | - |
474 | - def fetch_access_token(self, oauth_consumer, oauth_token): |
475 | - # -> OAuthToken |
476 | - raise NotImplementedError |
477 | - |
478 | - def authorize_request_token(self, oauth_token, user): |
479 | - # -> OAuthToken |
480 | - raise NotImplementedError |
481 | - |
482 | -# OAuthSignatureMethod is a strategy class that implements a signature method |
483 | -class OAuthSignatureMethod(object): |
484 | - def get_name(): |
485 | - # -> str |
486 | - raise NotImplementedError |
487 | - |
488 | - def build_signature_base_string(oauth_request, oauth_consumer, oauth_token): |
489 | - # -> str key, str raw |
490 | - raise NotImplementedError |
491 | - |
492 | - def build_signature(oauth_request, oauth_consumer, oauth_token): |
493 | - # -> str |
494 | - raise NotImplementedError |
495 | - |
496 | -class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod): |
497 | - |
498 | - def get_name(self): |
499 | - return 'HMAC-SHA1' |
500 | - |
501 | - def build_signature_base_string(self, oauth_request, consumer, token): |
502 | - sig = ( |
503 | - escape(oauth_request.get_normalized_http_method()), |
504 | - escape(oauth_request.get_normalized_http_url()), |
505 | - escape(oauth_request.get_normalized_parameters()), |
506 | - ) |
507 | - |
508 | - key = '%s&' % escape(consumer.secret) |
509 | - if token: |
510 | - key += escape(token.secret) |
511 | - raw = '&'.join(sig) |
512 | - return key, raw |
513 | - |
514 | - def build_signature(self, oauth_request, consumer, token): |
515 | - # build the base signature string |
516 | - key, raw = self.build_signature_base_string(oauth_request, consumer, token) |
517 | - |
518 | - # hmac object |
519 | - try: |
520 | - import hashlib # 2.5 |
521 | - hashed = hmac.new(key, raw, hashlib.sha1) |
522 | - except: |
523 | - import sha # deprecated |
524 | - hashed = hmac.new(key, raw, sha) |
525 | - |
526 | - # calculate the digest base 64 |
527 | - return base64.b64encode(hashed.digest()) |
528 | - |
529 | -class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod): |
530 | - |
531 | - def get_name(self): |
532 | - return 'PLAINTEXT' |
533 | - |
534 | - def build_signature_base_string(self, oauth_request, consumer, token): |
535 | - # concatenate the consumer key and secret |
536 | - sig = escape(consumer.secret) |
537 | - if token: |
538 | - sig = '&'.join((sig, escape(token.secret))) |
539 | - return sig |
540 | - |
541 | - def build_signature(self, oauth_request, consumer, token): |
542 | - return self.build_signature_base_string(oauth_request, consumer, token) |
543 | diff --git a/lib/lp/services/webapp/authentication.py b/lib/lp/services/webapp/authentication.py |
544 | index acab459..81a2384 100644 |
545 | --- a/lib/lp/services/webapp/authentication.py |
546 | +++ b/lib/lp/services/webapp/authentication.py |
547 | @@ -14,7 +14,8 @@ __all__ = [ |
548 | |
549 | import binascii |
550 | |
551 | -from contrib.oauth import OAuthRequest |
552 | +from oauthlib import oauth1 |
553 | +from oauthlib.oauth1.rfc5849.utils import parse_authorization_header |
554 | import six |
555 | from zope.authentication.interfaces import ILoginPassword |
556 | from zope.component import getUtility |
557 | @@ -274,6 +275,15 @@ class LaunchpadPrincipal: |
558 | return self.title |
559 | |
560 | |
561 | +def _parse_oauth_authorization_header(header): |
562 | + # http://oauth.net/core/1.0/#encoding_parameters says "Text names |
563 | + # and values MUST be encoded as UTF-8 octets before percent-encoding |
564 | + # them", so we can reasonably fail if this hasn't been done. |
565 | + return dict(oauth1.rfc5849.signature.collect_parameters( |
566 | + headers={"Authorization": six.ensure_text(header)}, |
567 | + exclude_oauth_signature=False)) |
568 | + |
569 | + |
570 | def get_oauth_authorization(request): |
571 | """Retrieve OAuth authorization information from a request. |
572 | |
573 | @@ -286,10 +296,7 @@ def get_oauth_authorization(request): |
574 | """ |
575 | header = request._auth |
576 | if header is not None and header.startswith("OAuth "): |
577 | - # http://oauth.net/core/1.0/#encoding_parameters says "Text names |
578 | - # and values MUST be encoded as UTF-8 octets before percent-encoding |
579 | - # them", so we can reasonably fail if this hasn't been done. |
580 | - return OAuthRequest._split_header(six.ensure_text(header)) |
581 | + return _parse_oauth_authorization_header(header) |
582 | else: |
583 | return request.form |
584 | |
585 | diff --git a/lib/lp/services/webapp/tests/test_authentication.py b/lib/lp/services/webapp/tests/test_authentication.py |
586 | index d69e248..a348b0b 100644 |
587 | --- a/lib/lp/services/webapp/tests/test_authentication.py |
588 | +++ b/lib/lp/services/webapp/tests/test_authentication.py |
589 | @@ -8,9 +8,10 @@ __metaclass__ = type |
590 | |
591 | import unittest |
592 | |
593 | -from contrib.oauth import OAuthRequest |
594 | - |
595 | -from lp.services.webapp.authentication import check_oauth_signature |
596 | +from lp.services.webapp.authentication import ( |
597 | + _parse_oauth_authorization_header, |
598 | + check_oauth_signature, |
599 | + ) |
600 | from lp.services.webapp.servers import LaunchpadTestRequest |
601 | from lp.testing import ( |
602 | TestCase, |
603 | @@ -31,20 +32,21 @@ class TestOAuthParsing(TestCase): |
604 | |
605 | def test_split_oauth(self): |
606 | # OAuth headers are parsed correctly: see bug 314507. |
607 | - # This was really a bug in the underlying contrib/oauth.py module, but |
608 | - # it has no standalone test case. |
609 | + # This was originally a bug in the underlying contrib/oauth.py |
610 | + # module, but it's useful to test that parsing works as we expect |
611 | + # for whatever OAuth library we're currently using. |
612 | # |
613 | # Note that the 'realm' parameter is not returned, because it's not |
614 | # included in the OAuth calculations. |
615 | - headers = OAuthRequest._split_header( |
616 | + headers = _parse_oauth_authorization_header( |
617 | 'OAuth realm="foo", oauth_consumer_key="justtesting"') |
618 | self.assertEqual(headers, |
619 | {'oauth_consumer_key': 'justtesting'}) |
620 | - headers = OAuthRequest._split_header( |
621 | + headers = _parse_oauth_authorization_header( |
622 | 'OAuth oauth_consumer_key="justtesting"') |
623 | self.assertEqual(headers, |
624 | {'oauth_consumer_key': 'justtesting'}) |
625 | - headers = OAuthRequest._split_header( |
626 | + headers = _parse_oauth_authorization_header( |
627 | 'OAuth oauth_consumer_key="justtesting", realm="realm"') |
628 | self.assertEqual(headers, |
629 | {'oauth_consumer_key': 'justtesting'}) |
630 | diff --git a/lib/lp/services/webapp/tests/test_publication.py b/lib/lp/services/webapp/tests/test_publication.py |
631 | index a2a4953..7a539d4 100644 |
632 | --- a/lib/lp/services/webapp/tests/test_publication.py |
633 | +++ b/lib/lp/services/webapp/tests/test_publication.py |
634 | @@ -7,13 +7,8 @@ __metaclass__ = type |
635 | |
636 | import sys |
637 | |
638 | -from contrib.oauth import ( |
639 | - OAuthConsumer, |
640 | - OAuthRequest, |
641 | - OAuthSignatureMethod_PLAINTEXT, |
642 | - OAuthToken, |
643 | - ) |
644 | from fixtures import FakeLogger |
645 | +from oauthlib import oauth1 |
646 | from storm.database import ( |
647 | STATE_DISCONNECTED, |
648 | STATE_RECONNECT, |
649 | @@ -115,16 +110,16 @@ class TestWebServicePublication(TestCaseWithFactory): |
650 | person, permission=OAuthPermission.READ_PUBLIC, context=None) |
651 | access_token, access_secret = request_token.createAccessToken() |
652 | |
653 | - # Use oauth.OAuthRequest just to generate a dictionary containing all |
654 | - # the parameters we need to use in a valid OAuth request, using the |
655 | - # access token we just created for our new person. |
656 | - oauth_consumer = OAuthConsumer(consumer.key, u'') |
657 | - oauth_token = OAuthToken(access_token.key, access_secret) |
658 | - oauth_request = OAuthRequest.from_consumer_and_token( |
659 | - oauth_consumer, oauth_token) |
660 | - oauth_request.sign_request( |
661 | - OAuthSignatureMethod_PLAINTEXT(), oauth_consumer, oauth_token) |
662 | - return LaunchpadTestRequest(form=oauth_request.parameters) |
663 | + # Make an OAuth signature using the access token we just created for |
664 | + # our new person. |
665 | + client = oauth1.Client( |
666 | + consumer.key, |
667 | + resource_owner_key=access_token.key, |
668 | + resource_owner_secret=access_secret, |
669 | + signature_method=oauth1.SIGNATURE_PLAINTEXT) |
670 | + _, headers, _ = client.sign('/dummy') |
671 | + return LaunchpadTestRequest( |
672 | + environ={'HTTP_AUTHORIZATION': headers['Authorization']}) |
673 | |
674 | def test_getPrincipal_for_person_and_account_with_different_ids(self): |
675 | # WebServicePublication.getPrincipal() does not rely on accounts |
676 | diff --git a/lib/lp/testing/pages.py b/lib/lp/testing/pages.py |
677 | index 71045f0..f0d37ed 100644 |
678 | --- a/lib/lp/testing/pages.py |
679 | +++ b/lib/lp/testing/pages.py |
680 | @@ -27,13 +27,8 @@ from bs4.element import ( |
681 | ProcessingInstruction, |
682 | Tag, |
683 | ) |
684 | -from contrib.oauth import ( |
685 | - OAuthConsumer, |
686 | - OAuthRequest, |
687 | - OAuthSignatureMethod_PLAINTEXT, |
688 | - OAuthToken, |
689 | - ) |
690 | from lazr.restful.testing.webservice import WebServiceCaller |
691 | +from oauthlib import oauth1 |
692 | import six |
693 | from six.moves.urllib.parse import urljoin |
694 | from soupsieve import escape as css_escape |
695 | @@ -147,20 +142,17 @@ class LaunchpadWebServiceCaller(WebServiceCaller): |
696 | calls. |
697 | """ |
698 | if oauth_consumer_key is not None and oauth_access_key is not None: |
699 | - # XXX cjwatson 2016-01-25: Callers should be updated to pass |
700 | - # Unicode directly, but that's a big change. |
701 | - oauth_consumer_key = six.ensure_text(oauth_consumer_key) |
702 | - self.consumer = OAuthConsumer(oauth_consumer_key, u'') |
703 | if oauth_access_secret is None: |
704 | oauth_access_secret = SAMPLEDATA_ACCESS_SECRETS.get( |
705 | oauth_access_key, u'') |
706 | - self.access_token = OAuthToken( |
707 | - oauth_access_key, oauth_access_secret) |
708 | - # This shouldn't be here, but many old tests expect it. |
709 | + self.oauth_client = oauth1.Client( |
710 | + oauth_consumer_key, |
711 | + resource_owner_key=oauth_access_key, |
712 | + resource_owner_secret=oauth_access_secret, |
713 | + signature_method=oauth1.SIGNATURE_PLAINTEXT) |
714 | logout() |
715 | else: |
716 | - self.consumer = None |
717 | - self.access_token = None |
718 | + self.oauth_client = None |
719 | self.handle_errors = handle_errors |
720 | if default_api_version is not None: |
721 | self.default_api_version = default_api_version |
722 | @@ -169,13 +161,9 @@ class LaunchpadWebServiceCaller(WebServiceCaller): |
723 | default_api_version = "beta" |
724 | |
725 | def addHeadersTo(self, full_url, full_headers): |
726 | - if self.consumer is not None and self.access_token is not None: |
727 | - request = OAuthRequest.from_consumer_and_token( |
728 | - self.consumer, self.access_token, http_url=full_url) |
729 | - request.sign_request( |
730 | - OAuthSignatureMethod_PLAINTEXT(), self.consumer, |
731 | - self.access_token) |
732 | - oauth_headers = request.to_header(OAUTH_REALM) |
733 | + if self.oauth_client is not None: |
734 | + _, oauth_headers, _ = self.oauth_client.sign( |
735 | + full_url, realm=OAUTH_REALM) |
736 | full_headers.update({ |
737 | wsgi_native_string(key): wsgi_native_string(value) |
738 | for key, value in oauth_headers.items()}) |
739 | diff --git a/setup.py b/setup.py |
740 | index f6d5b6b..173e1c9 100644 |
741 | --- a/setup.py |
742 | +++ b/setup.py |
743 | @@ -199,6 +199,7 @@ setup( |
744 | 'meliae', |
745 | 'mock', |
746 | 'oauth', |
747 | + 'oauthlib', |
748 | 'oops', |
749 | 'oops_amqp', |
750 | 'oops_datedir_repo', |
This is a good amount of red.