Skip to content

Commit

Permalink
added support for 0x100 bad id association TIF code
Browse files Browse the repository at this point in the history
using set_unusable_password() in RandomPasswordUserCreationForm
renamed RandomPasswordUserCreationForm to PasswordLessUserCreationForm
added SQRL management option to Django admin by introducing AdminSiteSQRLIdentityManagementView
adjusted existing tests as necessary to accommodate changes
added form submit buttons next to  SQRL QR codes to make it more obvious that SQRL can be triggered by clicking vs only scanning QR code
added instructions in README on how to add SQRL urls
  • Loading branch information
miki725 committed May 25, 2015
1 parent c1e6484 commit 5d4abb9
Show file tree
Hide file tree
Showing 16 changed files with 322 additions and 75 deletions.
80 changes: 64 additions & 16 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,34 +31,68 @@ Once installed there are a few required changes in Django settings:

#. Add ``sqrl`` to ``INSTALLED_APPS``::

INSTALLED_APPS = [
...
'sqrl',
]
INSTALLED_APPS = [
...
'sqrl',
]

#. Make sure that some required Django apps are used::

INSTALLED_APPS = [
...,
'django.contrib.auth',
'django.contrib.sessions',
'django.contrib.staticfiles',
'sqrl',
]
INSTALLED_APPS = [
...,
'sqrl',
'django.contrib.auth',
'django.contrib.sessions',
'django.contrib.staticfiles',
]

#. Make sure that some required Django middleware are used::

MIDDLEWARE_CLASSES = [
MIDDLEWARE_CLASSES = [
...
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
]
]

#. Change ``AUTHENTICATION_BACKENDS`` to use SQRL backend vs Django's ``ModelBackend`` (default)::

AUTHENTICATION_BACKENDS = [
'sqrl.backends.SQRLModelBackend',
]
AUTHENTICATION_BACKENDS = [
'sqrl.backends.SQRLModelBackend',
]

#. If you are using Django admin, following are required:

#. Make sure that ``sqrl`` is listed before ``admin`` in the ``INSTALLED_APPS``. This allows Django to prioritize ``sqrl`` templates since ``django-sqrl`` overwrites some of them.

::

INSTALLED_APPS = [
...,
'sqrl',
'django.contrib.admin',
...
]

#. Make sure to add a custom template directory in settings. ``django-sqrl`` extends Django admin's ``base.html`` which by default causes infinite recursion. To solve that, simply add a custom template directory which allows ``django-sqrl`` to explicitly extend from ``django.contrib.admin`` ``base.html`` template::

import os
import django
TEMPLATE_DIRS = [
os.path.dirname(django.__file__),
]

URLs
~~~~

All of SQRL functionality is enabled by adding its urls to the root url config::

url(r'^sqrl/', include(sqrl_urlpatterns, namespace='sqrl')),

If you use Django admin, then you should also want to add some SQRL urls to admin urls so that SQRL identity can be managed within Django admin::

from sqrl.views import AdminSiteSQRLIdentityManagementView
url(r'^admin/sqrl_manage/$', AdminSiteSQRLIdentityManagementView.as_view(), name='admin-sqrl_manage'),
url(r'^admin/', include(admin.site.urls)),

Templates
~~~~~~~~~
Expand All @@ -74,6 +108,20 @@ Now that SQRL is installed in a Django project you can use it in any login page

The above template will add a QR image as a link which when used with SQRL client, will allow users to authenticate using SQRL.

If you would like to also add explicit button to trigger SQRL client on desktop appliations, you can also use HTML form::

{% sqrl as sqrl %}
<form method="get" action="{{ sqrl.sqrl_url }}">
{% sqrl_status_url_script_tag sqrl %}
<a href="{{ sqrl.sqrl_url }}">
<img src="{% sqrl_qr_image_url sqrl %}">
</a>
<input type="hidden" name="nut" value="{{ sqrl.nut.nonce }}">
<input type="submit" value="Log in using SQRL">
</form>
{% sqrl_status_url_script_tag sqrl %}
<script src="{% static 'sqrl/sqrl.js' %}"></script>

Management Command
~~~~~~~~~~~~~~~~~~

Expand Down
3 changes: 3 additions & 0 deletions sqrl/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,6 @@ def authenticate(self, *args, **kwargs):
return

return user


SQRL_MODEL_BACKEND = '{}.{}'.format(SQRLModelBackend.__module__, SQRLModelBackend.__name__)
3 changes: 3 additions & 0 deletions sqrl/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ class TIF(int):
"""SQRL command failed for any reason"""
CLIENT_FAILURE = 0x80
"""SQRL command failed because SQRL client sent invalid data"""
BAD_ID_ASSOCIATION = 0x100
"""SQRL Identity is already a ssociated with a different account"""

