|
|
# -*- coding: utf-8 -*-
|
|
|
"""
|
|
|
Abstract Classes for Providers
|
|
|
------------------------------
|
|
|
|
|
|
Abstract base classes for implementation of protocol specific providers.
|
|
|
|
|
|
.. note::
|
|
|
|
|
|
Attributes prefixed with ``_x_`` serve the purpose of unification
|
|
|
of differences across providers.
|
|
|
|
|
|
.. autosummary::
|
|
|
|
|
|
login_decorator
|
|
|
BaseProvider
|
|
|
AuthorizationProvider
|
|
|
AuthenticationProvider
|
|
|
|
|
|
"""
|
|
|
|
|
|
import abc
|
|
|
import base64
|
|
|
import hashlib
|
|
|
import logging
|
|
|
import random
|
|
|
import sys
|
|
|
import traceback
|
|
|
import uuid
|
|
|
|
|
|
import authomatic.core
|
|
|
from authomatic.exceptions import (
|
|
|
ConfigError,
|
|
|
FetchError,
|
|
|
CredentialsError,
|
|
|
)
|
|
|
from authomatic import six
|
|
|
from authomatic.six.moves import urllib_parse as parse
|
|
|
from authomatic.six.moves import http_client
|
|
|
from authomatic.exceptions import CancellationError
|
|
|
|
|
|
__all__ = [
|
|
|
'BaseProvider',
|
|
|
'AuthorizationProvider',
|
|
|
'AuthenticationProvider',
|
|
|
'login_decorator']
|
|
|
|
|
|
|
|
|
def _error_traceback_html(exc_info, traceback_):
|
|
|
"""
|
|
|
Generates error traceback HTML.
|
|
|
|
|
|
:param tuple exc_info:
|
|
|
Output of :func:`sys.exc_info` function.
|
|
|
|
|
|
:param traceback:
|
|
|
Output of :func:`traceback.format_exc` function.
|
|
|
|
|
|
"""
|
|
|
|
|
|
html = """
|
|
|
<html>
|
|
|
<head>
|
|
|
<title>ERROR: {error}</title>
|
|
|
</head>
|
|
|
<body style="font-family: sans-serif">
|
|
|
<h4>The Authomatic library encountered an error!</h4>
|
|
|
<h1>{error}</h1>
|
|
|
<pre>{traceback}</pre>
|
|
|
</body>
|
|
|
</html>
|
|
|
"""
|
|
|
|
|
|
return html.format(error=exc_info[1], traceback=traceback_)
|
|
|
|
|
|
|
|
|
def login_decorator(func):
|
|
|
"""
|
|
|
Decorate the :meth:`.BaseProvider.login` implementations with this
|
|
|
decorator.
|
|
|
|
|
|
Provides mechanism for error reporting and returning result which
|
|
|
makes the :meth:`.BaseProvider.login` implementation cleaner.
|
|
|
|
|
|
"""
|
|
|
|
|
|
def wrap(provider, *args, **kwargs):
|
|
|
error = None
|
|
|
result = authomatic.core.LoginResult(provider)
|
|
|
|
|
|
try:
|
|
|
func(provider, *args, **kwargs)
|
|
|
except Exception as e: # pylint:disable=broad-except
|
|
|
if provider.settings.report_errors:
|
|
|
error = e
|
|
|
if not isinstance(error, CancellationError):
|
|
|
provider._log(
|
|
|
logging.ERROR,
|
|
|
u'Reported suppressed exception: {0}!'.format(
|
|
|
repr(error)),
|
|
|
exc_info=1)
|
|
|
else:
|
|
|
if provider.settings.debug:
|
|
|
# TODO: Check whether it actually works without middleware
|
|
|
provider.write(
|
|
|
_error_traceback_html(
|
|
|
sys.exc_info(),
|
|
|
traceback.format_exc()))
|
|
|
raise
|
|
|
|
|
|
# If there is user or error the login procedure has finished
|
|
|
if provider.user or error:
|
|
|
result = authomatic.core.LoginResult(provider)
|
|
|
# Add error to result
|
|
|
result.error = error
|
|
|
|
|
|
# delete session cookie
|
|
|
if isinstance(provider.session, authomatic.core.Session):
|
|
|
provider.session.delete()
|
|
|
|
|
|
provider._log(logging.INFO, u'Procedure finished.')
|
|
|
|
|
|
if provider.callback:
|
|
|
provider.callback(result)
|
|
|
return result
|
|
|
else:
|
|
|
# Save session
|
|
|
provider.save_session()
|
|
|
|
|
|
return wrap
|
|
|
|
|
|
|
|
|
class BaseProvider(object):
|
|
|
"""
|
|
|
Abstract base class for all providers.
|
|
|
"""
|
|
|
|
|
|
PROVIDER_TYPE_ID = 0
|
|
|
|
|
|
_repr_ignore = ('user',)
|
|
|
|
|
|
__metaclass__ = abc.ABCMeta
|
|
|
|
|
|
supported_user_attributes = authomatic.core.SupportedUserAttributes()
|
|
|
|
|
|
def __init__(self, settings, adapter, provider_name, session=None,
|
|
|
session_saver=None, callback=None, js_callback=None,
|
|
|
prefix='authomatic', **kwargs):
|
|
|
|
|
|
self.settings = settings
|
|
|
self.adapter = adapter
|
|
|
|
|
|
self.session = session
|
|
|
self.save_session = session_saver
|
|
|
|
|
|
#: :class:`str` The provider name as specified in the :doc:`config`.
|
|
|
self.name = provider_name
|
|
|
|
|
|
#: :class:`callable` An optional callback called when the login
|
|
|
#: procedure is finished with :class:`.core.LoginResult` passed as
|
|
|
#: argument.
|
|
|
self.callback = callback
|
|
|
|
|
|
#: :class:`str` Name of an optional javascript callback.
|
|
|
self.js_callback = js_callback
|
|
|
|
|
|
#: :class:`.core.User`.
|
|
|
self.user = None
|
|
|
|
|
|
#: :class:`bool` If ``True``, the
|
|
|
#: :attr:`.BaseProvider.user_authorization_url` will be displayed
|
|
|
#: in a *popup mode*, if the **provider** supports it.
|
|
|
self.popup = self._kwarg(kwargs, 'popup')
|
|
|
|
|
|
@property
|
|
|
def url(self):
|
|
|
return self.adapter.url
|
|
|
|
|
|
@property
|
|
|
def params(self):
|
|
|
return self.adapter.params
|
|
|
|
|
|
def write(self, value):
|
|
|
self.adapter.write(value)
|
|
|
|
|
|
def set_header(self, key, value):
|
|
|
self.adapter.set_header(key, value)
|
|
|
|
|
|
def set_status(self, status):
|
|
|
self.adapter.set_status(status)
|
|
|
|
|
|
def redirect(self, url):
|
|
|
self.set_status('302 Found')
|
|
|
self.set_header('Location', url)
|
|
|
|
|
|
# ========================================================================
|
|
|
# Abstract methods
|
|
|
# ========================================================================
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
def login(self):
|
|
|
"""
|
|
|
Launches the *login procedure* to get **user's credentials** from
|
|
|
**provider**.
|
|
|
|
|
|
Should be decorated with :func:`.login_decorator`. The *login
|
|
|
procedure* is considered finished when the :attr:`.user`
|
|
|
attribute is not empty when the method runs out of it's flow or
|
|
|
when there are errors.
|
|
|
|
|
|
"""
|
|
|
|
|
|
# ========================================================================
|
|
|
# Exposed methods
|
|
|
# ========================================================================
|
|
|
|
|
|
def to_dict(self):
|
|
|
"""
|
|
|
Converts the provider instance to a :class:`dict`.
|
|
|
|
|
|
:returns:
|
|
|
:class:`dict`
|
|
|
|
|
|
"""
|
|
|
|
|
|
return dict(name=self.name,
|
|
|
id=getattr(self, 'id', None),
|
|
|
type_id=self.type_id,
|
|
|
type=self.get_type(),
|
|
|
scope=getattr(self, 'scope', None),
|
|
|
user=self.user.id if self.user else None)
|
|
|
|
|
|
@classmethod
|
|
|
def get_type(cls):
|
|
|
"""
|
|
|
Returns the provider type.
|
|
|
|
|
|
:returns:
|
|
|
:class:`str` The full dotted path to base class e.g.
|
|
|
:literal:`"authomatic.providers.oauth2.OAuth2"`.
|
|
|
|
|
|
"""
|
|
|
|
|
|
return cls.__module__ + '.' + cls.__bases__[0].__name__
|
|
|
|
|
|
def update_user(self):
|
|
|
"""
|
|
|
Updates and returns :attr:`.user`.
|
|
|
|
|
|
:returns:
|
|
|
:class:`.User`
|
|
|
|
|
|
"""
|
|
|
|
|
|
# ========================================================================
|
|
|
# Internal methods
|
|
|
# ========================================================================
|
|
|
|
|
|
@property
|
|
|
def type_id(self):
|
|
|
pass
|
|
|
|
|
|
def _kwarg(self, kwargs, kwname, default=None):
|
|
|
"""
|
|
|
Resolves keyword arguments from constructor or :doc:`config`.
|
|
|
|
|
|
.. note::
|
|
|
|
|
|
The keyword arguments take this order of precedence:
|
|
|
|
|
|
1. Arguments passed to constructor through the
|
|
|
:func:`authomatic.login`.
|
|
|
2. Provider specific arguments from :doc:`config`.
|
|
|
3. Arguments from :doc:`config` set in the ``__defaults__`` key.
|
|
|
2. The value from :data:`default` argument.
|
|
|
|
|
|
:param dict kwargs:
|
|
|
Keyword arguments dictionary.
|
|
|
:param str kwname:
|
|
|
Name of the desired keyword argument.
|
|
|
|
|
|
"""
|
|
|
|
|
|
return kwargs.get(kwname) or \
|
|
|
self.settings.config.get(self.name, {}).get(kwname) or \
|
|
|
self.settings.config.get('__defaults__', {}).get(kwname) or \
|
|
|
default
|
|
|
|
|
|
def _session_key(self, key):
|
|
|
"""
|
|
|
Generates session key string.
|
|
|
|
|
|
:param str key:
|
|
|
e.g. ``"authomatic:facebook:key"``
|
|
|
|
|
|
"""
|
|
|
|
|
|
return '{0}:{1}:{2}'.format(self.settings.prefix, self.name, key)
|
|
|
|
|
|
def _session_set(self, key, value):
|
|
|
"""
|
|
|
Saves a value to session.
|
|
|
"""
|
|
|
|
|
|
self.session[self._session_key(key)] = value
|
|
|
|
|
|
def _session_get(self, key):
|
|
|
"""
|
|
|
Retrieves a value from session.
|
|
|
"""
|
|
|
|
|
|
return self.session.get(self._session_key(key))
|
|
|
|
|
|
@staticmethod
|
|
|
def csrf_generator(secret):
|
|
|
"""
|
|
|
Generates CSRF token.
|
|
|
|
|
|
Inspired by this article:
|
|
|
http://blog.ptsecurity.com/2012/10/random-number-security-in-python.html
|
|
|
|
|
|
:returns:
|
|
|
:class:`str` Random unguessable string.
|
|
|
|
|
|
"""
|
|
|
|
|
|
# Create hash from random string plus salt.
|
|
|
hashed = hashlib.md5(uuid.uuid4().bytes + six.b(secret)).hexdigest()
|
|
|
|
|
|
# Each time return random portion of the hash.
|
|
|
span = 5
|
|
|
shift = random.randint(0, span)
|
|
|
return hashed[shift:shift - span - 1]
|
|
|
|
|
|
@classmethod
|
|
|
def _log(cls, level, msg, **kwargs):
|
|
|
"""
|
|
|
Logs a message with pre-formatted prefix.
|
|
|
|
|
|
:param int level:
|
|
|
Logging level as specified in the
|
|
|
`login module <http://docs.python.org/2/library/logging.html>`_ of
|
|
|
Python standard library.
|
|
|
|
|
|
:param str msg:
|
|
|
The actual message.
|
|
|
|
|
|
"""
|
|
|
|
|
|
logger = getattr(cls, '_logger', None) or authomatic.core._logger
|
|
|
logger.log(
|
|
|
level, ': '.join(
|
|
|
('authomatic', cls.__name__, msg)), **kwargs)
|
|
|
|
|
|
def _fetch(self, url, method='GET', params=None, headers=None,
|
|
|
body='', max_redirects=5, content_parser=None):
|
|
|
"""
|
|
|
Fetches a URL.
|
|
|
|
|
|
:param str url:
|
|
|
The URL to fetch.
|
|
|
|
|
|
:param str method:
|
|
|
HTTP method of the request.
|
|
|
|
|
|
:param dict params:
|
|
|
Dictionary of request parameters.
|
|
|
|
|
|
:param dict headers:
|
|
|
HTTP headers of the request.
|
|
|
|
|
|
:param str body:
|
|
|
Body of ``POST``, ``PUT`` and ``PATCH`` requests.
|
|
|
|
|
|
:param int max_redirects:
|
|
|
Number of maximum HTTP redirects to follow.
|
|
|
|
|
|
:param function content_parser:
|
|
|
A callable to be used to parse the :attr:`.Response.data`
|
|
|
from :attr:`.Response.content`.
|
|
|
|
|
|
"""
|
|
|
# 'magic' using _kwarg method
|
|
|
# pylint:disable=no-member
|
|
|
params = params or {}
|
|
|
params.update(self.access_params)
|
|
|
|
|
|
headers = headers or {}
|
|
|
headers.update(self.access_headers)
|
|
|
|
|
|
scheme, host, path, query, fragment = parse.urlsplit(url)
|
|
|
query = parse.urlencode(params)
|
|
|
|
|
|
if method in ('POST', 'PUT', 'PATCH'):
|
|
|
if not body:
|
|
|
# Put querystring to body
|
|
|
body = query
|
|
|
query = ''
|
|
|
headers.update(
|
|
|
{'Content-Type': 'application/x-www-form-urlencoded'})
|
|
|
request_path = parse.urlunsplit(('', '', path or '', query or '', ''))
|
|
|
|
|
|
self._log(logging.DEBUG, u' \u251C\u2500 host: {0}'.format(host))
|
|
|
self._log(
|
|
|
logging.DEBUG,
|
|
|
u' \u251C\u2500 path: {0}'.format(request_path))
|
|
|
self._log(logging.DEBUG, u' \u251C\u2500 method: {0}'.format(method))
|
|
|
self._log(logging.DEBUG, u' \u251C\u2500 body: {0}'.format(body))
|
|
|
self._log(logging.DEBUG, u' \u251C\u2500 params: {0}'.format(params))
|
|
|
self._log(logging.DEBUG, u' \u2514\u2500 headers: {0}'.format(headers))
|
|
|
|
|
|
# Connect
|
|
|
if scheme.lower() == 'https':
|
|
|
connection = http_client.HTTPSConnection(host)
|
|
|
else:
|
|
|
connection = http_client.HTTPConnection(host)
|
|
|
|
|
|
try:
|
|
|
connection.request(method, request_path, body, headers)
|
|
|
except Exception as e:
|
|
|
raise FetchError('Fetching URL failed',
|
|
|
original_message=str(e),
|
|
|
url=request_path)
|
|
|
|
|
|
response = connection.getresponse()
|
|
|
location = response.getheader('Location')
|
|
|
|
|
|
if response.status in (300, 301, 302, 303, 307) and location:
|
|
|
if location == url:
|
|
|
raise FetchError('Url redirects to itself!',
|
|
|
url=location,
|
|
|
status=response.status)
|
|
|
|
|
|
elif max_redirects > 0:
|
|
|
remaining_redirects = max_redirects - 1
|
|
|
|
|
|
self._log(logging.DEBUG, u'Redirecting to {0}'.format(url))
|
|
|
self._log(logging.DEBUG, u'Remaining redirects: {0}'
|
|
|
.format(remaining_redirects))
|
|
|
|
|
|
# Call this method again.
|
|
|
response = self._fetch(url=location,
|
|
|
params=params,
|
|
|
method=method,
|
|
|
headers=headers,
|
|
|
max_redirects=remaining_redirects)
|
|
|
|
|
|
else:
|
|
|
raise FetchError('Max redirects reached!',
|
|
|
url=location,
|
|
|
status=response.status)
|
|
|
else:
|
|
|
self._log(logging.DEBUG, u'Got response:')
|
|
|
self._log(logging.DEBUG, u' \u251C\u2500 url: {0}'.format(url))
|
|
|
self._log(
|
|
|
logging.DEBUG,
|
|
|
u' \u251C\u2500 status: {0}'.format(
|
|
|
response.status))
|
|
|
self._log(
|
|
|
logging.DEBUG,
|
|
|
u' \u2514\u2500 headers: {0}'.format(
|
|
|
response.getheaders()))
|
|
|
|
|
|
return authomatic.core.Response(response, content_parser)
|
|
|
|
|
|
def _update_or_create_user(self, data, credentials=None, content=None):
|
|
|
"""
|
|
|
Updates or creates :attr:`.user`.
|
|
|
|
|
|
:returns:
|
|
|
:class:`.User`
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not self.user:
|
|
|
self.user = authomatic.core.User(self, credentials=credentials)
|
|
|
|
|
|
self.user.content = content
|
|
|
self.user.data = data
|
|
|
|
|
|
# Update.
|
|
|
for key in self.user.__dict__:
|
|
|
# Exclude data.
|
|
|
if key not in ('data', 'content'):
|
|
|
# Extract every data item whose key matches the user
|
|
|
# property name, but only if it has a value.
|
|
|
value = data.get(key)
|
|
|
if value:
|
|
|
setattr(self.user, key, value)
|
|
|
|
|
|
# Handle different structure of data by different providers.
|
|
|
self.user = self._x_user_parser(self.user, data)
|
|
|
|
|
|
if self.user.id:
|
|
|
self.user.id = str(self.user.id)
|
|
|
|
|
|
# TODO: Move to User
|
|
|
# If there is no user.name,
|
|
|
if not self.user.name:
|
|
|
if self.user.first_name and self.user.last_name:
|
|
|
# Create it from first name and last name if available.
|
|
|
self.user.name = ' '.join((self.user.first_name,
|
|
|
self.user.last_name))
|
|
|
else:
|
|
|
# Or use one of these.
|
|
|
self.user.name = (self.user.username or self.user.nickname or
|
|
|
self.user.first_name or self.user.last_name)
|
|
|
|
|
|
if not self.user.location:
|
|
|
if self.user.city and self.user.country:
|
|
|
self.user.location = '{0}, {1}'.format(self.user.city,
|
|
|
self.user.country)
|
|
|
else:
|
|
|
self.user.location = self.user.city or self.user.country
|
|
|
|
|
|
return self.user
|
|
|
|
|
|
@staticmethod
|
|
|
def _x_user_parser(user, data):
|
|
|
"""
|
|
|
Handles different structure of user info data by different providers.
|
|
|
|
|
|
:param user:
|
|
|
:class:`.User`
|
|
|
:param dict data:
|
|
|
User info data returned by provider.
|
|
|
|
|
|
"""
|
|
|
|
|
|
return user
|
|
|
|
|
|
@staticmethod
|
|
|
def _http_status_in_category(status, category):
|
|
|
"""
|
|
|
Checks whether a HTTP status code is in the category denoted by the
|
|
|
hundreds digit.
|
|
|
"""
|
|
|
|
|
|
assert category < 10, 'HTTP status category must be a one-digit int!'
|
|
|
cat = category * 100
|
|
|
return status >= cat and status < cat + 100
|
|
|
|
|
|
|
|
|
class AuthorizationProvider(BaseProvider):
|
|
|
"""
|
|
|
Base provider for *authorization protocols* i.e. protocols which allow a
|
|
|
**provider** to authorize a **consumer** to access **protected resources**
|
|
|
of a **user**.
|
|
|
|
|
|
e.g. `OAuth 2.0 <http://oauth.net/2/>`_ or `OAuth 1.0a
|
|
|
<http://oauth.net/core/1.0a/>`_.
|
|
|
|
|
|
"""
|
|
|
|
|
|
USER_AUTHORIZATION_REQUEST_TYPE = 2
|
|
|
ACCESS_TOKEN_REQUEST_TYPE = 3
|
|
|
PROTECTED_RESOURCE_REQUEST_TYPE = 4
|
|
|
REFRESH_TOKEN_REQUEST_TYPE = 5
|
|
|
|
|
|
BEARER = 'Bearer'
|
|
|
|
|
|
_x_term_dict = {}
|
|
|
|
|
|
#: If ``True`` the provider doesn't support Cross-site HTTP requests.
|
|
|
same_origin = True
|
|
|
|
|
|
#: :class:`bool` Whether the provider supports JSONP requests.
|
|
|
supports_jsonp = False
|
|
|
|
|
|
# Whether to use the HTTP Authorization header.
|
|
|
_x_use_authorization_header = True
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
"""
|
|
|
Accepts additional keyword arguments:
|
|
|
|
|
|
:arg str consumer_key:
|
|
|
The *key* assigned to our application (**consumer**) by the
|
|
|
**provider**.
|
|
|
|
|
|
:arg str consumer_secret:
|
|
|
The *secret* assigned to our application (**consumer**) by the
|
|
|
**provider**.
|
|
|
|
|
|
:arg int id:
|
|
|
A unique numeric ID used to serialize :class:`.Credentials`.
|
|
|
|
|
|
:arg dict user_authorization_params:
|
|
|
A dictionary of additional request parameters for
|
|
|
**user authorization request**.
|
|
|
|
|
|
:arg dict access_token_params:
|
|
|
A dictionary of additional request parameters for
|
|
|
**access_with_credentials token request**.
|
|
|
|
|
|
:arg dict access_headers:
|
|
|
A dictionary of default HTTP headers that will be used when
|
|
|
accessing **user's** protected resources.
|
|
|
Applied by :meth:`.access()`, :meth:`.update_user()` and
|
|
|
:meth:`.User.update()`
|
|
|
|
|
|
:arg dict access_params:
|
|
|
A dictionary of default query string parameters that will be used
|
|
|
when accessing **user's** protected resources.
|
|
|
Applied by :meth:`.access()`, :meth:`.update_user()` and
|
|
|
:meth:`.User.update()`
|
|
|
|
|
|
"""
|
|
|
|
|
|
super(AuthorizationProvider, self).__init__(*args, **kwargs)
|
|
|
|
|
|
self.consumer_key = self._kwarg(kwargs, 'consumer_key')
|
|
|
self.consumer_secret = self._kwarg(kwargs, 'consumer_secret')
|
|
|
|
|
|
self.user_authorization_params = self._kwarg(
|
|
|
kwargs, 'user_authorization_params', {})
|
|
|
|
|
|
self.access_token_headers = self._kwarg(
|
|
|
kwargs, 'user_authorization_headers', {})
|
|
|
self.access_token_params = self._kwarg(
|
|
|
kwargs, 'access_token_params', {})
|
|
|
|
|
|
self.id = self._kwarg(kwargs, 'id')
|
|
|
|
|
|
self.access_headers = self._kwarg(kwargs, 'access_headers', {})
|
|
|
self.access_params = self._kwarg(kwargs, 'access_params', {})
|
|
|
|
|
|
#: :class:`.Credentials` to access **user's protected resources**.
|
|
|
self.credentials = authomatic.core.Credentials(
|
|
|
self.settings.config, provider=self)
|
|
|
|
|
|
#: Response of the *access token request*.
|
|
|
self.access_token_response = None
|
|
|
|
|
|
# ========================================================================
|
|
|
# Abstract properties
|
|
|
# ========================================================================
|
|
|
|
|
|
@abc.abstractproperty
|
|
|
def user_authorization_url(self):
|
|
|
"""
|
|
|
:class:`str` URL to which we redirect the **user** to grant our app
|
|
|
i.e. the **consumer** an **authorization** to access his
|
|
|
**protected resources**. See
|
|
|
http://tools.ietf.org/html/rfc6749#section-4.1.1 and
|
|
|
http://oauth.net/core/1.0a/#auth_step2.
|
|
|
"""
|
|
|
|
|
|
@abc.abstractproperty
|
|
|
def access_token_url(self):
|
|
|
"""
|
|
|
:class:`str` URL where we can get the *access token* to access
|
|
|
**protected resources** of a **user**. See
|
|
|
http://tools.ietf.org/html/rfc6749#section-4.1.3 and
|
|
|
http://oauth.net/core/1.0a/#auth_step3.
|
|
|
"""
|
|
|
|
|
|
@abc.abstractproperty
|
|
|
def user_info_url(self):
|
|
|
"""
|
|
|
:class:`str` URL where we can get the **user** info.
|
|
|
see http://tools.ietf.org/html/rfc6749#section-7 and
|
|
|
http://oauth.net/core/1.0a/#anchor12.
|
|
|
"""
|
|
|
|
|
|
# ========================================================================
|
|
|
# Abstract methods
|
|
|
# ========================================================================
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
def to_tuple(self, credentials):
|
|
|
"""
|
|
|
Must convert :data:`credentials` to a :class:`tuple` to be used by
|
|
|
:meth:`.Credentials.serialize`.
|
|
|
|
|
|
.. warning::
|
|
|
|
|
|
|classmethod|
|
|
|
|
|
|
:param credentials:
|
|
|
:class:`.Credentials`
|
|
|
|
|
|
:returns:
|
|
|
:class:`tuple`
|
|
|
|
|
|
"""
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
def reconstruct(self, deserialized_tuple, credentials, cfg):
|
|
|
"""
|
|
|
Must convert the :data:`deserialized_tuple` back to
|
|
|
:class:`.Credentials`.
|
|
|
|
|
|
.. warning::
|
|
|
|
|
|
|classmethod|
|
|
|
|
|
|
:param tuple deserialized_tuple:
|
|
|
A tuple whose first index is the :attr:`.id` and the rest
|
|
|
are all the items of the :class:`tuple` created by
|
|
|
:meth:`.to_tuple`.
|
|
|
|
|
|
:param credentials:
|
|
|
A :class:`.Credentials` instance.
|
|
|
|
|
|
:param dict cfg:
|
|
|
Provider configuration from :doc:`config`.
|
|
|
|
|
|
"""
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
def create_request_elements(self, request_type, credentials,
|
|
|
url, method='GET', params=None, headers=None,
|
|
|
body=''):
|
|
|
"""
|
|
|
Must return :class:`.RequestElements`.
|
|
|
|
|
|
.. warning::
|
|
|
|
|
|
|classmethod|
|
|
|
|
|
|
:param int request_type:
|
|
|
Type of the request specified by one of the class's constants.
|
|
|
|
|
|
:param credentials:
|
|
|
:class:`.Credentials` of the **user** whose
|
|
|
**protected resource** we want to access.
|
|
|
|
|
|
:param str url:
|
|
|
URL of the request.
|
|
|
|
|
|
:param str method:
|
|
|
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.
|
|
|
|
|
|
:returns:
|
|
|
:class:`.RequestElements`
|
|
|
|
|
|
"""
|
|
|
|
|
|
# ========================================================================
|
|
|
# Exposed methods
|
|
|
# ========================================================================
|
|
|
|
|
|
@property
|
|
|
def type_id(self):
|
|
|
"""
|
|
|
A short string representing the provider implementation id used for
|
|
|
serialization of :class:`.Credentials` and to identify the type of
|
|
|
provider in JavaScript.
|
|
|
|
|
|
The part before hyphen denotes the type of the provider, the part
|
|
|
after hyphen denotes the class id e.g.
|
|
|
``oauth2.Facebook.type_id = '2-5'``,
|
|
|
``oauth1.Twitter.type_id = '1-5'``.
|
|
|
|
|
|
"""
|
|
|
|
|
|
cls = self.__class__
|
|
|
mod = sys.modules.get(cls.__module__)
|
|
|
|
|
|
return str(self.PROVIDER_TYPE_ID) + '-' + \
|
|
|
str(mod.PROVIDER_ID_MAP.index(cls))
|
|
|
|
|
|
def access(self, url, params=None, method='GET', headers=None,
|
|
|
body='', max_redirects=5, content_parser=None):
|
|
|
"""
|
|
|
Fetches the **protected resource** of an authenticated **user**.
|
|
|
|
|
|
:param credentials:
|
|
|
The **user's** :class:`.Credentials` (serialized or normal).
|
|
|
|
|
|
:param str url:
|
|
|
The URL of the **protected resource**.
|
|
|
|
|
|
: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`
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not self.user and not self.credentials:
|
|
|
raise CredentialsError(u'There is no authenticated user!')
|
|
|
|
|
|
headers = headers or {}
|
|
|
|
|
|
self._log(
|
|
|
logging.INFO,
|
|
|
u'Accessing protected resource {0}.'.format(url))
|
|
|
|
|
|
request_elements = self.create_request_elements(
|
|
|
request_type=self.PROTECTED_RESOURCE_REQUEST_TYPE,
|
|
|
credentials=self.credentials,
|
|
|
url=url,
|
|
|
body=body,
|
|
|
params=params,
|
|
|
headers=headers,
|
|
|
method=method
|
|
|
)
|
|
|
|
|
|
response = self._fetch(*request_elements,
|
|
|
max_redirects=max_redirects,
|
|
|
content_parser=content_parser)
|
|
|
|
|
|
self._log(
|
|
|
logging.INFO,
|
|
|
u'Got response. HTTP status = {0}.'.format(
|
|
|
response.status))
|
|
|
return response
|
|
|
|
|
|
def async_access(self, *args, **kwargs):
|
|
|
"""
|
|
|
Same as :meth:`.access` but runs asynchronously in a separate thread.
|
|
|
|
|
|
.. warning::
|
|
|
|
|
|
|async|
|
|
|
|
|
|
:returns:
|
|
|
:class:`.Future` instance representing the separate thread.
|
|
|
|
|
|
"""
|
|
|
|
|
|
return authomatic.core.Future(self.access, *args, **kwargs)
|
|
|
|
|
|
def update_user(self):
|
|
|
"""
|
|
|
Updates the :attr:`.BaseProvider.user`.
|
|
|
|
|
|
.. warning::
|
|
|
Fetches the :attr:`.user_info_url`!
|
|
|
|
|
|
:returns:
|
|
|
:class:`.UserInfoResponse`
|
|
|
|
|
|
"""
|
|
|
if self.user_info_url:
|
|
|
response = self._access_user_info()
|
|
|
self.user = self._update_or_create_user(response.data,
|
|
|
content=response.content)
|
|
|
return authomatic.core.UserInfoResponse(self.user,
|
|
|
response.httplib_response)
|
|
|
|
|
|
# ========================================================================
|
|
|
# Internal methods
|
|
|
# ========================================================================
|
|
|
|
|
|
@classmethod
|
|
|
def _authorization_header(cls, credentials):
|
|
|
"""
|
|
|
Creates authorization headers if the provider supports it. See:
|
|
|
http://en.wikipedia.org/wiki/Basic_access_authentication.
|
|
|
|
|
|
:param credentials:
|
|
|
:class:`.Credentials`
|
|
|
|
|
|
:returns:
|
|
|
Headers as :class:`dict`.
|
|
|
|
|
|
"""
|
|
|
|
|
|
if cls._x_use_authorization_header:
|
|
|
res = ':'.join(
|
|
|
(credentials.consumer_key,
|
|
|
credentials.consumer_secret))
|
|
|
res = base64.b64encode(six.b(res)).decode()
|
|
|
return {'Authorization': 'Basic {0}'.format(res)}
|
|
|
else:
|
|
|
return {}
|
|
|
|
|
|
def _check_consumer(self):
|
|
|
"""
|
|
|
Validates the :attr:`.consumer`.
|
|
|
"""
|
|
|
|
|
|
# 'magic' using _kwarg method
|
|
|
# pylint:disable=no-member
|
|
|
if not self.consumer.key:
|
|
|
raise ConfigError(
|
|
|
'Consumer key not specified for provider {0}!'.format(
|
|
|
self.name))
|
|
|
|
|
|
if not self.consumer.secret:
|
|
|
raise ConfigError(
|
|
|
'Consumer secret not specified for provider {0}!'.format(
|
|
|
self.name))
|
|
|
|
|
|
@staticmethod
|
|
|
def _split_url(url):
|
|
|
"""
|
|
|
Splits given url to url base and params converted to list of tuples.
|
|
|
"""
|
|
|
|
|
|
split = parse.urlsplit(url)
|
|
|
base = parse.urlunsplit((split.scheme, split.netloc, split.path, 0, 0))
|
|
|
params = parse.parse_qsl(split.query, True)
|
|
|
|
|
|
return base, params
|
|
|
|
|
|
@classmethod
|
|
|
def _x_request_elements_filter(
|
|
|
cls, request_type, request_elements, credentials):
|
|
|
"""
|
|
|
Override this to handle special request requirements of zealous
|
|
|
providers.
|
|
|
|
|
|
.. warning::
|
|
|
|
|
|
|classmethod|
|
|
|
|
|
|
:param int request_type:
|
|
|
Type of request.
|
|
|
|
|
|
:param request_elements:
|
|
|
:class:`.RequestElements`
|
|
|
|
|
|
:param credentials:
|
|
|
:class:`.Credentials`
|
|
|
|
|
|
:returns:
|
|
|
:class:`.RequestElements`
|
|
|
|
|
|
"""
|
|
|
|
|
|
return request_elements
|
|
|
|
|
|
@staticmethod
|
|
|
def _x_credentials_parser(credentials, data):
|
|
|
"""
|
|
|
Override this to handle differences in naming conventions across
|
|
|
providers.
|
|
|
|
|
|
:param credentials:
|
|
|
:class:`.Credentials`
|
|
|
|
|
|
:param dict data:
|
|
|
Response data dictionary.
|
|
|
|
|
|
:returns:
|
|
|
:class:`.Credentials`
|
|
|
|
|
|
"""
|
|
|
return credentials
|
|
|
|
|
|
def _access_user_info(self):
|
|
|
"""
|
|
|
Accesses the :attr:`.user_info_url`.
|
|
|
|
|
|
:returns:
|
|
|
:class:`.UserInfoResponse`
|
|
|
|
|
|
"""
|
|
|
url = self.user_info_url.format(**self.user.__dict__)
|
|
|
return self.access(url)
|
|
|
|
|
|
|
|
|
class AuthenticationProvider(BaseProvider):
|
|
|
"""
|
|
|
Base provider for *authentication protocols* i.e. protocols which allow a
|
|
|
**provider** to authenticate a *claimed identity* of a **user**.
|
|
|
|
|
|
e.g. `OpenID <http://openid.net/>`_.
|
|
|
|
|
|
"""
|
|
|
|
|
|
#: Indicates whether the **provider** supports access_with_credentials to
|
|
|
#: **user's** protected resources.
|
|
|
# TODO: Useless
|
|
|
has_protected_resources = False
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
super(AuthenticationProvider, self).__init__(*args, **kwargs)
|
|
|
|
|
|
# Lookup default identifier, if available in provider
|
|
|
default_identifier = getattr(self, 'identifier', None)
|
|
|
|
|
|
# Allow for custom name for the "id" querystring parameter.
|
|
|
self.identifier_param = kwargs.get('identifier_param', 'id')
|
|
|
|
|
|
# Get the identifier from request params, or use default as fallback.
|
|
|
self.identifier = self.params.get(
|
|
|
self.identifier_param, default_identifier)
|
|
|
|
|
|
|
|
|
PROVIDER_ID_MAP = [
|
|
|
AuthenticationProvider,
|
|
|
AuthorizationProvider,
|
|
|
BaseProvider,
|
|
|
]
|
|
|
|