Completing functional conversions

This commit is contained in:
= 2019-09-02 04:56:45 -05:00
parent 3cfd9840e8
commit edb16382f3
Signed by: kiichan
GPG key ID: 619DFD67F0976616
29 changed files with 1798 additions and 28 deletions

View file

@ -1,3 +1,21 @@
Mon, 02 Sep 2019 04:56:45 -0500
Keaton <kii-chan@tutanota.com>
Completing functional conversions
Not much is verified to work yet. All tests pass, however functionality
is not guaranteed. This update is being pushed to the dev git in order
for an easier setup of a test server with SSL.
Things changed: No idea, just squashed bugs that were causing tests to
fail.
Future development note: The original package supports insecure "qrl://"
schema, and as far as I can tell, that's not supported. I will probably
post on the SQRL forums about it, and remove the functionality completely
if the consensus is that it is not legitimate.
--------------------
Mon, 02 Sep 2019 02:03:23 -0500
Keaton <kii-chan@tutanota.com>
Still building initial conversions

View file

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
__author__ = 'Miroslav Shubernetskiy | Keaton Brown'
__email__ = 'miroslav@miki725.com | kii-chan@tutanota.com'
__version__ = '0.1.0'

View file

@ -1,5 +0,0 @@
from django.apps import AppConfig
class SqrlConfig(AppConfig):
name = 'sqrl'

44
sqrl/backends.py Normal file
View file

@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
from django.contrib.auth.backends import ModelBackend
from .models import SQRLIdentity
class SQRLModelBackend(ModelBackend):
"""
Custom SQRL Authentication backend which honors ``only_sqrl`` when enabled.
SQRL, by its spec, allows users to send ``only_sqrl`` flag to the server
which indicates to the server that it should only use SQRL
for authentication and disable all other methods of authentication.
This custom authentication backend implements that requirement.
It honors the ``only_sqrl`` spec and does not allow to authenticate
a user when following conditions are all ``True``:
* user successfully validated credentials using traditional auth method
* user has SQRL identity associated with their account
* :attr:`.models.SQRLIdentity.in_only_sqrl` is ``True``
"""
def authenticate(self, *args, **kwargs):
"""
Same as Django's ``ModelBackend.authenticate`` except
this method honors ``only_sqrl`` SQRL spec.
"""
user = super(SQRLModelBackend, self).authenticate(*args, **kwargs)
if user is None:
return
try:
sqrl_identity = user.sqrl_identity
except SQRLIdentity.DoesNotExist:
return user
else:
if sqrl_identity.is_only_sqrl and sqrl_identity.is_enabled:
return
return user
SQRL_MODEL_BACKEND = '{}.{}'.format(SQRLModelBackend.__module__, SQRLModelBackend.__name__)