is_id_match = property(_make_tif_property(ID_MATCH))
is_previous_id_match = property(_make_tif_property(PREVIOUS_ID_MATCH))
Expand All @@ -92,6 +94,7 @@ class TIF(int):
is_command_failed = property(_make_tif_property(COMMAND_FAILED))
is_client_failure = property(_make_tif_property(CLIENT_FAILURE))
is_not_supported = property(_make_tif_property(NOT_SUPPORTED))
is_bad_id_association = property(_make_tif_property(BAD_ID_ASSOCIATION))

def as_hex_string(self):
"""
Expand Down
50 changes: 33 additions & 17 deletions sqrl/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
from django.contrib.auth import SESSION_KEY, get_user_model
from django.contrib.auth.forms import UserCreationForm
from django.contrib.sessions.middleware import SessionMiddleware
from django.utils.crypto import get_random_string

from .crypto import HMAC, Ed25519
from .exceptions import TIF
from .fields import (
Base64ConditionalPairsField,
Base64Field,
Expand Down Expand Up @@ -114,6 +114,7 @@ def __init__(self, nut, *args, **kwargs):
self.session = None
self.identity = None
self.previous_identity = None
self.tif = TIF(0)
super(RequestForm, self).__init__(*args, **kwargs)

def clean_client(self):
Expand Down Expand Up @@ -328,10 +329,25 @@ def _clean_client_cmd_remove(self, client):
)

def _clean_session(self):
if not self.session:
return

user_id = self.session.get(SESSION_KEY)
if not user_id:
return

try:
user_id = int(user_id)
except ValueError:
return

if self.identity and self.identity.user_id != user_id:
self.tif = self.tif.update(TIF.BAD_ID_ASSOCIATION)
raise forms.ValidationError(
'Cannot use SQRL within existing authenticated session'
'when SQRL identity is already associated with a different account'
)

user = get_user_model().objects.filter(pk=user_id).first()

try:
Expand Down Expand Up @@ -360,6 +376,7 @@ def _clean_session(self):
pidk = Base64.encode(self.cleaned_data['client'].get('pidk', b''))

if user.sqrl_identity.public_key not in [idk, pidk]:
self.tif = self.tif.update(TIF.BAD_ID_ASSOCIATION)
raise forms.ValidationError(
'Both current and previous identities do not match user\'s already '
'associated SQRL identity. If the identity needs to be changed, '
Expand Down Expand Up @@ -425,36 +442,35 @@ def _get_identity(self, key):
).first()


class RandomPasswordUserCreationForm(UserCreationForm):
class PasswordLessUserCreationForm(UserCreationForm):
"""
Form for creating user account without password.
This form is used when user successfully completes SQRL transaction
however does not yet have a user account. Since they already successfully
used SQRL, this implies that they they prefer to use SQRL over
username/password. Django however requires password in order to create
a user so we simply generate a random one. If the user will later wish
to authenticate via password, they will need to follow password-reset
procedure.
username/password. Therefore we simply create a user with unusable
password using Django's ``set_unusable_password`` capability.
"""

def __init__(self, *args, **kwargs):
super(RandomPasswordUserCreationForm, self).__init__(*args, **kwargs)
super(PasswordLessUserCreationForm, self).__init__(*args, **kwargs)
# loop over all the fields and remove all password fields
# by default this removes both password and verify_password fields
for field in self.fields:
if 'password' in field:
self.fields.pop(field)

def clean(self):
"""
This method assigns a random password when complete form is validated.
def save(self, commit=True):
"""
cleaned_data = super(RandomPasswordUserCreationForm, self).clean()
Custom user save implementation which saves user with unusable password.
# Create artificial password.
# If user will want to use non-SQRL password,
# they will need to reset the password.
cleaned_data['password1'] = get_random_string(length=25)

return cleaned_data
The implementation is very similar to how ``UserCreationForm`` saves
a user model, except this method uses :meth:`AbstractBaseUser.set_unusable_password`
to set a users password field.
"""
user = super(UserCreationForm, self).save(commit=False)
user.set_unusable_password()
if commit:
user.save()
return user
13 changes: 13 additions & 0 deletions sqrl/static/admin/sqrl.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
height: 0;
}

.login #container {
margin-top: 30px;
}

.login .submit-row {
text-align: center;
clear: both;
Expand All @@ -21,6 +25,10 @@
display: inline-block;
}

.sqrl {
margin-bottom: 15px;
}

