Still building initial conversions

This commit is contained in:
= 2019-09-02 02:03:23 -05:00
parent cea56d2868
commit 3cfd9840e8
Signed by: kiichan
GPG Key ID: 619DFD67F0976616
23 changed files with 2925 additions and 21 deletions

View File

@ -1,3 +1,19 @@
Mon, 02 Sep 2019 02:03:23 -0500
Keaton <kii-chan@tutanota.com>
Still building initial conversions
Everything is still completely untested. These conversions were done a few weeks
ago, so my memory on any design changes is a little rusty. Aside from what's
listed below, all changes are just compatibility fixes.
- Removed references to six since we're dropping Python 2 support
- Removed references to QR code urls since those will be browser generated
- Added template for creating a drop-in SQRL login element
- Also changed some variable names to be less confusing
- Changed login pages to use drop-in element
--------------------
Wed, 14 Aug 2019 12:14:52 -0500
Keaton <kii-chan@tutanota.com>
fix typo

136
sqrl/exceptions.py Normal file
View File

@ -0,0 +1,136 @@
# -*- coding: utf-8 -*-
def _make_tif_property(val):
"""
Helper function for generating property methods for :obj:`.TIF`
which will boolean whether a particular SQRL ``TIF`` bit is ``True``
in the ``TIF`` value.
Parameters
----------
val : int
Value with particular ``True`` bit which will be tested
within the generated property.
Returns
-------
function
Function which can be made into a property
"""
def is_bit_present(self):
"""
Property which returns boolean whether ``{hex}`` or ``{bits}``
bit is present in the TIF value.
"""
return bool(self & val)
is_bit_present.__doc__ = is_bit_present.__doc__.format(
hex=hex(val),
bits=bin(val),
)
return is_bit_present
class TIF(int):
"""
SQRL ``TIF`` ``int`` subclass which can represent SQRL ``TIF`` flags.
Example
-------
::
>>> tif = TIF(TIF.IP_MATCH | TIF.TRANSIENT_FAILURE | TIF.COMMAND_FAILED)
>>> tif.is_ip_match
True
>>> tif.is_id_match
False
>>> tif.is_transient_failure
True
>>> tif
100
>>> tif.as_hex_string()
'64'
>>> tif.breakdown() == {
... 'id_match': False,
... 'previous_id_match': False,
... 'ip_match': True,
... 'sqrl_disabled': False,
... 'not_supported': False,
... 'transient_failure': True,
... 'command_failed': True,
... 'client_failure': False,
... }
True
"""
ID_MATCH = 0x1
"""SQRL ID was found in DB"""
PREVIOUS_ID_MATCH = 0x2
"""Previous SQRL ID was found in DB"""
IP_MATCH = 0x4
"""SQRL client is used from same IP as where transaction started"""
SQRL_DISABLED = 0x8
"""SQRL auth is disabled for the found SQRL identity as per users request"""
NOT_SUPPORTED = 0x10
"""SQRL client requested SQRl operation which is not supported"""
TRANSIENT_FAILURE = 0x20
"""SQRL command failed transiently. Most likely restarting SQRL transaction should fix this"""
COMMAND_FAILED = 0x40
"""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))
is_ip_match = property(_make_tif_property(IP_MATCH))
is_sqrl_disabled = property(_make_tif_property(SQRL_DISABLED))
is_transient_failure = property(_make_tif_property(TRANSIENT_FAILURE))
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):
"""
Return TIF value as hex string
"""
return '{:x}'.format(self)
def breakdown(self):
"""
Returns a full breakdown of the TIF value.
Returns
-------
dict
Keys are the SQRL TIF property and values are booleans.
"""
return {
k.lower(): bool(self & v)
for k, v in vars(self.__class__).items()
if not k.startswith('_') and k.isupper()
}
def update(self, other):
"""
Return updated TIF which will contain both bits already set
in the ``self`` value as well as the ``other value.
Parameters
----------
other : int
Other ``TIF`` value which be merged with ``self`` bits
Returns
-------
TIF
New :obj:`.TIF` value which has merged bits.
"""
return type(self)(self | other)
class TIFException(Exception):
"""
Custom Exception which can be used in the views to raise
specific :obj:`.TIF` bits and immediately return appropriate response
to the user.
"""
def __init__(self, tif):
self.tif = TIF(tif)

212
sqrl/fields.py Normal file
View File

@ -0,0 +1,212 @@
# -*- coding: utf-8 -*-
from collections import OrderedDict
from django import forms
from django.urls import Resolver404, resolve
from django.core.validators import URLValidator
from django.http import QueryDict
from urllib.parse import urlparse, unquote
from .utils import Base64
class NextUrlField(forms.CharField):
"""
Custom ``CharField`` which validates that a value is a valid next URL.
It validates that by checking that the value can be resolved to a view
hence guaranteeing that when redirected URL will not fail.
"""
default_error_messages = {
'invalid_url': 'Invalid next url.'
}
def to_python(self, value):
"""
Validate that value is a valid URL for this project.
"""
value = super(NextUrlField, self).to_python(value)
if value in self.empty_values:
return value
path = urlparse(value).path
try:
resolve(path)
except Resolver404:
raise forms.ValidationError(self.error_messages['invalid_url'])
else:
return path
class ExtractedNextUrlField(NextUrlField):
"""
Similar to :obj:`.NextUrlField` however this extracts next url from full encoded URL.
"""
default_error_messages = {
'missing_next': 'Missing next query parameter.'
}
def to_python(self, value):
"""
Extract next url from full URL string and then use :obj:`.NextUrlField`
to validate that value is valid URL.
"""
value = forms.CharField.to_python(self, value)
if value in self.empty_values:
return value
decoded = urlparse(unquote(value))
data = QueryDict(decoded.query)
if 'next' not in data:
raise forms.ValidationError(self.error_messages['missing_next'])
return super(ExtractedNextUrlField, self).to_python(data['next'])
class SQRLURLValidator(URLValidator):
"""
Custom URL validator which validates that a URL is a valid SQRL url.
These are the differences with regular HTTP URLs:
* scheme is either sqrl (secure) and qrl (non-secure)
* ``:`` is a valid path separator which can be used to indicate
which section of the SQRL should be used to generate
public/provate keypair for the domain.
Okay, but is there support for qrl schema???
"""
schemes = ['sqrl', 'qrl']
# schemes = ['sqrl']
class SQRLURLField(forms.URLField):
"""
SQRL URL field which uses :obj:`.SQRLURLValidator` for validation.
"""
default_validators = [SQRLURLValidator()]
class Base64Field(forms.CharField):
"""
Field which decodes base64 values using :meth:`.utils.Base64.decode`.
"""
default_error_messages = {
'base64': 'Invalid value. Must be base64url encoded string.',
}
def to_python(self, value):
"""
Decodes base64 value and returns binary data.
"""
value = super(Base64Field, self).to_python(value)
if not value:
return b''
try:
return Base64.decode(value)
except (ValueError, TypeError):
raise forms.ValidationError(self.error_messages['base64'])
class Base64CharField(Base64Field):
"""
Similar to :obj:`.Base64Field` however this field normalizes to ``str`` (``unicode``) data.
"""
default_error_messages = {
'base64_ascii': 'Invalid value. Must be ascii base64url encoded string.',
}
def to_python(self, value):
"""
Returns base64 decoded data as string.
Uses :meth:`.Base64Field.to_python` to decode base64 value
which returns binary data and then this method further
decodes ascii data to return ``str`` (``unicode``) data.
"""
value = super(Base64CharField, self).to_python(value)
if not value:
return ''
try:
return value.decode('ascii')
except UnicodeDecodeError:
raise forms.ValidationError(self.error_messages['base64_ascii'])
class Base64PairsField(Base64CharField):
"""
Field which normalizes base64 encoded multistring key-value pairs to ``OrderedDict``.
Attributes
----------
always_pairs : bool
Boolean which enforces that the value must always be keypairs.
When ``False`` and the value is not a keypair, the value itself
is returned.
"""
default_error_messages = {
'crlf': 'Invalid value. Must be multi-line string separated by CRLF.',
'pairs': 'Invalid value. Must be multi-line string of pair of values.',
}
always_pairs = True
def to_python(self, value):
"""
Normalizes multiline base64 keypairs string to ``OrderedDict``.
"""
value = super(Base64PairsField, self).to_python(value)
if not value:
return OrderedDict()
if not value.endswith('\r\n'):
if self.always_pairs:
raise forms.ValidationError(self.error_messages['crlf'])
else:
return value
try:
return OrderedDict(
line.split('=', 1) for line in filter(None, value.splitlines())
)
except ValueError:
raise forms.ValidationError(self.error_messages['pairs'])
class Base64ConditionalPairsField(Base64PairsField):
"""
Similar to :obj:`.Base64PairsField` but this field does not force
the value to be keypairs.
"""
always_pairs = False
class TildeMultipleValuesField(forms.CharField):
"""
Field which returns tilde-separated list.
"""
def to_python(self, value):
"""
Normalizes to a Python list by splitting string by tilde (~) delimiter.
"""
value = super(TildeMultipleValuesField, self).to_python(value)
if not value:
return []
return value.split('~')
class TildeMultipleValuesFieldChoiceField(TildeMultipleValuesField, forms.ChoiceField):
"""
Similar to :obj:`.TildeMultipleValuesField` however this field also validates
each value to be a valid choice.
"""
def validate(self, value):
for i in value:
super(TildeMultipleValuesFieldChoiceField, self).validate(i)

442
sqrl/forms.py Normal file
View File

@ -0,0 +1,442 @@
# -*- coding: utf-8 -*-
from django import forms
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 .crypto import HMAC, Ed25519
from .exceptions import TIF
from .fields import (
Base64ConditionalPairsField,
Base64Field,
Base64PairsField,
ExtractedNextUrlField,
NextUrlField,
SQRLURLField,
TildeMultipleValuesField,
)
from .models import SQRLIdentity
from .utils import Base64
class NextUrlForm(forms.Form):
"""
Form which validates that next URL is a valid URL.
Currently is used by :obj:`.views.SQRLCompleteRegistrationView` to validate
next URL to which user will be redirected when registration is completed.
"""
next = NextUrlField()
class ExtractedNextUrlForm(forms.Form):
"""
Form which extracts and validates next URL from a full URL.
Currently is used by :obj:`.views.SQRLStatusView` to return an appropriate
URL to the JS to which JS will redirect to when SQRL transaction is complete.
The reason we want to extract the next url here vs js is for simplicity
of the js code. Now js can simply find ``<input name=next/>`` or if not present
pass current full URL which might contain ``?next=<url>`` to the server.
Python then can extract next url when present and if valid, return appropriate
URL for the user to redirect to.
"""
url = ExtractedNextUrlField()
class AuthQueryDictForm(forms.Form):
"""
Form to validate the ``request.GET`` in :obj:`.views.SQRLAuthView`.
This allows to validate that the nut is of expected length and matches
particular regex pattern before we attempt to look it up in the db.
Normally this would be validated by a url pattern however in SQRL,
nut is sent as a querystring parameter which is outside of the scope
of URL pattern matching in Django.
"""
nut = forms.SlugField(max_length=43)
class ClientForm(forms.Form):
"""
Form used to validate client portion of the SQRL request payload.
Since this form is used as nested form by :obj:`.RequestForm`
and therefore it does not have access to the signatures, this form
only validates the values themselves. :obj:`.RequestForm` takes care
of all conditional validations such as validating signatures
when some keys are present in this form.
"""
ver = forms.IntegerField(label='Version', min_value=1, max_value=1)
cmd = TildeMultipleValuesField(label='Command')
opt = TildeMultipleValuesField(label='Options', required=False)
idk = Base64Field(label='Identity Key')
pidk = Base64Field(label='Previous Identity Key', required=False)
suk = Base64Field(label='Server Unlock Key', required=False)
vuk = Base64Field(label='Verify Unlock Key', required=False)
class RequestForm(forms.Form):
"""
This is a main form for validating SQRL request payload.
This form uses uses :obj:`.ClientForm` for validating client portion of the request.
Therefore some of the validation portions by the time they are executed,
they expect client form to be validated so that they can lookup client values.
Currently this does not require any *magic* solutions since ``client``
is first defined field which means it will be validated first.
Parameters
----------
nut : SQRLNut
Nut for which this form will be validated
"""
client = Base64PairsField()
server = Base64ConditionalPairsField()
ids = Base64Field()
pids = Base64Field(required=False)
urs = Base64Field(required=False)
def __init__(self, nut, *args, **kwargs):
self.nut = nut
self.session = None
self.identity = None
self.previous_identity = None
self.tif = TIF(0)
super(RequestForm, self).__init__(*args, **kwargs)
def clean_client(self):
"""
Since Django forms dont support nested forms, this method is used
to do that in ad-hoc fashion.
By the time this method is called, we know that ``client`` data will be
``OrderedDict``, as guaranteed by :obj:`.fields.Base64PairsField`.
We use that information and simply pass the client data to the
:obj:`.ClientForm`. If validated, :obj:`.ClientForm` cleaned data will be
a dictionary which we then simply return in this method. If not validated,
we simply raise validation exception here which stop the rest of the
validation steps.
"""
client_form = ClientForm(data=self.cleaned_data['client'])
if not client_form.is_valid():
raise forms.ValidationError(client_form.errors)
return client_form.cleaned_data
def clean_server(self):
"""
This method conditionally validates ``server`` field.
The reason why we validate it conditionally is because on initial SQRL
request, client only sends initial SQRL URL since does not have access
to the server response yet which would be a dict. In that case we do not
validate server field at all.
.. note::
Even though on initial SQRL request, we dont validate server field,
it is still indirectly validated against tampering via signatures.
When we do validate it, we check the following:
* nut value matches the looked up nut nonce
(this should ever happen but just in case)
* validate that ``mac`` is present in the server dict
* validate the ``mac`` value that is correctly signs the server response
excluding ``mac`` value itself.
"""
server = self.cleaned_data['server']
if not isinstance(server, dict):
return server
if server.get('nut') != self.nut.nonce:
raise forms.ValidationError('Nut mismatch between server value and looked up nut.')
if 'mac' not in server:
raise forms.ValidationError('Missing server signature.')
try:
is_valid_signature = HMAC(self.nut, server).is_signature_valid(
Base64.decode(server['mac'])
)
except (ValueError, TypeError):
is_valid_signature = False
if not is_valid_signature:
raise forms.ValidationError('Invalid server signature.')
return server
def _validate_signature(self, name, key, signature):
client = self.data['client']
server = self.data['server']
msg = (client + server).encode('ascii')
if not Ed25519(key, None, msg).is_signature_valid(signature):
raise forms.ValidationError('Invalid {} signature.'.format(name))
def clean_ids(self):
"""
Validate SQRL ID signature which always must be present.
This validates that the ID signature correctly signed raw client + raw server
concatenated base64 encoded strings.
"""
idk = self.cleaned_data['client']['idk']
ids = self.cleaned_data['ids']
self._validate_signature('ids', idk, ids)
return ids
def clean_pids(self):
"""
Validate Previous SQRL ID signature when present.
This validates:
* if either previous ID or previous signature are present, both are required
* previous ID signature correctly signed raw client + raw server
concatenated base64 encoded strings.
"""
pidk = self.cleaned_data['client'].get('pidk')
pids = self.cleaned_data.get('pids')
if not all((pids, pidk)):
raise forms.ValidationError(
'Cannot validate previous ID signature without server knowing pid public and secret keys.'
)
if pidk and pids:
self._validate_signature('pids', pidk, pids)
return pids
def _clean_urs(self):
vuk = getattr(self.identity, 'verify_unlock_key', None)
suk = getattr(self.identity, 'server_unlock_key', None)
urs = self.cleaned_data.get('urs')
if urs:
if not all((vuk, suk)):
raise forms.ValidationError(
'Cannot validate urs signature without server knowing vuk and suk keys.'
)
self._validate_signature('urs', Base64.decode(vuk), urs)
return urs
def _clean_client_cmd(self):
client = self.cleaned_data['client']
cmds = client['cmd']
for cmd in cmds:
method_name = '_clean_client_cmd_{}'.format(cmd)
if hasattr(self, method_name):
getattr(self, method_name)(client)
def _clean_client_cmd_ident(self, client):
suk = client.get('suk')
vuk = client.get('vuk')
if not self.identity and not all([suk, vuk]):
raise forms.ValidationError(
'Missing suk or vuk which are required when creating new identity.'
)
if self.identity and any([suk, vuk]):
raise forms.ValidationError(
'Cannot send suk or vuk when SQRL identity is already associated.'
)
if 'disable' in client['cmd']:
raise forms.ValidationError(
'Cannot use disable command at the same time as ident command.'
)
# since we only store a single identity at the time
# its impossible for when identity is being changed
# self.identity will exist since by definition server
# should only be aware of the previous identity
# since the client is sending a new identity for storage.
if all((not self.identity,
self.previous_identity,
not self.cleaned_data.get('urs'))):
raise forms.ValidationError(
'Must supply urs (unlock request signature) when switching identities '
'from previously stored identity (pidk) to new current identity (idk).'
)
def _clean_client_cmd_disable(self, client):
if not self.identity:
raise forms.ValidationError(
'Must have identity associated in order to disable SQRL.'
)
if 'enable' in client['cmd']:
raise forms.ValidationError(
'Cannot use enable command at the same time as disable command.'
)
def _clean_client_cmd_enable(self, client):
if not self.identity:
raise forms.ValidationError(
'Must have identity associated in order to enable SQRL.'
)
if not self.cleaned_data.get('urs'):
raise forms.ValidationError(
'Must supply urs (unlock request signature) to enable SQRL access.'
)
if 'disable' in client['cmd']:
raise forms.ValidationError(
'Cannot use disable command at the same time as enable command.'
)
def _clean_client_cmd_remove(self, client):
if not self.identity:
raise forms.ValidationError(
'Must have identity associated in order to remove SQRL.'
)
if not self.cleaned_data.get('urs'):
raise forms.ValidationError(
'Must supply urs (unlock request signature) to enable SQRL access.'
)
if client['cmd'] != ['remove']:
raise forms.ValidationError(
'Cannot use any other commands at the same time as remove command.'
)
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:
if not user or not user.sqrl_identity:
return
except SQRLIdentity.DoesNotExist:
return
# We want to make sure that if the user is logged in
# and already has an identity associated with the account,
# that either current or previous identity supplied in the
# client request matches the identity already associated
# with the account.
# That will force the user to go through the SQRL identity
# [un]lock processes to change the sqrl identity
# (e.g. force the user to load the identity unlock key
# and use that to change identities).
# If this condition is not checked, it will be possible
# for any SQRL identity to overwrite existing identity,
# without any additional checks, which is not desirable.
# For example if the existing auth scheme (e.g. username+password)
# gets compromised, it will be possible for malicious party to
# associate their SQRL identity with the account hence
# locking out legitimate account owner.
idk = Base64.encode(self.cleaned_data['client']['idk'])
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, '
'SQRL identity unlock processes must be followed.'
)
def clean(self):
"""
Assuming all fields successfully validated, this method validates form as a whole.
Also this field does any db lookups such as retrieving SQRL identities.
That is necessary for validation however since lookup already happens here,
the :obj:`.views.SQRLAuthView` reuses the looked up values so that it
does not have to lookup same objects twice.
This method does the following (sometimes using private methods):
#. lookups identities - both current and previous
#. validates client commands (``client.cmd``) such as ``ident``.
They can only be validated when identities are looked up.
For example ``disable`` cannot be requested when no SQRL identity is found.
#. validates ``urs`` (unlock request signature).
That can only be done here as this requires to use data stored
in the stored identity.
#. lookups client session (where user will be logged in)
#. validates session. For example if the user is already logged with existing
matching SQRL identity, this validates that only stored SQRL identity
public key can be used in SQRL transaction.
"""
cleaned_data = super(RequestForm, self).clean()
self.find_identities()
self._clean_client_cmd()
self._clean_urs()
self.find_session()
self._clean_session()
return cleaned_data
def find_session(self):
"""
This method finds the session where SQRL transaction was initiated.
This is the session where user potentially will be signed in.
"""
self.session = SessionMiddleware().SessionStore(self.nut.session_key)
def find_identities(self):
"""
This method finds both current and previous SQRL identities.
At most only one of them should be found.
"""
self.identity = self._get_identity(self.cleaned_data['client']['idk'])
self.previous_identity = self._get_identity(self.cleaned_data['client'].get('pidk'))
def _get_identity(self, key):
if not key:
return None
return SQRLIdentity.objects.filter(
public_key=Base64.encode(key)
).first()
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. Therefore we simply create a user with unusable
password using Django's ``set_unusable_password`` capability.
"""
def __init__(self, *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 list(self.fields):
if 'password' in field:
self.fields.pop(field)
def save(self, commit=True):
"""
Custom user save implementation which saves user with unusable password.
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

View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from __future__ import print_function, unicode_literals

View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from __future__ import print_function, unicode_literals

View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
from datetime import timedelta
from django.conf import settings
from django.core.management.base import BaseCommand
from django.utils.timezone import now
from sqrl.models import SQRLNut
class Command(BaseCommand):
help = ('Clears expired SQRL nuts. '
'This command should be used as a cron job. '
'The recommended execution frequency is 5 minutes '
'which will result in longest nut lifespan of 10 minutes.')
def handle(self, *args, **options):
ttl = getattr(settings, 'SQRL', {}).get('TTL', 60 * 5) # 5 minutes
delete_before = now() + timedelta(seconds=-ttl)
SQRLNut.objects.filter(timestamp__lt=delete_before).delete()

66
sqrl/response.py Normal file
View File

@ -0,0 +1,66 @@
# -*- coding: utf-8 -*-
import logging
from pprint import pformat
from django.conf import settings
from django.http import HttpResponse
from .crypto import HMAC
from .exceptions import TIF
from .utils import Encoder
log = logging.getLogger(__name__)
class SQRLHttpResponse(HttpResponse):
"""
Custom ``HTTPResponse`` class used to return SQRL-formatted response.
The response is automatically signed, normalized and encoded
as per SQRL specification.
This view also adds a couple of DEBUG logs for easier SQRL debugging
and also returns all SQRL data back as ``X-SQRL-*`` headers.
Parameters
----------
nut : SQRLNut
Nut which will be used to sign the response data.
data : OrderedDict
Data to be returned back to the user.
"""
def __init__(self, nut, data, *args, **kwargs):
normalized_data = Encoder.normalize(self.sign_response(nut, data))
content = Encoder.base64_dumps(normalized_data)
kwargs.setdefault('content_type', 'application/sqrl')
super(SQRLHttpResponse, self).__init__(content, *args, **kwargs)
self['Content-Length'] = len(self.content)
if settings.DEBUG:
for k, v in normalized_data.items():
self['X-SQRL-{}'.format(k)] = v
log.debug('Response encoded data:\n{}'
''.format(content))
log.debug('Response data:\n{}'
''.format(pformat(normalized_data)))
log.debug('Response TIF breakdown:\n{}'
''.format(pformat(TIF(int(data['tif'], 16)).breakdown())))
def sign_response(self, nut, data):
"""
When nut is present, this method signs the data by adding ``mac`` key.
For signing :meth:`.crypto.HMAC.sign_data` is used.
"""
if not nut:
return data
data['mac'] = HMAC(nut, data).sign_data()
return data

View File

@ -136,8 +136,6 @@ class SQRLInitialization(object):
def sqrl_url(self):
"""
Property for getting full SQRL auth view URL including SQRL scheme and full domain with port.
Is qrl:// a recognized schema? I can't find documentation of it anywhere...
"""
return (
'{scheme}://{host}{url}'

View File

@ -0,0 +1,82 @@
.align-center {
text-align: center;
}
#footer {
padding: 0;
margin: 0;
height: 0;
}
.login #container {
margin-top: 30px;
}
.login .submit-row {
text-align: center;
clear: both;
padding: 1em 0 0 0;
}
.line-center {
margin: 0;
padding: 0 10px;
background: #ffffff;
display: inline-block;
}
.sqrl {
margin-bottom: 15px;
}
.sqrl .or {
position: relative;
z-index: 2;
}
.sqrl .or:after {
content: "";
position: absolute;
top: 50%;
left: 0;
right: 0;
border-top: solid 1px #dddddd;
z-index: -1;
}
.sqrl .or {
margin: 20px;
}
.sqrl h3 {
margin-bottom: 15px;
}
.sqrl .img {
text-align: center;
margin-bottom: 0;
}
.sqrl .img img {
width: 60%;
}
.sqrl .submit-row {
padding-top: 0;
}
.sqrl-wrap {
padding: 15px;
background-color: #fff;
text-align: center;
max-width: 300px;
border-radius: 5%;
}
#sqrl-id img {
margin: auto;
width: 100%;
padding-top: 3px;
}
.sqrl-wrap a {
font-size: 50%;
}

850
sqrl/static/sqrl/sqrl.js Normal file
View File

@ -0,0 +1,850 @@
'use strict';
(function() {
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=',
next_url = get_next_url();
if (next_url !== null) {
url = url + encodeURIComponent('?next=' + next_url);
} else {
url = url + encodeURIComponent(current_url);
}
request.open('POST', url, false);
request.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
request.onreadystatechange = handleStateChange;
function handleStateChange() {
if (request.readyState === 4) {
if (this.status === 200) {
var data = JSON.parse(this.responseText);
if (data.transaction_complete === true) {
if (data.redirect_to !== undefined) {
window.location.href = data.redirect_to;
} else {
console.error('Server indicated that SQRL transaction is complete ' +
'but has not indicated where to redirect');
}
} else {
sqrl_call();
}
}
}
}
try {
request.send(null);
} catch (exception) {
// do not send anymore requests if error occurred
}
};
window.onload = sqrl_handler;
})();
// This javascript is a mostly unchanged version of David Shim's qrcode.js, originally released under the MIT liscense with no conditionals specified
// All credit goes to them
// The original can be found here:
// https://davidshimjs.github.io/qrcodejs/
var QRCode;
! function() {
function a(a) {
this.mode = c.MODE_8BIT_BYTE, this.data = a, this.parsedData = [];
for (var b = [], d = 0, e = this.data.length; e > d; d++) {
var f = this.data.charCodeAt(d);
f > 65536 ? (b[0] = 240 | (1835008 & f) >>> 18, b[1] = 128 | (258048 & f) >>> 12, b[2] = 128 | (4032 & f) >>> 6, b[3] = 128 | 63 & f) : f > 2048 ? (b[0] = 224 | (61440 & f) >>> 12, b[1] = 128 | (4032 & f) >>> 6, b[2] = 128 | 63 & f) : f > 128 ? (b[0] = 192 | (1984 & f) >>> 6, b[1] = 128 | 63 & f) : b[0] = f, this.parsedData = this.parsedData.concat(b)
}
this.parsedData.length != this.data.length && (this.parsedData.unshift(191), this.parsedData.unshift(187), this.parsedData.unshift(239))
}
function b(a, b) {
this.typeNumber = a, this.errorCorrectLevel = b, this.modules = null, this.moduleCount = 0, this.dataCache = null, this.dataList = []
}
function i(a, b) {
if (void 0 == a.length) throw new Error(a.length + "/" + b);
for (var c = 0; c < a.length && 0 == a[c];) c++;
this.num = new Array(a.length - c + b);
for (var d = 0; d < a.length - c; d++) this.num[d] = a[d + c]
}
function j(a, b) {
this.totalCount = a, this.dataCount = b
}
function k() {
this.buffer = [], this.length = 0
}
function m() {
return "undefined" != typeof CanvasRenderingContext2D
}
function n() {
var a = !1,
b = navigator.userAgent;
return /android/i.test(b) && (a = !0, aMat = b.toString().match(/android ([0-9]\.[0-9])/i), aMat && aMat[1] && (a = parseFloat(aMat[1]))), a
}
function r(a, b) {
for (var c = 1, e = s(a), f = 0, g = l.length; g >= f; f++) {
var h = 0;
switch (b) {
case d.L:
h = l[f][0];
break;
case d.M:
h = l[f][1];
break;
case d.Q:
h = l[f][2];
break;
case d.H:
h = l[f][3]
}
if (h >= e) break;
c++
}
if (c > l.length) throw new Error("Too long data");
return c
}
function s(a) {
var b = encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g, "a");
return b.length + (b.length != a ? 3 : 0)
}
a.prototype = {
getLength: function() {
return this.parsedData.length
},
write: function(a) {
for (var b = 0, c = this.parsedData.length; c > b; b++) a.put(this.parsedData[b], 8)
}
}, b.prototype = {
addData: function(b) {
var c = new a(b);
this.dataList.push(c), this.dataCache = null
},
isDark: function(a, b) {
if (0 > a || this.moduleCount <= a || 0 > b || this.moduleCount <= b) throw new Error(a + "," + b);
return this.modules[a][b]
},
getModuleCount: function() {
return this.moduleCount
},
make: function() {
this.makeImpl(!1, this.getBestMaskPattern())
},
makeImpl: function(a, c) {
this.moduleCount = 4 * this.typeNumber + 17, this.modules = new Array(this.moduleCount);
for (var d = 0; d < this.moduleCount; d++) {
this.modules[d] = new Array(this.moduleCount);
for (var e = 0; e < this.moduleCount; e++) this.modules[d][e] = null
}
this.setupPositionProbePattern(0, 0), this.setupPositionProbePattern(this.moduleCount - 7, 0), this.setupPositionProbePattern(0, this.moduleCount - 7), this.setupPositionAdjustPattern(), this.setupTimingPattern(), this.setupTypeInfo(a, c), this.typeNumber >= 7 && this.setupTypeNumber(a), null == this.dataCache && (this.dataCache = b.createData(this.typeNumber, this.errorCorrectLevel, this.dataList)), this.mapData(this.dataCache, c)
},
setupPositionProbePattern: function(a, b) {
for (var c = -1; 7 >= c; c++)
if (!(-1 >= a + c || this.moduleCount <= a + c))
for (var d = -1; 7 >= d; d++) - 1 >= b + d || this.moduleCount <= b + d || (this.modules[a + c][b + d] = c >= 0 && 6 >= c && (0 == d || 6 == d) || d >= 0 && 6 >= d && (0 == c || 6 == c) || c >= 2 && 4 >= c && d >= 2 && 4 >= d ? !0 : !1)
},
getBestMaskPattern: function() {
for (var a = 0, b = 0, c = 0; 8 > c; c++) {
this.makeImpl(!0, c);
var d = f.getLostPoint(this);
(0 == c || a > d) && (a = d, b = c)
}
return b
},
createMovieClip: function(a, b, c) {
var d = a.createEmptyMovieClip(b, c),
e = 1;
this.make();
for (var f = 0; f < this.modules.length; f++)
for (var g = f * e, h = 0; h < this.modules[f].length; h++) {
var i = h * e,
j = this.modules[f][h];
j && (d.beginFill(0, 100), d.moveTo(i, g), d.lineTo(i + e, g), d.lineTo(i + e, g + e), d.lineTo(i, g + e), d.endFill())
}
return d
},
setupTimingPattern: function() {
for (var a = 8; a < this.moduleCount - 8; a++) null == this.modules[a][6] && (this.modules[a][6] = 0 == a % 2);
for (var b = 8; b < this.moduleCount - 8; b++) null == this.modules[6][b] && (this.modules[6][b] = 0 == b % 2)
},
setupPositionAdjustPattern: function() {
for (var a = f.getPatternPosition(this.typeNumber), b = 0; b < a.length; b++)
for (var c = 0; c < a.length; c++) {
var d = a[b],
e = a[c];
if (null == this.modules[d][e])
for (var g = -2; 2 >= g; g++)
for (var h = -2; 2 >= h; h++) this.modules[d + g][e + h] = -2 == g || 2 == g || -2 == h || 2 == h || 0 == g && 0 == h ? !0 : !1
}
},
setupTypeNumber: function(a) {
for (var b = f.getBCHTypeNumber(this.typeNumber), c = 0; 18 > c; c++) {
var d = !a && 1 == (1 & b >> c);
this.modules[Math.floor(c / 3)][c % 3 + this.moduleCount - 8 - 3] = d
}
for (var c = 0; 18 > c; c++) {
var d = !a && 1 == (1 & b >> c);
this.modules[c % 3 + this.moduleCount - 8 - 3][Math.floor(c / 3)] = d
}
},
setupTypeInfo: function(a, b) {
for (var c = this.errorCorrectLevel << 3 | b, d = f.getBCHTypeInfo(c), e = 0; 15 > e; e++) {
var g = !a && 1 == (1 & d >> e);
6 > e ? this.modules[e][8] = g : 8 > e ? this.modules[e + 1][8] = g : this.modules[this.moduleCount - 15 + e][8] = g
}
for (var e = 0; 15 > e; e++) {
var g = !a && 1 == (1 & d >> e);
8 > e ? this.modules[8][this.moduleCount - e - 1] = g : 9 > e ? this.modules[8][15 - e - 1 + 1] = g : this.modules[8][15 - e - 1] = g
}
this.modules[this.moduleCount - 8][8] = !a
},
mapData: function(a, b) {
for (var c = -1, d = this.moduleCount - 1, e = 7, g = 0, h = this.moduleCount - 1; h > 0; h -= 2)
for (6 == h && h--;;) {
for (var i = 0; 2 > i; i++)
if (null == this.modules[d][h - i]) {
var j = !1;
g < a.length && (j = 1 == (1 & a[g] >>> e));
var k = f.getMask(b, d, h - i);
k && (j = !j), this.modules[d][h - i] = j, e--, -1 == e && (g++, e = 7)
} if (d += c, 0 > d || this.moduleCount <= d) {
d -= c, c = -c;
break
}
}
}
}, b.PAD0 = 236, b.PAD1 = 17, b.createData = function(a, c, d) {
for (var e = j.getRSBlocks(a, c), g = new k, h = 0; h < d.length; h++) {
var i = d[h];
g.put(i.mode, 4), g.put(i.getLength(), f.getLengthInBits(i.mode, a)), i.write(g)
}
for (var l = 0, h = 0; h < e.length; h++) l += e[h].dataCount;
if (g.getLengthInBits() > 8 * l) throw new Error("code length overflow. (" + g.getLengthInBits() + ">" + 8 * l + ")");
for (g.getLengthInBits() + 4 <= 8 * l && g.put(0, 4); 0 != g.getLengthInBits() % 8;) g.putBit(!1);
for (;;) {
if (g.getLengthInBits() >= 8 * l) break;
if (g.put(b.PAD0, 8), g.getLengthInBits() >= 8 * l) break;
g.put(b.PAD1, 8)
}
return b.createBytes(g, e)
}, b.createBytes = function(a, b) {
for (var c = 0, d = 0, e = 0, g = new Array(b.length), h = new Array(b.length), j = 0; j < b.length; j++) {
var k = b[j].dataCount,
l = b[j].totalCount - k;
d = Math.max(d, k), e = Math.max(e, l), g[j] = new Array(k);
for (var m = 0; m < g[j].length; m++) g[j][m] = 255 & a.buffer[m + c];
c += k;
var n = f.getErrorCorrectPolynomial(l),
o = new i(g[j], n.getLength() - 1),
p = o.mod(n);
h[j] = new Array(n.getLength() - 1);
for (var m = 0; m < h[j].length; m++) {
var q = m + p.getLength() - h[j].length;
h[j][m] = q >= 0 ? p.get(q) : 0
}
}
for (var r = 0, m = 0; m < b.length; m++) r += b[m].totalCount;
for (var s = new Array(r), t = 0, m = 0; d > m; m++)
for (var j = 0; j < b.length; j++) m < g[j].length && (s[t++] = g[j][m]);
for (var m = 0; e > m; m++)
for (var j = 0; j < b.length; j++) m < h[j].length && (s[t++] = h[j][m]);
return s
};
for (var c = {
MODE_NUMBER: 1,
MODE_ALPHA_NUM: 2,
MODE_8BIT_BYTE: 4,
MODE_KANJI: 8
}, d = {
L: 1,
M: 0,
Q: 3,
H: 2
}, e = {
PATTERN000: 0,
PATTERN001: 1,
PATTERN010: 2,
PATTERN011: 3,
PATTERN100: 4,
PATTERN101: 5,
PATTERN110: 6,
PATTERN111: 7
}, f = {
PATTERN_POSITION_TABLE: [
[],
[6, 18],
[6, 22],
[6, 26],
[6, 30],
[6, 34],
[6, 22, 38],
[6, 24, 42],
[6, 26, 46],
[6, 28, 50],
[6, 30, 54],
[6, 32, 58],
[6, 34, 62],
[6, 26, 46, 66],
[6, 26, 48, 70],
[6, 26, 50, 74],
[6, 30, 54, 78],
[6, 30, 56, 82],
[6, 30, 58, 86],
[6, 34, 62, 90],
[6, 28, 50, 72, 94],
[6, 26, 50, 74, 98],
[6, 30, 54, 78, 102],
[6, 28, 54, 80, 106],
[6, 32, 58, 84, 110],
[6, 30, 58, 86, 114],
[6, 34, 62, 90, 118],
[6, 26, 50, 74, 98, 122],
[6, 30, 54, 78, 102, 126],
[6, 26, 52, 78, 104, 130],
[6, 30, 56, 82, 108, 134],
[6, 34, 60, 86, 112, 138],
[6, 30, 58, 86, 114, 142],
[6, 34, 62, 90, 118, 146],
[6, 30, 54, 78, 102, 126, 150],
[6, 24, 50, 76, 102, 128, 154],
[6, 28, 54, 80, 106, 132, 158],
[6, 32, 58, 84, 110, 136, 162],
[6, 26, 54, 82, 110, 138, 166],
[6, 30, 58, 86, 114, 142, 170]
],
G15: 1335,
G18: 7973,
G15_MASK: 21522,
getBCHTypeInfo: function(a) {
for (var b = a << 10; f.getBCHDigit(b) - f.getBCHDigit(f.G15) >= 0;) b ^= f.G15 << f.getBCHDigit(b) - f.getBCHDigit(f.G15);
return (a << 10 | b) ^ f.G15_MASK
},
getBCHTypeNumber: function(a) {
for (var b = a << 12; f.getBCHDigit(b) - f.getBCHDigit(f.G18) >= 0;) b ^= f.G18 << f.getBCHDigit(b) - f.getBCHDigit(f.G18);
return a << 12 | b
},
getBCHDigit: function(a) {
for (var b = 0; 0 != a;) b++, a >>>= 1;
return b
},
getPatternPosition: function(a) {
return f.PATTERN_POSITION_TABLE[a - 1]
},
getMask: function(a, b, c) {
switch (a) {
case e.PATTERN000:
return 0 == (b + c) % 2;
case e.PATTERN001:
return 0 == b % 2;
case e.PATTERN010:
return 0 == c % 3;
case e.PATTERN011:
return 0 == (b + c) % 3;
case e.PATTERN100:
return 0 == (Math.floor(b / 2) + Math.floor(c / 3)) % 2;
case e.PATTERN101:
return 0 == b * c % 2 + b * c % 3;
case e.PATTERN110:
return 0 == (b * c % 2 + b * c % 3) % 2;
case e.PATTERN111:
return 0 == (b * c % 3 + (b + c) % 2) % 2;
default:
throw new Error("bad maskPattern:" + a)
}
},
getErrorCorrectPolynomial: function(a) {
for (var b = new i([1], 0), c = 0; a > c; c++) b = b.multiply(new i([1, g.gexp(c)], 0));
return b
},
getLengthInBits: function(a, b) {
if (b >= 1 && 10 > b) switch (a) {
case c.MODE_NUMBER:
return 10;
case c.MODE_ALPHA_NUM:
return 9;
case c.MODE_8BIT_BYTE:
return 8;
case c.MODE_KANJI:
return 8;
default:
throw new Error("mode:" + a)
} else if (27 > b) switch (a) {
case c.MODE_NUMBER:
return 12;
case c.MODE_ALPHA_NUM:
return 11;
case c.MODE_8BIT_BYTE:
return 16;
case c.MODE_KANJI:
return 10;
default:
throw new Error("mode:" + a)
} else {
if (!(41 > b)) throw new Error("type:" + b);
switch (a) {
case c.MODE_NUMBER:
return 14;
case c.MODE_ALPHA_NUM:
return 13;
case c.MODE_8BIT_BYTE:
return 16;
case c.MODE_KANJI:
return 12;
default:
throw new Error("mode:" + a)
}
}
},
getLostPoint: function(a) {
for (var b = a.getModuleCount(), c = 0, d = 0; b > d; d++)
for (var e = 0; b > e; e++) {
for (var f = 0, g = a.isDark(d, e), h = -1; 1 >= h; h++)
if (!(0 > d + h || d + h >= b))
for (var i = -1; 1 >= i; i++) 0 > e + i || e + i >= b || (0 != h || 0 != i) && g == a.isDark(d + h, e + i) && f++;
f > 5 && (c += 3 + f - 5)
}
for (var d = 0; b - 1 > d; d++)
for (var e = 0; b - 1 > e; e++) {
var j = 0;
a.isDark(d, e) && j++, a.isDark(d + 1, e) && j++, a.isDark(d, e + 1) && j++, a.isDark(d + 1, e + 1) && j++, (0 == j || 4 == j) && (c += 3)
}
for (var d = 0; b > d; d++)
for (var e = 0; b - 6 > e; e++) a.isDark(d, e) && !a.isDark(d, e + 1) && a.isDark(d, e + 2) && a.isDark(d, e + 3) && a.isDark(d, e + 4) && !a.isDark(d, e + 5) && a.isDark(d, e + 6) && (c += 40);
for (var e = 0; b > e; e++)
for (var d = 0; b - 6 > d; d++) a.isDark(d, e) && !a.isDark(d + 1, e) && a.isDark(d + 2, e) && a.isDark(d + 3, e) && a.isDark(d + 4, e) && !a.isDark(d + 5, e) && a.isDark(d + 6, e) && (c += 40);
for (var k = 0, e = 0; b > e; e++)
for (var d = 0; b > d; d++) a.isDark(d, e) && k++;
var l = Math.abs(100 * k / b / b - 50) / 5;
return c += 10 * l
}
}, g = {
glog: function(a) {
if (1 > a) throw new Error("glog(" + a + ")");
return g.LOG_TABLE[a]
},
gexp: function(a) {
for (; 0 > a;) a += 255;
for (; a >= 256;) a -= 255;
return g.EXP_TABLE[a]
},
EXP_TABLE: new Array(256),
LOG_TABLE: new Array(256)
}, h = 0; 8 > h; h++) g.EXP_TABLE[h] = 1 << h;
for (var h = 8; 256 > h; h++) g.EXP_TABLE[h] = g.EXP_TABLE[h - 4] ^ g.EXP_TABLE[h - 5] ^ g.EXP_TABLE[h - 6] ^ g.EXP_TABLE[h - 8];
for (var h = 0; 255 > h; h++) g.LOG_TABLE[g.EXP_TABLE[h]] = h;
i.prototype = {
get: function(a) {
return this.num[a]
},
getLength: function() {
return this.num.length
},
multiply: function(a) {
for (var b = new Array(this.getLength() + a.getLength() - 1), c = 0; c < this.getLength(); c++)
for (var d = 0; d < a.getLength(); d++) b[c + d] ^= g.gexp(g.glog(this.get(c)) + g.glog(a.get(d)));
return new i(b, 0)
},
mod: function(a) {
if (this.getLength() - a.getLength() < 0) return this;
for (var b = g.glog(this.get(0)) - g.glog(a.get(0)), c = new Array(this.getLength()), d = 0; d < this.getLength(); d++) c[d] = this.get(d);
for (var d = 0; d < a.getLength(); d++) c[d] ^= g.gexp(g.glog(a.get(d)) + b);
return new i(c, 0).mod(a)
}
}, j.RS_BLOCK_TABLE = [
[1, 26, 19],
[1, 26, 16],
[1, 26, 13],
[1, 26, 9],
[1, 44, 34],
[1, 44, 28],
[1, 44, 22],
[1, 44, 16],
[1, 70, 55],
[1, 70, 44],
[2, 35, 17],
[2, 35, 13],
[1, 100, 80],
[2, 50, 32],
[2, 50, 24],
[4, 25, 9],
[1, 134, 108],
[2, 67, 43],
[2, 33, 15, 2, 34, 16],
[2, 33, 11, 2, 34, 12],
[2, 86, 68],
[4, 43, 27],
[4, 43, 19],
[4, 43, 15],
[2, 98, 78],
[4, 49, 31],
[2, 32, 14, 4, 33, 15],
[4, 39, 13, 1, 40, 14],
[2, 121, 97],
[2, 60, 38, 2, 61, 39],
[4, 40, 18, 2, 41, 19],
[4, 40, 14, 2, 41, 15],
[2, 146, 116],
[3, 58, 36, 2, 59, 37],
[4, 36, 16, 4, 37, 17],
[4, 36, 12, 4, 37, 13],
[2, 86, 68, 2, 87, 69],
[4, 69, 43, 1, 70, 44],
[6, 43, 19, 2, 44, 20],
[6, 43, 15, 2, 44, 16],
[4, 101, 81],
[1, 80, 50, 4, 81, 51],
[4, 50, 22, 4, 51, 23],
[3, 36, 12, 8, 37, 13],
[2, 116, 92, 2, 117, 93],
[6, 58, 36, 2, 59, 37],
[4, 46, 20, 6, 47, 21],
[7, 42, 14, 4, 43, 15],
[4, 133, 107],
[8, 59, 37, 1, 60, 38],
[8, 44, 20, 4, 45, 21],
[12, 33, 11, 4, 34, 12],
[3, 145, 115, 1, 146, 116],
[4, 64, 40, 5, 65, 41],
[11, 36, 16, 5, 37, 17],
[11, 36, 12, 5, 37, 13],
[5, 109, 87, 1, 110, 88],
[5, 65, 41, 5, 66, 42],
[5, 54, 24, 7, 55, 25],
[11, 36, 12],
[5, 122, 98, 1, 123, 99],
[7, 73, 45, 3, 74, 46],
[15, 43, 19, 2, 44, 20],
[3, 45, 15, 13, 46, 16],
[1, 135, 107, 5, 136, 108],
[10, 74, 46, 1, 75, 47],
[1, 50, 22, 15, 51, 23],
[2, 42, 14, 17, 43, 15],
[5, 150, 120, 1, 151, 121],
[9, 69, 43, 4, 70, 44],
[17, 50, 22, 1, 51, 23],
[2, 42, 14, 19, 43, 15],
[3, 141, 113, 4, 142, 114],
[3, 70, 44, 11, 71, 45],
[17, 47, 21, 4, 48, 22],
[9, 39, 13, 16, 40, 14],
[3, 135, 107, 5, 136, 108],
[3, 67, 41, 13, 68, 42],
[15, 54, 24, 5, 55, 25],
[15, 43, 15, 10, 44, 16],
[4, 144, 116, 4, 145, 117],
[17, 68, 42],
[17, 50, 22, 6, 51, 23],
[19, 46, 16, 6, 47, 17],
[2, 139, 111, 7, 140, 112],
[17, 74, 46],
[7, 54, 24, 16, 55, 25],
[34, 37, 13],
[4, 151, 121, 5, 152, 122],
[4, 75, 47, 14, 76, 48],
[11, 54, 24, 14, 55, 25],
[16, 45, 15, 14, 46, 16],
[6, 147, 117, 4, 148, 118],
[6, 73, 45, 14, 74, 46],
[11, 54, 24, 16, 55, 25],
[30, 46, 16, 2, 47, 17],
[8, 132, 106, 4, 133, 107],
[8, 75, 47, 13, 76, 48],
[7, 54, 24, 22, 55, 25],
[22, 45, 15, 13, 46, 16],
[10, 142, 114, 2, 143, 115],
[19, 74, 46, 4, 75, 47],
[28, 50, 22, 6, 51, 23],
[33, 46, 16, 4, 47, 17],
[8, 152, 122, 4, 153, 123],
[22, 73, 45, 3, 74, 46],
[8, 53, 23, 26, 54, 24],
[12, 45, 15, 28, 46, 16],
[3, 147, 117, 10, 148, 118],
[3, 73, 45, 23, 74, 46],
[4, 54, 24, 31, 55, 25],
[11, 45, 15, 31, 46, 16],
[7, 146, 116, 7, 147, 117],
[21, 73, 45, 7, 74, 46],
[1, 53, 23, 37, 54, 24],
[19, 45, 15, 26, 46, 16],
[5, 145, 115, 10, 146, 116],
[19, 75, 47, 10, 76, 48],
[15, 54, 24, 25, 55, 25],
[23, 45, 15, 25, 46, 16],
[13, 145, 115, 3, 146, 116],
[2, 74, 46, 29, 75, 47],
[42, 54, 24, 1, 55, 25],
[23, 45, 15, 28, 46, 16],
[17, 145, 115],
[10, 74, 46, 23, 75, 47],
[10, 54, 24, 35, 55, 25],
[19, 45, 15, 35, 46, 16],
[17, 145, 115, 1, 146, 116],
[14, 74, 46, 21, 75, 47],
[29, 54, 24, 19, 55, 25],
[11, 45, 15, 46, 46, 16],
[13, 145, 115, 6, 146, 116],
[14, 74, 46, 23, 75, 47],
[44, 54, 24, 7, 55, 25],
[59, 46, 16, 1, 47, 17],
[12, 151, 121, 7, 152, 122],
[12, 75, 47, 26, 76, 48],
[39, 54, 24, 14, 55, 25],
[22, 45, 15, 41, 46, 16],
[6, 151, 121, 14, 152, 122],
[6, 75, 47, 34, 76, 48],
[46, 54, 24, 10, 55, 25],
[2, 45, 15, 64, 46, 16],
[17, 152, 122, 4, 153, 123],
[29, 74, 46, 14, 75, 47],
[49, 54, 24, 10, 55, 25],
[24, 45, 15, 46, 46, 16],
[4, 152, 122, 18, 153, 123],
[13, 74, 46, 32, 75, 47],
[48, 54, 24, 14, 55, 25],
[42, 45, 15, 32, 46, 16],
[20, 147, 117, 4, 148, 118],
[40, 75, 47, 7, 76, 48],
[43, 54, 24, 22, 55, 25],
[10, 45, 15, 67, 46, 16],
[19, 148, 118, 6, 149, 119],
[18, 75, 47, 31, 76, 48],
[34, 54, 24, 34, 55, 25],
[20, 45, 15, 61, 46, 16]
], j.getRSBlocks = function(a, b) {
var c = j.getRsBlockTable(a, b);
if (void 0 == c) throw new Error("bad rs block @ typeNumber:" + a + "/errorCorrectLevel:" + b);
for (var d = c.length / 3, e = [], f = 0; d > f; f++)
for (var g = c[3 * f + 0], h = c[3 * f + 1], i = c[3 * f + 2], k = 0; g > k; k++) e.push(new j(h, i));
return e
}, j.getRsBlockTable = function(a, b) {
switch (b) {
case d.L:
return j.RS_BLOCK_TABLE[4 * (a - 1) + 0];
case d.M:
return j.RS_BLOCK_TABLE[4 * (a - 1) + 1];
case d.Q:
return j.RS_BLOCK_TABLE[4 * (a - 1) + 2];
case d.H:
return j.RS_BLOCK_TABLE[4 * (a - 1) + 3];
default:
return void 0
}
}, k.prototype = {
get: function(a) {
var b = Math.floor(a / 8);
return 1 == (1 & this.buffer[b] >>> 7 - a % 8)
},
put: function(a, b) {
for (var c = 0; b > c; c++) this.putBit(1 == (1 & a >>> b - c - 1))
},
getLengthInBits: function() {
return this.length
},
putBit: function(a) {
var b = Math.floor(this.length / 8);
this.buffer.length <= b && this.buffer.push(0), a && (this.buffer[b] |= 128 >>> this.length % 8), this.length++
}
};
var l = [
[17, 14, 11, 7],
[32, 26, 20, 14],
[53, 42, 32, 24],
[78, 62, 46, 34],
[106, 84, 60, 44],
[134, 106, 74, 58],
[154, 122, 86, 64],
[192, 152, 108, 84],
[230, 180, 130, 98],
[271, 213, 151, 119],
[321, 251, 177, 137],
[367, 287, 203, 155],
[425, 331, 241, 177],
[458, 362, 258, 194],
[520, 412, 292, 220],
[586, 450, 322, 250],
[644, 504, 364, 280],
[718, 560, 394, 310],
[792, 624, 442, 338],
[858, 666, 482, 382],
[929, 711, 509, 403],
[1003, 779, 565, 439],
[1091, 857, 611, 461],
[1171, 911, 661, 511],
[1273, 997, 715, 535],
[1367, 1059, 751, 593],
[1465, 1125, 805, 625],
[1528, 1190, 868, 658],
[1628, 1264, 908, 698],
[1732, 1370, 982, 742],
[1840, 1452, 1030, 790],
[1952, 1538, 1112, 842],
[2068, 1628, 1168, 898],
[2188, 1722, 1228, 958],
[2303, 1809, 1283, 983],
[2431, 1911, 1351, 1051],
[2563, 1989, 1423, 1093],
[2699, 2099, 1499, 1139],
[2809, 2213, 1579, 1219],
[2953, 2331, 1663, 1273]
],
o = function() {
var a = function(a, b) {
this._el = a, this._htOption = b
};
return a.prototype.draw = function(a) {
function g(a, b) {
var c = document.createElementNS("http://www.w3.org/2000/svg", a);
for (var d in b) b.hasOwnProperty(d) && c.setAttribute(d, b[d]);
return c
}
var b = this._htOption,
c = this._el,
d = a.getModuleCount();
Math.floor(b.width / d), Math.floor(b.height / d), this.clear();
var h = g("svg", {
viewBox: "0 0 " + String(d) + " " + String(d),
width: "100%",
height: "100%",
fill: b.colorLight
});
h.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xlink", "http://www.w3.org/1999/xlink"), c.appendChild(h), h.appendChild(g("rect", {
fill: b.colorDark,
width: "1",
height: "1",
id: "template"
}));
for (var i = 0; d > i; i++)
for (var j = 0; d > j; j++)
if (a.isDark(i, j)) {
var k = g("use", {
x: String(i),
y: String(j)
});
k.setAttributeNS("http://www.w3.org/1999/xlink", "href", "#template"), h.appendChild(k)
}
}, a.prototype.clear = function() {
for (; this._el.hasChildNodes();) this._el.removeChild(this._el.lastChild)
}, a
}(),
p = "svg" === document.documentElement.tagName.toLowerCase(),
q = p ? o : m() ? function() {
function a() {
this._elImage.src = this._elCanvas.toDataURL("image/png"), this._elImage.style.display = "block", this._elCanvas.style.display = "none"
}
function d(a, b) {
var c = this;
if (c._fFail = b, c._fSuccess = a, null === c._bSupportDataURI) {
var d = document.createElement("img"),
e = function() {
c._bSupportDataURI = !1, c._fFail && _fFail.call(c)
},
f = function() {
c._bSupportDataURI = !0, c._fSuccess && c._fSuccess.call(c)
};
return d.onabort = e, d.onerror = e, d.onload = f, d.src = "data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==", void 0
}
c._bSupportDataURI === !0 && c._fSuccess ? c._fSuccess.call(c) : c._bSupportDataURI === !1 && c._fFail && c._fFail.call(c)
}
if (this._android && this._android <= 2.1) {
var b = 1 / window.devicePixelRatio,
c = CanvasRenderingContext2D.prototype.drawImage;
CanvasRenderingContext2D.prototype.drawImage = function(a, d, e, f, g, h, i, j) {
if ("nodeName" in a && /img/i.test(a.nodeName))
for (var l = arguments.length - 1; l >= 1; l--) arguments[l] = arguments[l] * b;
else "undefined" == typeof j && (arguments[1] *= b, arguments[2] *= b, arguments[3] *= b, arguments[4] *= b);
c.apply(this, arguments)
}
}
var e = function(a, b) {
this._bIsPainted = !1, this._android = n(), this._htOption = b, this._elCanvas = document.createElement("canvas"), this._elCanvas.width = b.width, this._elCanvas.height = b.height, a.appendChild(this._elCanvas), this._el = a, this._oContext = this._elCanvas.getContext("2d"), this._bIsPainted = !1, this._elImage = document.createElement("img"), this._elImage.style.display = "none", this._el.appendChild(this._elImage), this._bSupportDataURI = null
};
return e.prototype.draw = function(a) {
var b = this._elImage,
c = this._oContext,
d = this._htOption,
e = a.getModuleCount(),
f = d.width / e,
g = d.height / e,
h = Math.round(f),
i = Math.round(g);
b.style.display = "none", this.clear();
for (var j = 0; e > j; j++)
for (var k = 0; e > k; k++) {
var l = a.isDark(j, k),
m = k * f,
n = j * g;
c.strokeStyle = l ? d.colorDark : d.colorLight, c.lineWidth = 1, c.fillStyle = l ? d.colorDark : d.colorLight, c.fillRect(m, n, f, g), c.strokeRect(Math.floor(m) + .5, Math.floor(n) + .5, h, i), c.strokeRect(Math.ceil(m) - .5, Math.ceil(n) - .5, h, i)
}
this._bIsPainted = !0
}, e.prototype.makeImage = function() {
this._bIsPainted && d.call(this, a)
}, e.prototype.isPainted = function() {
return this._bIsPainted
}, e.prototype.clear = function() {
this._oContext.clearRect(0, 0, this._elCanvas.width, this._elCanvas.height), this._bIsPainted = !1
}, e.prototype.round = function(a) {
return a ? Math.floor(1e3 * a) / 1e3 : a
}, e
}() : function() {
var a = function(a, b) {
this._el = a, this._htOption = b
};
return a.prototype.draw = function(a) {
for (var b = this._htOption, c = this._el, d = a.getModuleCount(), e = Math.floor(b.width / d), f = Math.floor(b.height / d), g = ['<table style="border:0;border-collapse:collapse;">'], h = 0; d > h; h++) {
g.push("<tr>");
for (var i = 0; d > i; i++) g.push('<td style="border:0;border-collapse:collapse;padding:0;margin:0;width:' + e + "px;height:" + f + "px;background-color:" + (a.isDark(h, i) ? b.colorDark : b.colorLight) + ';"></td>');
g.push("</tr>")
}
g.push("</table>"), c.innerHTML = g.join("");
var j = c.childNodes[0],
k = (b.width - j.offsetWidth) / 2,
l = (b.height - j.offsetHeight) / 2;
k > 0 && l > 0 && (j.style.margin = l + "px " + k + "px")
}, a.prototype.clear = function() {
this._el.innerHTML = ""
}, a
}();
QRCode = function(a, b) {
if (this._htOption = {
width: 256,
height: 256,
typeNumber: 4,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: d.H
}, "string" == typeof b && (b = {
text: b
}), b)
for (var c in b) this._htOption[c] = b[c];
"string" == typeof a && (a = document.getElementById(a)), this._android = n(), this._el = a, this._oQRCode = null, this._oDrawing = new q(this._el, this._htOption), this._htOption.text && this.makeCode(this._htOption.text)
}, QRCode.prototype.makeCode = function(a) {
this._oQRCode = new b(r(a, this._htOption.correctLevel), this._htOption.correctLevel), this._oQRCode.addData(a), this._oQRCode.make(), this._oDrawing.draw(this._oQRCode), this.makeImage()
}, QRCode.prototype.makeImage = function() {
"function" == typeof this._oDrawing.makeImage && (!this._android || this._android >= 3) && this._oDrawing.makeImage()
}, QRCode.prototype.clear = function() {
this._oDrawing.clear()
}, QRCode.CorrectLevel = d
}();
var qr = document.getElementById("sqrl-qr");
new QRCode(qr, qr.dataset.sqrl);

View File

@ -0,0 +1,85 @@
{% 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' %}"/>
<link rel="stylesheet" type="text/css" href="{% static 'admin/sqrl.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 session_sqrl %}
<fieldset class="module aligned">
<div class="sqrl-wrap">
SQRL Login
<a href="{{ session_sqrl.sqrl_url }}">
<div id="sqrl-qr" data-sqrl="{{ session_sqrl.sqrl_url }}"></div>
</a>
<a href="https://www.grc.com/sqrl/sqrl.htm">What is SQRL?</a>
{# redirect to manage page after successful SQRL transaction #}
<input type="hidden" name="next" value="{% url 'admin-sqrl_manage' %}">
</div>
</fieldset>
<form method="get" action="{{ session_sqrl.sqrl_url }}" class="sqrl">
<div class="submit-row">
<input type="hidden" name="nut" value="{{ session_sqrl.nut.nonce }}">
<input type="submit" value="Manage SQRL" class="default" style="float: left;">
</div>
</form>
</div>
{% endblock %}

View File

@ -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 %}

View File

@ -0,0 +1,91 @@
{% extends "admin/base_site.html" %}
{% load i18n admin_static %}
{% load sqrl %}
{% block extrastyle %}
{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static 'admin/css/login.css' %}"/>
<link rel="stylesheet" type="text/css" href="{% static 'admin/sqrl.css' %}"/>
{% endblock %}
{% block bodyclass %}{{ block.super }} login{% endblock %}
{% block nav-global %}{% endblock %}
{% block content_title %}{% endblock %}
{% block breadcrumbs %}{% endblock %}
{% block content %}
{% if form.errors and not form.non_field_errors %}
<p class="errornote">
{% if form.errors.items|length == 1 %}
{% trans "Please correct the error below." %}{% else %}
{% trans "Please correct the errors below." %}{% endif %}
</p>
{% endif %}
{% if form.non_field_errors %}
{% for error in form.non_field_errors %}
<p class="errornote">
{{ error }}
</p>
{% endfor %}
{% endif %}
<div id="content-main">
<form action="{{ app_path }}" method="post" id="login-form">{% csrf_token %}
<div class="form-row">
{{ form.username.errors }}
<label for="id_username"
class="required">{{ form.username.label }}:</label> {{ form.username }}
</div>
<div class="form-row">
{{ form.password.errors }}
<label for="id_password"
class="required">{% trans 'Password:' %}</label> {{ form.password }}
<input type="hidden" name="next" value="{{ next }}"/>
</div>
{% url 'admin_password_reset' as password_reset_url %}
{% if password_reset_url %}
<div class="password-reset-link">
<a href="{{ password_reset_url }}">{% trans 'Forgotten your password or username?' %}</a>
</div>
{% endif %}
<div class="submit-row">
<label>&nbsp;</label><input type="submit" value="{% trans 'Log in' %}"/>
</div>
</form>
{% sqrl as session_sqrl %}
<form method="get" action="{{ session_sqrl.sqrl_url }}" class="sqrl">
<p class="align-center or">
<span class="line-center">or</span>
</p>
<div>
<h3>Login using SQRL</h3>
<div class="sqrl-wrap">
SQRL Login
<a href="{{ session_sqrl.sqrl_url }}">
<div id="sqrl-qr" data-sqrl="{{ session_sqrl.sqrl_url }}"></div>
</a>
<a href="https://www.grc.com/sqrl/sqrl.htm">What is SQRL?</a>
</div>
<div class="submit-row">
<input type="hidden" name="nut" value="{{ session_sqrl.nut.nonce }}">
<input type="submit" value="{% trans 'Log in using SQRL' %}"/>
</div>
</div>
</form>
{% sqrl_status_url_script_tag session_sqrl %}
<script type="text/javascript">
document.getElementById('id_username').focus()
</script>
<script src="{% static 'sqrl/sqrl.js' %}"></script>
</div>
{% endblock %}

View File

@ -0,0 +1,27 @@
{% extends "sqrl/sqrl_base.html" %}
{% load sqrl %}
{% load static %}
{% block title %}Log in via SQRL{% endblock %}
{% block content %}
<h1>Log in</h1>
<p>
Please use via SQRL using the information below.
</p>
{% sqrl as session_sqrl %}
{% sqrl_login_dropin session_sqrl %}
{% endblock %}
{% comment %}
**sqrl/login.html**
This is SQRL-only login page.
No special variables are passed in the context for the login to work.
All necessary data should already be in the context.
Please note again that this template is for SQRL-exclusive logins.
If you would like to add SQRL login to an existing login page,
you should rather adjust that template as it is probably way more involved.
{% endcomment %}

View File

@ -0,0 +1,54 @@
{% extends "sqrl/sqrl_base.html" %}
{% load sqrl %}
{% load static %}
{% block title %}Manage SQRL Identity{% endblock %}
{% block content %}
<h1>Manage SQRL Identity with your account</h1>
{% 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 session_sqrl %}
{% sqrl_login_dropin session_sqrl method="manage" %}
{% endblock %}
{% block scripts %}
<script src="{% static 'sqrl/sqrl.js' %}"></script>
{% endblock %}
{% comment %}
**sqrl/manage.html**
Used to manage SQRL identity in relation to existing logged in account.
When no SQRL identity is already associated with the account,
it can be used to associate a new identity.
When existing SQRL is already associated, this page can be used
to either change or delete existing identity.
No special variables are passed in the context for the association
to work. All necessary data should already be in the context.
{% endcomment %}

View File

@ -0,0 +1,30 @@
{% extends "sqrl/sqrl_base.html" %}
{% block title %}Complete User Tegistration{% endblock %}
{% block content %}
<h1>Complete User Registration</h1>
<p>
Already have account?
Login <a href="{% url 'login' %}">here</a> to associate
SQRL identity with existing account.
</p>
<form method="post">
{{ form.as_p }}
<input type="submit">
</form>
{% endblock %}
{% comment %}
**sqrl/register.html**
Used to complete user registration. This template is used when user is trying to login
with SQRL however does not already have that SQRL identity associated with any
existing account. Since SQRL does not provide any additional information other then
SQRL identity itslef, we need to collect some additinal information from the user
such as username. All the relevant information is collected via form.
No special variables are passed in the context.
All necessary data should already be in the context.
{% endcomment %}

View File

@ -0,0 +1,41 @@
{% load static %}
{% load sqrl %}
<style>
.sqrl-wrap {
padding: 15px;
background-color: #fff;
text-align: center;
max-width: 300px;
border-radius: 5%;
}
#sqrl-id img {
margin: auto;
width: 100%;
padding-top: 3px;
}
.sqrl-wrap a {
font-size: 50%;
}
</style>
<div>
<form method="get" action="{{ session_sqrl.sqrl_url }}">
<div class="sqrl-wrap">
SQRL Login
<a href="{{ session_sqrl.sqrl_url }}">
<div id="sqrl-qr" data-sqrl="{{ session_sqrl.sqrl_url }}"></div>
</a>
<a href="https://www.grc.com/sqrl/sqrl.htm">What is SQRL?</a>
</div>
<input type="hidden" name="nut" value="{{ session_sqrl.nut.nonce }}">
{% if session_sqrl.method == "manage" %}
{# redirect to manage page after successful SQRL transaction #}
<input type="hidden" name="next" value="{% url 'sqrl:manage' %}">
<input type="submit" value="Manage SQRL">
{% else %}
<input type="submit" value="Log in using SQRL">
{% endif %}
</form>
</div>
{% sqrl_status_url_script_tag session_sqrl %}
<script src="{% static sqrl/sqrl.js %}"></script>

View File

@ -0,0 +1 @@
{% extends "base.html" %}

44
sqrl/templatetags/sqrl.py Normal file
View File

@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
from django import template
from django.urls import reverse
from django.template.defaultfilters import urlencode
from ..sqrl import SQRLInitialization
register = template.Library()
@register.assignment_tag(takes_context=True)
def sqrl(context):
return SQRLInitialization(context['request'])
@register.inclusion_tag('sqrl/sqrl-dropin.html')
def sqrl_login_dropin(session_sqrl, method="login"):
"""
Creates a drop-in SQRL element in your template pages.
Add it to your login template to make it SQRL-aware.
Usage:
{% load sqrl %}
{% sqrl as session_sqrl %}
{% sqrl_login_dropin session_sqrl [method=METHOD] %}
METHOD is an optional argument that changes the way the form
behaves. Possible arguments are:
- login: The default method. No special redirections occur
- manage: Will redirect the user to sqrl/manage
Notes:
The drop-in is defaulted to a max-width of 300px. Set the width
property of the parent if you want or need it smaller. You will
likely want to change the font-size as well in this case.
"""
return {'session_sqrl':session_sqrl, 'method': method}
@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)

View File

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
from django.urls import path, re_path
from .views import (
SQRLAuthView,
SQRLCompleteRegistrationView,
SQRLIdentityManagementView,
SQRLLoginView,
SQRLStatusView,
)
urlpatterns = [
path("auth/", SQRLAuthView.as_view(), name="auth"),
path("login/", SQRLLoginView.as_view(), name="login"),
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

@ -2,8 +2,7 @@
from base64 import urlsafe_b64decode, urlsafe_b64encode
from collections import OrderedDict
import six
from django.core.files.base import ContentFile
class Base64(object):
"""
@ -14,26 +13,24 @@ class Base64(object):
def encode(cls, s):
"""
Encode binary string as base64. Remaining "=" characters are removed.
Parameters
----------
s: bytes
Bytes string to be encoded as base64
"""
assert isinstance(s, (six.binary_type, bytearray))
assert isinstance(s, (bites, bytearray))
return urlsafe_b64encode(s).decode('ascii').rstrip('=')
@classmethod
def decode(cls, s):
"""
Decode unicode string from base64 where remaining "=" characters were stripped.
Parameters
----------
s: str
Unicode string to be decoded from base64
"""
assert isinstance(s, six.text_type)
assert isinstance(s, str)
return urlsafe_b64decode((s + '=' * (4 - len(s) % 4)).encode('ascii'))
@ -46,11 +43,9 @@ class Encoder(object):
def base64_dumps(cls, data):
"""
Dumps given data into a single Base64 string.
Practically this is the same as :meth:`dumps` except :meth:`dumps`
can return multiline string for ``dict``. This method normalizes that
further by converting that multiline string to a single base64 encoded value.
Returns
-------
binary
@ -64,11 +59,8 @@ class Encoder(object):
def dumps(cls, data):
"""
Recursively dumps given data to SQRL response format.
Before data is dumped out, it is normalized by using :meth:`.normalize`.
This dumps each data type as follows:
:``dict``: returns an ``\\r\\n`` multiline string. Each line is for a single key-pair
of format ``<key>=<dumped value>``.
:``list``: tilde (``~``) joined dumped list of values
@ -90,9 +82,7 @@ class Encoder(object):
def normalize(cls, data):
"""
Recursively normalize data for encoding.
This encodes each data type as follows:
:``dict``: returns an ``OrderedDict`` where all values are recursively normalized.
Empty dict is normalized to empty string
:``list``: each value is recursively normalized
@ -110,12 +100,12 @@ class Encoder(object):
return ''
elif isinstance(data, (list, tuple)):
return [cls.dumps(i) for i in data]
elif isinstance(data, six.binary_type):
elif isinstance(data, bytes):
return Base64.encode(data)
elif isinstance(data, six.text_type):
elif isinstance(data, str):
return data
else:
return six.text_type(data)
return str(data)
def get_user_ip(request):
"""
@ -128,4 +118,4 @@ def get_user_ip(request):
return (
request.META.get('HTTP_X_REAL_IP')
or request.META['REMOTE_ADDR']
)
)

