Skip to content

Commit

Permalink
Here goes nothing
Browse files Browse the repository at this point in the history
  • Loading branch information
vr2262 committed Jan 10, 2017
1 parent bf52549 commit e641ab6
Show file tree
Hide file tree
Showing 8 changed files with 208 additions and 87 deletions.
4 changes: 2 additions & 2 deletions dokomoforms/handlers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""All the Tornado RequestHandlers used in Dokomo Forms."""
from dokomoforms.handlers.root import Index, NotFound
from dokomoforms.handlers.auth import (
Login, Logout, GenerateToken, CheckLoginStatus
VerifyLoginHandler, Logout, GenerateToken, CheckLoginStatus
)
from dokomoforms.handlers.user.admin import (
AdminHomepageHandler,
Expand All @@ -22,7 +22,7 @@

__all__ = (
'Index', 'NotFound',
'Login', 'Logout', 'GenerateToken',
'VerifyLoginHandler', 'Logout', 'GenerateToken',
'AdminHomepageHandler', 'CheckLoginStatus',
'ViewSurveyHandler', 'ViewSurveyDataHandler', 'ViewUserAdminHandler',
'ViewSubmissionHandler',
Expand Down
98 changes: 25 additions & 73 deletions dokomoforms/handlers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,92 +16,43 @@
from passlib.hash import bcrypt_sha256

from dokomoforms.options import options
from dokomoforms.handlers.porter import get_verified_email, redis_kv
from dokomoforms.handlers.util import BaseHandler, authenticated_admin
from dokomoforms.models import User, Administrator, Email


class Login(BaseHandler):
class VerifyLoginHandler(BaseHandler):
"""Handlers for portier verification."""

"""POST here to verify the assertion generated by Mozilla Persona."""
def check_xsrf_cookie(self):
"""Disable XSRF check.
def _async_post(self,
http_client, url, input_data) -> tornado.concurrent.Future:
"""Asynchronously POSTs input_data to the url using http_client.fetch.
:param http_client: the HTTP client
:param url: the URL for POSTing
:param input_data: the data to POST
:return: a tornado.concurrent.Future that will contain the response
OpenID doesn't reply with _xsrf header.
https://github.com/portier/demo-rp/issues/10
"""
return tornado.gen.Task(
http_client.fetch,
url,
method='POST',
body=urllib.parse.urlencode(input_data),
)

@tornado.web.asynchronous
@tornado.gen.engine
def post(self):
"""POST to Mozilla's verifier service.
Accepts:
{ "assertion": <assertion> }
Then, POSTS to https://verifier.login.persona.org/verify to verify that
the assertion is valid. If so, attempts to log the user in by e-mail.
Responds with:
200 OK
{ "email": <e-mail address> }
:raise tornado.web.HTTPError: 400 Bad Request if the assertion is not
verified
422 Unprocessable Entity if the e-mail
address is not associated with a user
account.
"""
assertion = self.get_argument('assertion')
http_client = tornado.httpclient.AsyncHTTPClient()
url = options.persona_verification_url
input_data = {'assertion': assertion, 'audience': self.request.host}
response = yield self._async_post(http_client, url, input_data)
data = json_decode(response.body)
if data['status'] != 'okay':
raise tornado.web.HTTPError(400, 'Failed assertion test')

pass

async def post(self):
"""Verify the response from the portier broker."""
if 'error' in self.request.arguments:
error = self.get_argument('error')
description = self.get_argument('error_description')
raise tornado.web.HTTPError(400)
token = self.get_argument('id_token')
email = await get_verified_email(token)
try:
user = (
self.session
.query(User.id, User.name)
.join(Email)
.filter(Email.address == data['email'])
.query(models.User)
.filter_by(email=email)
.one()
)
except NoResultFound:
if data['email'] != options.admin_email:
_ = self.locale.translate
raise tornado.web.HTTPError(
422,
reason=_(
'There is no account associated with the e-mail'
' address {}'.format(data['email'])
),
)
with self.session.begin():
user = Administrator(
name=options.admin_name,
emails=[Email(address=options.admin_email)]
)
self.session.add(user)
cookie_options = {
'httponly': True,
}
if options.https:
cookie_options['secure'] = True
self.set_secure_cookie('user', user.id, **cookie_options)
self.write({'email': data['email']})
self.finish()
raise tornado.web.HTTPError(400)
self.set_secure_cookie(
'user', str(user.id),
httponly=True, secure=True)
self.redirect(self.get_argument('next', '/'))