.sqrl .or {
position: relative;
z-index: 2;
Expand All @@ -46,8 +54,13 @@

.sqrl .img {
text-align: center;
margin-bottom: 0;
}

.sqrl .img img {
width: 60%;
}

.sqrl .submit-row {
padding-top: 0;
}
11 changes: 7 additions & 4 deletions sqrl/static/sqrl/sqrl.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
'use strict';

(function() {
var next_input = document.querySelectorAll('input[name="next"]'),
next_url = next_input.length > 0 ? next_input[0].value : null,
var get_next_url = function() {
var input = document.querySelectorAll('input[name="next"]');
return input.length > 0 ? input[0].value : null;
},
current_url = window.location.href,
sqrl_frequency = 1500,
sqrl_call = function() {
setTimeout(sqrl_handler, sqrl_frequency);
},
sqrl_handler = function() {
var request = new XMLHttpRequest(),
url = SQRL_CHECK_URL + '?url=';
var request = new XMLHttpRequest(),
url = SQRL_CHECK_URL + '?url=',
next_url = get_next_url();

if (next_url !== null) {
url = url + encodeURIComponent('?next=' + next_url);
Expand Down
82 changes: 82 additions & 0 deletions sqrl/templates/admin/auth/user/sqrl_manage.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
{% extends "admin/base_site.html" %}
{% load i18n admin_static %}
{% load sqrl %}

{% block title %}Manage SQRL{% endblock %}
{% block content_title %}<h1>Manage SQRL</h1>{% endblock %}

{% block extrahead %}{{ block.super }}
<script src="{% static 'sqrl/sqrl.js' %}"></script>
{% endblock %}

{% block extrastyle %}{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static "admin/css/forms.css" %}"/>
{% endblock %}


{% block userlinks %}
{% url 'django-admindocs-docroot' as docsroot %}
{% if docsroot %}
<a href="{{ docsroot }}">{% trans 'Documentation' %}</a> /
{% endif %}
{% trans 'Manage SQRL' %} /
<a href="{% url 'admin:logout' %}">{% trans 'Log out' %}</a>
{% endblock %}

{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
&rsaquo; {% trans 'Manage SQRL' %}
</div>
{% endblock %}

{% block content %}
<div id="content-main">

{% if not user.sqrl_identity %}
<p>
You dont have SQRL identity associated with your account yet.
Please use SQRL link/QR code below to associate SQRL identity with your account.
</p>
{% else %}
<p>
Congratulations! You already have SQRL identity associated with your account.
If you would like to either change or delete existing SQRL identity
associated with your account, you can do that by selecting appropriate
option in your SQRL client and then using the SQRL link/QR code below.
</p>

<p>
<strong>Note:</strong> For both changing or deleting your SQRL identity,
you will need to load your SQRL rescue code.
</p>

<p>
<strong>Caution:</strong> Normally it is not advised to change or delete
your SQRL identity. Usually these operations are only required when
SQRL identity is compromised.
</p>
{% endif %}

{% sqrl as sqrl %}

<fieldset class="module aligned">
<p>
{% sqrl_status_url_script_tag sqrl %}
<a href="{{ sqrl.sqrl_url }}">
<img src="{% sqrl_qr_image_url sqrl %}" width="200">
</a>
{# redirect to manage page after successful SQRL transaction #}
<input type="hidden" name="next" value="{% url 'admin-sqrl_manage' %}">
</p>
</fieldset>

<form method="get" action="{{ sqrl.sqrl_url }}" class="sqrl">
<div class="submit-row">
<input type="hidden" name="nut" value="{{ sqrl.nut.nonce }}">
<input type="submit" value="Manage SQRL" class="default" style="float: left;">
</div>
</form>

</div>
{% endblock %}
19 changes: 19 additions & 0 deletions sqrl/templates/admin/base.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{% extends 'contrib/admin/templates/admin/base.html' %}
{% load i18n admin_static %}

{% block userlinks %}
{% if site_url %}
<a href="{{ site_url }}">{% trans 'View site' %}</a> /
{% endif %}
{% if user.is_active and user.is_staff %}
{% url 'django-admindocs-docroot' as docsroot %}
{% if docsroot %}
<a href="{{ docsroot }}">{% trans 'Documentation' %}</a> /
{% endif %}
{% endif %}
{% if user.has_usable_password %}
<a href="{% url 'admin:password_change' %}">{% trans 'Change password' %}</a> /
{% endif %}
<a href="{% url 'admin-sqrl_manage' %}">{% trans 'Manage SQRL' %}</a> /
<a href="{% url 'admin:logout' %}">{% trans 'Log out' %}</a>
{% endblock %}

0 comments on commit 5d4abb9

Please sign in to comment.