View file

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
from collections import OrderedDict
from os import urandom
import os
import ed25519
from django.utils.crypto import constant_time_compare, salted_hmac
@ -9,6 +10,8 @@ from .utils import Base64, Encoder
class HMAC(object):
"""
Utility class for generating and verifying HMAC signatures.
@ -131,4 +134,4 @@ def generate_randomness(size=32):
str
:meth:`.Base64.encode` encoded random sample
"""
return Base64.encode(urandom(size))
return Base64.encode(bytearray(os.urandom(size)))

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from django.core.urlresolvers import reverse
from django.urls import reverse
from django.http import QueryDict
from .crypto import generate_randomness
@ -139,7 +139,7 @@ class SQRLInitialization(object):
"""
return (
'{scheme}://{host}{url}'
''.format(scheme='sqrl' if self.request.is_secure() else 'qrl',
''.format(scheme='sqrl', if self.request.is_secure() else 'qrl',
host=self.request.get_host(),
url=self.url)
)

View file

@ -72,7 +72,7 @@
max-width: 300px;
border-radius: 5%;
}
#sqrl-id img {
#sqrl-qr img {
margin: auto;
width: 100%;
padding-top: 3px;

View file

@ -1,5 +1,3 @@
'use strict';
(function() {
var get_next_url = function() {
var input = document.querySelectorAll('input[name="next"]');
@ -59,7 +57,7 @@
// The original can be found here:
// https://davidshimjs.github.io/qrcodejs/
var QRCode;
var QRCode;
! function() {
function a(a) {
this.mode = c.MODE_8BIT_BYTE, this.data = a, this.parsedData = [];

View file

@ -82,10 +82,10 @@
</div>
</form>
{% sqrl_status_url_script_tag session_sqrl %}
<script type="text/javascript">
SQRL_CHECK_URL="{% sqrl_status_url_script_tag session_sqrl %}"
document.getElementById('id_username').focus()
</script>
<script src="{% static 'sqrl/sqrl.js' %}"></script>
<script type="application/javascript" src="{% static 'sqrl/sqrl.js' %}"></script>
</div>
{% endblock %}

46
sqrl/templates/base.html Normal file
View file

@ -0,0 +1,46 @@
{% load static %}
<!doctype html>
<html lang="en">
<head>
<title>{% block title %}SQRL Test{% endblock %}</title>
</head>
<body>
<header>
{% block header %}
<div>SQRL Test Server</div>
<nav>
{% if user.is_authenticated %}
Logged in as {{ user.username }}
{% endif %}
<ul>
{% if user.is_authenticated %}
<li>
<a href="{% url 'logout' %}">Log out</a>
</li>
<li>
<a href="{% url 'sqrl:manage' %}">Manage SQRL</a>
</li>
{% else %}
<li>
<a href="{% url 'sqrl:login' %}">Log In</a>
</li>
{% endif %}
</ul>
</nav>
{% endblock %}
</header>
<div id="content">
{% block content %}
{% endblock content %}
</div>
{% block scripts %}
{% endblock %}
</body>
</html>

View file

@ -8,7 +8,7 @@
max-width: 300px;
border-radius: 5%;
}
#sqrl-id img {
#sqrl-qr img {
margin: auto;
width: 100%;
padding-top: 3px;
@ -37,5 +37,5 @@
{% endif %}
</form>
</div>
{% sqrl_status_url_script_tag session_sqrl %}
<script src="{% static sqrl/sqrl.js %}"></script>
<script>SQRL_CHECK_URL="{% sqrl_status_url_script_tag session_sqrl %}"</script>
<script type="application/javascript" src="{% static 'sqrl/sqrl.js' %}"></script>

View file

@ -7,9 +7,10 @@ from ..sqrl import SQRLInitialization
register = template.Library()
print(register)
@register.assignment_tag(takes_context=True)
@register.simple_tag(takes_context=True)
def sqrl(context):
return SQRLInitialization(context['request'])
@ -41,4 +42,4 @@ def sqrl_login_dropin(session_sqrl, method="login"):
@register.simple_tag
def sqrl_status_url_script_tag(sqrl):
url = reverse('sqrl:status', kwargs={'transaction': sqrl.nut.transaction_nonce})
return '<script>SQRL_CHECK_URL="{url}"</script>'.format(url=url)
return url

View file

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

1
sqrl/tests/__init__.py Normal file
View file

@ -0,0 +1 @@

View file

@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
import unittest
import mock
from django.contrib.auth.backends import ModelBackend
from ..backends import SQRLModelBackend
from ..models import SQRLIdentity
class TestSQRLModelBackend(unittest.TestCase):
@mock.patch.object(ModelBackend, 'authenticate')
def test_authenticate_no_user(self, mock_authenticate):
mock_authenticate.return_value = None
self.assertIsNone(SQRLModelBackend().authenticate(
username='user',
password='password'
))
@mock.patch.object(ModelBackend, 'authenticate')
def test_authenticate_no_sqrl_identity(self, mock_authenticate):
class UserMock(mock.MagicMock):
@property
def sqrl_identity(self):
raise SQRLIdentity.DoesNotExist
user = UserMock()
mock_authenticate.return_value = user
self.assertEqual(SQRLModelBackend().authenticate(
username='user',
password='password'
), user)
@mock.patch.object(ModelBackend, 'authenticate')
def test_authenticate_disabled(self, mock_authenticate):
user = mock.MagicMock()
user.sqrl_identity.is_only_sqrl = True
user.sqrl_identity.is_enabled = True
mock_authenticate.return_value = user
self.assertIsNone(SQRLModelBackend().authenticate(
username='user',
password='password'
))
@mock.patch.object(ModelBackend, 'authenticate')
def test_authenticate_enabled(self, mock_authenticate):
user = mock.MagicMock()
user.sqrl_identity.is_only_sqrl = False
user.sqrl_identity.is_enabled = True
mock_authenticate.return_value = user
self.assertEqual(SQRLModelBackend().authenticate(
username='user',
password='password'
), user)

186
sqrl/tests/test_crypto.py Normal file
View file

@ -0,0 +1,186 @@
# -*- coding: utf-8 -*-
import unittest
from collections import OrderedDict
from django.conf import settings
import os
import ed25519
import mock
from ..crypto import HMAC, Ed25519, generate_randomness
from ..models import SQRLNut
from ..utils import Base64
TESTING_MODULE = 'sqrl.crypto'
class TestHMAC(unittest.TestCase):
def setUp(self):
super(TestHMAC, self).setUp()
self.nut = SQRLNut(session_key='0123456789')
self.data = OrderedDict([
('ver', '1'),
('tif', 0),
('mac', 'value'),
('qry', 'foo?nut=bar'),
])
self.hmac = HMAC(self.nut, self.data)
def test_init(self):
hmac = HMAC(mock.sentinel.nut, mock.sentinel.data)
self.assertEqual(hmac.nut, mock.sentinel.nut)
self.assertEqual(hmac.data, mock.sentinel.data)
def test_sign_data(self):
temp = str(settings.SECRET_KEY)
settings.SECRET_KEY = "foo"
signature = self.hmac.sign_data()
self.assertIsInstance(signature, bytes)
self.assertEqual(len(signature), 20)
self.assertEqual(
signature,
b'R\xfc\xb2\xbd\x12\x85\xae\xb0>\xdd\xed\x16P\xc2\x82\xae\x06\x0c\xc5\xd3'
)
settings.SECRET_KEY = str(temp)
@mock.patch(TESTING_MODULE + '.salted_hmac')
def test_sign_data_mock(self, mock_salted_hmac):
signature = self.hmac.sign_data()
self.assertEqual(
signature,
mock_salted_hmac.return_value.digest.return_value
)
mock_salted_hmac.assert_called_once_with(
self.nut.session_key,
Base64.encode(b'ver=1\r\n'
b'tif=0\r\n'
b'qry=foo?nut=bar\r\n')
)
def test_sign_data_not_dict(self):
with self.assertRaises(AssertionError):
HMAC(mock.sentinel.nut, mock.sentinel.data).sign_data()
@mock.patch.object(HMAC, 'sign_data')
def test_is_signature_valid(self, mock_sign_data):
mock_sign_data.return_value = 'foo_signature'
self.assertTrue(self.hmac.is_signature_valid('foo_signature'))
self.assertFalse(self.hmac.is_signature_valid('foo-signature'))
def test_validation_loop(self):
signature = self.hmac.sign_data()
self.assertTrue(self.hmac.is_signature_valid(signature))
self.assertFalse(self.hmac.is_signature_valid(b'a' + signature[:-1]))
class TestEd25519(unittest.TestCase):
def setUp(self):
super(TestEd25519, self).setUp()
self.signing_key = (b'\xbbH\xdfx\xed\xc5\xdbR\x94\xe4\xff\xa6~5\xbb\xbd\xf2\x16&'
b'\xfc\x89\x8a\xc8\\\\\xeb\xea\x91Db~Hm+b\x88\xf2\x10\xfb:H'
b'\xe4\xfb0\x00\r\xe7n|\xa64\x05m@\xc8\xef"\x07k{O\xf0\xff%')
self.verifying_key = (b'm+b\x88\xf2\x10\xfb:H\xe4\xfb0\x00\r\xe7n|\xa64\x05m@'
b'\xc8\xef"\x07k{O\xf0\xff%')
self.data = b'data'
self.sig = Ed25519(self.verifying_key, self.signing_key, self.data)
def test_init(self):
sig = Ed25519(mock.sentinel.pub_key,
mock.sentinel.priv_key,
mock.sentinel.msg)
self.assertEqual(sig.public_key, mock.sentinel.pub_key)
self.assertEqual(sig.private_key, mock.sentinel.priv_key)
self.assertEqual(sig.msg, mock.sentinel.msg)
def test_sign_data(self):
signature = self.sig.sign_data()
self.assertIsInstance(signature, bytes)
self.assertEqual(len(signature), 64)
self.assertEqual(
signature,
b'\xac\xe0\x81\xc4\xd5\x7f\xd4\xe3\xc1\x03>\x0f\x90\xb5\x9eG<\xe0\xd41'
b'\x1cZ\xd7\x15F\xba\xdeS/\xfa\xbbL\x9bh\x8dn;\xcfP\xb1\x16\x14&d\xde'
b'\x97\x145\x90N[\xb9\xfc\x8e\x8a\x9e\xd2=\xad\x84\xcd\xf1\x93\x06'
)
@mock.patch('ed25519.SigningKey')
def test_sign_data_mock(self, mock_signing_key):
signature = self.sig.sign_data()
self.assertEqual(signature, mock_signing_key.return_value.sign.return_value)
mock_signing_key.assert_called_once_with(self.sig.private_key)
mock_signing_key.return_value.sign.assert_called_once_with(self.sig.msg)
def test_is_signature_valid(self):
signature = self.sig.sign_data()
self.assertTrue(self.sig.is_signature_valid(signature))
self.assertFalse(self.sig.is_signature_valid(b'a' + signature[:-1]))
@mock.patch('ed25519.VerifyingKey')
def test_is_signature_mock(self, mock_verifying_key):
is_valid = self.sig.is_signature_valid(mock.sentinel.signature)
self.assertTrue(is_valid)
mock_verifying_key.assert_called_once_with(self.sig.public_key)
mock_verifying_key.return_value.verify.assert_called_once_with(
mock.sentinel.signature, self.data
)
@mock.patch('ed25519.VerifyingKey')
def test_is_signature_mock_assertion_error(self, mock_verifying_key):
mock_verifying_key.return_value.verify.side_effect = AssertionError
is_valid = self.sig.is_signature_valid(mock.sentinel.signature)
self.assertFalse(is_valid)
mock_verifying_key.assert_called_once_with(self.sig.public_key)
mock_verifying_key.return_value.verify.assert_called_once_with(
mock.sentinel.signature, self.data
)
@mock.patch('ed25519.VerifyingKey')
def test_is_signature_mock_bas_signature_error(self, mock_verifying_key):
mock_verifying_key.return_value.verify.side_effect = ed25519.BadSignatureError
is_valid = self.sig.is_signature_valid(mock.sentinel.signature)
self.assertFalse(is_valid)
mock_verifying_key.assert_called_once_with(self.sig.public_key)
mock_verifying_key.return_value.verify.assert_called_once_with(
mock.sentinel.signature, self.data
)
class TestUtils(unittest.TestCase):
@mock.patch(TESTING_MODULE + '.bytearray', create=True)
@mock.patch.object(Base64, 'encode')
@mock.patch.object(os, 'urandom')
def test_generate_randomness_mock(self, mock_urandom, mock_encode, mock_bytearray):
_mock_bytearray = mock.MagicMock()
def _bytearray(a):
list(a)
return _mock_bytearray(a)
mock_bytearray.side_effect = _bytearray
randomness = generate_randomness()
self.assertEqual(randomness, mock_encode.return_value)
self.assertEqual(mock_urandom.call_count, 1)
mock_urandom.assert_called_with(32)
mock_encode.assert_called_once_with(_mock_bytearray.return_value)
def test_generate_randomness(self):
randomness = generate_randomness()
self.assertIsInstance(randomness, str)

View file

@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
import unittest
import mock
from ..exceptions import TIF, TIFException
TESTING_MODULE = 'sqrl.exceptions'
class TestTIF(unittest.TestCase):
def test_as_hex_string(self):
self.assertEqual(TIF(0x1).as_hex_string(), '1')
self.assertEqual(TIF(0x4).as_hex_string(), '4')
self.assertEqual(TIF(0x88).as_hex_string(), '88')
self.assertEqual(TIF(0x84).as_hex_string(), '84')
def test_breakdown(self):
self.assertDictEqual(TIF(0x34).breakdown(), {
'id_match': False,
'previous_id_match': False,
'ip_match': True,
'sqrl_disabled': False,
'not_supported': True,
'transient_failure': True,
'command_failed': False,
'client_failure': False,
'bad_id_association': False,
})
def test_update(self):
tif = TIF(0x3).update(TIF(0x40))
self.assertIsInstance(tif, TIF)
self.assertEqual(tif, 0x43)
def test_properties(self):
self.assertTrue(TIF(TIF.ID_MATCH).is_id_match)
self.assertTrue(TIF(TIF.PREVIOUS_ID_MATCH).is_previous_id_match)
self.assertTrue(TIF(TIF.IP_MATCH).is_ip_match)
self.assertTrue(TIF(TIF.SQRL_DISABLED).is_sqrl_disabled)
self.assertTrue(TIF(TIF.NOT_SUPPORTED).is_not_supported)
self.assertTrue(TIF(TIF.TRANSIENT_FAILURE).is_transient_failure)
self.assertTrue(TIF(TIF.COMMAND_FAILED).is_command_failed)
self.assertTrue(TIF(TIF.CLIENT_FAILURE).is_client_failure)
self.assertTrue(TIF(TIF.BAD_ID_ASSOCIATION).is_bad_id_association)
class TestTIFException(unittest.TestCase):
@mock.patch(TESTING_MODULE + '.TIF')
def test_init(self, mock_tif):
e = TIFException(mock.sentinel.tif)
self.assertEqual(e.tif, mock_tif.return_value)
mock_tif.assert_called_once_with(mock.sentinel.tif)

196
sqrl/tests/test_fields.py Normal file
View file

@ -0,0 +1,196 @@
# -*- coding: utf-8 -*-
import unittest
from collections import OrderedDict
import mock
from django import forms
from django.urls import Resolver404
from ..fields import (
Base64CharField,
Base64ConditionalPairsField,
Base64Field,
Base64PairsField,
ExtractedNextUrlField,
NextUrlField,
SQRLURLField,
SQRLURLValidator,
TildeMultipleValuesField,
TildeMultipleValuesFieldChoiceField,
)
from ..utils import Base64
TESTING_MODULE = 'sqrl.fields'
class TestNextUrlField(unittest.TestCase):
def test_to_python_empty(self):
self.assertEqual(NextUrlField().to_python(None), '')
@mock.patch(TESTING_MODULE + '.resolve')
def test_to_python_valid(self, mock_resolve):
value = 'http://example.com/path/here/?querystring=here'
self.assertEqual(NextUrlField().to_python(value), '/path/here/')
@mock.patch(TESTING_MODULE + '.resolve')
def test_to_python_invalid(self, mock_resolve):
mock_resolve.side_effect = Resolver404
value = 'http://example.com/path/here/?querystring=here'
with self.assertRaises(forms.ValidationError):
NextUrlField().to_python(value)
class TestExtractedNextUrlField(unittest.TestCase):
def test_to_python_empty(self):
self.assertEqual(ExtractedNextUrlField().to_python(None), '')
def test_to_python_next_not_present(self):
value = 'http://example.com/path/here/?querystring=here'
with self.assertRaises(forms.ValidationError):
ExtractedNextUrlField().to_python(value)
@mock.patch(TESTING_MODULE + '.resolve')
def test_to_python(self, mock_resolve):
value = 'http://example.com/path/here/?next=/next/here/'
self.assertEqual(
ExtractedNextUrlField().to_python(value),
'/next/here/'
)
class TestSQRLURLValidator(unittest.TestCase):
def test_valid(self):
self.assertIsNone(
SQRLURLValidator()('qrl://example.com:8000/sqrl/?nut=hello')
)
self.assertIsNone(
SQRLURLValidator()('sqrl://example.com:8000/sqrl/?nut=hello')
)
def test_invalid(self):
with self.assertRaises(forms.ValidationError):
SQRLURLValidator()('http://example.com:Base64PairsField8000/sqrl/?nut=hello')
class TestSQRLURLField(unittest.TestCase):
def test_default_validators(self):
validator_types = list(map(type, SQRLURLField.default_validators))
self.assertIn(SQRLURLValidator, validator_types)
class TestBase64Field(unittest.TestCase):
def test_empty_value(self):
value = Base64Field().to_python(None)
self.assertEqual(value, b'')
self.assertIsInstance(value, bytes)
def test_value(self):
value = Base64Field().to_python('aGVsbG8')
self.assertEqual(value, b'hello')
self.assertIsInstance(value, bytes)
def test_value_invalid(self):
with self.assertRaises(forms.ValidationError):
Base64Field().to_python('hello')
class TestBase64CharField(unittest.TestCase):
def test_empty_value(self):
value = Base64CharField().to_python(None)
self.assertEqual(value, '')
self.assertIsInstance(value, str)
def test_value(self):
value = Base64CharField().to_python('aGVsbG8')
self.assertEqual(value, 'hello')
self.assertIsInstance(value, str)
def test_value_invalid(self):
with self.assertRaises(forms.ValidationError):
Base64CharField().to_python('z4A')
class TestBase64PairsField(unittest.TestCase):
def test_to_python_empty(self):
value = Base64PairsField().to_python(None)
self.assertEqual(value, OrderedDict())
def test_to_python(self):
value = Base64.encode(
b'ver=1\r\n'
b'foo=bar\r\n'
)
value = Base64PairsField().to_python(value)
self.assertEqual(value, OrderedDict([
('ver', '1'),
('foo', 'bar'),
]))
def test_to_python_not_pars(self):
value = Base64.encode(
b'ver=1\r\n'
b'foo\r\n'
)
with self.assertRaises(forms.ValidationError):
Base64PairsField().to_python(value)
def test_to_python_not_multiline(self):
value = Base64.encode(
b'ver=1'
)
with self.assertRaises(forms.ValidationError):
Base64PairsField().to_python(value)
class TestBase64ConditionalPairsField(unittest.TestCase):
def test_to_python_not_pars(self):
value = Base64.encode(
b'foo'
)
self.assertEqual(Base64ConditionalPairsField().to_python(value), 'foo')
class TestTildeMultipleValuesField(unittest.TestCase):
def test_empty(self):
self.assertListEqual(TildeMultipleValuesField().to_python(None), [])
def test_to_python(self):
self.assertListEqual(
TildeMultipleValuesField().to_python('hello~world'),
['hello', 'world']
)
class TestTildeMultipleValuesFieldChoiceField(unittest.TestCase):
def test_valid(self):
field = TildeMultipleValuesFieldChoiceField(choices=[
('hello', 'hello'),
('world', 'world'),
])
self.assertListEqual(
field.clean('hello~world'),
['hello', 'world']
)
def test_invalid(self):
field = TildeMultipleValuesFieldChoiceField(choices=[
('hello', 'hello'),
])
with self.assertRaises(forms.ValidationError):
field.clean('hello~world')

641
sqrl/tests/test_forms.py Normal file
View file

@ -0,0 +1,641 @@
# -*- coding: utf-8 -*-
import unittest
from collections import OrderedDict
import ed25519
import mock
from django import forms, test
from django.contrib.auth import SESSION_KEY, get_user_model
from django.utils.timezone import now
from ..crypto import HMAC, Ed25519, generate_randomness
from ..forms import PasswordLessUserCreationForm, RequestForm
from ..models import SQRLIdentity, SQRLNut
from ..utils import Base64, Encoder
TESTING_MODULE = 'sqrl.forms'
class TestRequestForm(test.TestCase):
def get_key_pair(self):
signing_key, verifying_key = ed25519.create_keypair()
signing_key = signing_key.to_bytes()
verifying_key = verifying_key.to_bytes()
return signing_key, verifying_key
def _setup(self):
hasattr(self, 'nut') and self.nut.delete()
hasattr(self, 'user') and self.user.delete()
hasattr(self, 'identity') and self.identity.delete()
self.nut = SQRLNut(
nonce=generate_randomness(),
transaction_nonce=generate_randomness(),
session_key=generate_randomness(20),
is_transaction_complete=False,
ip_address='127.0.0.1',
timestamp=now(),
)
self.nut.save()
self.user = get_user_model().objects.create(
username='test_clean_session',
)
self.identity = SQRLIdentity(
user_id=self.user.pk,
public_key=Base64.encode(self.public_key),
server_unlock_key=Base64.encode(self.server_unlock_key),
verify_unlock_key=Base64.encode(self.verify_unlock_key),
is_enabled=True,
is_only_sqrl=False,
)
self.identity.save()
self.server_data = OrderedDict([
('ver', 1),
('nut', self.nut.nonce),
('tif', '8'),
('qry', '/sqrl/auth/?nut=nonce'),
('sfn', 'Test Server'),
])
self.server_data['mac'] = HMAC(self.nut, self.server_data).sign_data()
self.server_data = Encoder.normalize(self.server_data)
self.client_data = OrderedDict([
('ver', 1),
('cmd', self.cmd),
('opt', ['sqrlonly']),
])
if self.include_idk:
self.client_data['idk'] = self.public_key
if self.include_pidk:
self.client_data['pidk'] = self.previous_public_key
if self.include_suk:
self.client_data['suk'] = self.server_unlock_key
if self.include_vuk:
self.client_data['vuk'] = self.verify_unlock_key
self.payload_client_data = Encoder.normalize(OrderedDict(
(k, v if not isinstance(v, list) else '~'.join(v))
for k, v in self.client_data.items()
))
self.data = {
'client': Encoder.base64_dumps(self.client_data),
'server': Encoder.base64_dumps(self.server_data),
}
self.signable_data = (
self.data['client'] + self.data['server']
).encode('ascii')
if self.include_ids:
self.data['ids'] = Ed25519(
self.public_key, self.identity_key, self.signable_data
).sign_data()
if self.include_pids:
self.data['pids'] = Ed25519(
self.previous_public_key, self.previous_identity_key, self.signable_data
).sign_data()
if self.include_urs:
self.data['urs'] = Ed25519(
self.verify_unlock_key, self.unlock_key, self.signable_data
).sign_data()
self.cleaned_data = self.data.copy()
self.cleaned_data.update({
'client': self.client_data,
'server': self.server_data,
})
self.form = RequestForm(self.nut, data=self.data)
def setUp(self):
super(TestRequestForm, self).setUp()
self.cmd = ['query']
self.identity_key, self.public_key = self.get_key_pair()
self.previous_identity_key, self.previous_public_key = self.get_key_pair()
self.unlock_key, self.verify_unlock_key = self.get_key_pair()
self.server_unlock_key = b'hello'
self.include_idk = True
self.include_pidk = True
self.include_suk = True
self.include_vuk = True
self.include_ids = True
self.include_pids = True
self.include_urs = True
self._setup()
def tearDown(self):
self.user and self.user.delete()
self.identity and self.identity.delete()
self.nut and self.nut.delete()
super(TestRequestForm, self).tearDown()
def test_init(self):
form = RequestForm(mock.sentinel.nut)
self.assertEqual(form.nut, mock.sentinel.nut)
self.assertIsNone(form.session)
self.assertIsNone(form.identity)
self.assertIsNone(form.previous_identity)
def test_clean_client(self):
self.form.cleaned_data = {'client': self.payload_client_data}
self.assertEqual(self.form.clean_client(), dict(self.client_data))
def test_clean_client_invalid(self):
self.form.cleaned_data = {
'client': {
'ver': '2',
}
}
with self.assertRaises(forms.ValidationError):
self.form.clean_client()
def test_clean_server_not_dict(self):
self.form.cleaned_data = {'server': mock.sentinel.server_data}
self.assertEqual(self.form.clean_server(), mock.sentinel.server_data)
def test_clean_server(self):
self.form.cleaned_data = {
'server': self.server_data
}
self.assertEqual(
self.form.clean_server(),
self.server_data
)
def test_clean_server_mac_not_base64(self):
self.server_data['mac'] = 'hello'
self.form.cleaned_data = {'server': self.server_data}
with self.assertRaises(forms.ValidationError):
self.form.clean_server()
def test_clean_server_mismatch_nut(self):
self.server_data['nut'] = self.server_data['nut'][::-1]
self.form.cleaned_data = {'server': self.server_data}
with self.assertRaises(forms.ValidationError):
self.form.clean_server()
def test_clean_server_mismatch_missing_mac(self):
del self.server_data['mac']
self.form.cleaned_data = {'server': self.server_data}
with self.assertRaises(forms.ValidationError):
self.form.clean_server()
def test_clean_server_invalid_mac(self):
self.server_data['mac'] = self.server_data['mac'][::-1]
self.form.cleaned_data = {'server': self.server_data}
with self.assertRaises(forms.ValidationError):
self.form.clean_server()
def test_clean_ids(self):
self.form.cleaned_data = {
'client': self.client_data,
'ids': self.data['ids'],
}
self.assertEqual(
self.form.clean_ids(),
self.data['ids']
)
def test_clean_ids_invalid(self):
self.form.cleaned_data = {
'client': self.client_data,
'ids': self.data['ids'][::-1],
}
with self.assertRaises(forms.ValidationError):
self.form.clean_ids()
def test_clean_pids_valid(self):
self.form.cleaned_data = {
'client': self.client_data,
'pids': self.data['pids'],
}
self.assertEqual(
self.form.clean_pids(),
self.data['pids']
)
def test_clean_pids_invalid(self):
self.form.cleaned_data = {
'client': self.client_data,
'pids': self.data['pids'][::-1],
}
with self.assertRaises(forms.ValidationError):
self.form.clean_pids()
def test_clean_pids_missing_pids(self):
self.form.cleaned_data = {
'client': self.client_data,
}
with self.assertRaises(forms.ValidationError):
self.form.clean_pids()
def test_clean_pids_missing_pidk(self):
self.client_data.pop('pidk')
self.form.cleaned_data = {
'client': self.client_data,
'pids': self.data['pids'],
}
with self.assertRaises(forms.ValidationError):
self.form.clean_pids()
def test_clean_urs(self):
self.form.cleaned_data = self.cleaned_data
self.form.identity = self.identity
self.assertEqual(
self.form._clean_urs(),
self.data['urs']
)
def test_clean_urs_invalid(self):
self.form.cleaned_data = {
'client': self.client_data,
'urs': self.data['urs'][::-1],
}
self.form.identity = self.identity
with self.assertRaises(forms.ValidationError):
self.form._clean_urs()
def test_clean_urs_no_suk(self):
self.form.cleaned_data = {
'client': self.client_data,
'urs': self.data['urs'],
}
self.form.identity = self.identity
self.identity.server_unlock_key = None
with self.assertRaises(forms.ValidationError):
self.form._clean_urs()
def test_clean_urs_no_vuk(self):
self.form.cleaned_data = {
'client': self.client_data,
'urs': self.data['urs'],
}
self.form.identity = self.identity
self.identity.verify_unlock_key = None
with self.assertRaises(forms.ValidationError):
self.form._clean_urs()
def test_clean_cmd_query(self):
self.cmd = ['query']
self._setup()
self.form.cleaned_data = {
'client': self.client_data,
}
self.assertIsNone(self.form._clean_client_cmd())
def test_clean_cmd_ident(self):
self.cmd = ['ident']
self._setup()
self.form.cleaned_data = {
'client': self.client_data,
}
self.assertIsNone(self.form._clean_client_cmd())
def test_clean_cmd_ident_no_suk_vuk_without_identity(self):
self.cmd = ['ident']
self.include_suk = None
self.include_vuk = None
self._setup()
self.form.cleaned_data = {
'client': self.client_data,
}
with self.assertRaises(forms.ValidationError):
self.form._clean_client_cmd()
def test_clean_cmd_ident_suk_vuk_with_identity(self):
self.cmd = ['ident']
self._setup()
self.form.identity = self.identity
self.form.cleaned_data = {
'client': self.client_data,
}
with self.assertRaises(forms.ValidationError):
self.form._clean_client_cmd()
def test_clean_cmd_ident_with_disable(self):
self.cmd = ['ident', 'disable']
self._setup()
self.form.cleaned_data = {
'client': self.client_data,
}
with self.assertRaises(forms.ValidationError):
self.form._clean_client_cmd()
def test_clean_cmd_ident_no_urs_with_previous_identity(self):
self.cmd = ['ident']
self._setup()
self.form.previous_identity = self.identity
self.form.cleaned_data = {
'client': self.client_data,
}
with self.assertRaises(forms.ValidationError):
self.form._clean_client_cmd()
def test_clean_cmd_disable(self):
self.cmd = ['disable']
self._setup()
self.form.identity = self.identity
self.form.cleaned_data = {
'client': self.client_data,
}
self.assertIsNone(self.form._clean_client_cmd())
def test_clean_cmd_disable_no_identity(self):
self.cmd = ['disable']
self._setup()
self.form.cleaned_data = {
'client': self.client_data,
}
with self.assertRaises(forms.ValidationError):
self.form._clean_client_cmd()
def test_clean_cmd_disable_with_enable(self):
self.cmd = ['disable', 'enable']
self._setup()
self.form.identity = self.identity
self.form.cleaned_data = {
'client': self.client_data,
}
with self.assertRaises(forms.ValidationError):
self.form._clean_client_cmd()
def test_clean_cmd_enable(self):
self.cmd = ['enable']
self._setup()
self.form.identity = self.identity
self.form.cleaned_data = self.cleaned_data
self.assertIsNone(self.form._clean_client_cmd())
def test_clean_cmd_enable_no_urs(self):
self.cmd = ['enable']
self.include_urs = False
self._setup()
self.form.identity = self.identity
self.form.cleaned_data = self.cleaned_data
with self.assertRaises(forms.ValidationError):
self.form._clean_client_cmd()
def test_clean_cmd_enable_no_identity(self):
self.cmd = ['enable']
self._setup()
self.form.cleaned_data = self.cleaned_data
with self.assertRaises(forms.ValidationError):
self.form._clean_client_cmd()
def test_clean_cmd_enable_with_disable(self):
self.cmd = ['enable', 'disable']
self._setup()
self.form.identity = self.identity
self.form.cleaned_data = self.cleaned_data
with self.assertRaises(forms.ValidationError):
self.form._clean_client_cmd()
def test_clean_cmd_remove(self):
self.cmd = ['remove']
self._setup()
self.form.identity = self.identity
self.form.cleaned_data = self.cleaned_data
self.assertIsNone(self.form._clean_client_cmd())
def test_clean_cmd_remove_no_identity(self):
self.cmd = ['remove']
self._setup()
self.form.cleaned_data = self.cleaned_data
with self.assertRaises(forms.ValidationError):
self.form._clean_client_cmd()
def test_clean_cmd_remove_no_urs(self):
self.cmd = ['remove']
self.include_urs = False
self._setup()
self.form.identity = self.identity
self.form.cleaned_data = self.cleaned_data
with self.assertRaises(forms.ValidationError):
self.form._clean_client_cmd()
def test_clean_cmd_remove_with_other_cmd(self):
self.cmd = ['remove', 'ident']
self._setup()
self.form.identity = self.identity
self.form.cleaned_data = self.cleaned_data
with self.assertRaises(forms.ValidationError):
self.form._clean_client_cmd()
def test_clean_session_empty(self):
self.form.session = {}
self.assertIsNone(self.form._clean_session())
def test_clean_session_user_not_found(self):
assert not get_user_model().objects.filter(pk=1000).first()
self.form.session = {
SESSION_KEY: '1000',
}
self.assertIsNone(self.form._clean_session())
def test_clean_session_user_not_int(self):
self.form.session = {
SESSION_KEY: 'aaa',
}
self.assertIsNone(self.form._clean_session())
def test_clean_session_no_sqrl_identity(self):
self.identity.delete()
self.identity = None
self.form.session = {
SESSION_KEY: str(self.user.pk),
}
self.assertIsNone(self.form._clean_session())
def test_clean_session_public_key_not_matches(self):
self.identity.public_key = self.identity.public_key[::-1]
self.identity.save()
self.form.session = {
SESSION_KEY: str(self.user.pk),
}
self.form.cleaned_data = self.cleaned_data
with self.assertRaises(forms.ValidationError):
self.form._clean_session()
def test_clean_session_user_code_mismatch(self):
self.form.identity = self.identity
self.form.session = {
SESSION_KEY: str(self.user.pk + 1),
}
self.form.cleaned_data = self.cleaned_data
with self.assertRaises(forms.ValidationError):
self.form._clean_session()
def test_clean(self):
self.form.cleaned_data = self.cleaned_data
actual = self.form.clean()
self.assertEqual(actual, self.cleaned_data)
@mock.patch.object(RequestForm, 'find_identities')
@mock.patch.object(RequestForm, '_clean_client_cmd')
@mock.patch.object(RequestForm, '_clean_urs')
@mock.patch.object(RequestForm, 'find_session')
@mock.patch.object(RequestForm, '_clean_session')
@mock.patch.object(forms.Form, 'clean')
def test_clean_mock(self,
mock_super_clean,
mock_clean_session,
mock_find_session,
mock_clean_urs,
mock_clean_client_cmd,
mock_find_identities):
mock_super_clean.return_value = mock.sentinel.cleaned_data
actual = self.form.clean()
self.assertEqual(actual, mock.sentinel.cleaned_data)
mock_super_clean.assert_called_once_with()
mock_clean_session.assert_called_once_with()
mock_find_session.assert_called_once_with()
mock_clean_urs.assert_called_once_with()
mock_clean_client_cmd.assert_called_once_with()
mock_find_identities.assert_called_once_with()
@mock.patch(TESTING_MODULE + '.SessionMiddleware')
def test_find_session(self, mock_session_middleware):
self.form.find_session()
self.assertEqual(
self.form.session,
mock_session_middleware.return_value.SessionStore.return_value
)
mock_session_middleware.return_value.SessionStore.assert_called_once_with(
self.nut.session_key,
)
def test_find_identities(self):
self.form.cleaned_data = self.cleaned_data
self.form.find_identities()
self.assertIsNotNone(self.form.identity)
self.assertIsInstance(self.form.identity, SQRLIdentity)
self.assertEqual(self.form.identity.public_key, self.identity.public_key)
self.assertIsNone(self.form.previous_identity)
@mock.patch.object(RequestForm, '_get_identity')
def test_find_identities_mock(self, mock_get_identity):
self.form.cleaned_data = self.cleaned_data
mock_get_identity.side_effect = mock.sentinel.identity, mock.sentinel.previous_identity
self.form.find_identities()
self.assertEqual(self.form.identity, mock.sentinel.identity)
self.assertEqual(self.form.previous_identity, mock.sentinel.previous_identity)
mock_get_identity.assert_has_calls([
mock.call(self.public_key),
mock.call(self.previous_public_key),
])
@mock.patch(TESTING_MODULE + '.SQRLIdentity')
def test_get_identity(self, mock_sqrl_identity):
actual = self.form._get_identity(self.public_key)
self.assertEqual(
actual,
mock_sqrl_identity.objects.filter.return_value.first.return_value
)
mock_sqrl_identity.objects.filter.assert_called_once_with(
public_key=Base64.encode(self.public_key)
)
def test_get_identity_no_key(self):
self.assertIsNone(self.form._get_identity(None))
class TestRandomPasswordUserCreationForm(unittest.TestCase):
def test_init(self):
self.assertIn('password1', PasswordLessUserCreationForm.base_fields)
self.assertIn('password2', PasswordLessUserCreationForm.base_fields)
form = PasswordLessUserCreationForm()
self.assertNotIn('password1', form.fields)
self.assertNotIn('password2', form.fields)
def test_save(self):
form = PasswordLessUserCreationForm({'username': 'test'})
self.assertTrue(form.is_valid())
user = form.save()
self.assertEqual(user.username, 'test')
self.assertTrue(user.password.startswith('!'))
user.delete()

View file

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
import unittest
import mock
from ..managers import SQRLNutManager
class TestSQRLNutManager(unittest.TestCase):
@mock.patch.object(SQRLNutManager, 'get_queryset')
@mock.patch.object(SQRLNutManager, 'create')
def test_replace_or_create(self, mock_create, mock_getqueryset):
actual = SQRLNutManager().replace_or_create(
session_key=mock.sentinel.session_key,
nonce=mock.sentinel.nonce,
transaction_nonce=mock.sentinel.transaction_nonce,
is_transaction_complete=False,
ip_address=mock.sentinel.ip_address,
)
self.assertEqual(actual, mock_create.return_value)
mock_getqueryset.return_value.filter.assert_called_once_with(
session_key=mock.sentinel.session_key
)
mock_getqueryset.return_value.filter.return_value.delete.assert_called_once_with()
mock_create.assert_called_once_with(
session_key=mock.sentinel.session_key,
nonce=mock.sentinel.nonce,
transaction_nonce=mock.sentinel.transaction_nonce,
is_transaction_complete=False,
ip_address=mock.sentinel.ip_address,
)

32
sqrl/tests/test_models.py Normal file
View file

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
import unittest
import mock
from ..managers import SQRLNutManager
from ..models import SQRLNut
TESTING_MODULE = 'sqrl.models'
class TestSQRLNut(unittest.TestCase):
def test_objects(self):
self.assertIsInstance(SQRLNut.objects, SQRLNutManager)
def test_str(self):
self.assertEqual(
str(SQRLNut(nonce='nonce')),
'nonce'
)
@mock.patch(TESTING_MODULE + '.generate_randomness')
@mock.patch.object(SQRLNut, 'delete')
@mock.patch.object(SQRLNut, 'save')
def test_renew(self, mock_save, mock_delete, mock_generate_randomness):
nut = SQRLNut(nonce='nonce')
self.assertIsNone(nut.renew())
self.assertEqual(nut.nonce, mock_generate_randomness.return_value)
mock_save.assert_called_once_with()
mock_generate_randomness.assert_called_once_with()

View file

@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
import unittest
from collections import OrderedDict
import mock
from django.test.utils import override_settings
from ..crypto import HMAC
from ..response import SQRLHttpResponse
from ..utils import Encoder
TESTING_MODULE = 'sqrl.response'
class TestSQRLHttpResponse(unittest.TestCase):
def setUp(self):
super(TestSQRLHttpResponse, self).setUp()
self.data = OrderedDict([
('ver', 1),
('nut', b'nonce'),
('tif', '8'),
('qry', '/sqrl/auth/?nut=nonce'),
('sfn', 'Test Server'),
])
self.nut = mock.MagicMock(session_key='session')
@override_settings(DEBUG=False)
def test_response(self):
response = SQRLHttpResponse(self.nut, self.data)
expected_data = self.data.copy()
expected_data['mac'] = HMAC(self.nut, self.data).sign_data()
self.assertEqual(
response.content,
Encoder.base64_dumps(expected_data).encode('ascii')
)
self.assertEqual(response['Content-Length'], str(len(response.content)))
self.assertEqual(response['Content-Type'], 'application/sqrl')
@override_settings(DEBUG=False)
def test_response_without_nut(self):
response = SQRLHttpResponse(None, self.data)
self.assertEqual(
response.content,
Encoder.base64_dumps(self.data).encode('ascii')
)
self.assertEqual(response['Content-Length'], str(len(response.content)))
self.assertEqual(response['Content-Type'], 'application/sqrl')
@override_settings(DEBUG=True)
@mock.patch(TESTING_MODULE + '.log')
def test_response_debug(self, mock_log):
response = SQRLHttpResponse(self.nut, self.data)
self.assertEqual(response['X-SQRL-ver'], '1')
self.assertEqual(response['X-SQRL-nut'], 'bm9uY2U')
self.assertEqual(response['X-SQRL-tif'], '8')
self.assertEqual(response['X-SQRL-qry'], '/sqrl/auth/?nut=nonce')
self.assertEqual(response['X-SQRL-sfn'], 'Test Server')
self.assertIn('X-SQRL-mac', response)

116
sqrl/tests/test_sqrl.py Normal file
View file

@ -0,0 +1,116 @@
# -*- coding: utf-8 -*-
import unittest
import mock
from sqrl.managers import SQRLNutManager
from ..sqrl import SQRLInitialization
TESTING_MODULE = 'sqrl.sqrl'
class TestSQRLInitialization(unittest.TestCase):
def test_init(self):
sqrl = SQRLInitialization(mock.sentinel.request, mock.sentinel.nut)
self.assertEqual(sqrl.request, mock.sentinel.request)
self.assertEqual(sqrl.nut, mock.sentinel.nut)
def test_get_or_create_session_key_exists(self):
m = mock.MagicMock()
self.assertEqual(
SQRLInitialization(m).get_or_create_session_key(),
m.session.session_key
)
self.assertFalse(m.session.create.called)
def test_get_or_create_session_key_create(self):
m = mock.MagicMock()
m.session.session_key = None
self.assertEqual(
SQRLInitialization(m).get_or_create_session_key(),
m.session.session_key
)
m.session.create.assert_called_once_with()
@mock.patch.object(SQRLInitialization, 'generate_nut_kwargs')
@mock.patch.object(SQRLNutManager, 'replace_or_create')
def test_nut(self, mock_replace_or_create, mock_generate_nut_kwargs):
mock_generate_nut_kwargs.return_value = {
'foo': 'bar'
}
sqrl = SQRLInitialization(None)
self.assertEqual(sqrl.nut, mock_replace_or_create.return_value)
mock_replace_or_create.assert_called_once_with(foo='bar')
mock_generate_nut_kwargs.assert_called_once_with()
def test_nut_setter(self):
sqrl = SQRLInitialization(None)
# sanity check
self.assertFalse(hasattr(sqrl, '_nut'))
sqrl.nut = mock.sentinel.nut
self.assertTrue(hasattr(sqrl, '_nut'))
self.assertEqual(sqrl._nut, mock.sentinel.nut)
self.assertEqual(sqrl.nut, mock.sentinel.nut)
@mock.patch.object(SQRLInitialization, 'get_or_create_session_key')
@mock.patch(TESTING_MODULE + '.get_user_ip')
@mock.patch(TESTING_MODULE + '.generate_randomness')
def test_generate_nut_kwargs(self, mock_generate_randomness, mock_get_user_ip,
mock_get_or_create_session_key):
mock_generate_randomness.return_value = 'abc123'
actual = SQRLInitialization(mock.sentinel.request).generate_nut_kwargs()
self.assertDictEqual(
actual, {
'session_key': mock_get_or_create_session_key.return_value,
'nonce': 'abc',
'transaction_nonce': '123',
'is_transaction_complete': False,
'ip_address': mock_get_user_ip.return_value,
}
)
mock_generate_randomness.assert_called_once_with(64)
mock_get_user_ip.assert_called_once_with(mock.sentinel.request)
@mock.patch(TESTING_MODULE + '.reverse')
def test_get_sqrl_url(self, mock_reverse):
actual = SQRLInitialization(None).get_sqrl_url()
self.assertEqual(actual, mock_reverse.return_value)
def test_get_sqrl_url_params(self):
actual = SQRLInitialization(None, mock.MagicMock(nonce='foo&bar')).get_sqrl_url_params()
self.assertEqual(actual, 'nut=foo%26bar')
@mock.patch.object(SQRLInitialization, 'get_sqrl_url_params')
@mock.patch.object(SQRLInitialization, 'get_sqrl_url')
def test_url(self, mock_get_sqrl_url, mock_get_sqrl_url_params):
mock_get_sqrl_url.return_value = '/sqrl/auth/'
mock_get_sqrl_url_params.return_value = 'nut=nonce'
actual = SQRLInitialization(None).url
self.assertEqual(actual, '/sqrl/auth/?nut=nonce')
@mock.patch.object(SQRLInitialization, 'url', new_callable=mock.PropertyMock)
def test_sqrl_url(self, mock_url):
mock_url.return_value = '/sqrl/auth/?nut=nonce'
request = mock.MagicMock()
request.is_secure.return_value = True
request.get_host.return_value = 'example.com:8000'
actual = SQRLInitialization(request).sqrl_url
self.assertEqual(actual, 'sqrl://example.com:8000/sqrl/auth/?nut=nonce')

7
sqrl/tests/test_urls.py Normal file
View file

@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
from .. import urls # noqa
# nothing to test here however simply importing
# url module evals urls so coverage is higher

80
sqrl/tests/test_utils.py Normal file
View file

@ -0,0 +1,80 @@
# -*- coding: utf-8 -*-
import unittest
from collections import OrderedDict
import mock
from ..utils import Base64, Encoder, get_user_ip
TESTING_MODULE = 'sqrl.utils'
class TestBase64(unittest.TestCase):
def test_encode(self):
value = Base64.encode(b'hello')
# normal base64 is aGVsbG8= however = should be missing
self.assertEqual(value, 'aGVsbG8')
self.assertIsInstance(value, str)
def test_encode_not_binary(self):
with self.assertRaises(AssertionError):
Base64.encode('hello')
def test_decode(self):
value = Base64.decode('aGVsbG8')
# normal base64 is aGVsbG8= however = should be missing
self.assertEqual(value, b'hello')
self.assertIsInstance(value, bytes)
def test_decode_not_unicode(self):
with self.assertRaises(AssertionError):
Base64.decode(b'aGVsbG8')
class TestEncode(unittest.TestCase):
def test_base64_dumps(self):
self.assertEqual(Encoder.base64_dumps(5), '5')
self.assertEqual(Encoder.base64_dumps('hello'), 'hello')
self.assertEqual(Encoder.base64_dumps(b'hello'), 'aGVsbG8')
self.assertEqual(Encoder.base64_dumps([b'hello', 'hello']), 'aGVsbG8~hello')
self.assertEqual(
Encoder.base64_dumps(OrderedDict([('a', b'hello'), ('b', 'hello')])),
'YT1hR1ZzYkc4DQpiPWhlbGxvDQo'
)
def test_dumps(self):
self.assertEqual(Encoder.dumps(5), '5')
self.assertEqual(Encoder.dumps('hello'), 'hello')
self.assertEqual(Encoder.dumps(b'hello'), 'aGVsbG8')
self.assertEqual(Encoder.dumps([b'hello', 'hello']), 'aGVsbG8~hello')
self.assertEqual(Encoder.dumps(OrderedDict()), '')
self.assertEqual(
Encoder.dumps(OrderedDict([('a', b'hello'), ('b', 'hello')])),
'a=aGVsbG8\r\nb=hello\r\n'
)
def test_normalize(self):
self.assertEqual(Encoder.normalize(b'hello'), 'aGVsbG8')
self.assertEqual(Encoder.normalize('hello'), 'hello')
self.assertEqual(Encoder.normalize(5), '5')
self.assertEqual(Encoder.normalize([b'hello', 'hello']), ['aGVsbG8', 'hello'])
self.assertEqual(Encoder.normalize(OrderedDict()), '')
self.assertEqual(
Encoder.normalize(OrderedDict([('a', b'hello'), ('b', 'hello')])),
OrderedDict([('a', 'aGVsbG8'), ('b', 'hello')])
)
class TestUtils(unittest.TestCase):
def test_get_user_ip(self):
request = mock.MagicMock(META={'REMOTE_ADDR': mock.sentinel.ip})
self.assertEqual(get_user_ip(request), mock.sentinel.ip)
def test_get_user_ip_proxy(self):
request = mock.MagicMock(META={'HTTP_X_REAL_IP': mock.sentinel.ip})
self.assertEqual(get_user_ip(request), mock.sentinel.ip)

194
sqrl/tests/test_views.py Normal file
View file

@ -0,0 +1,194 @@
# -*- coding: utf-8 -*-
import json
import unittest
import mock
from django import test
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core import serializers
from django.urls import reverse
from django.http import Http404, QueryDict
from django.views.generic import FormView
from ..models import SQRLIdentity, SQRLNut
from ..views import (
SQRL_IDENTITY_SESSION_KEY,
SQRLCompleteRegistrationView,
SQRLStatusView,
)
TESTING_MODULE = 'sqrl.views'
class TestSQRLStatusView(test.TestCase):
def setUp(self):
super(TestSQRLStatusView, self).setUp()
self.view = SQRLStatusView()
self.view.request = mock.MagicMock(GET={})
self.view.request.is_ajax.return_value = True
self.view.kwargs = {
'transaction': '123',
}
self.nut = SQRLNut.objects.create(
nonce='hello',
transaction_nonce='123',
ip_address='127.0.0.1',
is_transaction_complete=True,
)
def tearDown(self):
self.nut and self.nut.delete()
super(TestSQRLStatusView, self).tearDown()
def test_get_success_url(self):
self.assertEqual(
self.view.get_success_url(),
settings.LOGIN_REDIRECT_URL
)
def test_get_success_url_from_querystring(self):
self.view.request.GET['url'] = '?next={}'.format(reverse('sqrl:login'))
self.assertEqual(
self.view.get_success_url(),
reverse('sqrl:login'),
)
def test_get_success_url_complete_registration(self):
self.view.request.GET['url'] = '?next={}'.format(reverse('sqrl:login'))
self.view.request.user.is_authenticated.return_value = False
self.view.request.session = {SQRL_IDENTITY_SESSION_KEY: ''}
self.assertEqual(
self.view.get_success_url(),
reverse('sqrl:complete-registration') + '?next={}'.format(reverse('sqrl:login')),
)
def test_get_object_404(self):
self.nut.delete()
self.nut = None
with self.assertRaises(Http404):
self.view.get_object()
def test_get_object(self):
actual = self.view.get_object()
self.assertIsInstance(actual, SQRLNut)
self.assertEqual(actual.nonce, self.nut.nonce)
self.assertEqual(actual.transaction_nonce, self.nut.transaction_nonce)
def test_post(self):
response = self.view.post(self.view.request, self.nut.transaction_nonce)
self.assertEqual(response.status_code, 200)
self.assertEqual(response['content-type'], 'application/json')
self.assertEqual(json.loads(response.content.decode('utf-8')), {
'transaction_complete': True,
'redirect_to': settings.LOGIN_REDIRECT_URL,
})
def test_post_not_ajax(self):
self.view.request.is_ajax.return_value = False
response = self.view.post(self.view.request, self.nut.transaction_nonce)
self.assertEqual(response.status_code, 405)
class TestSQRLAuthView(test.TestCase):
pass
class TestCompleteRegistrationView(test.TestCase):
def setUp(self):
super(TestCompleteRegistrationView, self).setUp()
self.view = SQRLCompleteRegistrationView()
self.username = 'foobartest'
self.view.request = mock.MagicMock(
method='POST',
POST={
'username': self.username,
},
)
self.identity = SQRLIdentity(
public_key='a' * 43,
verify_unlock_key='b' * 43,
server_unlock_key='c' * 43,
is_enabled=True,
is_only_sqrl=False,
)
self.view.request.session = {
SQRL_IDENTITY_SESSION_KEY: serializers.serialize('json', [self.identity]),
}
def tearDown(self):
SQRLIdentity.objects.filter(public_key=self.identity.public_key).delete()
get_user_model().objects.filter(username=self.username).delete()
super(TestCompleteRegistrationView, self).tearDown()
def test_check_session_for_sqrl_identity_or_404(self):
self.assertIsNone(self.view.check_session_for_sqrl_identity_or_404())
def test_check_session_for_sqrl_identity_or_404_raises(self):
self.view.request.session = {}
with self.assertRaises(Http404):
self.view.check_session_for_sqrl_identity_or_404()
@mock.patch.object(SQRLCompleteRegistrationView, 'check_session_for_sqrl_identity_or_404')
@mock.patch.object(FormView, 'get')
def test_get(self, mock_super_get, mock_check_session_for_sqrl_identity_or_404):
response = self.view.get(self.view.request)
self.assertEqual(response, mock_super_get.return_value)
mock_check_session_for_sqrl_identity_or_404.assert_called_once_with()
@mock.patch.object(SQRLCompleteRegistrationView, 'check_session_for_sqrl_identity_or_404')
@mock.patch.object(FormView, 'post')
def test_post(self, mock_super_post, mock_check_session_for_sqrl_identity_or_404):
response = self.view.post(self.view.request)
self.assertEqual(response, mock_super_post.return_value)
mock_check_session_for_sqrl_identity_or_404.assert_called_once_with()
def test_get_success_url(self):
self.assertEqual(
self.view.get_success_url(),
settings.LOGIN_REDIRECT_URL
)
def test_get_success_url_from_querystring(self):
self.view.request.GET = {'next': reverse('sqrl:manage')}
self.assertEqual(
self.view.get_success_url(),
reverse('sqrl:manage'),
)
@mock.patch(TESTING_MODULE + '.login')
def test_form_valid(self, mock_login):
form = self.view.get_form(self.view.get_form_class())
form.is_valid()
# sanity checks
self.assertFalse(get_user_model().objects.filter(username=self.username).count())
self.assertFalse(SQRLIdentity.objects.filter(public_key=self.identity.public_key).count())
response = self.view.form_valid(form)
user = get_user_model().objects.filter(username=self.username).first()
self.assertIsNotNone(user)
self.assertEqual(user.username, self.username)
self.assertIsInstance(user.sqrl_identity, SQRLIdentity)
self.assertEqual(user.sqrl_identity.public_key, self.identity.public_key)
self.assertEqual(response.status_code, 302)
def test_form_valid_could_not_decode_identity(self):
self.view.request.session[SQRL_IDENTITY_SESSION_KEY] = ''
response = self.view.form_valid(self.view.get_form(self.view.get_form_class()))
self.assertEqual(response.status_code, 500)

View file

@ -9,10 +9,12 @@ from .views import (
SQRLStatusView,
)
app_name = "sqrl"
urlpatterns = [
path("auth/", SQRLAuthView.as_view(), name="auth"),
path("login/", SQRLLoginView.as_view(), name="login"),
path("manage/", SQRLIdentityManagementView.as_view(), name='manage')
path("manage/", SQRLIdentityManagementView.as_view(), name='manage'),
path("register/",SQRLCompleteRegistrationView.as_view(), name='complete-registration'),
re_path(r"^status/(?P<transaction>[A-Za-z0-9_-]{43})/$", SQRLStatusView.as_view(), name='status'),
]

View file

@ -18,7 +18,7 @@ class Base64(object):
s: bytes
Bytes string to be encoded as base64
"""
assert isinstance(s, (bites, bytearray))
assert isinstance(s, (bytes, bytearray))
return urlsafe_b64encode(s).decode('ascii').rstrip('=')
@classmethod

View file

@ -13,7 +13,7 @@ from django.contrib.auth import (
login,
)
from django.core import serializers
from django.url import reverse
from django.urls import reverse
from django.http import Http404, HttpResponse, JsonResponse, QueryDict
from django.shortcuts import get_object_or_404, redirect
from django.views.generic import FormView, TemplateView, View
@ -23,7 +23,6 @@ from .exceptions import TIF, TIFException
from .forms import (
AuthQueryDictForm,
ExtractedNextUrlForm,
GenerateQRForm,
NextUrlForm,
PasswordLessUserCreationForm,
RequestForm,
@ -31,7 +30,7 @@ from .forms import (
from .models import SQRLIdentity, SQRLNut
from .response import SQRLHttpResponse
from .sqrl import SQRLInitialization
from .utils import Base64, QRGenerator, get_user_ip
from .utils import Base64, get_user_ip
SQRL_IDENTITY_SESSION_KEY = '_sqrl_identity'