Source code for authomatic.providers

# -*- 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 copy
import datetime
import hashlib
import httplib
import logging
import os
import random
import sys
import traceback
import urllib
import urlparse
import uuid

import authomatic.core
from authomatic.core import Session
from authomatic.exceptions import ConfigError, AuthenticationError, FetchError


__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)


[docs]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: if provider.settings.report_errors: error = e provider._log(logging.ERROR, 'Reported suppressed exception: {}!'.format(repr(error))) 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, 'Procedure finished.') if provider.callback: provider.callback(result) return result else: # Save session provider.save_session() return wrap
[docs]class BaseProvider(object): """ Abstract base class for all providers. """ PROVIDER_TYPE_ID = 0 _repr_ignore = ('user',) __metaclass__ = abc.ABCMeta 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') ############################################### # From Middleware ############################################### @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
[docs] 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 #===========================================================================
[docs] def to_dict(self): """ Converts the provider instance to a :class:`dict`. :returns: :class:`dict` """ return dict(name=self.name, id=self.id if hasattr(self, 'id') else None, type_id=self.type_id, type=self.get_type(), scope=self.scope if hasattr(self, 'scope') else None, user=self.user.id if self.user else None)
@classmethod
[docs] 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__
[docs] 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 '{}:{}:{}'.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
[docs] 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(str(uuid.uuid4()) + str(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): """ 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. """ authomatic.core._logger.log(level, ': '.join(('authomatic', cls.__name__, msg))) @classmethod def _fetch(cls, 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`. """ params = params or {} headers = headers or {} scheme, host, path, query, fragment = urlparse.urlsplit(url) query = urllib.urlencode(params) if method in ('POST', 'PUT', 'PATCH'): if not body: # Put querystring to body body = query query = None headers.update({'Content-Type': 'application/x-www-form-urlencoded'}) request_path = urlparse.urlunsplit((None, None, path, query, None)) cls._log(logging.DEBUG, u' \u251C\u2500 host: {}'.format(host)) cls._log(logging.DEBUG, u' \u251C\u2500 path: {}'.format(request_path)) cls._log(logging.DEBUG, u' \u251C\u2500 method: {}'.format(method)) cls._log(logging.DEBUG, u' \u251C\u2500 body: {}'.format(body)) cls._log(logging.DEBUG, u' \u251C\u2500 params: {}'.format(params)) cls._log(logging.DEBUG, u' \u2514\u2500 headers: {}'.format(headers)) # Connect if scheme.lower() == 'https': connection = httplib.HTTPSConnection(host) else: connection = httplib.HTTPConnection(host) try: connection.request(method, request_path, body, headers) except Exception as e: raise FetchError('Could not connect!', original_message=e.message, 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 cls._log(logging.DEBUG, 'Redirecting to {}'.format(url)) cls._log(logging.DEBUG, 'Remaining redirects: '.format(remaining_redirects)) # Call this method again. response = cls._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: cls._log(logging.DEBUG, u'Got response:') cls._log(logging.DEBUG, u' \u251C\u2500 url: {}'.format(url)) cls._log(logging.DEBUG, u' \u251C\u2500 status: {}'.format(response.status)) cls._log(logging.DEBUG, u' \u2514\u2500 headers: {}'.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__.keys(): # 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 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
[docs]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**. """ 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') #: :class:`.Credentials` to access **user's protected resources**. self.credentials = authomatic.core.Credentials(self.settings.config, provider=self) #=========================================================================== # Abstract properties #=========================================================================== @abc.abstractproperty
[docs] 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
[docs] 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
[docs] 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
[docs] 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
[docs] 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
[docs] 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
[docs] 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))
@classmethod
[docs] def access_with_credentials(cls, credentials, url, params=None, method='GET', headers=None, body='', max_redirects=5, content_parser=None): """ Fetches the **protected resource** of the **user** to whom belong the supplied :data:`.credentials`. :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` """ headers = headers or {} cls._log(logging.INFO, 'Accessing protected resource {}.'.format(url)) request_elements = cls.create_request_elements(request_type=cls.PROTECTED_RESOURCE_REQUEST_TYPE, credentials=credentials, url=url, body=body, params=params, headers=headers, method=method) response = cls._fetch(*request_elements, max_redirects=max_redirects, content_parser=content_parser) cls._log(logging.INFO, 'Got response. HTTP status = {}.'.format(response.status)) return response
[docs] def access(self, url, params=None, method='GET', headers={}, max_redirects=5, content_parser=None): """ Fetches the **protected resource** of the logged in **user**. :param credentials: The **user's** :class:`.Credentials` (serialized or normal). :param str method: HTTP method of the request. :param dict headers: HTTP headers of the request. :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` """ return self.access_with_credentials(credentials=self.credentials, url=url, params=params, method=method, headers=headers, max_redirects=max_redirects, content_parser=content_parser)
[docs] 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)
[docs] def update_user(self): """ Updates the :attr:`.BaseProvider.user`. .. warning:: Fetches the :attr:`.user_info_url`! :returns: :class:`.UserInfoResponse` """ if self.user_info_url: return self._access_user_info() #=========================================================================== # 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(res) return {'Authorization': 'Basic {}'.format(res)} else: return {} def _check_consumer(self): """ Validates the :attr:`.consumer`. """ if not self.consumer.key: raise ConfigError('Consumer key not specified for provider {}!'.format(self.name)) if not self.consumer.secret: raise ConfigError('Consumer secret not specified for provider {}!'.format(self.name)) @staticmethod def _split_url(url): "Splits given url to url base and params converted to list of tuples" split = urlparse.urlsplit(url) base = urlparse.urlunsplit((split.scheme, split.netloc, split.path, 0, 0)) params = urlparse.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__) response = self.access_with_credentials(self.credentials, url) # Create user. self.user = self._update_or_create_user(response.data, content=response.content) # Return UserInfoResponse. return authomatic.core.UserInfoResponse(self.user, response.httplib_response)
[docs]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) # Allow for custom name for the "id" querystring parameter. self.identifier_param = kwargs.get('identifier_param', 'id') # Get the identifier from request params. self.identifier = self.params.get(self.identifier_param)
PROVIDER_ID_MAP = [BaseProvider, AuthorizationProvider, AuthenticationProvider]
Fork me on GitHub