2019-09-02 02:03:23 -05:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
import json
|
|
|
|
import logging
|
|
|
|
from collections import OrderedDict
|
|
|
|
from pprint import pformat
|
2019-08-14 11:52:59 -05:00
|
|
|
|
2019-09-02 02:03:23 -05:00
|
|
|
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
|
2019-09-02 04:56:45 -05:00
|
|
|
from django.urls import reverse
|
2019-09-02 02:03:23 -05:00
|
|
|
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
|
2019-09-02 09:42:24 -05:00
|
|
|
from django.utils.decorators import method_decorator
|
|
|
|
from django.views.decorators.csrf import csrf_exempt
|
2019-09-02 02:03:23 -05:00
|
|
|
|
|
|
|
from .backends import SQRL_MODEL_BACKEND
|
|
|
|
from .exceptions import TIF, TIFException
|
|
|
|
from .forms import (
|
|
|
|
AuthQueryDictForm,
|
|
|
|
ExtractedNextUrlForm,
|
|
|
|
NextUrlForm,
|
|
|
|
PasswordLessUserCreationForm,
|
|
|
|
RequestForm,
|
|
|
|
)
|
|
|
|
from .models import SQRLIdentity, SQRLNut
|
|
|
|
from .response import SQRLHttpResponse
|
|
|
|
from .sqrl import SQRLInitialization
|
2019-09-02 04:56:45 -05:00
|
|
|
from .utils import Base64, get_user_ip
|
2019-09-02 02:03:23 -05:00
|
|
|
|
|
|
|
|
|
|
|
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'
|
|
|
|
|
2019-09-02 09:42:24 -05:00
|
|
|
@method_decorator(csrf_exempt,"dispatch")
|
2019-09-02 02:03:23 -05:00
|
|
|
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
|
|
|
|
|
2019-09-02 09:42:24 -05:00
|
|
|
if all([not self.request.user.is_authenticated,
|
2019-09-02 02:03:23 -05:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
2019-09-02 09:42:24 -05:00
|
|
|
@method_decorator(csrf_exempt,"dispatch")
|
2019-09-02 02:03:23 -05:00
|
|
|
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
|