Initial commit
This commit is contained in:
commit
d6ff82eab2
14 changed files with 688 additions and 0 deletions
9
CHANGELOG
Normal file
9
CHANGELOG
Normal file
|
@ -0,0 +1,9 @@
|
|||
Wed, 14 Aug 2019 11:52:59 -0500
|
||||
Keaton <kii-chan@tutanota.com>
|
||||
Initial commit
|
||||
|
||||
Project is still completely unworkable and unfinished, just starting to initialize things.
|
||||
|
||||
--------------------
|
||||
|
||||
|
9
readme.md
Normal file
9
readme.md
Normal file
|
@ -0,0 +1,9 @@
|
|||
# SQRL for Django
|
||||
|
||||
This is a reboot of [Miki725's django-sqrl](https://github.com/miki725/django-sqrl],
|
||||
updated for Python 3.7 and Django 2.2. The vast majority of the code is unchanged,
|
||||
so all credit for this working belongs with them.
|
||||
|
||||
## Installation (eventually)
|
||||
|
||||
It still isn't complete. This is placeholder.
|
1
requirements.txt
Normal file
1
requirements.txt
Normal file
|
@ -0,0 +1 @@
|
|||
ed25519
|
0
sqrl/__init__.py
Normal file
0
sqrl/__init__.py
Normal file
3
sqrl/admin.py
Normal file
3
sqrl/admin.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
5
sqrl/apps.py
Normal file
5
sqrl/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class SqrlConfig(AppConfig):
|
||||
name = 'sqrl'
|
134
sqrl/crypto.py
Normal file
134
sqrl/crypto.py
Normal file
|
@ -0,0 +1,134 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from collections import OrderedDict
|
||||
from os import urandom
|
||||
|
||||
import ed25519
|
||||
from django.utils.crypto import constant_time_compare, salted_hmac
|
||||
|
||||
from .utils import Base64, Encoder
|
||||
|
||||
|
||||
|
||||
class HMAC(object):
|
||||
"""
|
||||
Utility class for generating and verifying HMAC signatures.
|
||||
|
||||
This class relies on Django's built in :func:`salted_hmac`
|
||||
to compute actual HMAC values by using ``SECRET_KEY`` as key.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
nut : SQRLNut
|
||||
Nut from which necessary data is extracted to add a salt value
|
||||
to the HMAC input data.
|
||||
Currently only :attr:`.models.SQRLNut.session_key` is used.
|
||||
data : OrderedDict
|
||||
Dict for which to either compute or validate HMAC signature.
|
||||
"""
|
||||
|
||||
def __init__(self, nut, data):
|
||||
self.nut = nut
|
||||
self.data = data
|
||||
|
||||
def sign_data(self):
|
||||
"""
|
||||
Generate HMAC signature for the provided data.
|
||||
|
||||
Note
|
||||
----
|
||||
``max`` key is ignored in the input data if that key is present.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bytes
|
||||
Binary signature of the data
|
||||
"""
|
||||
assert isinstance(self.data, OrderedDict)
|
||||
|
||||
encoded = Encoder.base64_dumps(OrderedDict(
|
||||
(k, v) for k, v in self.data.items()
|
||||
if k != 'mac'
|
||||
))
|
||||
signature = salted_hmac(self.nut.session_key, encoded).digest()
|
||||
|
||||
return signature
|
||||
|
||||
def is_signature_valid(self, other_signature):
|
||||
"""
|
||||
Check if the ``other_signature`` is a valid signature for the
|
||||
provided data and the nut.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
Boolean indicating whether validation has succeeded.
|
||||
"""
|
||||
expected_signature = self.sign_data()
|
||||
return constant_time_compare(expected_signature, other_signature)
|
||||
|
||||
|
||||
class Ed25519(object):
|
||||
"""
|
||||
Utility class for signing and verifying ed25519 signatures.
|
||||
|
||||
More information about ed25519 can be found at `<http://ed25519.cr.yp.to/>`_.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
public_key : bytes
|
||||
Key used for verifying signature.
|
||||
private_key : bytes
|
||||
Key used for signing data.
|
||||
msg : bytes
|
||||
Binary data for which to generate the signature.
|
||||
"""
|
||||
|
||||
def __init__(self, public_key, private_key, msg):
|
||||
self.public_key = public_key
|
||||
self.private_key = private_key
|
||||
self.msg = msg
|
||||
|
||||
def is_signature_valid(self, other_signature):
|
||||
"""
|
||||
Check if ``other_signature`` is a valid signature for the provided message.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
Boolean indicating whether validation has succeeded.
|
||||
"""
|
||||
try:
|
||||
vk = ed25519.VerifyingKey(self.public_key)
|
||||
vk.verify(other_signature, self.msg)
|
||||
return True
|
||||
except (AssertionError, ed25519.BadSignatureError):
|
||||
return False
|
||||
|
||||
def sign_data(self):
|
||||
"""
|
||||
Generate ed25519 signature for the provided data.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bytes
|
||||
ed25519 signature
|
||||
"""
|
||||
sk = ed25519.SigningKey(self.private_key)
|
||||
return sk.sign(self.msg)
|
||||
|
||||
|
||||
def generate_randomness(size=32):
|
||||
"""
|
||||
Generate random sample of specified size ``size``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
size : int, optional
|
||||
Number of bytes to generate random sample
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
:meth:`.Base64.encode` encoded random sample
|
||||
"""
|
||||
return Base64.encode(urandom(size))
|
24
sqrl/managers.py
Normal file
24
sqrl/managers.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from django.db import models
|
||||
|
||||
|
||||
class SQRLNutManager(models.Manager):
|
||||
"""
|
||||
Customer :obj:`.models.SQRLNut` model manager.
|
||||
"""
|
||||
|
||||
def replace_or_create(self, session_key, **kwargs):
|
||||
"""
|
||||
This method creates new :obj:`.models.SQRLNut` with given parameters.
|
||||
|
||||
If nut already exists, it removes it before creating new nut.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
session_key : str
|
||||
Key of the session. All nuts with matching session will be removed.
|
||||
**kwargs
|
||||
Kwargs which will be used to create new :obj:`.models.SQRLNut`
|
||||
"""
|
||||
self.get_queryset().filter(session_key=session_key).delete()
|
||||
return self.create(session_key=session_key, **kwargs)
|
219
sqrl/models.py
Normal file
219
sqrl/models.py
Normal file
|
@ -0,0 +1,219 @@
|
|||
# -*- 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()
|
147
sqrl/sqrl.py
Normal file
147
sqrl/sqrl.py
Normal file
|
@ -0,0 +1,147 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import QueryDict
|
||||
|
||||
from .crypto import generate_randomness
|
||||
from .models import SQRLNut
|
||||
from .utils import get_user_ip
|
||||
|
||||
|
||||
class SQRLInitialization(object):
|
||||
"""
|
||||
SQRL class for initializing SQRL transaction.
|
||||
|
||||
This class is mainly responsible for initially creating and storing
|
||||
:obj:`.models.SQRLNut`. Also this class has helper properties
|
||||
for getting SQRL urls.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
request : HttpRequest
|
||||
Django standard request object
|
||||
nut : SQRLNut, optional
|
||||
SQRLNut for which to do SQRL initialization
|
||||
"""
|
||||
|
||||
def __init__(self, request, nut=None):
|
||||
self.request = request
|
||||
if nut is not None:
|
||||
self.nut = nut
|
||||
|
||||
def get_or_create_session_key(self):
|
||||
"""
|
||||
Get or create the session key from the request object.
|
||||
|
||||
When not present yet, this initializes the session for the user.
|
||||
As a result, the request then returns session cookie to the user
|
||||
via session middleware.
|
||||
"""
|
||||
session_key = self.request.session.session_key
|
||||
|
||||
if session_key is None:
|
||||
self.request.session.create()
|
||||
session_key = self.request.session.session_key
|
||||
|
||||
return session_key
|
||||
|
||||
@property
|
||||
def nut(self):
|
||||
"""
|
||||
Cached property for getting :obj:`.models.SQRLNut`.
|
||||
|
||||
When accessed for the first time, this property either replaces or creates
|
||||
new :obj:`.models.SQRLNut` by using :meth:`.managers.SQRLNutManager.replace_or_create`.
|
||||
All the data for the creation of the nut is created by using :meth:`.generate_nut_kwargs`.
|
||||
"""
|
||||
if hasattr(self, '_nut'):
|
||||
return self._nut
|
||||
|
||||
self._nut = SQRLNut.objects.replace_or_create(
|
||||
**self.generate_nut_kwargs()
|
||||
)
|
||||
|
||||
return self._nut
|
||||
|
||||
@nut.setter
|
||||
def nut(self, value):
|
||||
self._nut = value
|
||||
|
||||
def generate_nut_kwargs(self):
|
||||
"""
|
||||
Generate kwargs which can be used to create new :obj:`.models.SQRLNut`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
All required kwargs to instantiate and create :obj:`.models.SQRLNut`.
|
||||
"""
|
||||
randomness = generate_randomness(64)
|
||||
l = len(randomness) // 2
|
||||
|
||||
return {
|
||||
'session_key': self.get_or_create_session_key(),
|
||||
'nonce': randomness[:l],
|
||||
'transaction_nonce': randomness[l:],
|
||||
'is_transaction_complete': False,
|
||||
'ip_address': get_user_ip(self.request),
|
||||
}
|
||||
|
||||
def get_sqrl_url(self):
|
||||
"""
|
||||
Get the server URL of where SQRL client will make first request.
|
||||
|
||||
This method should be customized when a custom namespace should be used
|
||||
by the SQRL client when generating on the fly per-site public-private keypair.
|
||||
For example this can be used when a web site is a SAAS in which different
|
||||
"sub-sites" are determined tenant within a URL path - ``mysaas.com/<tenant>``.
|
||||
In that case the returned SQRL auth url should be something like -
|
||||
``mysaas.com/mytenant:sqrl/auth/?nut=<nut value>``.
|
||||
By using ``:`` within the path will let SQRL client know that up until
|
||||
that point full domain name should be used to generate public-private keypair.
|
||||
"""
|
||||
return reverse('sqrl:auth')
|
||||
|
||||
def get_sqrl_url_params(self):
|
||||
"""
|
||||
Get SQRL url params to be added as querystring params in the SQRL url.
|
||||
|
||||
By default this only adds ``nut=<nut>``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
URLEncoded querystring params
|
||||
"""
|
||||
qd = QueryDict('', mutable=True)
|
||||
qd.update({
|
||||
'nut': self.nut.nonce,
|
||||
})
|
||||
return qd.urlencode()
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
"""
|
||||
Property for getting only server-side SQRL auth view URL.
|
||||
|
||||
This does not include the full domain within the URL.
|
||||
The URL is always relative to the current domain of the site.
|
||||
"""
|
||||
return (
|
||||
'{url}?{params}'
|
||||
''.format(url=self.get_sqrl_url(),
|
||||
params=self.get_sqrl_url_params())
|
||||
)
|
||||
|
||||
@property
|
||||
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}'
|
||||
''.format(scheme='sqrl' if self.request.is_secure() else 'qrl',
|
||||
host=self.request.get_host(),
|
||||
url=self.url)
|
||||
)
|
3
sqrl/tests.py
Normal file
3
sqrl/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
0
sqrl/urls.py
Normal file
0
sqrl/urls.py
Normal file
131
sqrl/utils.py
Normal file
131
sqrl/utils.py
Normal file
|
@ -0,0 +1,131 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from base64 import urlsafe_b64decode, urlsafe_b64encode
|
||||
from collections import OrderedDict
|
||||
|
||||
import six
|
||||
|
||||
|
||||
class Base64(object):
|
||||
"""
|
||||
Helper class for base64 encoding/decoding
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
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))
|
||||
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)
|
||||
return urlsafe_b64decode((s + '=' * (4 - len(s) % 4)).encode('ascii'))
|
||||
|
||||
|
||||
class Encoder(object):
|
||||
"""
|
||||
Helper class for encoding/decoding SQRL response data.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
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
|
||||
Base64 encoded binary data of input ``data``
|
||||
"""
|
||||
if data and isinstance(data, dict):
|
||||
return Base64.encode(cls.dumps(data).encode('ascii'))
|
||||
return cls.dumps(data)
|
||||
|
||||
@classmethod
|
||||
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
|
||||
:other: no operation
|
||||
"""
|
||||
data = cls.normalize(data)
|
||||
|
||||
if isinstance(data, dict):
|
||||
return '\r\n'.join(
|
||||
'{}={}'.format(k, cls.dumps(v))
|
||||
for k, v in data.items()
|
||||
) + '\r\n'
|
||||
elif isinstance(data, (list, tuple)):
|
||||
return '~'.join(cls.dumps(i) for i in data)
|
||||
else:
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
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
|
||||
:``binary``: Base64 encode data
|
||||
:``str``: no operation
|
||||
:other: data is casted to string using ``__str__`` (or ``__unicode__``)
|
||||
"""
|
||||
if isinstance(data, dict):
|
||||
if data:
|
||||
return OrderedDict((
|
||||
(k, cls.normalize(v))
|
||||
for k, v in data.items()
|
||||
))
|
||||
else:
|
||||
return ''
|
||||
elif isinstance(data, (list, tuple)):
|
||||
return [cls.dumps(i) for i in data]
|
||||
elif isinstance(data, six.binary_type):
|
||||
return Base64.encode(data)
|
||||
elif isinstance(data, six.text_type):
|
||||
return data
|
||||
else:
|
||||
return six.text_type(data)
|
||||
|
||||
def get_user_ip(request):
|
||||
"""
|
||||
Utility function for getting user's IP from request address.
|
||||
|
||||
This either returns the IP address from the ``request.REMOTE_ADDR``
|
||||
or ``request.META'HTTP_X_REAL_IP']`` when request might of
|
||||
been reverse proxied.
|
||||
"""
|
||||
return (
|
||||
request.META.get('HTTP_X_REAL_IP')
|
||||
or request.META['REMOTE_ADDR']
|
||||
)
|
3
sqrl/views.py
Normal file
3
sqrl/views.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
Loading…
Reference in a new issue