View File

@ -1,3 +1,680 @@
from django.shortcuts import render
# -*- coding: utf-8 -*-
import json
import logging
from collections import OrderedDict
from pprint import pformat
# Create your views here.
from braces.views._access import LoginRequiredMixin
from django.conf import settings
from django.contrib.auth import (
BACKEND_SESSION_KEY,
HASH_SESSION_KEY,
SESSION_KEY,
login,
)
from django.core import serializers
from django.url 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
from .backends import SQRL_MODEL_BACKEND
from .exceptions import TIF, TIFException
from .forms import (
AuthQueryDictForm,
ExtractedNextUrlForm,
GenerateQRForm,
NextUrlForm,
PasswordLessUserCreationForm,
RequestForm,
)
from .models import SQRLIdentity, SQRLNut
from .response import SQRLHttpResponse
from .sqrl import SQRLInitialization
from .utils import Base64, QRGenerator, get_user_ip
SQRL_IDENTITY_SESSION_KEY = '_sqrl_identity'
log = logging.getLogger(__name__)
class SQRLLoginView(TemplateView):
"""
Simple ``TemplateView`` which renders ``sqrl/login.html`` template.
The template can (and probably should) be customized in each Django Project.
.. note::
This templates only provides SQRL auth method. If other methods are required
on the same login page, it is probably better to add SQRL auth method to
existing login page vs customizing this template/view.
"""
template_name = 'sqrl/login.html'
class SQRLStatusView(View):
"""
Ajax view which returns the status of the SQRL transaction back to the user.
The state of the transaction is looked up by finding the appropriate
:obj:`.models.SQRLNut` by the transaction ID which is a kwarg in the url pattern.
When the nut is found, :attr:`.models.SQRLNut.is_transaction_complete` is
used to determine the state of the transaction.
This view is useful because when it returns a redirect upon successful completing
of SQRL transaction, js can dynamically redirect the user to that page.
Without this behaviour, user will have to manually refresh the page
which is inconvenient.
.. note::
Currently this view is being used via polling on js side
however this view's concept can easily be adopted to any other
real-time technology such as websockets.
"""
success_url = settings.LOGIN_REDIRECT_URL
def get_success_url(self):
"""
Get the url to which the user will be redirect to after
successful SQRL transaction.
The url is computed using :obj:`.forms.ExtractedNextUrlForm`.
Following URLs are used depending if the form is valid:
:``True``: Next url within the ``?url=`` querystring parameter
:``False``: ``settings.LOGIN_REDIRECT_URL``
When however the user is not logged in, even after successful
SQRL transaction and has pending registration, user will be
redirected to ``sqrl-complete-registration`` view with the
``?next=`` querystring parameter set to the url computed above.
"""
next_form = ExtractedNextUrlForm(self.request.GET)
if next_form.is_valid():
url = next_form.cleaned_data['url']
else:
url = self.success_url
if all([not self.request.user.is_authenticated(),
SQRL_IDENTITY_SESSION_KEY in self.request.session]):
return reverse('sqrl:complete-registration') + '?next={}'.format(url)
else:
return url
def get_object(self):
"""
Get the :obj:`.models.SQRLNut` by transaction is or raise ``404``.
"""
return get_object_or_404(SQRLNut, transaction_nonce=self.kwargs['transaction'])
def post(self, request, transaction, *args, **kwargs):
"""
Handle the request and return appropriate data back to the user.
Following keys can be returned:
:``transaction_complete``: boolean which is always returned
:``redirect_to``: also present when ``transaction_complete == True``
and this is where the js should redirect the user to.
.. note::
This view is restricted to ajax calls as to restrict its
use from regular forms.
"""
if not request.is_ajax():
return HttpResponse(status=405) # method not allowed
transaction = self.get_object()
data = {
'transaction_complete': False,
}
if transaction.is_transaction_complete:
data.update({
'transaction_complete': True,
'redirect_to': self.get_success_url(),
})
return JsonResponse(data)
class SQRLAuthView(View):
"""
This is the main view responsible for all interactions with SQRL client.
The responsibilities of this view are:
* validate that URL is correct since nut value is part of querystring
which cannot be matched in url patterns.
When invalid, 404 should be returned.
* find the nut via nut nonce or return transient error TIF
* validate client payload by using :obj:`.forms.RequestForm` which
includes validating validity of signatures and looking up stored
SQRL identities.
* executing all SQRL commands such as ``query``, ``ident``, etc
as instructed in the SQRL payload.
If any of the commands are not supported not supported TIF
is returned.
* finalize the any remaining state such as saving identity objects
if all commands successfully completed
* returning response back to the user
"""
http_method_names = ['post']
def __init__(self, *args, **kwargs):
super(SQRLAuthView, self).__init__(*args, **kwargs)
self.tif = TIF(0)
self.nut_value = None
self.nut = None
self.client = None
self.identity = None
self.previous_identity = None
self.session = None
self.is_disabled = False
def dispatch(self, request, *args, **kwargs):
"""
Standard ``dispatch`` with custom exception handling
for :obj:`.exceptions.TIFException` in which error response is returned
with TIF code as specified in the exception.
"""
try:
return super(SQRLAuthView, self).dispatch(request, *args, **kwargs)
except TIFException as e:
self.tif = self.tif.update(e.tif)
return self.render_to_response()
def get_server_data(self):
"""
Get data to be returned back to SQRL client.
This method does not encode for the response. It simply returns
a dictionary of information which later on can be used by
:obj:`.response.SQRLHttpResponse` to actually construct
data to be sent back to the client.
Returns
-------
OrderedDict
Dict of data to be sent back to SQRL client.
The ``Ordered`` part is important as SQRL requires
to send some data first such as SQRL version number.
"""
if self.nut:
self.nut.renew()
nut = self.nut.nonce
qry = SQRLInitialization(self.request, self.nut).url
else:
nut = self.nut_value
qry = self.request.get_full_path()
data = OrderedDict((
('ver', 1),
('nut', nut),
('tif', self.tif.as_hex_string()),
('qry', qry),
('sfn', getattr(settings, 'SQRL_SERVER_FRIENDLY_NAME',
self.request.get_host().split(':')[0])[:64]),
))
if self.identity:
data['suk'] = self.identity.server_unlock_key
return data
def render_to_response(self):
"""
Render a response which will be send to SQRL client.
Internally this method uses :meth:`.get_server_data` to construct the response
data and :obj:`.response.SQRLHttpResponse` to render that data into
SQRL-compatible format.
Returns
-------
SQRLHttpResponse
Completely rendered response ready to the sent to the SQRL client
"""
return SQRLHttpResponse(self.nut, self.get_server_data())
def do_ips_match(self):
"""
This method updates internal TIF state with :attr:`.exceptions.TIF.IP_MATCH` bit.
The bit is only set when the IP address of the SQRL client making request
to this view matches IP address of device used to initiate SQRL transation
(where SQRL link/QR code were generated).
"""
if get_user_ip(self.request) == self.nut.ip_address:
self.tif = self.tif.update(TIF.IP_MATCH)
def do_ids_match(self):
"""
This method updates internal TIF state with :attr:`.exceptions.TIF.ID_MATCH`
and :attr:`.exceptions.TIF.PREVIOUS_ID_MATCH` bits.
The appropriate bits are only set when the the corresponding identity is found
on the server.
"""
if self.identity:
self.tif = self.tif.update(TIF.ID_MATCH)
if self.previous_identity:
self.tif = self.tif.update(TIF.PREVIOUS_ID_MATCH)
def is_sqrl_disabled(self):
"""
This method updates internal TIF state with :attr:`.exceptions.TIF.SQRL_DISABLED` bit.
The bit is only set when either current or previous identity are found
and they have :attr:`.models.SQRLIdentity.is_enabled` set as ``False``
which means previously SQRL client requested server to disable SQRL
auth method for that user.
Also this method sets ``self.is_disabled`` attribute which later on can be
used by other methods to customize their behaviour.
"""
self.is_disabled = False
if self.identity and not self.identity.is_enabled:
self.tif = self.tif.update(TIF.SQRL_DISABLED)
self.is_disabled = True
if self.previous_identity and not self.previous_identity.is_enabled:
self.tif = self.tif.update(TIF.SQRL_DISABLED)
self.is_disabled = True
def get_nut_or_error(self):
"""
This method finds the :obj:`.models.SQRLNut` by nut nonce in the
querystring or if not not raises appropriate :obj:`.exceptions.TIFException`.
When nut is found, it is saved as ``self.nut``. In addition, this method
triggers :meth:`.do_ips_match` to update internal TIF state.
Returns
-------
SQRLNut
Found :obj:`.models.SQRLNut` via nut nonce from querystring
Raises
------
TIFException
:obj:`.exceptions.TIFException` with :attr:`.exceptions.TIF.TRANSIENT_FAILURE`
and :attr:`.exceptions.TIF.COMMAND_FAILED` bits sets as ``True``
when nut is not found.
"""
self.nut = (SQRLNut.objects
.filter(nonce=self.nut_value,
is_transaction_complete=False)
.first())
if not self.nut:
log.debug('Nut not found')
raise TIFException(TIF.TRANSIENT_FAILURE | TIF.COMMAND_FAILED)
self.do_ips_match()
return self.nut
def post(self, request, *args, **kwargs):
"""
Main view handler since all SQRL requests are ``POST`` requests.
This method does not implement a lot of logic. It mostly relies on other
methods which it then orchestrates. For information on what responsibilities
which method has, you can take a look at :obj:`.SQRLAuthView` description.
Some implementation details worth mentioning:
* This method uses multiple forms to validate different sections of the payload.
Specifically it uses :obj:`.forms.AuthQueryDictForm` to validate the presence
of ``?nut=`` within querystring; and :obj:`.forms.RequestForm` to validate
the SQRL payload itself.
* This method extensively uses :obj:`.exceptions.TIFException` to immediately
return some sort of error to the user which is handled by :meth:`.dispatch`.
The only exception to that is that it still raises ``Http404`` when nut pattern
is not validated. Normally in Django that would of been validated in url patterns
however since SQRL forces to use ``?nut=`` querystring, we mimic same behaviour
404 Not Found in the view.
* To atomically process all SQRL commands (all or nothing), this view
implements all SQRL commands as dedicated methods (e.g. :meth:`.query`).
That allows this method to find all appropriate handlers for all the commands
and if not all are found, :obj:`.exceptions.TIFException` can be raised
with :attr:`.exceptions.TIF.NOT_SUPPORTED` bit set as ``True``.
If all are found, then it simply processes them in the order they were requested.
* Since any SQRL command can potentially fail, none of the SQRL command handlers
save any state in either the session or models because other commands can fail
after them. If all succeed, this method then explicitly finalizes/saves all the
state which includes session and models.
"""
log.debug('-' * 50)
log.debug('Raw request body:\n{}'.format(request.body))
# in case content-type is not given in which case
# request.POST will be empty in which case manually parse
# raw request body
if not request.POST:
request.POST = QueryDict(request.body)
# nut is not part of URL regex so validate it here
# using a form and if not valid, return 404,
# same as if nut would of been validated in URL regex
self.query_form = AuthQueryDictForm(request.GET)
if not self.query_form.is_valid():
log.debug('Query form failed with {}'
''.format(repr(self.query_form.errors)))
raise Http404
log.debug('Request payload:\n{}'
''.format(pformat(request.POST)))
self.nut_value = self.query_form.cleaned_data['nut']
self.nut = self.get_nut_or_error()
# validate the client data
# this also validates submitted signatures and verifies
# that echoed back server response was not altered
self.payload_form = RequestForm(self.nut, request.POST)
if not self.payload_form.is_valid():
log.debug('Request payload validation failed with {}'
''.format(repr(self.payload_form.errors)))
if self.payload_form.tif:
raise TIFException(TIF.COMMAND_FAILED | self.payload_form.tif)
raise TIFException(TIF.COMMAND_FAILED | TIF.CLIENT_FAILURE)
log.debug('Request payload successfully parsed and validated:\n{}'
''.format(pformat(self.payload_form.cleaned_data)))
self.client = self.payload_form.cleaned_data['client']
self.identity = self.payload_form.identity
self.previous_identity = self.payload_form.previous_identity
self.session = self.payload_form.session
self.do_ids_match()
self.is_sqrl_disabled()
cmds = [getattr(self, i, None) for i in self.client['cmd']]
if not all(cmds):
raise TIFException(TIF.COMMAND_FAILED | TIF.NOT_SUPPORTED)
for cmd in cmds:
cmd()
self.finalize()
return self.render_to_response()
def query(self):
"""
Handler for SQRL ``query`` command.
Since all necessary information by default is already returned to the user,
this method does not have to do anything.
"""
def ident(self):
"""
Handler for SQRL ``ident`` command.
"""
if self.is_disabled:
return
self.create_or_update_identity()
# user is already logged in
# so simply associate identity with the user
if all((self.session.get(i) for i in
[SESSION_KEY, BACKEND_SESSION_KEY, HASH_SESSION_KEY])):
self.identity.user_id = self.session.get(SESSION_KEY)
# user is already associated with identity
# so we can login the user
elif self.identity.user_id:
user = self.identity.user
user.backend = SQRL_MODEL_BACKEND
session_auth_hash = user.get_session_auth_hash()
self.session[SESSION_KEY] = user.pk
self.session[BACKEND_SESSION_KEY] = user.backend
self.session[HASH_SESSION_KEY] = session_auth_hash
log.info('Successfully authenticated user "{}" via SQRL'.format(user.username))
# user was not found so lets save identity information in session
# so that we can complete user registration
else:
serialized = serializers.serialize('json', [self.identity])
self.session[SQRL_IDENTITY_SESSION_KEY] = serialized
log.debug('Storing sqrl identity in session "{}" to complete registration:\n{}'
''.format(self.session.session_key,
pformat(json.loads(serialized)[0]['fields'])))
self.nut.is_transaction_complete = True
def disable(self):
"""
Handler for SQRL ``disable`` command.
By the time this handler is called, :obj:`.forms.RequestForm` is already validated
which guarantees that in order to use ``disable``, user must already have associated
:obj:`.models.SQRLIdentity` so this method simply sets :attr:`.models.SQRLIdentity.is_enabled`
to ``False``. Then if the rest of the SQRL commands succeed, :meth:`.finalize` will
save that change.
"""
self.create_or_update_identity()
self.identity.is_enabled = False
self.nut.is_transaction_complete = True
def enable(self):
"""
Handler for SQRL ``enable`` command.
By the time this handler is called, :obj:`.forms.RequestForm` is already validated
which guarantees that in order to use ``enable``, user must already have associated
:obj:`.models.SQRLIdentity` and that the user correctly supplied ``urs`` signature.
Therefore this method simply sets :attr:`.models.SQRLIdentity.is_enabled` to ``True``.
Then if the rest of the SQRL commands succeed, :meth:`.finalize` will save that change.
"""
self.create_or_update_identity()
self.identity.is_enabled = True
self.nut.is_transaction_complete = True
def remove(self):
"""
Handler for SQRL ``remove`` command.
By the time this handler is called, :obj:`.forms.RequestForm` is already validated
which guarantees that in order to use ``remove``, user must already have associated
:obj:`.models.SQRLIdentity`, that the user correctly supplied ``urs`` signature
and that that ``remove`` is the only command.
Since all finalizing of the state should be handled by :meth:`.finalize`, this method
does not actually delete the identity model but marks it for deletion.
"""
self.identity.to_remove = True
self.nut.is_transaction_complete = True
def finalize(self):
"""
State finalization method.
This is necessary since SQRL can request multiple commands at the same time
and any of them can fail. Therefore no state should be saved in any of the
command handlers. They should adjust the state but not actually save it.
Instead this method saves all the state. This allows the SQRL request
processing to be atomic. Current it saves:
* :obj:`.models.SQRLIdentity`
* session data
"""
if self.identity:
if getattr(self.identity, 'to_remove', False):
self.identity.delete()
elif self.identity.user_id:
self.identity.save()
if self.session:
self.session.save()
def create_or_update_identity(self):
"""
This method updates existing :obj:`.models.SQRLIdentity` or creates it
when not already present.
This is used to handle:
* new users creating their :obj:`.models.SQRLIdentity` in which case
all of the data will be set from scratch such as
:attr:`.models.SQRLIdentity.public_key`, etc
* existing users since they could be sending specific SQRL options
(e.g. ``sqrlonly``) which should always update :obj:`.models.SQRLIdentity`
depending on the presence of the options in the request.
"""
if not self.identity:
self.identity = SQRLIdentity()
# by this point form has validated that if the identity is being switched
# all necessary signatures were provided and validated
# so we can safely set the public key which will either
# 1) set new public key for new identity associations
# 2) overwrite the existing public key with the same public key
# 3) overwrite the existing public key which by this point
# is previous identity with new current identity
self.identity.public_key = Base64.encode(self.client['idk'])
self.identity.is_only_sqrl = 'sqrlonly' in self.client['opt']
# form validation will make sure that these are supplied when
# necessary (e.g. creating new identity)
# the reason we don't simply want to always overwrite these is
# because for already associated identities, client will not supply
# them so we dont want to overwrite the model with empty values
if self.client.get('vuk'):
self.identity.verify_unlock_key = Base64.encode(self.client['vuk'])
if self.client.get('suk'):
self.identity.server_unlock_key = Base64.encode(self.client['suk'])
return self.identity
class SQRLCompleteRegistrationView(FormView):
"""
This view is used to complete user registration.
That happens when SQRL transaction is successfully completed
however does not have account yet setup. In that case :obj:`.SQRLAuthView`
stores SQRL identity information in the session which this view can use.
To complete registration, a form is displayed to the user.
When form is successfully filled out, this view creates a new user and
automatically assigns the stored SQRL identity from the session to the
new user.
"""
form_class = PasswordLessUserCreationForm
template_name = 'sqrl/register.html'
success_url = settings.LOGIN_REDIRECT_URL
def check_session_for_sqrl_identity_or_404(self):
"""
Check if the SQRL identity is stored within the session
and if not, raise ``Http404``.
Raises
------
Http404
When SQRL identity is not stored in session
"""
if SQRL_IDENTITY_SESSION_KEY not in self.request.session:
raise Http404
def get(self, request, *args, **kwargs):
"""
Same as regular ``FormView`` except this also checks for identity within session
by using :meth:`.check_session_for_sqrl_identity_or_404`.
"""
self.check_session_for_sqrl_identity_or_404()
return super(SQRLCompleteRegistrationView, self).get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
"""
Same as regular ``FormView`` except this also checks for identity within session
by using :meth:`.check_session_for_sqrl_identity_or_404`.
"""
self.check_session_for_sqrl_identity_or_404()
return super(SQRLCompleteRegistrationView, self).post(request, *args, **kwargs)
def get_success_url(self):
"""
Get success url to which user will be redirected to when registration is complete.
The url from the ``?next=`` is used if :obj:`.forms.NextUrlForm` is valid.
Otherwise :attr:`.success_url` is used.
"""
next_form = NextUrlForm(self.request.GET)
if next_form.is_valid():
return next_form.cleaned_data['next']
return self.success_url
def form_valid(self, form):
"""
When registration form is valid, this method finishes up
creating new user with new SQRL identity.
It does the following:
#. decodes the stored SQRL identity in the session.
If this step fails, this method returns ``500`` response.
#. saves the new user and assigned the decoded identity to it
#. logs in the new user
#. redirects to url returned by :meth:`.get_success_url`
"""
try:
identity = next(iter(serializers.deserialize(
'json', self.request.session.pop(SQRL_IDENTITY_SESSION_KEY)
))).object
except Exception:
return HttpResponse(status=500)
user = form.save()
user.backend = SQRL_MODEL_BACKEND
identity.user = user
identity.save()
login(self.request, user)
log.info('Successfully registered and authenticated user '
'"{}" via SQRL'.format(user.username))
return redirect(self.get_success_url())
class SQRLIdentityManagementView(LoginRequiredMixin, TemplateView):
"""
Simple ``TemplateView`` which renders ``sqrl/manage.html`` template.
The template can (and probably should) be customized in each Django Project.
.. warning::
Since this view is to exclusively manage SQRL identity,
no other auth methods should be added to this template/view.
"""
template_name = 'sqrl/manage.html'
class AdminSiteSQRLIdentityManagementView(LoginRequiredMixin, TemplateView):
template_name = 'admin/auth/user/sqrl_manage.html'
def get_context_data(self, **kwargs):
context = super(AdminSiteSQRLIdentityManagementView, self).get_context_data(**kwargs)
context.update({
'has_permission': True,
})
return context