##// END OF EJS Templates
packages: vendor authomatic to provide bitbucket oath2 capabilities....
marcink -
r3912:9bf26830 default
parent child Browse files
Show More
@@ -0,0 +1,29 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 # This package contains non rhodecode licensed packages that are
22 # vendored for various reasons
23
24 import os
25 import sys
26
27 vendor_dir = os.path.abspath(os.path.dirname(__file__))
28
29 sys.path.append(vendor_dir)
@@ -0,0 +1,14 b''
1 # -*- coding: utf-8 -*-
2 """
3 Helper functions for use with :class:`Authomatic`.
4
5 .. autosummary::
6 :nosignatures:
7
8 authomatic.provider_id
9
10 """
11
12 from . import six
13 from .core import Authomatic
14 from .core import provider_id
@@ -0,0 +1,282 b''
1 # -*- coding: utf-8 -*-
2 """
3 Adapters
4 --------
5
6 .. contents::
7 :backlinks: none
8
9 The :func:`authomatic.login` function needs access to functionality like
10 getting the **URL** of the handler where it is being called, getting the
11 **request params** and **cookies** and **writing the body**, **headers**
12 and **status** to the response.
13
14 Since implementation of these features varies across Python web frameworks,
15 the Authomatic library uses **adapters** to unify these differences into a
16 single interface.
17
18 Available Adapters
19 ^^^^^^^^^^^^^^^^^^
20
21 If you are missing an adapter for the framework of your choice, please
22 open an `enhancement issue <https://github.com/authomatic/authomatic/issues>`_
23 or consider a contribution to this module by
24 :ref:`implementing <implement_adapters>` one by yourself.
25 Its very easy and shouldn't take you more than a few minutes.
26
27 .. autoclass:: DjangoAdapter
28 :members:
29
30 .. autoclass:: Webapp2Adapter
31 :members:
32
33 .. autoclass:: WebObAdapter
34 :members:
35
36 .. autoclass:: WerkzeugAdapter
37 :members:
38
39 .. _implement_adapters:
40
41 Implementing an Adapter
42 ^^^^^^^^^^^^^^^^^^^^^^^
43
44 Implementing an adapter for a Python web framework is pretty easy.
45
46 Do it by subclassing the :class:`.BaseAdapter` abstract class.
47 There are only **six** members that you need to implement.
48
49 Moreover if your framework is based on the |webob|_ or |werkzeug|_ package
50 you can subclass the :class:`.WebObAdapter` or :class:`.WerkzeugAdapter`
51 respectively.
52
53 .. autoclass:: BaseAdapter
54 :members:
55
56 """
57
58 import abc
59 from authomatic.core import Response
60
61
62 class BaseAdapter(object):
63 """
64 Base class for platform adapters.
65
66 Defines common interface for WSGI framework specific functionality.
67
68 """
69
70 __metaclass__ = abc.ABCMeta
71
72 @abc.abstractproperty
73 def params(self):
74 """
75 Must return a :class:`dict` of all request parameters of any HTTP
76 method.
77
78 :returns:
79 :class:`dict`
80
81 """
82
83 @abc.abstractproperty
84 def url(self):
85 """
86 Must return the url of the actual request including path but without
87 query and fragment.
88
89 :returns:
90 :class:`str`
91
92 """
93
94 @abc.abstractproperty
95 def cookies(self):
96 """
97 Must return cookies as a :class:`dict`.
98
99 :returns:
100 :class:`dict`
101
102 """
103
104 @abc.abstractmethod
105 def write(self, value):
106 """
107 Must write specified value to response.
108
109 :param str value:
110 String to be written to response.
111
112 """
113
114 @abc.abstractmethod
115 def set_header(self, key, value):
116 """
117 Must set response headers to ``Key: value``.
118
119 :param str key:
120 Header name.
121
122 :param str value:
123 Header value.
124
125 """
126
127 @abc.abstractmethod
128 def set_status(self, status):
129 """
130 Must set the response status e.g. ``'302 Found'``.
131
132 :param str status:
133 The HTTP response status.
134
135 """
136
137
138 class DjangoAdapter(BaseAdapter):
139 """
140 Adapter for the |django|_ framework.
141 """
142
143 def __init__(self, request, response):
144 """
145 :param request:
146 An instance of the :class:`django.http.HttpRequest` class.
147
148 :param response:
149 An instance of the :class:`django.http.HttpResponse` class.
150 """
151 self.request = request
152 self.response = response
153
154 @property
155 def params(self):
156 params = {}
157 params.update(self.request.GET.dict())
158 params.update(self.request.POST.dict())
159 return params
160
161 @property
162 def url(self):
163 return self.request.build_absolute_uri(self.request.path)
164
165 @property
166 def cookies(self):
167 return dict(self.request.COOKIES)
168
169 def write(self, value):
170 self.response.write(value)
171
172 def set_header(self, key, value):
173 self.response[key] = value
174
175 def set_status(self, status):
176 status_code, reason = status.split(' ', 1)
177 self.response.status_code = int(status_code)
178
179
180 class WebObAdapter(BaseAdapter):
181 """
182 Adapter for the |webob|_ package.
183 """
184
185 def __init__(self, request, response):
186 """
187 :param request:
188 A |webob|_ :class:`Request` instance.
189
190 :param response:
191 A |webob|_ :class:`Response` instance.
192 """
193 self.request = request
194 self.response = response
195
196 # =========================================================================
197 # Request
198 # =========================================================================
199
200 @property
201 def url(self):
202 return self.request.path_url
203
204 @property
205 def params(self):
206 return dict(self.request.params)
207
208 @property
209 def cookies(self):
210 return dict(self.request.cookies)
211
212 # =========================================================================
213 # Response
214 # =========================================================================
215
216 def write(self, value):
217 self.response.write(value)
218
219 def set_header(self, key, value):
220 self.response.headers[key] = str(value)
221
222 def set_status(self, status):
223 self.response.status = status
224
225
226 class Webapp2Adapter(WebObAdapter):
227 """
228 Adapter for the |webapp2|_ framework.
229
230 Inherits from the :class:`.WebObAdapter`.
231
232 """
233
234 def __init__(self, handler):
235 """
236 :param handler:
237 A :class:`webapp2.RequestHandler` instance.
238 """
239 self.request = handler.request
240 self.response = handler.response
241
242
243 class WerkzeugAdapter(BaseAdapter):
244 """
245 Adapter for |flask|_ and other |werkzeug|_ based frameworks.
246
247 Thanks to `Mark Steve Samson <http://marksteve.com>`_.
248
249 """
250
251 @property
252 def params(self):
253 return self.request.args
254
255 @property
256 def url(self):
257 return self.request.base_url
258
259 @property
260 def cookies(self):
261 return self.request.cookies
262
263 def __init__(self, request, response):
264 """
265 :param request:
266 Instance of the :class:`werkzeug.wrappers.Request` class.
267
268 :param response:
269 Instance of the :class:`werkzeug.wrappers.Response` class.
270 """
271
272 self.request = request
273 self.response = response
274
275 def write(self, value):
276 self.response.data = self.response.data.decode('utf-8') + value
277
278 def set_header(self, key, value):
279 self.response.headers[key] = value
280
281 def set_status(self, status):
282 self.response.status = status
This diff has been collapsed as it changes many lines, (1764 lines changed) Show them Hide them
@@ -0,0 +1,1764 b''
1 # -*- coding: utf-8 -*-
2
3 import collections
4 import copy
5 import datetime
6 import hashlib
7 import hmac
8 import json
9 import logging
10 try:
11 import cPickle as pickle
12 except ImportError:
13 import pickle
14 import sys
15 import threading
16 import time
17 from xml.etree import ElementTree
18
19 from authomatic.exceptions import (
20 ConfigError,
21 CredentialsError,
22 ImportStringError,
23 RequestElementsError,
24 SessionError,
25 )
26 from authomatic import six
27 from authomatic.six.moves import urllib_parse as parse
28
29
30 # =========================================================================
31 # Global variables !!!
32 # =========================================================================
33
34 _logger = logging.getLogger(__name__)
35 _logger.addHandler(logging.StreamHandler(sys.stdout))
36
37 _counter = None
38
39
40 def normalize_dict(dict_):
41 """
42 Replaces all values that are single-item iterables with the value of its
43 index 0.
44
45 :param dict dict_:
46 Dictionary to normalize.
47
48 :returns:
49 Normalized dictionary.
50
51 """
52
53 return dict([(k, v[0] if not isinstance(v, str) and len(v) == 1 else v)
54 for k, v in list(dict_.items())])
55
56
57 def items_to_dict(items):
58 """
59 Converts list of tuples to dictionary with duplicate keys converted to
60 lists.
61
62 :param list items:
63 List of tuples.
64
65 :returns:
66 :class:`dict`
67
68 """
69
70 res = collections.defaultdict(list)
71
72 for k, v in items:
73 res[k].append(v)
74
75 return normalize_dict(dict(res))
76
77
78 class Counter(object):
79 """
80 A simple counter to be used in the config to generate unique `id` values.
81 """
82
83 def __init__(self, start=0):
84 self._count = start
85
86 def count(self):
87 self._count += 1
88 return self._count
89
90
91 _counter = Counter()
92
93
94 def provider_id():
95 """
96 A simple counter to be used in the config to generate unique `IDs`.
97
98 :returns:
99 :class:`int`.
100
101 Use it in the :doc:`config` like this:
102 ::
103
104 import authomatic
105
106 CONFIG = {
107 'facebook': {
108 'class_': authomatic.providers.oauth2.Facebook,
109 'id': authomatic.provider_id(), # returns 1
110 'consumer_key': '##########',
111 'consumer_secret': '##########',
112 'scope': ['user_about_me', 'email']
113 },
114 'google': {
115 'class_': 'authomatic.providers.oauth2.Google',
116 'id': authomatic.provider_id(), # returns 2
117 'consumer_key': '##########',
118 'consumer_secret': '##########',
119 'scope': ['https://www.googleapis.com/auth/userinfo.profile',
120 'https://www.googleapis.com/auth/userinfo.email']
121 },
122 'windows_live': {
123 'class_': 'oauth2.WindowsLive',
124 'id': authomatic.provider_id(), # returns 3
125 'consumer_key': '##########',
126 'consumer_secret': '##########',
127 'scope': ['wl.basic', 'wl.emails', 'wl.photos']
128 },
129 }
130
131 """
132
133 return _counter.count()
134
135
136 def escape(s):
137 """
138 Escape a URL including any /.
139 """
140 return parse.quote(s.encode('utf-8'), safe='~')
141
142
143 def json_qs_parser(body):
144 """
145 Parses response body from JSON, XML or query string.
146
147 :param body:
148 string
149
150 :returns:
151 :class:`dict`, :class:`list` if input is JSON or query string,
152 :class:`xml.etree.ElementTree.Element` if XML.
153
154 """
155 try:
156 # Try JSON first.
157 return json.loads(body)
158 except (OverflowError, TypeError, ValueError):
159 pass
160
161 try:
162 # Then XML.
163 return ElementTree.fromstring(body)
164 except (ElementTree.ParseError, TypeError, ValueError):
165 pass
166
167 # Finally query string.
168 return dict(parse.parse_qsl(body))
169
170
171 def import_string(import_name, silent=False):
172 """
173 Imports an object by string in dotted notation.
174
175 taken `from webapp2.import_string() <http://webapp-
176 improved.appspot.com/api/webapp2.html#webapp2.import_string>`_
177
178 """
179
180 try:
181 if '.' in import_name:
182 module, obj = import_name.rsplit('.', 1)
183 return getattr(__import__(module, None, None, [obj]), obj)
184 else:
185 return __import__(import_name)
186 except (ImportError, AttributeError) as e:
187 if not silent:
188 raise ImportStringError('Import from string failed for path {0}'
189 .format(import_name), str(e))
190
191
192 def resolve_provider_class(class_):
193 """
194 Returns a provider class.
195
196 :param class_name: :class:`string` or
197 :class:`authomatic.providers.BaseProvider` subclass.
198
199 """
200
201 if isinstance(class_, str):
202 # prepare path for authomatic.providers package
203 path = '.'.join([__package__, 'providers', class_])
204
205 # try to import class by string from providers module or by fully
206 # qualified path
207 return import_string(class_, True) or import_string(path)
208 else:
209 return class_
210
211
212 def id_to_name(config, short_name):
213 """
214 Returns the provider :doc:`config` key based on it's ``id`` value.
215
216 :param dict config:
217 :doc:`config`.
218 :param id:
219 Value of the id parameter in the :ref:`config` to search for.
220
221 """
222
223 for k, v in list(config.items()):
224 if v.get('id') == short_name:
225 return k
226
227 raise Exception(
228 'No provider with id={0} found in the config!'.format(short_name))
229
230
231 class ReprMixin(object):
232 """
233 Provides __repr__() method with output *ClassName(arg1=value, arg2=value)*.
234
235 Ignored are attributes
236
237 * which values are considered false.
238 * with leading underscore.
239 * listed in _repr_ignore.
240
241 Values of attributes listed in _repr_sensitive will be replaced by *###*.
242 Values which repr() string is longer than _repr_length_limit will be
243 represented as *ClassName(...)*
244
245 """
246
247 #: Iterable of attributes to be ignored.
248 _repr_ignore = []
249 #: Iterable of attributes which value should not be visible.
250 _repr_sensitive = []
251 #: `int` Values longer than this will be truncated to *ClassName(...)*.
252 _repr_length_limit = 20
253
254 def __repr__(self):
255
256 # get class name
257 name = self.__class__.__name__
258
259 # construct keyword arguments
260 args = []
261
262 for k, v in list(self.__dict__.items()):
263
264 # ignore attributes with leading underscores and those listed in
265 # _repr_ignore
266 if v and not k.startswith('_') and k not in self._repr_ignore:
267
268 # replace sensitive values
269 if k in self._repr_sensitive:
270 v = '###'
271
272 # if repr is too long
273 if len(repr(v)) > self._repr_length_limit:
274 # Truncate to ClassName(...)
275 v = '{0}(...)'.format(v.__class__.__name__)
276 else:
277 v = repr(v)
278
279 args.append('{0}={1}'.format(k, v))
280
281 return '{0}({1})'.format(name, ', '.join(args))
282
283
284 class Future(threading.Thread):
285 """
286 Represents an activity run in a separate thread. Subclasses the standard
287 library :class:`threading.Thread` and adds :attr:`.get_result` method.
288
289 .. warning::
290
291 |async|
292
293 """
294
295 def __init__(self, func, *args, **kwargs):
296 """
297 :param callable func:
298 The function to be run in separate thread.
299
300 Calls :data:`func` in separate thread and returns immediately.
301 Accepts arbitrary positional and keyword arguments which will be
302 passed to :data:`func`.
303 """
304
305 super(Future, self).__init__()
306 self._func = func
307 self._args = args
308 self._kwargs = kwargs
309 self._result = None
310
311 self.start()
312
313 def run(self):
314 self._result = self._func(*self._args, **self._kwargs)
315
316 def get_result(self, timeout=None):
317 """
318 Waits for the wrapped :data:`func` to finish and returns its result.
319
320 .. note::
321
322 This will block the **calling thread** until the :data:`func`
323 returns.
324
325 :param timeout:
326 :class:`float` or ``None`` A timeout for the :data:`func` to
327 return in seconds.
328
329 :returns:
330 The result of the wrapped :data:`func`.
331
332 """
333
334 self.join(timeout)
335 return self._result
336
337
338 class Session(object):
339 """
340 A dictionary-like secure cookie session implementation.
341 """
342
343 def __init__(self, adapter, secret, name='authomatic', max_age=600,
344 secure=False):
345 """
346 :param str secret:
347 Session secret used to sign the session cookie.
348 :param str name:
349 Session cookie name.
350 :param int max_age:
351 Maximum allowed age of session cookie nonce in seconds.
352 :param bool secure:
353 If ``True`` the session cookie will be saved with ``Secure``
354 attribute.
355 """
356
357 self.adapter = adapter
358 self.name = name
359 self.secret = secret
360 self.max_age = max_age
361 self.secure = secure
362 self._data = {}
363
364 def create_cookie(self, delete=None):
365 """
366 Creates the value for ``Set-Cookie`` HTTP header.
367
368 :param bool delete:
369 If ``True`` the cookie value will be ``deleted`` and the
370 Expires value will be ``Thu, 01-Jan-1970 00:00:01 GMT``.
371
372 """
373 value = 'deleted' if delete else self._serialize(self.data)
374 split_url = parse.urlsplit(self.adapter.url)
375 domain = split_url.netloc.split(':')[0]
376
377 # Work-around for issue #11, failure of WebKit-based browsers to accept
378 # cookies set as part of a redirect response in some circumstances.
379 if '.' not in domain:
380 template = '{name}={value}; Path={path}; HttpOnly{secure}{expires}'
381 else:
382 template = ('{name}={value}; Domain={domain}; Path={path}; '
383 'HttpOnly{secure}{expires}')
384
385 return template.format(
386 name=self.name,
387 value=value,
388 domain=domain,
389 path=split_url.path,
390 secure='; Secure' if self.secure else '',
391 expires='; Expires=Thu, 01-Jan-1970 00:00:01 GMT' if delete else ''
392 )
393
394 def save(self):
395 """
396 Adds the session cookie to headers.
397 """
398 if self.data:
399 cookie = self.create_cookie()
400 cookie_len = len(cookie)
401
402 if cookie_len > 4093:
403 raise SessionError('Cookie too long! The cookie size {0} '
404 'is more than 4093 bytes.'
405 .format(cookie_len))
406
407 self.adapter.set_header('Set-Cookie', cookie)
408
409 # Reset data
410 self._data = {}
411
412 def delete(self):
413 self.adapter.set_header('Set-Cookie', self.create_cookie(delete=True))
414
415 def _get_data(self):
416 """
417 Extracts the session data from cookie.
418 """
419 cookie = self.adapter.cookies.get(self.name)
420 return self._deserialize(cookie) if cookie else {}
421
422 @property
423 def data(self):
424 """
425 Gets session data lazily.
426 """
427 if not self._data:
428 self._data = self._get_data()
429 # Always return a dict, even if deserialization returned nothing
430 if self._data is None:
431 self._data = {}
432 return self._data
433
434 def _signature(self, *parts):
435 """
436 Creates signature for the session.
437 """
438 signature = hmac.new(six.b(self.secret), digestmod=hashlib.sha1)
439 signature.update(six.b('|'.join(parts)))
440 return signature.hexdigest()
441
442 def _serialize(self, value):
443 """
444 Converts the value to a signed string with timestamp.
445
446 :param value:
447 Object to be serialized.
448
449 :returns:
450 Serialized value.
451
452 """
453
454 # data = copy.deepcopy(value)
455 data = value
456
457 # 1. Serialize
458 serialized = pickle.dumps(data).decode('latin-1')
459
460 # 2. Encode
461 # Percent encoding produces smaller result then urlsafe base64.
462 encoded = parse.quote(serialized, '')
463
464 # 3. Concatenate
465 timestamp = str(int(time.time()))
466 signature = self._signature(self.name, encoded, timestamp)
467 concatenated = '|'.join([encoded, timestamp, signature])
468
469 return concatenated
470
471 def _deserialize(self, value):
472 """
473 Deserializes and verifies the value created by :meth:`._serialize`.
474
475 :param str value:
476 The serialized value.
477
478 :returns:
479 Deserialized object.
480
481 """
482
483 # 3. Split
484 encoded, timestamp, signature = value.split('|')
485
486 # Verify signature
487 if not signature == self._signature(self.name, encoded, timestamp):
488 raise SessionError('Invalid signature "{0}"!'.format(signature))
489
490 # Verify timestamp
491 if int(timestamp) < int(time.time()) - self.max_age:
492 return None
493
494 # 2. Decode
495 decoded = parse.unquote(encoded)
496
497 # 1. Deserialize
498 deserialized = pickle.loads(decoded.encode('latin-1'))
499
500 return deserialized
501
502 def __setitem__(self, key, value):
503 self._data[key] = value
504
505 def __getitem__(self, key):
506 return self.data.__getitem__(key)
507
508 def __delitem__(self, key):
509 return self._data.__delitem__(key)
510
511 def get(self, key, default=None):
512 return self.data.get(key, default)
513
514
515 class User(ReprMixin):
516 """
517 Provides unified interface to selected **user** info returned by different
518 **providers**.
519
520 .. note:: The value format may vary across providers.
521
522 """
523
524 def __init__(self, provider, **kwargs):
525 #: A :doc:`provider <providers>` instance.
526 self.provider = provider
527
528 #: An :class:`.Credentials` instance.
529 self.credentials = kwargs.get('credentials')
530
531 #: A :class:`dict` containing all the **user** information returned
532 #: by the **provider**.
533 #: The structure differs across **providers**.
534 self.data = kwargs.get('data')
535
536 #: The :attr:`.Response.content` of the request made to update
537 #: the user.
538 self.content = kwargs.get('content')
539
540 #: :class:`str` ID assigned to the **user** by the **provider**.
541 self.id = kwargs.get('id')
542 #: :class:`str` User name e.g. *andrewpipkin*.
543 self.username = kwargs.get('username')
544 #: :class:`str` Name e.g. *Andrew Pipkin*.
545 self.name = kwargs.get('name')
546 #: :class:`str` First name e.g. *Andrew*.
547 self.first_name = kwargs.get('first_name')
548 #: :class:`str` Last name e.g. *Pipkin*.
549 self.last_name = kwargs.get('last_name')
550 #: :class:`str` Nickname e.g. *Andy*.
551 self.nickname = kwargs.get('nickname')
552 #: :class:`str` Link URL.
553 self.link = kwargs.get('link')
554 #: :class:`str` Gender.
555 self.gender = kwargs.get('gender')
556 #: :class:`str` Timezone.
557 self.timezone = kwargs.get('timezone')
558 #: :class:`str` Locale.
559 self.locale = kwargs.get('locale')
560 #: :class:`str` E-mail.
561 self.email = kwargs.get('email')
562 #: :class:`str` phone.
563 self.phone = kwargs.get('phone')
564 #: :class:`str` Picture URL.
565 self.picture = kwargs.get('picture')
566 #: Birth date as :class:`datetime.datetime()` or :class:`str`
567 # if parsing failed or ``None``.
568 self.birth_date = kwargs.get('birth_date')
569 #: :class:`str` Country.
570 self.country = kwargs.get('country')
571 #: :class:`str` City.
572 self.city = kwargs.get('city')
573 #: :class:`str` Geographical location.
574 self.location = kwargs.get('location')
575 #: :class:`str` Postal code.
576 self.postal_code = kwargs.get('postal_code')
577 #: Instance of the Google App Engine Users API
578 #: `User <https://developers.google.com/appengine/docs/python/users/userclass>`_ class.
579 #: Only present when using the :class:`authomatic.providers.gaeopenid.GAEOpenID` provider.
580 self.gae_user = kwargs.get('gae_user')
581
582 def update(self):
583 """
584 Updates the user info by fetching the **provider's** user info URL.
585
586 :returns:
587 Updated instance of this class.
588
589 """
590
591 return self.provider.update_user()
592
593 def async_update(self):
594 """
595 Same as :meth:`.update` but runs asynchronously in a separate thread.
596
597 .. warning::
598
599 |async|
600
601 :returns:
602 :class:`.Future` instance representing the separate thread.
603
604 """
605
606 return Future(self.update)
607
608 def to_dict(self):
609 """
610 Converts the :class:`.User` instance to a :class:`dict`.
611
612 :returns:
613 :class:`dict`
614
615 """
616
617 # copy the dictionary
618 d = copy.copy(self.__dict__)
619
620 # Keep only the provider name to avoid circular reference
621 d['provider'] = self.provider.name
622 d['credentials'] = self.credentials.serialize(
623 ) if self.credentials else None
624 d['birth_date'] = str(d['birth_date'])
625
626 # Remove content
627 d.pop('content')
628
629 if isinstance(self.data, ElementTree.Element):
630 d['data'] = None
631
632 return d
633
634
635 SupportedUserAttributesNT = collections.namedtuple(
636 typename='SupportedUserAttributesNT',
637 field_names=['birth_date', 'city', 'country', 'email', 'first_name',
638 'gender', 'id', 'last_name', 'link', 'locale', 'location',
639 'name', 'nickname', 'phone', 'picture', 'postal_code',
640 'timezone', 'username', ]
641 )
642
643
644 class SupportedUserAttributes(SupportedUserAttributesNT):
645 def __new__(cls, **kwargs):
646 defaults = dict((i, False) for i in SupportedUserAttributes._fields) # pylint:disable=no-member
647 defaults.update(**kwargs)
648 return super(SupportedUserAttributes, cls).__new__(cls, **defaults)
649
650
651 class Credentials(ReprMixin):
652 """
653 Contains all necessary information to fetch **user's protected resources**.
654 """
655
656 _repr_sensitive = ('token', 'refresh_token', 'token_secret',
657 'consumer_key', 'consumer_secret')
658
659 def __init__(self, config, **kwargs):
660
661 #: :class:`dict` :doc:`config`.
662 self.config = config
663
664 #: :class:`str` User **access token**.
665 self.token = kwargs.get('token', '')
666
667 #: :class:`str` Access token type.
668 self.token_type = kwargs.get('token_type', '')
669
670 #: :class:`str` Refresh token.
671 self.refresh_token = kwargs.get('refresh_token', '')
672
673 #: :class:`str` Access token secret.
674 self.token_secret = kwargs.get('token_secret', '')
675
676 #: :class:`int` Expiration date as UNIX timestamp.
677 self.expiration_time = int(kwargs.get('expiration_time', 0))
678
679 #: A :doc:`Provider <providers>` instance**.
680 provider = kwargs.get('provider')
681
682 self.expire_in = int(kwargs.get('expire_in', 0))
683
684 if provider:
685 #: :class:`str` Provider name specified in the :doc:`config`.
686 self.provider_name = provider.name
687
688 #: :class:`str` Provider type e.g.
689 # ``"authomatic.providers.oauth2.OAuth2"``.
690 self.provider_type = provider.get_type()
691
692 #: :class:`str` Provider type e.g.
693 # ``"authomatic.providers.oauth2.OAuth2"``.
694 self.provider_type_id = provider.type_id
695
696 #: :class:`str` Provider short name specified in the :doc:`config`.
697 self.provider_id = int(provider.id) if provider.id else None
698
699 #: :class:`class` Provider class.
700 self.provider_class = provider.__class__
701
702 #: :class:`str` Consumer key specified in the :doc:`config`.
703 self.consumer_key = provider.consumer_key
704
705 #: :class:`str` Consumer secret specified in the :doc:`config`.
706 self.consumer_secret = provider.consumer_secret
707
708 else:
709 self.provider_name = kwargs.get('provider_name', '')
710 self.provider_type = kwargs.get('provider_type', '')
711 self.provider_type_id = kwargs.get('provider_type_id')
712 self.provider_id = kwargs.get('provider_id')
713 self.provider_class = kwargs.get('provider_class')
714
715 self.consumer_key = kwargs.get('consumer_key', '')
716 self.consumer_secret = kwargs.get('consumer_secret', '')
717
718 @property
719 def expire_in(self):
720 """
721
722 """
723
724 return self._expire_in
725
726 @expire_in.setter
727 def expire_in(self, value):
728 """
729 Computes :attr:`.expiration_time` when the value is set.
730 """
731
732 # pylint:disable=attribute-defined-outside-init
733 if value:
734 self._expiration_time = int(time.time()) + int(value)
735 self._expire_in = value
736
737 @property
738 def expiration_time(self):
739 return self._expiration_time
740
741 @expiration_time.setter
742 def expiration_time(self, value):
743
744 # pylint:disable=attribute-defined-outside-init
745 self._expiration_time = int(value)
746 self._expire_in = self._expiration_time - int(time.time())
747
748 @property
749 def expiration_date(self):
750 """
751 Expiration date as :class:`datetime.datetime` or ``None`` if
752 credentials never expire.
753 """
754
755 if self.expire_in < 0:
756 return None
757 else:
758 return datetime.datetime.fromtimestamp(self.expiration_time)
759
760 @property
761 def valid(self):
762 """
763 ``True`` if credentials are valid, ``False`` if expired.
764 """
765
766 if self.expiration_time:
767 return self.expiration_time > int(time.time())
768 else:
769 return True
770
771 def expire_soon(self, seconds):
772 """
773 Returns ``True`` if credentials expire sooner than specified.
774
775 :param int seconds:
776 Number of seconds.
777
778 :returns:
779 ``True`` if credentials expire sooner than specified,
780 else ``False``.
781
782 """
783
784 if self.expiration_time:
785 return self.expiration_time < int(time.time()) + int(seconds)
786 else:
787 return False
788
789 def refresh(self, force=False, soon=86400):
790 """
791 Refreshes the credentials only if the **provider** supports it and if
792 it will expire in less than one day. It does nothing in other cases.
793
794 .. note::
795
796 The credentials will be refreshed only if it gives sense
797 i.e. only |oauth2|_ has the notion of credentials
798 *refreshment/extension*.
799 And there are also differences across providers e.g. Google
800 supports refreshment only if there is a ``refresh_token`` in
801 the credentials and that in turn is present only if the
802 ``access_type`` parameter was set to ``offline`` in the
803 **user authorization request**.
804
805 :param bool force:
806 If ``True`` the credentials will be refreshed even if they
807 won't expire soon.
808
809 :param int soon:
810 Number of seconds specifying what means *soon*.
811
812 """
813
814 if hasattr(self.provider_class, 'refresh_credentials'):
815 if force or self.expire_soon(soon):
816 logging.info('PROVIDER NAME: {0}'.format(self.provider_name))
817 return self.provider_class(
818 self, None, self.provider_name).refresh_credentials(self)
819
820 def async_refresh(self, *args, **kwargs):
821 """
822 Same as :meth:`.refresh` but runs asynchronously in a separate thread.
823
824 .. warning::
825
826 |async|
827
828 :returns:
829 :class:`.Future` instance representing the separate thread.
830
831 """
832
833 return Future(self.refresh, *args, **kwargs)
834
835 def provider_type_class(self):
836 """
837 Returns the :doc:`provider <providers>` class specified in the
838 :doc:`config`.
839
840 :returns:
841 :class:`authomatic.providers.BaseProvider` subclass.
842
843 """
844
845 return resolve_provider_class(self.provider_type)
846
847 def serialize(self):
848 """
849 Converts the credentials to a percent encoded string to be stored for
850 later use.
851
852 :returns:
853 :class:`string`
854
855 """
856
857 if self.provider_id is None:
858 raise ConfigError(
859 'To serialize credentials you need to specify a '
860 'unique integer under the "id" key in the config '
861 'for each provider!')
862
863 # Get the provider type specific items.
864 rest = self.provider_type_class().to_tuple(self)
865
866 # Provider ID and provider type ID are always the first two items.
867 result = (self.provider_id, self.provider_type_id) + rest
868
869 # Make sure that all items are strings.
870 stringified = [str(i) for i in result]
871
872 # Concatenate by newline.
873 concatenated = '\n'.join(stringified)
874
875 # Percent encode.
876 return parse.quote(concatenated, '')
877
878 @classmethod
879 def deserialize(cls, config, credentials):
880 """
881 A *class method* which reconstructs credentials created by
882 :meth:`serialize`. You can also pass it a :class:`.Credentials`
883 instance.
884
885 :param dict config:
886 The same :doc:`config` used in the :func:`.login` to get the
887 credentials.
888 :param str credentials:
889 :class:`string` The serialized credentials or
890 :class:`.Credentials` instance.
891
892 :returns:
893 :class:`.Credentials`
894
895 """
896
897 # Accept both serialized and normal.
898 if isinstance(credentials, Credentials):
899 return credentials
900
901 decoded = parse.unquote(credentials)
902
903 split = decoded.split('\n')
904
905 # We need the provider ID to move forward.
906 if split[0] is None:
907 raise CredentialsError(
908 'To deserialize credentials you need to specify a unique '
909 'integer under the "id" key in the config for each provider!')
910
911 # Get provider config by short name.
912 provider_name = id_to_name(config, int(split[0]))
913 cfg = config.get(provider_name)
914
915 # Get the provider class.
916 ProviderClass = resolve_provider_class(cfg.get('class_'))
917
918 deserialized = Credentials(config)
919
920 deserialized.provider_id = provider_id
921 deserialized.provider_type = ProviderClass.get_type()
922 deserialized.provider_type_id = split[1]
923 deserialized.provider_class = ProviderClass
924 deserialized.provider_name = provider_name
925 deserialized.provider_class = ProviderClass
926
927 # Add provider type specific properties.
928 return ProviderClass.reconstruct(split[2:], deserialized, cfg)
929
930
931 class LoginResult(ReprMixin):
932 """
933 Result of the :func:`authomatic.login` function.
934 """
935
936 def __init__(self, provider):
937 #: A :doc:`provider <providers>` instance.
938 self.provider = provider
939
940 #: An instance of the :exc:`authomatic.exceptions.BaseError` subclass.
941 self.error = None
942
943 def popup_js(self, callback_name=None, indent=None,
944 custom=None, stay_open=False):
945 """
946 Returns JavaScript that:
947
948 #. Triggers the ``options.onLoginComplete(result, closer)``
949 handler set with the :ref:`authomatic.setup() <js_setup>`
950 function of :ref:`javascript.js <js>`.
951 #. Calls the JavasScript callback specified by :data:`callback_name`
952 on the opener of the *login handler popup* and passes it the
953 *login result* JSON object as first argument and the `closer`
954 function which you should call in your callback to close the popup.
955
956 :param str callback_name:
957 The name of the javascript callback e.g ``foo.bar.loginCallback``
958 will result in ``window.opener.foo.bar.loginCallback(result);``
959 in the HTML.
960
961 :param int indent:
962 The number of spaces to indent the JSON result object.
963 If ``0`` or negative, only newlines are added.
964 If ``None``, no newlines are added.
965
966 :param custom:
967 Any JSON serializable object that will be passed to the
968 ``result.custom`` attribute.
969
970 :param str stay_open:
971 If ``True``, the popup will stay open.
972
973 :returns:
974 :class:`str` with JavaScript.
975
976 """
977
978 custom_callback = """
979 try {{ window.opener.{cb}(result, closer); }} catch(e) {{}}
980 """.format(cb=callback_name) if callback_name else ''
981
982 # TODO: Move the window.close() to the opener
983 return """
984 (function(){{
985
986 closer = function(){{
987 window.close();
988 }};
989
990 var result = {result};
991 result.custom = {custom};
992
993 {custom_callback}
994
995 try {{
996 window.opener.authomatic.loginComplete(result, closer);
997 }} catch(e) {{}}
998
999 }})();
1000
1001 """.format(result=self.to_json(indent),
1002 custom=json.dumps(custom),
1003 custom_callback=custom_callback,
1004 stay_open='// ' if stay_open else '')
1005
1006 def popup_html(self, callback_name=None, indent=None,
1007 title='Login | {0}', custom=None, stay_open=False):
1008 """
1009 Returns a HTML with JavaScript that:
1010
1011 #. Triggers the ``options.onLoginComplete(result, closer)`` handler
1012 set with the :ref:`authomatic.setup() <js_setup>` function of
1013 :ref:`javascript.js <js>`.
1014 #. Calls the JavasScript callback specified by :data:`callback_name`
1015 on the opener of the *login handler popup* and passes it the
1016 *login result* JSON object as first argument and the `closer`
1017 function which you should call in your callback to close the popup.
1018
1019 :param str callback_name:
1020 The name of the javascript callback e.g ``foo.bar.loginCallback``
1021 will result in ``window.opener.foo.bar.loginCallback(result);``
1022 in the HTML.
1023
1024 :param int indent:
1025 The number of spaces to indent the JSON result object.
1026 If ``0`` or negative, only newlines are added.
1027 If ``None``, no newlines are added.
1028
1029 :param str title:
1030 The text of the HTML title. You can use ``{0}`` tag inside,
1031 which will be replaced by the provider name.
1032
1033 :param custom:
1034 Any JSON serializable object that will be passed to the
1035 ``result.custom`` attribute.
1036
1037 :param str stay_open:
1038 If ``True``, the popup will stay open.
1039
1040 :returns:
1041 :class:`str` with HTML.
1042
1043 """
1044
1045 return """
1046 <!DOCTYPE html>
1047 <html>
1048 <head><title>{title}</title></head>
1049 <body>
1050 <script type="text/javascript">
1051 {js}
1052 </script>
1053 </body>
1054 </html>
1055 """.format(
1056 title=title.format(self.provider.name if self.provider else ''),
1057 js=self.popup_js(callback_name, indent, custom, stay_open)
1058 )
1059
1060 @property
1061 def user(self):
1062 """
1063 A :class:`.User` instance.
1064 """
1065
1066 return self.provider.user if self.provider else None
1067
1068 def to_dict(self):
1069 return dict(provider=self.provider, user=self.user, error=self.error)
1070
1071 def to_json(self, indent=4):
1072 return json.dumps(self, default=lambda obj: obj.to_dict(
1073 ) if hasattr(obj, 'to_dict') else '', indent=indent)
1074
1075
1076 class Response(ReprMixin):
1077 """
1078 Wraps :class:`httplib.HTTPResponse` and adds.
1079
1080 :attr:`.content` and :attr:`.data` attributes.
1081
1082 """
1083
1084 def __init__(self, httplib_response, content_parser=None):
1085 """
1086 :param httplib_response:
1087 The wrapped :class:`httplib.HTTPResponse` instance.
1088
1089 :param function content_parser:
1090 Callable which accepts :attr:`.content` as argument,
1091 parses it and returns the parsed data as :class:`dict`.
1092 """
1093
1094 self.httplib_response = httplib_response
1095 self.content_parser = content_parser or json_qs_parser
1096 self._data = None
1097 self._content = None
1098
1099 #: Same as :attr:`httplib.HTTPResponse.msg`.
1100 self.msg = httplib_response.msg
1101 #: Same as :attr:`httplib.HTTPResponse.version`.
1102 self.version = httplib_response.version
1103 #: Same as :attr:`httplib.HTTPResponse.status`.
1104 self.status = httplib_response.status
1105 #: Same as :attr:`httplib.HTTPResponse.reason`.
1106 self.reason = httplib_response.reason
1107
1108 def read(self, amt=None):
1109 """
1110 Same as :meth:`httplib.HTTPResponse.read`.
1111
1112 :param amt:
1113
1114 """
1115
1116 return self.httplib_response.read(amt)
1117
1118 def getheader(self, name, default=None):
1119 """
1120 Same as :meth:`httplib.HTTPResponse.getheader`.
1121
1122 :param name:
1123 :param default:
1124
1125 """
1126
1127 return self.httplib_response.getheader(name, default)
1128
1129 def fileno(self):
1130 """
1131 Same as :meth:`httplib.HTTPResponse.fileno`.
1132 """
1133 return self.httplib_response.fileno()
1134
1135 def getheaders(self):
1136 """
1137 Same as :meth:`httplib.HTTPResponse.getheaders`.
1138 """
1139 return self.httplib_response.getheaders()
1140
1141 @staticmethod
1142 def is_binary_string(content):
1143 """
1144 Return true if string is binary data.
1145 """
1146
1147 textchars = (bytearray([7, 8, 9, 10, 12, 13, 27]) +
1148 bytearray(range(0x20, 0x100)))
1149 return bool(content.translate(None, textchars))
1150
1151 @property
1152 def content(self):
1153 """
1154 The whole response content.
1155 """
1156
1157 if not self._content:
1158 content = self.httplib_response.read()
1159 if self.is_binary_string(content):
1160 self._content = content
1161 else:
1162 self._content = content.decode('utf-8')
1163 return self._content
1164
1165 @property
1166 def data(self):
1167 """
1168 A :class:`dict` of data parsed from :attr:`.content`.
1169 """
1170
1171 if not self._data:
1172 self._data = self.content_parser(self.content)
1173 return self._data
1174
1175
1176 class UserInfoResponse(Response):
1177 """
1178 Inherits from :class:`.Response`, adds :attr:`~UserInfoResponse.user`
1179 attribute.
1180 """
1181
1182 def __init__(self, user, *args, **kwargs):
1183 super(UserInfoResponse, self).__init__(*args, **kwargs)
1184
1185 #: :class:`.User` instance.
1186 self.user = user
1187
1188
1189 class RequestElements(tuple):
1190 """
1191 A tuple of ``(url, method, params, headers, body)`` request elements.
1192
1193 With some additional properties.
1194
1195 """
1196
1197 def __new__(cls, url, method, params, headers, body):
1198 return tuple.__new__(cls, (url, method, params, headers, body))
1199
1200 @property
1201 def url(self):
1202 """
1203 Request URL.
1204 """
1205
1206 return self[0]
1207
1208 @property
1209 def method(self):
1210 """
1211 HTTP method of the request.
1212 """
1213
1214 return self[1]
1215
1216 @property
1217 def params(self):
1218 """
1219 Dictionary of request parameters.
1220 """
1221
1222 return self[2]
1223
1224 @property
1225 def headers(self):
1226 """
1227 Dictionary of request headers.
1228 """
1229
1230 return self[3]
1231
1232 @property
1233 def body(self):
1234 """
1235 :class:`str` Body of ``POST``, ``PUT`` and ``PATCH`` requests.
1236 """
1237
1238 return self[4]
1239
1240 @property
1241 def query_string(self):
1242 """
1243 Query string of the request.
1244 """
1245
1246 return parse.urlencode(self.params)
1247
1248 @property
1249 def full_url(self):
1250 """
1251 URL with query string.
1252 """
1253
1254 return self.url + '?' + self.query_string
1255
1256 def to_json(self):
1257 return json.dumps(dict(url=self.url,
1258 method=self.method,
1259 params=self.params,
1260 headers=self.headers,
1261 body=self.body))
1262
1263
1264 class Authomatic(object):
1265 def __init__(
1266 self, config, secret, session_max_age=600, secure_cookie=False,
1267 session=None, session_save_method=None, report_errors=True,
1268 debug=False, logging_level=logging.INFO, prefix='authomatic',
1269 logger=None
1270 ):
1271 """
1272 Encapsulates all the functionality of this package.
1273
1274 :param dict config:
1275 :doc:`config`
1276
1277 :param str secret:
1278 A secret string that will be used as the key for signing
1279 :class:`.Session` cookie and as a salt by *CSRF* token generation.
1280
1281 :param session_max_age:
1282 Maximum allowed age of :class:`.Session` cookie nonce in seconds.
1283
1284 :param bool secure_cookie:
1285 If ``True`` the :class:`.Session` cookie will be saved wit
1286 ``Secure`` attribute.
1287
1288 :param session:
1289 Custom dictionary-like session implementation.
1290
1291 :param callable session_save_method:
1292 A method of the supplied session or any mechanism that saves the
1293 session data and cookie.
1294
1295 :param bool report_errors:
1296 If ``True`` exceptions encountered during the **login procedure**
1297 will be caught and reported in the :attr:`.LoginResult.error`
1298 attribute.
1299 Default is ``True``.
1300
1301 :param bool debug:
1302 If ``True`` traceback of exceptions will be written to response.
1303 Default is ``False``.
1304
1305 :param int logging_level:
1306 The logging level threshold for the default logger as specified in
1307 the standard Python
1308 `logging library <http://docs.python.org/2/library/logging.html>`_.
1309 This setting is ignored when :data:`logger` is set.
1310 Default is ``logging.INFO``.
1311
1312 :param str prefix:
1313 Prefix used as the :class:`.Session` cookie name.
1314
1315 :param logger:
1316 A :class:`logging.logger` instance.
1317
1318 """
1319
1320 self.config = config
1321 self.secret = secret
1322 self.session_max_age = session_max_age
1323 self.secure_cookie = secure_cookie
1324 self.session = session
1325 self.session_save_method = session_save_method
1326 self.report_errors = report_errors
1327 self.debug = debug
1328 self.logging_level = logging_level
1329 self.prefix = prefix
1330 self._logger = logger or logging.getLogger(str(id(self)))
1331
1332 # Set logging level.
1333 if logger is None:
1334 self._logger.setLevel(logging_level)
1335
1336 def login(self, adapter, provider_name, callback=None,
1337 session=None, session_saver=None, **kwargs):
1338 """
1339 If :data:`provider_name` specified, launches the login procedure for
1340 corresponding :doc:`provider </reference/providers>` and returns
1341 :class:`.LoginResult`.
1342
1343 If :data:`provider_name` is empty, acts like
1344 :meth:`.Authomatic.backend`.
1345
1346 .. warning::
1347
1348 The method redirects the **user** to the **provider** which in
1349 turn redirects **him/her** back to the *request handler* where
1350 it has been called.
1351
1352 :param str provider_name:
1353 Name of the provider as specified in the keys of the :doc:`config`.
1354
1355 :param callable callback:
1356 If specified the method will call the callback with
1357 :class:`.LoginResult` passed as argument and will return nothing.
1358
1359 :param bool report_errors:
1360
1361 .. note::
1362
1363 Accepts additional keyword arguments that will be passed to
1364 :doc:`provider <providers>` constructor.
1365
1366 :returns:
1367 :class:`.LoginResult`
1368
1369 """
1370
1371 if provider_name:
1372 # retrieve required settings for current provider and raise
1373 # exceptions if missing
1374 provider_settings = self.config.get(provider_name)
1375 if not provider_settings:
1376 raise ConfigError('Provider name "{0}" not specified!'
1377 .format(provider_name))
1378
1379 if not (session is None or session_saver is None):
1380 session = session
1381 session_saver = session_saver
1382 else:
1383 session = Session(adapter=adapter,
1384 secret=self.secret,
1385 max_age=self.session_max_age,
1386 name=self.prefix,
1387 secure=self.secure_cookie)
1388
1389 session_saver = session.save
1390
1391 # Resolve provider class.
1392 class_ = provider_settings.get('class_')
1393 if not class_:
1394 raise ConfigError(
1395 'The "class_" key not specified in the config'
1396 ' for provider {0}!'.format(provider_name))
1397 ProviderClass = resolve_provider_class(class_)
1398
1399 # FIXME: Find a nicer solution
1400 ProviderClass._logger = self._logger
1401
1402 # instantiate provider class
1403 provider = ProviderClass(self,
1404 adapter=adapter,
1405 provider_name=provider_name,
1406 callback=callback,
1407 session=session,
1408 session_saver=session_saver,
1409 **kwargs)
1410
1411 # return login result
1412 return provider.login()
1413
1414 else:
1415 # Act like backend.
1416 self.backend(adapter)
1417
1418 def credentials(self, credentials):
1419 """
1420 Deserializes credentials.
1421
1422 :param credentials:
1423 Credentials serialized with :meth:`.Credentials.serialize` or
1424 :class:`.Credentials` instance.
1425
1426 :returns:
1427 :class:`.Credentials`
1428
1429 """
1430
1431 return Credentials.deserialize(self.config, credentials)
1432
1433 def access(self, credentials, url, params=None, method='GET',
1434 headers=None, body='', max_redirects=5, content_parser=None):
1435 """
1436 Accesses **protected resource** on behalf of the **user**.
1437
1438 :param credentials:
1439 The **user's** :class:`.Credentials` (serialized or normal).
1440
1441 :param str url:
1442 The **protected resource** URL.
1443
1444 :param str method:
1445 HTTP method of the request.
1446
1447 :param dict headers:
1448 HTTP headers of the request.
1449
1450 :param str body:
1451 Body of ``POST``, ``PUT`` and ``PATCH`` requests.
1452
1453 :param int max_redirects:
1454 Maximum number of HTTP redirects to follow.
1455
1456 :param function content_parser:
1457 A function to be used to parse the :attr:`.Response.data`
1458 from :attr:`.Response.content`.
1459
1460 :returns:
1461 :class:`.Response`
1462
1463 """
1464
1465 # Deserialize credentials.
1466 credentials = Credentials.deserialize(self.config, credentials)
1467
1468 # Resolve provider class.
1469 ProviderClass = credentials.provider_class
1470 logging.info('ACCESS HEADERS: {0}'.format(headers))
1471 # Access resource and return response.
1472
1473 provider = ProviderClass(
1474 self, adapter=None, provider_name=credentials.provider_name)
1475 provider.credentials = credentials
1476
1477 return provider.access(url=url,
1478 params=params,
1479 method=method,
1480 headers=headers,
1481 body=body,
1482 max_redirects=max_redirects,
1483 content_parser=content_parser)
1484
1485 def async_access(self, *args, **kwargs):
1486 """
1487 Same as :meth:`.Authomatic.access` but runs asynchronously in a
1488 separate thread.
1489
1490 .. warning::
1491
1492 |async|
1493
1494 :returns:
1495 :class:`.Future` instance representing the separate thread.
1496
1497 """
1498
1499 return Future(self.access, *args, **kwargs)
1500
1501 def request_elements(
1502 self, credentials=None, url=None, method='GET', params=None,
1503 headers=None, body='', json_input=None, return_json=False
1504 ):
1505 """
1506 Creates request elements for accessing **protected resource of a
1507 user**. Required arguments are :data:`credentials` and :data:`url`. You
1508 can pass :data:`credentials`, :data:`url`, :data:`method`, and
1509 :data:`params` as a JSON object.
1510
1511 :param credentials:
1512 The **user's** credentials (can be serialized).
1513
1514 :param str url:
1515 The url of the protected resource.
1516
1517 :param str method:
1518 The HTTP method of the request.
1519
1520 :param dict params:
1521 Dictionary of request parameters.
1522
1523 :param dict headers:
1524 Dictionary of request headers.
1525
1526 :param str body:
1527 Body of ``POST``, ``PUT`` and ``PATCH`` requests.
1528
1529 :param str json_input:
1530 you can pass :data:`credentials`, :data:`url`, :data:`method`,
1531 :data:`params` and :data:`headers` in a JSON object.
1532 Values from arguments will be used for missing properties.
1533
1534 ::
1535
1536 {
1537 "credentials": "###",
1538 "url": "https://example.com/api",
1539 "method": "POST",
1540 "params": {
1541 "foo": "bar"
1542 },
1543 "headers": {
1544 "baz": "bing",
1545 "Authorization": "Bearer ###"
1546 },
1547 "body": "Foo bar baz bing."
1548 }
1549
1550 :param bool return_json:
1551 if ``True`` the function returns a json object.
1552
1553 ::
1554
1555 {
1556 "url": "https://example.com/api",
1557 "method": "POST",
1558 "params": {
1559 "access_token": "###",
1560 "foo": "bar"
1561 },
1562 "headers": {
1563 "baz": "bing",
1564 "Authorization": "Bearer ###"
1565 },
1566 "body": "Foo bar baz bing."
1567 }
1568
1569 :returns:
1570 :class:`.RequestElements` or JSON string.
1571
1572 """
1573
1574 # Parse values from JSON
1575 if json_input:
1576 parsed_input = json.loads(json_input)
1577
1578 credentials = parsed_input.get('credentials', credentials)
1579 url = parsed_input.get('url', url)
1580 method = parsed_input.get('method', method)
1581 params = parsed_input.get('params', params)
1582 headers = parsed_input.get('headers', headers)
1583 body = parsed_input.get('body', body)
1584
1585 if not credentials and url:
1586 raise RequestElementsError(
1587 'To create request elements, you must provide credentials '
1588 'and URL either as keyword arguments or in the JSON object!')
1589
1590 # Get the provider class
1591 credentials = Credentials.deserialize(self.config, credentials)
1592 ProviderClass = credentials.provider_class
1593
1594 # Create request elements
1595 request_elements = ProviderClass.create_request_elements(
1596 ProviderClass.PROTECTED_RESOURCE_REQUEST_TYPE,
1597 credentials=credentials,
1598 url=url,
1599 method=method,
1600 params=params,
1601 headers=headers,
1602 body=body)
1603
1604 if return_json:
1605 return request_elements.to_json()
1606
1607 else:
1608 return request_elements
1609
1610 def backend(self, adapter):
1611 """
1612 Converts a *request handler* to a JSON backend which you can use with
1613 :ref:`authomatic.js <js>`.
1614
1615 Just call it inside a *request handler* like this:
1616
1617 ::
1618
1619 class JSONHandler(webapp2.RequestHandler):
1620 def get(self):
1621 authomatic.backend(Webapp2Adapter(self))
1622
1623 :param adapter:
1624 The only argument is an :doc:`adapter <adapters>`.
1625
1626 The *request handler* will now accept these request parameters:
1627
1628 :param str type:
1629 Type of the request. Either ``auto``, ``fetch`` or ``elements``.
1630 Default is ``auto``.
1631
1632 :param str credentials:
1633 Serialized :class:`.Credentials`.
1634
1635 :param str url:
1636 URL of the **protected resource** request.
1637
1638 :param str method:
1639 HTTP method of the **protected resource** request.
1640
1641 :param str body:
1642 HTTP body of the **protected resource** request.
1643
1644 :param JSON params:
1645 HTTP params of the **protected resource** request as a JSON object.
1646
1647 :param JSON headers:
1648 HTTP headers of the **protected resource** request as a
1649 JSON object.
1650
1651 :param JSON json:
1652 You can pass all of the aforementioned params except ``type``
1653 in a JSON object.
1654
1655 .. code-block:: javascript
1656
1657 {
1658 "credentials": "######",
1659 "url": "https://example.com",
1660 "method": "POST",
1661 "params": {"foo": "bar"},
1662 "headers": {"baz": "bing"},
1663 "body": "the body of the request"
1664 }
1665
1666 Depending on the ``type`` param, the handler will either write
1667 a JSON object with *request elements* to the response,
1668 and add an ``Authomatic-Response-To: elements`` response header, ...
1669
1670 .. code-block:: javascript
1671
1672 {
1673 "url": "https://example.com/api",
1674 "method": "POST",
1675 "params": {
1676 "access_token": "###",
1677 "foo": "bar"
1678 },
1679 "headers": {
1680 "baz": "bing",
1681 "Authorization": "Bearer ###"
1682 }
1683 }
1684
1685 ... or make a fetch to the **protected resource** and forward
1686 it's response content, status and headers with an additional
1687 ``Authomatic-Response-To: fetch`` header to the response.
1688
1689 .. warning::
1690
1691 The backend will not work if you write anything to the
1692 response in the handler!
1693
1694 """
1695
1696 AUTHOMATIC_HEADER = 'Authomatic-Response-To'
1697
1698 # Collect request params
1699 request_type = adapter.params.get('type', 'auto')
1700 json_input = adapter.params.get('json')
1701 credentials = adapter.params.get('credentials')
1702 url = adapter.params.get('url')
1703 method = adapter.params.get('method', 'GET')
1704 body = adapter.params.get('body', '')
1705
1706 params = adapter.params.get('params')
1707 params = json.loads(params) if params else {}
1708
1709 headers = adapter.params.get('headers')
1710 headers = json.loads(headers) if headers else {}
1711
1712 ProviderClass = Credentials.deserialize(
1713 self.config, credentials).provider_class
1714
1715 if request_type == 'auto':
1716 # If there is a "callback" param, it's a JSONP request.
1717 jsonp = params.get('callback')
1718
1719 # JSONP is possible only with GET method.
1720 if ProviderClass.supports_jsonp and method is 'GET':
1721 request_type = 'elements'
1722 else:
1723 # Remove the JSONP callback
1724 if jsonp:
1725 params.pop('callback')
1726 request_type = 'fetch'
1727
1728 if request_type == 'fetch':
1729 # Access protected resource
1730 response = self.access(
1731 credentials, url, params, method, headers, body)
1732 result = response.content
1733
1734 # Forward status
1735 adapter.status = str(response.status) + ' ' + str(response.reason)
1736
1737 # Forward headers
1738 for k, v in response.getheaders():
1739 logging.info(' {0}: {1}'.format(k, v))
1740 adapter.set_header(k, v)
1741
1742 elif request_type == 'elements':
1743 # Create request elements
1744 if json_input:
1745 result = self.request_elements(
1746 json_input=json_input, return_json=True)
1747 else:
1748 result = self.request_elements(credentials=credentials,
1749 url=url,
1750 method=method,
1751 params=params,
1752 headers=headers,
1753 body=body,
1754 return_json=True)
1755
1756 adapter.set_header('Content-Type', 'application/json')
1757 else:
1758 result = '{"error": "Bad Request!"}'
1759
1760 # Add the authomatic header
1761 adapter.set_header(AUTHOMATIC_HEADER, request_type)
1762
1763 # Write result to response
1764 adapter.write(result)
@@ -0,0 +1,84 b''
1 # -*- coding: utf-8 -*-
2 """
3 Provides various exception types for the library.
4 """
5
6
7 class BaseError(Exception):
8 """
9 Base error for all errors.
10 """
11
12 def __init__(self, message, original_message='', url='', status=None):
13 super(BaseError, self).__init__(message)
14
15 #: Error message.
16 self.message = message
17
18 #: Original message.
19 self.original_message = original_message
20
21 #: URL related with the error.
22 self.url = url
23
24 #: HTTP status code related with the error.
25 self.status = status
26
27 def to_dict(self):
28 return self.__dict__
29
30
31 class ConfigError(BaseError):
32 pass
33
34
35 class SessionError(BaseError):
36 pass
37
38
39 class CredentialsError(BaseError):
40 pass
41
42
43 class HTTPError(BaseError):
44 pass
45
46
47 class CSRFError(BaseError):
48 pass
49
50
51 class ImportStringError(BaseError):
52 pass
53
54
55 class AuthenticationError(BaseError):
56 pass
57
58
59 class OAuth1Error(BaseError):
60 pass
61
62
63 class OAuth2Error(BaseError):
64 pass
65
66
67 class OpenIDError(BaseError):
68 pass
69
70
71 class CancellationError(BaseError):
72 pass
73
74
75 class FailureError(BaseError):
76 pass
77
78
79 class FetchError(BaseError):
80 pass
81
82
83 class RequestElementsError(BaseError):
84 pass
1 NO CONTENT: new file 100755
NO CONTENT: new file 100755
@@ -0,0 +1,47 b''
1 # -*- coding: utf-8 -*-
2 """
3 |flask| Extras
4 --------------
5
6 Utilities you can use when using this library with the |flask|_ framework.
7
8 Thanks to `Mark Steve Samson <http://marksteve.com>`_.
9 """
10
11 from __future__ import absolute_import
12 from functools import wraps
13
14 from authomatic.adapters import WerkzeugAdapter
15 from authomatic import Authomatic
16 from flask import make_response, request, session
17
18
19 class FlaskAuthomatic(Authomatic):
20 """
21 Flask Plugin for authomatic support.
22 """
23
24 result = None
25
26 def login(self, *login_args, **login_kwargs):
27 """
28 Decorator for Flask view functions.
29 """
30
31 def decorator(f):
32 @wraps(f)
33 def decorated(*args, **kwargs):
34 self.response = make_response()
35 adapter = WerkzeugAdapter(request, self.response)
36 login_kwargs.setdefault('session', session)
37 login_kwargs.setdefault('session_saver', self.session_saver)
38 self.result = super(FlaskAuthomatic, self).login(
39 adapter,
40 *login_args,
41 **login_kwargs)
42 return f(*args, **kwargs)
43 return decorated
44 return decorator
45
46 def session_saver(self):
47 session.modified = True
@@ -0,0 +1,241 b''
1 # -*- coding: utf-8 -*-
2 """
3 |gae| Extras
4 ------------
5
6 Utilities you can use when using this library on |gae|_.
7 """
8
9 from google.appengine.ext import ndb
10 from webapp2_extras import sessions
11
12 from authomatic import exceptions
13 from authomatic.extras import interfaces
14 from authomatic.extras.gae.openid import NDBOpenIDStore
15
16
17 __all__ = ['ndb_config', 'Webapp2Session']
18
19
20 class GAEError(exceptions.BaseError):
21 pass
22
23
24 class Webapp2Session(interfaces.BaseSession):
25 """
26 A simple wrapper for |webapp2|_ sessions. If you provide a session it wraps
27 it and adds the :meth:`.save` method.
28
29 If you don't provide a session it creates a new one but you must provide
30 the :data:`.secret`.
31
32 For more about |webapp2| sessions see:
33 http://webapp-improved.appspot.com/api/webapp2_extras/sessions.html.
34
35 """
36
37 def __init__(self, handler, session=None, secret=None,
38 cookie_name='webapp2authomatic', backend='memcache',
39 config=None):
40 """
41 .. warning::
42
43 Do not use the ``'securecookie'`` backend with
44 :class:`.providers.OpenID` provider. The
45 `python-openid`_ library saves **non json serializable** objects
46 to session which the ``'securecookie'`` backend cannot cope with.
47
48 :param handler:
49 A :class:`webapp2.RequestHandler` instance.
50
51 :param session:
52 A :class:`webapp2_extras.session.SessionDict` instance.
53
54 :param str secret:
55 The session secret.
56
57 :param str cookie_name:
58 The name of the session cookie.
59
60 :param backend:
61 The session backend. One of ``'memcache'`` or ``'datastore'``.
62
63 :param config:
64 The session config.
65
66 """
67
68 self.handler = handler
69
70 if session is None:
71 if not secret:
72 raise GAEError('Either session or secret must be specified!')
73 else:
74 # Create new session.
75 cfg = config or dict(
76 secret_key=secret, cookie_name=cookie_name)
77 session_store = sessions.SessionStore(handler.request, cfg)
78 self.session_dict = session_store.get_session(backend=backend)
79 else:
80 # Use supplied session.
81 self.session_dict = session
82
83 def save(self):
84 return self.session_dict.container.save_session(self.handler.response)
85
86 def __setitem__(self, key, value):
87 return self.session_dict.__setitem__(key, value)
88
89 def __getitem__(self, key):
90 return self.session_dict.__getitem__(key)
91
92 def __delitem__(self, key):
93 return self.session_dict.__delitem__(key)
94
95 def get(self, key):
96 return self.session_dict.get(key)
97
98
99 class NDBConfig(ndb.Model):
100 """
101 |gae| `NDB <https://developers.google.com/appengine/docs/python/ndb/>`_
102 based :doc:`config`.
103
104 .. note::
105
106 By :class:`.OpenID` provider uses :class:`.NDBOpenIDStore`
107 as default :attr:`.OpenID.store`.
108
109 """
110
111 # General properties
112 provider_name = ndb.StringProperty()
113 class_ = ndb.StringProperty()
114
115 # AuthorizationProvider properties
116 provider_id = ndb.IntegerProperty()
117 consumer_key = ndb.StringProperty()
118 consumer_secret = ndb.StringProperty()
119
120 # OAuth2 properties
121 scope = ndb.StringProperty()
122 offline = ndb.BooleanProperty()
123
124 # AuthenticationProvider properties
125 identifier_param = ndb.StringProperty()
126
127 @classmethod
128 def get(cls, key, default=None):
129 """
130 Resembles the :meth:`dict.get` method.
131
132 :returns:
133 A configuration dictionary for specified provider.
134
135 """
136
137 # Query datastore.
138 result = cls.query(cls.provider_name == key).get()
139
140 if result:
141 result_dict = result.to_dict()
142
143 # Use NDBOpenIDStore by default
144 result_dict['store'] = NDBOpenIDStore
145
146 # Convert coma-separated values to list. Currently only scope is
147 # csv.
148 for i in ('scope', ):
149 prop = result_dict.get(i)
150 if prop:
151 result_dict[i] = [s.strip() for s in prop.split(',')]
152 else:
153 result_dict[i] = None
154
155 return result_dict
156 else:
157 return default
158
159 @classmethod
160 def values(cls):
161 """
162 Resembles the :meth:`dict.values` method.
163 """
164
165 # get all items
166 results = cls.query().fetch()
167 # return list of dictionaries
168 return [result.to_dict() for result in results]
169
170 @classmethod
171 def initialize(cls):
172 """
173 Creates an **"Example"** entity of kind **"NDBConfig"** in the
174 datastore if the model is empty and raises and error to inform you that
175 you should populate the model with data.
176
177 .. note::
178
179 The *Datastore Viewer* in the ``_ah/admin/`` won't let you add
180 properties to a model if there is not an entity with that
181 property already. Therefore it is a good idea to keep the
182 **"Example"** entity (which has all possible properties set) in
183 the datastore.
184
185 """
186
187 if not len(cls.query().fetch()):
188
189 example = cls.get_or_insert('Example')
190
191 example.class_ = 'Provider class e.g. ' + \
192 '"authomatic.providers.oauth2.Facebook".'
193 example.provider_name = 'Your custom provider name e.g. "fb".'
194
195 # AuthorizationProvider
196 example.consumer_key = 'Consumer key.'
197 example.consumer_secret = 'Consumer secret'
198 example.provider_id = 1
199
200 # OAuth2
201 example.scope = 'coma, separated, list, of, scopes'
202
203 # AuthenticationProvider
204 example.identifier_param = 'Querystring parameter for claimed ' + \
205 'id. default is "id"'
206
207 # Save the example
208 example.put()
209
210 # Raise an information error.
211 raise GAEError(
212 'A NDBConfig data model was created! Go to Datastore Viewer '
213 'in your dashboard and populate it with data!')
214
215
216 def ndb_config():
217 """
218 Allows you to have a **datastore** :doc:`config` instead of a hardcoded
219 one.
220
221 This function creates an **"Example"** entity of kind **"NDBConfig"** in
222 the datastore if the model is empty and raises and error to inform you
223 that you should populate the model with data.
224
225 .. note::
226
227 The *Datastore Viewer* of the |gae|_ admin won't let you add
228 properties to a model if there is not an entity with that property
229 already. Therefore it is a good idea to keep the **"Example"**
230 entity (which has all properties set) in the datastore.
231
232 :raises:
233 :exc:`.GAEError`
234
235 :returns:
236 :class:`.NDBConfig`
237
238 """
239
240 NDBConfig.initialize()
241 return NDBConfig
@@ -0,0 +1,156 b''
1 # -*- coding: utf-8 -*-
2
3 # We need absolute import to import from openid library which has the same
4 # name as this module
5 from __future__ import absolute_import
6 import logging
7 import datetime
8
9 from google.appengine.ext import ndb
10 import openid.store.interface
11
12
13 class NDBOpenIDStore(ndb.Expando, openid.store.interface.OpenIDStore):
14 """
15 |gae| `NDB <https://developers.google.com/appengine/docs/python/ndb/>`_
16 based implementation of the :class:`openid.store.interface.OpenIDStore`
17 interface of the `python-openid`_ library.
18 """
19
20 serialized = ndb.StringProperty()
21 expiration_date = ndb.DateTimeProperty()
22 # we need issued to sort by most recently issued
23 issued = ndb.IntegerProperty()
24
25 @staticmethod
26 def _log(*args, **kwargs):
27 pass
28
29 @classmethod
30 def storeAssociation(cls, server_url, association):
31 # store an entity with key = server_url
32
33 issued = datetime.datetime.fromtimestamp(association.issued)
34 lifetime = datetime.timedelta(0, association.lifetime)
35
36 expiration_date = issued + lifetime
37 entity = cls.get_or_insert(
38 association.handle, parent=ndb.Key(
39 'ServerUrl', server_url))
40
41 entity.serialized = association.serialize()
42 entity.expiration_date = expiration_date
43 entity.issued = association.issued
44
45 cls._log(
46 logging.DEBUG,
47 u'NDBOpenIDStore: Putting OpenID association to datastore.')
48
49 entity.put()
50
51 @classmethod
52 def cleanupAssociations(cls):
53 # query for all expired
54 cls._log(
55 logging.DEBUG,
56 u'NDBOpenIDStore: Querying datastore for OpenID associations.')
57 query = cls.query(cls.expiration_date <= datetime.datetime.now())
58
59 # fetch keys only
60 expired = query.fetch(keys_only=True)
61
62 # delete all expired
63 cls._log(
64 logging.DEBUG,
65 u'NDBOpenIDStore: Deleting expired OpenID associations from datastore.')
66 ndb.delete_multi(expired)
67
68 return len(expired)
69
70 @classmethod
71 def getAssociation(cls, server_url, handle=None):
72 cls.cleanupAssociations()
73
74 if handle:
75 key = ndb.Key('ServerUrl', server_url, cls, handle)
76 cls._log(
77 logging.DEBUG,
78 u'NDBOpenIDStore: Getting OpenID association from datastore by key.')
79 entity = key.get()
80 else:
81 # return most recently issued association
82 cls._log(
83 logging.DEBUG,
84 u'NDBOpenIDStore: Querying datastore for OpenID associations by ancestor.')
85 entity = cls.query(ancestor=ndb.Key(
86 'ServerUrl', server_url)).order(-cls.issued).get()
87
88 if entity and entity.serialized:
89 return openid.association.Association.deserialize(
90 entity.serialized)
91
92 @classmethod
93 def removeAssociation(cls, server_url, handle):
94 key = ndb.Key('ServerUrl', server_url, cls, handle)
95 cls._log(
96 logging.DEBUG,
97 u'NDBOpenIDStore: Getting OpenID association from datastore by key.')
98 if key.get():
99 cls._log(
100 logging.DEBUG,
101 u'NDBOpenIDStore: Deleting OpenID association from datastore.')
102 key.delete()
103 return True
104
105 @classmethod
106 def useNonce(cls, server_url, timestamp, salt):
107
108 # check whether there is already an entity with the same ancestor path
109 # in the datastore
110 key = ndb.Key(
111 'ServerUrl',
112 str(server_url) or 'x',
113 'TimeStamp',
114 str(timestamp),
115 cls,
116 str(salt))
117
118 cls._log(
119 logging.DEBUG,
120 u'NDBOpenIDStore: Getting OpenID nonce from datastore by key.')
121 result = key.get()
122
123 if result:
124 # if so, the nonce is not valid so return False
125 cls._log(
126 logging.WARNING,
127 u'NDBOpenIDStore: Nonce was already used!')
128 return False
129 else:
130 # if not, store the key to datastore and return True
131 nonce = cls(key=key)
132 nonce.expiration_date = datetime.datetime.fromtimestamp(
133 timestamp) + datetime.timedelta(0, openid.store.nonce.SKEW)
134 cls._log(
135 logging.DEBUG,
136 u'NDBOpenIDStore: Putting new nonce to datastore.')
137 nonce.put()
138 return True
139
140 @classmethod
141 def cleanupNonces(cls):
142 # get all expired nonces
143 cls._log(
144 logging.DEBUG,
145 u'NDBOpenIDStore: Querying datastore for OpenID nonces ordered by expiration date.')
146 expired = cls.query().filter(
147 cls.expiration_date <= datetime.datetime.now()).fetch(
148 keys_only=True)
149
150 # delete all expired
151 cls._log(
152 logging.DEBUG,
153 u'NDBOpenIDStore: Deleting expired OpenID nonces from datastore.')
154 ndb.delete_multi(expired)
155
156 return len(expired)
@@ -0,0 +1,73 b''
1 # -*- coding: utf-8 -*-
2 """
3 Interfaces
4 ^^^^^^^^^^
5
6 If you want to implement framework specific extras, use these abstract
7 classes as bases:
8
9 """
10
11 import abc
12
13
14 class BaseSession(object):
15 """
16 Abstract class for custom session implementations.
17 """
18
19 __metaclass__ = abc.ABCMeta
20
21 @abc.abstractmethod
22 def save(self):
23 """
24 Called only once per request.
25
26 Should implement a mechanism for setting the the session
27 **cookie** and saving the session **data** to storage.
28
29 """
30
31 @abc.abstractmethod
32 def __setitem__(self, key, value):
33 """
34 Same as :meth:`dict.__setitem__`.
35 """
36
37 @abc.abstractmethod
38 def __getitem__(self, key):
39 """
40 Same as :meth:`dict.__getitem__`.
41 """
42
43 @abc.abstractmethod
44 def __delitem__(self, key):
45 """
46 Same as :meth:`dict.__delitem__`.
47 """
48
49 @abc.abstractmethod
50 def get(self, key):
51 """
52 Same as :meth:`dict.get`.
53 """
54
55
56 class BaseConfig(object):
57 """
58 Abstract class for :doc:`config` implementations.
59 """
60
61 __metaclass__ = abc.ABCMeta
62
63 @abc.abstractmethod
64 def get(self, key):
65 """
66 Same as :attr:`dict.get`.
67 """
68
69 @abc.abstractmethod
70 def values(self):
71 """
72 Same as :meth:`dict.values`.
73 """
This diff has been collapsed as it changes many lines, (1012 lines changed) Show them Hide them
@@ -0,0 +1,1012 b''
1 # -*- coding: utf-8 -*-
2 """
3 Abstract Classes for Providers
4 ------------------------------
5
6 Abstract base classes for implementation of protocol specific providers.
7
8 .. note::
9
10 Attributes prefixed with ``_x_`` serve the purpose of unification
11 of differences across providers.
12
13 .. autosummary::
14
15 login_decorator
16 BaseProvider
17 AuthorizationProvider
18 AuthenticationProvider
19
20 """
21
22 import abc
23 import base64
24 import hashlib
25 import logging
26 import random
27 import sys
28 import traceback
29 import uuid
30
31 import authomatic.core
32 from authomatic.exceptions import (
33 ConfigError,
34 FetchError,
35 CredentialsError,
36 )
37 from authomatic import six
38 from authomatic.six.moves import urllib_parse as parse
39 from authomatic.six.moves import http_client
40 from authomatic.exceptions import CancellationError
41
42 __all__ = [
43 'BaseProvider',
44 'AuthorizationProvider',
45 'AuthenticationProvider',
46 'login_decorator']
47
48
49 def _error_traceback_html(exc_info, traceback_):
50 """
51 Generates error traceback HTML.
52
53 :param tuple exc_info:
54 Output of :func:`sys.exc_info` function.
55
56 :param traceback:
57 Output of :func:`traceback.format_exc` function.
58
59 """
60
61 html = """
62 <html>
63 <head>
64 <title>ERROR: {error}</title>
65 </head>
66 <body style="font-family: sans-serif">
67 <h4>The Authomatic library encountered an error!</h4>
68 <h1>{error}</h1>
69 <pre>{traceback}</pre>
70 </body>
71 </html>
72 """
73
74 return html.format(error=exc_info[1], traceback=traceback_)
75
76
77 def login_decorator(func):
78 """
79 Decorate the :meth:`.BaseProvider.login` implementations with this
80 decorator.
81
82 Provides mechanism for error reporting and returning result which
83 makes the :meth:`.BaseProvider.login` implementation cleaner.
84
85 """
86
87 def wrap(provider, *args, **kwargs):
88 error = None
89 result = authomatic.core.LoginResult(provider)
90
91 try:
92 func(provider, *args, **kwargs)
93 except Exception as e: # pylint:disable=broad-except
94 if provider.settings.report_errors:
95 error = e
96 if not isinstance(error, CancellationError):
97 provider._log(
98 logging.ERROR,
99 u'Reported suppressed exception: {0}!'.format(
100 repr(error)),
101 exc_info=1)
102 else:
103 if provider.settings.debug:
104 # TODO: Check whether it actually works without middleware
105 provider.write(
106 _error_traceback_html(
107 sys.exc_info(),
108 traceback.format_exc()))
109 raise
110
111 # If there is user or error the login procedure has finished
112 if provider.user or error:
113 result = authomatic.core.LoginResult(provider)
114 # Add error to result
115 result.error = error
116
117 # delete session cookie
118 if isinstance(provider.session, authomatic.core.Session):
119 provider.session.delete()
120
121 provider._log(logging.INFO, u'Procedure finished.')
122
123 if provider.callback:
124 provider.callback(result)
125 return result
126 else:
127 # Save session
128 provider.save_session()
129
130 return wrap
131
132
133 class BaseProvider(object):
134 """
135 Abstract base class for all providers.
136 """
137
138 PROVIDER_TYPE_ID = 0
139
140 _repr_ignore = ('user',)
141
142 __metaclass__ = abc.ABCMeta
143
144 supported_user_attributes = authomatic.core.SupportedUserAttributes()
145
146 def __init__(self, settings, adapter, provider_name, session=None,
147 session_saver=None, callback=None, js_callback=None,
148 prefix='authomatic', **kwargs):
149
150 self.settings = settings
151 self.adapter = adapter
152
153 self.session = session
154 self.save_session = session_saver
155
156 #: :class:`str` The provider name as specified in the :doc:`config`.
157 self.name = provider_name
158
159 #: :class:`callable` An optional callback called when the login
160 #: procedure is finished with :class:`.core.LoginResult` passed as
161 #: argument.
162 self.callback = callback
163
164 #: :class:`str` Name of an optional javascript callback.
165 self.js_callback = js_callback
166
167 #: :class:`.core.User`.
168 self.user = None
169
170 #: :class:`bool` If ``True``, the
171 #: :attr:`.BaseProvider.user_authorization_url` will be displayed
172 #: in a *popup mode*, if the **provider** supports it.
173 self.popup = self._kwarg(kwargs, 'popup')
174
175 @property
176 def url(self):
177 return self.adapter.url
178
179 @property
180 def params(self):
181 return self.adapter.params
182
183 def write(self, value):
184 self.adapter.write(value)
185
186 def set_header(self, key, value):
187 self.adapter.set_header(key, value)
188
189 def set_status(self, status):
190 self.adapter.set_status(status)
191
192 def redirect(self, url):
193 self.set_status('302 Found')
194 self.set_header('Location', url)
195
196 # ========================================================================
197 # Abstract methods
198 # ========================================================================
199
200 @abc.abstractmethod
201 def login(self):
202 """
203 Launches the *login procedure* to get **user's credentials** from
204 **provider**.
205
206 Should be decorated with :func:`.login_decorator`. The *login
207 procedure* is considered finished when the :attr:`.user`
208 attribute is not empty when the method runs out of it's flow or
209 when there are errors.
210
211 """
212
213 # ========================================================================
214 # Exposed methods
215 # ========================================================================
216
217 def to_dict(self):
218 """
219 Converts the provider instance to a :class:`dict`.
220
221 :returns:
222 :class:`dict`
223
224 """
225
226 return dict(name=self.name,
227 id=getattr(self, 'id', None),
228 type_id=self.type_id,
229 type=self.get_type(),
230 scope=getattr(self, 'scope', None),
231 user=self.user.id if self.user else None)
232
233 @classmethod
234 def get_type(cls):
235 """
236 Returns the provider type.
237
238 :returns:
239 :class:`str` The full dotted path to base class e.g.
240 :literal:`"authomatic.providers.oauth2.OAuth2"`.
241
242 """
243
244 return cls.__module__ + '.' + cls.__bases__[0].__name__
245
246 def update_user(self):
247 """
248 Updates and returns :attr:`.user`.
249
250 :returns:
251 :class:`.User`
252
253 """
254
255 # ========================================================================
256 # Internal methods
257 # ========================================================================
258
259 @property
260 def type_id(self):
261 pass
262
263 def _kwarg(self, kwargs, kwname, default=None):
264 """
265 Resolves keyword arguments from constructor or :doc:`config`.
266
267 .. note::
268
269 The keyword arguments take this order of precedence:
270
271 1. Arguments passed to constructor through the
272 :func:`authomatic.login`.
273 2. Provider specific arguments from :doc:`config`.
274 3. Arguments from :doc:`config` set in the ``__defaults__`` key.
275 2. The value from :data:`default` argument.
276
277 :param dict kwargs:
278 Keyword arguments dictionary.
279 :param str kwname:
280 Name of the desired keyword argument.
281
282 """
283
284 return kwargs.get(kwname) or \
285 self.settings.config.get(self.name, {}).get(kwname) or \
286 self.settings.config.get('__defaults__', {}).get(kwname) or \
287 default
288
289 def _session_key(self, key):
290 """
291 Generates session key string.
292
293 :param str key:
294 e.g. ``"authomatic:facebook:key"``
295
296 """
297
298 return '{0}:{1}:{2}'.format(self.settings.prefix, self.name, key)
299
300 def _session_set(self, key, value):
301 """
302 Saves a value to session.
303 """
304
305 self.session[self._session_key(key)] = value
306
307 def _session_get(self, key):
308 """
309 Retrieves a value from session.
310 """
311
312 return self.session.get(self._session_key(key))
313
314 @staticmethod
315 def csrf_generator(secret):
316 """
317 Generates CSRF token.
318
319 Inspired by this article:
320 http://blog.ptsecurity.com/2012/10/random-number-security-in-python.html
321
322 :returns:
323 :class:`str` Random unguessable string.
324
325 """
326
327 # Create hash from random string plus salt.
328 hashed = hashlib.md5(uuid.uuid4().bytes + six.b(secret)).hexdigest()
329
330 # Each time return random portion of the hash.
331 span = 5
332 shift = random.randint(0, span)
333 return hashed[shift:shift - span - 1]
334
335 @classmethod
336 def _log(cls, level, msg, **kwargs):
337 """
338 Logs a message with pre-formatted prefix.
339
340 :param int level:
341 Logging level as specified in the
342 `login module <http://docs.python.org/2/library/logging.html>`_ of
343 Python standard library.
344
345 :param str msg:
346 The actual message.
347
348 """
349
350 logger = getattr(cls, '_logger', None) or authomatic.core._logger
351 logger.log(
352 level, ': '.join(
353 ('authomatic', cls.__name__, msg)), **kwargs)
354
355 def _fetch(self, url, method='GET', params=None, headers=None,
356 body='', max_redirects=5, content_parser=None):
357 """
358 Fetches a URL.
359
360 :param str url:
361 The URL to fetch.
362
363 :param str method:
364 HTTP method of the request.
365
366 :param dict params:
367 Dictionary of request parameters.
368
369 :param dict headers:
370 HTTP headers of the request.
371
372 :param str body:
373 Body of ``POST``, ``PUT`` and ``PATCH`` requests.
374
375 :param int max_redirects:
376 Number of maximum HTTP redirects to follow.
377
378 :param function content_parser:
379 A callable to be used to parse the :attr:`.Response.data`
380 from :attr:`.Response.content`.
381
382 """
383 # 'magic' using _kwarg method
384 # pylint:disable=no-member
385 params = params or {}
386 params.update(self.access_params)
387
388 headers = headers or {}
389 headers.update(self.access_headers)
390
391 scheme, host, path, query, fragment = parse.urlsplit(url)
392 query = parse.urlencode(params)
393
394 if method in ('POST', 'PUT', 'PATCH'):
395 if not body:
396 # Put querystring to body
397 body = query
398 query = ''
399 headers.update(
400 {'Content-Type': 'application/x-www-form-urlencoded'})
401 request_path = parse.urlunsplit(('', '', path or '', query or '', ''))
402
403 self._log(logging.DEBUG, u' \u251C\u2500 host: {0}'.format(host))
404 self._log(
405 logging.DEBUG,
406 u' \u251C\u2500 path: {0}'.format(request_path))
407 self._log(logging.DEBUG, u' \u251C\u2500 method: {0}'.format(method))
408 self._log(logging.DEBUG, u' \u251C\u2500 body: {0}'.format(body))
409 self._log(logging.DEBUG, u' \u251C\u2500 params: {0}'.format(params))
410 self._log(logging.DEBUG, u' \u2514\u2500 headers: {0}'.format(headers))
411
412 # Connect
413 if scheme.lower() == 'https':
414 connection = http_client.HTTPSConnection(host)
415 else:
416 connection = http_client.HTTPConnection(host)
417
418 try:
419 connection.request(method, request_path, body, headers)
420 except Exception as e:
421 raise FetchError('Fetching URL failed',
422 original_message=str(e),
423 url=request_path)
424
425 response = connection.getresponse()
426 location = response.getheader('Location')
427
428 if response.status in (300, 301, 302, 303, 307) and location:
429 if location == url:
430 raise FetchError('Url redirects to itself!',
431 url=location,
432 status=response.status)
433
434 elif max_redirects > 0:
435 remaining_redirects = max_redirects - 1
436
437 self._log(logging.DEBUG, u'Redirecting to {0}'.format(url))
438 self._log(logging.DEBUG, u'Remaining redirects: {0}'
439 .format(remaining_redirects))
440
441 # Call this method again.
442 response = self._fetch(url=location,
443 params=params,
444 method=method,
445 headers=headers,
446 max_redirects=remaining_redirects)
447
448 else:
449 raise FetchError('Max redirects reached!',
450 url=location,
451 status=response.status)
452 else:
453 self._log(logging.DEBUG, u'Got response:')
454 self._log(logging.DEBUG, u' \u251C\u2500 url: {0}'.format(url))
455 self._log(
456 logging.DEBUG,
457 u' \u251C\u2500 status: {0}'.format(
458 response.status))
459 self._log(
460 logging.DEBUG,
461 u' \u2514\u2500 headers: {0}'.format(
462 response.getheaders()))
463
464 return authomatic.core.Response(response, content_parser)
465
466 def _update_or_create_user(self, data, credentials=None, content=None):
467 """
468 Updates or creates :attr:`.user`.
469
470 :returns:
471 :class:`.User`
472
473 """
474
475 if not self.user:
476 self.user = authomatic.core.User(self, credentials=credentials)
477
478 self.user.content = content
479 self.user.data = data
480
481 # Update.
482 for key in self.user.__dict__:
483 # Exclude data.
484 if key not in ('data', 'content'):
485 # Extract every data item whose key matches the user
486 # property name, but only if it has a value.
487 value = data.get(key)
488 if value:
489 setattr(self.user, key, value)
490
491 # Handle different structure of data by different providers.
492 self.user = self._x_user_parser(self.user, data)
493
494 if self.user.id:
495 self.user.id = str(self.user.id)
496
497 # TODO: Move to User
498 # If there is no user.name,
499 if not self.user.name:
500 if self.user.first_name and self.user.last_name:
501 # Create it from first name and last name if available.
502 self.user.name = ' '.join((self.user.first_name,
503 self.user.last_name))
504 else:
505 # Or use one of these.
506 self.user.name = (self.user.username or self.user.nickname or
507 self.user.first_name or self.user.last_name)
508
509 if not self.user.location:
510 if self.user.city and self.user.country:
511 self.user.location = '{0}, {1}'.format(self.user.city,
512 self.user.country)
513 else:
514 self.user.location = self.user.city or self.user.country
515
516 return self.user
517
518 @staticmethod
519 def _x_user_parser(user, data):
520 """
521 Handles different structure of user info data by different providers.
522
523 :param user:
524 :class:`.User`
525 :param dict data:
526 User info data returned by provider.
527
528 """
529
530 return user
531
532 @staticmethod
533 def _http_status_in_category(status, category):
534 """
535 Checks whether a HTTP status code is in the category denoted by the
536 hundreds digit.
537 """
538
539 assert category < 10, 'HTTP status category must be a one-digit int!'
540 cat = category * 100
541 return status >= cat and status < cat + 100
542
543
544 class AuthorizationProvider(BaseProvider):
545 """
546 Base provider for *authorization protocols* i.e. protocols which allow a
547 **provider** to authorize a **consumer** to access **protected resources**
548 of a **user**.
549
550 e.g. `OAuth 2.0 <http://oauth.net/2/>`_ or `OAuth 1.0a
551 <http://oauth.net/core/1.0a/>`_.
552
553 """
554
555 USER_AUTHORIZATION_REQUEST_TYPE = 2
556 ACCESS_TOKEN_REQUEST_TYPE = 3
557 PROTECTED_RESOURCE_REQUEST_TYPE = 4
558 REFRESH_TOKEN_REQUEST_TYPE = 5
559
560 BEARER = 'Bearer'
561
562 _x_term_dict = {}
563
564 #: If ``True`` the provider doesn't support Cross-site HTTP requests.
565 same_origin = True
566
567 #: :class:`bool` Whether the provider supports JSONP requests.
568 supports_jsonp = False
569
570 # Whether to use the HTTP Authorization header.
571 _x_use_authorization_header = True
572
573 def __init__(self, *args, **kwargs):
574 """
575 Accepts additional keyword arguments:
576
577 :arg str consumer_key:
578 The *key* assigned to our application (**consumer**) by the
579 **provider**.
580
581 :arg str consumer_secret:
582 The *secret* assigned to our application (**consumer**) by the
583 **provider**.
584
585 :arg int id:
586 A unique numeric ID used to serialize :class:`.Credentials`.
587
588 :arg dict user_authorization_params:
589 A dictionary of additional request parameters for
590 **user authorization request**.
591
592 :arg dict access_token_params:
593 A dictionary of additional request parameters for
594 **access_with_credentials token request**.
595
596 :arg dict access_headers:
597 A dictionary of default HTTP headers that will be used when
598 accessing **user's** protected resources.
599 Applied by :meth:`.access()`, :meth:`.update_user()` and
600 :meth:`.User.update()`
601
602 :arg dict access_params:
603 A dictionary of default query string parameters that will be used
604 when accessing **user's** protected resources.
605 Applied by :meth:`.access()`, :meth:`.update_user()` and
606 :meth:`.User.update()`
607
608 """
609
610 super(AuthorizationProvider, self).__init__(*args, **kwargs)
611
612 self.consumer_key = self._kwarg(kwargs, 'consumer_key')
613 self.consumer_secret = self._kwarg(kwargs, 'consumer_secret')
614
615 self.user_authorization_params = self._kwarg(
616 kwargs, 'user_authorization_params', {})
617
618 self.access_token_headers = self._kwarg(
619 kwargs, 'user_authorization_headers', {})
620 self.access_token_params = self._kwarg(
621 kwargs, 'access_token_params', {})
622
623 self.id = self._kwarg(kwargs, 'id')
624
625 self.access_headers = self._kwarg(kwargs, 'access_headers', {})
626 self.access_params = self._kwarg(kwargs, 'access_params', {})
627
628 #: :class:`.Credentials` to access **user's protected resources**.
629 self.credentials = authomatic.core.Credentials(
630 self.settings.config, provider=self)
631
632 #: Response of the *access token request*.
633 self.access_token_response = None
634
635 # ========================================================================
636 # Abstract properties
637 # ========================================================================
638
639 @abc.abstractproperty
640 def user_authorization_url(self):
641 """
642 :class:`str` URL to which we redirect the **user** to grant our app
643 i.e. the **consumer** an **authorization** to access his
644 **protected resources**. See
645 http://tools.ietf.org/html/rfc6749#section-4.1.1 and
646 http://oauth.net/core/1.0a/#auth_step2.
647 """
648
649 @abc.abstractproperty
650 def access_token_url(self):
651 """
652 :class:`str` URL where we can get the *access token* to access
653 **protected resources** of a **user**. See
654 http://tools.ietf.org/html/rfc6749#section-4.1.3 and
655 http://oauth.net/core/1.0a/#auth_step3.
656 """
657
658 @abc.abstractproperty
659 def user_info_url(self):
660 """
661 :class:`str` URL where we can get the **user** info.
662 see http://tools.ietf.org/html/rfc6749#section-7 and
663 http://oauth.net/core/1.0a/#anchor12.
664 """
665
666 # ========================================================================
667 # Abstract methods
668 # ========================================================================
669
670 @abc.abstractmethod
671 def to_tuple(self, credentials):
672 """
673 Must convert :data:`credentials` to a :class:`tuple` to be used by
674 :meth:`.Credentials.serialize`.
675
676 .. warning::
677
678 |classmethod|
679
680 :param credentials:
681 :class:`.Credentials`
682
683 :returns:
684 :class:`tuple`
685
686 """
687
688 @abc.abstractmethod
689 def reconstruct(self, deserialized_tuple, credentials, cfg):
690 """
691 Must convert the :data:`deserialized_tuple` back to
692 :class:`.Credentials`.
693
694 .. warning::
695
696 |classmethod|
697
698 :param tuple deserialized_tuple:
699 A tuple whose first index is the :attr:`.id` and the rest
700 are all the items of the :class:`tuple` created by
701 :meth:`.to_tuple`.
702
703 :param credentials:
704 A :class:`.Credentials` instance.
705
706 :param dict cfg:
707 Provider configuration from :doc:`config`.
708
709 """
710
711 @abc.abstractmethod
712 def create_request_elements(self, request_type, credentials,
713 url, method='GET', params=None, headers=None,
714 body=''):
715 """
716 Must return :class:`.RequestElements`.
717
718 .. warning::
719
720 |classmethod|
721
722 :param int request_type:
723 Type of the request specified by one of the class's constants.
724
725 :param credentials:
726 :class:`.Credentials` of the **user** whose
727 **protected resource** we want to access.
728
729 :param str url:
730 URL of the request.
731
732 :param str method:
733 HTTP method of the request.
734
735 :param dict params:
736 Dictionary of request parameters.
737
738 :param dict headers:
739 Dictionary of request headers.
740
741 :param str body:
742 Body of ``POST``, ``PUT`` and ``PATCH`` requests.
743
744 :returns:
745 :class:`.RequestElements`
746
747 """
748
749 # ========================================================================
750 # Exposed methods
751 # ========================================================================
752
753 @property
754 def type_id(self):
755 """
756 A short string representing the provider implementation id used for
757 serialization of :class:`.Credentials` and to identify the type of
758 provider in JavaScript.
759
760 The part before hyphen denotes the type of the provider, the part
761 after hyphen denotes the class id e.g.
762 ``oauth2.Facebook.type_id = '2-5'``,
763 ``oauth1.Twitter.type_id = '1-5'``.
764
765 """
766
767 cls = self.__class__
768 mod = sys.modules.get(cls.__module__)
769
770 return str(self.PROVIDER_TYPE_ID) + '-' + \
771 str(mod.PROVIDER_ID_MAP.index(cls))
772
773 def access(self, url, params=None, method='GET', headers=None,
774 body='', max_redirects=5, content_parser=None):
775 """
776 Fetches the **protected resource** of an authenticated **user**.
777
778 :param credentials:
779 The **user's** :class:`.Credentials` (serialized or normal).
780
781 :param str url:
782 The URL of the **protected resource**.
783
784 :param str method:
785 HTTP method of the request.
786
787 :param dict headers:
788 HTTP headers of the request.
789
790 :param str body:
791 Body of ``POST``, ``PUT`` and ``PATCH`` requests.
792
793 :param int max_redirects:
794 Maximum number of HTTP redirects to follow.
795
796 :param function content_parser:
797 A function to be used to parse the :attr:`.Response.data`
798 from :attr:`.Response.content`.
799
800 :returns:
801 :class:`.Response`
802
803 """
804
805 if not self.user and not self.credentials:
806 raise CredentialsError(u'There is no authenticated user!')
807
808 headers = headers or {}
809
810 self._log(
811 logging.INFO,
812 u'Accessing protected resource {0}.'.format(url))
813
814 request_elements = self.create_request_elements(
815 request_type=self.PROTECTED_RESOURCE_REQUEST_TYPE,
816 credentials=self.credentials,
817 url=url,
818 body=body,
819 params=params,
820 headers=headers,
821 method=method
822 )
823
824 response = self._fetch(*request_elements,
825 max_redirects=max_redirects,
826 content_parser=content_parser)
827
828 self._log(
829 logging.INFO,
830 u'Got response. HTTP status = {0}.'.format(
831 response.status))
832 return response
833
834 def async_access(self, *args, **kwargs):
835 """
836 Same as :meth:`.access` but runs asynchronously in a separate thread.
837
838 .. warning::
839
840 |async|
841
842 :returns:
843 :class:`.Future` instance representing the separate thread.
844
845 """
846
847 return authomatic.core.Future(self.access, *args, **kwargs)
848
849 def update_user(self):
850 """
851 Updates the :attr:`.BaseProvider.user`.
852
853 .. warning::
854 Fetches the :attr:`.user_info_url`!
855
856 :returns:
857 :class:`.UserInfoResponse`
858
859 """
860 if self.user_info_url:
861 response = self._access_user_info()
862 self.user = self._update_or_create_user(response.data,
863 content=response.content)
864 return authomatic.core.UserInfoResponse(self.user,
865 response.httplib_response)
866
867 # ========================================================================
868 # Internal methods
869 # ========================================================================
870
871 @classmethod
872 def _authorization_header(cls, credentials):
873 """
874 Creates authorization headers if the provider supports it. See:
875 http://en.wikipedia.org/wiki/Basic_access_authentication.
876
877 :param credentials:
878 :class:`.Credentials`
879
880 :returns:
881 Headers as :class:`dict`.
882
883 """
884
885 if cls._x_use_authorization_header:
886 res = ':'.join(
887 (credentials.consumer_key,
888 credentials.consumer_secret))
889 res = base64.b64encode(six.b(res)).decode()
890 return {'Authorization': 'Basic {0}'.format(res)}
891 else:
892 return {}
893
894 def _check_consumer(self):
895 """
896 Validates the :attr:`.consumer`.
897 """
898
899 # 'magic' using _kwarg method
900 # pylint:disable=no-member
901 if not self.consumer.key:
902 raise ConfigError(
903 'Consumer key not specified for provider {0}!'.format(
904 self.name))
905
906 if not self.consumer.secret:
907 raise ConfigError(
908 'Consumer secret not specified for provider {0}!'.format(
909 self.name))
910
911 @staticmethod
912 def _split_url(url):
913 """
914 Splits given url to url base and params converted to list of tuples.
915 """
916
917 split = parse.urlsplit(url)
918 base = parse.urlunsplit((split.scheme, split.netloc, split.path, 0, 0))
919 params = parse.parse_qsl(split.query, True)
920
921 return base, params
922
923 @classmethod
924 def _x_request_elements_filter(
925 cls, request_type, request_elements, credentials):
926 """
927 Override this to handle special request requirements of zealous
928 providers.
929
930 .. warning::
931
932 |classmethod|
933
934 :param int request_type:
935 Type of request.
936
937 :param request_elements:
938 :class:`.RequestElements`
939
940 :param credentials:
941 :class:`.Credentials`
942
943 :returns:
944 :class:`.RequestElements`
945
946 """
947
948 return request_elements
949
950 @staticmethod
951 def _x_credentials_parser(credentials, data):
952 """
953 Override this to handle differences in naming conventions across
954 providers.
955
956 :param credentials:
957 :class:`.Credentials`
958
959 :param dict data:
960 Response data dictionary.
961
962 :returns:
963 :class:`.Credentials`
964
965 """
966 return credentials
967
968 def _access_user_info(self):
969 """
970 Accesses the :attr:`.user_info_url`.
971
972 :returns:
973 :class:`.UserInfoResponse`
974
975 """
976 url = self.user_info_url.format(**self.user.__dict__)
977 return self.access(url)
978
979
980 class AuthenticationProvider(BaseProvider):
981 """
982 Base provider for *authentication protocols* i.e. protocols which allow a
983 **provider** to authenticate a *claimed identity* of a **user**.
984
985 e.g. `OpenID <http://openid.net/>`_.
986
987 """
988
989 #: Indicates whether the **provider** supports access_with_credentials to
990 #: **user's** protected resources.
991 # TODO: Useless
992 has_protected_resources = False
993
994 def __init__(self, *args, **kwargs):
995 super(AuthenticationProvider, self).__init__(*args, **kwargs)
996
997 # Lookup default identifier, if available in provider
998 default_identifier = getattr(self, 'identifier', None)
999
1000 # Allow for custom name for the "id" querystring parameter.
1001 self.identifier_param = kwargs.get('identifier_param', 'id')
1002
1003 # Get the identifier from request params, or use default as fallback.
1004 self.identifier = self.params.get(
1005 self.identifier_param, default_identifier)
1006
1007
1008 PROVIDER_ID_MAP = [
1009 AuthenticationProvider,
1010 AuthorizationProvider,
1011 BaseProvider,
1012 ]
@@ -0,0 +1,112 b''
1 # -*- coding: utf-8 -*-
2 """
3 Google App Engine OpenID Providers
4 ----------------------------------
5
6 |openid|_ provider implementations based on the |gae_users_api|_.
7
8 .. note::
9
10 When using the :class:`GAEOpenID` provider, the :class:`.User` object
11 will always have only the
12 :attr:`.User.user_id`,
13 :attr:`.User.email`,
14 :attr:`.User.gae_user`
15 attributes populated with data.
16 Moreover the :attr:`.User.user_id` will always be empty on the
17 `GAE Development Server
18 <https://developers.google.com/appengine/docs/python/tools/devserver>`_.
19
20 .. autosummary::
21
22 GAEOpenID
23 Yahoo
24 Google
25
26 """
27
28 import logging
29
30 from google.appengine.api import users
31
32 import authomatic.core as core
33 from authomatic import providers
34 from authomatic.exceptions import FailureError
35
36
37 __all__ = ['GAEOpenID', 'Yahoo', 'Google']
38
39
40 class GAEOpenID(providers.AuthenticationProvider):
41 """
42 |openid|_ provider based on the |gae_users_api|_.
43
44 Accepts additional keyword arguments inherited from
45 :class:`.AuthenticationProvider`.
46
47 """
48
49 @providers.login_decorator
50 def login(self):
51 """
52 Launches the OpenID authentication procedure.
53 """
54
55 if self.params.get(self.identifier_param):
56 # =================================================================
57 # Phase 1 before redirect.
58 # =================================================================
59 self._log(
60 logging.INFO,
61 u'Starting OpenID authentication procedure.')
62
63 url = users.create_login_url(
64 dest_url=self.url, federated_identity=self.identifier)
65
66 self._log(logging.INFO, u'Redirecting user to {0}.'.format(url))
67
68 self.redirect(url)
69 else:
70 # =================================================================
71 # Phase 2 after redirect.
72 # =================================================================
73
74 self._log(
75 logging.INFO,
76 u'Continuing OpenID authentication procedure after redirect.')
77
78 user = users.get_current_user()
79
80 if user:
81 self._log(logging.INFO, u'Authentication successful.')
82 self._log(logging.INFO, u'Creating user.')
83 self.user = core.User(self,
84 id=user.federated_identity(),
85 email=user.email(),
86 gae_user=user)
87
88 # =============================================================
89 # We're done
90 # =============================================================
91 else:
92 raise FailureError(
93 'Unable to authenticate identifier "{0}"!'.format(
94 self.identifier))
95
96
97 class Yahoo(GAEOpenID):
98 """
99 :class:`.GAEOpenID` provider with the :attr:`.identifier` set to
100 ``"me.yahoo.com"``.
101 """
102
103 identifier = 'me.yahoo.com'
104
105
106 class Google(GAEOpenID):
107 """
108 :class:`.GAEOpenID` provider with the :attr:`.identifier` set to
109 ``"https://www.google.com/accounts/o8/id"``.
110 """
111
112 identifier = 'https://www.google.com/accounts/o8/id'
This diff has been collapsed as it changes many lines, (1377 lines changed) Show them Hide them
@@ -0,0 +1,1377 b''
1 # -*- coding: utf-8 -*-
2 """
3 |oauth1| Providers
4 --------------------
5
6 Providers which implement the |oauth1|_ protocol.
7
8 .. autosummary::
9
10 OAuth1
11 Bitbucket
12 Flickr
13 Meetup
14 Plurk
15 Twitter
16 Tumblr
17 UbuntuOne
18 Vimeo
19 Xero
20 Xing
21 Yahoo
22
23 """
24
25 import abc
26 import binascii
27 import datetime
28 import hashlib
29 import hmac
30 import logging
31 import time
32 import uuid
33
34 import authomatic.core as core
35 from authomatic import providers
36 from authomatic.exceptions import (
37 CancellationError,
38 FailureError,
39 OAuth1Error,
40 )
41 from authomatic import six
42 from authomatic.six.moves import urllib_parse as parse
43
44
45 __all__ = [
46 'OAuth1',
47 'Bitbucket',
48 'Flickr',
49 'Meetup',
50 'Plurk',
51 'Twitter',
52 'Tumblr',
53 'UbuntuOne',
54 'Vimeo',
55 'Xero',
56 'Xing',
57 'Yahoo'
58 ]
59
60
61 def _normalize_params(params):
62 """
63 Returns a normalized query string sorted first by key, then by value
64 excluding the ``realm`` and ``oauth_signature`` parameters as specified
65 here: http://oauth.net/core/1.0a/#rfc.section.9.1.1.
66
67 :param params:
68 :class:`dict` or :class:`list` of tuples.
69
70 """
71
72 if isinstance(params, dict):
73 params = list(params.items())
74
75 # remove "realm" and "oauth_signature"
76 params = sorted([
77 (k, v) for k, v in params
78 if k not in ('oauth_signature', 'realm')
79 ])
80 # sort
81 # convert to query string
82 qs = parse.urlencode(params)
83 # replace "+" to "%20"
84 qs = qs.replace('+', '%20')
85 # replace "%7E" to "%20"
86 qs = qs.replace('%7E', '~')
87
88 return qs
89
90
91 def _join_by_ampersand(*args):
92 return '&'.join([core.escape(i) for i in args])
93
94
95 def _create_base_string(method, base, params):
96 """
97 Returns base string for HMAC-SHA1 signature as specified in:
98 http://oauth.net/core/1.0a/#rfc.section.9.1.3.
99 """
100
101 normalized_qs = _normalize_params(params)
102 return _join_by_ampersand(method, base, normalized_qs)
103
104
105 class BaseSignatureGenerator(object):
106 """
107 Abstract base class for all signature generators.
108 """
109
110 __metaclass__ = abc.ABCMeta
111
112 #: :class:`str` The name of the signature method.
113 method = ''
114
115 @abc.abstractmethod
116 def create_signature(self, method, base, params,
117 consumer_secret, token_secret=''):
118 """
119 Must create signature based on the parameters as specified in
120 http://oauth.net/core/1.0a/#signing_process.
121
122 .. warning::
123
124 |classmethod|
125
126 :param str method:
127 HTTP method of the request to be signed.
128
129 :param str base:
130 Base URL of the request without query string an fragment.
131
132 :param dict params:
133 Dictionary or list of tuples of the request parameters.
134
135 :param str consumer_secret:
136 :attr:`.core.Consumer.secret`
137
138 :param str token_secret:
139 Access token secret as specified in
140 http://oauth.net/core/1.0a/#anchor3.
141
142 :returns:
143 The signature string.
144
145 """
146
147
148 class HMACSHA1SignatureGenerator(BaseSignatureGenerator):
149 """
150 HMAC-SHA1 signature generator.
151
152 See: http://oauth.net/core/1.0a/#anchor15
153
154 """
155
156 method = 'HMAC-SHA1'
157
158 @classmethod
159 def _create_key(cls, consumer_secret, token_secret=''):
160 """
161 Returns a key for HMAC-SHA1 signature as specified at:
162 http://oauth.net/core/1.0a/#rfc.section.9.2.
163
164 :param str consumer_secret:
165 :attr:`.core.Consumer.secret`
166
167 :param str token_secret:
168 Access token secret as specified in
169 http://oauth.net/core/1.0a/#anchor3.
170
171 :returns:
172 Key to sign the request with.
173
174 """
175
176 return _join_by_ampersand(consumer_secret, token_secret or '')
177
178 @classmethod
179 def create_signature(cls, method, base, params,
180 consumer_secret, token_secret=''):
181 """
182 Returns HMAC-SHA1 signature as specified at:
183 http://oauth.net/core/1.0a/#rfc.section.9.2.
184
185 :param str method:
186 HTTP method of the request to be signed.
187
188 :param str base:
189 Base URL of the request without query string an fragment.
190
191 :param dict params:
192 Dictionary or list of tuples of the request parameters.
193
194 :param str consumer_secret:
195 :attr:`.core.Consumer.secret`
196
197 :param str token_secret:
198 Access token secret as specified in
199 http://oauth.net/core/1.0a/#anchor3.
200
201 :returns:
202 The signature string.
203
204 """
205
206 base_string = _create_base_string(method, base, params)
207 key = cls._create_key(consumer_secret, token_secret)
208
209 hashed = hmac.new(
210 six.b(key),
211 base_string.encode('utf-8'),
212 hashlib.sha1)
213
214 base64_encoded = binascii.b2a_base64(hashed.digest())[:-1]
215
216 return base64_encoded
217
218
219 class PLAINTEXTSignatureGenerator(BaseSignatureGenerator):
220 """
221 PLAINTEXT signature generator.
222
223 See: http://oauth.net/core/1.0a/#anchor21
224
225 """
226
227 method = 'PLAINTEXT'
228
229 @classmethod
230 def create_signature(cls, method, base, params,
231 consumer_secret, token_secret=''):
232
233 consumer_secret = parse.quote(consumer_secret, '')
234 token_secret = parse.quote(token_secret, '')
235
236 return parse.quote('&'.join((consumer_secret, token_secret)), '')
237
238
239 class OAuth1(providers.AuthorizationProvider):
240 """
241 Base class for |oauth1|_ providers.
242 """
243
244 _signature_generator = HMACSHA1SignatureGenerator
245
246 PROVIDER_TYPE_ID = 1
247 REQUEST_TOKEN_REQUEST_TYPE = 1
248
249 def __init__(self, *args, **kwargs):
250 """
251 Accepts additional keyword arguments:
252
253 :param str consumer_key:
254 The *key* assigned to our application (**consumer**) by
255 the **provider**.
256
257 :param str consumer_secret:
258 The *secret* assigned to our application (**consumer**) by
259 the **provider**.
260
261 :param id:
262 A unique short name used to serialize :class:`.Credentials`.
263
264 :param dict user_authorization_params:
265 A dictionary of additional request parameters for
266 **user authorization request**.
267
268 :param dict access_token_params:
269 A dictionary of additional request parameters for
270 **access token request**.
271
272 :param dict request_token_params:
273 A dictionary of additional request parameters for
274 **request token request**.
275
276 """
277
278 super(OAuth1, self).__init__(*args, **kwargs)
279
280 self.request_token_params = self._kwarg(
281 kwargs, 'request_token_params', {})
282
283 # ========================================================================
284 # Abstract properties
285 # ========================================================================
286
287 @abc.abstractproperty
288 def request_token_url(self):
289 """
290 :class:`str` URL where we can get the |oauth1| request token.
291 see http://oauth.net/core/1.0a/#auth_step1.
292 """
293
294 # ========================================================================
295 # Internal methods
296 # ========================================================================
297
298 @classmethod
299 def create_request_elements(
300 cls, request_type, credentials, url, params=None, headers=None,
301 body='', method='GET', verifier='', callback=''
302 ):
303 """
304 Creates |oauth1| request elements.
305 """
306
307 params = params or {}
308 headers = headers or {}
309
310 consumer_key = credentials.consumer_key or ''
311 consumer_secret = credentials.consumer_secret or ''
312 token = credentials.token or ''
313 token_secret = credentials.token_secret or ''
314
315 # separate url base and query parameters
316 url, base_params = cls._split_url(url)
317
318 # add extracted params to future params
319 params.update(dict(base_params))
320
321 if request_type == cls.USER_AUTHORIZATION_REQUEST_TYPE:
322 # no need for signature
323 if token:
324 params['oauth_token'] = token
325 else:
326 raise OAuth1Error(
327 'Credentials with valid token are required to create '
328 'User Authorization URL!')
329 else:
330 # signature needed
331 if request_type == cls.REQUEST_TOKEN_REQUEST_TYPE:
332 # Request Token URL
333 if consumer_key and consumer_secret and callback:
334 params['oauth_consumer_key'] = consumer_key
335 params['oauth_callback'] = callback
336 else:
337 raise OAuth1Error(
338 'Credentials with valid consumer_key, consumer_secret '
339 'and callback are required to create Request Token '
340 'URL!')
341
342 elif request_type == cls.ACCESS_TOKEN_REQUEST_TYPE:
343 # Access Token URL
344 if consumer_key and consumer_secret and token and verifier:
345 params['oauth_token'] = token
346 params['oauth_consumer_key'] = consumer_key
347 params['oauth_verifier'] = verifier
348 else:
349 raise OAuth1Error(
350 'Credentials with valid consumer_key, '
351 'consumer_secret, token and argument verifier'
352 ' are required to create Access Token URL!')
353
354 elif request_type == cls.PROTECTED_RESOURCE_REQUEST_TYPE:
355 # Protected Resources URL
356 if consumer_key and consumer_secret and token and token_secret:
357 params['oauth_token'] = token
358 params['oauth_consumer_key'] = consumer_key
359 else:
360 raise OAuth1Error(
361 'Credentials with valid consumer_key, ' +
362 'consumer_secret, token and token_secret are required '
363 'to create Protected Resources URL!')
364
365 # Sign request.
366 # http://oauth.net/core/1.0a/#anchor13
367
368 # Prepare parameters for signature base string
369 # http://oauth.net/core/1.0a/#rfc.section.9.1
370 params['oauth_signature_method'] = cls._signature_generator.method
371 params['oauth_timestamp'] = str(int(time.time()))
372 params['oauth_nonce'] = cls.csrf_generator(str(uuid.uuid4()))
373 params['oauth_version'] = '1.0'
374
375 # add signature to params
376 params['oauth_signature'] = cls._signature_generator.create_signature( # noqa
377 method, url, params, consumer_secret, token_secret)
378
379 request_elements = core.RequestElements(
380 url, method, params, headers, body)
381
382 return cls._x_request_elements_filter(
383 request_type, request_elements, credentials)
384
385 # ========================================================================
386 # Exposed methods
387 # ========================================================================
388
389 @staticmethod
390 def to_tuple(credentials):
391 return (credentials.token, credentials.token_secret)
392
393 @classmethod
394 def reconstruct(cls, deserialized_tuple, credentials, cfg):
395
396 token, token_secret = deserialized_tuple
397
398 credentials.token = token
399 credentials.token_secret = token_secret
400 credentials.consumer_key = cfg.get('consumer_key', '')
401 credentials.consumer_secret = cfg.get('consumer_secret', '')
402
403 return credentials
404
405 @providers.login_decorator
406 def login(self):
407 # get request parameters from which we can determine the login phase
408 denied = self.params.get('denied')
409 verifier = self.params.get('oauth_verifier', '')
410 request_token = self.params.get('oauth_token', '')
411
412 if request_token and verifier:
413 # Phase 2 after redirect with success
414 self._log(
415 logging.INFO,
416 u'Continuing OAuth 1.0a authorization procedure after '
417 u'redirect.')
418 token_secret = self._session_get('token_secret')
419 if not token_secret:
420 raise FailureError(
421 u'Unable to retrieve token secret from storage!')
422
423 # Get Access Token
424 self._log(
425 logging.INFO,
426 u'Fetching for access token from {0}.'.format(
427 self.access_token_url))
428
429 self.credentials.token = request_token
430 self.credentials.token_secret = token_secret
431
432 request_elements = self.create_request_elements(
433 request_type=self.ACCESS_TOKEN_REQUEST_TYPE,
434 url=self.access_token_url,
435 credentials=self.credentials,
436 verifier=verifier,
437 params=self.access_token_params
438 )
439
440 response = self._fetch(*request_elements)
441 self.access_token_response = response
442
443 if not self._http_status_in_category(response.status, 2):
444 raise FailureError(
445 'Failed to obtain OAuth 1.0a oauth_token from {0}! '
446 'HTTP status code: {1}.'
447 .format(self.access_token_url, response.status),
448 original_message=response.content,
449 status=response.status,
450 url=self.access_token_url
451 )
452
453 self._log(logging.INFO, u'Got access token.')
454 self.credentials.token = response.data.get('oauth_token', '')
455 self.credentials.token_secret = response.data.get(
456 'oauth_token_secret', ''
457 )
458
459 self.credentials = self._x_credentials_parser(self.credentials,
460 response.data)
461 self._update_or_create_user(response.data, self.credentials)
462
463 # =================================================================
464 # We're done!
465 # =================================================================
466
467 elif denied:
468 # Phase 2 after redirect denied
469 raise CancellationError(
470 'User denied the request token {0} during a redirect'
471 'to {1}!'.format(denied, self.user_authorization_url),
472 original_message=denied,
473 url=self.user_authorization_url)
474 else:
475 # Phase 1 before redirect
476 self._log(
477 logging.INFO,
478 u'Starting OAuth 1.0a authorization procedure.')
479
480 # Fetch for request token
481 request_elements = self.create_request_elements(
482 request_type=self.REQUEST_TOKEN_REQUEST_TYPE,
483 credentials=self.credentials,
484 url=self.request_token_url,
485 callback=self.url,
486 params=self.request_token_params
487 )
488
489 self._log(
490 logging.INFO,
491 u'Fetching for request token and token secret.')
492 response = self._fetch(*request_elements)
493
494 # check if response status is OK
495 if not self._http_status_in_category(response.status, 2):
496 raise FailureError(
497 u'Failed to obtain request token from {0}! HTTP status '
498 u'code: {1} content: {2}'.format(
499 self.request_token_url,
500 response.status,
501 response.content
502 ),
503 original_message=response.content,
504 status=response.status,
505 url=self.request_token_url)
506
507 # extract request token
508 request_token = response.data.get('oauth_token')
509 if not request_token:
510 raise FailureError(
511 'Response from {0} doesn\'t contain oauth_token '
512 'parameter!'.format(self.request_token_url),
513 original_message=response.content,
514 url=self.request_token_url)
515
516 # we need request token for user authorization redirect
517 self.credentials.token = request_token
518
519 # extract token secret and save it to storage
520 token_secret = response.data.get('oauth_token_secret')
521 if token_secret:
522 # we need token secret after user authorization redirect to get
523 # access token
524 self._session_set('token_secret', token_secret)
525 else:
526 raise FailureError(
527 u'Failed to obtain token secret from {0}!'.format(
528 self.request_token_url),
529 original_message=response.content,
530 url=self.request_token_url)
531
532 self._log(logging.INFO, u'Got request token and token secret')
533
534 # Create User Authorization URL
535 request_elements = self.create_request_elements(
536 request_type=self.USER_AUTHORIZATION_REQUEST_TYPE,
537 credentials=self.credentials,
538 url=self.user_authorization_url,
539 params=self.user_authorization_params
540 )
541
542 self._log(
543 logging.INFO,
544 u'Redirecting user to {0}.'.format(
545 request_elements.full_url))
546
547 self.redirect(request_elements.full_url)
548
549
550 class Bitbucket(OAuth1):
551 """
552 Bitbucket |oauth1| provider.
553
554 * Dashboard: https://bitbucket.org/account/user/peterhudec/api
555 * Docs: https://confluence.atlassian.com/display/BITBUCKET/oauth+Endpoint
556 * API reference:
557 https://confluence.atlassian.com/display/BITBUCKET/Using+the+Bitbucket+REST+APIs
558
559 Supported :class:`.User` properties:
560
561 * first_name
562 * id
563 * last_name
564 * link
565 * name
566 * picture
567 * username
568 * email
569
570 Unsupported :class:`.User` properties:
571
572 * birth_date
573 * city
574 * country
575 * gender
576 * locale
577 * location
578 * nickname
579 * phone
580 * postal_code
581 * timezone
582
583 .. note::
584
585 To get the full user info, you need to select both the *Account Read*
586 and the *Repositories Read* permission in the Bitbucket application
587 edit form.
588
589 """
590
591 supported_user_attributes = core.SupportedUserAttributes(
592 first_name=True,
593 id=True,
594 last_name=True,
595 link=True,
596 name=True,
597 picture=True,
598 username=True,
599 email=True
600 )
601
602 request_token_url = 'https://bitbucket.org/!api/1.0/oauth/request_token'
603 user_authorization_url = 'https://bitbucket.org/!api/1.0/oauth/' + \
604 'authenticate'
605 access_token_url = 'https://bitbucket.org/!api/1.0/oauth/access_token'
606 user_info_url = 'https://api.bitbucket.org/1.0/user'
607 user_email_url = 'https://api.bitbucket.org/1.0/emails'
608
609 @staticmethod
610 def _x_user_parser(user, data):
611 _user = data.get('user', {})
612 user.username = user.id = _user.get('username')
613 user.name = _user.get('display_name')
614 user.first_name = _user.get('first_name')
615 user.last_name = _user.get('last_name')
616 user.picture = _user.get('avatar')
617 user.link = 'https://bitbucket.org/api{0}'\
618 .format(_user.get('resource_uri'))
619 return user
620
621 def _access_user_info(self):
622 """
623 Email is available in separate method so second request is needed.
624 """
625 response = super(Bitbucket, self)._access_user_info()
626
627 response.data.setdefault("email", None)
628
629 email_response = self.access(self.user_email_url)
630 if email_response.data:
631 for item in email_response.data:
632 if item.get("primary", False):
633 response.data.update(email=item.get("email", None))
634
635 return response
636
637
638 class Flickr(OAuth1):
639 """
640 Flickr |oauth1| provider.
641
642 * Dashboard: https://www.flickr.com/services/apps/
643 * Docs: https://www.flickr.com/services/api/auth.oauth.html
644 * API reference: https://www.flickr.com/services/api/
645
646 Supported :class:`.User` properties:
647
648 * id
649 * name
650 * username
651
652 Unsupported :class:`.User` properties:
653
654 * birth_date
655 * city
656 * country
657 * email
658 * first_name
659 * gender
660 * last_name
661 * link
662 * locale
663 * location
664 * nickname
665 * phone
666 * picture
667 * postal_code
668 * timezone
669
670 .. note::
671
672 If you encounter the "Oops! Flickr doesn't recognise the
673 permission set." message, you need to add the ``perms=read`` or
674 ``perms=write`` parameter to the *user authorization request*.
675 You can do it by adding the ``user_authorization_params``
676 key to the :doc:`config`:
677
678 .. code-block:: python
679 :emphasize-lines: 6
680
681 CONFIG = {
682 'flickr': {
683 'class_': oauth1.Flickr,
684 'consumer_key': '##########',
685 'consumer_secret': '##########',
686 'user_authorization_params': dict(perms='read'),
687 },
688 }
689
690 """
691
692 supported_user_attributes = core.SupportedUserAttributes(
693 id=True,
694 name=True,
695 username=True
696 )
697
698 request_token_url = 'http://www.flickr.com/services/oauth/request_token'
699 user_authorization_url = 'http://www.flickr.com/services/oauth/authorize'
700 access_token_url = 'http://www.flickr.com/services/oauth/access_token'
701 user_info_url = None
702
703 supports_jsonp = True
704
705 @staticmethod
706 def _x_user_parser(user, data):
707 _user = data.get('user', {})
708
709 user.name = data.get('fullname') or _user.get(
710 'username', {}).get('_content')
711 user.id = data.get('user_nsid') or _user.get('id')
712
713 return user
714
715
716 class Meetup(OAuth1):
717 """
718 Meetup |oauth1| provider.
719
720 .. note::
721
722 Meetup also supports |oauth2| but you need the **user ID** to update
723 the **user** info, which they don't provide in the |oauth2| access
724 token response.
725
726 * Dashboard: http://www.meetup.com/meetup_api/oauth_consumers/
727 * Docs: http://www.meetup.com/meetup_api/auth/#oauth
728 * API: http://www.meetup.com/meetup_api/docs/
729
730 Supported :class:`.User` properties:
731
732 * city
733 * country
734 * id
735 * link
736 * locale
737 * location
738 * name
739 * picture
740
741 Unsupported :class:`.User` properties:
742
743 * birth_date
744 * email
745 * first_name
746 * gender
747 * last_name
748 * nickname
749 * phone
750 * postal_code
751 * timezone
752 * username
753
754 """
755
756 supported_user_attributes = core.SupportedUserAttributes(
757 city=True,
758 country=True,
759 id=True,
760 link=True,
761 locale=True,
762 location=True,
763 name=True,
764 picture=True
765 )
766
767 request_token_url = 'https://api.meetup.com/oauth/request/'
768 user_authorization_url = 'http://www.meetup.com/authorize/'
769 access_token_url = 'https://api.meetup.com/oauth/access/'
770 user_info_url = 'https://api.meetup.com/2/member/{id}'
771
772 @staticmethod
773 def _x_user_parser(user, data):
774
775 user.id = data.get('id') or data.get('member_id')
776 user.locale = data.get('lang')
777 user.picture = data.get('photo', {}).get('photo_link')
778
779 return user
780
781
782 class Plurk(OAuth1):
783 """
784 Plurk |oauth1| provider.
785
786 * Dashboard: http://www.plurk.com/PlurkApp/
787 * Docs:
788 * API: http://www.plurk.com/API
789 * API explorer: http://www.plurk.com/OAuth/test/
790
791 Supported :class:`.User` properties:
792
793 * birth_date
794 * city
795 * country
796 * email
797 * gender
798 * id
799 * link
800 * locale
801 * location
802 * name
803 * nickname
804 * picture
805 * timezone
806 * username
807
808 Unsupported :class:`.User` properties:
809
810 * first_name
811 * last_name
812 * phone
813 * postal_code
814
815 """
816
817 supported_user_attributes = core.SupportedUserAttributes(
818 birth_date=True,
819 city=True,
820 country=True,
821 email=True,
822 gender=True,
823 id=True,
824 link=True,
825 locale=True,
826 location=True,
827 name=True,
828 nickname=True,
829 picture=True,
830 timezone=True,
831 username=True
832 )
833
834 request_token_url = 'http://www.plurk.com/OAuth/request_token'
835 user_authorization_url = 'http://www.plurk.com/OAuth/authorize'
836 access_token_url = 'http://www.plurk.com/OAuth/access_token'
837 user_info_url = 'http://www.plurk.com/APP/Profile/getOwnProfile'
838
839 @staticmethod
840 def _x_user_parser(user, data):
841
842 _user = data.get('user_info', {})
843
844 user.email = _user.get('email')
845 user.gender = _user.get('gender')
846 user.id = _user.get('id') or _user.get('uid')
847 user.locale = _user.get('default_lang')
848 user.name = _user.get('full_name')
849 user.nickname = _user.get('nick_name')
850 user.picture = 'http://avatars.plurk.com/{0}-big2.jpg'.format(user.id)
851 user.timezone = _user.get('timezone')
852 user.username = _user.get('display_name')
853
854 user.link = 'http://www.plurk.com/{0}/'.format(user.username)
855
856 user.city, user.country = _user.get('location', ',').split(',')
857 user.city = user.city.strip()
858 user.country = user.country.strip()
859
860 _bd = _user.get('date_of_birth')
861 if _bd:
862 try:
863 user.birth_date = datetime.datetime.strptime(
864 _bd,
865 "%a, %d %b %Y %H:%M:%S %Z"
866 )
867 except ValueError:
868 pass
869
870 return user
871
872
873 class Twitter(OAuth1):
874 """
875 Twitter |oauth1| provider.
876
877 * Dashboard: https://dev.twitter.com/apps
878 * Docs: https://dev.twitter.com/docs
879 * API reference: https://dev.twitter.com/docs/api
880
881 .. note:: To prevent multiple authorization attempts, you should enable
882 the option:
883 ``Allow this application to be used to Sign in with Twitter``
884 in the Twitter 'Application Management' page. (http://apps.twitter.com)
885
886 Supported :class:`.User` properties:
887
888 * email
889 * city
890 * country
891 * id
892 * link
893 * locale
894 * location
895 * name
896 * picture
897 * username
898
899 Unsupported :class:`.User` properties:
900
901 * birth_date
902 * email
903 * gender
904 * first_name
905 * last_name
906 * locale
907 * nickname
908 * phone
909 * postal_code
910 * timezone
911
912 """
913
914 supported_user_attributes = core.SupportedUserAttributes(
915 city=True,
916 country=True,
917 id=True,
918 email=False,
919 link=True,
920 locale=False,
921 location=True,
922 name=True,
923 picture=True,
924 username=True
925 )
926
927 request_token_url = 'https://api.twitter.com/oauth/request_token'
928 user_authorization_url = 'https://api.twitter.com/oauth/authenticate'
929 access_token_url = 'https://api.twitter.com/oauth/access_token'
930 user_info_url = (
931 'https://api.twitter.com/1.1/account/verify_credentials.json?'
932 'include_entities=true&include_email=true'
933 )
934 supports_jsonp = True
935
936 @staticmethod
937 def _x_user_parser(user, data):
938 user.username = data.get('screen_name')
939 user.id = data.get('id') or data.get('user_id')
940 user.picture = data.get('profile_image_url')
941 user.locale = data.get('lang')
942 user.link = data.get('url')
943 _location = data.get('location', '')
944 if _location:
945 user.location = _location.strip()
946 _split_location = _location.split(',')
947 if len(_split_location) > 1:
948 _city, _country = _split_location
949 user.country = _country.strip()
950 else:
951 _city = _split_location[0]
952 user.city = _city.strip()
953 return user
954
955
956 class Tumblr(OAuth1):
957 """
958 Tumblr |oauth1| provider.
959
960 * Dashboard: http://www.tumblr.com/oauth/apps
961 * Docs: http://www.tumblr.com/docs/en/api/v2#auth
962 * API reference: http://www.tumblr.com/docs/en/api/v2
963
964 Supported :class:`.User` properties:
965
966 * id
967 * name
968 * username
969
970 Unsupported :class:`.User` properties:
971
972 * birth_date
973 * city
974 * country
975 * email
976 * gender
977 * first_name
978 * last_name
979 * link
980 * locale
981 * location
982 * nickname
983 * phone
984 * picture
985 * postal_code
986 * timezone
987
988 """
989
990 supported_user_attributes = core.SupportedUserAttributes(
991 id=True,
992 name=True,
993 username=True
994 )
995
996 request_token_url = 'http://www.tumblr.com/oauth/request_token'
997 user_authorization_url = 'http://www.tumblr.com/oauth/authorize'
998 access_token_url = 'http://www.tumblr.com/oauth/access_token'
999 user_info_url = 'http://api.tumblr.com/v2/user/info'
1000
1001 supports_jsonp = True
1002
1003 @staticmethod
1004 def _x_user_parser(user, data):
1005 _user = data.get('response', {}).get('user', {})
1006 user.username = user.id = _user.get('name')
1007 return user
1008
1009
1010 class UbuntuOne(OAuth1):
1011 """
1012 Ubuntu One |oauth1| provider.
1013
1014 .. note::
1015
1016 The UbuntuOne service
1017 `has been shut down <http://blog.canonical.com/2014/04/02/
1018 shutting-down-ubuntu-one-file-services/>`__.
1019
1020 .. warning::
1021
1022 Uses the `PLAINTEXT <http://oauth.net/core/1.0a/#anchor21>`_
1023 Signature method!
1024
1025 * Dashboard: https://one.ubuntu.com/developer/account_admin/auth/web
1026 * Docs: https://one.ubuntu.com/developer/account_admin/auth/web
1027 * API reference: https://one.ubuntu.com/developer/contents
1028
1029 """
1030
1031 _signature_generator = PLAINTEXTSignatureGenerator
1032
1033 request_token_url = 'https://one.ubuntu.com/oauth/request/'
1034 user_authorization_url = 'https://one.ubuntu.com/oauth/authorize/'
1035 access_token_url = 'https://one.ubuntu.com/oauth/access/'
1036 user_info_url = 'https://one.ubuntu.com/api/account/'
1037
1038
1039 class Vimeo(OAuth1):
1040 """
1041 Vimeo |oauth1| provider.
1042
1043 .. warning::
1044
1045 Vimeo needs one more fetch to get rich user info!
1046
1047 * Dashboard: https://developer.vimeo.com/apps
1048 * Docs: https://developer.vimeo.com/apis/advanced#oauth-endpoints
1049 * API reference: https://developer.vimeo.com/apis
1050
1051 Supported :class:`.User` properties:
1052
1053 * id
1054 * link
1055 * location
1056 * name
1057 * picture
1058
1059 Unsupported :class:`.User` properties:
1060
1061 * birth_date
1062 * city
1063 * country
1064 * email
1065 * gender
1066 * first_name
1067 * last_name
1068 * locale
1069 * nickname
1070 * phone
1071 * postal_code
1072 * timezone
1073 * username
1074
1075 """
1076
1077 supported_user_attributes = core.SupportedUserAttributes(
1078 id=True,
1079 link=True,
1080 location=True,
1081 name=True,
1082 picture=True
1083 )
1084
1085 request_token_url = 'https://vimeo.com/oauth/request_token'
1086 user_authorization_url = 'https://vimeo.com/oauth/authorize'
1087 access_token_url = 'https://vimeo.com/oauth/access_token'
1088 user_info_url = ('http://vimeo.com/api/rest/v2?'
1089 'format=json&method=vimeo.oauth.checkAccessToken')
1090
1091 def _access_user_info(self):
1092 """
1093 Vimeo requires the user ID to access the user info endpoint, so we need
1094 to make two requests: one to get user ID and second to get user info.
1095 """
1096 response = super(Vimeo, self)._access_user_info()
1097 uid = response.data.get('oauth', {}).get('user', {}).get('id')
1098 if uid:
1099 return self.access('http://vimeo.com/api/v2/{0}/info.json'
1100 .format(uid))
1101 return response
1102
1103 @staticmethod
1104 def _x_user_parser(user, data):
1105 user.name = data.get('display_name')
1106 user.link = data.get('profile_url')
1107 user.picture = data.get('portrait_huge')
1108 return user
1109
1110
1111 class Xero(OAuth1):
1112 """
1113 Xero |oauth1| provider.
1114
1115 .. note::
1116
1117 API returns XML!
1118
1119 * Dashboard: https://api.xero.com/Application
1120 * Docs: http://blog.xero.com/developer/api-overview/public-applications/
1121 * API reference: http://blog.xero.com/developer/api/
1122
1123 Supported :class:`.User` properties:
1124
1125 * email
1126 * first_name
1127 * id
1128 * last_name
1129 * name
1130
1131 Unsupported :class:`.User` properties:
1132
1133 * birth_date
1134 * city
1135 * country
1136 * gender
1137 * link
1138 * locale
1139 * location
1140 * nickname
1141 * phone
1142 * picture
1143 * postal_code
1144 * timezone
1145 * username
1146
1147 """
1148
1149 supported_user_attributes = core.SupportedUserAttributes(
1150 email=True,
1151 first_name=True,
1152 id=True,
1153 last_name=True,
1154 name=True
1155 )
1156
1157 request_token_url = 'https://api.xero.com/oauth/RequestToken'
1158 user_authorization_url = 'https://api.xero.com/oauth/Authorize'
1159 access_token_url = 'https://api.xero.com/oauth/AccessToken'
1160 user_info_url = 'https://api.xero.com/api.xro/2.0/Users'
1161
1162 @staticmethod
1163 def _x_user_parser(user, data):
1164 # Data is xml.etree.ElementTree.Element object.
1165 if not isinstance(data, dict):
1166 # But only on user.update()
1167 _user = data.find('Users/User')
1168 user.id = _user.find('UserID').text
1169 user.first_name = _user.find('FirstName').text
1170 user.last_name = _user.find('LastName').text
1171 user.email = _user.find('EmailAddress').text
1172
1173 return user
1174
1175
1176 class Yahoo(OAuth1):
1177 """
1178 Yahoo |oauth1| provider.
1179
1180 * Dashboard: https://developer.apps.yahoo.com/dashboard/
1181 * Docs: http://developer.yahoo.com/oauth/guide/oauth-auth-flow.html
1182 * API: http://developer.yahoo.com/everything.html
1183 * API explorer: http://developer.yahoo.com/yql/console/
1184
1185 Supported :class:`.User` properties:
1186
1187 * city
1188 * country
1189 * id
1190 * link
1191 * location
1192 * name
1193 * nickname
1194 * picture
1195
1196 Unsupported :class:`.User` properties:
1197
1198 * birth_date
1199 * gender
1200 * locale
1201 * phone
1202 * postal_code
1203 * timezone
1204 * username
1205
1206 """
1207
1208 supported_user_attributes = core.SupportedUserAttributes(
1209 city=True,
1210 country=True,
1211 id=True,
1212 link=True,
1213 location=True,
1214 name=True,
1215 nickname=True,
1216 picture=True
1217 )
1218
1219 request_token_url = 'https://api.login.yahoo.com/oauth/v2/' + \
1220 'get_request_token'
1221 user_authorization_url = 'https://api.login.yahoo.com/oauth/v2/' + \
1222 'request_auth'
1223 access_token_url = 'https://api.login.yahoo.com/oauth/v2/get_token'
1224 user_info_url = (
1225 'https://query.yahooapis.com/v1/yql?q=select%20*%20from%20'
1226 'social.profile%20where%20guid%3Dme%3B&format=json'
1227 )
1228
1229 same_origin = False
1230 supports_jsonp = True
1231
1232 @staticmethod
1233 def _x_user_parser(user, data):
1234
1235 _user = data.get('query', {}).get('results', {}).get('profile', {})
1236
1237 user.id = _user.get('guid')
1238 user.gender = _user.get('gender')
1239 user.nickname = _user.get('nickname')
1240 user.link = _user.get('profileUrl')
1241
1242 emails = _user.get('emails')
1243 if isinstance(emails, list):
1244 for email in emails:
1245 if 'primary' in list(email.keys()):
1246 user.email = email.get('handle')
1247 elif isinstance(emails, dict):
1248 user.email = emails.get('handle')
1249
1250 user.picture = _user.get('image', {}).get('imageUrl')
1251
1252 try:
1253 user.city, user.country = _user.get('location', ',').split(',')
1254 user.city = user.city.strip()
1255 user.country = user.country.strip()
1256 except ValueError:
1257 # probably user hasn't activated Yahoo Profile
1258 user.city = None
1259 user.country = None
1260 return user
1261
1262
1263 class Xing(OAuth1):
1264 """
1265 Xing |oauth1| provider.
1266
1267 * Dashboard: https://dev.xing.com/applications
1268 * Docs: https://dev.xing.com/docs/authentication
1269 * API reference: https://dev.xing.com/docs/resources
1270
1271 Supported :class:`.User` properties:
1272
1273 * birth_date
1274 * city
1275 * country
1276 * email
1277 * first_name
1278 * gender
1279 * id
1280 * last_name
1281 * link
1282 * locale
1283 * location
1284 * name
1285 * phone
1286 * picture
1287 * postal_code
1288 * timezone
1289 * username
1290
1291 Unsupported :class:`.User` properties:
1292
1293 * nickname
1294
1295 """
1296
1297 request_token_url = 'https://api.xing.com/v1/request_token'
1298 user_authorization_url = 'https://api.xing.com/v1/authorize'
1299 access_token_url = 'https://api.xing.com/v1/access_token'
1300 user_info_url = 'https://api.xing.com/v1/users/me'
1301
1302 supported_user_attributes = core.SupportedUserAttributes(
1303 birth_date=True,
1304 city=True,
1305 country=True,
1306 email=True,
1307 first_name=True,
1308 gender=True,
1309 id=True,
1310 last_name=True,
1311 link=True,
1312 locale=True,
1313 location=True,
1314 name=True,
1315 phone=True,
1316 picture=True,
1317 postal_code=True,
1318 timezone=True,
1319 username=True,
1320 )
1321
1322 @staticmethod
1323 def _x_user_parser(user, data):
1324 _users = data.get('users', [])
1325 if _users and _users[0]:
1326 _user = _users[0]
1327 user.id = _user.get('id')
1328 user.name = _user.get('display_name')
1329 user.first_name = _user.get('first_name')
1330 user.last_name = _user.get('last_name')
1331 user.gender = _user.get('gender')
1332 user.timezone = _user.get('time_zone', {}).get('name')
1333 user.email = _user.get('active_email')
1334 user.link = _user.get('permalink')
1335 user.username = _user.get('page_name')
1336 user.picture = _user.get('photo_urls', {}).get('large')
1337
1338 _address = _user.get('business_address', {})
1339 if _address:
1340 user.city = _address.get('city')
1341 user.country = _address.get('country')
1342 user.postal_code = _address.get('zip_code')
1343 user.phone = (
1344 _address.get('phone', '') or
1345 _address.get('mobile_phone', '')).replace('|', '')
1346
1347 _languages = list(_user.get('languages', {}).keys())
1348 if _languages and _languages[0]:
1349 user.locale = _languages[0]
1350
1351 _birth_date = _user.get('birth_date', {})
1352 _year = _birth_date.get('year')
1353 _month = _birth_date.get('month')
1354 _day = _birth_date.get('day')
1355 if _year and _month and _day:
1356 user.birth_date = datetime.datetime(_year, _month, _day)
1357
1358 return user
1359
1360
1361 # The provider type ID is generated from this list's indexes!
1362 # Always append new providers at the end so that ids of existing providers
1363 # don't change!
1364 PROVIDER_ID_MAP = [
1365 Bitbucket,
1366 Flickr,
1367 Meetup,
1368 OAuth1,
1369 Plurk,
1370 Tumblr,
1371 Twitter,
1372 UbuntuOne,
1373 Vimeo,
1374 Xero,
1375 Xing,
1376 Yahoo,
1377 ]
This diff has been collapsed as it changes many lines, (2053 lines changed) Show them Hide them
@@ -0,0 +1,2053 b''
1 # -*- coding: utf-8 -*-
2 """
3 |oauth2| Providers
4 -------------------
5
6 Providers which implement the |oauth2|_ protocol.
7
8 .. autosummary::
9
10 OAuth2
11 Amazon
12 Behance
13 Bitly
14 Bitbucket
15 Cosm
16 DeviantART
17 Eventbrite
18 Facebook
19 Foursquare
20 GitHub
21 Google
22 LinkedIn
23 PayPal
24 Reddit
25 Viadeo
26 VK
27 WindowsLive
28 Yammer
29 Yandex
30
31 """
32
33 import base64
34 import datetime
35 import json
36 import logging
37
38 from authomatic.six.moves.urllib.parse import unquote
39 from authomatic import providers
40 from authomatic.exceptions import CancellationError, FailureError, OAuth2Error
41 import authomatic.core as core
42
43
44 __all__ = [
45 'OAuth2',
46 'Amazon',
47 'Behance',
48 'Bitly',
49 'Bitbucket',
50 'Cosm',
51 'DeviantART',
52 'Eventbrite',
53 'Facebook',
54 'Foursquare',
55 'GitHub',
56 'Google',
57 'LinkedIn',
58 'PayPal',
59 'Reddit',
60 'Viadeo',
61 'VK',
62 'WindowsLive',
63 'Yammer',
64 'Yandex'
65 ]
66
67
68 class OAuth2(providers.AuthorizationProvider):
69 """
70 Base class for |oauth2|_ providers.
71 """
72
73 PROVIDER_TYPE_ID = 2
74 TOKEN_TYPES = ['', 'Bearer']
75
76 #: A scope preset to get most of the **user** info.
77 #: Use it in the :doc:`config` like
78 #: ``{'scope': oauth2.Facebook.user_info_scope}``.
79 user_info_scope = []
80
81 #: :class:`bool` If ``False``, the provider doesn't support CSRF
82 #: protection.
83 supports_csrf_protection = True
84
85 #: :class:`bool` If ``False``, the provider doesn't support user_state.
86 supports_user_state = True
87
88 token_request_method = 'POST' # method for requesting an access token
89
90 def __init__(self, *args, **kwargs):
91 """
92 Accepts additional keyword arguments:
93
94 :param list scope:
95 List of strings specifying requested permissions as described
96 in the
97 `OAuth 2.0 spec <http://tools.ietf.org/html/rfc6749#section-3.3>`_.
98
99 :param bool offline:
100 If ``True`` the **provider** will be set up to request an
101 *offline access token*.
102 Default is ``False``.
103
104 As well as those inherited from :class:`.AuthorizationProvider`
105 constructor.
106
107 """
108
109 super(OAuth2, self).__init__(*args, **kwargs)
110
111 self.scope = self._kwarg(kwargs, 'scope', [])
112 self.offline = self._kwarg(kwargs, 'offline', False)
113
114 # ========================================================================
115 # Internal methods
116 # ========================================================================
117
118 def _x_scope_parser(self, scope):
119 """
120 Override this to handle differences between accepted format of scope
121 across providers.
122
123 :attr list scope:
124 List of scopes.
125
126 """
127
128 # pylint:disable=no-self-use
129
130 # Most providers accept csv scope.
131 return ','.join(scope) if scope else ''
132
133 @classmethod
134 def create_request_elements(
135 cls, request_type, credentials, url, method='GET', params=None,
136 headers=None, body='', secret=None, redirect_uri='', scope='',
137 csrf='', user_state=''
138 ):
139 """
140 Creates |oauth2| request elements.
141 """
142
143 headers = headers or {}
144 params = params or {}
145
146 consumer_key = credentials.consumer_key or ''
147 consumer_secret = credentials.consumer_secret or ''
148 token = credentials.token or ''
149 refresh_token = credentials.refresh_token or credentials.token or ''
150
151 # Separate url base and query parameters.
152 url, base_params = cls._split_url(url)
153
154 # Add params extracted from URL.
155 params.update(dict(base_params))
156
157 if request_type == cls.USER_AUTHORIZATION_REQUEST_TYPE:
158 # User authorization request.
159 # TODO: Raise error for specific message for each missing argument.
160 if consumer_key and redirect_uri and (
161 csrf or not cls.supports_csrf_protection):
162 params['client_id'] = consumer_key
163 params['redirect_uri'] = redirect_uri
164 params['scope'] = scope
165 if cls.supports_user_state:
166 params['state'] = base64.urlsafe_b64encode(
167 json.dumps(
168 {"csrf": csrf, "user_state": user_state}
169 ).encode('utf-8')
170 )
171 else:
172 params['state'] = csrf
173 params['response_type'] = 'code'
174
175 # Add authorization header
176 headers.update(cls._authorization_header(credentials))
177 else:
178 raise OAuth2Error(
179 'Credentials with valid consumer_key and arguments '
180 'redirect_uri, scope and state are required to create '
181 'OAuth 2.0 user authorization request elements!')
182
183 elif request_type == cls.ACCESS_TOKEN_REQUEST_TYPE:
184 # Access token request.
185 if consumer_key and consumer_secret:
186 params['code'] = token
187 params['client_id'] = consumer_key
188 params['client_secret'] = consumer_secret
189 params['redirect_uri'] = redirect_uri
190 params['grant_type'] = 'authorization_code'
191
192 # TODO: Check whether all providers accept it
193 headers.update(cls._authorization_header(credentials))
194 else:
195 raise OAuth2Error(
196 'Credentials with valid token, consumer_key, '
197 'consumer_secret and argument redirect_uri are required '
198 'to create OAuth 2.0 access token request elements!')
199
200 elif request_type == cls.REFRESH_TOKEN_REQUEST_TYPE:
201 # Refresh access token request.
202 if refresh_token and consumer_key and consumer_secret:
203 params['refresh_token'] = refresh_token
204 params['client_id'] = consumer_key
205 params['client_secret'] = consumer_secret
206 params['grant_type'] = 'refresh_token'
207 else:
208 raise OAuth2Error(
209 'Credentials with valid refresh_token, consumer_key, '
210 'consumer_secret are required to create OAuth 2.0 '
211 'refresh token request elements!')
212
213 elif request_type == cls.PROTECTED_RESOURCE_REQUEST_TYPE:
214 # Protected resource request.
215
216 # Add Authorization header. See:
217 # http://tools.ietf.org/html/rfc6749#section-7.1
218 if credentials.token_type == cls.BEARER:
219 # http://tools.ietf.org/html/rfc6750#section-2.1
220 headers.update(
221 {'Authorization': 'Bearer {0}'.format(credentials.token)})
222
223 elif token:
224 params['access_token'] = token
225 else:
226 raise OAuth2Error(
227 'Credentials with valid token are required to create '
228 'OAuth 2.0 protected resources request elements!')
229
230 request_elements = core.RequestElements(
231 url, method, params, headers, body)
232
233 return cls._x_request_elements_filter(
234 request_type, request_elements, credentials)
235
236 @staticmethod
237 def _x_refresh_credentials_if(credentials):
238 """
239 Override this to specify conditions when it gives sense to refresh
240 credentials.
241
242 .. warning::
243
244 |classmethod|
245
246 :param credentials:
247 :class:`.Credentials`
248
249 :returns:
250 ``True`` or ``False``
251
252 """
253
254 if credentials.refresh_token:
255 return True
256
257 # ========================================================================
258 # Exposed methods
259 # ========================================================================
260
261 @classmethod
262 def to_tuple(cls, credentials):
263 return (credentials.token,
264 credentials.refresh_token,
265 credentials.expiration_time,
266 cls.TOKEN_TYPES.index(credentials.token_type))
267
268 @classmethod
269 def reconstruct(cls, deserialized_tuple, credentials, cfg):
270
271 token, refresh_token, expiration_time, token_type = deserialized_tuple
272
273 credentials.token = token
274 credentials.refresh_token = refresh_token
275 credentials.expiration_time = expiration_time
276 credentials.token_type = cls.TOKEN_TYPES[int(token_type)]
277
278 return credentials
279
280 @classmethod
281 def decode_state(cls, state, param='user_state'):
282 """
283 Decode state and return param.
284
285 :param str state:
286 state parameter passed through by provider
287
288 :param str param:
289 key to query from decoded state variable. Options include 'csrf'
290 and 'user_state'.
291
292 :returns:
293 string value from decoded state
294
295 """
296 if state and cls.supports_user_state:
297 # urlsafe_b64 may include = which the browser quotes so must
298 # unquote Cast to str to void b64decode translation error. Base64
299 # should be str compatible.
300 return json.loads(base64.urlsafe_b64decode(
301 unquote(str(state))).decode('utf-8'))[param]
302 else:
303 return state if param == 'csrf' else ''
304
305 def refresh_credentials(self, credentials):
306 """
307 Refreshes :class:`.Credentials` if it gives sense.
308
309 :param credentials:
310 :class:`.Credentials` to be refreshed.
311
312 :returns:
313 :class:`.Response`.
314
315 """
316
317 if not self._x_refresh_credentials_if(credentials):
318 return
319
320 # We need consumer key and secret to make this kind of request.
321 cfg = credentials.config.get(credentials.provider_name)
322 credentials.consumer_key = cfg.get('consumer_key')
323 credentials.consumer_secret = cfg.get('consumer_secret')
324
325 request_elements = self.create_request_elements(
326 request_type=self.REFRESH_TOKEN_REQUEST_TYPE,
327 credentials=credentials,
328 url=self.access_token_url,
329 method='POST'
330 )
331
332 self._log(logging.INFO, u'Refreshing credentials.')
333 response = self._fetch(*request_elements)
334
335 # We no longer need consumer info.
336 credentials.consumer_key = None
337 credentials.consumer_secret = None
338
339 # Extract the refreshed data.
340 access_token = response.data.get('access_token')
341 refresh_token = response.data.get('refresh_token')
342
343 # Update credentials only if there is access token.
344 if access_token:
345 credentials.token = access_token
346 credentials.expire_in = response.data.get('expires_in')
347
348 # Update refresh token only if there is a new one.
349 if refresh_token:
350 credentials.refresh_token = refresh_token
351
352 # Handle different naming conventions across providers.
353 credentials = self._x_credentials_parser(
354 credentials, response.data)
355
356 return response
357
358 @providers.login_decorator
359 def login(self):
360
361 # get request parameters from which we can determine the login phase
362 authorization_code = self.params.get('code')
363 error = self.params.get('error')
364 error_message = self.params.get('error_message')
365 state = self.params.get('state')
366 # optional user_state to be passed in oauth2 state
367 user_state = self.params.get('user_state', '')
368
369 if authorization_code or not self.user_authorization_url:
370
371 if authorization_code:
372 # =============================================================
373 # Phase 2 after redirect with success
374 # =============================================================
375
376 self._log(
377 logging.INFO,
378 u'Continuing OAuth 2.0 authorization procedure after '
379 u'redirect.')
380
381 # validate CSRF token
382 if self.supports_csrf_protection:
383 self._log(
384 logging.INFO,
385 u'Validating request by comparing request state with '
386 u'stored state.')
387 stored_csrf = self._session_get('csrf')
388
389 state_csrf = self.decode_state(state, 'csrf')
390 if not stored_csrf:
391 raise FailureError(u'Unable to retrieve stored state!')
392 elif stored_csrf != state_csrf:
393 raise FailureError(
394 u'The returned state csrf cookie "{0}" doesn\'t '
395 u'match with the stored state!'.format(
396 state_csrf
397 ),
398 url=self.user_authorization_url)
399 self._log(logging.INFO, u'Request is valid.')
400 else:
401 self._log(logging.WARN, u'Skipping CSRF validation!')
402
403 elif not self.user_authorization_url:
404 # =============================================================
405 # Phase 1 without user authorization redirect.
406 # =============================================================
407
408 self._log(
409 logging.INFO,
410 u'Starting OAuth 2.0 authorization procedure without '
411 u'user authorization redirect.')
412
413 # exchange authorization code for access token by the provider
414 self._log(
415 logging.INFO,
416 u'Fetching access token from {0}.'.format(
417 self.access_token_url))
418
419 self.credentials.token = authorization_code
420
421 request_elements = self.create_request_elements(
422 request_type=self.ACCESS_TOKEN_REQUEST_TYPE,
423 credentials=self.credentials,
424 url=self.access_token_url,
425 method=self.token_request_method,
426 redirect_uri=self.url,
427 params=self.access_token_params,
428 headers=self.access_token_headers
429 )
430
431 response = self._fetch(*request_elements)
432 self.access_token_response = response
433
434 access_token = response.data.get('access_token', '')
435 refresh_token = response.data.get('refresh_token', '')
436
437 if response.status != 200 or not access_token:
438 raise FailureError(
439 'Failed to obtain OAuth 2.0 access token from {0}! '
440 'HTTP status: {1}, message: {2}.'.format(
441 self.access_token_url,
442 response.status,
443 response.content
444 ),
445 original_message=response.content,
446 status=response.status,
447 url=self.access_token_url)
448
449 self._log(logging.INFO, u'Got access token.')
450
451 if refresh_token:
452 self._log(logging.INFO, u'Got refresh access token.')
453
454 # OAuth 2.0 credentials need access_token, refresh_token,
455 # token_type and expire_in.
456 self.credentials.token = access_token
457 self.credentials.refresh_token = refresh_token
458 self.credentials.expire_in = response.data.get('expires_in')
459 self.credentials.token_type = response.data.get('token_type', '')
460 # sWe don't need these two guys anymore.
461 self.credentials.consumer_key = ''
462 self.credentials.consumer_secret = ''
463
464 # update credentials
465 self.credentials = self._x_credentials_parser(
466 self.credentials, response.data)
467
468 # create user
469 self._update_or_create_user(response.data, self.credentials)
470
471 # =================================================================
472 # We're done!
473 # =================================================================
474
475 elif error or error_message:
476 # =================================================================
477 # Phase 2 after redirect with error
478 # =================================================================
479
480 error_reason = self.params.get('error_reason') or error
481 error_description = self.params.get('error_description') \
482 or error_message or error
483
484 if error_reason and 'denied' in error_reason:
485 raise CancellationError(error_description,
486 url=self.user_authorization_url)
487 else:
488 raise FailureError(
489 error_description,
490 url=self.user_authorization_url)
491
492 elif (
493 not self.params or
494 len(self.params) == 1 and
495 'user_state' in self.params
496 ):
497 # =================================================================
498 # Phase 1 before redirect
499 # =================================================================
500
501 self._log(
502 logging.INFO,
503 u'Starting OAuth 2.0 authorization procedure.')
504
505 csrf = ''
506 if self.supports_csrf_protection:
507 # generate csfr
508 csrf = self.csrf_generator(self.settings.secret)
509 # and store it to session
510 self._session_set('csrf', csrf)
511 else:
512 self._log(
513 logging.WARN,
514 u'Provider doesn\'t support CSRF validation!')
515
516 request_elements = self.create_request_elements(
517 request_type=self.USER_AUTHORIZATION_REQUEST_TYPE,
518 credentials=self.credentials,
519 url=self.user_authorization_url,
520 redirect_uri=self.url,
521 scope=self._x_scope_parser(
522 self.scope),
523 csrf=csrf,
524 user_state=user_state,
525 params=self.user_authorization_params
526 )
527
528 self._log(
529 logging.INFO,
530 u'Redirecting user to {0}.'.format(
531 request_elements.full_url))
532
533 self.redirect(request_elements.full_url)
534
535
536 class Amazon(OAuth2):
537 """
538 Amazon |oauth2| provider.
539
540 Thanks to `Ghufran Syed <https://github.com/ghufransyed>`__.
541
542 * Dashboard: https://developer.amazon.com/lwa/sp/overview.html
543 * Docs: https://developer.amazon.com/public/apis/engage/login-with-amazon/docs/conceptual_overview.html
544 * API reference: https://developer.amazon.com/public/apis
545
546 .. note::
547
548 Amazon only accepts **redirect_uri** with **https** schema,
549 Therefore the *login handler* must also be accessible through
550 **https**.
551
552 Supported :class:`.User` properties:
553
554 * email
555 * id
556 * name
557 * postal_code
558
559 Unsupported :class:`.User` properties:
560
561 * birth_date
562 * city
563 * country
564 * first_name
565 * gender
566 * last_name
567 * link
568 * locale
569 * nickname
570 * phone
571 * picture
572 * timezone
573 * username
574
575 """
576
577 user_authorization_url = 'https://www.amazon.com/ap/oa'
578 access_token_url = 'https://api.amazon.com/auth/o2/token'
579 user_info_url = 'https://api.amazon.com/user/profile'
580 user_info_scope = ['profile', 'postal_code']
581
582 supported_user_attributes = core.SupportedUserAttributes(
583 email=True,
584 id=True,
585 name=True,
586 postal_code=True
587 )
588
589 def _x_scope_parser(self, scope):
590 # Amazon has space-separated scopes
591 return ' '.join(scope)
592
593 @staticmethod
594 def _x_user_parser(user, data):
595 user.id = data.get('user_id')
596 return user
597
598 @classmethod
599 def _x_credentials_parser(cls, credentials, data):
600 if data.get('token_type') == 'bearer':
601 credentials.token_type = cls.BEARER
602 return credentials
603
604
605 class Behance(OAuth2):
606 """
607 Behance |oauth2| provider.
608
609 .. note::
610
611 Behance doesn't support third party authorization anymore,
612 which renders this class pretty much useless.
613
614 * Dashboard: http://www.behance.net/dev/apps
615 * Docs: http://www.behance.net/dev/authentication
616 * API reference: http://www.behance.net/dev/api/endpoints/
617
618 """
619
620 user_authorization_url = 'https://www.behance.net/v2/oauth/authenticate'
621 access_token_url = 'https://www.behance.net/v2/oauth/token'
622 user_info_url = ''
623
624 user_info_scope = ['activity_read']
625
626 def _x_scope_parser(self, scope):
627 """
628 Behance has pipe-separated scopes.
629 """
630 return '|'.join(scope)
631
632 @staticmethod
633 def _x_user_parser(user, data):
634
635 _user = data.get('user', {})
636
637 user.id = _user.get('id')
638 user.first_name = _user.get('first_name')
639 user.last_name = _user.get('last_name')
640 user.username = _user.get('username')
641 user.city = _user.get('city')
642 user.country = _user.get('country')
643 user.link = _user.get('url')
644 user.name = _user.get('display_name')
645 user.picture = _user.get('images', {}).get('138')
646
647 return user
648
649
650 class Bitly(OAuth2):
651 """
652 Bitly |oauth2| provider.
653
654 .. warning::
655
656 |no-csrf|
657
658 * Dashboard: https://bitly.com/a/oauth_apps
659 * Docs: http://dev.bitly.com/authentication.html
660 * API reference: http://dev.bitly.com/api.html
661
662 Supported :class:`.User` properties:
663
664 * id
665 * link
666 * name
667 * picture
668 * username
669
670 Unsupported :class:`.User` properties:
671
672 * birth_date
673 * city
674 * country
675 * email
676 * first_name
677 * gender
678 * last_name
679 * locale
680 * nickname
681 * phone
682 * postal_code
683 * timezone
684
685 """
686
687 supported_user_attributes = core.SupportedUserAttributes(
688 id=True,
689 link=True,
690 name=True,
691 picture=True,
692 username=True
693 )
694
695 supports_csrf_protection = False
696 _x_use_authorization_header = False
697
698 user_authorization_url = 'https://bitly.com/oauth/authorize'
699 access_token_url = 'https://api-ssl.bitly.com/oauth/access_token'
700 user_info_url = 'https://api-ssl.bitly.com/v3/user/info'
701
702 def __init__(self, *args, **kwargs):
703 super(Bitly, self).__init__(*args, **kwargs)
704
705 if self.offline:
706 if 'grant_type' not in self.access_token_params:
707 self.access_token_params['grant_type'] = 'refresh_token'
708
709 @staticmethod
710 def _x_user_parser(user, data):
711 info = data.get('data', {})
712
713 user.id = info.get('login')
714 user.name = info.get('full_name')
715 user.username = info.get('display_name')
716 user.picture = info.get('profile_image')
717 user.link = info.get('profile_url')
718
719 return user
720
721
722 class Cosm(OAuth2):
723 """
724 Cosm |oauth2| provider.
725
726 .. note::
727
728 Cosm doesn't provide any *user info URL*.
729
730 * Dashboard: https://cosm.com/users/{your_username}/apps
731 * Docs: https://cosm.com/docs/
732 * API reference: https://cosm.com/docs/v2/
733
734 """
735
736 user_authorization_url = 'https://cosm.com/oauth/authenticate'
737 access_token_url = 'https://cosm.com/oauth/token'
738 user_info_url = ''
739
740 @staticmethod
741 def _x_user_parser(user, data):
742 user.id = user.username = data.get('user')
743 return user
744
745
746 class DeviantART(OAuth2):
747 """
748 DeviantART |oauth2| provider.
749
750 * Dashboard: https://www.deviantart.com/settings/myapps
751 * Docs: https://www.deviantart.com/developers/authentication
752 * API reference: http://www.deviantart.com/developers/oauth2
753
754 .. note::
755
756 Although it is not documented anywhere, DeviantART requires the
757 *access token* request to contain a ``User-Agent`` header.
758 You can apply a default ``User-Agent`` header for all API calls in the
759 config like this:
760
761 .. code-block:: python
762 :emphasize-lines: 6
763
764 CONFIG = {
765 'deviantart': {
766 'class_': oauth2.DeviantART,
767 'consumer_key': '#####',
768 'consumer_secret': '#####',
769 'access_headers': {'User-Agent': 'Some User Agent'},
770 }
771 }
772
773 Supported :class:`.User` properties:
774
775 * name
776 * picture
777 * username
778
779 Unsupported :class:`.User` properties:
780
781 * birth_date
782 * city
783 * country
784 * email
785 * first_name
786 * gender
787 * id
788 * last_name
789 * link
790 * locale
791 * nickname
792 * phone
793 * postal_code
794 * timezone
795
796 """
797
798 user_authorization_url = 'https://www.deviantart.com/oauth2/authorize'
799 access_token_url = 'https://www.deviantart.com/oauth2/token'
800 user_info_url = 'https://www.deviantart.com/api/oauth2/user/whoami'
801
802 user_info_scope = ['basic']
803
804 supported_user_attributes = core.SupportedUserAttributes(
805 name=True,
806 picture=True,
807 username=True
808 )
809
810 def __init__(self, *args, **kwargs):
811 super(DeviantART, self).__init__(*args, **kwargs)
812
813 if self.offline:
814 if 'grant_type' not in self.access_token_params:
815 self.access_token_params['grant_type'] = 'refresh_token'
816
817 @staticmethod
818 def _x_user_parser(user, data):
819 user.picture = data.get('usericonurl')
820 return user
821
822
823 class Eventbrite(OAuth2):
824 """
825 Eventbrite |oauth2| provider.
826
827 Thanks to `Paul Brown <http://www.paulsprogrammingnotes.com/>`__.
828
829 * Dashboard: http://www.eventbrite.com/myaccount/apps/
830 * Docs: https://developer.eventbrite.com/docs/auth/
831 * API: http://developer.eventbrite.com/docs/
832
833 Supported :class:`.User` properties:
834
835 * email
836 * first_name
837 * id
838 * last_name
839 * name
840
841 Unsupported :class:`.User` properties:
842
843 * birth_date
844 * city
845 * country
846 * gender
847 * link
848 * locale
849 * nickname
850 * phone
851 * picture
852 * postal_code
853 * timezone
854 * username
855
856 """
857
858 user_authorization_url = 'https://www.eventbrite.com/oauth/authorize'
859 access_token_url = 'https://www.eventbrite.com/oauth/token'
860 user_info_url = 'https://www.eventbriteapi.com/v3/users/me'
861
862 supported_user_attributes = core.SupportedUserAttributes(
863 email=True,
864 first_name=True,
865 id=True,
866 last_name=True,
867 name=True,
868 )
869
870 @classmethod
871 def _x_credentials_parser(cls, credentials, data):
872 if data.get('token_type') == 'bearer':
873 credentials.token_type = cls.BEARER
874 return credentials
875
876 @staticmethod
877 def _x_user_parser(user, data):
878 for email in data.get('emails', []):
879 if email.get('primary'):
880 user.email = email.get('email')
881 break
882
883 return user
884
885
886 class Facebook(OAuth2):
887 """
888 Facebook |oauth2| provider.
889
890 * Dashboard: https://developers.facebook.com/apps
891 * Docs: http://developers.facebook.com/docs/howtos/login/server-side-login/
892 * API reference: http://developers.facebook.com/docs/reference/api/
893 * API explorer: http://developers.facebook.com/tools/explorer
894
895 Supported :class:`.User` properties:
896
897 * birth_date
898 * email
899 * first_name
900 * id
901 * last_name
902 * name
903 * picture
904
905 Unsupported :class:`.User` properties:
906
907 * nickname
908 * phone
909 * postal_code
910 * username
911
912 """
913 user_authorization_url = 'https://www.facebook.com/dialog/oauth'
914 access_token_url = 'https://graph.facebook.com/oauth/access_token'
915 user_info_url = 'https://graph.facebook.com/v2.3/me'
916 user_info_scope = ['email', 'public_profile', 'user_birthday',
917 'user_location']
918 same_origin = False
919
920 supported_user_attributes = core.SupportedUserAttributes(
921 birth_date=True,
922 city=False,
923 country=False,
924 email=True,
925 first_name=True,
926 gender=False,
927 id=True,
928 last_name=True,
929 link=False,
930 locale=False,
931 location=False,
932 name=True,
933 picture=True,
934 timezone=False,
935 username=False,
936 )
937
938 @classmethod
939 def _x_request_elements_filter(cls, request_type, request_elements,
940 credentials):
941
942 if request_type == cls.REFRESH_TOKEN_REQUEST_TYPE:
943 # As always, Facebook has it's original name for "refresh_token"!
944 url, method, params, headers, body = request_elements
945 params['fb_exchange_token'] = params.pop('refresh_token')
946 params['grant_type'] = 'fb_exchange_token'
947 request_elements = core.RequestElements(url, method, params,
948 headers, body)
949
950 return request_elements
951
952 def __init__(self, *args, **kwargs):
953 super(Facebook, self).__init__(*args, **kwargs)
954
955 # Handle special Facebook requirements to be able
956 # to refresh the access token.
957 if self.offline:
958 # Facebook needs an offline_access scope.
959 if 'offline_access' not in self.scope:
960 self.scope.append('offline_access')
961
962 if self.popup:
963 self.user_authorization_url += '?display=popup'
964
965 @staticmethod
966 def _x_user_parser(user, data):
967 _birth_date = data.get('birthday')
968 if _birth_date:
969 try:
970 user.birth_date = datetime.datetime.strptime(_birth_date,
971 '%m/%d/%Y')
972 except ValueError:
973 pass
974
975 user.picture = ('http://graph.facebook.com/{0}/picture?type=large'
976 .format(user.id))
977
978 user.location = data.get('location', {}).get('name')
979 if user.location:
980 split_location = user.location.split(', ')
981 user.city = split_location[0].strip()
982 if len(split_location) > 1:
983 user.country = split_location[1].strip()
984
985 return user
986
987 @staticmethod
988 def _x_credentials_parser(credentials, data):
989 """
990 We need to override this method to fix Facebooks naming deviation.
991 """
992
993 # Facebook returns "expires" instead of "expires_in".
994 credentials.expire_in = data.get('expires')
995
996 if data.get('token_type') == 'bearer':
997 # TODO: cls is not available here, hardcode for now.
998 credentials.token_type = 'Bearer'
999
1000 return credentials
1001
1002 @staticmethod
1003 def _x_refresh_credentials_if(credentials):
1004 # Always refresh.
1005 return True
1006
1007 def access(self, url, params=None, **kwargs):
1008 if params is None:
1009 params = {}
1010 params['fields'] = 'id,first_name,last_name,picture,email,gender,' + \
1011 'timezone,location,birthday,locale'
1012
1013 return super(Facebook, self).access(url, params, **kwargs)
1014
1015
1016 class Foursquare(OAuth2):
1017 """
1018 Foursquare |oauth2| provider.
1019
1020 * Dashboard: https://foursquare.com/developers/apps
1021 * Docs: https://developer.foursquare.com/overview/auth.html
1022 * API reference: https://developer.foursquare.com/docs/
1023
1024 .. note::
1025
1026 Foursquare requires a *version* parameter in each request.
1027 The default value is ``v=20140501``. You can override the version in
1028 the ``params`` parameter of the :meth:`.Authomatic.access` method.
1029 See https://developer.foursquare.com/overview/versioning
1030
1031 Supported :class:`.User` properties:
1032
1033 * city
1034 * country
1035 * email
1036 * first_name
1037 * gender
1038 * id
1039 * last_name
1040 * location
1041 * name
1042 * phone
1043 * picture
1044
1045 Unsupported :class:`.User` properties:
1046
1047 * birth_date
1048 * link
1049 * locale
1050 * nickname
1051 * postal_code
1052 * timezone
1053 * username
1054
1055 """
1056
1057 user_authorization_url = 'https://foursquare.com/oauth2/authenticate'
1058 access_token_url = 'https://foursquare.com/oauth2/access_token'
1059 user_info_url = 'https://api.foursquare.com/v2/users/self'
1060
1061 same_origin = False
1062
1063 supported_user_attributes = core.SupportedUserAttributes(
1064 birth_date=True,
1065 city=True,
1066 country=True,
1067 email=True,
1068 first_name=True,
1069 gender=True,
1070 id=True,
1071 last_name=True,
1072 location=True,
1073 name=True,
1074 phone=True,
1075 picture=True
1076 )
1077
1078 @classmethod
1079 def _x_request_elements_filter(cls, request_type, request_elements,
1080 credentials):
1081
1082 if request_type == cls.PROTECTED_RESOURCE_REQUEST_TYPE:
1083 # Foursquare uses OAuth 1.0 "oauth_token" for what should be
1084 # "access_token" in OAuth 2.0!
1085 url, method, params, headers, body = request_elements
1086 params['oauth_token'] = params.pop('access_token')
1087
1088 # Foursquare needs the version "v" parameter in every request.
1089 # https://developer.foursquare.com/overview/versioning
1090 if not params.get('v'):
1091 params['v'] = '20140501'
1092
1093 request_elements = core.RequestElements(url, method, params,
1094 headers, body)
1095
1096 return request_elements
1097
1098 @staticmethod
1099 def _x_user_parser(user, data):
1100
1101 _resp = data.get('response', {})
1102 _user = _resp.get('user', {})
1103
1104 user.id = _user.get('id')
1105 user.first_name = _user.get('firstName')
1106 user.last_name = _user.get('lastName')
1107 user.gender = _user.get('gender')
1108
1109 _birth_date = _user.get('birthday')
1110 if _birth_date:
1111 user.birth_date = datetime.datetime.fromtimestamp(_birth_date)
1112
1113 _photo = _user.get('photo', {})
1114 if isinstance(_photo, dict):
1115 _photo_prefix = _photo.get('prefix', '').strip('/')
1116 _photo_suffix = _photo.get('suffix', '').strip('/')
1117 user.picture = '/'.join([_photo_prefix, _photo_suffix])
1118
1119 if isinstance(_photo, str):
1120 user.picture = _photo
1121
1122 user.location = _user.get('homeCity')
1123 if user.location:
1124 split_location = user.location.split(',')
1125 user.city = split_location[0].strip()
1126 if len(user.location) > 1:
1127 user.country = split_location[1].strip()
1128
1129 _contact = _user.get('contact', {})
1130 user.email = _contact.get('email')
1131 user.phone = _contact.get('phone')
1132
1133 return user
1134
1135
1136 class Bitbucket(OAuth2):
1137
1138 user_authorization_url = 'https://bitbucket.org/site/oauth2/authorize'
1139 access_token_url = 'https://bitbucket.org/site/oauth2/access_token'
1140 user_info_url = 'https://bitbucket.org/api/2.0/user'
1141 user_email_info_url = 'https://bitbucket.org/api/2.0/user/emails'
1142
1143 same_origin = False
1144
1145 supported_user_attributes = core.SupportedUserAttributes(
1146 id=True,
1147 first_name=True,
1148 last_name=True,
1149 link=True,
1150 name=True,
1151 picture=True,
1152 username=True,
1153 email=True
1154 )
1155
1156 @staticmethod
1157 def _x_user_parser(user, data):
1158 user.username = user.id = data.get('username')
1159 user.name = data.get('display_name')
1160 user.first_name = data.get('first_name')
1161 user.last_name = data.get('last_name')
1162
1163 return user
1164
1165 @classmethod
1166 def _x_credentials_parser(cls, credentials, data):
1167 if data.get('token_type') == 'bearer':
1168 credentials.token_type = cls.BEARER
1169 return credentials
1170
1171 def _access_user_info(self):
1172 """
1173 Email is available in separate method so second request is needed.
1174 """
1175 response = super(Bitbucket, self)._access_user_info()
1176
1177 response.data.setdefault("email", None)
1178
1179 email_response = self.access(self.user_email_info_url)
1180 emails = email_response.data.get('values', [])
1181 if emails:
1182 for item in emails:
1183 if item.get("is_primary", False):
1184 response.data.update(email=item.get("email", None))
1185
1186 return response
1187
1188
1189 class GitHub(OAuth2):
1190 """
1191 GitHub |oauth2| provider.
1192
1193 * Dashboard: https://github.com/settings/developers
1194 * Docs: http://developer.github.com/v3/#authentication
1195 * API reference: http://developer.github.com/v3/
1196
1197 .. note::
1198
1199 GitHub API
1200 `documentation <http://developer.github.com/v3/#user-agent-required>`_
1201 says:
1202
1203 all API requests MUST include a valid ``User-Agent`` header.
1204
1205 You can apply a default ``User-Agent`` header for all API calls in
1206 the config like this:
1207
1208 .. code-block:: python
1209 :emphasize-lines: 6
1210
1211 CONFIG = {
1212 'github': {
1213 'class_': oauth2.GitHub,
1214 'consumer_key': '#####',
1215 'consumer_secret': '#####',
1216 'access_headers': {'User-Agent': 'Awesome-Octocat-App'},
1217 }
1218 }
1219
1220 Supported :class:`.User` properties:
1221
1222 * email
1223 * id
1224 * link
1225 * location
1226 * name
1227 * picture
1228 * username
1229
1230 Unsupported :class:`.User` properties:
1231
1232 * birth_date
1233 * city
1234 * country
1235 * first_name
1236 * gender
1237 * last_name
1238 * locale
1239 * nickname
1240 * phone
1241 * postal_code
1242 * timezone
1243
1244 """
1245
1246 user_authorization_url = 'https://github.com/login/oauth/authorize'
1247 access_token_url = 'https://github.com/login/oauth/access_token'
1248 user_info_url = 'https://api.github.com/user'
1249
1250 same_origin = False
1251
1252 supported_user_attributes = core.SupportedUserAttributes(
1253 email=True,
1254 id=True,
1255 link=True,
1256 location=True,
1257 name=True,
1258 picture=True,
1259 username=True
1260 )
1261
1262 @staticmethod
1263 def _x_user_parser(user, data):
1264 user.username = data.get('login')
1265 user.picture = data.get('avatar_url')
1266 user.link = data.get('html_url')
1267 return user
1268
1269 @classmethod
1270 def _x_credentials_parser(cls, credentials, data):
1271 if data.get('token_type') == 'bearer':
1272 credentials.token_type = cls.BEARER
1273 return credentials
1274
1275
1276 class Google(OAuth2):
1277 """
1278 Google |oauth2| provider.
1279
1280 * Dashboard: https://console.developers.google.com/project
1281 * Docs: https://developers.google.com/accounts/docs/OAuth2
1282 * API reference: https://developers.google.com/gdata/docs/directory
1283 * API explorer: https://developers.google.com/oauthplayground/
1284
1285 Supported :class:`.User` properties:
1286
1287 * email
1288 * first_name
1289 * gender
1290 * id
1291 * last_name
1292 * link
1293 * locale
1294 * name
1295 * picture
1296
1297 Unsupported :class:`.User` properties:
1298
1299 * birth_date
1300 * city
1301 * country
1302 * nickname
1303 * phone
1304 * postal_code
1305 * timezone
1306 * username
1307
1308 .. note::
1309
1310 To get the user info, you need to activate the **Google+ API**
1311 in the **APIs & auth >> APIs** section of the`Google Developers Console
1312 <https://console.developers.google.com/project>`__.
1313
1314 """
1315
1316 user_authorization_url = 'https://accounts.google.com/o/oauth2/auth'
1317 access_token_url = 'https://accounts.google.com/o/oauth2/token'
1318 user_info_url = 'https://www.googleapis.com/oauth2/v3/userinfo?alt=json'
1319
1320 user_info_scope = ['profile',
1321 'email']
1322
1323 supported_user_attributes = core.SupportedUserAttributes(
1324 id=True,
1325 email=True,
1326 name=True,
1327 first_name=True,
1328 last_name=True,
1329 locale=True,
1330 picture=True
1331 )
1332
1333 def __init__(self, *args, **kwargs):
1334 super(Google, self).__init__(*args, **kwargs)
1335
1336 # Handle special Google requirements to be able to refresh the access
1337 # token.
1338 if self.offline:
1339 if 'access_type' not in self.user_authorization_params:
1340 # Google needs access_type=offline param in the user
1341 # authorization request.
1342 self.user_authorization_params['access_type'] = 'offline'
1343 if 'approval_prompt' not in self.user_authorization_params:
1344 # And also approval_prompt=force.
1345 self.user_authorization_params['approval_prompt'] = 'force'
1346
1347 @classmethod
1348 def _x_request_elements_filter(cls, request_type, request_elements,
1349 credentials):
1350 """
1351 Google doesn't accept client ID and secret to be at the same time in
1352 request parameters and in the basic authorization header in the access
1353 token request.
1354 """
1355 if request_type is cls.ACCESS_TOKEN_REQUEST_TYPE:
1356 params = request_elements[2]
1357 del params['client_id']
1358 del params['client_secret']
1359 return request_elements
1360
1361 @staticmethod
1362 def _x_user_parser(user, data):
1363 emails = data.get('emails', [])
1364 if emails:
1365 user.email = emails[0].get('value')
1366 for email in emails:
1367 if email.get('type') == 'account':
1368 user.email = email.get('value')
1369 break
1370
1371 user.id = data.get('sub')
1372 user.name = data.get('name')
1373 user.first_name = data.get('given_name', '')
1374 user.last_name = data.get('family_name', '')
1375 user.locale = data.get('locale', '')
1376 user.picture = data.get('picture', '')
1377
1378 user.email_verified = data.get("email_verified")
1379 user.hosted_domain = data.get("hd")
1380 return user
1381
1382 def _x_scope_parser(self, scope):
1383 """
1384 Google has space-separated scopes.
1385 """
1386 return ' '.join(scope)
1387
1388
1389 class LinkedIn(OAuth2):
1390 """
1391 Linked In |oauth2| provider.
1392
1393 .. note::
1394
1395 Doesn't support access token refreshment.
1396
1397 * Dashboard: https://www.linkedin.com/secure/developer
1398 * Docs: http://developer.linkedin.com/documents/authentication
1399 * API reference: http://developer.linkedin.com/rest
1400
1401 Supported :class:`.User` properties:
1402
1403 * city
1404 * country
1405 * email
1406 * first_name
1407 * id
1408 * last_name
1409 * link
1410 * name
1411 * picture
1412
1413 Unsupported :class:`.User` properties:
1414
1415 * birth_date
1416 * gender
1417 * locale
1418 * location
1419 * nickname
1420 * phone
1421 * postal_code
1422 * timezone
1423 * username
1424
1425 """
1426
1427 user_authorization_url = 'https://www.linkedin.com/uas/oauth2/' + \
1428 'authorization'
1429 access_token_url = 'https://www.linkedin.com/uas/oauth2/accessToken'
1430 user_info_url = ('https://api.linkedin.com/v1/people/~:'
1431 '(id,first-name,last-name,formatted-name,location,'
1432 'picture-url,public-profile-url,email-address)'
1433 '?format=json')
1434
1435 user_info_scope = ['r_emailaddress']
1436
1437 token_request_method = 'GET' # To avoid a bug with OAuth2.0 on Linkedin
1438 # http://developer.linkedin.com/forum/unauthorized-invalid-or-expired-token-immediately-after-receiving-oauth2-token
1439
1440 supported_user_attributes = core.SupportedUserAttributes(
1441 city=True,
1442 country=True,
1443 email=True,
1444 first_name=True,
1445 id=True,
1446 last_name=True,
1447 link=True,
1448 location=False,
1449 name=True,
1450 picture=True
1451 )
1452
1453 @classmethod
1454 def _x_request_elements_filter(cls, request_type, request_elements,
1455 credentials):
1456 if request_type == cls.PROTECTED_RESOURCE_REQUEST_TYPE:
1457 # LinkedIn too has it's own terminology!
1458 url, method, params, headers, body = request_elements
1459 params['oauth2_access_token'] = params.pop('access_token')
1460 request_elements = core.RequestElements(url, method, params,
1461 headers, body)
1462
1463 return request_elements
1464
1465 @staticmethod
1466 def _x_user_parser(user, data):
1467
1468 user.first_name = data.get('firstName')
1469 user.last_name = data.get('lastName')
1470 user.email = data.get('emailAddress')
1471 user.name = data.get('formattedName')
1472 user.city = user.city = data.get('location', {}).get('name')
1473 user.country = data.get('location', {}).get('country', {}).get('code')
1474 user.phone = data.get('phoneNumbers', {}).get('values', [{}])[0]\
1475 .get('phoneNumber')
1476 user.picture = data.get('pictureUrl')
1477 user.link = data.get('publicProfileUrl')
1478
1479 _birthdate = data.get('dateOfBirth', {})
1480 if _birthdate:
1481 _day = _birthdate.get('day')
1482 _month = _birthdate.get('month')
1483 _year = _birthdate.get('year')
1484 if _day and _month and _year:
1485 user.birth_date = datetime.datetime(_year, _month, _day)
1486
1487 return user
1488
1489
1490 class PayPal(OAuth2):
1491 """
1492 PayPal |oauth2| provider.
1493
1494 * Dashboard: https://developer.paypal.com/webapps/developer/applications
1495 * Docs:
1496 https://developer.paypal.com/webapps/developer/docs/integration/direct/make-your-first-call/
1497 * API reference: https://developer.paypal.com/webapps/developer/docs/api/
1498
1499 .. note::
1500
1501 Paypal doesn't redirect the **user** to authorize your app!
1502 It grants you an **access token** based on your **app's** key and
1503 secret instead.
1504
1505 """
1506
1507 _x_use_authorization_header = True
1508
1509 supported_user_attributes = core.SupportedUserAttributes()
1510
1511 @classmethod
1512 def _x_request_elements_filter(
1513 cls, request_type, request_elements, credentials):
1514
1515 if request_type == cls.ACCESS_TOKEN_REQUEST_TYPE:
1516 url, method, params, headers, body = request_elements
1517 params['grant_type'] = 'client_credentials'
1518 request_elements = core.RequestElements(
1519 url, method, params, headers, body)
1520
1521 return request_elements
1522
1523 user_authorization_url = ''
1524 access_token_url = 'https://api.sandbox.paypal.com/v1/oauth2/token'
1525 user_info_url = ''
1526
1527
1528 class Reddit(OAuth2):
1529 """
1530 Reddit |oauth2| provider.
1531
1532 .. note::
1533
1534 Currently credentials refreshment returns
1535 ``{"error": "invalid_request"}``.
1536
1537 * Dashboard: https://ssl.reddit.com/prefs/apps
1538 * Docs: https://github.com/reddit/reddit/wiki/OAuth2
1539 * API reference: http://www.reddit.com/dev/api
1540
1541 .. note::
1542
1543 According to Reddit API
1544 `docs <https://github.com/reddit/reddit/wiki/API#rules>`_,
1545 you have to include a `User-Agent` header in each API call.
1546
1547 You can apply a default ``User-Agent`` header for all API calls in the
1548 config like this:
1549
1550 .. code-block:: python
1551 :emphasize-lines: 6
1552
1553 CONFIG = {
1554 'reddit': {
1555 'class_': oauth2.Reddit,
1556 'consumer_key': '#####',
1557 'consumer_secret': '#####',
1558 'access_headers': {'User-Agent': "Andy Pipkin's App"},
1559 }
1560 }
1561
1562 Supported :class:`.User` properties:
1563
1564 * id
1565 * username
1566
1567 Unsupported :class:`.User` properties:
1568
1569 * birth_date
1570 * country
1571 * city
1572 * email
1573 * first_name
1574 * gender
1575 * last_name
1576 * link
1577 * locale
1578 * location
1579 * name
1580 * nickname
1581 * phone
1582 * picture
1583 * postal_code
1584 * timezone
1585
1586 """
1587
1588 user_authorization_url = 'https://ssl.reddit.com/api/v1/authorize'
1589 access_token_url = 'https://ssl.reddit.com/api/v1/access_token'
1590 user_info_url = 'https://oauth.reddit.com/api/v1/me.json'
1591
1592 user_info_scope = ['identity']
1593
1594 supported_user_attributes = core.SupportedUserAttributes(
1595 id=True,
1596 name=True,
1597 username=True
1598 )
1599
1600 def __init__(self, *args, **kwargs):
1601 super(Reddit, self).__init__(*args, **kwargs)
1602
1603 if self.offline:
1604 if 'duration' not in self.user_authorization_params:
1605 # http://www.reddit.com/r/changelog/comments/11jab9/reddit_change_permanent_oauth_grants_using/
1606 self.user_authorization_params['duration'] = 'permanent'
1607
1608 @classmethod
1609 def _x_credentials_parser(cls, credentials, data):
1610 if data.get('token_type') == 'bearer':
1611 credentials.token_type = cls.BEARER
1612 return credentials
1613
1614 @staticmethod
1615 def _x_user_parser(user, data):
1616 user.username = data.get('name')
1617 return user
1618
1619
1620 class Viadeo(OAuth2):
1621 """
1622 Viadeo |oauth2| provider.
1623
1624 .. note::
1625
1626 As stated in the `Viadeo documentation
1627 <http://dev.viadeo.com/documentation/authentication/request-an-api-key/>`__:
1628
1629 Viadeo restrains access to its API.
1630 They are now exclusively reserved for its strategic partners.
1631
1632 * Dashboard: http://dev.viadeo.com/dashboard/
1633 * Docs:
1634 http://dev.viadeo.com/documentation/authentication/oauth-authentication/
1635 * API reference: http://dev.viadeo.com/documentation/
1636
1637 .. note::
1638
1639 Viadeo doesn't support **credentials refreshment**.
1640 As stated in their
1641 `docs
1642 <http://dev.viadeo.com/documentation/authentication/oauth-authentication/>`_:
1643 "The access token has an infinite time to live."
1644
1645 """
1646
1647 user_authorization_url = 'https://secure.viadeo.com/oauth-provider/' + \
1648 'authorize2'
1649 access_token_url = 'https://secure.viadeo.com/oauth-provider/access_token2'
1650 user_info_url = 'https://api.viadeo.com/me'
1651
1652 @classmethod
1653 def _x_credentials_parser(cls, credentials, data):
1654 if data.get('token_type') == 'bearer_token':
1655 credentials.token_type = cls.BEARER
1656 return credentials
1657
1658 @staticmethod
1659 def _x_refresh_credentials_if(credentials):
1660 # Never refresh.
1661 return False
1662
1663 @staticmethod
1664 def _x_user_parser(user, data):
1665 user.username = data.get('nickname')
1666 user.picture = data.get('picture_large')
1667 user.picture = data.get('picture_large')
1668 user.locale = data.get('language')
1669 user.email = data.get('')
1670 user.email = data.get('')
1671 user.country = data.get('location', {}).get('country')
1672 user.city = data.get('location', {}).get('city')
1673 user.postal_code = data.get('location', {}).get('zipcode')
1674 user.timezone = data.get('location', {}).get('timezone')
1675
1676 return user
1677
1678
1679 class VK(OAuth2):
1680 """
1681 VK.com |oauth2| provider.
1682
1683 * Dashboard: http://vk.com/apps?act=manage
1684 * Docs: http://vk.com/developers.php?oid=-17680044&p=Authorizing_Sites
1685 * API reference: http://vk.com/developers.php?oid=-17680044&p=API_
1686 Method_Description
1687
1688 .. note::
1689
1690 VK uses a
1691 `bitmask scope
1692 <http://vk.com/developers.php?oid=-17680044&p=Application_Rights>`_!
1693 Use it like this:
1694
1695 .. code-block:: python
1696 :emphasize-lines: 7
1697
1698 CONFIG = {
1699 'vk': {
1700 'class_': oauth2.VK,
1701 'consumer_key': '#####',
1702 'consumer_secret': '#####',
1703 'id': authomatic.provider_id(),
1704 'scope': ['1024'] # Always a single item.
1705 }
1706 }
1707
1708 Supported :class:`.User` properties:
1709
1710 * birth_date
1711 * city
1712 * country
1713 * first_name
1714 * gender
1715 * id
1716 * last_name
1717 * location
1718 * name
1719 * picture
1720 * timezone
1721
1722 Unsupported :class:`.User` properties:
1723
1724 * email
1725 * link
1726 * locale
1727 * nickname
1728 * phone
1729 * postal_code
1730 * username
1731
1732 """
1733
1734 user_authorization_url = 'http://api.vkontakte.ru/oauth/authorize'
1735 access_token_url = 'https://api.vkontakte.ru/oauth/access_token'
1736 user_info_url = 'https://api.vk.com/method/getProfiles?' + \
1737 'fields=uid,first_name,last_name,nickname,sex,bdate,' + \
1738 'city,country,timezone,photo_big'
1739
1740 supported_user_attributes = core.SupportedUserAttributes(
1741 birth_date=True,
1742 city=True,
1743 country=True,
1744 first_name=True,
1745 gender=True,
1746 id=True,
1747 last_name=True,
1748 location=True,
1749 name=True,
1750 picture=True,
1751 timezone=True,
1752 )
1753
1754 def __init__(self, *args, **kwargs):
1755 super(VK, self).__init__(*args, **kwargs)
1756
1757 if self.offline:
1758 if 'offline' not in self.scope:
1759 self.scope.append('offline')
1760
1761 @staticmethod
1762 def _x_user_parser(user, data):
1763 _resp = data.get('response', [{}])[0]
1764
1765 _birth_date = _resp.get('bdate')
1766 if _birth_date:
1767 user.birth_date = datetime.datetime.strptime(
1768 _birth_date, '%d.%m.%Y')
1769 user.id = _resp.get('uid')
1770 user.first_name = _resp.get('first_name')
1771 user.gender = _resp.get('sex')
1772 user.last_name = _resp.get('last_name')
1773 user.nickname = _resp.get('nickname')
1774 user.city = _resp.get('city')
1775 user.country = _resp.get('country')
1776 user.timezone = _resp.get('timezone')
1777 user.picture = _resp.get('photo_big')
1778
1779 return user
1780
1781
1782 class WindowsLive(OAuth2):
1783 """
1784 Windows Live |oauth2| provider.
1785
1786 * Dashboard: https://account.live.com/developers/applications
1787 * Docs: http://msdn.microsoft.com/en-us/library/hh243647.aspx
1788 * API explorer: http://isdk.dev.live.com/?mkt=en-us
1789
1790 Supported :class:`.User` properties:
1791
1792 * email
1793 * first_name
1794 * id
1795 * last_name
1796 * link
1797 * locale
1798 * name
1799 * picture
1800
1801 Unsupported :class:`.User` properties:
1802
1803 * birth_date
1804 * city
1805 * country
1806 * gender
1807 * nickname
1808 * location
1809 * phone
1810 * postal_code
1811 * timezone
1812 * username
1813
1814 """
1815
1816 user_authorization_url = 'https://login.live.com/oauth20_authorize.srf'
1817 access_token_url = 'https://login.live.com/oauth20_token.srf'
1818 user_info_url = 'https://apis.live.net/v5.0/me'
1819
1820 user_info_scope = ['wl.basic', 'wl.emails', 'wl.photos']
1821
1822 supported_user_attributes = core.SupportedUserAttributes(
1823 email=True,
1824 first_name=True,
1825 id=True,
1826 last_name=True,
1827 link=True,
1828 locale=True,
1829 name=True,
1830 picture=True
1831 )
1832
1833 def __init__(self, *args, **kwargs):
1834 super(WindowsLive, self).__init__(*args, **kwargs)
1835
1836 if self.offline:
1837 if 'wl.offline_access' not in self.scope:
1838 self.scope.append('wl.offline_access')
1839
1840 @classmethod
1841 def _x_credentials_parser(cls, credentials, data):
1842 if data.get('token_type') == 'bearer':
1843 credentials.token_type = cls.BEARER
1844 return credentials
1845
1846 @staticmethod
1847 def _x_user_parser(user, data):
1848 user.email = data.get('emails', {}).get('preferred')
1849 user.picture = 'https://apis.live.net/v5.0/{0}/picture'.format(
1850 data.get('id'))
1851 return user
1852
1853
1854 class Yammer(OAuth2):
1855 """
1856 Yammer |oauth2| provider.
1857
1858 * Dashboard: https://www.yammer.com/client_applications
1859 * Docs: https://developer.yammer.com/authentication/
1860 * API reference: https://developer.yammer.com/restapi/
1861
1862 Supported :class:`.User` properties:
1863
1864 * birth_date
1865 * city
1866 * country
1867 * email
1868 * first_name
1869 * id
1870 * last_name
1871 * link
1872 * locale
1873 * location
1874 * name
1875 * phone
1876 * picture
1877 * timezone
1878 * username
1879
1880 Unsupported :class:`.User` properties:
1881
1882 * gender
1883 * nickname
1884 * postal_code
1885
1886 """
1887
1888 user_authorization_url = 'https://www.yammer.com/dialog/oauth'
1889 access_token_url = 'https://www.yammer.com/oauth2/access_token.json'
1890 user_info_url = 'https://www.yammer.com/api/v1/users/current.json'
1891
1892 supported_user_attributes = core.SupportedUserAttributes(
1893 birth_date=True,
1894 city=True,
1895 country=True,
1896 email=True,
1897 first_name=True,
1898 id=True,
1899 last_name=True,
1900 link=True,
1901 locale=True,
1902 location=True,
1903 name=True,
1904 phone=True,
1905 picture=True,
1906 timezone=True,
1907 username=True
1908 )
1909
1910 @classmethod
1911 def _x_credentials_parser(cls, credentials, data):
1912 # import pdb; pdb.set_trace()
1913 credentials.token_type = cls.BEARER
1914 _access_token = data.get('access_token', {})
1915 credentials.token = _access_token.get('token')
1916 _expire_in = _access_token.get('expires_at', 0)
1917 if _expire_in:
1918 credentials.expire_in = _expire_in
1919 return credentials
1920
1921 @staticmethod
1922 def _x_user_parser(user, data):
1923
1924 # Yammer provides most of the user info in the access token request,
1925 # but provides more on in user info request.
1926 _user = data.get('user', {})
1927 if not _user:
1928 # If there is "user key", it is token request.
1929 _user = data
1930
1931 user.username = _user.get('name')
1932 user.name = _user.get('full_name')
1933 user.link = _user.get('web_url')
1934 user.picture = _user.get('mugshot_url')
1935
1936 user.city, user.country = _user.get('location', ',').split(',')
1937 user.city = user.city.strip()
1938 user.country = user.country.strip()
1939 user.locale = _user.get('web_preferences', {}).get('locale')
1940
1941 # Contact
1942 _contact = _user.get('contact', {})
1943 user.phone = _contact.get('phone_numbers', [{}])[0].get('number')
1944 _emails = _contact.get('email_addresses', [])
1945 for email in _emails:
1946 if email.get('type', '') == 'primary':
1947 user.email = email.get('address')
1948 break
1949
1950 try:
1951 user.birth_date = datetime.datetime.strptime(
1952 _user.get('birth_date'), "%B %d")
1953 except ValueError:
1954 user.birth_date = _user.get('birth_date')
1955
1956 return user
1957
1958
1959 class Yandex(OAuth2):
1960 """
1961 Yandex |oauth2| provider.
1962
1963 * Dashboard: https://oauth.yandex.com/client/my
1964 * Docs:
1965 http://api.yandex.com/oauth/doc/dg/reference/obtain-access-token.xml
1966 * API reference:
1967
1968 Supported :class:`.User` properties:
1969
1970 * id
1971 * name
1972 * username
1973
1974 Unsupported :class:`.User` properties:
1975
1976 * birth_date
1977 * city
1978 * country
1979 * email
1980 * first_name
1981 * gender
1982 * last_name
1983 * link
1984 * locale
1985 * location
1986 * nickname
1987 * phone
1988 * picture
1989 * postal_code
1990 * timezone
1991
1992 """
1993
1994 user_authorization_url = 'https://oauth.yandex.com/authorize'
1995 access_token_url = 'https://oauth.yandex.com/token'
1996 user_info_url = 'https://login.yandex.ru/info'
1997
1998 supported_user_attributes = core.SupportedUserAttributes(
1999 id=True,
2000 name=True,
2001 username=True
2002 )
2003
2004 @classmethod
2005 def _x_credentials_parser(cls, credentials, data):
2006 if data.get('token_type') == 'bearer':
2007 credentials.token_type = cls.BEARER
2008 return credentials
2009
2010 @staticmethod
2011 def _x_user_parser(user, data):
2012
2013 # http://api.yandex.ru/login/doc/dg/reference/response.xml
2014 user.name = data.get('real_name')
2015 user.nickname = data.get('display_name')
2016 user.gender = data.get('Sex')
2017 user.email = data.get('Default_email')
2018 user.username = data.get('login')
2019
2020 try:
2021 user.birth_date = datetime.datetime.strptime(
2022 data.get('birthday'), "%Y-%m-%d")
2023 except ValueError:
2024 user.birth_date = data.get('birthday')
2025
2026 return user
2027
2028
2029 # The provider type ID is generated from this list's indexes!
2030 # Always append new providers at the end so that ids of existing providers
2031 # don't change!
2032 PROVIDER_ID_MAP = [
2033 Amazon,
2034 Behance,
2035 Bitly,
2036 Bitbucket,
2037 Cosm,
2038 DeviantART,
2039 Eventbrite,
2040 Facebook,
2041 Foursquare,
2042 GitHub,
2043 Google,
2044 LinkedIn,
2045 OAuth2,
2046 PayPal,
2047 Reddit,
2048 Viadeo,
2049 VK,
2050 WindowsLive,
2051 Yammer,
2052 Yandex,
2053 ]
This diff has been collapsed as it changes many lines, (505 lines changed) Show them Hide them
@@ -0,0 +1,505 b''
1 # -*- coding: utf-8 -*-
2 """
3 |openid| Providers
4 ----------------------------------
5
6 Providers which implement the |openid|_ protocol based on the
7 `python-openid`_ library.
8
9 .. warning::
10
11 This providers are dependent on the |pyopenid|_ package.
12
13 .. autosummary::
14
15 OpenID
16 Yahoo
17 Google
18
19 """
20
21 # We need absolute import to import from openid library which has the same
22 # name as this module
23 from __future__ import absolute_import
24 import datetime
25 import logging
26 import time
27
28 from openid import oidutil
29 from openid.consumer import consumer
30 from openid.extensions import ax, pape, sreg
31 from openid.association import Association
32
33 from authomatic import providers
34 from authomatic.exceptions import FailureError, CancellationError, OpenIDError
35
36
37 __all__ = ['OpenID', 'Yahoo', 'Google']
38
39
40 # Suppress openid logging.
41 oidutil.log = lambda message, level=0: None
42
43
44 REALM_HTML = \
45 """
46 <!DOCTYPE html>
47 <html>
48 <head>
49 <meta http-equiv="X-XRDS-Location" content="{xrds_location}" />
50 </head>
51 <body>{body}</body>
52 </html>
53 """
54
55
56 XRDS_XML = \
57 """
58 <?xml version="1.0" encoding="UTF-8"?>
59 <xrds:XRDS
60 xmlns:xrds="xri://$xrds"
61 xmlns:openid="http://openid.net/xmlns/1.0"
62 xmlns="xri://$xrd*($v*2.0)">
63 <XRD>
64 <Service priority="1">
65 <Type>http://specs.openid.net/auth/2.0/return_to</Type>
66 <URI>{return_to}</URI>
67 </Service>
68 </XRD>
69 </xrds:XRDS>
70 """
71
72
73 class SessionOpenIDStore(object):
74 """
75 A very primitive session-based implementation of the.
76
77 :class:`openid.store.interface.OpenIDStore` interface of the
78 `python-openid`_ library.
79
80 .. warning::
81
82 Nonces get verified only by their timeout. Use on your own risk!
83
84 """
85
86 @staticmethod
87 def _log(level, message):
88 return None
89
90 ASSOCIATION_KEY = ('authomatic.providers.openid.SessionOpenIDStore:'
91 'association')
92
93 def __init__(self, session, nonce_timeout=None):
94 """
95 :param int nonce_timeout:
96
97 Nonces older than this in seconds will be considered expired.
98 Default is 600.
99 """
100 self.session = session
101 self.nonce_timeout = nonce_timeout or 600
102
103 def storeAssociation(self, server_url, association):
104 self._log(logging.DEBUG,
105 'SessionOpenIDStore: Storing association to session.')
106
107 serialized = association.serialize()
108 decoded = serialized.decode('latin-1')
109
110 assoc = decoded
111 # assoc = serialized
112
113 # Always store only one association as a tuple.
114 self.session[self.ASSOCIATION_KEY] = (server_url, association.handle,
115 assoc)
116
117 def getAssociation(self, server_url, handle=None):
118 # Try to get association.
119 assoc = self.session.get(self.ASSOCIATION_KEY)
120 if assoc and assoc[0] == server_url:
121 # If found deserialize and return it.
122 self._log(logging.DEBUG, u'SessionOpenIDStore: Association found.')
123 return Association.deserialize(assoc[2].encode('latin-1'))
124 else:
125 self._log(logging.DEBUG,
126 u'SessionOpenIDStore: Association not found.')
127
128 def removeAssociation(self, server_url, handle):
129 # Just inform the caller that it's gone.
130 return True
131
132 def useNonce(self, server_url, timestamp, salt):
133 # Evaluate expired nonces as false.
134 age = int(time.time()) - int(timestamp)
135 if age < self.nonce_timeout:
136 return True
137 else:
138 self._log(logging.ERROR, u'SessionOpenIDStore: Expired nonce!')
139 return False
140
141
142 class OpenID(providers.AuthenticationProvider):
143 """
144 |openid|_ provider based on the `python-openid`_ library.
145 """
146
147 AX = ['http://axschema.org/contact/email',
148 'http://schema.openid.net/contact/email',
149 'http://axschema.org/namePerson',
150 'http://openid.net/schema/namePerson/first',
151 'http://openid.net/schema/namePerson/last',
152 'http://openid.net/schema/gender',
153 'http://openid.net/schema/language/pref',
154 'http://openid.net/schema/contact/web/default',
155 'http://openid.net/schema/media/image',
156 'http://openid.net/schema/timezone']
157
158 AX_REQUIRED = ['http://schema.openid.net/contact/email']
159
160 SREG = ['nickname',
161 'email',
162 'fullname',
163 'dob',
164 'gender',
165 'postcode',
166 'country',
167 'language',
168 'timezone']
169
170 PAPE = [
171 'http://schemas.openid.net/pape/policies/2007/06/'
172 'multi-factor-physical',
173 'http://schemas.openid.net/pape/policies/2007/06/multi-factor',
174 'http://schemas.openid.net/pape/policies/2007/06/phishing-resistant'
175 ]
176
177 def __init__(self, *args, **kwargs):
178 """
179 Accepts additional keyword arguments:
180
181 :param store:
182 Any object which implements
183 :class:`openid.store.interface.OpenIDStore`
184 of the `python-openid`_ library.
185
186 :param bool use_realm:
187 Whether to use `OpenID realm
188 <http://openid.net/specs/openid-authentication-2_0-12.html#realms>`_
189 If ``True`` the realm HTML document will be accessible at
190 ``{current url}?{realm_param}={realm_param}``
191 e.g. ``http://example.com/path?realm=realm``.
192
193 :param str realm_body:
194 Contents of the HTML body tag of the realm.
195
196 :param str realm_param:
197 Name of the query parameter to be used to serve the realm.
198
199 :param str xrds_param:
200 The name of the query parameter to be used to serve the
201 `XRDS document
202 <http://openid.net/specs/openid-authentication-2_0-12.html#XRDS_Sample>`_.
203
204 :param list sreg:
205 List of strings of optional
206 `SREG
207 <http://openid.net/specs/openid-simple-registration-extension-1_0.html>`_
208 fields.
209 Default = :attr:`OpenID.SREG`.
210
211 :param list sreg_required:
212 List of strings of required
213 `SREG
214 <http://openid.net/specs/openid-simple-registration-extension-1_0.html>`_
215 fields.
216 Default = ``[]``.
217
218 :param list ax:
219 List of strings of optional
220 `AX
221 <http://openid.net/specs/openid-attribute-exchange-1_0.html>`_
222 schemas.
223 Default = :attr:`OpenID.AX`.
224
225 :param list ax_required:
226 List of strings of required
227 `AX
228 <http://openid.net/specs/openid-attribute-exchange-1_0.html>`_
229 schemas.
230 Default = :attr:`OpenID.AX_REQUIRED`.
231
232 :param list pape:
233 of requested
234 `PAPE
235 <http://openid.net/specs/openid-provider-authentication-policy-extension-1_0.html>`_
236 policies.
237 Default = :attr:`OpenID.PAPE`.
238
239 As well as those inherited from :class:`.AuthenticationProvider`
240 constructor.
241
242 """
243
244 super(OpenID, self).__init__(*args, **kwargs)
245
246 # Allow for other openid store implementations.
247 self.store = self._kwarg(
248 kwargs, 'store', SessionOpenIDStore(
249 self.session))
250
251 # Realm
252 self.use_realm = self._kwarg(kwargs, 'use_realm', True)
253 self.realm_body = self._kwarg(kwargs, 'realm_body', '')
254 self.realm_param = self._kwarg(kwargs, 'realm_param', 'realm')
255 self.xrds_param = self._kwarg(kwargs, 'xrds_param', 'xrds')
256
257 # SREG
258 self.sreg = self._kwarg(kwargs, 'sreg', self.SREG)
259 self.sreg_required = self._kwarg(kwargs, 'sreg_required', [])
260
261 # AX
262 self.ax = self._kwarg(kwargs, 'ax', self.AX)
263 self.ax_required = self._kwarg(kwargs, 'ax_required', self.AX_REQUIRED)
264 # add required schemas to schemas if not already there
265 for i in self.ax_required:
266 if i not in self.ax:
267 self.ax.append(i)
268
269 # PAPE
270 self.pape = self._kwarg(kwargs, 'pape', self.PAPE)
271
272 @staticmethod
273 def _x_user_parser(user, data):
274
275 user.first_name = data.get('ax', {}).get(
276 'http://openid.net/schema/namePerson/first')
277 user.last_name = data.get('ax', {}).get(
278 'http://openid.net/schema/namePerson/last')
279 user.id = data.get('guid')
280 user.link = data.get('ax', {}).get(
281 'http://openid.net/schema/contact/web/default')
282 user.picture = data.get('ax', {}).get(
283 'http://openid.net/schema/media/image')
284 user.nickname = data.get('sreg', {}).get('nickname')
285 user.country = data.get('sreg', {}).get('country')
286 user.postal_code = data.get('sreg', {}).get('postcode')
287
288 user.name = data.get('sreg', {}).get('fullname') or \
289 data.get('ax', {}).get('http://axschema.org/namePerson')
290
291 user.gender = data.get('sreg', {}).get('gender') or \
292 data.get('ax', {}).get('http://openid.net/schema/gender')
293
294 user.locale = data.get('sreg', {}).get('language') or \
295 data.get('ax', {}).get('http://openid.net/schema/language/pref')
296
297 user.timezone = data.get('sreg', {}).get('timezone') or \
298 data.get('ax', {}).get('http://openid.net/schema/timezone')
299
300 user.email = data.get('sreg', {}).get('email') or \
301 data.get('ax', {}).get('http://axschema.org/contact/email') or \
302 data.get('ax', {}).get('http://schema.openid.net/contact/email')
303
304 if data.get('sreg', {}).get('dob'):
305 user.birth_date = datetime.datetime.strptime(
306 data.get('sreg', {}).get('dob'),
307 '%Y-%m-%d'
308 )
309 else:
310 user.birth_date = None
311
312 return user
313
314 @providers.login_decorator
315 def login(self):
316 # Instantiate consumer
317 self.store._log = self._log
318 oi_consumer = consumer.Consumer(self.session, self.store)
319
320 # handle realm and XRDS if there is only one query parameter
321 if self.use_realm and len(self.params) == 1:
322 realm_request = self.params.get(self.realm_param)
323 xrds_request = self.params.get(self.xrds_param)
324 else:
325 realm_request = None
326 xrds_request = None
327
328 # determine type of request
329 if realm_request:
330 # =================================================================
331 # Realm HTML
332 # =================================================================
333
334 self._log(
335 logging.INFO,
336 u'Writing OpenID realm HTML to the response.')
337 xrds_location = '{u}?{x}={x}'.format(u=self.url, x=self.xrds_param)
338 self.write(
339 REALM_HTML.format(
340 xrds_location=xrds_location,
341 body=self.realm_body))
342
343 elif xrds_request:
344 # =================================================================
345 # XRDS XML
346 # =================================================================
347
348 self._log(
349 logging.INFO,
350 u'Writing XRDS XML document to the response.')
351 self.set_header('Content-Type', 'application/xrds+xml')
352 self.write(XRDS_XML.format(return_to=self.url))
353
354 elif self.params.get('openid.mode'):
355 # =================================================================
356 # Phase 2 after redirect
357 # =================================================================
358
359 self._log(
360 logging.INFO,
361 u'Continuing OpenID authentication procedure after redirect.')
362
363 # complete the authentication process
364 response = oi_consumer.complete(self.params, self.url)
365
366 # on success
367 if response.status == consumer.SUCCESS:
368
369 data = {}
370
371 # get user ID
372 data['guid'] = response.getDisplayIdentifier()
373
374 self._log(logging.INFO, u'Authentication successful.')
375
376 # get user data from AX response
377 ax_response = ax.FetchResponse.fromSuccessResponse(response)
378 if ax_response and ax_response.data:
379 self._log(logging.INFO, u'Got AX data.')
380 ax_data = {}
381 # convert iterable values to their first item
382 for k, v in ax_response.data.items():
383 if v and isinstance(v, (list, tuple)):
384 ax_data[k] = v[0]
385 data['ax'] = ax_data
386
387 # get user data from SREG response
388 sreg_response = sreg.SRegResponse.fromSuccessResponse(response)
389 if sreg_response and sreg_response.data:
390 self._log(logging.INFO, u'Got SREG data.')
391 data['sreg'] = sreg_response.data
392
393 # get data from PAPE response
394 pape_response = pape.Response.fromSuccessResponse(response)
395 if pape_response and pape_response.auth_policies:
396 self._log(logging.INFO, u'Got PAPE data.')
397 data['pape'] = pape_response.auth_policies
398
399 # create user
400 self._update_or_create_user(data)
401
402 # =============================================================
403 # We're done!
404 # =============================================================
405
406 elif response.status == consumer.CANCEL:
407 raise CancellationError(
408 u'User cancelled the verification of ID "{0}"!'.format(
409 response.getDisplayIdentifier()))
410
411 elif response.status == consumer.FAILURE:
412 raise FailureError(response.message)
413
414 elif self.identifier: # As set in AuthenticationProvider.__init__
415 # =================================================================
416 # Phase 1 before redirect
417 # =================================================================
418
419 self._log(
420 logging.INFO,
421 u'Starting OpenID authentication procedure.')
422
423 # get AuthRequest object
424 try:
425 auth_request = oi_consumer.begin(self.identifier)
426 except consumer.DiscoveryFailure as e:
427 raise FailureError(
428 u'Discovery failed for identifier {0}!'.format(
429 self.identifier
430 ),
431 url=self.identifier,
432 original_message=e.message)
433
434 self._log(
435 logging.INFO,
436 u'Service discovery for identifier {0} successful.'.format(
437 self.identifier))
438
439 # add SREG extension
440 # we need to remove required fields from optional fields because
441 # addExtension then raises an error
442 self.sreg = [i for i in self.sreg if i not in self.sreg_required]
443 auth_request.addExtension(
444 sreg.SRegRequest(
445 optional=self.sreg,
446 required=self.sreg_required)
447 )
448
449 # add AX extension
450 ax_request = ax.FetchRequest()
451 # set AX schemas
452 for i in self.ax:
453 required = i in self.ax_required
454 ax_request.add(ax.AttrInfo(i, required=required))
455 auth_request.addExtension(ax_request)
456
457 # add PAPE extension
458 auth_request.addExtension(pape.Request(self.pape))
459
460 # prepare realm and return_to URLs
461 if self.use_realm:
462 realm = return_to = '{u}?{r}={r}'.format(
463 u=self.url, r=self.realm_param)
464 else:
465 realm = return_to = self.url
466
467 url = auth_request.redirectURL(realm, return_to)
468
469 if auth_request.shouldSendRedirect():
470 # can be redirected
471 url = auth_request.redirectURL(realm, return_to)
472 self._log(
473 logging.INFO,
474 u'Redirecting user to {0}.'.format(url))
475 self.redirect(url)
476 else:
477 # must be sent as POST
478 # this writes a html post form with auto-submit
479 self._log(
480 logging.INFO,
481 u'Writing an auto-submit HTML form to the response.')
482 form = auth_request.htmlMarkup(
483 realm, return_to, False, dict(
484 id='openid_form'))
485 self.write(form)
486 else:
487 raise OpenIDError('No identifier specified!')
488
489
490 class Yahoo(OpenID):
491 """
492 Yahoo :class:`.OpenID` provider with the :attr:`.identifier` predefined to
493 ``"me.yahoo.com"``.
494 """
495
496 identifier = 'me.yahoo.com'
497
498
499 class Google(OpenID):
500 """
501 Google :class:`.OpenID` provider with the :attr:`.identifier` predefined to
502 ``"https://www.google.com/accounts/o8/id"``.
503 """
504
505 identifier = 'https://www.google.com/accounts/o8/id'
@@ -0,0 +1,6 b''
1 # -*- coding: utf-8 -*-
2 from authomatic import providers
3
4
5 class MozillaPersona(providers.AuthenticationProvider):
6 pass
This diff has been collapsed as it changes many lines, (839 lines changed) Show them Hide them
@@ -0,0 +1,839 b''
1 # -*- coding: utf-8 -*-
2 """Utilities for writing code that runs on Python 2 and 3"""
3
4 # Copyright (c) 2010-2015 Benjamin Peterson
5 #
6 # Permission is hereby granted, free of charge, to any person obtaining a copy
7 # of this software and associated documentation files (the "Software"), to deal
8 # in the Software without restriction, including without limitation the rights
9 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 # copies of the Software, and to permit persons to whom the Software is
11 # furnished to do so, subject to the following conditions:
12 #
13 # The above copyright notice and this permission notice shall be included in all
14 # copies or substantial portions of the Software.
15 #
16 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 # SOFTWARE.
23
24 from __future__ import absolute_import
25
26 import functools
27 import itertools
28 import operator
29 import sys
30 import types
31
32 __author__ = "Benjamin Peterson <benjamin@python.org>"
33 __version__ = "1.9.0"
34
35
36 # Useful for very coarse version differentiation.
37 PY2 = sys.version_info[0] == 2
38 PY3 = sys.version_info[0] == 3
39
40 if PY3:
41 string_types = str,
42 integer_types = int,
43 class_types = type,
44 text_type = str
45 binary_type = bytes
46
47 MAXSIZE = sys.maxsize
48 else:
49 string_types = basestring,
50 integer_types = (int, long)
51 class_types = (type, types.ClassType)
52 text_type = unicode
53 binary_type = str
54
55 if sys.platform.startswith("java"):
56 # Jython always uses 32 bits.
57 MAXSIZE = int((1 << 31) - 1)
58 else:
59 # It's possible to have sizeof(long) != sizeof(Py_ssize_t).
60 class X(object):
61 def __len__(self):
62 return 1 << 31
63 try:
64 len(X())
65 except OverflowError:
66 # 32-bit
67 MAXSIZE = int((1 << 31) - 1)
68 else:
69 # 64-bit
70 MAXSIZE = int((1 << 63) - 1)
71 del X
72
73
74 def _add_doc(func, doc):
75 """Add documentation to a function."""
76 func.__doc__ = doc
77
78
79 def _import_module(name):
80 """Import module, returning the module after the last dot."""
81 __import__(name)
82 return sys.modules[name]
83
84
85 class _LazyDescr(object):
86
87 def __init__(self, name):
88 self.name = name
89
90 def __get__(self, obj, tp):
91 result = self._resolve()
92 setattr(obj, self.name, result) # Invokes __set__.
93 try:
94 # This is a bit ugly, but it avoids running this again by
95 # removing this descriptor.
96 delattr(obj.__class__, self.name)
97 except AttributeError:
98 pass
99 return result
100
101
102 class MovedModule(_LazyDescr):
103
104 def __init__(self, name, old, new=None):
105 super(MovedModule, self).__init__(name)
106 if PY3:
107 if new is None:
108 new = name
109 self.mod = new
110 else:
111 self.mod = old
112
113 def _resolve(self):
114 return _import_module(self.mod)
115
116 def __getattr__(self, attr):
117 _module = self._resolve()
118 value = getattr(_module, attr)
119 setattr(self, attr, value)
120 return value
121
122
123 class _LazyModule(types.ModuleType):
124
125 def __init__(self, name):
126 super(_LazyModule, self).__init__(name)
127 self.__doc__ = self.__class__.__doc__
128
129 def __dir__(self):
130 attrs = ["__doc__", "__name__"]
131 attrs += [attr.name for attr in self._moved_attributes]
132 return attrs
133
134 # Subclasses should override this
135 _moved_attributes = []
136
137
138 class MovedAttribute(_LazyDescr):
139
140 def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None):
141 super(MovedAttribute, self).__init__(name)
142 if PY3:
143 if new_mod is None:
144 new_mod = name
145 self.mod = new_mod
146 if new_attr is None:
147 if old_attr is None:
148 new_attr = name
149 else:
150 new_attr = old_attr
151 self.attr = new_attr
152 else:
153 self.mod = old_mod
154 if old_attr is None:
155 old_attr = name
156 self.attr = old_attr
157
158 def _resolve(self):
159 module = _import_module(self.mod)
160 return getattr(module, self.attr)
161
162
163 class _SixMetaPathImporter(object):
164 """
165 A meta path importer to import six.moves and its submodules.
166
167 This class implements a PEP302 finder and loader. It should be compatible
168 with Python 2.5 and all existing versions of Python3
169 """
170 def __init__(self, six_module_name):
171 self.name = six_module_name
172 self.known_modules = {}
173
174 def _add_module(self, mod, *fullnames):
175 for fullname in fullnames:
176 self.known_modules[self.name + "." + fullname] = mod
177
178 def _get_module(self, fullname):
179 return self.known_modules[self.name + "." + fullname]
180
181 def find_module(self, fullname, path=None):
182 if fullname in self.known_modules:
183 return self
184 return None
185
186 def __get_module(self, fullname):
187 try:
188 return self.known_modules[fullname]
189 except KeyError:
190 raise ImportError("This loader does not know module " + fullname)
191
192 def load_module(self, fullname):
193 try:
194 # in case of a reload
195 return sys.modules[fullname]
196 except KeyError:
197 pass
198 mod = self.__get_module(fullname)
199 if isinstance(mod, MovedModule):
200 mod = mod._resolve()
201 else:
202 mod.__loader__ = self
203 sys.modules[fullname] = mod
204 return mod
205
206 def is_package(self, fullname):
207 """
208 Return true, if the named module is a package.
209
210 We need this method to get correct spec objects with
211 Python 3.4 (see PEP451)
212 """
213 return hasattr(self.__get_module(fullname), "__path__")
214
215 def get_code(self, fullname):
216 """Return None
217
218 Required, if is_package is implemented"""
219 self.__get_module(fullname) # eventually raises ImportError
220 return None
221 get_source = get_code # same as get_code
222
223 _importer = _SixMetaPathImporter(__name__)
224
225
226 class _MovedItems(_LazyModule):
227 """Lazy loading of moved objects"""
228 __path__ = [] # mark as package
229
230
231 _moved_attributes = [
232 MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"),
233 MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"),
234 MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"),
235 MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"),
236 MovedAttribute("intern", "__builtin__", "sys"),
237 MovedAttribute("map", "itertools", "builtins", "imap", "map"),
238 MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"),
239 MovedAttribute("reload_module", "__builtin__", "imp", "reload"),
240 MovedAttribute("reduce", "__builtin__", "functools"),
241 MovedAttribute("shlex_quote", "pipes", "shlex", "quote"),
242 MovedAttribute("StringIO", "StringIO", "io"),
243 MovedAttribute("UserDict", "UserDict", "collections"),
244 MovedAttribute("UserList", "UserList", "collections"),
245 MovedAttribute("UserString", "UserString", "collections"),
246 MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"),
247 MovedAttribute("zip", "itertools", "builtins", "izip", "zip"),
248 MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"),
249
250 MovedModule("builtins", "__builtin__"),
251 MovedModule("configparser", "ConfigParser"),
252 MovedModule("copyreg", "copy_reg"),
253 MovedModule("dbm_gnu", "gdbm", "dbm.gnu"),
254 MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread"),
255 MovedModule("http_cookiejar", "cookielib", "http.cookiejar"),
256 MovedModule("http_cookies", "Cookie", "http.cookies"),
257 MovedModule("html_entities", "htmlentitydefs", "html.entities"),
258 MovedModule("html_parser", "HTMLParser", "html.parser"),
259 MovedModule("http_client", "httplib", "http.client"),
260 MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"),
261 MovedModule("email_mime_nonmultipart", "email.MIMENonMultipart", "email.mime.nonmultipart"),
262 MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"),
263 MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"),
264 MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"),
265 MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"),
266 MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"),
267 MovedModule("cPickle", "cPickle", "pickle"),
268 MovedModule("queue", "Queue"),
269 MovedModule("reprlib", "repr"),
270 MovedModule("socketserver", "SocketServer"),
271 MovedModule("_thread", "thread", "_thread"),
272 MovedModule("tkinter", "Tkinter"),
273 MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"),
274 MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"),
275 MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"),
276 MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"),
277 MovedModule("tkinter_tix", "Tix", "tkinter.tix"),
278 MovedModule("tkinter_ttk", "ttk", "tkinter.ttk"),
279 MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"),
280 MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"),
281 MovedModule("tkinter_colorchooser", "tkColorChooser",
282 "tkinter.colorchooser"),
283 MovedModule("tkinter_commondialog", "tkCommonDialog",
284 "tkinter.commondialog"),
285 MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"),
286 MovedModule("tkinter_font", "tkFont", "tkinter.font"),
287 MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"),
288 MovedModule("tkinter_tksimpledialog", "tkSimpleDialog",
289 "tkinter.simpledialog"),
290 MovedModule("urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"),
291 MovedModule("urllib_error", __name__ + ".moves.urllib_error", "urllib.error"),
292 MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"),
293 MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"),
294 MovedModule("xmlrpc_client", "xmlrpclib", "xmlrpc.client"),
295 MovedModule("xmlrpc_server", "SimpleXMLRPCServer", "xmlrpc.server"),
296 MovedModule("winreg", "_winreg"),
297 ]
298 for attr in _moved_attributes:
299 setattr(_MovedItems, attr.name, attr)
300 if isinstance(attr, MovedModule):
301 _importer._add_module(attr, "moves." + attr.name)
302 del attr
303
304 _MovedItems._moved_attributes = _moved_attributes
305
306 moves = _MovedItems(__name__ + ".moves")
307 _importer._add_module(moves, "moves")
308
309
310 class Module_six_moves_urllib_parse(_LazyModule):
311 """Lazy loading of moved objects in six.moves.urllib_parse"""
312
313
314 _urllib_parse_moved_attributes = [
315 MovedAttribute("ParseResult", "urlparse", "urllib.parse"),
316 MovedAttribute("SplitResult", "urlparse", "urllib.parse"),
317 MovedAttribute("parse_qs", "urlparse", "urllib.parse"),
318 MovedAttribute("parse_qsl", "urlparse", "urllib.parse"),
319 MovedAttribute("urldefrag", "urlparse", "urllib.parse"),
320 MovedAttribute("urljoin", "urlparse", "urllib.parse"),
321 MovedAttribute("urlparse", "urlparse", "urllib.parse"),
322 MovedAttribute("urlsplit", "urlparse", "urllib.parse"),
323 MovedAttribute("urlunparse", "urlparse", "urllib.parse"),
324 MovedAttribute("urlunsplit", "urlparse", "urllib.parse"),
325 MovedAttribute("quote", "urllib", "urllib.parse"),
326 MovedAttribute("quote_plus", "urllib", "urllib.parse"),
327 MovedAttribute("unquote", "urllib", "urllib.parse"),
328 MovedAttribute("unquote_plus", "urllib", "urllib.parse"),
329 MovedAttribute("urlencode", "urllib", "urllib.parse"),
330 MovedAttribute("splitquery", "urllib", "urllib.parse"),
331 MovedAttribute("splittag", "urllib", "urllib.parse"),
332 MovedAttribute("splituser", "urllib", "urllib.parse"),
333 MovedAttribute("uses_fragment", "urlparse", "urllib.parse"),
334 MovedAttribute("uses_netloc", "urlparse", "urllib.parse"),
335 MovedAttribute("uses_params", "urlparse", "urllib.parse"),
336 MovedAttribute("uses_query", "urlparse", "urllib.parse"),
337 MovedAttribute("uses_relative", "urlparse", "urllib.parse"),
338 ]
339 for attr in _urllib_parse_moved_attributes:
340 setattr(Module_six_moves_urllib_parse, attr.name, attr)
341 del attr
342
343 Module_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes
344
345 _importer._add_module(Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse"),
346 "moves.urllib_parse", "moves.urllib.parse")
347
348
349 class Module_six_moves_urllib_error(_LazyModule):
350 """Lazy loading of moved objects in six.moves.urllib_error"""
351
352
353 _urllib_error_moved_attributes = [
354 MovedAttribute("URLError", "urllib2", "urllib.error"),
355 MovedAttribute("HTTPError", "urllib2", "urllib.error"),
356 MovedAttribute("ContentTooShortError", "urllib", "urllib.error"),
357 ]
358 for attr in _urllib_error_moved_attributes:
359 setattr(Module_six_moves_urllib_error, attr.name, attr)
360 del attr
361
362 Module_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes
363
364 _importer._add_module(Module_six_moves_urllib_error(__name__ + ".moves.urllib.error"),
365 "moves.urllib_error", "moves.urllib.error")
366
367
368 class Module_six_moves_urllib_request(_LazyModule):
369 """Lazy loading of moved objects in six.moves.urllib_request"""
370
371
372 _urllib_request_moved_attributes = [
373 MovedAttribute("urlopen", "urllib2", "urllib.request"),
374 MovedAttribute("install_opener", "urllib2", "urllib.request"),
375 MovedAttribute("build_opener", "urllib2", "urllib.request"),
376 MovedAttribute("pathname2url", "urllib", "urllib.request"),
377 MovedAttribute("url2pathname", "urllib", "urllib.request"),
378 MovedAttribute("getproxies", "urllib", "urllib.request"),
379 MovedAttribute("Request", "urllib2", "urllib.request"),
380 MovedAttribute("OpenerDirector", "urllib2", "urllib.request"),
381 MovedAttribute("HTTPDefaultErrorHandler", "urllib2", "urllib.request"),
382 MovedAttribute("HTTPRedirectHandler", "urllib2", "urllib.request"),
383 MovedAttribute("HTTPCookieProcessor", "urllib2", "urllib.request"),
384 MovedAttribute("ProxyHandler", "urllib2", "urllib.request"),
385 MovedAttribute("BaseHandler", "urllib2", "urllib.request"),
386 MovedAttribute("HTTPPasswordMgr", "urllib2", "urllib.request"),
387 MovedAttribute("HTTPPasswordMgrWithDefaultRealm", "urllib2", "urllib.request"),
388 MovedAttribute("AbstractBasicAuthHandler", "urllib2", "urllib.request"),
389 MovedAttribute("HTTPBasicAuthHandler", "urllib2", "urllib.request"),
390 MovedAttribute("ProxyBasicAuthHandler", "urllib2", "urllib.request"),
391 MovedAttribute("AbstractDigestAuthHandler", "urllib2", "urllib.request"),
392 MovedAttribute("HTTPDigestAuthHandler", "urllib2", "urllib.request"),
393 MovedAttribute("ProxyDigestAuthHandler", "urllib2", "urllib.request"),
394 MovedAttribute("HTTPHandler", "urllib2", "urllib.request"),
395 MovedAttribute("HTTPSHandler", "urllib2", "urllib.request"),
396 MovedAttribute("FileHandler", "urllib2", "urllib.request"),
397 MovedAttribute("FTPHandler", "urllib2", "urllib.request"),
398 MovedAttribute("CacheFTPHandler", "urllib2", "urllib.request"),
399 MovedAttribute("UnknownHandler", "urllib2", "urllib.request"),
400 MovedAttribute("HTTPErrorProcessor", "urllib2", "urllib.request"),
401 MovedAttribute("urlretrieve", "urllib", "urllib.request"),
402 MovedAttribute("urlcleanup", "urllib", "urllib.request"),
403 MovedAttribute("URLopener", "urllib", "urllib.request"),
404 MovedAttribute("FancyURLopener", "urllib", "urllib.request"),
405 MovedAttribute("proxy_bypass", "urllib", "urllib.request"),
406 ]
407 for attr in _urllib_request_moved_attributes:
408 setattr(Module_six_moves_urllib_request, attr.name, attr)
409 del attr
410
411 Module_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes
412
413 _importer._add_module(Module_six_moves_urllib_request(__name__ + ".moves.urllib.request"),
414 "moves.urllib_request", "moves.urllib.request")
415
416
417 class Module_six_moves_urllib_response(_LazyModule):
418 """Lazy loading of moved objects in six.moves.urllib_response"""
419
420
421 _urllib_response_moved_attributes = [
422 MovedAttribute("addbase", "urllib", "urllib.response"),
423 MovedAttribute("addclosehook", "urllib", "urllib.response"),
424 MovedAttribute("addinfo", "urllib", "urllib.response"),
425 MovedAttribute("addinfourl", "urllib", "urllib.response"),
426 ]
427 for attr in _urllib_response_moved_attributes:
428 setattr(Module_six_moves_urllib_response, attr.name, attr)
429 del attr
430
431 Module_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes
432
433 _importer._add_module(Module_six_moves_urllib_response(__name__ + ".moves.urllib.response"),
434 "moves.urllib_response", "moves.urllib.response")
435
436
437 class Module_six_moves_urllib_robotparser(_LazyModule):
438 """Lazy loading of moved objects in six.moves.urllib_robotparser"""
439
440
441 _urllib_robotparser_moved_attributes = [
442 MovedAttribute("RobotFileParser", "robotparser", "urllib.robotparser"),
443 ]
444 for attr in _urllib_robotparser_moved_attributes:
445 setattr(Module_six_moves_urllib_robotparser, attr.name, attr)
446 del attr
447
448 Module_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes
449
450 _importer._add_module(Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser"),
451 "moves.urllib_robotparser", "moves.urllib.robotparser")
452
453
454 class Module_six_moves_urllib(types.ModuleType):
455 """Create a six.moves.urllib namespace that resembles the Python 3 namespace"""
456 __path__ = [] # mark as package
457 parse = _importer._get_module("moves.urllib_parse")
458 error = _importer._get_module("moves.urllib_error")
459 request = _importer._get_module("moves.urllib_request")
460 response = _importer._get_module("moves.urllib_response")
461 robotparser = _importer._get_module("moves.urllib_robotparser")
462
463 def __dir__(self):
464 return ['parse', 'error', 'request', 'response', 'robotparser']
465
466 _importer._add_module(Module_six_moves_urllib(__name__ + ".moves.urllib"),
467 "moves.urllib")
468
469
470 def add_move(move):
471 """Add an item to six.moves."""
472 setattr(_MovedItems, move.name, move)
473
474
475 def remove_move(name):
476 """Remove item from six.moves."""
477 try:
478 delattr(_MovedItems, name)
479 except AttributeError:
480 try:
481 del moves.__dict__[name]
482 except KeyError:
483 raise AttributeError("no such move, %r" % (name,))
484
485
486 if PY3:
487 _meth_func = "__func__"
488 _meth_self = "__self__"
489
490 _func_closure = "__closure__"
491 _func_code = "__code__"
492 _func_defaults = "__defaults__"
493 _func_globals = "__globals__"
494 else:
495 _meth_func = "im_func"
496 _meth_self = "im_self"
497
498 _func_closure = "func_closure"
499 _func_code = "func_code"
500 _func_defaults = "func_defaults"
501 _func_globals = "func_globals"
502
503
504 try:
505 advance_iterator = next
506 except NameError:
507 def advance_iterator(it):
508 return it.next()
509 next = advance_iterator
510
511
512 try:
513 callable = callable
514 except NameError:
515 def callable(obj):
516 return any("__call__" in klass.__dict__ for klass in type(obj).__mro__)
517
518
519 if PY3:
520 def get_unbound_function(unbound):
521 return unbound
522
523 create_bound_method = types.MethodType
524
525 Iterator = object
526 else:
527 def get_unbound_function(unbound):
528 return unbound.im_func
529
530 def create_bound_method(func, obj):
531 return types.MethodType(func, obj, obj.__class__)
532
533 class Iterator(object):
534
535 def next(self):
536 return type(self).__next__(self)
537
538 callable = callable
539 _add_doc(get_unbound_function,
540 """Get the function out of a possibly unbound function""")
541
542
543 get_method_function = operator.attrgetter(_meth_func)
544 get_method_self = operator.attrgetter(_meth_self)
545 get_function_closure = operator.attrgetter(_func_closure)
546 get_function_code = operator.attrgetter(_func_code)
547 get_function_defaults = operator.attrgetter(_func_defaults)
548 get_function_globals = operator.attrgetter(_func_globals)
549
550
551 if PY3:
552 def iterkeys(d, **kw):
553 return iter(d.keys(**kw))
554
555 def itervalues(d, **kw):
556 return iter(d.values(**kw))
557
558 def iteritems(d, **kw):
559 return iter(d.items(**kw))
560
561 def iterlists(d, **kw):
562 return iter(d.lists(**kw))
563
564 viewkeys = operator.methodcaller("keys")
565
566 viewvalues = operator.methodcaller("values")
567
568 viewitems = operator.methodcaller("items")
569 else:
570 def iterkeys(d, **kw):
571 return iter(d.iterkeys(**kw))
572
573 def itervalues(d, **kw):
574 return iter(d.itervalues(**kw))
575
576 def iteritems(d, **kw):
577 return iter(d.iteritems(**kw))
578
579 def iterlists(d, **kw):
580 return iter(d.iterlists(**kw))
581
582 viewkeys = operator.methodcaller("viewkeys")
583
584 viewvalues = operator.methodcaller("viewvalues")
585
586 viewitems = operator.methodcaller("viewitems")
587
588 _add_doc(iterkeys, "Return an iterator over the keys of a dictionary.")
589 _add_doc(itervalues, "Return an iterator over the values of a dictionary.")
590 _add_doc(iteritems,
591 "Return an iterator over the (key, value) pairs of a dictionary.")
592 _add_doc(iterlists,
593 "Return an iterator over the (key, [values]) pairs of a dictionary.")
594
595
596 if PY3:
597 def b(s):
598 return s.encode("latin-1")
599 def u(s):
600 return s
601 unichr = chr
602 if sys.version_info[1] <= 1:
603 def int2byte(i):
604 return bytes((i,))
605 else:
606 # This is about 2x faster than the implementation above on 3.2+
607 int2byte = operator.methodcaller("to_bytes", 1, "big")
608 byte2int = operator.itemgetter(0)
609 indexbytes = operator.getitem
610 iterbytes = iter
611 import io
612 StringIO = io.StringIO
613 BytesIO = io.BytesIO
614 _assertCountEqual = "assertCountEqual"
615 _assertRaisesRegex = "assertRaisesRegex"
616 _assertRegex = "assertRegex"
617 else:
618 def b(s):
619 return s
620 # Workaround for standalone backslash
621 def u(s):
622 return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape")
623 unichr = unichr
624 int2byte = chr
625 def byte2int(bs):
626 return ord(bs[0])
627 def indexbytes(buf, i):
628 return ord(buf[i])
629 iterbytes = functools.partial(itertools.imap, ord)
630 import StringIO
631 StringIO = BytesIO = StringIO.StringIO
632 _assertCountEqual = "assertItemsEqual"
633 _assertRaisesRegex = "assertRaisesRegexp"
634 _assertRegex = "assertRegexpMatches"
635 _add_doc(b, """Byte literal""")
636 _add_doc(u, """Text literal""")
637
638
639 def assertCountEqual(self, *args, **kwargs):
640 return getattr(self, _assertCountEqual)(*args, **kwargs)
641
642
643 def assertRaisesRegex(self, *args, **kwargs):
644 return getattr(self, _assertRaisesRegex)(*args, **kwargs)
645
646
647 def assertRegex(self, *args, **kwargs):
648 return getattr(self, _assertRegex)(*args, **kwargs)
649
650
651 if PY3:
652 exec_ = getattr(moves.builtins, "exec")
653
654
655 def reraise(tp, value, tb=None):
656 if value is None:
657 value = tp()
658 if value.__traceback__ is not tb:
659 raise value.with_traceback(tb)
660 raise value
661
662 else:
663 def exec_(_code_, _globs_=None, _locs_=None):
664 """Execute code in a namespace."""
665 if _globs_ is None:
666 frame = sys._getframe(1)
667 _globs_ = frame.f_globals
668 if _locs_ is None:
669 _locs_ = frame.f_locals
670 del frame
671 elif _locs_ is None:
672 _locs_ = _globs_
673 exec("""exec _code_ in _globs_, _locs_""")
674
675
676 exec_("""def reraise(tp, value, tb=None):
677 raise tp, value, tb
678 """)
679
680
681 if sys.version_info[:2] == (3, 2):
682 exec_("""def raise_from(value, from_value):
683 if from_value is None:
684 raise value
685 raise value from from_value
686 """)
687 elif sys.version_info[:2] > (3, 2):
688 exec_("""def raise_from(value, from_value):
689 raise value from from_value
690 """)
691 else:
692 def raise_from(value, from_value):
693 raise value
694
695
696 print_ = getattr(moves.builtins, "print", None)
697 if print_ is None:
698 def print_(*args, **kwargs):
699 """The new-style print function for Python 2.4 and 2.5."""
700 fp = kwargs.pop("file", sys.stdout)
701 if fp is None:
702 return
703 def write(data):
704 if not isinstance(data, basestring):
705 data = str(data)
706 # If the file has an encoding, encode unicode with it.
707 if (isinstance(fp, file) and
708 isinstance(data, unicode) and
709 fp.encoding is not None):
710 errors = getattr(fp, "errors", None)
711 if errors is None:
712 errors = "strict"
713 data = data.encode(fp.encoding, errors)
714 fp.write(data)
715 want_unicode = False
716 sep = kwargs.pop("sep", None)
717 if sep is not None:
718 if isinstance(sep, unicode):
719 want_unicode = True
720 elif not isinstance(sep, str):
721 raise TypeError("sep must be None or a string")
722 end = kwargs.pop("end", None)
723 if end is not None:
724 if isinstance(end, unicode):
725 want_unicode = True
726 elif not isinstance(end, str):
727 raise TypeError("end must be None or a string")
728 if kwargs:
729 raise TypeError("invalid keyword arguments to print()")
730 if not want_unicode:
731 for arg in args:
732 if isinstance(arg, unicode):
733 want_unicode = True
734 break
735 if want_unicode:
736 newline = unicode("\n")
737 space = unicode(" ")
738 else:
739 newline = "\n"
740 space = " "
741 if sep is None:
742 sep = space
743 if end is None:
744 end = newline
745 for i, arg in enumerate(args):
746 if i:
747 write(sep)
748 write(arg)
749 write(end)
750 if sys.version_info[:2] < (3, 3):
751 _print = print_
752 def print_(*args, **kwargs):
753 fp = kwargs.get("file", sys.stdout)
754 flush = kwargs.pop("flush", False)
755 _print(*args, **kwargs)
756 if flush and fp is not None:
757 fp.flush()
758
759 _add_doc(reraise, """Reraise an exception.""")
760
761 if sys.version_info[0:2] < (3, 4):
762 def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS,
763 updated=functools.WRAPPER_UPDATES):
764 def wrapper(f):
765 f = functools.wraps(wrapped, assigned, updated)(f)
766 f.__wrapped__ = wrapped
767 return f
768 return wrapper
769 else:
770 wraps = functools.wraps
771
772 def with_metaclass(meta, *bases):
773 """Create a base class with a metaclass."""
774 # This requires a bit of explanation: the basic idea is to make a dummy
775 # metaclass for one level of class instantiation that replaces itself with
776 # the actual metaclass.
777 class metaclass(meta):
778 def __new__(cls, name, this_bases, d):
779 return meta(name, bases, d)
780 return type.__new__(metaclass, 'temporary_class', (), {})
781
782
783 def add_metaclass(metaclass):
784 """Class decorator for creating a class with a metaclass."""
785 def wrapper(cls):
786 orig_vars = cls.__dict__.copy()
787 slots = orig_vars.get('__slots__')
788 if slots is not None:
789 if isinstance(slots, str):
790 slots = [slots]
791 for slots_var in slots:
792 orig_vars.pop(slots_var)
793 orig_vars.pop('__dict__', None)
794 orig_vars.pop('__weakref__', None)
795 return metaclass(cls.__name__, cls.__bases__, orig_vars)
796 return wrapper
797
798
799 def python_2_unicode_compatible(klass):
800 """
801 A decorator that defines __unicode__ and __str__ methods under Python 2.
802 Under Python 3 it does nothing.
803
804 To support Python 2 and 3 with a single code base, define a __str__ method
805 returning text and apply this decorator to the class.
806 """
807 if PY2:
808 if '__str__' not in klass.__dict__:
809 raise ValueError("@python_2_unicode_compatible cannot be applied "
810 "to %s because it doesn't define __str__()." %
811 klass.__name__)
812 klass.__unicode__ = klass.__str__
813 klass.__str__ = lambda self: self.__unicode__().encode('utf-8')
814 return klass
815
816
817 # Complete the moves implementation.
818 # This code is at the end of this module to speed up module loading.
819 # Turn this module into a package.
820 __path__ = [] # required for PEP 302 and PEP 451
821 __package__ = __name__ # see PEP 366 @ReservedAssignment
822 if globals().get("__spec__") is not None:
823 __spec__.submodule_search_locations = [] # PEP 451 @UndefinedVariable
824 # Remove other six meta path importers, since they cause problems. This can
825 # happen if six is removed from sys.modules and then reloaded. (Setuptools does
826 # this for some reason.)
827 if sys.meta_path:
828 for i, importer in enumerate(sys.meta_path):
829 # Here's some real nastiness: Another "instance" of the six module might
830 # be floating around. Therefore, we can't use isinstance() to check for
831 # the six meta path importer, since the other six instance will have
832 # inserted an importer with different class.
833 if (type(importer).__name__ == "_SixMetaPathImporter" and
834 importer.name == __name__):
835 del sys.meta_path[i]
836 break
837 del i, importer
838 # Finally, add the importer to the meta path import hook.
839 sys.meta_path.append(_importer)
@@ -84,17 +84,6 b' self: super: {'
84 license = [ pkgs.lib.licenses.mit ];
84 license = [ pkgs.lib.licenses.mit ];
85 };
85 };
86 };
86 };
87 "authomatic" = super.buildPythonPackage {
88 name = "authomatic-0.1.0.post1";
89 doCheck = false;
90 src = fetchurl {
91 url = "https://code.rhodecode.com/upstream/authomatic/artifacts/download/0-4fe9c041-a567-4f84-be4c-7efa2a606d3c.tar.gz?md5=f6bdc3c769688212db68233e8d2b0383";
92 sha256 = "0pc716mva0ym6xd8jwzjbjp8dqxy9069wwwv2aqwb8lyhl4757ab";
93 };
94 meta = {
95 license = [ pkgs.lib.licenses.mit ];
96 };
97 };
98 "babel" = super.buildPythonPackage {
87 "babel" = super.buildPythonPackage {
99 name = "babel-1.3";
88 name = "babel-1.3";
100 doCheck = false;
89 doCheck = false;
@@ -920,11 +909,11 b' self: super: {'
920 };
909 };
921 };
910 };
922 "meld3" = super.buildPythonPackage {
911 "meld3" = super.buildPythonPackage {
923 name = "meld3-1.0.2";
912 name = "meld3-2.0.0";
924 doCheck = false;
913 doCheck = false;
925 src = fetchurl {
914 src = fetchurl {
926 url = "https://files.pythonhosted.org/packages/45/a0/317c6422b26c12fe0161e936fc35f36552069ba8e6f7ecbd99bbffe32a5f/meld3-1.0.2.tar.gz";
915 url = "https://files.pythonhosted.org/packages/00/3b/023446ddc1bf0b519c369cbe88269c30c6a64bd10af4817c73f560c302f7/meld3-2.0.0.tar.gz";
927 sha256 = "0n4mkwlpsqnmn0dm0wm5hn9nkda0nafl0jdy5sdl5977znh59dzp";
916 sha256 = "1fbyafwi0d54394hkmp65nf6vk0qm4kipf5z60pdp4244rvadz8y";
928 };
917 };
929 meta = {
918 meta = {
930 license = [ { fullName = "BSD-derived (http://www.repoze.org/LICENSE.txt)"; } ];
919 license = [ { fullName = "BSD-derived (http://www.repoze.org/LICENSE.txt)"; } ];
@@ -1271,11 +1260,11 b' self: super: {'
1271 };
1260 };
1272 };
1261 };
1273 "pyasn1" = super.buildPythonPackage {
1262 "pyasn1" = super.buildPythonPackage {
1274 name = "pyasn1-0.4.6";
1263 name = "pyasn1-0.4.7";
1275 doCheck = false;
1264 doCheck = false;
1276 src = fetchurl {
1265 src = fetchurl {
1277 url = "https://files.pythonhosted.org/packages/e3/12/dfffc84b783e280e942409d6b651fe4a5a746433c34589da7362db2c99c6/pyasn1-0.4.6.tar.gz";
1266 url = "https://files.pythonhosted.org/packages/ca/f8/2a60a2c88a97558bdd289b6dc9eb75b00bd90ff34155d681ba6dbbcb46b2/pyasn1-0.4.7.tar.gz";
1278 sha256 = "11mwdsvrbwvjmny40cxa76h81bbc8jfr1prvw6hw7yvg374xawxp";
1267 sha256 = "0146ryp4g09ycy8p3l2vigmgfg42n4gb8whgg8cysrhxr9b56jd9";
1279 };
1268 };
1280 meta = {
1269 meta = {
1281 license = [ pkgs.lib.licenses.bsdOriginal ];
1270 license = [ pkgs.lib.licenses.bsdOriginal ];
@@ -1738,7 +1727,6 b' self: super: {'
1738 doCheck = true;
1727 doCheck = true;
1739 propagatedBuildInputs = [
1728 propagatedBuildInputs = [
1740 self."amqp"
1729 self."amqp"
1741 self."authomatic"
1742 self."babel"
1730 self."babel"
1743 self."beaker"
1731 self."beaker"
1744 self."bleach"
1732 self."bleach"
@@ -1916,11 +1904,11 b' self: super: {'
1916 };
1904 };
1917 };
1905 };
1918 "setuptools" = super.buildPythonPackage {
1906 "setuptools" = super.buildPythonPackage {
1919 name = "setuptools-41.1.0";
1907 name = "setuptools-41.2.0";
1920 doCheck = false;
1908 doCheck = false;
1921 src = fetchurl {
1909 src = fetchurl {
1922 url = "https://files.pythonhosted.org/packages/68/0c/e470db6866aedbff3c4c88faf7f81b90343d8ff32cd68b62db1b65037fb4/setuptools-41.1.0.zip";
1910 url = "https://files.pythonhosted.org/packages/d9/ca/7279974e489e8b65003fe618a1a741d6350227fa2bf48d16be76c7422423/setuptools-41.2.0.zip";
1923 sha256 = "1a246z6cikg42adqmpswzjp59hkqwr7xxqs7xyags4cr556bh6f5";
1911 sha256 = "04k0dp9msmlv3g3zx7f5p8wdjr6hdf5c0bgmczlc4yncwyx6pf36";
1924 };
1912 };
1925 meta = {
1913 meta = {
1926 license = [ pkgs.lib.licenses.mit ];
1914 license = [ pkgs.lib.licenses.mit ];
@@ -1,8 +1,6 b''
1 ## dependencies
1 ## dependencies
2
2
3 amqp==2.3.1
3 amqp==2.3.1
4 # not released authomatic that has updated some oauth providers
5 https://code.rhodecode.com/upstream/authomatic/artifacts/download/0-4fe9c041-a567-4f84-be4c-7efa2a606d3c.tar.gz?md5=f6bdc3c769688212db68233e8d2b0383#egg=authomatic==0.1.0.post1
6
4
7 babel==1.3
5 babel==1.3
8 beaker==1.9.1
6 beaker==1.9.1
General Comments 0
You need to be logged in to leave comments. Login now