# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. import logging from datetime import datetime from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.utils import six from django.utils.encoding import python_2_unicode_compatible import requests from django_browserid.compat import pybrowserid_found from django_browserid.util import same_origin logger = logging.getLogger(__name__) @python_2_unicode_compatible class BrowserIDException(Exception): """Raised when there is an issue verifying an assertion.""" def __init__(self, exc): #: Original exception that caused this to be raised. self.exc = exc def __str__(self): return six.text_type(self.exc) def sanity_checks(request): """ Small checks for common errors. Checks are normally only enabled if DEBUG is True. You can explicitly disable the checks using the BROWSERID_DISABLE_SANITY_CHECKS. :returns: True if the checks were run, False if they were skipped. """ if getattr(settings, 'BROWSERID_DISABLE_SANITY_CHECKS', not settings.DEBUG): return False # Return value helps us test if the checks ran. # SESSION_COOKIE_SECURE should be False in development unless you can # use https. if settings.SESSION_COOKIE_SECURE and not request.is_secure(): logger.warning('SESSION_COOKIE_SECURE is currently set to True, ' 'which may cause issues with django_browserid ' 'login during local development. Consider setting ' 'it to False.') # If you're using django-csp, you should include persona. if 'csp.middleware.CSPMiddleware' in settings.MIDDLEWARE_CLASSES: persona = 'https://login.persona.org' in_default = persona in getattr(settings, 'CSP_DEFAULT_SRC', ()) in_script = persona in getattr(settings, 'CSP_SCRIPT_SRC', ()) in_frame = persona in getattr(settings, 'CSP_FRAME_SRC', ()) if (not in_script or not in_frame) and not in_default: logger.warning('django-csp detected, but %s was not found in ' 'your CSP policies. Consider adding it to ' 'CSP_SCRIPT_SRC and CSP_FRAME_SRC', persona) return True def get_audience(request): """ Determine the audience to use for verification from the given request. Relies on the BROWSERID_AUDIENCES setting, which is an explicit list of acceptable audiences for your site. :returns: The first audience in BROWSERID_AUDIENCES that has the same origin as the request's URL. :raises: :class:`django.core.exceptions.ImproperlyConfigured`: If BROWSERID_AUDIENCES isn't defined, or if no matching audience could be found. """ protocol = 'https' if request.is_secure() else 'http' host = '{0}://{1}'.format(protocol, request.get_host()) try: audiences = settings.BROWSERID_AUDIENCES if not audiences and settings.DEBUG: return host except AttributeError: if settings.DEBUG: return host raise ImproperlyConfigured('Required setting BROWSERID_AUDIENCES not found!') for audience in audiences: if same_origin(host, audience): return audience # No audience found? We must not be configured properly, otherwise why are we getting this # request? raise ImproperlyConfigured('No audience could be found in BROWSERID_AUDIENCES for host `{0}`.' .format(host)) @python_2_unicode_compatible class VerificationResult(object): """ Result of an attempt to verify an assertion. VerificationResult objects can be treated as booleans to test if the verification succeeded or not. The fields returned by the remote verification service, such as ``email`` or ``issuer``, are available as attributes if they were included in the response. For example, a failure result will raise an AttributeError if you try to access the ``email`` attribute. """ def __init__(self, response): """ :param response: Dictionary of the response from the remote verification service. """ self._response = response def __getattr__(self, name): if name in self._response: return self._response[name] else: raise AttributeError @property def expires(self): """The expiration date of the assertion as a naive :class:`datetime.datetime` in UTC.""" try: return datetime.utcfromtimestamp(int(self._response['expires']) / 1000.0) except KeyError: raise AttributeError except ValueError: timestamp = self._response['expires'] logger.warning('Could not parse expires timestamp: `{0}`'.format(timestamp)) return timestamp def __nonzero__(self): return self._response.get('status') == 'okay' def __bool__(self): return self.__nonzero__() def __str__(self): result = six.u('Success') if self else six.u('Failure') email = getattr(self, 'email', None) email_string = six.u(' email={0}').format(email) if email else six.u('') return six.u('').format(result, email_string) class RemoteVerifier(object): """ Verifies BrowserID assertions using a remote verification service. By default, this uses the Mozilla Persona service for remote verification. """ verification_service_url = 'https://verifier.login.persona.org/verify' requests_parameters = { 'timeout': 5 } def verify(self, assertion, audience, **kwargs): """ Verify an assertion using a remote verification service. :param assertion: BrowserID assertion to verify. :param audience: The protocol, hostname and port of your website. Used to confirm that the assertion was meant for your site and not for another site. :param kwargs: Extra keyword arguments are passed on to requests.post to allow customization. :returns: :class:`.VerificationResult` :raises: :class:`.BrowserIDException`: Error connecting to the remote verification service, or error parsing the response received from the service. """ parameters = dict(self.requests_parameters, **{ 'data': { 'assertion': assertion, 'audience': audience, } }) parameters['data'].update(kwargs) try: response = requests.post(self.verification_service_url, **parameters) except requests.exceptions.RequestException as err: raise BrowserIDException(err) try: return VerificationResult(response.json()) except (ValueError, TypeError) as err: # If the returned JSON is invalid, log a warning and return a failure result. logger.warning('Failed to parse remote verifier response: `{0}`' .format(response.content)) return VerificationResult({ 'status': 'failure', 'reason': 'Could not parse verifier response: {0}'.format(err) }) class MockVerifier(object): """Mock-verifies BrowserID assertions.""" def __init__(self, email, **kwargs): """ :param email: Email address to include in successful verification result. If None, verify will return a failure result. :param kwargs: Extra keyword arguments are used to update successful verification results. This allows for mocking attributes on the result, such as the issuer. """ self.email = email self.result_attributes = kwargs def verify(self, assertion, audience, **kwargs): """ Mock-verify an assertion. The return value is determined by the parameters given to the constructor. """ if not self.email: return VerificationResult({ 'status': 'failure', 'reason': 'No email given to MockVerifier.' }) else: result = { 'status': 'okay', 'audience': audience, 'email': self.email, 'issuer': 'mockissuer.example.com:443', 'expires': '1311377222765' } result.update(self.result_attributes) return VerificationResult(result) if pybrowserid_found: from browserid.errors import Error as PyBrowserIDError from browserid.verifiers.local import LocalVerifier as PyBrowserIDLocalVerifier class LocalVerifier(object): """ Verifies BrowserID assertions locally instead of using the remote verification service. """ def __init__(self, *args, **kwargs): super(LocalVerifier, self).__init__(*args, **kwargs) self.pybid_verifier = PyBrowserIDLocalVerifier() def verify(self, assertion, audience, **kwargs): """ Verify an assertion locally. :param assertion: BrowserID assertion to verify. :param audience: The protocol, hostname and port of your website. Used to confirm that the assertion was meant for your site and not for another site. :returns: :class:`.VerificationResult` """ try: result = self.pybid_verifier.verify(assertion, audience) except PyBrowserIDError as error: return VerificationResult({ 'status': 'failure', 'reason': error }) return VerificationResult(result) else: # If someone tries to use LocalVerifier, let's show a helpful error # instead of just raising an ImportError. class LocalVerifier(object): def __init__(self, *args, **kwargs): raise RuntimeError('You\'re attempting to use local assertion verification without ' 'PyBrowserID installed. Please install PyBrowserID in order to ' 'enable local assertion verification.')