Skip to content
This repository has been archived by the owner on Feb 8, 2018. It is now read-only.

Commit

Permalink
Add funcs to handle email nonces
Browse files Browse the repository at this point in the history
  • Loading branch information
rohitpaulk committed Jul 13, 2017
1 parent bf076dc commit b1e0472
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 0 deletions.
3 changes: 3 additions & 0 deletions gratipay/security/authentication/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
"""Gratipay authentication module.
"""

from __future__ import absolute_import, division, print_function, unicode_literals
59 changes: 59 additions & 0 deletions gratipay/security/authentication/email.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import datetime
import uuid

from aspen.utils import utcnow

class VerificationResult(object):
def __init__(self, name):
self.name = name
def __repr__(self):
return "<VerificationResult: %r>" % self.name
__str__ = __repr__

#: Signal that the nonce doesn't exist in our database
NONCE_INVALID = VerificationResult('Invalid')

#: Signal that the nonce exists, but has expired
NONCE_EXPIRED = VerificationResult('Expired')

#: Signal that the nonce exists, and is valid
NONCE_VALID = VerificationResult('Valid')

#: Time for nonce to expire
NONCE_EXPIRY_MINUTES = 60

def create_signin_nonce(db, email_address):
nonce = str(uuid.uuid4())
db.run("""
INSERT INTO email_auth_nonces (email_address, intent, nonce)
VALUES (
%(email_address)s,
'sign-in',
%(nonce)s
)
""", locals())

return nonce

def verify_nonce(db, email_address, nonce):
record = db.one("""
SELECT email_address, ctime, intent
FROM email_auth_nonces
WHERE nonce = %(nonce)s
AND email_address = %(email_address)s
""", locals(), back_as=dict)

if not record:
return NONCE_INVALID

if utcnow() - record['ctime'] > datetime.timedelta(minutes=NONCE_EXPIRY_MINUTES):
return NONCE_EXPIRED

return NONCE_VALID

def invalidate_nonce(db, email_address, nonce):
db.run("""
DELETE FROM email_auth_nonces
WHERE nonce = %(nonce)s
AND email_address = %(email_address)s
""", locals())
77 changes: 77 additions & 0 deletions tests/py/security/authentication/test_email_signin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function, unicode_literals
from uuid import UUID

from psycopg2 import IntegrityError
import pytest

from gratipay.security.authentication.email import create_signin_nonce, verify_nonce, invalidate_nonce
from gratipay.security.authentication.email import NONCE_VALID, NONCE_INVALID, NONCE_EXPIRED
from gratipay.testing import Harness

class TestCreateSigninNonce(Harness):

def setUp(self):
alice = self.make_participant('alice')
self.add_and_verify_email(alice, '[email protected]')

def _fetch_rec_from_db(self, nonce):
return self.db.one("SELECT * FROM email_auth_nonces WHERE nonce = %s", (nonce, ), back_as=dict)

def test_inserts_into_db(self):
nonce = create_signin_nonce(self.db, '[email protected]')
rec = self._fetch_rec_from_db(nonce)

assert rec['nonce'] == nonce
assert rec['intent'] == 'sign-in'
assert rec['email_address'] == '[email protected]'

def test_nonce_is_valid_uuid(self):
nonce = create_signin_nonce(self.db, '[email protected]')

assert UUID(nonce, version=4).__class__ == UUID


class TestVerifyNonce(Harness):

def setUp(self):
alice = self.make_participant('alice')
self.add_and_verify_email(alice, '[email protected]')

def test_valid_nonce(self):
nonce = create_signin_nonce(self.db, '[email protected]')
assert verify_nonce(self.db, '[email protected]', nonce) == NONCE_VALID

def test_expired_nonce(self):
nonce = create_signin_nonce(self.db, '[email protected]')
self.db.run("UPDATE email_auth_nonces SET ctime = ctime - interval '1 day'")
assert verify_nonce(self.db, '[email protected]', nonce) == NONCE_EXPIRED

def test_invalid_nonce(self):
create_signin_nonce(self.db, '[email protected]')
assert verify_nonce(self.db, '[email protected]', "dummy_nonce") == NONCE_INVALID

class TestInvalidateNonce(Harness):

def setUp(self):
alice = self.make_participant('alice')
self.add_and_verify_email(alice, '[email protected]')

def test_deletes_nonce(self):
nonce = create_signin_nonce(self.db, '[email protected]')
invalidate_nonce(self.db, '[email protected]', nonce)

assert verify_nonce(self.db, '[email protected]', nonce) == NONCE_INVALID

def test_only_deletes_one_nonce(self):
nonce1 = create_signin_nonce(self.db, '[email protected]')
nonce2 = create_signin_nonce(self.db, '[email protected]')
invalidate_nonce(self.db, '[email protected]', nonce1)

assert verify_nonce(self.db, '[email protected]', nonce1) == NONCE_INVALID
assert verify_nonce(self.db, '[email protected]', nonce2) == NONCE_VALID

def test_tolerates_invalidated_nonce(self):
nonce = create_signin_nonce(self.db, '[email protected]')
invalidate_nonce(self.db, '[email protected]', nonce)
invalidate_nonce(self.db, '[email protected]', nonce) # Should not throw an error

0 comments on commit b1e0472

Please sign in to comment.