220 lines
10 KiB
Python
220 lines
10 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
from django.conf import settings
|
||
|
from django.db import models
|
||
|
|
||
|
from .crypto import generate_randomness
|
||
|
from .managers import SQRLNutManager
|
||
|
|
||
|
|
||
|
class SQRLIdentity(models.Model):
|
||
|
"""
|
||
|
SQRL identity associated with a user.
|
||
|
|
||
|
This model stores all necessary for SQRL to complete SQRL transactions
|
||
|
and return any data to the client when necessary.
|
||
|
|
||
|
The reason this is a standalone model vs lets say extending Django User model
|
||
|
is because if added to the user model, each row for the user table is forced
|
||
|
to allocate space to store SQRL information which might not be desired.
|
||
|
By using a dedicated column, it allows to use SQRL only for users who use it.
|
||
|
Also this makes Django-SQRL more modular so that it is easier integrated with
|
||
|
existing projects.
|
||
|
|
||
|
Attributes
|
||
|
----------
|
||
|
public_key : str
|
||
|
Base64 encoded public key of per-site user's SQRL public-private key pair.
|
||
|
This key is used to verify users signature signing SQRL transaction
|
||
|
including server generated random nut using users private key.
|
||
|
server_unlock_key : str
|
||
|
Base64 encoded server unlock key which is a public unlock key sent to client
|
||
|
which client can use to generate urs (unlock request signature) signature which
|
||
|
server can validate using vuk.
|
||
|
More information can be found at https://www.grc.com/sqrl/idlock.htm.
|
||
|
verify_unlock_key : str
|
||
|
Base64 encoded verify unlock key which is a key stored by server which is used
|
||
|
to validate urs (unlock request signature) signatures. This key is not sent to user.
|
||
|
More information can be found at https://www.grc.com/sqrl/idlock.htm.
|
||
|
is_enabled : bool
|
||
|
Boolean indicating whether user can authenticate using SQRL.
|
||
|
in_only_sqrl : bool
|
||
|
Boolean indicating that only SQRL should be allowed to authenticate user.
|
||
|
When enabled via flag in SQRL client requests, this should disable all other
|
||
|
methods of authentication such as username/password.
|
||
|
"""
|
||
|
user = models.OneToOneField(settings.AUTH_USER_MODEL, related_name='sqrl_identity', on_delete=models.CASCADE)
|
||
|
"""Foreign key to Django's auth ``User`` object for whom this SQRL identity belongs to."""
|
||
|
|
||
|
public_key = models.CharField(
|
||
|
max_length=43, db_index=True, unique=True,
|
||
|
help_text='Public key of per-site users public-private key pair. '
|
||
|
'This key is used to verify users signature signing SQRL transaction '
|
||
|
'including server generated random nut using users private key.'
|
||
|
)
|
||
|
server_unlock_key = models.CharField(
|
||
|
max_length=43, blank=True,
|
||
|
help_text='This is public unlock key sent to client which client can use to '
|
||
|
'generate urs (unlock request signature) signature which server can validate '
|
||
|
'using vuk.'
|
||
|
)
|
||
|
verify_unlock_key = models.CharField(
|
||
|
max_length=43, blank=True,
|
||
|
help_text='This is a key stored by server which is used to validate '
|
||
|
'urs (unlock request signature) signatures. This key is not sent to user.'
|
||
|
)
|
||
|
is_enabled = models.BooleanField(
|
||
|
default=True,
|
||
|
help_text='Boolean indicating whether user can authenticate using SQRL.'
|
||
|
)
|
||
|
is_only_sqrl = models.BooleanField(
|
||
|
default=False,
|
||
|
help_text='Boolean indicating that only SQRL should be allowed to authenticate user. '
|
||
|
'When enabled via flag in SQRL client requests, this should disable all other '
|
||
|
'methods of authentication such as username/password.'
|
||
|
)
|
||
|
|
||
|
class Meta(object):
|
||
|
# Redefine db_table so that table name is not funny
|
||
|
# like sqrl_sqrlidentity.
|
||
|
# One way to solve that is to simply name the model
|
||
|
# Identity however that is generic enough which might cause
|
||
|
# name conflicts so its easier to rename the model
|
||
|
# and manually overwrite the table name
|
||
|
db_table = 'sqrl_identity'
|
||
|
verbose_name = 'SQRL Identity'
|
||
|
verbose_name_plural = 'SQRL Identities'
|
||
|
|
||
|
def __str__(self):
|
||
|
if self.user_id:
|
||
|
return '{} for {}'.format(self.__class__.__name__, self.user)
|
||
|
else:
|
||
|
return '{} with idk={}'.format(self.__class__.__name__, self.public_key)
|
||
|
|
||
|
|
||
|
class SQRLNut(models.Model):
|
||
|
"""
|
||
|
Model for storing temporary state for SQRL transactions.
|
||
|
|
||
|
This model by SQRL protocol is not strictly required.
|
||
|
Here is the reasoning for it though:
|
||
|
|
||
|
SQRL protocol requires couple of things:
|
||
|
|
||
|
1. Each SQRL interaction must use random ``nonce`` or in SQRL terminology SQRL nut
|
||
|
2. Strict enforcement that each nut can only be used at most once
|
||
|
3. Each non-initiating SQRL request should return to the client some information
|
||
|
about the initiating request such as whether IP address matches that of
|
||
|
the IP where SQRL transaction was initiated.
|
||
|
|
||
|
``#3`` can easily be solved by encoding all necessary information in the nut value itself.
|
||
|
On subsequent requests, server can simply decode all the information from the nut.
|
||
|
That however will require nut value to grow pretty big. In addition this might also require
|
||
|
encryption so that nut value by itself is not revealing. That in turn requires some
|
||
|
rotating key management structure so that not all nuts are encrypted with same encryption key.
|
||
|
All of the above is possible however is not very elegant, but again, is still possible.
|
||
|
|
||
|
In addition, encrypting state in nut value does not hinder ``#1`` since some random bit
|
||
|
can also be part of the nut so ``#1`` is not an issue.
|
||
|
|
||
|
So far we dont need any state on the server. Each request can have its own nut value.
|
||
|
|
||
|
The problem however is with ``#2`` requirement. The only way for the server to guarantee
|
||
|
that nuts are never reused is to either keep a state of which nuts were used or keep state
|
||
|
of all currently available nuts. Since keeping state of all used nuts will infinitely grow,
|
||
|
probably the latter option is better. The default way of adding state in Django project
|
||
|
would of been to add a model. But at that point, if you are creating a model anyway,
|
||
|
might as well store everything in that model. As a benefit, it makes whole system
|
||
|
simpler by not requiring any fancy approaches to encrypting nuts and maintaining
|
||
|
rotated key encryption schedule of some sort.
|
||
|
|
||
|
Attributes
|
||
|
----------
|
||
|
nonce : str
|
||
|
Base64 encoded value of a random nonce. This nonce is used to identify SQRL transaction.
|
||
|
It is regenerated for each SQRL communication within a single SQRL transaction.
|
||
|
Since this nonce is a one-time token, it allows for the server to prevent replay attacks.
|
||
|
This columns is a primary key of the table.
|
||
|
transaction_nonce : str
|
||
|
Base64 encoded random nonce used to identify a full SQRL transaction from start to finish.
|
||
|
Session key cannot be used since it is persistent across complete user visit which can
|
||
|
include multiple tabs/windows. This transaction id is regenerated for each new tab which
|
||
|
allows the client to identify when a particular SQRL transaction has completed
|
||
|
hence redirect user to more appropriate page.
|
||
|
session_key : str
|
||
|
Session key of the session where SQRL transaction initiated which is used
|
||
|
to associate client session to SQRL transaction. This is important because SQRL
|
||
|
transaction can be completed on a different device which does not have access
|
||
|
to original user session.
|
||
|
is_transaction_complete : bool
|
||
|
Boolean indicating whether SQRL transaction has completed. It is used by the
|
||
|
:obj:`.views.SQRLStatusView` to return redirect URL to the user
|
||
|
when transaction has completed.
|
||
|
ip_address : str
|
||
|
IP address of the user who initiated SQRL transaction. This is where
|
||
|
initially SQRL link/qr code were generated.
|
||
|
timestamp : datetime
|
||
|
Last timestamp when nut was either created or modified. Used for puging purses
|
||
|
to remove expired nuts.
|
||
|
"""
|
||
|
nonce = models.CharField(
|
||
|
max_length=43, unique=True, db_index=True, primary_key=True,
|
||
|
help_text='Single-use random nonce used to identify SQRL transaction. '
|
||
|
'This nonce is regenerated for each SQRL communication within '
|
||
|
'a single SQRL transaction. Since this nonce is a one-time token, '
|
||
|
'it allows for the sviewerver to prevent replay attacks.'
|
||
|
)
|
||
|
transaction_nonce = models.CharField(
|
||
|
max_length=43, unique=True, db_index=True,
|
||
|
help_text='A random nonce used to identify a full SQRL transaction. '
|
||
|
'Session key cannot be used since it is persistent across '
|
||
|
'complete user visit which can include multiple tabs/windows. '
|
||
|
'This transaction id is regenerated for each new tab which '
|
||
|
'allows the client to identify when a particular SQRL transaction '
|
||
|
'has completed hence redirect user to more appropriate page.'
|
||
|
)
|
||
|
|
||
|
session_key = models.CharField(
|
||
|
max_length=32, unique=True, db_index=True,
|
||
|
help_text='User regular session key. This is used to associate client session '
|
||
|
'to a SQRL transaction since transaction can be completed on a different '
|
||
|
'device which does not have access to original user session.'
|
||
|
)
|
||
|
|
||
|
is_transaction_complete = models.BooleanField(
|
||
|
default=False,
|
||
|
help_text='Indicator whether transaction is complete. '
|
||
|
'Can we used by UI to automatically redirect to appropriate page '
|
||
|
'once SQRL transaction is complete.',
|
||
|
)
|
||
|
ip_address = models.GenericIPAddressField(
|
||
|
help_text='Originating IP address of client who initiated SQRL transaction. '
|
||
|
'Used to set appropriate TIF response code.',
|
||
|
)
|
||
|
timestamp = models.DateTimeField(
|
||
|
auto_now=True,
|
||
|
help_text='Last timestamp when nut was either created or modified. '
|
||
|
'Used for purging purposes.',
|
||
|
)
|
||
|
|
||
|
objects = SQRLNutManager()
|
||
|
|
||
|
class Meta(object):
|
||
|
# Explicitly define db_table for clearer table name
|
||
|
db_table = 'sqrl_nut'
|
||
|
verbose_name = 'SQRL Nut'
|
||
|
verbose_name_plural = 'SQRL Nuts'
|
||
|
|
||
|
def __str__(self):
|
||
|
return self.nonce
|
||
|
|
||
|
def renew(self):
|
||
|
"""
|
||
|
Renew instance of the nut.
|
||
|
|
||
|
This is done by deleting the nut from the db since nonce is a primary key.
|
||
|
Then nonce is regenerated and re-saved.
|
||
|
"""
|
||
|
self.delete()
|
||
|
self.nonce = generate_randomness()
|
||
|
self.save()
|