Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

first commit

  • Loading branch information...
commit 9c42178e22b628a7a5ec8f520085bbd90e996c7e 0 parents
@madjar authored
3  .gitignore
@@ -0,0 +1,3 @@
+.idea
+*.pyc
+pyramid_persona.egg-info/
4 CHANGES.rst
@@ -0,0 +1,4 @@
+1.0
+---
+
+- Initial version
5 LICENSE
@@ -0,0 +1,5 @@
+Copyright (c) 2012 Georges Dubus
+
+Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
2  MANIFEST.in
@@ -0,0 +1,2 @@
+include **.rst
+recursive-include pyramid_persona *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml
103 README.rst
@@ -0,0 +1,103 @@
+pyramid_persona README
+======================
+
+`pyramid_persona` let you quickly set up authentication using persona_ on your pyramid_ project. It aims at giving as
+much as possible with as little configuration as possible, while still letting you customize if you want.
+
+.. _persona: https://login.persona.org/
+.. _pyramid: http://www.pylonsproject.org/
+
+Very basic usage
+----------------
+
+First of all, include `pyramid_persona`. Add this in your project configuration ::
+
+ config.include("pyramid_persona")
+
+Then, we need two little lines in your config files : a secret used to sign cookies, and the audience,
+the hostname and port of your website (this is needed for security reasons)::
+
+ persona.secret = This is some secret string
+ persona.audience = http://localhost:6543
+
+There, we're done. We now have a nice forbidden view with a persona login button.
+
+Less basic usage
+----------------
+
+`pyramid_persona` also provides you a way to easily put a login or logout button on your pages. To do so, you need to
+include jquery, the persona library, and some application-specific in your heads. The application specific javascript
+can be accessed as `request.persona_js`.
+
+Then, you can add the button in your page. `request.persona_button` provides a login if the user is not logged in, and
+a logout button if they are.
+
+A basic page might be (using mako) ::
+
+ <html>
+ <head>
+ <script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
+ <script src="https://browserid.org/include.js" type="text/javascript"></script>
+ <script type="text/javascript">${request.persona_js}</script>
+ </head>
+ <body>
+ Hello ${user}
+ ${request.persona_button}
+ </body>
+ </html>
+
+Customized buttons
+------------------
+
+You can also use your own buttons. For that, you have to include the javascript like in the previous section and give
+your login and logout button the `signin` and `signout` classes. For example ::
+
+ <button id='signin'>login</button>
+ <button id='signout'>logout</button>
+
+What it does
+------------
+
+Here is, in details, what including `pyramid_persona` does :
+
+- it defines an authentication policy, an authorization policy, and a session factory (this is needed for csrf
+ protection, and is why we need a secret). Defaults are `SessionAuthenticationPolicy`, `ACLAuthorizationPolicy` and
+ `UnencryptedCookieSessionFactoryConfig`. You can override it if you prefer.
+- it adds a `persona_js` request attribute containing the javascript code needed to make persona work.
+- it adds a `persona_button` request attribute containing html code for quickly putting a login button.
+- it defines the `/login` and `/logout` views to handle the persona workflow.
+- it defines a basic forbidden view with a login button.
+
+Configuration
+-------------
+
+You can override any policy or view defined by `pyramid_persona` by defining them the usual way.
+
+`pyramid_persona` defines the following settings :
+
+persona.secret
+ A secret string used to sign cookies. Required only if you do not defined another session factory.
+
+persona.audience
+ The protocol, domain name, and port of your site, as defined in the `persona documentation`_. Required.
+
+persona.login_route
+ The login route name. Optional, default is 'login'.
+
+persona.login_path
+ The login route path. Optional, default is '/login'.
+
+persona.logout_route
+ The logout route name. Optional, default is 'logout'.
+
+persona.logout_path
+ The logout route path. Optional, default is '/logout'.
+
+.. _`persona documentation`: https://developer.mozilla.org/en-US/docs/Persona/Remote_Verification_API
+
+Contact
+-------
+
+This project is made by Georges Dubus (`@georgesdubus`_). Bug reports and pull requests are welcome.
+
+.. _`@georgesdubus`: https://twitter.com/georgesdubus
67 pyramid_persona/__init__.py
@@ -0,0 +1,67 @@
+from pyramid.authentication import SessionAuthenticationPolicy
+from pyramid.authorization import ACLAuthorizationPolicy
+from pyramid.config import ConfigurationError
+from pyramid.interfaces import ISessionFactory
+from pyramid.session import UnencryptedCookieSessionFactoryConfig
+from pyramid_persona.utils import button, js
+from pyramid_persona.views import login, logout, forbidden
+
+
+def includeme(config):
+ """Include persona settings into a pyramid config.
+
+ This function does the following:
+
+ * Setup default authentication and authorization policies, and a default session factory.
+ Keep in mind that the sessions are not encrypted, if you need to store secret information in it, please
+ override the session factory.
+ * Add two request attributes :
+ * persona_js, the javascript code to inclue on a page to make persona work.
+ * persona_button, the html for a default login/logout button.
+ * Set login and logout views for use with persona.
+ * Set a forbidden view with a loggin button
+ """
+ settings = config.get_settings()
+
+ if not 'persona.audience' in settings:
+ raise ConfigurationError('Missing persona.audience settings. This is needed for security reasons. '
+ 'See https://developer.mozilla.org/en-US/docs/Persona/Security_Considerations for details.')
+
+ # Default authentication and authorization policies. Those are needed to remember the userid.
+ authn_policy = SessionAuthenticationPolicy()
+ authz_policy = ACLAuthorizationPolicy()
+ config.set_authentication_policy(authn_policy)
+ config.set_authorization_policy(authz_policy)
+
+ # A default session factory, needed for the csrf check.
+
+ secret = settings.get('persona.secret', None)
+ session_factory = UnencryptedCookieSessionFactoryConfig(secret)
+ config.set_session_factory(session_factory)
+
+ # Either a secret must be provided or the session factory must be overriden.
+ def check():
+ if config.registry.queryUtility(ISessionFactory) == session_factory and not secret:
+ raise ConfigurationError('If you do not override the session factory, you have to provide a persona.secret settings.')
+ config.action(None, check)
+
+
+ # Login and logout views.
+ login_route = settings.get('persona.login_route', 'login')
+ login_path = settings.get('persona.login_path', '/login')
+ config.add_route(login_route, login_path)
+ config.add_view(login, route_name=login_route, check_csrf=True)
+
+ logout_route = settings.get('persona.logout_route', 'logout')
+ logout_path = settings.get('persona.logout_path', '/logout')
+ config.add_route(logout_route, logout_path)
+ config.add_view(logout, route_name=logout_route, check_csrf=True)
+
+ # A simple 403 view, with a login button.
+ config.add_forbidden_view(forbidden)
+
+ # A quick access to the login button
+ config.add_request_method(button, 'persona_button', reify=True)
+
+ # The javascript needed by persona
+ config.add_request_method(js, 'persona_js', reify=True)
13 pyramid_persona/templates/forbidden.html
@@ -0,0 +1,13 @@
+<html>
+<head>
+ <title>403 Forbidden</title>
+ <script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
+ <script src="https://browserid.org/include.js" type="text/javascript"></script>
+ <script type="text/javascript">%(js)s</script>
+</head>
+<body>
+<h1>403 Forbidden</h1>
+Access was denied to this resource.<br/><br/>
+%(button)s
+</body>
+</html>
29 pyramid_persona/templates/persona.js
@@ -0,0 +1,29 @@
+$(function() {
+ $('#signin').click(function() { navigator.id.request(); return false;});
+
+ $('#signout').click(function() { navigator.id.logout(); return false;});
+
+ var currentUser = %(user)s;
+
+ navigator.id.watch({
+ loggedInUser: currentUser,
+ onlogin: function(assertion) {
+ $.ajax({
+ type: 'POST',
+ url: '%(login)s',
+ data: {assertion: assertion, csrf_token: "%(csrf_token)s"},
+ success: function(res, status, xhr) { window.location.reload(); },
+ error: function(res, status, xhr) { alert("login failure" + status); }
+ });
+ },
+ onlogout: function() {
+ $.ajax({
+ type: 'POST',
+ url: '%(logout)s',
+ data: {csrf_token: "%(csrf_token)s"},
+ success: function(res, status, xhr) { window.location.reload(); },
+ error: function(res, status, xhr) { alert("logout failure" + status); }
+ });
+ }
+ });
+});
51 pyramid_persona/tests.py
@@ -0,0 +1,51 @@
+import unittest
+from pyramid.interfaces import IAuthorizationPolicy, IAuthenticationPolicy
+from pyramid.testing import DummySecurityPolicy
+import requests
+
+from pyramid import testing
+
+
+class SecurityPolicy(DummySecurityPolicy):
+ remembered = None
+ forgotten = None
+ def remember(self, request, principal, **kw):
+ self.remembered = principal
+ return []
+
+ def forget(self, request):
+ self.forgotten = True
+ return []
+
+
+class ViewTests(unittest.TestCase):
+ def setUp(self):
+ self.config = testing.setUp()
+ self.config.add_settings({'persona.audience': 'http://someaudience'})
+# self.config.include('pyramid_persona')
+ self.security_policy = SecurityPolicy()
+ self.config.registry.registerUtility(self.security_policy, IAuthorizationPolicy)
+ self.config.registry.registerUtility(self.security_policy, IAuthenticationPolicy)
+
+ def tearDown(self):
+ testing.tearDown()
+
+ def test_login(self):
+ from .views import login
+ data = requests.get('http://personatestuser.org/email_with_assertion/http%3A%2F%2Fsomeaudience').json
+ email = data['email']
+ assertion = data['assertion']
+
+ request = testing.DummyRequest({'assertion': assertion})
+ response = login(request)
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(self.security_policy.remembered, email)
+
+ def test_logout(self):
+ from .views import logout
+ request = testing.DummyRequest()
+ response = logout(request)
+
+ self.assertEqual(response.status_code, 200)
+ self.assertTrue(self.security_policy.forgotten)
29 pyramid_persona/utils.py
@@ -0,0 +1,29 @@
+import markupsafe
+import pkg_resources
+from pyramid.security import authenticated_userid
+
+
+SIGNIN_HTML = "<img src='https://login.persona.org/i/sign_in_blue.png' id='signin' alt='sign-in button'/>"
+SIGNOUT_HTML = "<button id='signout'>logout</button>"
+
+
+def button(request):
+ """If the user is logged in, returns the logout button, otherwise returns the login button"""
+ if not authenticated_userid(request):
+ return markupsafe.Markup(SIGNIN_HTML)
+ else:
+ return markupsafe.Markup(SIGNOUT_HTML)
+
+
+def js(request):
+ """Returns the javascript needed to run persona"""
+ userid = authenticated_userid(request)
+ user = markupsafe.Markup("'%s'")%userid if userid else "null"
+ data = {
+ 'user': user,
+ 'login': '/login',
+ 'logout': '/logout',
+ 'csrf_token': request.session.get_csrf_token()
+ }
+ template = markupsafe.Markup(pkg_resources.resource_string('pyramid_persona', 'templates/persona.js'))
+ return template % data
24 pyramid_persona/views.py
@@ -0,0 +1,24 @@
+import browserid
+import pkg_resources
+from pyramid.response import Response
+from pyramid.security import remember, forget
+
+
+def login(request):
+ """View to check the persona assertion and remember the user"""
+ data = browserid.verify(request.POST['assertion'], request.registry.settings['persona.audience'])
+ headers = remember(request, data['email'])
+ return Response(headers=headers)
+
+
+def logout(request):
+ """View to forget the user"""
+ headers = forget(request)
+ return Response(headers=headers)
+
+
+def forbidden(request):
+ """A basic 403 view, with a login button"""
+ template = pkg_resources.resource_string('pyramid_persona', 'templates/forbidden.html')
+ html = template % {'js': request.persona_js, 'button': request.persona_button}
+ return Response(html, status='403 Forbidden')
33 setup.py
@@ -0,0 +1,33 @@
+import os
+
+from setuptools import setup, find_packages
+
+here = os.path.abspath(os.path.dirname(__file__))
+README = open(os.path.join(here, 'README.rst')).read()
+CHANGES = open(os.path.join(here, 'CHANGES.rst')).read()
+
+requires = [
+ 'pyramid',
+ 'PyBrowserID',
+ ]
+
+setup(name='pyramid_persona',
+ version='1.0',
+ description='pyramid_persona',
+ long_description=README + '\n\n' + CHANGES,
+ classifiers=[
+ "Programming Language :: Python",
+ "Framework :: Pyramid",
+ "Topic :: Internet :: WWW/HTTP",
+ ],
+ author='Georges Dubus',
+ author_email='georges.dubus@gmail.com',
+ url='https://github.com/madjar/pyramid_persona',
+ keywords='web pyramid pylons authentication persona',
+ packages=find_packages(),
+ include_package_data=True,
+ zip_safe=False,
+ install_requires=requires,
+ tests_require=requires,
+ test_suite="pyramid_persona",
+ )
Please sign in to comment.
Something went wrong with that request. Please try again.