Authomatic
is an authorization / authentication
client library for Python web applications
inspired by Alex Vagin’s Simpleauth.
In fact, I almost named it Deadsimpleauth,
but that name would be too long
for a succinct library.

- Tweet

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

    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

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

        @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=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
        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 '{}:{}:{}'.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(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)))

        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`.
            """

            params = params or {}
            params.update(self.access_params)

            headers = headers or {}
            headers.update(self.access_headers)

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

            self._log(logging.DEBUG, u' \u251C\u2500 host: {}'.format(host))
            self._log(logging.DEBUG, u' \u251C\u2500 path: {}'.format(request_path))
            self._log(logging.DEBUG, u' \u251C\u2500 method: {}'.format(method))
            self._log(logging.DEBUG, u' \u251C\u2500 body: {}'.format(body))
            self._log(logging.DEBUG, u' \u251C\u2500 params: {}'.format(params))
            self._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

                    self._log(logging.DEBUG, 'Redirecting to {}'.format(url))
                    self._log(logging.DEBUG, 'Remaining redirects: '.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: {}'.format(url))
                self._log(logging.DEBUG, u' \u251C\u2500 status: {}'.format(response.status))
                self._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

    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)

        #===========================================================================
        # 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_with_credentials(self, 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 {}

            self._log(logging.INFO, 'Accessing protected resource {}.'.format(url))

            request_elements = self.create_request_elements(request_type=self.PROTECTED_RESOURCE_REQUEST_TYPE,
                                                            credentials=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, 'Got response. HTTP status = {}.'.format(response.status))
            return response

        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)

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

    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]