|
|
|
|
|
|
|
|
import collections
|
|
|
import copy
|
|
|
import datetime
|
|
|
import hashlib
|
|
|
import hmac
|
|
|
import json
|
|
|
import logging
|
|
|
try:
|
|
|
import cPickle as pickle
|
|
|
except ImportError:
|
|
|
import pickle
|
|
|
import sys
|
|
|
import threading
|
|
|
import time
|
|
|
from xml.etree import ElementTree
|
|
|
|
|
|
from authomatic.exceptions import (
|
|
|
ConfigError,
|
|
|
CredentialsError,
|
|
|
ImportStringError,
|
|
|
RequestElementsError,
|
|
|
SessionError,
|
|
|
)
|
|
|
from authomatic import six
|
|
|
from authomatic.six.moves import urllib_parse as parse
|
|
|
|
|
|
|
|
|
# =========================================================================
|
|
|
# Global variables !!!
|
|
|
# =========================================================================
|
|
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
_logger.addHandler(logging.StreamHandler(sys.stdout))
|
|
|
|
|
|
_counter = None
|
|
|
|
|
|
|
|
|
def normalize_dict(dict_):
|
|
|
"""
|
|
|
Replaces all values that are single-item iterables with the value of its
|
|
|
index 0.
|
|
|
|
|
|
:param dict dict_:
|
|
|
Dictionary to normalize.
|
|
|
|
|
|
:returns:
|
|
|
Normalized dictionary.
|
|
|
|
|
|
"""
|
|
|
|
|
|
return dict([(k, v[0] if not isinstance(v, str) and len(v) == 1 else v)
|
|
|
for k, v in list(dict_.items())])
|
|
|
|
|
|
|
|
|
def items_to_dict(items):
|
|
|
"""
|
|
|
Converts list of tuples to dictionary with duplicate keys converted to
|
|
|
lists.
|
|
|
|
|
|
:param list items:
|
|
|
List of tuples.
|
|
|
|
|
|
:returns:
|
|
|
:class:`dict`
|
|
|
|
|
|
"""
|
|
|
|
|
|
res = collections.defaultdict(list)
|
|
|
|
|
|
for k, v in items:
|
|
|
res[k].append(v)
|
|
|
|
|
|
return normalize_dict(dict(res))
|
|
|
|
|
|
|
|
|
class Counter(object):
|
|
|
"""
|
|
|
A simple counter to be used in the config to generate unique `id` values.
|
|
|
"""
|
|
|
|
|
|
def __init__(self, start=0):
|
|
|
self._count = start
|
|
|
|
|
|
def count(self):
|
|
|
self._count += 1
|
|
|
return self._count
|
|
|
|
|
|
|
|
|
_counter = Counter()
|
|
|
|
|
|
|
|
|
def provider_id():
|
|
|
"""
|
|
|
A simple counter to be used in the config to generate unique `IDs`.
|
|
|
|
|
|
:returns:
|
|
|
:class:`int`.
|
|
|
|
|
|
Use it in the :doc:`config` like this:
|
|
|
::
|
|
|
|
|
|
import authomatic
|
|
|
|
|
|
CONFIG = {
|
|
|
'facebook': {
|
|
|
'class_': authomatic.providers.oauth2.Facebook,
|
|
|
'id': authomatic.provider_id(), # returns 1
|
|
|
'consumer_key': '##########',
|
|
|
'consumer_secret': '##########',
|
|
|
'scope': ['user_about_me', 'email']
|
|
|
},
|
|
|
'google': {
|
|
|
'class_': 'authomatic.providers.oauth2.Google',
|
|
|
'id': authomatic.provider_id(), # returns 2
|
|
|
'consumer_key': '##########',
|
|
|
'consumer_secret': '##########',
|
|
|
'scope': ['https://www.googleapis.com/auth/userinfo.profile',
|
|
|
'https://www.googleapis.com/auth/userinfo.email']
|
|
|
},
|
|
|
'windows_live': {
|
|
|
'class_': 'oauth2.WindowsLive',
|
|
|
'id': authomatic.provider_id(), # returns 3
|
|
|
'consumer_key': '##########',
|
|
|
'consumer_secret': '##########',
|
|
|
'scope': ['wl.basic', 'wl.emails', 'wl.photos']
|
|
|
},
|
|
|
}
|
|
|
|
|
|
"""
|
|
|
|
|
|
return _counter.count()
|
|
|
|
|
|
|
|
|
def escape(s):
|
|
|
"""
|
|
|
Escape a URL including any /.
|
|
|
"""
|
|
|
return parse.quote(s.encode('utf-8'), safe='~')
|
|
|
|
|
|
|
|
|
def json_qs_parser(body):
|
|
|
"""
|
|
|
Parses response body from JSON, XML or query string.
|
|
|
|
|
|
:param body:
|
|
|
string
|
|
|
|
|
|
:returns:
|
|
|
:class:`dict`, :class:`list` if input is JSON or query string,
|
|
|
:class:`xml.etree.ElementTree.Element` if XML.
|
|
|
|
|
|
"""
|
|
|
try:
|
|
|
# Try JSON first.
|
|
|
return json.loads(body)
|
|
|
except (OverflowError, TypeError, ValueError):
|
|
|
pass
|
|
|
|
|
|
try:
|
|
|
# Then XML.
|
|
|
return ElementTree.fromstring(body)
|
|
|
except (ElementTree.ParseError, TypeError, ValueError):
|
|
|
pass
|
|
|
|
|
|
# Finally query string.
|
|
|
return dict(parse.parse_qsl(body))
|
|
|
|
|
|
|
|
|
def import_string(import_name, silent=False):
|
|
|
"""
|
|
|
Imports an object by string in dotted notation.
|
|
|
|
|
|
taken `from webapp2.import_string() <http://webapp-
|
|
|
improved.appspot.com/api/webapp2.html#webapp2.import_string>`_
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
if '.' in import_name:
|
|
|
module, obj = import_name.rsplit('.', 1)
|
|
|
return getattr(__import__(module, None, None, [obj]), obj)
|
|
|
else:
|
|
|
return __import__(import_name)
|
|
|
except (ImportError, AttributeError) as e:
|
|
|
if not silent:
|
|
|
raise ImportStringError('Import from string failed for path {0}'
|
|
|
.format(import_name), str(e))
|
|
|
|
|
|
|
|
|
def resolve_provider_class(class_):
|
|
|
"""
|
|
|
Returns a provider class.
|
|
|
|
|
|
:param class_name: :class:`string` or
|
|
|
:class:`authomatic.providers.BaseProvider` subclass.
|
|
|
|
|
|
"""
|
|
|
|
|
|
if isinstance(class_, str):
|
|
|
# prepare path for authomatic.providers package
|
|
|
path = '.'.join([__package__, 'providers', class_])
|
|
|
|
|
|
# try to import class by string from providers module or by fully
|
|
|
# qualified path
|
|
|
return import_string(class_, True) or import_string(path)
|
|
|
else:
|
|
|
return class_
|
|
|
|
|
|
|
|
|
def id_to_name(config, short_name):
|
|
|
"""
|
|
|
Returns the provider :doc:`config` key based on it's ``id`` value.
|
|
|
|
|
|
:param dict config:
|
|
|
:doc:`config`.
|
|
|
:param id:
|
|
|
Value of the id parameter in the :ref:`config` to search for.
|
|
|
|
|
|
"""
|
|
|
|
|
|
for k, v in list(config.items()):
|
|
|
if v.get('id') == short_name:
|
|
|
return k
|
|
|
|
|
|
raise Exception(
|
|
|
'No provider with id={0} found in the config!'.format(short_name))
|
|
|
|
|
|
|
|
|
class ReprMixin(object):
|
|
|
"""
|
|
|
Provides __repr__() method with output *ClassName(arg1=value, arg2=value)*.
|
|
|
|
|
|
Ignored are attributes
|
|
|
|
|
|
* which values are considered false.
|
|
|
* with leading underscore.
|
|
|
* listed in _repr_ignore.
|
|
|
|
|
|
Values of attributes listed in _repr_sensitive will be replaced by *###*.
|
|
|
Values which repr() string is longer than _repr_length_limit will be
|
|
|
represented as *ClassName(...)*
|
|
|
|
|
|
"""
|
|
|
|
|
|
#: Iterable of attributes to be ignored.
|
|
|
_repr_ignore = []
|
|
|
#: Iterable of attributes which value should not be visible.
|
|
|
_repr_sensitive = []
|
|
|
#: `int` Values longer than this will be truncated to *ClassName(...)*.
|
|
|
_repr_length_limit = 20
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
|
|
# get class name
|
|
|
name = self.__class__.__name__
|
|
|
|
|
|
# construct keyword arguments
|
|
|
args = []
|
|
|
|
|
|
for k, v in list(self.__dict__.items()):
|
|
|
|
|
|
# ignore attributes with leading underscores and those listed in
|
|
|
# _repr_ignore
|
|
|
if v and not k.startswith('_') and k not in self._repr_ignore:
|
|
|
|
|
|
# replace sensitive values
|
|
|
if k in self._repr_sensitive:
|
|
|
v = '###'
|
|
|
|
|
|
# if repr is too long
|
|
|
if len(repr(v)) > self._repr_length_limit:
|
|
|
# Truncate to ClassName(...)
|
|
|
v = '{0}(...)'.format(v.__class__.__name__)
|
|
|
else:
|
|
|
v = repr(v)
|
|
|
|
|
|
args.append('{0}={1}'.format(k, v))
|
|
|
|
|
|
return '{0}({1})'.format(name, ', '.join(args))
|
|
|
|
|
|
|
|
|
class Future(threading.Thread):
|
|
|
"""
|
|
|
Represents an activity run in a separate thread. Subclasses the standard
|
|
|
library :class:`threading.Thread` and adds :attr:`.get_result` method.
|
|
|
|
|
|
.. warning::
|
|
|
|
|
|
|async|
|
|
|
|
|
|
"""
|
|
|
|
|
|
def __init__(self, func, *args, **kwargs):
|
|
|
"""
|
|
|
:param callable func:
|
|
|
The function to be run in separate thread.
|
|
|
|
|
|
Calls :data:`func` in separate thread and returns immediately.
|
|
|
Accepts arbitrary positional and keyword arguments which will be
|
|
|
passed to :data:`func`.
|
|
|
"""
|
|
|
|
|
|
super(Future, self).__init__()
|
|
|
self._func = func
|
|
|
self._args = args
|
|
|
self._kwargs = kwargs
|
|
|
self._result = None
|
|
|
|
|
|
self.start()
|
|
|
|
|
|
def run(self):
|
|
|
self._result = self._func(*self._args, **self._kwargs)
|
|
|
|
|
|
def get_result(self, timeout=None):
|
|
|
"""
|
|
|
Waits for the wrapped :data:`func` to finish and returns its result.
|
|
|
|
|
|
.. note::
|
|
|
|
|
|
This will block the **calling thread** until the :data:`func`
|
|
|
returns.
|
|
|
|
|
|
:param timeout:
|
|
|
:class:`float` or ``None`` A timeout for the :data:`func` to
|
|
|
return in seconds.
|
|
|
|
|
|
:returns:
|
|
|
The result of the wrapped :data:`func`.
|
|
|
|
|
|
"""
|
|
|
|
|
|
self.join(timeout)
|
|
|
return self._result
|
|
|
|
|
|
|
|
|
class Session(object):
|
|
|
"""
|
|
|
A dictionary-like secure cookie session implementation.
|
|
|
"""
|
|
|
|
|
|
def __init__(self, adapter, secret, name='authomatic', max_age=600,
|
|
|
secure=False):
|
|
|
"""
|
|
|
:param str secret:
|
|
|
Session secret used to sign the session cookie.
|
|
|
:param str name:
|
|
|
Session cookie name.
|
|
|
:param int max_age:
|
|
|
Maximum allowed age of session cookie nonce in seconds.
|
|
|
:param bool secure:
|
|
|
If ``True`` the session cookie will be saved with ``Secure``
|
|
|
attribute.
|
|
|
"""
|
|
|
|
|
|
self.adapter = adapter
|
|
|
self.name = name
|
|
|
self.secret = secret
|
|
|
self.max_age = max_age
|
|
|
self.secure = secure
|
|
|
self._data = {}
|
|
|
|
|
|
def create_cookie(self, delete=None):
|
|
|
"""
|
|
|
Creates the value for ``Set-Cookie`` HTTP header.
|
|
|
|
|
|
:param bool delete:
|
|
|
If ``True`` the cookie value will be ``deleted`` and the
|
|
|
Expires value will be ``Thu, 01-Jan-1970 00:00:01 GMT``.
|
|
|
|
|
|
"""
|
|
|
value = 'deleted' if delete else self._serialize(self.data)
|
|
|
split_url = parse.urlsplit(self.adapter.url)
|
|
|
domain = split_url.netloc.split(':')[0]
|
|
|
|
|
|
# Work-around for issue #11, failure of WebKit-based browsers to accept
|
|
|
# cookies set as part of a redirect response in some circumstances.
|
|
|
if '.' not in domain:
|
|
|
template = '{name}={value}; Path={path}; HttpOnly{secure}{expires}'
|
|
|
else:
|
|
|
template = ('{name}={value}; Domain={domain}; Path={path}; '
|
|
|
'HttpOnly{secure}{expires}')
|
|
|
|
|
|
return template.format(
|
|
|
name=self.name,
|
|
|
value=value,
|
|
|
domain=domain,
|
|
|
path=split_url.path,
|
|
|
secure='; Secure' if self.secure else '',
|
|
|
expires='; Expires=Thu, 01-Jan-1970 00:00:01 GMT' if delete else ''
|
|
|
)
|
|
|
|
|
|
def save(self):
|
|
|
"""
|
|
|
Adds the session cookie to headers.
|
|
|
"""
|
|
|
if self.data:
|
|
|
cookie = self.create_cookie()
|
|
|
cookie_len = len(cookie)
|
|
|
|
|
|
if cookie_len > 4093:
|
|
|
raise SessionError('Cookie too long! The cookie size {0} '
|
|
|
'is more than 4093 bytes.'
|
|
|
.format(cookie_len))
|
|
|
|
|
|
self.adapter.set_header('Set-Cookie', cookie)
|
|
|
|
|
|
# Reset data
|
|
|
self._data = {}
|
|
|
|
|
|
def delete(self):
|
|
|
self.adapter.set_header('Set-Cookie', self.create_cookie(delete=True))
|
|
|
|
|
|
def _get_data(self):
|
|
|
"""
|
|
|
Extracts the session data from cookie.
|
|
|
"""
|
|
|
cookie = self.adapter.cookies.get(self.name)
|
|
|
return self._deserialize(cookie) if cookie else {}
|
|
|
|
|
|
@property
|
|
|
def data(self):
|
|
|
"""
|
|
|
Gets session data lazily.
|
|
|
"""
|
|
|
if not self._data:
|
|
|
self._data = self._get_data()
|
|
|
# Always return a dict, even if deserialization returned nothing
|
|
|
if self._data is None:
|
|
|
self._data = {}
|
|
|
return self._data
|
|
|
|
|
|
def _signature(self, *parts):
|
|
|
"""
|
|
|
Creates signature for the session.
|
|
|
"""
|
|
|
signature = hmac.new(six.b(self.secret), digestmod=hashlib.sha1)
|
|
|
signature.update(six.b('|'.join(parts)))
|
|
|
return signature.hexdigest()
|
|
|
|
|
|
def _serialize(self, value):
|
|
|
"""
|
|
|
Converts the value to a signed string with timestamp.
|
|
|
|
|
|
:param value:
|
|
|
Object to be serialized.
|
|
|
|
|
|
:returns:
|
|
|
Serialized value.
|
|
|
|
|
|
"""
|
|
|
|
|
|
# data = copy.deepcopy(value)
|
|
|
data = value
|
|
|
|
|
|
# 1. Serialize
|
|
|
serialized = pickle.dumps(data).decode('latin-1')
|
|
|
|
|
|
# 2. Encode
|
|
|
# Percent encoding produces smaller result then urlsafe base64.
|
|
|
encoded = parse.quote(serialized, '')
|
|
|
|
|
|
# 3. Concatenate
|
|
|
timestamp = str(int(time.time()))
|
|
|
signature = self._signature(self.name, encoded, timestamp)
|
|
|
concatenated = '|'.join([encoded, timestamp, signature])
|
|
|
|
|
|
return concatenated
|
|
|
|
|
|
def _deserialize(self, value):
|
|
|
"""
|
|
|
Deserializes and verifies the value created by :meth:`._serialize`.
|
|
|
|
|
|
:param str value:
|
|
|
The serialized value.
|
|
|
|
|
|
:returns:
|
|
|
Deserialized object.
|
|
|
|
|
|
"""
|
|
|
|
|
|
# 3. Split
|
|
|
encoded, timestamp, signature = value.split('|')
|
|
|
|
|
|
# Verify signature
|
|
|
if not signature == self._signature(self.name, encoded, timestamp):
|
|
|
raise SessionError('Invalid signature "{0}"!'.format(signature))
|
|
|
|
|
|
# Verify timestamp
|
|
|
if int(timestamp) < int(time.time()) - self.max_age:
|
|
|
return None
|
|
|
|
|
|
# 2. Decode
|
|
|
decoded = parse.unquote(encoded)
|
|
|
|
|
|
# 1. Deserialize
|
|
|
deserialized = pickle.loads(decoded.encode('latin-1'))
|
|
|
|
|
|
return deserialized
|
|
|
|
|
|
def __setitem__(self, key, value):
|
|
|
self._data[key] = value
|
|
|
|
|
|
def __getitem__(self, key):
|
|
|
return self.data.__getitem__(key)
|
|
|
|
|
|
def __delitem__(self, key):
|
|
|
return self._data.__delitem__(key)
|
|
|
|
|
|
def get(self, key, default=None):
|
|
|
return self.data.get(key, default)
|
|
|
|
|
|
|
|
|
class User(ReprMixin):
|
|
|
"""
|
|
|
Provides unified interface to selected **user** info returned by different
|
|
|
**providers**.
|
|
|
|
|
|
.. note:: The value format may vary across providers.
|
|
|
|
|
|
"""
|
|
|
|
|
|
def __init__(self, provider, **kwargs):
|
|
|
#: A :doc:`provider <providers>` instance.
|
|
|
self.provider = provider
|
|
|
|
|
|
#: An :class:`.Credentials` instance.
|
|
|
self.credentials = kwargs.get('credentials')
|
|
|
|
|
|
#: A :class:`dict` containing all the **user** information returned
|
|
|
#: by the **provider**.
|
|
|
#: The structure differs across **providers**.
|
|
|
self.data = kwargs.get('data')
|
|
|
|
|
|
#: The :attr:`.Response.content` of the request made to update
|
|
|
#: the user.
|
|
|
self.content = kwargs.get('content')
|
|
|
|
|
|
#: :class:`str` ID assigned to the **user** by the **provider**.
|
|
|
self.id = kwargs.get('id')
|
|
|
#: :class:`str` User name e.g. *andrewpipkin*.
|
|
|
self.username = kwargs.get('username')
|
|
|
#: :class:`str` Name e.g. *Andrew Pipkin*.
|
|
|
self.name = kwargs.get('name')
|
|
|
#: :class:`str` First name e.g. *Andrew*.
|
|
|
self.first_name = kwargs.get('first_name')
|
|
|
#: :class:`str` Last name e.g. *Pipkin*.
|
|
|
self.last_name = kwargs.get('last_name')
|
|
|
#: :class:`str` Nickname e.g. *Andy*.
|
|
|
self.nickname = kwargs.get('nickname')
|
|
|
#: :class:`str` Link URL.
|
|
|
self.link = kwargs.get('link')
|
|
|
#: :class:`str` Gender.
|
|
|
self.gender = kwargs.get('gender')
|
|
|
#: :class:`str` Timezone.
|
|
|
self.timezone = kwargs.get('timezone')
|
|
|
#: :class:`str` Locale.
|
|
|
self.locale = kwargs.get('locale')
|
|
|
#: :class:`str` E-mail.
|
|
|
self.email = kwargs.get('email')
|
|
|
#: :class:`str` phone.
|
|
|
self.phone = kwargs.get('phone')
|
|
|
#: :class:`str` Picture URL.
|
|
|
self.picture = kwargs.get('picture')
|
|
|
#: Birth date as :class:`datetime.datetime()` or :class:`str`
|
|
|
# if parsing failed or ``None``.
|
|
|
self.birth_date = kwargs.get('birth_date')
|
|
|
#: :class:`str` Country.
|
|
|
self.country = kwargs.get('country')
|
|
|
#: :class:`str` City.
|
|
|
self.city = kwargs.get('city')
|
|
|
#: :class:`str` Geographical location.
|
|
|
self.location = kwargs.get('location')
|
|
|
#: :class:`str` Postal code.
|
|
|
self.postal_code = kwargs.get('postal_code')
|
|
|
#: Instance of the Google App Engine Users API
|
|
|
#: `User <https://developers.google.com/appengine/docs/python/users/userclass>`_ class.
|
|
|
#: Only present when using the :class:`authomatic.providers.gaeopenid.GAEOpenID` provider.
|
|
|
self.gae_user = kwargs.get('gae_user')
|
|
|
|
|
|
def update(self):
|
|
|
"""
|
|
|
Updates the user info by fetching the **provider's** user info URL.
|
|
|
|
|
|
:returns:
|
|
|
Updated instance of this class.
|
|
|
|
|
|
"""
|
|
|
|
|
|
return self.provider.update_user()
|
|
|
|
|
|
def async_update(self):
|
|
|
"""
|
|
|
Same as :meth:`.update` but runs asynchronously in a separate thread.
|
|
|
|
|
|
.. warning::
|
|
|
|
|
|
|async|
|
|
|
|
|
|
:returns:
|
|
|
:class:`.Future` instance representing the separate thread.
|
|
|
|
|
|
"""
|
|
|
|
|
|
return Future(self.update)
|
|
|
|
|
|
def to_dict(self):
|
|
|
"""
|
|
|
Converts the :class:`.User` instance to a :class:`dict`.
|
|
|
|
|
|
:returns:
|
|
|
:class:`dict`
|
|
|
|
|
|
"""
|
|
|
|
|
|
# copy the dictionary
|
|
|
d = copy.copy(self.__dict__)
|
|
|
|
|
|
# Keep only the provider name to avoid circular reference
|
|
|
d['provider'] = self.provider.name
|
|
|
d['credentials'] = self.credentials.serialize(
|
|
|
) if self.credentials else None
|
|
|
d['birth_date'] = str(d['birth_date'])
|
|
|
|
|
|
# Remove content
|
|
|
d.pop('content')
|
|
|
|
|
|
if isinstance(self.data, ElementTree.Element):
|
|
|
d['data'] = None
|
|
|
|
|
|
return d
|
|
|
|
|
|
|
|
|
SupportedUserAttributesNT = collections.namedtuple(
|
|
|
typename='SupportedUserAttributesNT',
|
|
|
field_names=['birth_date', 'city', 'country', 'email', 'first_name',
|
|
|
'gender', 'id', 'last_name', 'link', 'locale', 'location',
|
|
|
'name', 'nickname', 'phone', 'picture', 'postal_code',
|
|
|
'timezone', 'username', ]
|
|
|
)
|
|
|
|
|
|
|
|
|
class SupportedUserAttributes(SupportedUserAttributesNT):
|
|
|
def __new__(cls, **kwargs):
|
|
|
defaults = dict((i, False) for i in SupportedUserAttributes._fields) # pylint:disable=no-member
|
|
|
defaults.update(**kwargs)
|
|
|
return super(SupportedUserAttributes, cls).__new__(cls, **defaults)
|
|
|
|
|
|
|
|
|
class Credentials(ReprMixin):
|
|
|
"""
|
|
|
Contains all necessary information to fetch **user's protected resources**.
|
|
|
"""
|
|
|
|
|
|
_repr_sensitive = ('token', 'refresh_token', 'token_secret',
|
|
|
'consumer_key', 'consumer_secret')
|
|
|
|
|
|
def __init__(self, config, **kwargs):
|
|
|
|
|
|
#: :class:`dict` :doc:`config`.
|
|
|
self.config = config
|
|
|
|
|
|
#: :class:`str` User **access token**.
|
|
|
self.token = kwargs.get('token', '')
|
|
|
|
|
|
#: :class:`str` Access token type.
|
|
|
self.token_type = kwargs.get('token_type', '')
|
|
|
|
|
|
#: :class:`str` Refresh token.
|
|
|
self.refresh_token = kwargs.get('refresh_token', '')
|
|
|
|
|
|
#: :class:`str` Access token secret.
|
|
|
self.token_secret = kwargs.get('token_secret', '')
|
|
|
|
|
|
#: :class:`int` Expiration date as UNIX timestamp.
|
|
|
self.expiration_time = int(kwargs.get('expiration_time', 0))
|
|
|
|
|
|
#: A :doc:`Provider <providers>` instance**.
|
|
|
provider = kwargs.get('provider')
|
|
|
|
|
|
self.expire_in = int(kwargs.get('expire_in', 0))
|
|
|
|
|
|
if provider:
|
|
|
#: :class:`str` Provider name specified in the :doc:`config`.
|
|
|
self.provider_name = provider.name
|
|
|
|
|
|
#: :class:`str` Provider type e.g.
|
|
|
# ``"authomatic.providers.oauth2.OAuth2"``.
|
|
|
self.provider_type = provider.get_type()
|
|
|
|
|
|
#: :class:`str` Provider type e.g.
|
|
|
# ``"authomatic.providers.oauth2.OAuth2"``.
|
|
|
self.provider_type_id = provider.type_id
|
|
|
|
|
|
#: :class:`str` Provider short name specified in the :doc:`config`.
|
|
|
self.provider_id = int(provider.id) if provider.id else None
|
|
|
|
|
|
#: :class:`class` Provider class.
|
|
|
self.provider_class = provider.__class__
|
|
|
|
|
|
#: :class:`str` Consumer key specified in the :doc:`config`.
|
|
|
self.consumer_key = provider.consumer_key
|
|
|
|
|
|
#: :class:`str` Consumer secret specified in the :doc:`config`.
|
|
|
self.consumer_secret = provider.consumer_secret
|
|
|
|
|
|
else:
|
|
|
self.provider_name = kwargs.get('provider_name', '')
|
|
|
self.provider_type = kwargs.get('provider_type', '')
|
|
|
self.provider_type_id = kwargs.get('provider_type_id')
|
|
|
self.provider_id = kwargs.get('provider_id')
|
|
|
self.provider_class = kwargs.get('provider_class')
|
|
|
|
|
|
self.consumer_key = kwargs.get('consumer_key', '')
|
|
|
self.consumer_secret = kwargs.get('consumer_secret', '')
|
|
|
|
|
|
@property
|
|
|
def expire_in(self):
|
|
|
"""
|
|
|
|
|
|
"""
|
|
|
|
|
|
return self._expire_in
|
|
|
|
|
|
@expire_in.setter
|
|
|
def expire_in(self, value):
|
|
|
"""
|
|
|
Computes :attr:`.expiration_time` when the value is set.
|
|
|
"""
|
|
|
|
|
|
# pylint:disable=attribute-defined-outside-init
|
|
|
if value:
|
|
|
self._expiration_time = int(time.time()) + int(value)
|
|
|
self._expire_in = value
|
|
|
|
|
|
@property
|
|
|
def expiration_time(self):
|
|
|
return self._expiration_time
|
|
|
|
|
|
@expiration_time.setter
|
|
|
def expiration_time(self, value):
|
|
|
|
|
|
# pylint:disable=attribute-defined-outside-init
|
|
|
self._expiration_time = int(value)
|
|
|
self._expire_in = self._expiration_time - int(time.time())
|
|
|
|
|
|
@property
|
|
|
def expiration_date(self):
|
|
|
"""
|
|
|
Expiration date as :class:`datetime.datetime` or ``None`` if
|
|
|
credentials never expire.
|
|
|
"""
|
|
|
|
|
|
if self.expire_in < 0:
|
|
|
return None
|
|
|
else:
|
|
|
return datetime.datetime.fromtimestamp(self.expiration_time)
|
|
|
|
|
|
@property
|
|
|
def valid(self):
|
|
|
"""
|
|
|
``True`` if credentials are valid, ``False`` if expired.
|
|
|
"""
|
|
|
|
|
|
if self.expiration_time:
|
|
|
return self.expiration_time > int(time.time())
|
|
|
else:
|
|
|
return True
|
|
|
|
|
|
def expire_soon(self, seconds):
|
|
|
"""
|
|
|
Returns ``True`` if credentials expire sooner than specified.
|
|
|
|
|
|
:param int seconds:
|
|
|
Number of seconds.
|
|
|
|
|
|
:returns:
|
|
|
``True`` if credentials expire sooner than specified,
|
|
|
else ``False``.
|
|
|
|
|
|
"""
|
|
|
|
|
|
if self.expiration_time:
|
|
|
return self.expiration_time < int(time.time()) + int(seconds)
|
|
|
else:
|
|
|
return False
|
|
|
|
|
|
def refresh(self, force=False, soon=86400):
|
|
|
"""
|
|
|
Refreshes the credentials only if the **provider** supports it and if
|
|
|
it will expire in less than one day. It does nothing in other cases.
|
|
|
|
|
|
.. note::
|
|
|
|
|
|
The credentials will be refreshed only if it gives sense
|
|
|
i.e. only |oauth2|_ has the notion of credentials
|
|
|
*refreshment/extension*.
|
|
|
And there are also differences across providers e.g. Google
|
|
|
supports refreshment only if there is a ``refresh_token`` in
|
|
|
the credentials and that in turn is present only if the
|
|
|
``access_type`` parameter was set to ``offline`` in the
|
|
|
**user authorization request**.
|
|
|
|
|
|
:param bool force:
|
|
|
If ``True`` the credentials will be refreshed even if they
|
|
|
won't expire soon.
|
|
|
|
|
|
:param int soon:
|
|
|
Number of seconds specifying what means *soon*.
|
|
|
|
|
|
"""
|
|
|
|
|
|
if hasattr(self.provider_class, 'refresh_credentials'):
|
|
|
if force or self.expire_soon(soon):
|
|
|
logging.info('PROVIDER NAME: {0}'.format(self.provider_name))
|
|
|
return self.provider_class(
|
|
|
self, None, self.provider_name).refresh_credentials(self)
|
|
|
|
|
|
def async_refresh(self, *args, **kwargs):
|
|
|
"""
|
|
|
Same as :meth:`.refresh` but runs asynchronously in a separate thread.
|
|
|
|
|
|
.. warning::
|
|
|
|
|
|
|async|
|
|
|
|
|
|
:returns:
|
|
|
:class:`.Future` instance representing the separate thread.
|
|
|
|
|
|
"""
|
|
|
|
|
|
return Future(self.refresh, *args, **kwargs)
|
|
|
|
|
|
def provider_type_class(self):
|
|
|
"""
|
|
|
Returns the :doc:`provider <providers>` class specified in the
|
|
|
:doc:`config`.
|
|
|
|
|
|
:returns:
|
|
|
:class:`authomatic.providers.BaseProvider` subclass.
|
|
|
|
|
|
"""
|
|
|
|
|
|
return resolve_provider_class(self.provider_type)
|
|
|
|
|
|
def serialize(self):
|
|
|
"""
|
|
|
Converts the credentials to a percent encoded string to be stored for
|
|
|
later use.
|
|
|
|
|
|
:returns:
|
|
|
:class:`string`
|
|
|
|
|
|
"""
|
|
|
|
|
|
if self.provider_id is None:
|
|
|
raise ConfigError(
|
|
|
'To serialize credentials you need to specify a '
|
|
|
'unique integer under the "id" key in the config '
|
|
|
'for each provider!')
|
|
|
|
|
|
# Get the provider type specific items.
|
|
|
rest = self.provider_type_class().to_tuple(self)
|
|
|
|
|
|
# Provider ID and provider type ID are always the first two items.
|
|
|
result = (self.provider_id, self.provider_type_id) + rest
|
|
|
|
|
|
# Make sure that all items are strings.
|
|
|
stringified = [str(i) for i in result]
|
|
|
|
|
|
# Concatenate by newline.
|
|
|
concatenated = '\n'.join(stringified)
|
|
|
|
|
|
# Percent encode.
|
|
|
return parse.quote(concatenated, '')
|
|
|
|
|
|
@classmethod
|
|
|
def deserialize(cls, config, credentials):
|
|
|
"""
|
|
|
A *class method* which reconstructs credentials created by
|
|
|
:meth:`serialize`. You can also pass it a :class:`.Credentials`
|
|
|
instance.
|
|
|
|
|
|
:param dict config:
|
|
|
The same :doc:`config` used in the :func:`.login` to get the
|
|
|
credentials.
|
|
|
:param str credentials:
|
|
|
:class:`string` The serialized credentials or
|
|
|
:class:`.Credentials` instance.
|
|
|
|
|
|
:returns:
|
|
|
:class:`.Credentials`
|
|
|
|
|
|
"""
|
|
|
|
|
|
# Accept both serialized and normal.
|
|
|
if isinstance(credentials, Credentials):
|
|
|
return credentials
|
|
|
|
|
|
decoded = parse.unquote(credentials)
|
|
|
|
|
|
split = decoded.split('\n')
|
|
|
|
|
|
# We need the provider ID to move forward.
|
|
|
if split[0] is None:
|
|
|
raise CredentialsError(
|
|
|
'To deserialize credentials you need to specify a unique '
|
|
|
'integer under the "id" key in the config for each provider!')
|
|
|
|
|
|
# Get provider config by short name.
|
|
|
provider_name = id_to_name(config, int(split[0]))
|
|
|
cfg = config.get(provider_name)
|
|
|
|
|
|
# Get the provider class.
|
|
|
ProviderClass = resolve_provider_class(cfg.get('class_'))
|
|
|
|
|
|
deserialized = Credentials(config)
|
|
|
|
|
|
deserialized.provider_id = int(split[0])
|
|
|
deserialized.provider_type = ProviderClass.get_type()
|
|
|
deserialized.provider_type_id = split[1]
|
|
|
deserialized.provider_class = ProviderClass
|
|
|
deserialized.provider_name = provider_name
|
|
|
deserialized.provider_class = ProviderClass
|
|
|
|
|
|
# Add provider type specific properties.
|
|
|
return ProviderClass.reconstruct(split[2:], deserialized, cfg)
|
|
|
|
|
|
|
|
|
class LoginResult(ReprMixin):
|
|
|
"""
|
|
|
Result of the :func:`authomatic.login` function.
|
|
|
"""
|
|
|
|
|
|
def __init__(self, provider):
|
|
|
#: A :doc:`provider <providers>` instance.
|
|
|
self.provider = provider
|
|
|
|
|
|
#: An instance of the :exc:`authomatic.exceptions.BaseError` subclass.
|
|
|
self.error = None
|
|
|
|
|
|
def popup_js(self, callback_name=None, indent=None,
|
|
|
custom=None, stay_open=False):
|
|
|
"""
|
|
|
Returns JavaScript that:
|
|
|
|
|
|
#. Triggers the ``options.onLoginComplete(result, closer)``
|
|
|
handler set with the :ref:`authomatic.setup() <js_setup>`
|
|
|
function of :ref:`javascript.js <js>`.
|
|
|
#. Calls the JavasScript callback specified by :data:`callback_name`
|
|
|
on the opener of the *login handler popup* and passes it the
|
|
|
*login result* JSON object as first argument and the `closer`
|
|
|
function which you should call in your callback to close the popup.
|
|
|
|
|
|
:param str callback_name:
|
|
|
The name of the javascript callback e.g ``foo.bar.loginCallback``
|
|
|
will result in ``window.opener.foo.bar.loginCallback(result);``
|
|
|
in the HTML.
|
|
|
|
|
|
:param int indent:
|
|
|
The number of spaces to indent the JSON result object.
|
|
|
If ``0`` or negative, only newlines are added.
|
|
|
If ``None``, no newlines are added.
|
|
|
|
|
|
:param custom:
|
|
|
Any JSON serializable object that will be passed to the
|
|
|
``result.custom`` attribute.
|
|
|
|
|
|
:param str stay_open:
|
|
|
If ``True``, the popup will stay open.
|
|
|
|
|
|
:returns:
|
|
|
:class:`str` with JavaScript.
|
|
|
|
|
|
"""
|
|
|
|
|
|
custom_callback = """
|
|
|
try {{ window.opener.{cb}(result, closer); }} catch(e) {{}}
|
|
|
""".format(cb=callback_name) if callback_name else ''
|
|
|
|
|
|
# TODO: Move the window.close() to the opener
|
|
|
return """
|
|
|
(function(){{
|
|
|
|
|
|
closer = function(){{
|
|
|
window.close();
|
|
|
}};
|
|
|
|
|
|
var result = {result};
|
|
|
result.custom = {custom};
|
|
|
|
|
|
{custom_callback}
|
|
|
|
|
|
try {{
|
|
|
window.opener.authomatic.loginComplete(result, closer);
|
|
|
}} catch(e) {{}}
|
|
|
|
|
|
}})();
|
|
|
|
|
|
""".format(result=self.to_json(indent),
|
|
|
custom=json.dumps(custom),
|
|
|
custom_callback=custom_callback,
|
|
|
stay_open='// ' if stay_open else '')
|
|
|
|
|
|
def popup_html(self, callback_name=None, indent=None,
|
|
|
title='Login | {0}', custom=None, stay_open=False):
|
|
|
"""
|
|
|
Returns a HTML with JavaScript that:
|
|
|
|
|
|
#. Triggers the ``options.onLoginComplete(result, closer)`` handler
|
|
|
set with the :ref:`authomatic.setup() <js_setup>` function of
|
|
|
:ref:`javascript.js <js>`.
|
|
|
#. Calls the JavasScript callback specified by :data:`callback_name`
|
|
|
on the opener of the *login handler popup* and passes it the
|
|
|
*login result* JSON object as first argument and the `closer`
|
|
|
function which you should call in your callback to close the popup.
|
|
|
|
|
|
:param str callback_name:
|
|
|
The name of the javascript callback e.g ``foo.bar.loginCallback``
|
|
|
will result in ``window.opener.foo.bar.loginCallback(result);``
|
|
|
in the HTML.
|
|
|
|
|
|
:param int indent:
|
|
|
The number of spaces to indent the JSON result object.
|
|
|
If ``0`` or negative, only newlines are added.
|
|
|
If ``None``, no newlines are added.
|
|
|
|
|
|
:param str title:
|
|
|
The text of the HTML title. You can use ``{0}`` tag inside,
|
|
|
which will be replaced by the provider name.
|
|
|
|
|
|
:param custom:
|
|
|
Any JSON serializable object that will be passed to the
|
|
|
``result.custom`` attribute.
|
|
|
|
|
|
:param str stay_open:
|
|
|
If ``True``, the popup will stay open.
|
|
|
|
|
|
:returns:
|
|
|
:class:`str` with HTML.
|
|
|
|
|
|
"""
|
|
|
|
|
|
return """
|
|
|
<!DOCTYPE html>
|
|
|
<html>
|
|
|
<head><title>{title}</title></head>
|
|
|
<body>
|
|
|
<script type="text/javascript">
|
|
|
{js}
|
|
|
</script>
|
|
|
</body>
|
|
|
</html>
|
|
|
""".format(
|
|
|
title=title.format(self.provider.name if self.provider else ''),
|
|
|
js=self.popup_js(callback_name, indent, custom, stay_open)
|
|
|
)
|
|
|
|
|
|
@property
|
|
|
def user(self):
|
|
|
"""
|
|
|
A :class:`.User` instance.
|
|
|
"""
|
|
|
|
|
|
return self.provider.user if self.provider else None
|
|
|
|
|
|
def to_dict(self):
|
|
|
return dict(provider=self.provider, user=self.user, error=self.error)
|
|
|
|
|
|
def to_json(self, indent=4):
|
|
|
return json.dumps(self, default=lambda obj: obj.to_dict(
|
|
|
) if hasattr(obj, 'to_dict') else '', indent=indent)
|
|
|
|
|
|
|
|
|
class Response(ReprMixin):
|
|
|
"""
|
|
|
Wraps :class:`httplib.HTTPResponse` and adds.
|
|
|
|
|
|
:attr:`.content` and :attr:`.data` attributes.
|
|
|
|
|
|
"""
|
|
|
|
|
|
def __init__(self, httplib_response, content_parser=None):
|
|
|
"""
|
|
|
:param httplib_response:
|
|
|
The wrapped :class:`httplib.HTTPResponse` instance.
|
|
|
|
|
|
:param function content_parser:
|
|
|
Callable which accepts :attr:`.content` as argument,
|
|
|
parses it and returns the parsed data as :class:`dict`.
|
|
|
"""
|
|
|
|
|
|
self.httplib_response = httplib_response
|
|
|
self.content_parser = content_parser or json_qs_parser
|
|
|
self._data = None
|
|
|
self._content = None
|
|
|
|
|
|
#: Same as :attr:`httplib.HTTPResponse.msg`.
|
|
|
self.msg = httplib_response.msg
|
|
|
#: Same as :attr:`httplib.HTTPResponse.version`.
|
|
|
self.version = httplib_response.version
|
|
|
#: Same as :attr:`httplib.HTTPResponse.status`.
|
|
|
self.status = httplib_response.status
|
|
|
#: Same as :attr:`httplib.HTTPResponse.reason`.
|
|
|
self.reason = httplib_response.reason
|
|
|
|
|
|
def read(self, amt=None):
|
|
|
"""
|
|
|
Same as :meth:`httplib.HTTPResponse.read`.
|
|
|
|
|
|
:param amt:
|
|
|
|
|
|
"""
|
|
|
|
|
|
return self.httplib_response.read(amt)
|
|
|
|
|
|
def getheader(self, name, default=None):
|
|
|
"""
|
|
|
Same as :meth:`httplib.HTTPResponse.getheader`.
|
|
|
|
|
|
:param name:
|
|
|
:param default:
|
|
|
|
|
|
"""
|
|
|
|
|
|
return self.httplib_response.getheader(name, default)
|
|
|
|
|
|
def fileno(self):
|
|
|
"""
|
|
|
Same as :meth:`httplib.HTTPResponse.fileno`.
|
|
|
"""
|
|
|
return self.httplib_response.fileno()
|
|
|
|
|
|
def getheaders(self):
|
|
|
"""
|
|
|
Same as :meth:`httplib.HTTPResponse.getheaders`.
|
|
|
"""
|
|
|
return self.httplib_response.getheaders()
|
|
|
|
|
|
@staticmethod
|
|
|
def is_binary_string(content):
|
|
|
"""
|
|
|
Return true if string is binary data.
|
|
|
"""
|
|
|
|
|
|
textchars = (bytearray([7, 8, 9, 10, 12, 13, 27])
|
|
|
+ bytearray(range(0x20, 0x100)))
|
|
|
return bool(content.translate(None, textchars))
|
|
|
|
|
|
@property
|
|
|
def content(self):
|
|
|
"""
|
|
|
The whole response content.
|
|
|
"""
|
|
|
|
|
|
if not self._content:
|
|
|
content = self.httplib_response.read()
|
|
|
if self.is_binary_string(content):
|
|
|
self._content = content
|
|
|
else:
|
|
|
self._content = content.decode('utf-8')
|
|
|
return self._content
|
|
|
|
|
|
@property
|
|
|
def data(self):
|
|
|
"""
|
|
|
A :class:`dict` of data parsed from :attr:`.content`.
|
|
|
"""
|
|
|
|
|
|
if not self._data:
|
|
|
self._data = self.content_parser(self.content)
|
|
|
return self._data
|
|
|
|
|
|
|
|
|
class UserInfoResponse(Response):
|
|
|
"""
|
|
|
Inherits from :class:`.Response`, adds :attr:`~UserInfoResponse.user`
|
|
|
attribute.
|
|
|
"""
|
|
|
|
|
|
def __init__(self, user, *args, **kwargs):
|
|
|
super(UserInfoResponse, self).__init__(*args, **kwargs)
|
|
|
|
|
|
#: :class:`.User` instance.
|
|
|
self.user = user
|
|
|
|
|
|
|
|
|
class RequestElements(tuple):
|
|
|
"""
|
|
|
A tuple of ``(url, method, params, headers, body)`` request elements.
|
|
|
|
|
|
With some additional properties.
|
|
|
|
|
|
"""
|
|
|
|
|
|
def __new__(cls, url, method, params, headers, body):
|
|
|
return tuple.__new__(cls, (url, method, params, headers, body))
|
|
|
|
|
|
@property
|
|
|
def url(self):
|
|
|
"""
|
|
|
Request URL.
|
|
|
"""
|
|
|
|
|
|
return self[0]
|
|
|
|
|
|
@property
|
|
|
def method(self):
|
|
|
"""
|
|
|
HTTP method of the request.
|
|
|
"""
|
|
|
|
|
|
return self[1]
|
|
|
|
|
|
@property
|
|
|
def params(self):
|
|
|
"""
|
|
|
Dictionary of request parameters.
|
|
|
"""
|
|
|
|
|
|
return self[2]
|
|
|
|
|
|
@property
|
|
|
def headers(self):
|
|
|
"""
|
|
|
Dictionary of request headers.
|
|
|
"""
|
|
|
|
|
|
return self[3]
|
|
|
|
|
|
@property
|
|
|
def body(self):
|
|
|
"""
|
|
|
:class:`str` Body of ``POST``, ``PUT`` and ``PATCH`` requests.
|
|
|
"""
|
|
|
|
|
|
return self[4]
|
|
|
|
|
|
@property
|
|
|
def query_string(self):
|
|
|
"""
|
|
|
Query string of the request.
|
|
|
"""
|
|
|
|
|
|
return parse.urlencode(self.params)
|
|
|
|
|
|
@property
|
|
|
def full_url(self):
|
|
|
"""
|
|
|
URL with query string.
|
|
|
"""
|
|
|
|
|
|
return self.url + '?' + self.query_string
|
|
|
|
|
|
def to_json(self):
|
|
|
return json.dumps(dict(url=self.url,
|
|
|
method=self.method,
|
|
|
params=self.params,
|
|
|
headers=self.headers,
|
|
|
body=self.body))
|
|
|
|
|
|
|
|
|
class Authomatic(object):
|
|
|
def __init__(
|
|
|
self, config, secret, session_max_age=600, secure_cookie=False,
|
|
|
session=None, session_save_method=None, report_errors=True,
|
|
|
debug=False, logging_level=logging.INFO, prefix='authomatic',
|
|
|
logger=None
|
|
|
):
|
|
|
"""
|
|
|
Encapsulates all the functionality of this package.
|
|
|
|
|
|
:param dict config:
|
|
|
:doc:`config`
|
|
|
|
|
|
:param str secret:
|
|
|
A secret string that will be used as the key for signing
|
|
|
:class:`.Session` cookie and as a salt by *CSRF* token generation.
|
|
|
|
|
|
:param session_max_age:
|
|
|
Maximum allowed age of :class:`.Session` cookie nonce in seconds.
|
|
|
|
|
|
:param bool secure_cookie:
|
|
|
If ``True`` the :class:`.Session` cookie will be saved wit
|
|
|
``Secure`` attribute.
|
|
|
|
|
|
:param session:
|
|
|
Custom dictionary-like session implementation.
|
|
|
|
|
|
:param callable session_save_method:
|
|
|
A method of the supplied session or any mechanism that saves the
|
|
|
session data and cookie.
|
|
|
|
|
|
:param bool report_errors:
|
|
|
If ``True`` exceptions encountered during the **login procedure**
|
|
|
will be caught and reported in the :attr:`.LoginResult.error`
|
|
|
attribute.
|
|
|
Default is ``True``.
|
|
|
|
|
|
:param bool debug:
|
|
|
If ``True`` traceback of exceptions will be written to response.
|
|
|
Default is ``False``.
|
|
|
|
|
|
:param int logging_level:
|
|
|
The logging level threshold for the default logger as specified in
|
|
|
the standard Python
|
|
|
`logging library <http://docs.python.org/2/library/logging.html>`_.
|
|
|
This setting is ignored when :data:`logger` is set.
|
|
|
Default is ``logging.INFO``.
|
|
|
|
|
|
:param str prefix:
|
|
|
Prefix used as the :class:`.Session` cookie name.
|
|
|
|
|
|
:param logger:
|
|
|
A :class:`logging.logger` instance.
|
|
|
|
|
|
"""
|
|
|
|
|
|
self.config = config
|
|
|
self.secret = secret
|
|
|
self.session_max_age = session_max_age
|
|
|
self.secure_cookie = secure_cookie
|
|
|
self.session = session
|
|
|
self.session_save_method = session_save_method
|
|
|
self.report_errors = report_errors
|
|
|
self.debug = debug
|
|
|
self.logging_level = logging_level
|
|
|
self.prefix = prefix
|
|
|
self._logger = logger or logging.getLogger(str(id(self)))
|
|
|
|
|
|
# Set logging level.
|
|
|
if logger is None:
|
|
|
self._logger.setLevel(logging_level)
|
|
|
|
|
|
def login(self, adapter, provider_name, callback=None,
|
|
|
session=None, session_saver=None, **kwargs):
|
|
|
"""
|
|
|
If :data:`provider_name` specified, launches the login procedure for
|
|
|
corresponding :doc:`provider </reference/providers>` and returns
|
|
|
:class:`.LoginResult`.
|
|
|
|
|
|
If :data:`provider_name` is empty, acts like
|
|
|
:meth:`.Authomatic.backend`.
|
|
|
|
|
|
.. warning::
|
|
|
|
|
|
The method redirects the **user** to the **provider** which in
|
|
|
turn redirects **him/her** back to the *request handler* where
|
|
|
it has been called.
|
|
|
|
|
|
:param str provider_name:
|
|
|
Name of the provider as specified in the keys of the :doc:`config`.
|
|
|
|
|
|
:param callable callback:
|
|
|
If specified the method will call the callback with
|
|
|
:class:`.LoginResult` passed as argument and will return nothing.
|
|
|
|
|
|
:param bool report_errors:
|
|
|
|
|
|
.. note::
|
|
|
|
|
|
Accepts additional keyword arguments that will be passed to
|
|
|
:doc:`provider <providers>` constructor.
|
|
|
|
|
|
:returns:
|
|
|
:class:`.LoginResult`
|
|
|
|
|
|
"""
|
|
|
|
|
|
if provider_name:
|
|
|
# retrieve required settings for current provider and raise
|
|
|
# exceptions if missing
|
|
|
provider_settings = self.config.get(provider_name)
|
|
|
if not provider_settings:
|
|
|
raise ConfigError('Provider name "{0}" not specified!'
|
|
|
.format(provider_name))
|
|
|
|
|
|
if not (session is None or session_saver is None):
|
|
|
session = session
|
|
|
session_saver = session_saver
|
|
|
else:
|
|
|
session = Session(adapter=adapter,
|
|
|
secret=self.secret,
|
|
|
max_age=self.session_max_age,
|
|
|
name=self.prefix,
|
|
|
secure=self.secure_cookie)
|
|
|
|
|
|
session_saver = session.save
|
|
|
|
|
|
# Resolve provider class.
|
|
|
class_ = provider_settings.get('class_')
|
|
|
if not class_:
|
|
|
raise ConfigError(
|
|
|
'The "class_" key not specified in the config'
|
|
|
' for provider {0}!'.format(provider_name))
|
|
|
ProviderClass = resolve_provider_class(class_)
|
|
|
|
|
|
# FIXME: Find a nicer solution
|
|
|
ProviderClass._logger = self._logger
|
|
|
|
|
|
# instantiate provider class
|
|
|
provider = ProviderClass(self,
|
|
|
adapter=adapter,
|
|
|
provider_name=provider_name,
|
|
|
callback=callback,
|
|
|
session=session,
|
|
|
session_saver=session_saver,
|
|
|
**kwargs)
|
|
|
|
|
|
# return login result
|
|
|
return provider.login()
|
|
|
|
|
|
else:
|
|
|
# Act like backend.
|
|
|
self.backend(adapter)
|
|
|
|
|
|
def credentials(self, credentials):
|
|
|
"""
|
|
|
Deserializes credentials.
|
|
|
|
|
|
:param credentials:
|
|
|
Credentials serialized with :meth:`.Credentials.serialize` or
|
|
|
:class:`.Credentials` instance.
|
|
|
|
|
|
:returns:
|
|
|
:class:`.Credentials`
|
|
|
|
|
|
"""
|
|
|
|
|
|
return Credentials.deserialize(self.config, credentials)
|
|
|
|
|
|
def access(self, credentials, url, params=None, method='GET',
|
|
|
headers=None, body='', max_redirects=5, content_parser=None):
|
|
|
"""
|
|
|
Accesses **protected resource** on behalf of the **user**.
|
|
|
|
|
|
:param credentials:
|
|
|
The **user's** :class:`.Credentials` (serialized or normal).
|
|
|
|
|
|
:param str url:
|
|
|
The **protected resource** URL.
|
|
|
|
|
|
:param str method:
|
|
|
HTTP method of the request.
|
|
|
|
|
|
:param dict headers:
|
|
|
HTTP headers of the request.
|
|
|
|
|
|
:param str body:
|
|
|
Body of ``POST``, ``PUT`` and ``PATCH`` requests.
|
|
|
|
|
|
:param int max_redirects:
|
|
|
Maximum number of HTTP redirects to follow.
|
|
|
|
|
|
:param function content_parser:
|
|
|
A function to be used to parse the :attr:`.Response.data`
|
|
|
from :attr:`.Response.content`.
|
|
|
|
|
|
:returns:
|
|
|
:class:`.Response`
|
|
|
|
|
|
"""
|
|
|
|
|
|
# Deserialize credentials.
|
|
|
credentials = Credentials.deserialize(self.config, credentials)
|
|
|
|
|
|
# Resolve provider class.
|
|
|
ProviderClass = credentials.provider_class
|
|
|
logging.info('ACCESS HEADERS: {0}'.format(headers))
|
|
|
# Access resource and return response.
|
|
|
|
|
|
provider = ProviderClass(
|
|
|
self, adapter=None, provider_name=credentials.provider_name)
|
|
|
provider.credentials = credentials
|
|
|
|
|
|
return provider.access(url=url,
|
|
|
params=params,
|
|
|
method=method,
|
|
|
headers=headers,
|
|
|
body=body,
|
|
|
max_redirects=max_redirects,
|
|
|
content_parser=content_parser)
|
|
|
|
|
|
def async_access(self, *args, **kwargs):
|
|
|
"""
|
|
|
Same as :meth:`.Authomatic.access` but runs asynchronously in a
|
|
|
separate thread.
|
|
|
|
|
|
.. warning::
|
|
|
|
|
|
|async|
|
|
|
|
|
|
:returns:
|
|
|
:class:`.Future` instance representing the separate thread.
|
|
|
|
|
|
"""
|
|
|
|
|
|
return Future(self.access, *args, **kwargs)
|
|
|
|
|
|
def request_elements(
|
|
|
self, credentials=None, url=None, method='GET', params=None,
|
|
|
headers=None, body='', json_input=None, return_json=False
|
|
|
):
|
|
|
"""
|
|
|
Creates request elements for accessing **protected resource of a
|
|
|
user**. Required arguments are :data:`credentials` and :data:`url`. You
|
|
|
can pass :data:`credentials`, :data:`url`, :data:`method`, and
|
|
|
:data:`params` as a JSON object.
|
|
|
|
|
|
:param credentials:
|
|
|
The **user's** credentials (can be serialized).
|
|
|
|
|
|
:param str url:
|
|
|
The url of the protected resource.
|
|
|
|
|
|
:param str method:
|
|
|
The HTTP method of the request.
|
|
|
|
|
|
:param dict params:
|
|
|
Dictionary of request parameters.
|
|
|
|
|
|
:param dict headers:
|
|
|
Dictionary of request headers.
|
|
|
|
|
|
:param str body:
|
|
|
Body of ``POST``, ``PUT`` and ``PATCH`` requests.
|
|
|
|
|
|
:param str json_input:
|
|
|
you can pass :data:`credentials`, :data:`url`, :data:`method`,
|
|
|
:data:`params` and :data:`headers` in a JSON object.
|
|
|
Values from arguments will be used for missing properties.
|
|
|
|
|
|
::
|
|
|
|
|
|
{
|
|
|
"credentials": "###",
|
|
|
"url": "https://example.com/api",
|
|
|
"method": "POST",
|
|
|
"params": {
|
|
|
"foo": "bar"
|
|
|
},
|
|
|
"headers": {
|
|
|
"baz": "bing",
|
|
|
"Authorization": "Bearer ###"
|
|
|
},
|
|
|
"body": "Foo bar baz bing."
|
|
|
}
|
|
|
|
|
|
:param bool return_json:
|
|
|
if ``True`` the function returns a json object.
|
|
|
|
|
|
::
|
|
|
|
|
|
{
|
|
|
"url": "https://example.com/api",
|
|
|
"method": "POST",
|
|
|
"params": {
|
|
|
"access_token": "###",
|
|
|
"foo": "bar"
|
|
|
},
|
|
|
"headers": {
|
|
|
"baz": "bing",
|
|
|
"Authorization": "Bearer ###"
|
|
|
},
|
|
|
"body": "Foo bar baz bing."
|
|
|
}
|
|
|
|
|
|
:returns:
|
|
|
:class:`.RequestElements` or JSON string.
|
|
|
|
|
|
"""
|
|
|
|
|
|
# Parse values from JSON
|
|
|
if json_input:
|
|
|
parsed_input = json.loads(json_input)
|
|
|
|
|
|
credentials = parsed_input.get('credentials', credentials)
|
|
|
url = parsed_input.get('url', url)
|
|
|
method = parsed_input.get('method', method)
|
|
|
params = parsed_input.get('params', params)
|
|
|
headers = parsed_input.get('headers', headers)
|
|
|
body = parsed_input.get('body', body)
|
|
|
|
|
|
if not credentials and url:
|
|
|
raise RequestElementsError(
|
|
|
'To create request elements, you must provide credentials '
|
|
|
'and URL either as keyword arguments or in the JSON object!')
|
|
|
|
|
|
# Get the provider class
|
|
|
credentials = Credentials.deserialize(self.config, credentials)
|
|
|
ProviderClass = credentials.provider_class
|
|
|
|
|
|
# Create request elements
|
|
|
request_elements = ProviderClass.create_request_elements(
|
|
|
ProviderClass.PROTECTED_RESOURCE_REQUEST_TYPE,
|
|
|
credentials=credentials,
|
|
|
url=url,
|
|
|
method=method,
|
|
|
params=params,
|
|
|
headers=headers,
|
|
|
body=body)
|
|
|
|
|
|
if return_json:
|
|
|
return request_elements.to_json()
|
|
|
|
|
|
else:
|
|
|
return request_elements
|
|
|
|
|
|
def backend(self, adapter):
|
|
|
"""
|
|
|
Converts a *request handler* to a JSON backend which you can use with
|
|
|
:ref:`authomatic.js <js>`.
|
|
|
|
|
|
Just call it inside a *request handler* like this:
|
|
|
|
|
|
::
|
|
|
|
|
|
class JSONHandler(webapp2.RequestHandler):
|
|
|
def get(self):
|
|
|
authomatic.backend(Webapp2Adapter(self))
|
|
|
|
|
|
:param adapter:
|
|
|
The only argument is an :doc:`adapter <adapters>`.
|
|
|
|
|
|
The *request handler* will now accept these request parameters:
|
|
|
|
|
|
:param str type:
|
|
|
Type of the request. Either ``auto``, ``fetch`` or ``elements``.
|
|
|
Default is ``auto``.
|
|
|
|
|
|
:param str credentials:
|
|
|
Serialized :class:`.Credentials`.
|
|
|
|
|
|
:param str url:
|
|
|
URL of the **protected resource** request.
|
|
|
|
|
|
:param str method:
|
|
|
HTTP method of the **protected resource** request.
|
|
|
|
|
|
:param str body:
|
|
|
HTTP body of the **protected resource** request.
|
|
|
|
|
|
:param JSON params:
|
|
|
HTTP params of the **protected resource** request as a JSON object.
|
|
|
|
|
|
:param JSON headers:
|
|
|
HTTP headers of the **protected resource** request as a
|
|
|
JSON object.
|
|
|
|
|
|
:param JSON json:
|
|
|
You can pass all of the aforementioned params except ``type``
|
|
|
in a JSON object.
|
|
|
|
|
|
.. code-block:: javascript
|
|
|
|
|
|
{
|
|
|
"credentials": "######",
|
|
|
"url": "https://example.com",
|
|
|
"method": "POST",
|
|
|
"params": {"foo": "bar"},
|
|
|
"headers": {"baz": "bing"},
|
|
|
"body": "the body of the request"
|
|
|
}
|
|
|
|
|
|
Depending on the ``type`` param, the handler will either write
|
|
|
a JSON object with *request elements* to the response,
|
|
|
and add an ``Authomatic-Response-To: elements`` response header, ...
|
|
|
|
|
|
.. code-block:: javascript
|
|
|
|
|
|
{
|
|
|
"url": "https://example.com/api",
|
|
|
"method": "POST",
|
|
|
"params": {
|
|
|
"access_token": "###",
|
|
|
"foo": "bar"
|
|
|
},
|
|
|
"headers": {
|
|
|
"baz": "bing",
|
|
|
"Authorization": "Bearer ###"
|
|
|
}
|
|
|
}
|
|
|
|
|
|
... or make a fetch to the **protected resource** and forward
|
|
|
it's response content, status and headers with an additional
|
|
|
``Authomatic-Response-To: fetch`` header to the response.
|
|
|
|
|
|
.. warning::
|
|
|
|
|
|
The backend will not work if you write anything to the
|
|
|
response in the handler!
|
|
|
|
|
|
"""
|
|
|
|
|
|
AUTHOMATIC_HEADER = 'Authomatic-Response-To'
|
|
|
|
|
|
# Collect request params
|
|
|
request_type = adapter.params.get('type', 'auto')
|
|
|
json_input = adapter.params.get('json')
|
|
|
credentials = adapter.params.get('credentials')
|
|
|
url = adapter.params.get('url')
|
|
|
method = adapter.params.get('method', 'GET')
|
|
|
body = adapter.params.get('body', '')
|
|
|
|
|
|
params = adapter.params.get('params')
|
|
|
params = json.loads(params) if params else {}
|
|
|
|
|
|
headers = adapter.params.get('headers')
|
|
|
headers = json.loads(headers) if headers else {}
|
|
|
|
|
|
ProviderClass = Credentials.deserialize(
|
|
|
self.config, credentials).provider_class
|
|
|
|
|
|
if request_type == 'auto':
|
|
|
# If there is a "callback" param, it's a JSONP request.
|
|
|
jsonp = params.get('callback')
|
|
|
|
|
|
# JSONP is possible only with GET method.
|
|
|
if ProviderClass.supports_jsonp and method == 'GET':
|
|
|
request_type = 'elements'
|
|
|
else:
|
|
|
# Remove the JSONP callback
|
|
|
if jsonp:
|
|
|
params.pop('callback')
|
|
|
request_type = 'fetch'
|
|
|
|
|
|
if request_type == 'fetch':
|
|
|
# Access protected resource
|
|
|
response = self.access(
|
|
|
credentials, url, params, method, headers, body)
|
|
|
result = response.content
|
|
|
|
|
|
# Forward status
|
|
|
adapter.status = str(response.status) + ' ' + str(response.reason)
|
|
|
|
|
|
# Forward headers
|
|
|
for k, v in response.getheaders():
|
|
|
logging.info(' {0}: {1}'.format(k, v))
|
|
|
adapter.set_header(k, v)
|
|
|
|
|
|
elif request_type == 'elements':
|
|
|
# Create request elements
|
|
|
if json_input:
|
|
|
result = self.request_elements(
|
|
|
json_input=json_input, return_json=True)
|
|
|
else:
|
|
|
result = self.request_elements(credentials=credentials,
|
|
|
url=url,
|
|
|
method=method,
|
|
|
params=params,
|
|
|
headers=headers,
|
|
|
body=body,
|
|
|
return_json=True)
|
|
|
|
|
|
adapter.set_header('Content-Type', 'application/json')
|
|
|
else:
|
|
|
result = '{"error": "Bad Request!"}'
|
|
|
|
|
|
# Add the authomatic header
|
|
|
adapter.set_header(AUTHOMATIC_HEADER, request_type)
|
|
|
|
|
|
# Write result to response
|
|
|
adapter.write(result)
|
|
|
|