class Logout(BaseHandler):
Expand All @@ -115,6 +66,7 @@ def post(self):
httponly.
"""
self.clear_cookie('user')
self.redirect('/')


class GenerateToken(BaseHandler): # We should probably do this in JS
Expand Down
146 changes: 146 additions & 0 deletions dokomoforms/handlers/portier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
"""Utility functions for portier login."""
from base64 import urlsafe_b64decode
from datetime import timedelta
import re

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import rsa

import jwt

import redis

from tornado.escape import json_decode
from tornado.httpclient import AsyncHTTPClient
import tornado.web

from minigrid.options import options


redis_kv = redis.StrictRedis.from_url(options.redis_url)


def b64dec(string):
"""Decode unpadded URL-safe Base64 strings.
Base64 values in JWTs and JWKs have their padding '=' characters stripped
during serialization. Before decoding, we must re-append padding characters
so that the encoded value's final length is evenly divisible by 4.
Taken from
github.com/portier/demo-rp/blob/6aee99fe126eceda527cae1f6da3f02a68401b6e
/server.py#L176-L184
"""
padding = '=' * ((4 - len(string) % 4) % 4)
return urlsafe_b64decode(string + padding)


async def get_verified_email(token):
"""Validate an Identity Token (JWT) and return its subject (email address).
In Portier, the subject field contains the user's verified email address.
This functions checks the authenticity of the JWT with the following steps:
1. Verify that the JWT has a valid signature from a trusted broker.
2. Validate that all claims are present and conform to expectations:
* ``aud`` (audience) must match this website's origin.
* ``iss`` (issuer) must match the broker's origin.
* ``exp`` (expires) must be in the future.
* ``iat`` (issued at) must be in the past.
* ``sub`` (subject) must be an email address.
* ``nonce`` (cryptographic nonce) must not have been seen previously.
3. If present, verify that the ``nbf`` (not before) claim is in the past.
Timestamps are allowed a few minutes of leeway to account for clock skew.
This demo relies on the `PyJWT`_ library to check signatures and validate
all claims except for ``sub`` and ``nonce``. Those are checked separately.
.. _PyJWT: https://github.com/jpadilla/pyjwt
Taken from
github.com/portier/demo-rp/blob/6aee99fe126eceda527cae1f6da3f02a68401b6e
/server.py#L240-L296
"""
keys = await discover_keys('https://broker.portier.io')
raw_header, _, _ = token.partition('.')
header = json_decode(b64dec(raw_header))
try:
pub_key = keys[header['kid']]
except KeyError:
raise tornado.web.HTTPError(400)
try:
payload = jwt.decode(
token, pub_key,
algorithms=['RS256'],
audience=options.minigrid_website_url,
issuer='https://broker.portier.io',
leeway=3*60,
)
except Exception as exc:
raise tornado.web.HTTPError(400)
if not re.match('.+@.+', payload['sub']):
raise tornado.web.HTTPError(400)
if not redis_kv.delete(payload['nonce']):
raise tornado.web.HTTPError(400)
return payload['sub']


def jwk_to_rsa(key):
"""Convert a deserialized JWK into an RSA Public Key instance.
Taken from
github.com/portier/demo-rp/blob/6aee99fe126eceda527cae1f6da3f02a68401b6e
/server.py#L233-L237
"""
e = int.from_bytes(b64dec(key['e']), 'big')
n = int.from_bytes(b64dec(key['n']), 'big')
return rsa.RSAPublicNumbers(e, n).public_key(default_backend())


async def discover_keys(broker):
"""Discover and return a Broker's public keys.
Returns a dict mapping from Key ID strings to Public Key instances.
Portier brokers implement the `OpenID Connect Discovery`_ specification.
This function follows that specification to discover the broker's current
cryptographic public keys:
1. Fetch the Discovery Document from ``/.well-known/openid-configuration``.
2. Parse it as JSON and read the ``jwks_uri`` property.
3. Fetch the URL referenced by ``jwks_uri`` to retrieve a `JWK Set`_.
4. Parse the JWK Set as JSON and extract keys from the ``keys`` property.
Portier currently only supports keys with the ``RS256`` algorithm type.
.. _OpenID Connect Discovery:
https://openid.net/specs/openid-connect-discovery-1_0.html
.. _JWK Set: https://tools.ietf.org/html/rfc7517#section-5
Taken from
github.com/portier/demo-rp/blob/6aee99fe126eceda527cae1f6da3f02a68401b6e
/server.py#L187-L206
"""
cache_key = 'jwks:' + broker
raw_jwks = redis_kv.get(cache_key)
if not raw_jwks:
http_client = AsyncHTTPClient()
url = broker + '/.well-known/openid-configuration'
response = await http_client.fetch(url)
discovery = json_decode(response.body)
if 'jwks_uri' not in discovery:
raise tornado.web.HTTPError(400)
raw_jwks = (await http_client.fetch(discovery['jwks_uri'])).body
redis_kv.setex(cache_key, timedelta(minutes=5), raw_jwks)
jwks = json_decode(raw_jwks)
if 'keys' not in jwks:
raise tornado.web.HTTPError(400)
return {
key['kid']: jwk_to_rsa(key)
for key in jwks['keys']
if key['alg'] == 'RS256'
}
21 changes: 21 additions & 0 deletions dokomoforms/handlers/root.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
"""Administrative handlers."""
from datetime import timedelta
from urllib.parse import urlencode
from uuid import uuid4

import tornado.web
import tornado.gen
import tornado.httpclient

from dokomoforms.handlers.portier import redis_kv
from dokomoforms.handlers.util import BaseHandler
from dokomoforms.models import Administrator, User
from dokomoforms.options import options


class Index(BaseHandler):
Expand All @@ -25,6 +31,21 @@ def get(self, msg=''):
message=msg,
)

def post(self):
"""Send login information to the portier broker."""
nonce = uuid4().hex
redis_kv.setex(nonce, timedelta(minutes=15), '')
query_args = urlencode({
'login_hint': self.get_argument('email'),
'scope': 'openid email',
'nonce': nonce,
'response_type': 'id_token',
'response_mode': 'form_post',
'client_id': options.minigrid_website_url,
'redirect_uri': options.minigrid_website_url + '/verify',
})
self.redirect('https://broker.portier.io/auth?' + query_args)


class NotFound(BaseHandler):

Expand Down
13 changes: 9 additions & 4 deletions dokomoforms/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@
{% if options.demo %}
<li role="presentation"><a class="demo-logout" role="menuitem" tabindex="-1" href="/demo/logout"><span class="glyphicon glyphicon-log-out icon-inline-left" id="logout"></span> Log Out</a></li>
{% else %}
<li role="presentation"><a class="btn-logout" role="menuitem" tabindex="-1" href="#"><span class="glyphicon glyphicon-log-out icon-inline-left" id="logout"></span> Log Out</a></li>
<li role="presentation"><form action="/user/logout" method="POST">
{% module xsrf_form_html() %}
<input type="submit" value=" Log Out" />
</form></li>
{% end %}
</ul>
</li>
Expand All @@ -62,7 +65,11 @@
{% elif options.demo %}
<li><a class="btn btn-block demo-login" href="/demo/login" id="login">Login or Register</a></li>
{% else %}
<li><a class="btn btn-block btn-login" href="#" id="login">Login or Register</a></li>
<li><form action="/" method="POST">
{% module xsrf_form_html() %}
E-mail: <input type="email" name="email" />
<input type="submit" value="Login or Register">
</form></li>
{% end %}
</ul>
</div>
Expand Down Expand Up @@ -100,8 +107,6 @@
<!-- VENDOR bundled -->
<script type="text/javascript" src="{{ static_url('dist/admin/js/vendor.js') }}"></script>

<!-- Persona for login stuff -->
<script src="https://login.persona.org/include.js"></script>

<script type="text/javascript">
window.CURRENT_USER_ID = '{{ current_user_id }}';
Expand Down
2 changes: 0 additions & 2 deletions dokomoforms/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ <h3 class="welcome-title">Dokomo<strong>Data</strong></h3>
{% end %}
{% if options.demo %}
<a class="btn btn-primary btn-large demo-login" href="/demo/login">Log In or Sign Up</a>
{% else %}
<a class="btn btn-primary btn-large btn-login" href="#">Log In or Sign Up</a>
{% end %}
<div id="msg"></div>
</div>
Expand Down
9 changes: 4 additions & 5 deletions dokomoforms/templates/view_visualize.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ <h4>Welcome {{ current_user }}</h4>


<script src="/static/lib.js"></script>
<script src="https://login.persona.org/include.js"></script>
<script src="/static/persona.js"></script>
<script src="https://d3js.org/d3.v3.js"></script>
<script src="/static/visualizations.js"></script>
{% if time_data %}
Expand All @@ -46,9 +44,10 @@ <h4>Welcome {{ current_user }}</h4>
{% block footer %}
<div class="bar bar-standard bar-footer">
<div class="bar-padded">
<a data-ignore="push" class="btn btn-block" href="#" id="logout">Logout</a>
<form action="/user/logout" method="POST">
{% module xsrf_form_html() %}
<input type="submit" value=" Log Out" />
</form>
</div>
</div>
<script src="https://login.persona.org/include.js"></script>
<script src="/static/persona.js"></script>
{% end %}

0 comments on commit e641ab6

Please sign in to comment.