├── docs ├── dev │ ├── authors.rst │ └── changelog.rst ├── index.rst ├── details │ ├── settings.rst │ ├── troubleshooting.rst │ └── advanced.rst ├── setup.rst ├── Makefile ├── make.bat └── conf.py ├── .gitignore ├── MANIFEST.in ├── requirements.txt ├── .travis.yml ├── django_browserid ├── tests │ ├── urls.py │ ├── models.py │ ├── settings.py │ ├── __init__.py │ ├── test_views.py │ ├── test_auth.py │ └── test_verification.py ├── __init__.py ├── urls.py ├── forms.py ├── context_processors.py ├── signals.py ├── static │ └── browserid │ │ └── browserid.js ├── views.py ├── base.py └── auth.py ├── setup.py ├── fabfile.py ├── AUTHORS.rst ├── CHANGELOG.rst ├── README.rst └── LICENSE /docs/dev/authors.rst: -------------------------------------------------------------------------------- 1 | Authors 2 | ======= 3 | 4 | 5 | .. include:: ../../AUTHORS.rst 6 | -------------------------------------------------------------------------------- /docs/dev/changelog.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 5 | .. include:: ../../CHANGELOG.rst 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | test.db 3 | /build/ 4 | /dist/ 5 | /django_browserid.egg-info/ 6 | docs/_build 7 | MANIFEST 8 | .DS_Store 9 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGELOG.rst 2 | include LICENSE 3 | include README.rst 4 | include requirements.txt 5 | recursive-include django_browserid/static/browserid *.js 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests>=0.9.1 2 | 3 | # Tests 4 | mock>=0.8.0 5 | Django>=1.3 6 | django-nose 7 | fabric 8 | unittest2==0.5.1 9 | 10 | # Documentation 11 | sphinx 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 2.6 4 | - 2.7 5 | env: 6 | - DJANGO_VERSION=1.3.4 7 | - DJANGO_VERSION=1.4.2 8 | - DJANGO_VERSION=1.5a1 9 | install: 10 | - pip install -e git+git://github.com/django/django.git@${DJANGO_VERSION}#egg=django 11 | - pip install -r requirements.txt --use-mirrors 12 | script: fab test 13 | -------------------------------------------------------------------------------- /django_browserid/tests/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | """ 6 | from django.conf.urls.defaults import include, patterns 7 | 8 | urlpatterns = patterns('', 9 | (r'^browserid/', include('django_browserid.urls')), 10 | ) 11 | -------------------------------------------------------------------------------- /django_browserid/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | django-browserid 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | """ 8 | __version__ = '0.7.1' 9 | 10 | from django_browserid.auth import BrowserIDBackend # NOQA 11 | from django_browserid.base import get_audience, verify # NOQA 12 | -------------------------------------------------------------------------------- /django_browserid/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | """ 6 | from django.conf.urls.defaults import patterns, url 7 | 8 | from django_browserid.views import Verify 9 | 10 | 11 | urlpatterns = patterns('', 12 | url('^browserid/verify/', Verify.as_view(), 13 | name='browserid_verify') 14 | ) 15 | -------------------------------------------------------------------------------- /django_browserid/forms.py: -------------------------------------------------------------------------------- 1 | """ 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | """ 6 | from django import forms 7 | 8 | 9 | class BrowserIDForm(forms.Form): 10 | assertion = forms.CharField(widget=forms.HiddenInput()) 11 | 12 | class Media: 13 | js = ('browserid/browserid.js', 'https://login.persona.org/include.js') 14 | -------------------------------------------------------------------------------- /django_browserid/context_processors.py: -------------------------------------------------------------------------------- 1 | """ 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | """ 6 | from django_browserid.forms import BrowserIDForm 7 | 8 | 9 | def browserid_form(request): 10 | """ 11 | A context processor that adds a BrowserID form to the request 12 | """ 13 | return {'browserid_form': BrowserIDForm()} 14 | -------------------------------------------------------------------------------- /django_browserid/signals.py: -------------------------------------------------------------------------------- 1 | """ 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | 6 | .. data:: user_created 7 | 8 | Signal triggered when the BrowserID authentication backend creates a new 9 | user. Sender is the function that created the user, user is the new user 10 | instance. 11 | """ 12 | from django.dispatch import Signal 13 | 14 | 15 | user_created = Signal(providing_args=['user']) 16 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | 4 | with open('README.rst') as file: 5 | long_description = file.read() 6 | 7 | setup( 8 | name='django-browserid', 9 | description='Django application for adding BrowserID support.', 10 | long_description=long_description, 11 | version='0.7.1', 12 | packages=['django_browserid', 'django_browserid.tests'], 13 | author='Paul Osman, Michael Kelly', 14 | author_email='mkelly@mozilla.com', 15 | url='https://github.com/mozilla/django-browserid', 16 | license='MPL v2.0', 17 | install_requires='requests>=0.9.1', 18 | package_data={'django_browserid': ['static/browserid/*.js']}, 19 | ) 20 | -------------------------------------------------------------------------------- /django_browserid/tests/models.py: -------------------------------------------------------------------------------- 1 | """ 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | """ 6 | from django.db import models 7 | 8 | try: 9 | from django.contrib.auth.models import AbstractBaseUser 10 | except ImportError: 11 | AbstractBaseUser = object 12 | 13 | 14 | class CustomUser(AbstractBaseUser): 15 | USERNAME_FIELD = 'email' 16 | 17 | email = models.EmailField(unique=True, db_index=True) 18 | 19 | def get_full_name(self): 20 | return self.email 21 | 22 | def get_short_name(self): 23 | return self.email 24 | -------------------------------------------------------------------------------- /fabfile.py: -------------------------------------------------------------------------------- 1 | """ 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | """ 6 | import os 7 | 8 | from fabric.api import local 9 | 10 | 11 | ROOT = os.path.abspath(os.path.dirname(__file__)) 12 | os.environ['PYTHONPATH'] = ROOT 13 | 14 | 15 | def test(): 16 | """Run test suite.""" 17 | os.environ['DJANGO_SETTINGS_MODULE'] = 'django_browserid.tests.settings' 18 | os.environ['REUSE_DB'] = '0' 19 | 20 | # Add tables and flush DB 21 | local('django-admin.py syncdb --noinput') 22 | local('django-admin.py flush --noinput') 23 | 24 | local('django-admin.py test') 25 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ``django-browserid`` is written and maintained by various contributors: 2 | 3 | Current Maintainer 4 | `````````````````` 5 | 6 | - Michael Kelly 7 | 8 | Previous Maintainers 9 | ```````````````````` 10 | 11 | - Paul Osman 12 | - Austin King 13 | - Ben Adida 14 | 15 | 16 | Patches and Suggestions 17 | ``````````````````````` 18 | 19 | - Thomas Grainger 20 | - Owen Coutts 21 | - Francois Marier 22 | - Andy McKay 23 | - Giorgos Logiotatidis 24 | - Alexis Metaireau 25 | - Rob Hudson 26 | - Ross Bruniges 27 | - Les Orchard 28 | - Charlie DeTar 29 | - Luke Crouch 30 | - shaib 31 | - Kumar McMillan 32 | - Carl Meyer 33 | - ptgolden 34 | - Will Kahn-Greene 35 | - Allen Short 36 | - meehow 37 | - Greg Koberger 38 | - Niran Babalola 39 | - callmekatootie 40 | - Paul Mclanahan 41 | -------------------------------------------------------------------------------- /django_browserid/tests/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | """ 6 | TEST_RUNNER = 'django_nose.runner.NoseTestSuiteRunner' 7 | 8 | SECRET_KEY = 'asdf' 9 | 10 | DATABASES = { 11 | 'default': { 12 | 'NAME': 'test.db', 13 | 'ENGINE': 'django.db.backends.sqlite3', 14 | } 15 | } 16 | 17 | INSTALLED_APPS = ( 18 | 'django_nose', 19 | 'django_browserid', 20 | 'django_browserid.tests', 21 | 22 | 'django.contrib.auth', 23 | 'django.contrib.contenttypes', 24 | ) 25 | 26 | ROOT_URLCONF = 'django_browserid.tests.urls' 27 | 28 | AUTHENTICATION_BACKENDS = ( 29 | 'django_browserid.auth.BrowserIDBackend', 30 | ) 31 | 32 | SITE_URL = 'http://testserver' 33 | 34 | BROWSERID_CREATE_USER = True 35 | BROWSERID_USERNAME_ALGO = None 36 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | django-browserid 2 | ================ 3 | 4 | django-browserid is a library that integrates BrowserID_ authentication into 5 | Django_. 6 | 7 | django-browserid provides an authentication backend, ``BrowserIDBackend``, that 8 | verifies BrowserID assertions using the browserid.org verification service and 9 | authenticates users. It also provides ``verify``, which lets you build more 10 | complex authentication systems based on BrowserID. 11 | 12 | django-browserid is a work in progress. Contributions are welcome. Feel free 13 | to fork_ and contribute! 14 | 15 | .. _Django: http://www.djangoproject.com/ 16 | .. _BrowserID: https://browserid.org/ 17 | .. _fork: https://github.com/mozilla/django-browserid 18 | 19 | .. toctree:: 20 | :maxdepth: 2 21 | 22 | setup 23 | details/advanced 24 | details/settings 25 | details/troubleshooting 26 | 27 | Developer Guide 28 | --------------- 29 | 30 | .. toctree:: 31 | :maxdepth: 1 32 | 33 | dev/changelog 34 | dev/authors 35 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | .. :changelog: 2 | 3 | History 4 | ------- 5 | 6 | 0.7.1 (2012-11-08) 7 | ++++++++++++++++++ 8 | 9 | - Add support for a working logout button. Switching to the Observer API in 0.7 10 | made the issue that we weren't calling ``navigator.id.logout`` more 11 | pronounced, so it makes sense to make a small new release to make it easier 12 | to add a logout button. 13 | 14 | 0.7 (2012-11-07) 15 | ++++++++++++++++ 16 | 17 | - Actually start updating the Changelog again. 18 | - Remove deprecated functions ``django_browserid.auth.get_audience`` and 19 | ``django_browserid.auth.BrowserIDBackend.verify``, as well as support for 20 | ``DOMAIN`` and ``PROTOCOL`` settings. 21 | - Add small fix for infinite login loops. 22 | - Add automated testing for Django 1.3.4, 1.4.2, and 1.5a1. 23 | - Switch to using ``format`` for all string formatting (**breaks Python 2.5 24 | compatibility**). 25 | - Add support for Django 1.5 Custom User Models. 26 | - Fix request timeouts so that they work properly. 27 | - Add ability to customize BrowserID login popup via arguments to 28 | ``navigator.id.request``. 29 | - Update JavaScript to use the new Observer API. 30 | - Change ``browserid.org`` urls to ``login.persona.org``. 31 | -------------------------------------------------------------------------------- /docs/details/settings.rst: -------------------------------------------------------------------------------- 1 | Settings 2 | ======== 3 | 4 | .. module:: django.conf.settings 5 | 6 | .. data:: LOGIN_REDIRECT_URL 7 | 8 | **Default:** ``'/accounts/profile'`` 9 | 10 | Path to redirect to on successful login. If you don't specify this, the 11 | default_ Django value will be used. 12 | 13 | .. data:: LOGIN_REDIRECT_URL_FAILURE 14 | 15 | **Default:** ``'/'`` 16 | 17 | Path to redirect to on an unsuccessful login attempt. 18 | 19 | .. data:: BROWSERID_CREATE_USER 20 | 21 | **Default:** ``True`` 22 | 23 | If ``True`` or ``False``, enables or disables automatic user creation during 24 | authentication. 25 | 26 | If set to a string, it is treated as an import path pointing to a custom 27 | user creation function. See :ref:`auto-user` for more information. 28 | 29 | .. data:: BROWSERID_VERIFICATION_URL 30 | 31 | **Default:** ``'https://browserid.org/verify`` 32 | 33 | Defines the URL for the BrowserID verification service to use. 34 | 35 | .. data:: BROWSERID_DISABLE_CERT_CHECK 36 | 37 | **Default:** ``False`` 38 | 39 | Disables SSL certificate verification during BrowserID verification. 40 | *Never disable this in production!* 41 | 42 | .. data:: BROWSERID_CACERT_FILE 43 | 44 | **Default:** ``None`` 45 | 46 | CA cert file used during validation. If none is provided, the default file 47 | included with requests_ is used. 48 | 49 | .. _requests: http://docs.python-requests.org/ 50 | 51 | .. _default: https://docs.djangoproject.com/en/dev/ref/settings/#login-redirect-url 52 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-browserid 2 | ================ 3 | 4 | |TravisCI|_ 5 | 6 | .. |TravisCI| image:: https://secure.travis-ci.org/mozilla/django-browserid.png?branch=master 7 | .. _TravisCI: https://secure.travis-ci.org/mozilla/django-browserid 8 | 9 | django-browserid is a library that integrates BrowserID_ authentication into 10 | Django_. 11 | 12 | .. _Django: http://www.djangoproject.com/ 13 | .. _BrowserID: https://login.persona.org/ 14 | 15 | Tested Under 16 | ------------ 17 | * Python 18 | 19 | * 2.6 20 | * 2.7 21 | 22 | * Django 23 | 24 | * 1.3.4 25 | * 1.4.2 26 | * 1.5a1 27 | 28 | Documentation 29 | ------------- 30 | 31 | http://django-browserid.rtfd.org 32 | 33 | Need Help? 34 | ---------- 35 | 36 | First, check out the `troubleshooting`_ section of the documentation, which 37 | covers solutions to several common problems. 38 | 39 | If that doesn't help, questions can be sent to the #webdev channel on 40 | irc.mozilla.org, or by email to the `current maintainer`_. 41 | 42 | .. _troubleshooting: http://django-browserid.readthedocs.org/en/latest/details/troubleshooting.html 43 | .. _current maintainer: mailto:mkelly@mozilla.org 44 | 45 | Testing 46 | ------- 47 | 0. (Recommended) Create a virtualenv for django-browserid testing. 48 | 1. Install test requirements with ``pip install -r requirements.txt`` 49 | 2. Run test suite with ``fab test`` 50 | 51 | License 52 | ------- 53 | 54 | This software is licensed under the `Mozilla Public License v. 2.0`_. For more 55 | information, read the file ``LICENSE``. 56 | 57 | .. _Mozilla Public License v. 2.0: http://mozilla.org/MPL/2.0/ 58 | -------------------------------------------------------------------------------- /django_browserid/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | """ 6 | from django.utils.functional import wraps 7 | 8 | from mock import patch 9 | 10 | 11 | def fake_create_user(email): 12 | pass 13 | 14 | 15 | class mock_browserid(object): 16 | """ 17 | Mocks django_browserid verification. Can be used as a context manager or 18 | as a decorator: 19 | 20 | with mock_browserid('a@b.com'): 21 | django_browserid.verify('random-token') # = {'status': 'okay', 22 | # 'email': 'a@b.com', 23 | # ...} 24 | 25 | @mock_browserid(None) 26 | def browserid_test(): 27 | django_browserid.verify('random-token') # = False 28 | """ 29 | def __init__(self, email=None, audience=None, unverified_email=None, 30 | pass_mock=False): 31 | self.pass_mock = pass_mock 32 | self.patcher = patch('django_browserid.base._verify_http_request') 33 | self.return_value = { 34 | u'audience': audience, 35 | u'email': email, 36 | u'issuer': u'login.persona.org:443', 37 | u'status': u'okay' if email is not None else u'failure', 38 | u'valid-until': 1311377222765 39 | } 40 | if unverified_email is not None: 41 | self.return_value['unverified-email'] = unverified_email 42 | del self.return_value['email'] 43 | 44 | def __enter__(self): 45 | mock = self.patcher.start() 46 | mock.return_value = self.return_value 47 | return mock 48 | 49 | def __exit__(self, exc_type, exc_value, traceback): 50 | self.patcher.stop() 51 | 52 | def __call__(self, func): 53 | @wraps(func) 54 | def inner(*args, **kwargs): 55 | with self as mock: 56 | if self.pass_mock: 57 | args += (mock,) 58 | return func(*args, **kwargs) 59 | return inner 60 | -------------------------------------------------------------------------------- /django_browserid/static/browserid/browserid.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | (function($) { 6 | 'use strict'; 7 | 8 | $(function() { 9 | // State? Ewwwwww. 10 | var logoutButton = null; 11 | var requestOptions = [ 12 | 'siteName', 13 | 'siteLogo', 14 | 'oncancel', 15 | 'privacyPolicy', 16 | 'returnTo', 17 | 'termsOfService' 18 | ]; 19 | 20 | $(document).delegate('.browserid-login, #browserid', 'click', function(e) { 21 | e.preventDefault(); 22 | 23 | // Arguments to navigator.id.request can be specified by data-attributes 24 | // on the BrowserID link: 25 | var options = {}; 26 | var $link = $(this); 27 | for (var k = 0; k < requestOptions.length; k++) { 28 | var name = requestOptions[k]; 29 | var value = $link.data(name); 30 | if (value !== undefined) { 31 | options[name] = value; 32 | } 33 | } 34 | 35 | navigator.id.request(options); // Triggers BrowserID login dialog. 36 | }); 37 | 38 | $('.browserid-logout').bind('click', function(e) { 39 | e.preventDefault(); 40 | logoutButton = this; 41 | navigator.id.logout(); // Clears User Agent BrowserID state. 42 | }); 43 | 44 | navigator.id.watch({ 45 | onlogin: function(assertion) { 46 | // Don't bother if login just failed. 47 | if (location.search.indexOf('bid_login_failed=1') !== -1) { 48 | navigator.id.logout(); 49 | } else if (assertion) { 50 | var $e = $('#id_assertion'); 51 | $e.val(assertion.toString()); 52 | $e.parent().submit(); 53 | } 54 | }, 55 | 56 | onlogout: function() { 57 | var currentButton = logoutButton; 58 | if (currentButton !== null) { 59 | logoutButton = null; 60 | if (currentButton.href) { 61 | window.location = currentButton.href; 62 | } 63 | } 64 | } 65 | }); 66 | }); 67 | })(jQuery); 68 | -------------------------------------------------------------------------------- /django_browserid/views.py: -------------------------------------------------------------------------------- 1 | """ 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | """ 6 | from django.conf import settings 7 | from django.contrib import auth 8 | from django.core.exceptions import ImproperlyConfigured 9 | from django.shortcuts import redirect 10 | from django.views.generic.edit import BaseFormView 11 | 12 | from django_browserid.forms import BrowserIDForm 13 | from django_browserid.base import get_audience 14 | 15 | 16 | class Verify(BaseFormView): 17 | form_class = BrowserIDForm 18 | failure_url = getattr(settings, 'LOGIN_REDIRECT_URL_FAILURE', '/') 19 | success_url = getattr(settings, 'LOGIN_REDIRECT_URL', '/') 20 | 21 | def login_success(self): 22 | """Handle a successful login. Use this to perform complex redirects 23 | post-login. 24 | """ 25 | auth.login(self.request, self.user) 26 | redirect_field_name = self.kwargs.get('redirect_field_name', 27 | auth.REDIRECT_FIELD_NAME) 28 | redirect_to = self.request.REQUEST.get(redirect_field_name, None) 29 | 30 | if redirect_to is not None: 31 | return redirect(redirect_to) 32 | else: 33 | return redirect(self.get_success_url()) 34 | 35 | def login_failure(self): 36 | """Handle a failed login. Use this to perform complex redirects 37 | post-login. 38 | """ 39 | # Append "?bid_login_failed=1" to the URL to notify the JavaScript that 40 | # login failed. 41 | failure_url = self.get_failure_url() 42 | 43 | if not failure_url.endswith('?'): 44 | failure_url += '?' if not '?' in failure_url else '&' 45 | failure_url += 'bid_login_failed=1' 46 | 47 | return redirect(failure_url) 48 | 49 | def form_valid(self, form): 50 | """Handles the return post request from the browserID form and puts 51 | interesting variables into the class. If everything checks out, then 52 | we call login_success to decide how to handle a valid user 53 | """ 54 | self.assertion = form.cleaned_data['assertion'] 55 | self.audience = get_audience(self.request) 56 | self.user = auth.authenticate( 57 | assertion=self.assertion, 58 | audience=self.audience) 59 | 60 | if self.user and self.user.is_active: 61 | return self.login_success() 62 | 63 | return self.login_failure() 64 | 65 | def form_invalid(self, *args, **kwargs): 66 | return self.login_failure() 67 | 68 | def get(self, *args, **kwargs): 69 | return self.login_failure() 70 | 71 | def get_failure_url(self): 72 | """ 73 | This is just the django version of get_success_url 74 | https://github.com/django/django/blob/master/django/views/generic/edit.py#L51 75 | """ 76 | if self.failure_url: 77 | url = self.failure_url 78 | else: 79 | raise ImproperlyConfigured( 80 | "No URL to redirect to. Provide a failure_url.") 81 | return url 82 | -------------------------------------------------------------------------------- /django_browserid/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | """ 6 | import logging 7 | import urllib 8 | from warnings import warn 9 | try: 10 | import json 11 | except ImportError: 12 | import simplejson as json # NOQA 13 | 14 | 15 | from django.conf import settings 16 | 17 | import requests 18 | 19 | 20 | log = logging.getLogger(__name__) 21 | 22 | 23 | DEFAULT_HTTP_TIMEOUT = 5 24 | DEFAULT_VERIFICATION_URL = 'https://verifier.login.persona.org/verify' 25 | OKAY_RESPONSE = 'okay' 26 | 27 | 28 | def get_audience(request): 29 | """Uses Django settings to format the audience. 30 | 31 | To use this function, make sure there is either a SITE_URL in 32 | your settings.py file or PROTOCOL and DOMAIN. 33 | 34 | Examples using SITE_URL: 35 | SITE_URL = 'http://127.0.0.1:8001' 36 | SITE_URL = 'https://example.com' 37 | SITE_URL = 'http://example.com' 38 | 39 | If none are set, we trust the request to populate the audience. 40 | This is *not secure*! 41 | """ 42 | site_url = getattr(settings, 'SITE_URL', False) 43 | 44 | # Note audience based on request for developer warnings 45 | if request.is_secure(): 46 | req_proto = 'https://' 47 | else: 48 | req_proto = 'http://' 49 | req_domain = request.get_host() 50 | 51 | req_url = "%s%s" % (req_proto, req_domain) 52 | if site_url != "%s%s" % (req_proto, req_domain): 53 | log.warning('Misconfigured SITE_URL? settings has {0}, but ' 54 | 'actual request was {1} BrowserID may fail on ' 55 | 'audience'.format(site_url, req_url)) 56 | return site_url 57 | 58 | 59 | def _verify_http_request(url, qs): 60 | parameters = { 61 | 'data': qs, 62 | 'proxies': getattr(settings, 'BROWSERID_PROXY_INFO', None), 63 | 'verify': not getattr(settings, 'BROWSERID_DISABLE_CERT_CHECK', False), 64 | 'headers': {'Content-type': 'application/x-www-form-urlencoded'}, 65 | 'timeout': getattr(settings, 'BROWSERID_HTTP_TIMEOUT', 66 | DEFAULT_HTTP_TIMEOUT), 67 | } 68 | 69 | if parameters['verify']: 70 | parameters['verify'] = getattr(settings, 'BROWSERID_CACERT_FILE', True) 71 | 72 | r = requests.post(url, **parameters) 73 | 74 | try: 75 | rv = json.loads(r.content) 76 | except ValueError: 77 | log.debug('Failed to decode JSON. Resp: {0}, Content: {1}'.format(r.status_code, r.content)) 78 | return dict(status='failure') 79 | 80 | return rv 81 | 82 | 83 | def verify(assertion, audience, extra_params=None): 84 | """Verify assertion using an external verification service. 85 | extra_params is a dict of additional parameters to send to the 86 | verification service. 87 | """ 88 | verify_url = getattr(settings, 'BROWSERID_VERIFICATION_URL', 89 | DEFAULT_VERIFICATION_URL) 90 | 91 | log.info("Verification URL: {0}".format(verify_url)) 92 | 93 | args = {'assertion': assertion, 94 | 'audience': audience} 95 | if extra_params: 96 | args.update(extra_params) 97 | result = _verify_http_request(verify_url, urllib.urlencode(args)) 98 | 99 | if result['status'] == OKAY_RESPONSE: 100 | return result 101 | 102 | log.error('BrowserID verification failure. Response: {0} ' 103 | 'Audience: {1}'.format(result, audience)) 104 | log.error("BID assert: {0}".format(assertion)) 105 | return False 106 | -------------------------------------------------------------------------------- /docs/details/troubleshooting.rst: -------------------------------------------------------------------------------- 1 | Troubleshooting 2 | =============== 3 | 4 | CSP WARN: Directive "..." violated by https://browserid.org/include.js 5 | ---------------------------------------------------------------------- 6 | 7 | This warning appears in the Error Console when your site uses 8 | `Content Security Policy`_ without making an exception for the browserid.org 9 | external JavaScript include. 10 | 11 | To fix this, include https://browserid.org in your script-src directive. If 12 | you're using the `django-csp`_ library, the following settings will work:: 13 | 14 | CSP_SCRIPT_SRC = ("'self'", 'https://browserid.org','https://login.persona.org') 15 | CSP_FRAME_SRC = ("'self'", 'https://browserid.org','https://login.persona.org') 16 | 17 | .. note:: The example above also includes the frame-src directive. There is an 18 | iframe used during BrowserID login, but some people report that login will 19 | work without the directive. In general, you should probably include it. 20 | 21 | .. _Content Security Policy: https://developer.mozilla.org/en/Security/CSP 22 | .. _django-csp: https://github.com/mozilla/django-csp 23 | 24 | 25 | Site keeps redirecting to the same page repeatedly 26 | -------------------------------------------------- 27 | 28 | Sometimes, after attempting to login, you might notice that the page keeps 29 | reloading itself over and over. This usually means that something has gone wrong 30 | in your login process, and you should check the log output as well as the 31 | solutions below to see if they can point you in the right direction. 32 | 33 | The reason for the repeating redirects has to do with Persona, the default 34 | BrowserID server that ``django-browserid`` uses. If you have attempted to log in 35 | to a site via Persona, and the site fails to accept your login, Persona will 36 | continue to attempt to log you in if the JavaScript shim that it provides is 37 | included on the page. 38 | 39 | The easiest way to get around this is to simply not include the login form on 40 | any pages when the user is logged in. ``django-browserid`` attempts to avoid 41 | these infinite loops in certain cases, but they may still come up if, for 42 | example, ``SESSION_COOKIE_SECURE`` is True on a development instance without 43 | SSL. 44 | 45 | 46 | Login fails silently due to SESSION_COOKIE_SECURE 47 | ------------------------------------------------- 48 | 49 | If you try to login on a local instance of a site and login fails without any 50 | error (typically redirecting you back to the login page), check to see if you've 51 | set `SESSION_COOKIE_SECURE` to True in your settings. 52 | 53 | `SESSION_COOKIE_SECURE` controls if the `secure` flag is set on the session 54 | cookie. If set to True on a local instance of a site that does not use HTTPS, 55 | the session cookie won't be sent by your browser because you're using an HTTP 56 | connection. 57 | 58 | The solution is to set `SESSION_COOKIE_SECURE` to False on your local instance, 59 | typically by adding it to `settings/local.py`:: 60 | 61 | SESSION_COOKIE_SECURE = False 62 | 63 | 64 | Login fails silently due to cache issues 65 | ---------------------------------------- 66 | 67 | Another possible cause of silently failing logins is an issue with having no 68 | cache configured locally. Several projects (especially projects based on 69 | playdoh_, which uses `django-session-csrf`_) store session info in the cache 70 | rather than the database, and if your local instance has no cache configured, 71 | the session information will not be stored and login will fail silently. 72 | 73 | To solve this issue, you should configure your local instance to use an 74 | in-memory cache with the following in your local settings file:: 75 | 76 | CACHES = { 77 | 'default': { 78 | 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', 79 | 'LOCATION': 'unique-snowflake' 80 | } 81 | } 82 | 83 | .. _playdoh: https://github.com/mozilla/playdoh 84 | .. _django-session-csrf: https://github.com/mozilla/django-session-csrf 85 | -------------------------------------------------------------------------------- /docs/setup.rst: -------------------------------------------------------------------------------- 1 | Setup 2 | ===== 3 | 4 | Installation 5 | ------------ 6 | 7 | You can use pip to install django-browserid and requirements: 8 | 9 | pip install django-browserid 10 | 11 | 12 | Configuration 13 | ------------- 14 | 15 | To use ``django-browserid``, add it to ``INSTALLED_APPS`` in ``settings.py``:: 16 | 17 | INSTALLED_APPS = ( 18 | # ... 19 | 'django.contrib.auth', 20 | 'django_browserid', # Load after auth to monkey-patch it. 21 | # ... 22 | ) 23 | 24 | and add ``django_browserid.auth.BrowserIDBackend`` to ``AUTHENTICATION_BACKENDS`` in ``settings.py``:: 25 | 26 | AUTHENTICATION_BACKENDS = ( 27 | # ... 28 | 'django_browserid.auth.BrowserIDBackend', 29 | # ... 30 | ) 31 | 32 | Edit your ``urls.py`` file and add the following:: 33 | 34 | urlpatterns = patterns('', 35 | # ... 36 | (r'', include('django_browserid.urls')), 37 | # ... 38 | ) 39 | 40 | You should also add the following in ``settings.py``:: 41 | 42 | # Note: No trailing slash 43 | SITE_URL = 'https://example.com:8000' 44 | 45 | BrowserID uses an assertion and an audience to verify the user. This 46 | ``SITE_URL`` is used to determine the audience. For security reasons, it is 47 | *very important* that you set ``SITE_URL`` correctly. 48 | 49 | You can also set the following optional config in ``settings.py`` 50 | (they have sensible defaults): :: 51 | 52 | # Path to redirect to on successful login. 53 | LOGIN_REDIRECT_URL = '/' 54 | 55 | # Path to redirect to on unsuccessful login attempt. 56 | LOGIN_REDIRECT_URL_FAILURE = '/' 57 | 58 | Somewhere in one of your templates, you'll need to create a link and a 59 | form with a single hidden input element, which you'll use to submit 60 | the BrowserID assertion to the server. If you want to use 61 | ``django_browserid.forms.BrowserIDForm``, you could use something like 62 | the following template snippet: :: 63 | 64 | {% if not user.is_authenticated %} 65 | Sign In 66 |
67 | {% csrf_token %} 68 | {{ browserid_form.as_p }} 69 |
70 | {% endif %} 71 | 72 | .. note:: If you're using the default JavaScript mentioned below, you can use as 73 | many login links as you like as long as they all have the class 74 | ``browserid-login``. However, you must only include the form on the page 75 | once. 76 | 77 | If you use browserid_form, it is further recommended that you add 78 | ``django_browserid.context_processors.browserid_form`` to 79 | ``TEMPLATE_CONTEXT_PROCESSORS``; this will create the 80 | ``browserid_form`` variable automatically in ``RequestContext`` 81 | instances when needed. That is, in ``settings.py``:: 82 | 83 | TEMPLATE_CONTEXT_PROCESSORS = ( 84 | # ... 85 | 'django_browserid.context_processors.browserid_form', 86 | # ... 87 | ) 88 | 89 | You will also need to include JavaScript to power the BrowserID popup 90 | and form. You can use django form media at the bottom of your page 91 | (see `Form Media`_ and `Managing static files`_ for more 92 | information):: 93 | 94 | {{ browserid_form.media }} 95 | 96 | This JavaScript file requires jQuery 1.6 or higher. 97 | 98 | .. note:: If you don't want to use the static files framework, you'll need to 99 | include the ``https://login.persona.org/include.js`` file, as well as 100 | JavaScript similar to ``django_browserid/static/browserid/browserid.js``:: 101 | 102 | 103 | 104 | 105 | .. note:: If your site uses `Content Security Policy`_, you will have to add 106 | directives to allow the external browserid.org JavaScript, as well as an 107 | iframe used as part of the login process. 108 | 109 | If you're using `django-csp`_, the following settings will work:: 110 | 111 | CSP_SCRIPT_SRC = ("'self'", 'https://login.persona.org') 112 | CSP_FRAME_SRC = ("'self'", 'https://login.persona.org') 113 | 114 | .. _Form Media: https://docs.djangoproject.com/en/1.3/topics/forms/media/ 115 | .. _Managing static files: https://docs.djangoproject.com/en/1.3/howto/static-files/ 116 | .. _Content Security Policy: https://developer.mozilla.org/en/Security/CSP 117 | .. _django-csp: https://github.com/mozilla/django-csp 118 | 119 | Logging Out 120 | ----------- 121 | 122 | To log users out, create a view that calls `django.contrib.auth.logout`, or use 123 | the standard logout view `django.contrib.auth.views.logout`. Then, add a link 124 | to your page with the `browserid-logout` class:: 125 | 126 | Log Out 127 | 128 | .. note:: Ensure that you include the form media on the same page, as it handles 129 | calling ``navigator.id.logout`` when the logout link is clicked. 130 | 131 | -------------------------------------------------------------------------------- /django_browserid/auth.py: -------------------------------------------------------------------------------- 1 | """ 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | """ 6 | import base64 7 | import hashlib 8 | import logging 9 | 10 | from django.conf import settings 11 | from django.core.exceptions import ImproperlyConfigured 12 | from django.utils.importlib import import_module 13 | 14 | from django_browserid.base import verify 15 | from django_browserid.signals import user_created 16 | 17 | try: 18 | from django.contrib.auth import get_user_model 19 | except ImportError: 20 | from django.contrib.auth.models import User 21 | 22 | def get_user_model(*args, **kwargs): 23 | return User 24 | 25 | 26 | log = logging.getLogger(__name__) 27 | 28 | 29 | def default_username_algo(email): 30 | # store the username as a base64 encoded sha1 of the email address 31 | # this protects against data leakage because usernames are often 32 | # treated as public identifiers (so we can't use the email address). 33 | username = base64.urlsafe_b64encode( 34 | hashlib.sha1(email).digest()).rstrip('=') 35 | return username 36 | 37 | 38 | class BrowserIDBackend(object): 39 | supports_anonymous_user = False 40 | supports_inactive_user = True 41 | supports_object_permissions = False 42 | 43 | def __init__(self): 44 | """ 45 | Store the current user model on creation to avoid issues if 46 | settings.AUTH_USER_MODEL changes, which usually only happens during 47 | tests. 48 | """ 49 | self.User = get_user_model() 50 | 51 | def filter_users_by_email(self, email): 52 | """Return all users matching the specified email.""" 53 | return self.User.objects.filter(email=email) 54 | 55 | def create_user(self, email): 56 | """Return object for a newly created user account.""" 57 | username_algo = getattr(settings, 'BROWSERID_USERNAME_ALGO', None) 58 | if username_algo is not None: 59 | username = username_algo(email) 60 | else: 61 | username = default_username_algo(email) 62 | 63 | return self.User.objects.create_user(username, email) 64 | 65 | def authenticate(self, assertion=None, audience=None, **kw): 66 | """``django.contrib.auth`` compatible authentication method. 67 | 68 | Given a BrowserID assertion and an audience, it attempts to 69 | verify them and then extract the email address for the authenticated 70 | user. 71 | 72 | An audience should be in the form ``https://example.com`` or 73 | ``http://localhost:8001``. 74 | 75 | See django_browserid.base.get_audience() 76 | """ 77 | result = verify(assertion, audience, extra_params=kw) 78 | if not result: 79 | return None 80 | 81 | email = result['email'] 82 | 83 | # in the rare case that two user accounts have the same email address, 84 | # log and bail. randomly selecting one seems really wrong. 85 | users = self.filter_users_by_email(email=email) 86 | if len(users) > 1: 87 | log.warn('{0} users with email address {1}.'.format(len(users), 88 | email)) 89 | return None 90 | if len(users) == 1: 91 | return users[0] 92 | 93 | create_user = getattr(settings, 'BROWSERID_CREATE_USER', True) 94 | if not create_user: 95 | return None 96 | else: 97 | if create_user is True: 98 | create_function = self.create_user 99 | else: 100 | # Find the function to call. 101 | create_function = self._load_module(create_user) 102 | 103 | user = create_function(email) 104 | user_created.send(create_function, user=user) 105 | return user 106 | 107 | def get_user(self, user_id): 108 | try: 109 | return self.User.objects.get(pk=user_id) 110 | except self.User.DoesNotExist: 111 | return None 112 | 113 | def _load_module(self, path): 114 | """Code to load create user module. Based off django's load_backend""" 115 | 116 | i = path.rfind('.') 117 | module, attr = path[:i], path[i + 1:] 118 | 119 | try: 120 | mod = import_module(module) 121 | except ImportError: 122 | raise ImproperlyConfigured('Error importing BROWSERID_CREATE_USER' 123 | ' function.') 124 | except ValueError: 125 | raise ImproperlyConfigured('Error importing BROWSERID_CREATE_USER' 126 | ' function. Is BROWSERID_CREATE_USER a' 127 | ' string?') 128 | 129 | try: 130 | create_user = getattr(mod, attr) 131 | except AttributeError: 132 | raise ImproperlyConfigured('Module {0} does not define a {1} ' 133 | 'function.'.format(module, attr)) 134 | return create_user 135 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-browserid.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-browserid.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/django-browserid" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-browserid" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | if NOT "%PAPER%" == "" ( 11 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 12 | ) 13 | 14 | if "%1" == "" goto help 15 | 16 | if "%1" == "help" ( 17 | :help 18 | echo.Please use `make ^` where ^ is one of 19 | echo. html to make standalone HTML files 20 | echo. dirhtml to make HTML files named index.html in directories 21 | echo. singlehtml to make a single large HTML file 22 | echo. pickle to make pickle files 23 | echo. json to make JSON files 24 | echo. htmlhelp to make HTML files and a HTML help project 25 | echo. qthelp to make HTML files and a qthelp project 26 | echo. devhelp to make HTML files and a Devhelp project 27 | echo. epub to make an epub 28 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 29 | echo. text to make text files 30 | echo. man to make manual pages 31 | echo. changes to make an overview over all changed/added/deprecated items 32 | echo. linkcheck to check all external links for integrity 33 | echo. doctest to run all doctests embedded in the documentation if enabled 34 | goto end 35 | ) 36 | 37 | if "%1" == "clean" ( 38 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 39 | del /q /s %BUILDDIR%\* 40 | goto end 41 | ) 42 | 43 | if "%1" == "html" ( 44 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 45 | if errorlevel 1 exit /b 1 46 | echo. 47 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 48 | goto end 49 | ) 50 | 51 | if "%1" == "dirhtml" ( 52 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 53 | if errorlevel 1 exit /b 1 54 | echo. 55 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 56 | goto end 57 | ) 58 | 59 | if "%1" == "singlehtml" ( 60 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 61 | if errorlevel 1 exit /b 1 62 | echo. 63 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 64 | goto end 65 | ) 66 | 67 | if "%1" == "pickle" ( 68 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 69 | if errorlevel 1 exit /b 1 70 | echo. 71 | echo.Build finished; now you can process the pickle files. 72 | goto end 73 | ) 74 | 75 | if "%1" == "json" ( 76 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished; now you can process the JSON files. 80 | goto end 81 | ) 82 | 83 | if "%1" == "htmlhelp" ( 84 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished; now you can run HTML Help Workshop with the ^ 88 | .hhp project file in %BUILDDIR%/htmlhelp. 89 | goto end 90 | ) 91 | 92 | if "%1" == "qthelp" ( 93 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 94 | if errorlevel 1 exit /b 1 95 | echo. 96 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 97 | .qhcp project file in %BUILDDIR%/qthelp, like this: 98 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-browserid.qhcp 99 | echo.To view the help file: 100 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-browserid.ghc 101 | goto end 102 | ) 103 | 104 | if "%1" == "devhelp" ( 105 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 106 | if errorlevel 1 exit /b 1 107 | echo. 108 | echo.Build finished. 109 | goto end 110 | ) 111 | 112 | if "%1" == "epub" ( 113 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 117 | goto end 118 | ) 119 | 120 | if "%1" == "latex" ( 121 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 122 | if errorlevel 1 exit /b 1 123 | echo. 124 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 125 | goto end 126 | ) 127 | 128 | if "%1" == "text" ( 129 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 130 | if errorlevel 1 exit /b 1 131 | echo. 132 | echo.Build finished. The text files are in %BUILDDIR%/text. 133 | goto end 134 | ) 135 | 136 | if "%1" == "man" ( 137 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 141 | goto end 142 | ) 143 | 144 | if "%1" == "changes" ( 145 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.The overview file is in %BUILDDIR%/changes. 149 | goto end 150 | ) 151 | 152 | if "%1" == "linkcheck" ( 153 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Link check complete; look for any errors in the above output ^ 157 | or in %BUILDDIR%/linkcheck/output.txt. 158 | goto end 159 | ) 160 | 161 | if "%1" == "doctest" ( 162 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 163 | if errorlevel 1 exit /b 1 164 | echo. 165 | echo.Testing of doctests in the sources finished, look at the ^ 166 | results in %BUILDDIR%/doctest/output.txt. 167 | goto end 168 | ) 169 | 170 | :end 171 | -------------------------------------------------------------------------------- /django_browserid/tests/test_views.py: -------------------------------------------------------------------------------- 1 | """ 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | """ 6 | from django.conf import settings 7 | from django.contrib import auth 8 | from django.test.client import RequestFactory 9 | 10 | from mock import patch 11 | 12 | from django_browserid import views 13 | from django_browserid.tests import mock_browserid 14 | 15 | 16 | factory = RequestFactory() 17 | 18 | 19 | def verify(request_type, redirect_field_name=None, success_url=None, 20 | failure_url=None, **kwargs): 21 | """Call the verify view function. All kwargs not specified above will be 22 | passed as GET or POST arguments. 23 | """ 24 | if request_type == 'get': 25 | request = factory.get('/browserid/verify', kwargs) 26 | else: 27 | request = factory.post('/browserid/verify', kwargs) 28 | 29 | # Patch settings prior to importing verify 30 | patches = [] 31 | if success_url is not None: 32 | patches.append(patch.object(settings, 'LOGIN_REDIRECT_URL', 33 | success_url, create=True)) 34 | if failure_url is not None: 35 | patches.append(patch.object(settings, 'LOGIN_REDIRECT_URL_FAILURE', 36 | failure_url, create=True)) 37 | # Create users if they don't exist for testing. 38 | patches.append(patch.object(settings, 'BROWSERID_CREATE_USER', True, 39 | create=True)) 40 | 41 | # Only pass redirect_field_name if it is specified 42 | verify_kwargs = {} 43 | if redirect_field_name is not None: 44 | verify_kwargs['redirect_field_name'] = redirect_field_name 45 | 46 | for p in patches: 47 | p.start() 48 | 49 | # We need to reload verify for the setting changes to take effect. 50 | reload(views) 51 | verify_view = views.Verify.as_view() 52 | with patch.object(auth, 'login'): 53 | response = verify_view(request, **verify_kwargs) 54 | 55 | for p in patches: 56 | p.stop() 57 | 58 | return response 59 | 60 | 61 | def test_get_redirect_failure(): 62 | # Issuing a GET to the verify view redirects to the failure URL. 63 | response = verify('get', failure_url='/fail') 64 | assert response.status_code == 302 65 | assert response['Location'].endswith('/fail?bid_login_failed=1') 66 | 67 | 68 | def test_invalid_redirect_failure(): 69 | # Invalid form arguments redirect to the failure URL. 70 | response = verify('post', failure_url='/fail', blah='asdf') 71 | assert response.status_code == 302 72 | assert response['Location'].endswith('/fail?bid_login_failed=1') 73 | 74 | 75 | @mock_browserid(None) 76 | def test_auth_fail_redirect_failure(): 77 | # If authentication fails, redirect to the failure URL. 78 | response = verify('post', failure_url='/fail', assertion='asdf') 79 | assert response.status_code == 302 80 | assert response['Location'].endswith('/fail?bid_login_failed=1') 81 | 82 | 83 | @mock_browserid(None) 84 | def test_auth_fail_url_parameters(): 85 | # Ensure that bid_login_failed=1 is appended to the failure url. 86 | response = verify('post', failure_url='/fail?', assertion='asdf') 87 | assert response['Location'].endswith('/fail?bid_login_failed=1') 88 | 89 | response = verify('post', failure_url='/fail?asdf', assertion='asdf') 90 | assert response['Location'].endswith('/fail?asdf&bid_login_failed=1') 91 | 92 | response = verify('post', failure_url='/fail?asdf=4', assertion='asdf') 93 | assert response['Location'].endswith('/fail?asdf=4&bid_login_failed=1') 94 | 95 | response = verify('post', failure_url='/fail?asdf=4&bid_login_failed=1', 96 | assertion='asdf') 97 | assert response['Location'].endswith('/fail?asdf=4&bid_login_failed=1' 98 | '&bid_login_failed=1') 99 | 100 | 101 | @mock_browserid('test@example.com') 102 | def test_auth_success_redirect_success(): 103 | # If authentication succeeds, redirect to the success URL. 104 | response = verify('post', success_url='/success', assertion='asdf') 105 | assert response.status_code == 302 106 | assert response['Location'].endswith('/success') 107 | 108 | 109 | @mock_browserid('test@example.com') 110 | def test_default_redirect_field(): 111 | # If a redirect is passed as an argument to the request, redirect to that 112 | # instead of the success URL. 113 | kwargs = {auth.REDIRECT_FIELD_NAME: '/field_success', 'assertion': 'asdf'} 114 | response = verify('post', success_url='/success', **kwargs) 115 | assert response.status_code == 302 116 | assert response['Location'].endswith('/field_success') 117 | 118 | 119 | @mock_browserid('test@example.com') 120 | def test_redirect_field_name(): 121 | # If a redirect field name is specified, use the request argument matching 122 | # that name as the path to redirect to. 123 | kwargs = {'my_redirect': '/field_success', 'assertion': 'asdf'} 124 | response = verify('post', success_url='/success', 125 | redirect_field_name='my_redirect', **kwargs) 126 | assert response.status_code == 302 127 | assert response['Location'].endswith('/field_success') 128 | -------------------------------------------------------------------------------- /docs/details/advanced.rst: -------------------------------------------------------------------------------- 1 | Advanced Usage 2 | ============== 3 | 4 | navigator.id.request Arguments 5 | ------------------------------ 6 | 7 | The ``navigator.id.request`` function accepts several optional arguments that 8 | customize the BrowserID login dialog. The default JavaScript provided by 9 | ``django-browserid`` checks for data attributes on the login link and passes 10 | them to ``navigator.id.request``. For example, the following customizes the 11 | website name shown in the BrowserID dialog:: 12 | 13 | Login 14 | 15 | .. note:: ``navigator.id.request`` arguments are in camelCase, but data 16 | attributes must use dashes in place of capitalization changes. 17 | 18 | See the `navigator.id.request documentation`_ for a list of accepted parameters. 19 | 20 | .. _navigator.id.request documentation: https://developer.mozilla.org/docs/DOM/navigator.id.request 21 | 22 | Automatic Account Creation 23 | -------------------------- 24 | 25 | ``django-browserid`` will automatically create a user account for new 26 | users. The user account will be created with the verified 27 | email returned from the BrowserID verification service, and a URL safe 28 | base64 encoded SHA1 of the email with the padding removed as the 29 | username. 30 | 31 | To provide a customized username, you can provide a different 32 | algorithm via your settings.py:: 33 | 34 | # settings.py 35 | BROWSERID_CREATE_USER = True 36 | def username(email): 37 | return email.rsplit('@', 1)[0] 38 | BROWSERID_USERNAME_ALGO = username 39 | 40 | You can can provide your own function to create users by setting 41 | ``BROWSERID_CREATE_USER`` to a string path pointing to a function:: 42 | 43 | # module/util.py 44 | def create_user(email): 45 | return User.objects.create_user(email, email) 46 | 47 | # settings.py 48 | BROWSERID_CREATE_USER = 'module.util.create_user' 49 | 50 | You can disable account creation, but continue to use the 51 | ``browserid_verify`` view to authenticate existing users with the 52 | following:: 53 | 54 | BROWSERID_CREATE_USER = False 55 | 56 | 57 | Custom Verification 58 | ------------------- 59 | 60 | If you want full control over account verification, don't use 61 | django-browserid's ``browserid_verify`` view. Create your own view and 62 | use ``verify`` to manually verify a BrowserID assertion with something 63 | like the following:: 64 | 65 | from django_browserid import get_audience, verify 66 | from django_browserid.forms import BrowserIDForm 67 | 68 | 69 | def myview(request): 70 | # ... 71 | if request.method == 'POST': 72 | form = BrowserIDForm(data=request.POST) 73 | if not form.is_valid(): 74 | result = verify(form.cleaned_data['assertion'], get_audience(request)) 75 | if result: 76 | # check for user account, create account for new users, etc 77 | user = my_get_or_create_user(result.email) 78 | 79 | ``result`` will be ``False`` if the assertion failed, or a dictionary 80 | similar to the following:: 81 | 82 | { 83 | u'audience': u'https://mysite.com:443', 84 | u'email': u'myemail@example.com', 85 | u'issuer': u'browserid.org', 86 | u'status': u'okay', 87 | u'expires': 1311377222765 88 | } 89 | 90 | You are of course then free to store the email in the session and 91 | prompt the user to sign up using a chosen identifier as their 92 | username, or whatever else makes sense for your site. 93 | 94 | 95 | Javascript Fallback 96 | ------------------- 97 | 98 | It is a good idea to provide an alternative method of authenticating with your 99 | site for users that do not have JavaScript available. An easy way of doing this 100 | is to modify the ``href`` of the link that you bind to BrowserID login to point 101 | to a traditional login and registration page:: 102 | 103 | Sign In 104 | 105 | If a user has JavaScript enabled, when they click the link the JavaScript will 106 | take over and show a BrowserID popup. If a user has JavaScript disabled, they 107 | will be directed to your login view (which should not require JavaScript, of 108 | course). 109 | 110 | 111 | Multiple Login Buttons 112 | ---------------------- 113 | 114 | If you are using the default JavaScript provided by ``django-browserid``, you 115 | can have multiple login buttons on a single page by marking them with the class 116 | ``browserid-login``. Be sure to only include the hidden login form on the page 117 | once to avoid errors from using the same id multiple times. 118 | 119 | 120 | Signals 121 | ------- 122 | 123 | .. module:: django_browserid.signals 124 | 125 | .. data:: user_created 126 | 127 | Signal triggered when a user is automatically created during authentication. 128 | 129 | * **sender**: The function that created the user instance. 130 | * **user**: The user instance that was created. 131 | 132 | 133 | Custom User Model 134 | ----------------- 135 | 136 | Django 1.5 allows you to specify a custom model to use in place of the built-in 137 | User model with the ``AUTH_USER_MODEL`` setting. ``django-browserid`` supports 138 | custom User models, however you will most likely need to subclass 139 | ``django-browserid.BrowserIDBackend`` and override the ``create_user``, 140 | ``get_user``, and ``filter_users_by_email`` functions to work with your class. 141 | -------------------------------------------------------------------------------- /django_browserid/tests/test_auth.py: -------------------------------------------------------------------------------- 1 | """ 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | """ 6 | from django.conf import settings 7 | from django.contrib.auth.models import User 8 | from django.test import TestCase 9 | 10 | from mock import ANY, patch 11 | 12 | from django_browserid.auth import BrowserIDBackend, default_username_algo 13 | from django_browserid.tests import mock_browserid 14 | 15 | # Support Python 2.6 by using unittest2 16 | try: 17 | from unittest import skipIf 18 | except ImportError: 19 | from unittest2 import skipIf 20 | 21 | try: 22 | from django.contrib.auth import get_user_model 23 | from django_browserid.tests.models import CustomUser 24 | except ImportError: 25 | get_user_model = False 26 | 27 | 28 | def new_user(email, username=None): 29 | """Creates a user with the specified email for testing.""" 30 | if username is None: 31 | username = default_username_algo(email) 32 | return User.objects.create_user(username, email) 33 | 34 | 35 | class BrowserIDBackendTests(TestCase): 36 | def auth(self, verified_email=None): 37 | """ 38 | Attempt to authenticate a user with BrowserIDBackend. 39 | 40 | If verified_email is None, verification will fail, otherwise it will 41 | pass and return the specified email. 42 | """ 43 | with mock_browserid(verified_email): 44 | backend = BrowserIDBackend() 45 | return backend.authenticate(assertion='asdf', audience='asdf') 46 | 47 | def test_failed_verification(self): 48 | # If verification fails, return None. 49 | self.assertTrue(self.auth(None) is None) 50 | 51 | def test_duplicate_emails(self): 52 | # If there are two users with the same email address, return None. 53 | new_user('a@example.com', 'test1') 54 | new_user('a@example.com', 'test2') 55 | self.assertTrue(self.auth('a@example.com') is None) 56 | 57 | def test_auth_success(self): 58 | # If a single user is found with the verified email, return an instance 59 | # of their user object. 60 | user = new_user('a@example.com') 61 | self.assertEqual(self.auth('a@example.com'), user) 62 | 63 | @patch.object(settings, 'BROWSERID_CREATE_USER', False) 64 | def test_no_create_user(self): 65 | # If user creation is disabled and no user is found, return None. 66 | self.assertTrue(self.auth('a@example.com') is None) 67 | 68 | @patch.object(settings, 'BROWSERID_CREATE_USER', True) 69 | def test_create_user(self): 70 | # If user creation is enabled and no user is found, return a new 71 | # User. 72 | user = self.auth('a@example.com') 73 | self.assertTrue(user is not None) 74 | self.assertTrue(isinstance(user, User)) 75 | self.assertEqual(user.email, 'a@example.com') 76 | 77 | @patch.object(settings, 'BROWSERID_CREATE_USER', 78 | 'django_browserid.tests.test_auth.new_user') 79 | @patch('django_browserid.tests.test_auth.new_user') 80 | def test_custom_create_user(self, create_user): 81 | # If user creation is enabled with a custom create function and no user 82 | # is found, return the new user created with the custom function. 83 | create_user.return_value = 'test' 84 | self.assertEqual(self.auth('a@example.com'), 'test') 85 | create_user.assert_called_with('a@example.com') 86 | 87 | @patch.object(settings, 'BROWSERID_USERNAME_ALGO') 88 | @patch.object(settings, 'BROWSERID_CREATE_USER', True) 89 | def test_custom_username_algorithm(self, username_algo): 90 | # If a custom username algorithm is specified, use it! 91 | username_algo.return_value = 'test' 92 | user = self.auth('a@b.com') 93 | self.assertEqual(user.username, 'test') 94 | 95 | @patch('django_browserid.auth.user_created') 96 | @patch.object(settings, 'BROWSERID_CREATE_USER', True) 97 | def test_user_created_signal(self, user_created): 98 | # Test that the user_created signal is called when a new user is 99 | # created. 100 | user = self.auth('a@b.com') 101 | user_created.send.assert_called_with(ANY, user=user) 102 | 103 | 104 | # Only run custom user model tests if we're using a version of Django that 105 | # supports it. 106 | @patch.object(settings, 'AUTH_USER_MODEL', 'tests.CustomUser') 107 | @skipIf(not get_user_model, 'Not supported in Django < 1.5') 108 | class CustomUserModelTests(TestCase): 109 | def _auth(self, backend=None, verified_email=None): 110 | if backend is None: 111 | backend = BrowserIDBackend() 112 | 113 | with mock_browserid(verified_email): 114 | return backend.authenticate(assertion='asdf', audience='asdf') 115 | 116 | def test_existing_user(self): 117 | """If a custom user exists with the given email, return them.""" 118 | user = CustomUser.objects.create(email='a@test.com') 119 | authed_user = self._auth(verified_email='a@test.com') 120 | self.assertEqual(user, authed_user) 121 | 122 | @patch.object(settings, 'BROWSERID_CREATE_USER', True) 123 | def test_create_new_user(self): 124 | """ 125 | If a custom user does not exist with the given email, create a new 126 | user and return them. 127 | """ 128 | class CustomUserBrowserIDBackend(BrowserIDBackend): 129 | def create_user(self, email): 130 | return CustomUser.objects.create(email=email) 131 | user = self._auth(backend=CustomUserBrowserIDBackend(), 132 | verified_email='b@test.com') 133 | self.assertTrue(isinstance(user, CustomUser)) 134 | self.assertEqual(user.email, 'b@test.com') 135 | -------------------------------------------------------------------------------- /django_browserid/tests/test_verification.py: -------------------------------------------------------------------------------- 1 | """ 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | """ 6 | from contextlib import contextmanager 7 | 8 | from django.conf import settings 9 | from django.contrib import auth 10 | from django.contrib.auth.models import User 11 | 12 | from mock import ANY, patch 13 | 14 | from django_browserid.base import verify 15 | from django_browserid.tests import mock_browserid 16 | from django_browserid.auth import BrowserIDBackend 17 | 18 | 19 | assertion = 'foo.bar.baz' 20 | audience = 'http://localhost:8000' 21 | 22 | 23 | authenticate_kwargs = { 24 | 'assertion': assertion, 25 | 'audience': audience, 26 | } 27 | 28 | 29 | @contextmanager 30 | def negative_assertion(fake_http_request, **kwargs): 31 | assertion = { 32 | u'status': u'failure' 33 | } 34 | assertion.update(kwargs) 35 | fake_http_request.expects_call().returns(assertion) 36 | yield 37 | 38 | 39 | @patch('django_browserid.auth.BrowserIDBackend.authenticate') 40 | def test_backend_authenticate(fake): 41 | # Test that the authentication backend is set up correctly. 42 | fake.return_value = None 43 | auth.authenticate(**authenticate_kwargs) 44 | fake.assert_called_with(**authenticate_kwargs) 45 | 46 | 47 | @patch('django_browserid.auth.verify') 48 | def test_backend_verify(fake): 49 | # Test that authenticate() calls verify(). 50 | fake.return_value = False 51 | auth.authenticate(**authenticate_kwargs) 52 | fake.assert_called_with(assertion, audience, extra_params={}) 53 | 54 | 55 | @mock_browserid(None) 56 | def test_backend_verify_invalid_assertion(): 57 | # Test that authenticate() returns None when credentials are bad. 58 | user = auth.authenticate(**authenticate_kwargs) 59 | assert user is None 60 | 61 | 62 | @patch('django_browserid.auth.verify') 63 | def test_auth_copes_with_false(verify): 64 | # Test that authenticate copes with False. 65 | verify.return_value = False 66 | assert BrowserIDBackend().authenticate(**authenticate_kwargs) is None 67 | 68 | 69 | @mock_browserid('myemail@example.com') 70 | def test_verify_correct_credentials(): 71 | # Test that verify() returns assertion details when assertion is valid. 72 | verification = verify(assertion, audience) 73 | assert verification['status'] == 'okay' 74 | assert verification['email'] == 'myemail@example.com' 75 | 76 | 77 | @patch.object(settings, 'BROWSERID_CREATE_USER', True, create=True) 78 | @patch.object(settings, 'BROWSERID_USERNAME_ALGO', None, create=True) 79 | @mock_browserid('bid_create@example.com') 80 | def test_authenticate_create_user(): 81 | # Test that automatic user creation works when enabled. 82 | User.objects.filter(email='bid_create@example.com').delete() 83 | ob = User.objects.filter(email='bid_create@example.com') 84 | assert ob.exists() is False 85 | auth.authenticate(**authenticate_kwargs) 86 | 87 | ob = User.objects.filter(email='bid_create@example.com') 88 | assert ob.exists() is True 89 | 90 | 91 | def username_algo(email): 92 | return email.split('@')[0] 93 | 94 | 95 | @patch.object(settings, 'BROWSERID_CREATE_USER', True, create=True) 96 | @patch.object(settings, 'BROWSERID_USERNAME_ALGO', username_algo, create=True) 97 | @mock_browserid('bid_alt_username@example.com') 98 | def test_authenticate_create_user_with_alternate_username_algo(): 99 | # Test that automatic user creation with an alternate username algo 100 | # works. 101 | user = auth.authenticate(**authenticate_kwargs) 102 | assert user.username == 'bid_alt_username' 103 | 104 | 105 | @patch.object(settings, 'BROWSERID_CREATE_USER', 106 | 'django_browserid.tests.fake_create_user', create=True) 107 | @patch('django_browserid.tests.fake_create_user') 108 | @mock_browserid('does.not.exist@example.org') 109 | def test_authenticate_create_user_with_callable(fake): 110 | # Test that automatic user creation with a callable function name works 111 | fake.return_value = None 112 | auth.authenticate(**authenticate_kwargs) 113 | fake.assert_called_with('does.not.exist@example.org') 114 | 115 | 116 | @patch.object(settings, 'BROWSERID_CREATE_USER', False, create=True) 117 | @mock_browserid('someotheremail@example.com') 118 | def test_authenticate_missing_user(): 119 | # Test that authenticate() returns None when user creation disabled. 120 | user = auth.authenticate(**authenticate_kwargs) 121 | assert user is None 122 | 123 | 124 | @patch.object(settings, 'BROWSERID_HTTP_TIMEOUT', 1, create=True) 125 | @patch.object(settings, 'BROWSERID_VERIFICATION_URL', 126 | 'https://custom.org/verify', create=True) 127 | @patch('django_browserid.base.requests.post') 128 | def test_verify_post_uses_custom_settings(post): 129 | post.return_value.content = '{"status": "okay"}' 130 | verify(assertion, audience) 131 | post.assert_called_with('https://custom.org/verify', 132 | verify=True, 133 | proxies=ANY, 134 | data=ANY, 135 | timeout=1, 136 | headers=ANY) 137 | 138 | 139 | @patch.object(settings, 'BROWSERID_ALLOW_UNVERIFIED', True, create=True) 140 | @patch.object(settings, 'BROWSERID_VERIFICATION_URL', 'https://unverifier.persona.org/verify', create=True) 141 | @mock_browserid(pass_mock=True) 142 | def test_authenticate_unverified_user(_verify_http_request): 143 | """ 144 | Test that extra parameters are passed through to _verify_http_request 145 | correctly. 146 | """ 147 | # In real life, BROWSERID_VERIFICATION_URL would point to the 148 | # BID Unverified Email verifier. (Yes, that makes my head hurt too.) 149 | args = dict(authenticate_kwargs) 150 | args['extra_params'] = {'issuer': 'a.b.c', 'allow_unverified': True} 151 | 152 | verify(**args) 153 | _verify_http_request.assert_called_once_with( 154 | 'https://unverifier.persona.org/verify', ANY) 155 | assert 'allow_unverified=True' in _verify_http_request.call_args[0][1] 156 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # django-browserid documentation build configuration file, created by 4 | # sphinx-quickstart on Wed Mar 14 00:04:52 2012. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.insert(0, os.path.abspath('.')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = ['sphinx.ext.autodoc'] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'django-browserid' 44 | copyright = u'2012, Paul Osman, Michael Kelly' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | # The short X.Y version. 51 | version = "0.7.1" 52 | # The full version, including alpha/beta/rc tags. 53 | release = "0.7.1" 54 | 55 | # The language for content autogenerated by Sphinx. Refer to documentation 56 | # for a list of supported languages. 57 | #language = None 58 | 59 | # There are two options for replacing |today|: either, you set today to some 60 | # non-false value, then it is used: 61 | #today = '' 62 | # Else, today_fmt is used as the format for a strftime call. 63 | #today_fmt = '%B %d, %Y' 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | exclude_patterns = ['_build'] 68 | 69 | # The reST default role (used for this markup: `text`) to use for all documents. 70 | #default_role = None 71 | 72 | # If true, '()' will be appended to :func: etc. cross-reference text. 73 | #add_function_parentheses = True 74 | 75 | # If true, the current module name will be prepended to all description 76 | # unit titles (such as .. function::). 77 | #add_module_names = True 78 | 79 | # If true, sectionauthor and moduleauthor directives will be shown in the 80 | # output. They are ignored by default. 81 | #show_authors = False 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = 'sphinx' 85 | 86 | # A list of ignored prefixes for module index sorting. 87 | #modindex_common_prefix = [] 88 | 89 | 90 | # -- Options for HTML output --------------------------------------------------- 91 | 92 | # The theme to use for HTML and HTML Help pages. See the documentation for 93 | # a list of builtin themes. 94 | html_theme = 'default' 95 | 96 | # Theme options are theme-specific and customize the look and feel of a theme 97 | # further. For a list of options available for each theme, see the 98 | # documentation. 99 | #html_theme_options = {} 100 | 101 | # Add any paths that contain custom themes here, relative to this directory. 102 | #html_theme_path = [] 103 | 104 | # The name for this set of Sphinx documents. If None, it defaults to 105 | # " v documentation". 106 | #html_title = None 107 | 108 | # A shorter title for the navigation bar. Default is the same as html_title. 109 | #html_short_title = None 110 | 111 | # The name of an image file (relative to this directory) to place at the top 112 | # of the sidebar. 113 | #html_logo = None 114 | 115 | # The name of an image file (within the static path) to use as favicon of the 116 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 117 | # pixels large. 118 | #html_favicon = None 119 | 120 | # Add any paths that contain custom static files (such as style sheets) here, 121 | # relative to this directory. They are copied after the builtin static files, 122 | # so a file named "default.css" will overwrite the builtin "default.css". 123 | html_static_path = ['_static'] 124 | 125 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 126 | # using the given strftime format. 127 | #html_last_updated_fmt = '%b %d, %Y' 128 | 129 | # If true, SmartyPants will be used to convert quotes and dashes to 130 | # typographically correct entities. 131 | #html_use_smartypants = True 132 | 133 | # Custom sidebar templates, maps document names to template names. 134 | #html_sidebars = {} 135 | 136 | # Additional templates that should be rendered to pages, maps page names to 137 | # template names. 138 | #html_additional_pages = {} 139 | 140 | # If false, no module index is generated. 141 | #html_domain_indices = True 142 | 143 | # If false, no index is generated. 144 | #html_use_index = True 145 | 146 | # If true, the index is split into individual pages for each letter. 147 | #html_split_index = False 148 | 149 | # If true, links to the reST sources are added to the pages. 150 | #html_show_sourcelink = True 151 | 152 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 153 | #html_show_sphinx = True 154 | 155 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 156 | #html_show_copyright = True 157 | 158 | # If true, an OpenSearch description file will be output, and all pages will 159 | # contain a tag referring to it. The value of this option must be the 160 | # base URL from which the finished HTML is served. 161 | #html_use_opensearch = '' 162 | 163 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 164 | #html_file_suffix = None 165 | 166 | # Output file base name for HTML help builder. 167 | htmlhelp_basename = 'django-browseriddoc' 168 | 169 | 170 | # -- Options for LaTeX output -------------------------------------------------- 171 | 172 | # The paper size ('letter' or 'a4'). 173 | #latex_paper_size = 'letter' 174 | 175 | # The font size ('10pt', '11pt' or '12pt'). 176 | #latex_font_size = '10pt' 177 | 178 | # Grouping the document tree into LaTeX files. List of tuples 179 | # (source start file, target name, title, author, documentclass [howto/manual]). 180 | latex_documents = [ 181 | ('index', 'django-browserid.tex', u'django-browserid Documentation', 182 | u'Paul Osman, Michael Kelly', 'manual'), 183 | ] 184 | 185 | # The name of an image file (relative to this directory) to place at the top of 186 | # the title page. 187 | #latex_logo = None 188 | 189 | # For "manual" documents, if this is true, then toplevel headings are parts, 190 | # not chapters. 191 | #latex_use_parts = False 192 | 193 | # If true, show page references after internal links. 194 | #latex_show_pagerefs = False 195 | 196 | # If true, show URL addresses after external links. 197 | #latex_show_urls = False 198 | 199 | # Additional stuff for the LaTeX preamble. 200 | #latex_preamble = '' 201 | 202 | # Documents to append as an appendix to all manuals. 203 | #latex_appendices = [] 204 | 205 | # If false, no module index is generated. 206 | #latex_domain_indices = True 207 | 208 | 209 | # -- Options for manual page output -------------------------------------------- 210 | 211 | # One entry per manual page. List of tuples 212 | # (source start file, name, description, authors, manual section). 213 | man_pages = [ 214 | ('index', 'django-browserid', u'django-browserid Documentation', 215 | [u'Paul Osman, Michael Kelly'], 1) 216 | ] 217 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | --------------------------------------------------------------------------------