This repository has been archived by the owner on Feb 8, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 308
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
1 parent
bf076dc
commit b1e0472
Showing
3 changed files
with
139 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 | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,4 @@ | ||
"""Gratipay authentication module. | ||
""" | ||
|
||
from __future__ import absolute_import, division, print_function, unicode_literals |
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 | 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()) |
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 | 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 |