Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 9c42178
Showing
12 changed files
with
363 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -0,0 +1,3 @@ | |||
.idea | |||
*.pyc | |||
pyramid_persona.egg-info/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -0,0 +1,4 @@ | |||
1.0 | |||
--- | |||
|
|||
- Initial version |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -0,0 +1,2 @@ | |||
include **.rst | |||
recursive-include pyramid_persona *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -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); } | |||
}); | |||
} | |||
}); | |||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -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') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -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", | |||
) |