From 86a1bbeda3da3b858b8561ec8c1096b81a61d93d Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Tue, 11 Jul 2017 10:02:22 +0530 Subject: [PATCH] Add controller to handle verification --- gratipay/security/user.py | 6 ++++ tests/py/test_www_email_auth.py | 55 ++++++++++++++++++++++++++++++++- www/auth/email/verify.html.spt | 48 ++++++++++++++++++++++++++++ 3 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 www/auth/email/verify.html.spt diff --git a/gratipay/security/user.py b/gratipay/security/user.py index 09cafcb4bf..a821caea0f 100644 --- a/gratipay/security/user.py +++ b/gratipay/security/user.py @@ -34,6 +34,12 @@ def from_id(cls, userid): """ return cls(Participant.from_id(userid)) + @classmethod + def from_email(cls, email): + """Find a participant based on id and return a User. + """ + return cls(Participant.from_email(email)) + @classmethod def from_username(cls, username): """Find a participant based on username and return a User. diff --git a/tests/py/test_www_email_auth.py b/tests/py/test_www_email_auth.py index ff5802cada..c4f99548b0 100644 --- a/tests/py/test_www_email_auth.py +++ b/tests/py/test_www_email_auth.py @@ -3,9 +3,10 @@ import json +from gratipay.security.authentication.email import create_signin_nonce, verify_nonce, NONCE_INVALID from gratipay.testing import Harness from gratipay.testing.email import QueuedEmailHarness - +from gratipay.utils import encode_for_querystring class TestSendLink(Harness): def test_returns_json(self): @@ -40,3 +41,55 @@ def test_sends_email(self): assert self.get_last_email()['to'] == 'alice ' assert 'Click the link below to sign in to Gratipay' in self.get_last_email()['body_text'] assert 'Click the button below to sign in to Gratipay' in self.get_last_email()['body_html'] + +class TestVerify(Harness): + def setUp(self): + self.make_participant('alice', email_address='alice@gratipay.com') + self.nonce = create_signin_nonce(self.db, 'alice@gratipay.com') + + def test_400_if_nonce_not_provided(self): + response = self.client.GxT('/auth/email/verify.html?email=abcd') + assert response.code == 400 + assert response.body == '`nonce` parameter must be provided' + + def test_400_if_email_not_provided(self): + response = self.client.GxT('/auth/email/verify.html?nonce=abcd') + assert response.code == 400 + assert response.body == '`email` parameter must be provided' + + def test_redirects_on_success(self): + link = self._get_link('alice@gratipay.com', self.nonce) + response = self.client.GET(link, raise_immediately=False) + + assert response.code == 302 + assert response.headers['Location'] == '/' + assert response.headers.cookie.get('session') + + def test_logs_in_on_success(self): + link = self._get_link('alice@gratipay.com', self.nonce) + response = self.client.GET(link, raise_immediately=False) + + assert response.headers.cookie.get('session') + + def test_invalidates_nonce_on_success(self): + link = self._get_link('alice@gratipay.com', self.nonce) + self.client.GET(link, raise_immediately=False) + + assert verify_nonce(self.db, 'alice@gratipay.com', self.nonce) == NONCE_INVALID + + def test_invalid_nonce(self): + response = self.client.GET(self._get_link('alice@gratipay.com', 'dummy_nonce')) + + assert "Sorry, that's a bad link." in response.body + + def test_expired_nonce(self): + self.db.run("UPDATE email_auth_nonces SET ctime = ctime - interval '1 day'") + response = self.client.GET(self._get_link('alice@gratipay.com', self.nonce)) + + assert "This link has expired. Please generate a new one." in response.body + + def _get_link(self, email, nonce): + encoded_email = encode_for_querystring(email) + + return '/auth/email/verify.html?nonce=%s&email=%s' % (nonce, encoded_email) + diff --git a/www/auth/email/verify.html.spt b/www/auth/email/verify.html.spt new file mode 100644 index 0000000000..9c97ae4789 --- /dev/null +++ b/www/auth/email/verify.html.spt @@ -0,0 +1,48 @@ +from aspen import Response + +from gratipay.security.authentication.email import verify_nonce, invalidate_nonce +from gratipay.security.authentication.email import NONCE_VALID, NONCE_INVALID, NONCE_EXPIRED +from gratipay.utils import decode_from_querystring +from gratipay.security.user import User + +[---] + +if 'nonce' not in request.qs: + raise Response(400, '`nonce` parameter must be provided') + +if 'email' not in request.qs: + raise Response(400, '`email` parameter must be provided') + +email = decode_from_querystring(request.qs['email']) +nonce = request.qs['nonce'] + +result = verify_nonce(website.db, email, nonce) + +if result == NONCE_VALID: + _user = User.from_email(email) # '_user' to avoid conflict with 'user' in template + _user.sign_in(response.headers.cookie) # TODO: What if user is already signed in? + invalidate_nonce(website.db, email, nonce) + website.redirect("/", response=response) # TODO: Why should response be passed? +else: + suppress_sidebar = True + +[---] text/html via jinja2 +{% extends "templates/base.html" %} +{% block content %} + {% if result == NONCE_EXPIRED %} +

{{ _("Link expired") }}

+

{{ _( "This link has expired. Please generate a new one.") }}

+ {# TODO: Add form for email right here? #} + {% else %} {# NONCE_INVALID #} +

{{ _("Bad Info") }}

+

+ {{ _( "Sorry, that's a bad link.") }} + +

+ + {{ _("If you think this is a mistake, please contact {a}support@gratipay.com.{_a}" + , a=(''|safe) + , _a=''|safe) }} +

+ {% endif %} +{% endblock %}