===
ska
===
Lets you easily sign data, using symmetric-key algorithm encryption. Allows
you to validate signed data and identify possible validation errors. Uses
sha-(1, 224, 256, 385 and 512)/hmac for signature encryption. Allows to use
custom hash algorithms. Comes with shortcut functions for signing (and
validating) dictionaries and URLs.
.. image:: https://img.shields.io/pypi/v/ska.svg
:target: https://pypi.python.org/pypi/ska
:alt: PyPI Version
.. image:: https://img.shields.io/pypi/pyversions/ska.svg
:target: https://pypi.python.org/pypi/ska/
:alt: Supported Python versions
.. image:: https://github.com/barseghyanartur/ska/workflows/test/badge.svg
:target: https://github.com/barseghyanartur/ska/actions
:alt: Build Status
.. image:: https://readthedocs.org/projects/ska/badge/?version=latest
:target: http://ska.readthedocs.io/en/latest/?badge=latest
:alt: Documentation Status
.. image:: https://img.shields.io/badge/license-GPL--2.0--only%20OR%20LGPL--2.1--or--later-blue.svg
:target: https://github.com/barseghyanartur/ska/#License
:alt: GPL-2.0-only OR LGPL-2.1-or-later
.. image:: https://coveralls.io/repos/github/barseghyanartur/ska/badge.svg?branch=main&service=github
:target: https://coveralls.io/github/barseghyanartur/ska?branch=main
:alt: Coverage
Key concepts
============
Hosts, that communicate with each other, share the Secret Key, which is used
to sign data (requests). Secret key is never sent around.
One of the cases is signing of HTTP requests. Each (HTTP) request is signed
on the sender side using the shared Secret Key and as an outcome produces the
triple (``signature``, ``auth_user``, ``valid_until``) which are used to sign
the requests.
- ``signature`` (``str``): Signature generated.
- ``auth_user`` (``str``): User making the request. Can be anything.
- ``valid_until`` (``float`` or ``str``): Signature expiration time (Unix timestamp).
On the recipient side, (HTTP request) data is validated using the shared
Secret Key. It's being checked whether signature is valid and not expired.
.. code-block:: text
┌─────────────┐ Data ┌─────────────┐
│ Host 1 ├────────────────────────────>│ Host 2 │
│ ─────────── │ │ ─────────── │
│ secret key │ │ secret key │
│ 'my-secret' │<────────────────────────────┤ 'my-secret' │
└─────────────┘ Data └─────────────┘
Features
========
Core `ska` module
-----------------
- Sign dictionaries.
- Validate signed dictionaries.
- Sign URLs. Append and sign additional URL data.
- Validate URLs.
- Use one of the built-in algorythms (HMAC SHA-1, HMAC SHA-224, HMAC SHA-256,
HMAC SHA-384 or HMAC SHA-512) or define a custom one.
Django `ska` module (`ska.contrib.django.ska`)
----------------------------------------------
- Model decorators for signing absolute URLs. View (including class-based
views) decorators for protecting views to authorised parties only (no
authentication required).
- Authentication backend for Django based on the signatures (tokens) generated
using `ska`, which allows you to get a password-less login to Django web
site. Multiple Secret Keys (per provider) supported. Comes with handy
callbacks (possible to customise per provider) for various states of
authentication.
- Template tags for signing URLs from within templates.
- `django-constance` integration (for password-less authentication).
- `Django REST Framework integration`_ (for protecting ViewSets, obtaining
JWT tokens for authentication).
Prerequisites
=============
Present
-------
- Core ``ska`` module requires Python 3.8, 3.9, 3.10 and 3.11.
- Django ``ska`` module (``ska.contrib.django.ska``) requires the mentioned
above plus Django 3.2, 4.1 or 4.2. Additionally, certain
versions of `django-constance` and `djangorestframework` are required.
Specific version requirement primarily depends on the used Django version.
Check the `example requirements
<https://github.com/barseghyanartur/ska/tree/main/examples/requirements>`_
to find out which versions of `django-constance` and `djangorestframework`
have been tested with specific Django versions.
Past
----
.. note::
In future releases (any time) compatibility with no-longer-supported
versions might/will be wiped out.
- Dropping support of Python 3.6 and 3.7 has been announced in version 1.10.
As of 1.9.1 everything still worked.
- Dropping support of Python 2.7 and 3.5 has been announced in version 1.8.
As of 1.7.5 everything still worked.
- Dropping support of Python 3.4 has been announced in version 1.6.8. As of
1.6.8 everything still worked.
- Dropping support of Django 2.2, 3.0, 3.1 and 4.0 has been announced in
version 1.10. As of 1.9.1 everything is still backwards compatible with
mentioned versions.
- Dropping support of Django 1.5, 1.6 and 1.7 has been announced in version
1.6. As of 1.6 everything is still backwards compatible with mentioned
versions.
- Dropping support of Python 2.6 and 3.3 has been announced in version 1.6.
As of 1.6 everything is still backwards compatible (as much as it's possible
within this package) with mentioned versions.
Eco-system
==========
Need ``ska`` for other languages? Check the following affiliated projects:
- `skajs <https://github.com/barseghyanartur/skajs>`_: ``ska`` implementation
for NodeJS (both CommonJS and ESM are supported, Node >= 14).
- `skaphp <https://github.com/barseghyanartur/skaphp>`_: ``ska`` implementation
for PHP (>= 7.2).
Generated signatures are inter-compatible between Python, NodeJS and PHP
implementations.
Installation
============
Latest stable version from PyPI:
.. code-block:: sh
pip install ska
or latest development version from GitHub.
.. code-block:: sh
pip install https://github.com/barseghyanartur/ska/archive/main.tar.gz
Usage examples
==============
For integration with Django, see the `Django integration`_ section.
Basic usage
-----------
Pure Python usage.
Sender side
~~~~~~~~~~~
Signing URLs is as simple as follows.
Required imports.
.. code-block:: python
from ska import sign_url
Producing a signed URL.
.. code-block:: python
signed_url = sign_url(
auth_user='user',
secret_key='your-secret_key',
url='http://e.com/api/'
)
.. code-block:: text
GET http://e.com/api/?valid_until=1378045287.0&auth_user=user&signature=YlZpLFsjUKBalL4x5trhkeEgqE8%3D
Default lifetime of a signature is 10 minutes (600 seconds). If you want it
to be different, provide a ``lifetime`` argument to ``sign_url`` function.
Default name of the (GET) param holding the generated signature value
is ``signature``. If you want it to be different, provide a ``signature_param``
argument to ``sign_url`` function.
Default name of the (GET) param holding the ``auth_user`` value is
``auth_user``. If you want it to be different, provide a ``auth_user_param``
argument to ``sign_url`` function.
Default name of the (GET) param holding the ``valid_until`` value is
`valid_until`. If you want it to be different, provide a ``valid_until_param``
argument to ``sign_url`` function.
Note, that by default a suffix '?' is added after the given ``url`` and
generated signature params. If you want that suffix to be custom, provide a
``suffix`` argument to the ``sign_url`` function. If you want it to be gone,
set its' value to empty string.
With all customisations, it would look as follows:
.. code-block:: python
from ska import HMACSHA512Signature # Use HMAC SHA-512 algorithm
signed_url = sign_url(
auth_user='user',
secret_key='your-secret_key',
lifetime=120,
url='http://e.com/api/',
signature_param='signature',
auth_user_param='auth_user',
valid_until_param='valid_until',
signature_cls=HMACSHA512Signature
)
It's also possible to add additional data to the signature by providing a
``extra`` argument (dict). Note, that additional data is signed as well.
If request is somehow tampered (values vary from originally provided ones),
signature becomes invalid.
.. code-block:: python
sign_url(
auth_user='user',
secret_key='your-secret_key',
url='http://e.com/api/',
extra={
'email': 'doe@example.com',
'last_name': 'Doe',
'first_name': 'Joe'
}
)
You may now proceed with the signed URL request. If you use the famous
``requests`` library, it would be as follows.
.. code-block:: python
import requests
requests.get(signed_url)
If you want to use POST method instead, you would likely want to get a
dictionary back, in order to append it to the POST data later.
Required imports.
.. code-block:: python
from ska import signature_to_dict
Producing a dictionary containing the signature data, ready to be put into
the request (for example POST) data. All customisations mentioned above for
the ``sign_url`` function, also apply to the ``signature_to_dict``:
.. code-block:: python
signature_dict = signature_to_dict(
auth_user='user',
secret_key='your-secret_key'
)
.. code-block:: text
{
'signature': 'YlZpLFsjUKBalL4x5trhkeEgqE8=',
'auth_user': 'user',
'valid_until': '1378045287.0'
}
Adding of additional data to the signature works in the same way:
.. code-block:: python
signature_dict = signature_to_dict(
auth_user='user',
secret_key='your-secret_key',
extra={
'email': 'john.doe@mail.example.com',
'first_name': 'John',
'last_name': 'Doe'
}
)
.. code-block:: text
{
'auth_user': 'user',
'email': 'john.doe@mail.example.com',
'extra': 'email,first_name,last_name',
'first_name': 'John',
'last_name': 'Doe',
'signature': 'cnSoU/LnJ/ZhfLtDLzab3a3gkug=',
'valid_until': 1387616469.0
}
If you for some reason prefer a lower level implementation, read the same
section in the `Advanced usage (low-level)`_ chapter.
Recipient side
~~~~~~~~~~~~~~
Validating the signed request data is as simple as follows.
Required imports.
.. code-block:: python
from ska import validate_signed_request_data
Validating the signed request data. Note, that ``data`` value is expected to
be a dictionary; ``request.GET`` is given as an example. It will most likely
vary from what's used in your framework (unless you use Django).
.. code-block:: python
validation_result = validate_signed_request_data(
data=request.GET, # Note, that ``request.GET`` is given as example.
secret_key='your-secret_key'
)
The ``validate_signed_request_data`` produces a
``ska.SignatureValidationResult`` object, which holds the following data.
- ``result`` (``bool``): True if data is valid. False otherwise.
- ``reason`` (``list``): List of strings, indicating validation errors. Empty list
in case if ``result`` is True.
Default name of the (GET) param holding the signature value is ``signature``.
If you want it to be different, provide a ``signature_param`` argument to
``validate_signed_request_data`` function.
Default name of the (GET) param holding the ``auth_user`` value is
``auth_user``. If you want it to be different, provide a ``auth_user_param``
argument to ``validate_signed_request_data`` function.
Default name of the (GET) param holding the ``valid_until`` value is
``valid_until``. If you want it to be different, provide a
``valid_until_param`` argument to ``validate_signed_request_data`` function.
With all customisations, it would look as follows. Note, that
``request.GET`` is given as example.
.. code-block:: python
from ska import HMACSHA256Signature # Use HMAC SHA-256 algorithm
validation_result = validate_signed_request_data(
data=request.GET,
secret_key='your-secret_key',
signature_param='signature',
auth_user_param='auth_user',
valid_until_param='valid_until',
signature_cls=HMACSHA256Signature
)
If you for some reason prefer a lower level implementation, read the same
section in the `Advanced usage (low-level)`_ chapter.
Command line usage
------------------
It's possible to generate a signed URL from command line using the
``ska.generate_signed_url`` module.
:Arguments:
.. code-block:: text
-h, --help show this help message and exit
-au AUTH_USER, --auth-user AUTH_USER
`auth_user` value
-sk SECRET_KEY, --secret-key SECRET_KEY
`secret_key` value
-vu VALID_UNTIL, --valid-until VALID_UNTIL
`valid_until` value
-l LIFETIME, --lifetime LIFETIME
`lifetime` value
-u URL, --url URL URL to sign
-sp SIGNATURE_PARAM, --signature-param SIGNATURE_PARAM
(GET) param holding the `signature` value
-aup AUTH_USER_PARAM, --auth-user-param AUTH_USER_PARAM
(GET) param holding the `auth_user` value
-vup VALID_UNTIL_PARAM, --valid-until-param VALID_UNTIL_PARAM
(GET) param holding the `auth_user` value
:Example:
.. code-block:: sh
ska-sign-url -au user -sk your-secret-key --url http://example.com
Advanced usage (low-level)
--------------------------
Sender side
~~~~~~~~~~~
Required imports.
.. code-block:: python
from ska import Signature, RequestHelper
Generate a signature.
.. code-block:: python
signature = Signature.generate_signature(
auth_user='user',
secret_key='your-secret-key'
)
Default lifetime of a signature is 10 minutes (600 seconds). If you want it to
be different, provide a ``lifetime`` argument to ``generate_signature``
method.
.. code-block:: python
signature = Signature.generate_signature(
auth_user='user',
secret_key='your-secret-key',
lifetime=120 # Signature lifetime set to 120 seconds.
)
Adding of additional data to the signature works in the same way as in
``sign_url``.
.. code-block:: python
signature = Signature.generate_signature(
auth_user='user',
secret_key='your-secret-key',
extra={
'email': 'doe@example.com',
'last_name': 'Doe',
'first_name': 'Joe'
}
)
For HMAC SHA-384 algorithm it would look as follows.
.. code-block:: python
from ska import HMACSHA384Signature
signature = HMACSHA384Signature.generate_signature(
auth_user='user',
secret_key='your-secret-key'
)
Your endpoint operates with certain param names and you need to wrap generated
signature params into the URL. In order to have the job done in an easy way,
create a request helper. Feed names of the (GET) params to the request helper
and let it make a signed endpoint URL for you.
.. code-block:: python
request_helper = RequestHelper(
signature_param='signature',
auth_user_param='auth_user',
valid_until_param='valid_until'
)
Append signature params to the endpoint URL.
.. code-block:: python
signed_url = request_helper.signature_to_url(
signature=signature,
endpoint_url='http://e.com/api/'
)
.. code-block:: text
GET http://e.com/api/?valid_until=1378045287.0&auth_user=user&signature=YlZpLFsjUKBalL4x5trhkeEgqE8%3D
Make a request.
.. code-block:: python
import requests
r = requests.get(signed_url)
For HMAC SHA-384 algorithm it would look as follows.
.. code-block:: python
from ska import HMACSHA384Signature
request_helper = RequestHelper(
signature_param='signature',
auth_user_param='auth_user',
valid_until_param='valid_until',
signature_cls=HMACSHA384Signature
)
signed_url = request_helper.signature_to_url(
signature=signature,
endpoint_url='http://e.com/api/'
)
Recipient side
~~~~~~~~~~~~~~
Required imports.
.. code-block:: python
from ska import RequestHelper
Create a request helper. Your endpoint operates with certain param names. In
order to have the job done in an easy way, we feed those params to the
request helper and let it extract data from signed request for us.
.. code-block:: python
request_helper = RequestHelper(
signature_param='signature',
auth_user_param='auth_user',
valid_until_param='valid_until'
)
Validate the request data. Note, that ``request.GET`` is given just as an
example.
.. code-block:: python
validation_result = request_helper.validate_request_data(
data=request.GET,
secret_key='your-secret-key'
)
Your implementation further depends on you, but may look as follows.
.. code-block:: python
if validation_result.result:
# Validated, proceed further
# ...
else:
# Validation not passed.
raise Http404(validation_result.reason)
You can also just validate the signature by calling ``validate_signature``
method of the ``ska.Signature``.
.. code-block:: python
Signature.validate_signature(
signature='EBS6ipiqRLa6TY5vxIvZU30FpnM=',
auth_user='user',
secret_key='your-secret-key',
valid_until='1377997396.0'
)
Django integration
------------------
``ska`` comes with Django model- and view-decorators for producing signed URLs
and and validating the endpoints, as well as with authentication backend,
which allows password-less login into Django web site using ``ska`` generated
signature tokens. There's also a template tag for signing URLs.
Demo
~~~~
In order to be able to quickly evaluate the ``ska``, a demo app (with a quick
installer) has been created (works on Ubuntu/Debian, may work on other Linux
systems as well, although not guaranteed). Follow the instructions below for
having the demo running within a minute.
Grab the latest ``ska_example_app_installer.sh`` and execute it:
.. code-block:: sh
wget -O - https://raw.github.com/barseghyanartur/ska/stable/examples/ska_example_app_installer.sh | bash
Open your browser and test the app.
Foo listing (ska protected views):
- URL: http://127.0.0.1:8001/foo/
Authentication page (ska authentication backend):
- URL: http://127.0.0.1:8001/foo/authenticate/
Django admin interface:
- URL: http://127.0.0.1:8001/admin/
- Admin username: test_admin
- Admin password: test
Configuration
~~~~~~~~~~~~~
Secret key (``str``) must be defined in ``settings`` module of your project.
.. code-block:: python
SKA_SECRET_KEY = 'my-secret-key'
The following variables can be overridden in ``settings`` module of your
project.
- ``SKA_UNAUTHORISED_REQUEST_ERROR_MESSAGE`` (``str``): Plain text error message.
Defaults to "Unauthorised request. {0}".
- ``SKA_UNAUTHORISED_REQUEST_ERROR_TEMPLATE`` (``str``): Path to 401 template that
should be rendered in case of 401
responses. Defaults to empty string (not provided).
- ``SKA_AUTH_USER`` (``str``): The ``auth_user`` argument for ``ska.sign_url``
function. Defaults to "ska-auth-user".
See the working `example project
<https://github.com/barseghyanartur/ska/tree/stable/example>`_.
Multiple secret keys
~~~~~~~~~~~~~~~~~~~~
Imagine, you have a site to which you want to offer a password-less login for
various clients/senders and you don't want them all to have one shared secret
key, but rather have their own one. Moreover, you specifically want to execute
very custom callbacks not only for each separate client/sender, but also for
different sort of users authenticating.
.. code-block:: text
┌────────────────┐
│ Site providing │
│ authentication │
│ ────────────── │
│ custom secret │
│ keys per │
│ client │
│ ────────────── │
│ Site 1: 'sk-1' │
┌───────────>│ Site 2: 'sk-2' │<───────────┐
│ │ Site 3: 'sk-3' │ │
│ ┌────>│ Site 4: 'sk-4' │<────┐ │
│ │ └────────────────┘ │ │
│ │ │ │
│ │ │ │
┌────────────┴─┐ ┌─┴────────────┐ ┌────────────┴─┐ ┌─┴────────────┐
│ Site 1 │ │ Site 2 │ │ Site 3 │ │ Site 4 │
│ ──────────── │ │ ──────────── │ │ ──────────── │ │ ──────────── │
│ secret key │ │ secret key │ │ secret key │ │ secret key │
│ 'sk-1' │ │ 'sk-2' │ │ 'sk-3' │ │ 'sk-4' │
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
In order to make the stated above possible, the concept of providers is
introduced. You can define a secret key, callbacks or redirect URL. See an
example below. Note, that keys of the ``SKA_PROVIDERS`` ("client_1",
"client_2", etc.) are the provider keys.
.. code-block:: python
SKA_PROVIDERS = {
# ********************************************************
# ******************** Basic gradation *******************
# ********************************************************
# Site 1
'client_1': {
'SECRET_KEY': 'sk-1',
},
# Site 2
'client_2': {
'SECRET_KEY': 'sk-2',
},
# Site 3
'client_3': {
'SECRET_KEY': 'sk-3',
},
# Site 4
'client_4': {
'SECRET_KEY': 'sk-4',
},
# ********************************************************
# ******* You make gradation as complex as you wish ******
# ********************************************************
# Client 1, group users
'client_1.users': {
'SECRET_KEY': 'client-1-users-secret-key',
},
# Client 1, group power_users
'client_1.power_users': {
'SECRET_KEY': 'client-1-power-users-secret-key',
'USER_CREATE_CALLBACK': 'foo.ska_callbacks.client1_power_users_create',
},
# Client 1, group admins
'client_1.admins': {
'SECRET_KEY': 'client-1-admins-secret-key',
'USER_CREATE_CALLBACK': 'foo.ska_callbacks.client1_admins_create',
'REDIRECT_AFTER_LOGIN': '/admin/'
},
}
See the `Callbacks`_ section for the list of callbacks. Note, that callbacks
defined in the ``SKA_PROVIDERS`` are overrides. If a certain callback isn't
defined in the ``SKA_PROVIDERS``, authentication backend falls back to the
respective default callback function.
Obviously, server would have to have the full list of providers defined. On
the client side you would only have to store the general secret key and of
course the provider UID(s).
When making a signed URL on the sender side, you should be providing the
``provider`` key in the ``extra`` argument. See the example below for how you
would do it for ``client_1.power_users``.
.. code-block:: python
from ska import sign_url
from ska.defaults import DEFAULT_PROVIDER_PARAM
server_ska_login_url = 'https://server-url.com/ska/login/'
signed_remote_ska_login_url = sign_url(
auth_user='test_ska_user',
# Using provider-specific secret key. This value shall be equal to
# the value of SKA_PROVIDERS['client_1.power_users']['SECRET_KEY'],
# defined in your projects' Django settings module.
secret_key='client-1-power-users-secret-key',
url=server_ska_login_url,
extra={
'email': 'test_ska_user@mail.example.com',
'first_name': 'John',
'last_name': 'Doe',
# Using provider specific string. This value shall be equal to
# the key string "client_1.power_users" of SKA_PROVIDERS,
# defined in your projcts' Django settings module.
DEFAULT_PROVIDER_PARAM: 'client_1.power_users',
}
)
Django model method decorator ``sign_url``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This is most likely be used in module ``models`` (models.py).
Imagine, you have a some objects listing and you want to protect the URLs to
be viewed by authorised parties only. You would then use
``get_signed_absolute_url`` method when rendering the listing (HTML).
.. code-block:: python
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.core.urlresolvers import reverse
from ska.contrib.django.ska.decorators import sign_url
class FooItem(models.Model):
title = models.CharField(_("Title"), max_length=100)
slug = models.SlugField(unique=True, verbose_name=_("Slug"))
body = models.TextField(_("Body"))
# Unsigned absolute URL, which goes to the foo item detail page.
def get_absolute_url(self):
return reverse('foo.detail', kwargs={'slug': self.slug})
# Signed absolute URL, which goes to the foo item detail page.
@sign_url()
def get_signed_absolute_url(self):
return reverse('foo.detail', kwargs={'slug': self.slug})
Note, that ``sign_url`` decorator accepts the following optional arguments.
- ``auth_user`` (``str``): Username of the user making the request.
- ``secret_key``: The shared secret key. If set, overrides
the ``SKA_SECRET_KEY`` variable set in the ``settings`` module of your
project.
- ``valid_until`` (``float`` or ``str``): Unix timestamp. If not given, generated
automatically (now + lifetime).
- ``lifetime`` (``int``): Signature lifetime in seconds.
- ``suffix`` (``str``): Suffix to add after the ``endpoint_url`` and before the
appended signature params.
- ``signature_param`` (``str``): Name of the GET param name which would hold the
generated signature value.
- ``auth_user_param`` (``str``): Name of the GET param name which would hold
the ``auth_user`` value.
- ``valid_until_param`` (``str``): Name of the GET param name which would hold
the ``valid_until`` value.
Django view decorator ``validate_signed_request``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To be used to protect views (file views.py). Should be applied to
views (endpoints) that require signed requests. If checks are not successful,
a ``ska.contrib.django.ska.http.HttpResponseUnauthorized`` is returned, which
is a subclass of Django's ``django.http.HttpResponse``. You can provide your
own template for 401 error. Simply point the
``SKA_UNAUTHORISED_REQUEST_ERROR_TEMPLATE`` in ``settings`` module to the right
template. See ``ska/contrib/django/ska/templates/ska/401.html`` as a template
example.
.. code-block:: python
from ska.contrib.django.ska.decorators import validate_signed_request
# Your view that shall be protected
@validate_signed_request()
def detail(request, slug, template_name='foo/detail.html'):
# Your code
Note, that ``validate_signed_request`` decorator accepts the following optional
arguments.
- ``secret_key`` (``str``) : The shared secret key. If set, overrides
the ``SKA_SECRET_KEY`` variable set in the ``settings`` module of your
project.
- ``signature_param`` (``str``): Name of the (for example GET or POST) param name
which holds the ``signature`` value.
- ``auth_user_param`` (``str``): Name of the (for example GET or POST) param name
which holds the ``auth_user`` value.
- ``valid_until_param`` (``str``): Name of the (foe example GET or POST) param
name which holds the ``valid_until`` value.
If you're using class based views, use the ``m_validate_signed_request``
decorator instead of ``validate_signed_request``.
Template tags
~~~~~~~~~~~~~
There are two template tags modules: ``ska_tags`` and ``ska_constance_tags``.
They are functionally identical, although ``ska_constance_tags`` is tied to
``django-constance``.
For standard settings configurations, template tags shall be loaded as follows:
.. code-block:: html
{% load ska_tags %}
For ``django-constance`` based settings configurations, template tags shall be
loaded as follows:
.. code-block:: html
{% load ska_constance_tags %}
Note, that if you want to use ``ska_constance_tags``, add
the ``ska.contrib.django.ska.integration.constance_integration`` line to
your``INSTALLED_APPS``:
.. code-block:: python
INSTALLED_APPS = (
# ...
'ska.contrib.django.ska.integration.constance_integration',
# ...
)
sign_url
++++++++
The ``sign_url`` template tag accepts template context and the following
params:
- ``url``
- ``auth_user``: If not given, ``request.user.get_username()`` is used.
- ``secret_key``: If not given, the secret key from settings is used.
- ``valid_until``: If not given, calculated from ``lifetime``.
- ``lifetime``: Defaults to ``ska.defaults.SIGNATURE_LIFETIME``.
- ``suffix``: Defaults to ``ska.defaults.DEFAULT_URL_SUFFIX``.
- ``signature_param``: Defaults to ``ska.defaultsDEFAULT_SIGNATURE_PARAM``.
- ``auth_user_param``: Defaults to ``ska.defaults.DEFAULT_AUTH_USER_PARAM``.
- ``valid_until_param``: Defaults to ``ska.defaults.DEFAULT_VALID_UNTIL_PARAM``.
- ``signature_cls``: Defaults to ``ska.signatures.Signature``.
Usage example:
.. code-block:: html
{% load ska_tags %}
{% for item in items%}
{% sign_url item.get_absolute_url as item_signed_absolute_url %}
<a href="{{ item_signed_absolute_url }}">{{ item }}</a>
{% endfor %}
provider_sign_url
+++++++++++++++++
The ``provider_sign_url`` template tag accepts template context and the
following params:
- ``url``
- ``provider``: Provider name.
- ``auth_user``: If not given, ``request.user.get_username()`` is used.
- ``valid_until``: If not given, calculated from ``lifetime``.
- ``lifetime``: Defaults to ``ska.defaults.SIGNATURE_LIFETIME``.
- ``suffix``: Defaults to ``ska.defaults.DEFAULT_URL_SUFFIX``.
- ``signature_param``: Defaults to ``ska.defaultsDEFAULT_SIGNATURE_PARAM``.
- ``auth_user_param``: Defaults to ``ska.defaults.DEFAULT_AUTH_USER_PARAM``.
- ``valid_until_param``: Defaults to ``ska.defaults.DEFAULT_VALID_UNTIL_PARAM``.
- ``signature_cls``: Defaults to ``ska.signatures.Signature``.
- ``fail_silently``: Defaults to False.
Usage example:
.. code-block:: html
{% load ska_tags %}
{% for item in items%}
{% provider_sign_url url=item.get_absolute_url provider='client_1.users' as item_signed_absolute_url %}
<a href="{{ item_signed_absolute_url }}">{{ item }}</a>
{% endfor %}
Authentication backends
~~~~~~~~~~~~~~~~~~~~~~~
Allows you to get a password-less login to Django web site.
At the moment there are two backends implemented:
- `SkaAuthenticationBackend`_: Uses standard Django settings.
- `SkaAuthenticationConstanceBackend`_: Relies on dynamic settings
functionality provided by ``django-constance``.
By default, number of logins using the same token is not limited. If you wish
that single tokens become invalid after first use, set the following variables
to True in your projects' Django settings module.
.. code-block:: python
SKA_DB_STORE_SIGNATURES = True
SKA_DB_PERFORM_SIGNATURE_CHECK = True
SkaAuthenticationBackend
++++++++++++++++++++++++
``SkaAuthenticationBackend`` uses standard Django settings.
Recipient side
^^^^^^^^^^^^^^
Recipient is the host (Django site), to which the sender tries to get
authenticated (log in). On the recipient side the following shall be present.
settings.py
***********
.. code-block:: python
AUTHENTICATION_BACKENDS = (
'ska.contrib.django.ska.backends.SkaAuthenticationBackend',
'django.contrib.auth.backends.ModelBackend',
)
INSTALLED_APPS = (
# ...
'ska.contrib.django.ska',
# ...
)
SKA_SECRET_KEY = 'secret-key'
SKA_UNAUTHORISED_REQUEST_ERROR_TEMPLATE = 'ska/401.html'
SKA_REDIRECT_AFTER_LOGIN = '/foo/logged-in/'
urls.py
*******
.. code-block:: python
urlpatterns = [
url(r'^ska/', include('ska.contrib.django.ska.urls')),
url(r'^admin/', include(admin.site.urls)),
]
Callbacks
*********
There are several callbacks implemented for authentication backend.
- ``USER_VALIDATE_CALLBACK`` (``str``): Validate request callback. Created to
allow adding custom logic to the incoming authentication requests. The main
purpose is to provide a flexible way of raising exceptions if the incoming
authentication request shall be blocked (for instance, email or username is
in black-list or right the opposite - not in the white list). The only aim of
the ``USER_VALIDATE_CALLBACK`` is to raise a ``django.core.PermissionDenied``
exception if request data is invalid. In that case authentication flow will
halt. All other exceptions would simply be ignored (but logged) and if no
exception raised, the normal flow would be continued.
- ``USER_GET_CALLBACK`` (``str``): Fired if user was successfully fetched from
database (existing user).
- ``USER_CREATE_CALLBACK`` (``str``): Fired right after user has been
created (user didn't exist).
- ``USER_INFO_CALLBACK`` (``str``): Fired upon successful authentication.
Example of a callback function (let's say, it resides in module
``my_app.ska_callbacks``):
.. code-block:: python
def my_callback(user, request, signed_request_data)
# Your code
...where:
- ``user`` is ``django.contrib.auth.models.User`` instance.
- ``request`` is ``django.http.HttpRequest`` instance.
- ``signed_request_data`` is dictionary with signed request data.
For example, if you need to assign user to some local Django group, you could
specify the group name on the client side (add it to the ``extra`` dictionary)
and based on that, add the user to the group in the callback.
The callback is a path qualifier of the callback function. Considering the
example above, it would be ``my_app.ska_callbacks.my_callback``.
Prefix names of each callback variable with ``SKA_`` in your projects' settings
module.
Example:
.. code-block:: python
SKA_USER_GET_CALLBACK = 'my_app.ska_callbacks.my_get_callback'
SKA_USER_CREATE_CALLBACK = 'my_app.ska_callbacks.my_create_callback'
Sender side
^^^^^^^^^^^
Sender is the host (another Django web site) from which users authenticate to
the Recipient using signed URLs.
On the sender side, the only thing necessary to be present is the ``ska``
module for Django and of course the same ``SECRET_KEY`` as on the server side.
Further, the server ``ska`` login URL (in our case "/ska/login/") shall be
signed using ``ska`` (for example, using ``sign_url`` function). The
``auth_user`` param would be used as a Django username. See the example below.
.. code-block:: python
from ska import sign_url
from ska.contrib.django.ska.settings import SECRET_KEY
server_ska_login_url = 'https://server-url.com/ska/login/'
signed_url = sign_url(
auth_user='test_ska_user_0',
secret_key=SECRET_KEY,
url=server_ska_login_url,
extra={
'email': 'john.doe@mail.example.com',
'first_name': 'John',
'last_name': 'Doe',
}
)
Note, that you ``extra`` dictionary is optional! If ``email``, ``first_name``
and ``last_name`` keys are present, upon successful validation, the data
would be saved into users' profile.
Put this code, for instance, in your view and then make the generated URL
available in template context and render it as a URL so that user can click
on it for authenticating to the server.
.. code-block:: python
def auth_to_server(request, template_name='auth_to_server.html'):
# Some code + obtaining the `signed_url` (code shown above)
context = {'signed_url': signed_url}
return render(request, template_name, context)
SkaAuthenticationConstanceBackend
+++++++++++++++++++++++++++++++++
Relies on dynamic settings functionality provided by
`django-constance <https://django-constance.readthedocs.io>`_.
*Only differences with `SkaAuthenticationBackend` are mentioned.*
.. note::
Additional requirements shall be installed. See the `constance.txt
<https://github.com/barseghyanartur/ska/blob/main/examples/requirements/constance.txt>`_
file for additional requirements (``django-constance``,
``django-json-widget``, ``django-picklefield``, ``jsonfield2`` and
``redis``).
settings.py
^^^^^^^^^^^
.. code-block:: python
AUTHENTICATION_BACKENDS = (
'ska.contrib.django.ska.backends.constance_backend.SkaAuthenticationConstanceBackend',
'django.contrib.auth.backends.ModelBackend',
)
INSTALLED_APPS = (
# ...
'constance', # django-constance
'ska.contrib.django.ska',
'django_json_widget', # For nice admin JSON widget
# ...
)
CONSTANCE_CONFIG = {
'SKA_PROVIDERS': (
{}, # The default value
'JSON data', # Help text in admin
'JSONField_config', # Field config
)
}
CONSTANCE_ADDITIONAL_FIELDS = {
'JSONField_config': [
# `jsonfield2` package might be used for storing the JSON field,
# however, at the moment of writing it has a bug which makes
# the JSON invalid after the first save. To avoid that, it has
# been patched and resides in examples/simple/jsonfield2_addons/
# module.
'jsonfield2_addons.forms.JSONField',
{
'widget': 'django_json_widget.widgets.JSONEditorWidget',
}
],
}
CONSTANCE_BACKEND = 'constance.backends.redisd.RedisBackend'
CONSTANCE_REDIS_CONNECTION = {
'host': 'localhost',
'port': 6379,
'db': 0,
}
.. note::
In very tiny bits, although not required, the
`jsonfield2 <https://pypi.org/project/jsonfield2/>`_ and
`django-json-widget <https://pypi.org/project/django-json-widget/>`_
packages are used for editing of the ``SKA_PROVIDERS`` setting in Django
admin.
.. note::
In the example shown above, the ``RedisBackend`` of ``django-constance``
is used. You could also use ``DatabaseBackend``. Study the
`documentation <https://django-constance.readthedocs.io/en/latest/backends.html>`_
for more.
.. note::
If your `SKA_PROVIDERS` settings are stored in the constance as ``str``
instead of ``dict``, set the setting
``SKA_CONSTANCE_SETTINGS_PARSE_FROM_JSON`` to ``True``.
With ``DatabaseBackend`` it would look as follows:
.. code-block:: python
CONSTANCE_BACKEND = 'constance.backends.database.DatabaseBackend'
INSTALLED_APPS = (
# ...
'constance.backends.database',
# ...
)
**Quick demo of the dynamic backend**
- Clone this project:
.. code-block:: sh
git clone git@github.com:barseghyanartur/ska.git
- Install/migrate:
.. code-block:: sh
./scripts/install.sh
pip install -r examples/requirements/django_2_1.txt
./scripts/migrate.sh --settings=settings.constance_settings
- Run:
.. code-block:: sh
./scripts/runserver.sh --settings=settings.constance_settings
- Go to `http://localhost:8000/admin/constance/config/
<http://localhost:8000/admin/constance/config/>`_.
- Paste the following code:
.. code-block:: javascript
{
"client_1.users":{
"SECRET_KEY":"client-1-users-secret-key"
},
"client_1.power_users":{
"SECRET_KEY":"client-1-power-users-secret-key",
"USER_CREATE_CALLBACK":"foo.ska_callbacks.client1_power_users_create"
},
"client_1.admins":{
"SECRET_KEY":"client-1-admins-secret-key",
"USER_CREATE_CALLBACK":"foo.ska_callbacks.client1_admins_create",
"USER_GET_CALLBACK":"foo.ska_callbacks.client1_admins_get",
"USER_INFO_CALLBACK":"foo.ska_callbacks.client1_admins_info_constance",
"REDIRECT_AFTER_LOGIN":"/admin/auth/user/"
}
}
- Open `http://localhost:8000/foo/authenticate/
<http://localhost:8000/foo/authenticate/>`_ in another browser and navigate
to the ``Log in - client_1.admins`` link in the ``Success`` table column of
the ``By provider`` section. Upon clicking, you should be logged in.
You have used the dynamic settings.
urls.py
^^^^^^^
``django-constance`` specific views and urls are used. See
`ska.contrib.django.ska.views.constance_views
<https://github.com/barseghyanartur/ska/blob/main/src/ska/contrib/django/ska/views/constance_views.py>`_
and `ska.contrib.django.ska.urls.constance_urls
<https://github.com/barseghyanartur/ska/blob/main/src/ska/contrib/django/ska/urls/constance_urls.py>`_
for the reference.
.. code-block:: python
urlpatterns = [
url(r'^ska/', include('ska.contrib.django.ska.urls.constance_urls')),
url(r'^admin/', include(admin.site.urls)),
]
Custom authentication backend
+++++++++++++++++++++++++++++
To implement alternative authentication backend, see the following example:
.. code-block:: python
from constance import config
from ska.contrib.django.backends import BaseSkaAuthenticationBackend
class SkaAuthenticationConstanceBackend(BaseSkaAuthenticationBackend):
"""Authentication backend."""
def get_settings(self):
"""
:return:
"""
return config.SKA_PROVIDERS
That's it. The only thing the ``get_settings`` method shall return is ``dict``
with providers data (see the `Multiple secret keys`_ for the reference;
return value of the ``get_settings` is ``SKA_PROVIDERS`` dict).
Purging of old signature data
+++++++++++++++++++++++++++++
If you have lots of visitors and the ``SKA_DB_STORE_SIGNATURES`` set to True,
your database grows. If you wish to get rid of old signature token data, you
may want to execute the following command using a cron job.
.. code-block:: sh
./manage.py ska_purge_stored_signature_data
Security notes
++++++++++++++
From point of security, you should be serving the following pages via HTTP
secure connection:
- The server login page (/ska/login/).
- The client page containing the authentication links.
Django REST Framework integration
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Permission classes
++++++++++++++++++
For protecting views without actually being authenticated into the system,
specific permission classes are implemented (for both plan settings and
provider settings, as well as both plain- and provider-settings work in
combination with ``django-constance`` package).
The following permission classes are implemented:
- ``SignedRequestRequired``
- ``ProviderSignedRequestRequired``
- ``ConstanceSignedRequestRequired``
- ``ConstanceProviderSignedRequestRequired``
**ProviderSignedRequestRequired example**
.. code-block:: python
from rest_framework.viewsets import ModelViewSet
from ska.contrib.django.ska.integration.drf.permissions import (
ProviderSignedRequestRequired
)
from .models import FooItem
from .serializers import FooItemSerializer
class FooItemViewSet(ModelViewSet):
"""FooItem model viewset."""
permission_classes = (ProviderSignedRequestRequired,)
queryset = FooItem.objects.all()
serializer_class = FooItemSerializer
**Signing requests**
Requests are signed the same way. Sample code:
.. code-block:: python
# Given that we have `auth_user`, `auth_user_email`, `provider_name`
# (and the rest), the code would look as follows:
from ska import sign_url
from ska.defaults import DEFAULT_PROVIDER_PARAM
extra = {
'email': auth_user_email,
'first_name': first_name,
'last_name': last_name,
}
if provider_name:
extra.update({DEFAULT_PROVIDER_PARAM: provider_name})
signed_url = sign_url(
auth_user=auth_user,
secret_key=secret_key,
url=url,
extra=extra
)
JWT tokens for authentication
+++++++++++++++++++++++++++++
For obtaining JWT tokens for authentication. Also works with
``django-constance``.
**settings example**
.. code-block:: python
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
),
}
**urls example**
.. code-block:: python
urlpatterns = [
# ...
url(
r'^ska-rest/',
include('ska.contrib.django.ska.integration.drf.urls.jwt_token')
),
]
**Sample request**
.. code-block:: text
http://localhost:8008/ska-rest/obtain-jwt-token/
?signature=P92KWDDe0U84Alvu0tvmYoi8e8s%3D
&auth_user=test_ska_user
&valid_until=1548195246.0
&extra=email%2Cfirst_name%2Clast_name
&email=test_ska_user%40mail.example.com
&first_name=John
&last_name=Doe
**Sample response**
.. code-block:: text
HTTP 200 OK
Allow: GET, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept
.. code-block:: javascript
{
"token": "eyJ0eXAiO.eyJ1c2VyX2lkIjo.m_saOvyKBO3"
}
Testing
=======
Simply type:
.. code-block:: sh
pytest
Or use tox:
.. code-block:: sh
tox
Or use tox to check specific env:
.. code-block:: sh
tox -e py39
Or run Django tests:
.. code-block:: sh
python examples/simple/manage.py test ska --settings=settings.testing
Writing documentation
=====================
Keep the following hierarchy.
.. code-block:: text
=====
title
=====
header
======
sub-header
----------
sub-sub-header
~~~~~~~~~~~~~~
sub-sub-sub-header
++++++++++++++++++
sub-sub-sub-sub-header
^^^^^^^^^^^^^^^^^^^^^^
sub-sub-sub-sub-sub-header
**************************
License
=======
GPL-2.0-only OR LGPL-2.1-or-later
Support
=======
For any issues contact me at the e-mail given in the `Author`_ section.
Author
======
Artur Barseghyan <artur.barseghyan@gmail.com>
Raw data
{
"_id": null,
"home_page": "https://github.com/barseghyanartur/ska",
"name": "ska",
"maintainer": "",
"docs_url": null,
"requires_python": "",
"maintainer_email": "",
"keywords": "sign data,sign (HTTP) request,symmetric-key algorithm encryption,sign URL,python,django,password-less login django,password-less authentication backend django",
"author": "Artur Barseghyan",
"author_email": "artur.barseghyan@gmail.com",
"download_url": "https://files.pythonhosted.org/packages/67/22/9789fade2645882af81174ca1adafdac40a01cddc6492a4a772ae254db33/ska-1.10.tar.gz",
"platform": null,
"description": "===\nska\n===\nLets you easily sign data, using symmetric-key algorithm encryption. Allows\nyou to validate signed data and identify possible validation errors. Uses\nsha-(1, 224, 256, 385 and 512)/hmac for signature encryption. Allows to use\ncustom hash algorithms. Comes with shortcut functions for signing (and\nvalidating) dictionaries and URLs.\n\n.. image:: https://img.shields.io/pypi/v/ska.svg\n :target: https://pypi.python.org/pypi/ska\n :alt: PyPI Version\n\n.. image:: https://img.shields.io/pypi/pyversions/ska.svg\n :target: https://pypi.python.org/pypi/ska/\n :alt: Supported Python versions\n\n.. image:: https://github.com/barseghyanartur/ska/workflows/test/badge.svg\n :target: https://github.com/barseghyanartur/ska/actions\n :alt: Build Status\n\n.. image:: https://readthedocs.org/projects/ska/badge/?version=latest\n :target: http://ska.readthedocs.io/en/latest/?badge=latest\n :alt: Documentation Status\n\n.. image:: https://img.shields.io/badge/license-GPL--2.0--only%20OR%20LGPL--2.1--or--later-blue.svg\n :target: https://github.com/barseghyanartur/ska/#License\n :alt: GPL-2.0-only OR LGPL-2.1-or-later\n\n.. image:: https://coveralls.io/repos/github/barseghyanartur/ska/badge.svg?branch=main&service=github\n :target: https://coveralls.io/github/barseghyanartur/ska?branch=main\n :alt: Coverage\n\nKey concepts\n============\nHosts, that communicate with each other, share the Secret Key, which is used\nto sign data (requests). Secret key is never sent around.\n\nOne of the cases is signing of HTTP requests. Each (HTTP) request is signed\non the sender side using the shared Secret Key and as an outcome produces the\ntriple (``signature``, ``auth_user``, ``valid_until``) which are used to sign\nthe requests.\n\n- ``signature`` (``str``): Signature generated.\n- ``auth_user`` (``str``): User making the request. Can be anything.\n- ``valid_until`` (``float`` or ``str``): Signature expiration time (Unix timestamp).\n\nOn the recipient side, (HTTP request) data is validated using the shared\nSecret Key. It's being checked whether signature is valid and not expired.\n\n.. code-block:: text\n\n \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 Data \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u2502 Host 1 \u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500>\u2502 Host 2 \u2502\n \u2502 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2502 \u2502 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2502\n \u2502 secret key \u2502 \u2502 secret key \u2502\n \u2502 'my-secret' \u2502<\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524 'my-secret' \u2502\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 Data \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n\nFeatures\n========\nCore `ska` module\n-----------------\n- Sign dictionaries.\n- Validate signed dictionaries.\n- Sign URLs. Append and sign additional URL data.\n- Validate URLs.\n- Use one of the built-in algorythms (HMAC SHA-1, HMAC SHA-224, HMAC SHA-256,\n HMAC SHA-384 or HMAC SHA-512) or define a custom one.\n\nDjango `ska` module (`ska.contrib.django.ska`)\n----------------------------------------------\n- Model decorators for signing absolute URLs. View (including class-based\n views) decorators for protecting views to authorised parties only (no\n authentication required).\n- Authentication backend for Django based on the signatures (tokens) generated\n using `ska`, which allows you to get a password-less login to Django web\n site. Multiple Secret Keys (per provider) supported. Comes with handy\n callbacks (possible to customise per provider) for various states of\n authentication.\n- Template tags for signing URLs from within templates.\n- `django-constance` integration (for password-less authentication).\n- `Django REST Framework integration`_ (for protecting ViewSets, obtaining\n JWT tokens for authentication).\n\nPrerequisites\n=============\nPresent\n-------\n- Core ``ska`` module requires Python 3.8, 3.9, 3.10 and 3.11.\n- Django ``ska`` module (``ska.contrib.django.ska``) requires the mentioned\n above plus Django 3.2, 4.1 or 4.2. Additionally, certain\n versions of `django-constance` and `djangorestframework` are required.\n Specific version requirement primarily depends on the used Django version.\n Check the `example requirements\n <https://github.com/barseghyanartur/ska/tree/main/examples/requirements>`_\n to find out which versions of `django-constance` and `djangorestframework`\n have been tested with specific Django versions.\n\nPast\n----\n.. note::\n\n In future releases (any time) compatibility with no-longer-supported\n versions might/will be wiped out.\n\n- Dropping support of Python 3.6 and 3.7 has been announced in version 1.10.\n As of 1.9.1 everything still worked.\n- Dropping support of Python 2.7 and 3.5 has been announced in version 1.8.\n As of 1.7.5 everything still worked.\n- Dropping support of Python 3.4 has been announced in version 1.6.8. As of\n 1.6.8 everything still worked.\n- Dropping support of Django 2.2, 3.0, 3.1 and 4.0 has been announced in\n version 1.10. As of 1.9.1 everything is still backwards compatible with\n mentioned versions.\n- Dropping support of Django 1.5, 1.6 and 1.7 has been announced in version\n 1.6. As of 1.6 everything is still backwards compatible with mentioned\n versions.\n- Dropping support of Python 2.6 and 3.3 has been announced in version 1.6.\n As of 1.6 everything is still backwards compatible (as much as it's possible\n within this package) with mentioned versions.\n\nEco-system\n==========\nNeed ``ska`` for other languages? Check the following affiliated projects:\n\n- `skajs <https://github.com/barseghyanartur/skajs>`_: ``ska`` implementation\n for NodeJS (both CommonJS and ESM are supported, Node >= 14).\n- `skaphp <https://github.com/barseghyanartur/skaphp>`_: ``ska`` implementation\n for PHP (>= 7.2).\n\nGenerated signatures are inter-compatible between Python, NodeJS and PHP\nimplementations.\n\nInstallation\n============\nLatest stable version from PyPI:\n\n.. code-block:: sh\n\n pip install ska\n\nor latest development version from GitHub.\n\n.. code-block:: sh\n\n pip install https://github.com/barseghyanartur/ska/archive/main.tar.gz\n\nUsage examples\n==============\nFor integration with Django, see the `Django integration`_ section.\n\nBasic usage\n-----------\nPure Python usage.\n\nSender side\n~~~~~~~~~~~\nSigning URLs is as simple as follows.\n\nRequired imports.\n\n.. code-block:: python\n\n from ska import sign_url\n\nProducing a signed URL.\n\n.. code-block:: python\n\n signed_url = sign_url(\n auth_user='user',\n secret_key='your-secret_key',\n url='http://e.com/api/'\n )\n\n.. code-block:: text\n\n GET http://e.com/api/?valid_until=1378045287.0&auth_user=user&signature=YlZpLFsjUKBalL4x5trhkeEgqE8%3D\n\nDefault lifetime of a signature is 10 minutes (600 seconds). If you want it\nto be different, provide a ``lifetime`` argument to ``sign_url`` function.\n\nDefault name of the (GET) param holding the generated signature value\nis ``signature``. If you want it to be different, provide a ``signature_param``\nargument to ``sign_url`` function.\n\nDefault name of the (GET) param holding the ``auth_user`` value is\n``auth_user``. If you want it to be different, provide a ``auth_user_param``\nargument to ``sign_url`` function.\n\nDefault name of the (GET) param holding the ``valid_until`` value is\n`valid_until`. If you want it to be different, provide a ``valid_until_param``\nargument to ``sign_url`` function.\n\nNote, that by default a suffix '?' is added after the given ``url`` and\ngenerated signature params. If you want that suffix to be custom, provide a\n``suffix`` argument to the ``sign_url`` function. If you want it to be gone,\nset its' value to empty string.\n\nWith all customisations, it would look as follows:\n\n.. code-block:: python\n\n from ska import HMACSHA512Signature # Use HMAC SHA-512 algorithm\n\n signed_url = sign_url(\n auth_user='user',\n secret_key='your-secret_key',\n lifetime=120,\n url='http://e.com/api/',\n signature_param='signature',\n auth_user_param='auth_user',\n valid_until_param='valid_until',\n signature_cls=HMACSHA512Signature\n )\n\nIt's also possible to add additional data to the signature by providing a\n``extra`` argument (dict). Note, that additional data is signed as well.\nIf request is somehow tampered (values vary from originally provided ones),\nsignature becomes invalid.\n\n.. code-block:: python\n\n sign_url(\n auth_user='user',\n secret_key='your-secret_key',\n url='http://e.com/api/',\n extra={\n 'email': 'doe@example.com',\n 'last_name': 'Doe',\n 'first_name': 'Joe'\n }\n )\n\nYou may now proceed with the signed URL request. If you use the famous\n``requests`` library, it would be as follows.\n\n.. code-block:: python\n\n import requests\n requests.get(signed_url)\n\nIf you want to use POST method instead, you would likely want to get a\ndictionary back, in order to append it to the POST data later.\n\nRequired imports.\n\n.. code-block:: python\n\n from ska import signature_to_dict\n\nProducing a dictionary containing the signature data, ready to be put into\nthe request (for example POST) data. All customisations mentioned above for\nthe ``sign_url`` function, also apply to the ``signature_to_dict``:\n\n.. code-block:: python\n\n signature_dict = signature_to_dict(\n auth_user='user',\n secret_key='your-secret_key'\n )\n\n.. code-block:: text\n\n {\n 'signature': 'YlZpLFsjUKBalL4x5trhkeEgqE8=',\n 'auth_user': 'user',\n 'valid_until': '1378045287.0'\n }\n\nAdding of additional data to the signature works in the same way:\n\n.. code-block:: python\n\n signature_dict = signature_to_dict(\n auth_user='user',\n secret_key='your-secret_key',\n extra={\n 'email': 'john.doe@mail.example.com',\n 'first_name': 'John',\n 'last_name': 'Doe'\n }\n )\n\n.. code-block:: text\n\n {\n 'auth_user': 'user',\n 'email': 'john.doe@mail.example.com',\n 'extra': 'email,first_name,last_name',\n 'first_name': 'John',\n 'last_name': 'Doe',\n 'signature': 'cnSoU/LnJ/ZhfLtDLzab3a3gkug=',\n 'valid_until': 1387616469.0\n }\n\nIf you for some reason prefer a lower level implementation, read the same\nsection in the `Advanced usage (low-level)`_ chapter.\n\nRecipient side\n~~~~~~~~~~~~~~\nValidating the signed request data is as simple as follows.\n\nRequired imports.\n\n.. code-block:: python\n\n from ska import validate_signed_request_data\n\nValidating the signed request data. Note, that ``data`` value is expected to\nbe a dictionary; ``request.GET`` is given as an example. It will most likely\nvary from what's used in your framework (unless you use Django).\n\n.. code-block:: python\n\n validation_result = validate_signed_request_data(\n data=request.GET, # Note, that ``request.GET`` is given as example.\n secret_key='your-secret_key'\n )\n\nThe ``validate_signed_request_data`` produces a\n``ska.SignatureValidationResult`` object, which holds the following data.\n\n- ``result`` (``bool``): True if data is valid. False otherwise.\n- ``reason`` (``list``): List of strings, indicating validation errors. Empty list\n in case if ``result`` is True.\n\nDefault name of the (GET) param holding the signature value is ``signature``.\nIf you want it to be different, provide a ``signature_param`` argument to\n``validate_signed_request_data`` function.\n\nDefault name of the (GET) param holding the ``auth_user`` value is\n``auth_user``. If you want it to be different, provide a ``auth_user_param``\nargument to ``validate_signed_request_data`` function.\n\nDefault name of the (GET) param holding the ``valid_until`` value is\n``valid_until``. If you want it to be different, provide a\n``valid_until_param`` argument to ``validate_signed_request_data`` function.\n\nWith all customisations, it would look as follows. Note, that\n``request.GET`` is given as example.\n\n.. code-block:: python\n\n from ska import HMACSHA256Signature # Use HMAC SHA-256 algorithm\n\n validation_result = validate_signed_request_data(\n data=request.GET,\n secret_key='your-secret_key',\n signature_param='signature',\n auth_user_param='auth_user',\n valid_until_param='valid_until',\n signature_cls=HMACSHA256Signature\n )\n\nIf you for some reason prefer a lower level implementation, read the same\nsection in the `Advanced usage (low-level)`_ chapter.\n\nCommand line usage\n------------------\nIt's possible to generate a signed URL from command line using the\n``ska.generate_signed_url`` module.\n\n:Arguments:\n\n.. code-block:: text\n\n -h, --help show this help message and exit\n\n -au AUTH_USER, --auth-user AUTH_USER\n `auth_user` value\n\n -sk SECRET_KEY, --secret-key SECRET_KEY\n `secret_key` value\n\n -vu VALID_UNTIL, --valid-until VALID_UNTIL\n `valid_until` value\n\n -l LIFETIME, --lifetime LIFETIME\n `lifetime` value\n\n -u URL, --url URL URL to sign\n\n -sp SIGNATURE_PARAM, --signature-param SIGNATURE_PARAM\n (GET) param holding the `signature` value\n\n -aup AUTH_USER_PARAM, --auth-user-param AUTH_USER_PARAM\n (GET) param holding the `auth_user` value\n\n -vup VALID_UNTIL_PARAM, --valid-until-param VALID_UNTIL_PARAM\n (GET) param holding the `auth_user` value\n\n:Example:\n\n.. code-block:: sh\n\n ska-sign-url -au user -sk your-secret-key --url http://example.com\n\nAdvanced usage (low-level)\n--------------------------\nSender side\n~~~~~~~~~~~\n\nRequired imports.\n\n.. code-block:: python\n\n from ska import Signature, RequestHelper\n\nGenerate a signature.\n\n.. code-block:: python\n\n signature = Signature.generate_signature(\n auth_user='user',\n secret_key='your-secret-key'\n )\n\nDefault lifetime of a signature is 10 minutes (600 seconds). If you want it to\nbe different, provide a ``lifetime`` argument to ``generate_signature``\nmethod.\n\n.. code-block:: python\n\n signature = Signature.generate_signature(\n auth_user='user',\n secret_key='your-secret-key',\n lifetime=120 # Signature lifetime set to 120 seconds.\n )\n\nAdding of additional data to the signature works in the same way as in\n``sign_url``.\n\n.. code-block:: python\n\n signature = Signature.generate_signature(\n auth_user='user',\n secret_key='your-secret-key',\n extra={\n 'email': 'doe@example.com',\n 'last_name': 'Doe',\n 'first_name': 'Joe'\n }\n )\n\nFor HMAC SHA-384 algorithm it would look as follows.\n\n.. code-block:: python\n\n from ska import HMACSHA384Signature\n\n signature = HMACSHA384Signature.generate_signature(\n auth_user='user',\n secret_key='your-secret-key'\n )\n\nYour endpoint operates with certain param names and you need to wrap generated\nsignature params into the URL. In order to have the job done in an easy way,\ncreate a request helper. Feed names of the (GET) params to the request helper\nand let it make a signed endpoint URL for you.\n\n.. code-block:: python\n\n request_helper = RequestHelper(\n signature_param='signature',\n auth_user_param='auth_user',\n valid_until_param='valid_until'\n )\n\nAppend signature params to the endpoint URL.\n\n.. code-block:: python\n\n signed_url = request_helper.signature_to_url(\n signature=signature,\n endpoint_url='http://e.com/api/'\n )\n\n.. code-block:: text\n\n GET http://e.com/api/?valid_until=1378045287.0&auth_user=user&signature=YlZpLFsjUKBalL4x5trhkeEgqE8%3D\n\nMake a request.\n\n.. code-block:: python\n\n import requests\n r = requests.get(signed_url)\n\n\nFor HMAC SHA-384 algorithm it would look as follows.\n\n.. code-block:: python\n\n from ska import HMACSHA384Signature\n\n request_helper = RequestHelper(\n signature_param='signature',\n auth_user_param='auth_user',\n valid_until_param='valid_until',\n signature_cls=HMACSHA384Signature\n )\n\n signed_url = request_helper.signature_to_url(\n signature=signature,\n endpoint_url='http://e.com/api/'\n )\n\nRecipient side\n~~~~~~~~~~~~~~\nRequired imports.\n\n.. code-block:: python\n\n from ska import RequestHelper\n\nCreate a request helper. Your endpoint operates with certain param names. In\norder to have the job done in an easy way, we feed those params to the\nrequest helper and let it extract data from signed request for us.\n\n.. code-block:: python\n\n request_helper = RequestHelper(\n signature_param='signature',\n auth_user_param='auth_user',\n valid_until_param='valid_until'\n )\n\nValidate the request data. Note, that ``request.GET`` is given just as an\nexample.\n\n.. code-block:: python\n\n validation_result = request_helper.validate_request_data(\n data=request.GET,\n secret_key='your-secret-key'\n )\n\nYour implementation further depends on you, but may look as follows.\n\n.. code-block:: python\n\n if validation_result.result:\n # Validated, proceed further\n # ...\n else:\n # Validation not passed.\n raise Http404(validation_result.reason)\n\nYou can also just validate the signature by calling ``validate_signature``\nmethod of the ``ska.Signature``.\n\n.. code-block:: python\n\n Signature.validate_signature(\n signature='EBS6ipiqRLa6TY5vxIvZU30FpnM=',\n auth_user='user',\n secret_key='your-secret-key',\n valid_until='1377997396.0'\n )\n\nDjango integration\n------------------\n``ska`` comes with Django model- and view-decorators for producing signed URLs\nand and validating the endpoints, as well as with authentication backend,\nwhich allows password-less login into Django web site using ``ska`` generated\nsignature tokens. There's also a template tag for signing URLs.\n\nDemo\n~~~~\nIn order to be able to quickly evaluate the ``ska``, a demo app (with a quick\ninstaller) has been created (works on Ubuntu/Debian, may work on other Linux\nsystems as well, although not guaranteed). Follow the instructions below for\nhaving the demo running within a minute.\n\nGrab the latest ``ska_example_app_installer.sh`` and execute it:\n\n.. code-block:: sh\n\n wget -O - https://raw.github.com/barseghyanartur/ska/stable/examples/ska_example_app_installer.sh | bash\n\nOpen your browser and test the app.\n\nFoo listing (ska protected views):\n\n- URL: http://127.0.0.1:8001/foo/\n\nAuthentication page (ska authentication backend):\n\n- URL: http://127.0.0.1:8001/foo/authenticate/\n\nDjango admin interface:\n\n- URL: http://127.0.0.1:8001/admin/\n- Admin username: test_admin\n- Admin password: test\n\nConfiguration\n~~~~~~~~~~~~~\nSecret key (``str``) must be defined in ``settings`` module of your project.\n\n.. code-block:: python\n\n SKA_SECRET_KEY = 'my-secret-key'\n\nThe following variables can be overridden in ``settings`` module of your\nproject.\n\n- ``SKA_UNAUTHORISED_REQUEST_ERROR_MESSAGE`` (``str``): Plain text error message.\n Defaults to \"Unauthorised request. {0}\".\n- ``SKA_UNAUTHORISED_REQUEST_ERROR_TEMPLATE`` (``str``): Path to 401 template that\n should be rendered in case of 401\n responses. Defaults to empty string (not provided).\n- ``SKA_AUTH_USER`` (``str``): The ``auth_user`` argument for ``ska.sign_url``\n function. Defaults to \"ska-auth-user\".\n\nSee the working `example project\n<https://github.com/barseghyanartur/ska/tree/stable/example>`_.\n\nMultiple secret keys\n~~~~~~~~~~~~~~~~~~~~\nImagine, you have a site to which you want to offer a password-less login for\nvarious clients/senders and you don't want them all to have one shared secret\nkey, but rather have their own one. Moreover, you specifically want to execute\nvery custom callbacks not only for each separate client/sender, but also for\ndifferent sort of users authenticating.\n\n.. code-block:: text\n\n \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u2502 Site providing \u2502\n \u2502 authentication \u2502\n \u2502 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2502\n \u2502 custom secret \u2502\n \u2502 keys per \u2502\n \u2502 client \u2502\n \u2502 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2502\n \u2502 Site 1: 'sk-1' \u2502\n \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500>\u2502 Site 2: 'sk-2' \u2502<\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u2502 \u2502 Site 3: 'sk-3' \u2502 \u2502\n \u2502 \u250c\u2500\u2500\u2500\u2500>\u2502 Site 4: 'sk-4' \u2502<\u2500\u2500\u2500\u2500\u2510 \u2502\n \u2502 \u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502 \u2502\n \u2502 \u2502 \u2502 \u2502\n \u2502 \u2502 \u2502 \u2502\n \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2510 \u250c\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2510 \u250c\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u2502 Site 1 \u2502 \u2502 Site 2 \u2502 \u2502 Site 3 \u2502 \u2502 Site 4 \u2502\n \u2502 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2502 \u2502 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2502 \u2502 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2502 \u2502 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2502\n \u2502 secret key \u2502 \u2502 secret key \u2502 \u2502 secret key \u2502 \u2502 secret key \u2502\n \u2502 'sk-1' \u2502 \u2502 'sk-2' \u2502 \u2502 'sk-3' \u2502 \u2502 'sk-4' \u2502\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n\nIn order to make the stated above possible, the concept of providers is\nintroduced. You can define a secret key, callbacks or redirect URL. See an\nexample below. Note, that keys of the ``SKA_PROVIDERS`` (\"client_1\",\n\"client_2\", etc.) are the provider keys.\n\n.. code-block:: python\n\n SKA_PROVIDERS = {\n # ********************************************************\n # ******************** Basic gradation *******************\n # ********************************************************\n # Site 1\n 'client_1': {\n 'SECRET_KEY': 'sk-1',\n },\n\n # Site 2\n 'client_2': {\n 'SECRET_KEY': 'sk-2',\n },\n\n # Site 3\n 'client_3': {\n 'SECRET_KEY': 'sk-3',\n },\n\n # Site 4\n 'client_4': {\n 'SECRET_KEY': 'sk-4',\n },\n\n # ********************************************************\n # ******* You make gradation as complex as you wish ******\n # ********************************************************\n # Client 1, group users\n 'client_1.users': {\n 'SECRET_KEY': 'client-1-users-secret-key',\n },\n\n # Client 1, group power_users\n 'client_1.power_users': {\n 'SECRET_KEY': 'client-1-power-users-secret-key',\n 'USER_CREATE_CALLBACK': 'foo.ska_callbacks.client1_power_users_create',\n },\n\n # Client 1, group admins\n 'client_1.admins': {\n 'SECRET_KEY': 'client-1-admins-secret-key',\n 'USER_CREATE_CALLBACK': 'foo.ska_callbacks.client1_admins_create',\n 'REDIRECT_AFTER_LOGIN': '/admin/'\n },\n }\n\nSee the `Callbacks`_ section for the list of callbacks. Note, that callbacks\ndefined in the ``SKA_PROVIDERS`` are overrides. If a certain callback isn't\ndefined in the ``SKA_PROVIDERS``, authentication backend falls back to the\nrespective default callback function.\n\nObviously, server would have to have the full list of providers defined. On\nthe client side you would only have to store the general secret key and of\ncourse the provider UID(s).\n\nWhen making a signed URL on the sender side, you should be providing the\n``provider`` key in the ``extra`` argument. See the example below for how you\nwould do it for ``client_1.power_users``.\n\n.. code-block:: python\n\n from ska import sign_url\n from ska.defaults import DEFAULT_PROVIDER_PARAM\n\n server_ska_login_url = 'https://server-url.com/ska/login/'\n\n signed_remote_ska_login_url = sign_url(\n auth_user='test_ska_user',\n # Using provider-specific secret key. This value shall be equal to\n # the value of SKA_PROVIDERS['client_1.power_users']['SECRET_KEY'],\n # defined in your projects' Django settings module.\n secret_key='client-1-power-users-secret-key',\n url=server_ska_login_url,\n extra={\n 'email': 'test_ska_user@mail.example.com',\n 'first_name': 'John',\n 'last_name': 'Doe',\n # Using provider specific string. This value shall be equal to\n # the key string \"client_1.power_users\" of SKA_PROVIDERS,\n # defined in your projcts' Django settings module.\n DEFAULT_PROVIDER_PARAM: 'client_1.power_users',\n }\n )\n\nDjango model method decorator ``sign_url``\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nThis is most likely be used in module ``models`` (models.py).\n\nImagine, you have a some objects listing and you want to protect the URLs to\nbe viewed by authorised parties only. You would then use\n``get_signed_absolute_url`` method when rendering the listing (HTML).\n\n.. code-block:: python\n\n from django.db import models\n from django.utils.translation import ugettext_lazy as _\n from django.core.urlresolvers import reverse\n\n from ska.contrib.django.ska.decorators import sign_url\n\n\n class FooItem(models.Model):\n\n title = models.CharField(_(\"Title\"), max_length=100)\n slug = models.SlugField(unique=True, verbose_name=_(\"Slug\"))\n body = models.TextField(_(\"Body\"))\n\n # Unsigned absolute URL, which goes to the foo item detail page.\n def get_absolute_url(self):\n return reverse('foo.detail', kwargs={'slug': self.slug})\n\n # Signed absolute URL, which goes to the foo item detail page.\n @sign_url()\n def get_signed_absolute_url(self):\n return reverse('foo.detail', kwargs={'slug': self.slug})\n\nNote, that ``sign_url`` decorator accepts the following optional arguments.\n\n- ``auth_user`` (``str``): Username of the user making the request.\n- ``secret_key``: The shared secret key. If set, overrides\n the ``SKA_SECRET_KEY`` variable set in the ``settings`` module of your\n project.\n- ``valid_until`` (``float`` or ``str``): Unix timestamp. If not given, generated\n automatically (now + lifetime).\n- ``lifetime`` (``int``): Signature lifetime in seconds.\n- ``suffix`` (``str``): Suffix to add after the ``endpoint_url`` and before the\n appended signature params.\n- ``signature_param`` (``str``): Name of the GET param name which would hold the\n generated signature value.\n- ``auth_user_param`` (``str``): Name of the GET param name which would hold\n the ``auth_user`` value.\n- ``valid_until_param`` (``str``): Name of the GET param name which would hold\n the ``valid_until`` value.\n\nDjango view decorator ``validate_signed_request``\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nTo be used to protect views (file views.py). Should be applied to\nviews (endpoints) that require signed requests. If checks are not successful,\na ``ska.contrib.django.ska.http.HttpResponseUnauthorized`` is returned, which\nis a subclass of Django's ``django.http.HttpResponse``. You can provide your\nown template for 401 error. Simply point the\n``SKA_UNAUTHORISED_REQUEST_ERROR_TEMPLATE`` in ``settings`` module to the right\ntemplate. See ``ska/contrib/django/ska/templates/ska/401.html`` as a template\nexample.\n\n.. code-block:: python\n\n from ska.contrib.django.ska.decorators import validate_signed_request\n\n # Your view that shall be protected\n @validate_signed_request()\n def detail(request, slug, template_name='foo/detail.html'):\n # Your code\n\nNote, that ``validate_signed_request`` decorator accepts the following optional\narguments.\n\n- ``secret_key`` (``str``) : The shared secret key. If set, overrides\n the ``SKA_SECRET_KEY`` variable set in the ``settings`` module of your\n project.\n- ``signature_param`` (``str``): Name of the (for example GET or POST) param name\n which holds the ``signature`` value.\n- ``auth_user_param`` (``str``): Name of the (for example GET or POST) param name\n which holds the ``auth_user`` value.\n- ``valid_until_param`` (``str``): Name of the (foe example GET or POST) param\n name which holds the ``valid_until`` value.\n\nIf you're using class based views, use the ``m_validate_signed_request``\ndecorator instead of ``validate_signed_request``.\n\nTemplate tags\n~~~~~~~~~~~~~\nThere are two template tags modules: ``ska_tags`` and ``ska_constance_tags``.\nThey are functionally identical, although ``ska_constance_tags`` is tied to\n``django-constance``.\n\nFor standard settings configurations, template tags shall be loaded as follows:\n\n.. code-block:: html\n\n {% load ska_tags %}\n\nFor ``django-constance`` based settings configurations, template tags shall be\nloaded as follows:\n\n.. code-block:: html\n\n {% load ska_constance_tags %}\n\nNote, that if you want to use ``ska_constance_tags``, add\nthe ``ska.contrib.django.ska.integration.constance_integration`` line to\nyour``INSTALLED_APPS``:\n\n.. code-block:: python\n\n INSTALLED_APPS = (\n # ...\n 'ska.contrib.django.ska.integration.constance_integration',\n # ...\n )\n\nsign_url\n++++++++\nThe ``sign_url`` template tag accepts template context and the following\nparams:\n\n- ``url``\n- ``auth_user``: If not given, ``request.user.get_username()`` is used.\n- ``secret_key``: If not given, the secret key from settings is used.\n- ``valid_until``: If not given, calculated from ``lifetime``.\n- ``lifetime``: Defaults to ``ska.defaults.SIGNATURE_LIFETIME``.\n- ``suffix``: Defaults to ``ska.defaults.DEFAULT_URL_SUFFIX``.\n- ``signature_param``: Defaults to ``ska.defaultsDEFAULT_SIGNATURE_PARAM``.\n- ``auth_user_param``: Defaults to ``ska.defaults.DEFAULT_AUTH_USER_PARAM``.\n- ``valid_until_param``: Defaults to ``ska.defaults.DEFAULT_VALID_UNTIL_PARAM``.\n- ``signature_cls``: Defaults to ``ska.signatures.Signature``.\n\nUsage example:\n\n.. code-block:: html\n\n {% load ska_tags %}\n\n {% for item in items%}\n\n {% sign_url item.get_absolute_url as item_signed_absolute_url %}\n <a href=\"{{ item_signed_absolute_url }}\">{{ item }}</a>\n\n {% endfor %}\n\nprovider_sign_url\n+++++++++++++++++\nThe ``provider_sign_url`` template tag accepts template context and the\nfollowing params:\n\n- ``url``\n- ``provider``: Provider name.\n- ``auth_user``: If not given, ``request.user.get_username()`` is used.\n- ``valid_until``: If not given, calculated from ``lifetime``.\n- ``lifetime``: Defaults to ``ska.defaults.SIGNATURE_LIFETIME``.\n- ``suffix``: Defaults to ``ska.defaults.DEFAULT_URL_SUFFIX``.\n- ``signature_param``: Defaults to ``ska.defaultsDEFAULT_SIGNATURE_PARAM``.\n- ``auth_user_param``: Defaults to ``ska.defaults.DEFAULT_AUTH_USER_PARAM``.\n- ``valid_until_param``: Defaults to ``ska.defaults.DEFAULT_VALID_UNTIL_PARAM``.\n- ``signature_cls``: Defaults to ``ska.signatures.Signature``.\n- ``fail_silently``: Defaults to False.\n\nUsage example:\n\n.. code-block:: html\n\n {% load ska_tags %}\n\n {% for item in items%}\n\n {% provider_sign_url url=item.get_absolute_url provider='client_1.users' as item_signed_absolute_url %}\n <a href=\"{{ item_signed_absolute_url }}\">{{ item }}</a>\n\n {% endfor %}\n\nAuthentication backends\n~~~~~~~~~~~~~~~~~~~~~~~\nAllows you to get a password-less login to Django web site.\n\nAt the moment there are two backends implemented:\n\n- `SkaAuthenticationBackend`_: Uses standard Django settings.\n- `SkaAuthenticationConstanceBackend`_: Relies on dynamic settings\n functionality provided by ``django-constance``.\n\nBy default, number of logins using the same token is not limited. If you wish\nthat single tokens become invalid after first use, set the following variables\nto True in your projects' Django settings module.\n\n.. code-block:: python\n\n SKA_DB_STORE_SIGNATURES = True\n SKA_DB_PERFORM_SIGNATURE_CHECK = True\n\nSkaAuthenticationBackend\n++++++++++++++++++++++++\n``SkaAuthenticationBackend`` uses standard Django settings.\n\nRecipient side\n^^^^^^^^^^^^^^\nRecipient is the host (Django site), to which the sender tries to get\nauthenticated (log in). On the recipient side the following shall be present.\n\nsettings.py\n***********\n.. code-block:: python\n\n AUTHENTICATION_BACKENDS = (\n 'ska.contrib.django.ska.backends.SkaAuthenticationBackend',\n 'django.contrib.auth.backends.ModelBackend',\n )\n\n INSTALLED_APPS = (\n # ...\n 'ska.contrib.django.ska',\n # ...\n )\n\n SKA_SECRET_KEY = 'secret-key'\n SKA_UNAUTHORISED_REQUEST_ERROR_TEMPLATE = 'ska/401.html'\n SKA_REDIRECT_AFTER_LOGIN = '/foo/logged-in/'\n\nurls.py\n*******\n.. code-block:: python\n\n urlpatterns = [\n url(r'^ska/', include('ska.contrib.django.ska.urls')),\n url(r'^admin/', include(admin.site.urls)),\n ]\n\nCallbacks\n*********\nThere are several callbacks implemented for authentication backend.\n\n- ``USER_VALIDATE_CALLBACK`` (``str``): Validate request callback. Created to\n allow adding custom logic to the incoming authentication requests. The main\n purpose is to provide a flexible way of raising exceptions if the incoming\n authentication request shall be blocked (for instance, email or username is\n in black-list or right the opposite - not in the white list). The only aim of\n the ``USER_VALIDATE_CALLBACK`` is to raise a ``django.core.PermissionDenied``\n exception if request data is invalid. In that case authentication flow will\n halt. All other exceptions would simply be ignored (but logged) and if no\n exception raised, the normal flow would be continued.\n- ``USER_GET_CALLBACK`` (``str``): Fired if user was successfully fetched from\n database (existing user).\n- ``USER_CREATE_CALLBACK`` (``str``): Fired right after user has been\n created (user didn't exist).\n- ``USER_INFO_CALLBACK`` (``str``): Fired upon successful authentication.\n\nExample of a callback function (let's say, it resides in module\n``my_app.ska_callbacks``):\n\n.. code-block:: python\n\n def my_callback(user, request, signed_request_data)\n # Your code\n\n...where:\n\n- ``user`` is ``django.contrib.auth.models.User`` instance.\n- ``request`` is ``django.http.HttpRequest`` instance.\n- ``signed_request_data`` is dictionary with signed request data.\n\nFor example, if you need to assign user to some local Django group, you could\nspecify the group name on the client side (add it to the ``extra`` dictionary)\nand based on that, add the user to the group in the callback.\n\nThe callback is a path qualifier of the callback function. Considering the\nexample above, it would be ``my_app.ska_callbacks.my_callback``.\n\nPrefix names of each callback variable with ``SKA_`` in your projects' settings\nmodule.\n\nExample:\n\n.. code-block:: python\n\n SKA_USER_GET_CALLBACK = 'my_app.ska_callbacks.my_get_callback'\n SKA_USER_CREATE_CALLBACK = 'my_app.ska_callbacks.my_create_callback'\n\nSender side\n^^^^^^^^^^^\nSender is the host (another Django web site) from which users authenticate to\nthe Recipient using signed URLs.\n\nOn the sender side, the only thing necessary to be present is the ``ska``\nmodule for Django and of course the same ``SECRET_KEY`` as on the server side.\nFurther, the server ``ska`` login URL (in our case \"/ska/login/\") shall be\nsigned using ``ska`` (for example, using ``sign_url`` function). The\n``auth_user`` param would be used as a Django username. See the example below.\n\n.. code-block:: python\n\n from ska import sign_url\n from ska.contrib.django.ska.settings import SECRET_KEY\n\n server_ska_login_url = 'https://server-url.com/ska/login/'\n\n signed_url = sign_url(\n auth_user='test_ska_user_0',\n secret_key=SECRET_KEY,\n url=server_ska_login_url,\n extra={\n 'email': 'john.doe@mail.example.com',\n 'first_name': 'John',\n 'last_name': 'Doe',\n }\n )\n\nNote, that you ``extra`` dictionary is optional! If ``email``, ``first_name``\nand ``last_name`` keys are present, upon successful validation, the data\nwould be saved into users' profile.\n\nPut this code, for instance, in your view and then make the generated URL\navailable in template context and render it as a URL so that user can click\non it for authenticating to the server.\n\n.. code-block:: python\n\n def auth_to_server(request, template_name='auth_to_server.html'):\n # Some code + obtaining the `signed_url` (code shown above)\n context = {'signed_url': signed_url}\n\n return render(request, template_name, context)\n\nSkaAuthenticationConstanceBackend\n+++++++++++++++++++++++++++++++++\nRelies on dynamic settings functionality provided by\n`django-constance <https://django-constance.readthedocs.io>`_.\n\n*Only differences with `SkaAuthenticationBackend` are mentioned.*\n\n.. note::\n\n Additional requirements shall be installed. See the `constance.txt\n <https://github.com/barseghyanartur/ska/blob/main/examples/requirements/constance.txt>`_\n file for additional requirements (``django-constance``,\n ``django-json-widget``, ``django-picklefield``, ``jsonfield2`` and\n ``redis``).\n\nsettings.py\n^^^^^^^^^^^\n\n.. code-block:: python\n\n AUTHENTICATION_BACKENDS = (\n 'ska.contrib.django.ska.backends.constance_backend.SkaAuthenticationConstanceBackend',\n 'django.contrib.auth.backends.ModelBackend',\n )\n\n INSTALLED_APPS = (\n # ...\n 'constance', # django-constance\n 'ska.contrib.django.ska',\n 'django_json_widget', # For nice admin JSON widget\n # ...\n )\n\n CONSTANCE_CONFIG = {\n 'SKA_PROVIDERS': (\n {}, # The default value\n 'JSON data', # Help text in admin\n 'JSONField_config', # Field config\n )\n }\n\n CONSTANCE_ADDITIONAL_FIELDS = {\n 'JSONField_config': [\n # `jsonfield2` package might be used for storing the JSON field,\n # however, at the moment of writing it has a bug which makes\n # the JSON invalid after the first save. To avoid that, it has\n # been patched and resides in examples/simple/jsonfield2_addons/\n # module.\n 'jsonfield2_addons.forms.JSONField',\n {\n 'widget': 'django_json_widget.widgets.JSONEditorWidget',\n }\n ],\n }\n\n CONSTANCE_BACKEND = 'constance.backends.redisd.RedisBackend'\n\n CONSTANCE_REDIS_CONNECTION = {\n 'host': 'localhost',\n 'port': 6379,\n 'db': 0,\n }\n\n.. note::\n\n In very tiny bits, although not required, the\n `jsonfield2 <https://pypi.org/project/jsonfield2/>`_ and\n `django-json-widget <https://pypi.org/project/django-json-widget/>`_\n packages are used for editing of the ``SKA_PROVIDERS`` setting in Django\n admin.\n\n.. note::\n\n In the example shown above, the ``RedisBackend`` of ``django-constance``\n is used. You could also use ``DatabaseBackend``. Study the\n `documentation <https://django-constance.readthedocs.io/en/latest/backends.html>`_\n for more.\n\n.. note::\n\n If your `SKA_PROVIDERS` settings are stored in the constance as ``str``\n instead of ``dict``, set the setting\n ``SKA_CONSTANCE_SETTINGS_PARSE_FROM_JSON`` to ``True``.\n\nWith ``DatabaseBackend`` it would look as follows:\n\n.. code-block:: python\n\n CONSTANCE_BACKEND = 'constance.backends.database.DatabaseBackend'\n\n INSTALLED_APPS = (\n # ...\n 'constance.backends.database',\n # ...\n )\n\n**Quick demo of the dynamic backend**\n\n- Clone this project:\n\n.. code-block:: sh\n\n git clone git@github.com:barseghyanartur/ska.git\n\n- Install/migrate:\n\n.. code-block:: sh\n\n ./scripts/install.sh\n pip install -r examples/requirements/django_2_1.txt\n ./scripts/migrate.sh --settings=settings.constance_settings\n\n- Run:\n\n.. code-block:: sh\n\n ./scripts/runserver.sh --settings=settings.constance_settings\n\n- Go to `http://localhost:8000/admin/constance/config/\n <http://localhost:8000/admin/constance/config/>`_.\n\n- Paste the following code:\n\n.. code-block:: javascript\n\n {\n \"client_1.users\":{\n \"SECRET_KEY\":\"client-1-users-secret-key\"\n },\n \"client_1.power_users\":{\n \"SECRET_KEY\":\"client-1-power-users-secret-key\",\n \"USER_CREATE_CALLBACK\":\"foo.ska_callbacks.client1_power_users_create\"\n },\n \"client_1.admins\":{\n \"SECRET_KEY\":\"client-1-admins-secret-key\",\n \"USER_CREATE_CALLBACK\":\"foo.ska_callbacks.client1_admins_create\",\n \"USER_GET_CALLBACK\":\"foo.ska_callbacks.client1_admins_get\",\n \"USER_INFO_CALLBACK\":\"foo.ska_callbacks.client1_admins_info_constance\",\n \"REDIRECT_AFTER_LOGIN\":\"/admin/auth/user/\"\n }\n }\n\n- Open `http://localhost:8000/foo/authenticate/\n <http://localhost:8000/foo/authenticate/>`_ in another browser and navigate\n to the ``Log in - client_1.admins`` link in the ``Success`` table column of\n the ``By provider`` section. Upon clicking, you should be logged in.\n You have used the dynamic settings.\n\nurls.py\n^^^^^^^\n``django-constance`` specific views and urls are used. See\n`ska.contrib.django.ska.views.constance_views\n<https://github.com/barseghyanartur/ska/blob/main/src/ska/contrib/django/ska/views/constance_views.py>`_\nand `ska.contrib.django.ska.urls.constance_urls\n<https://github.com/barseghyanartur/ska/blob/main/src/ska/contrib/django/ska/urls/constance_urls.py>`_\nfor the reference.\n\n.. code-block:: python\n\n urlpatterns = [\n url(r'^ska/', include('ska.contrib.django.ska.urls.constance_urls')),\n url(r'^admin/', include(admin.site.urls)),\n ]\n\nCustom authentication backend\n+++++++++++++++++++++++++++++\nTo implement alternative authentication backend, see the following example:\n\n.. code-block:: python\n\n from constance import config\n\n from ska.contrib.django.backends import BaseSkaAuthenticationBackend\n\n class SkaAuthenticationConstanceBackend(BaseSkaAuthenticationBackend):\n \"\"\"Authentication backend.\"\"\"\n\n def get_settings(self):\n \"\"\"\n\n :return:\n \"\"\"\n return config.SKA_PROVIDERS\n\nThat's it. The only thing the ``get_settings`` method shall return is ``dict``\nwith providers data (see the `Multiple secret keys`_ for the reference;\nreturn value of the ``get_settings` is ``SKA_PROVIDERS`` dict).\n\nPurging of old signature data\n+++++++++++++++++++++++++++++\nIf you have lots of visitors and the ``SKA_DB_STORE_SIGNATURES`` set to True,\nyour database grows. If you wish to get rid of old signature token data, you\nmay want to execute the following command using a cron job.\n\n.. code-block:: sh\n\n ./manage.py ska_purge_stored_signature_data\n\nSecurity notes\n++++++++++++++\nFrom point of security, you should be serving the following pages via HTTP\nsecure connection:\n\n- The server login page (/ska/login/).\n- The client page containing the authentication links.\n\nDjango REST Framework integration\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nPermission classes\n++++++++++++++++++\nFor protecting views without actually being authenticated into the system,\nspecific permission classes are implemented (for both plan settings and\nprovider settings, as well as both plain- and provider-settings work in\ncombination with ``django-constance`` package).\n\nThe following permission classes are implemented:\n\n- ``SignedRequestRequired``\n- ``ProviderSignedRequestRequired``\n- ``ConstanceSignedRequestRequired``\n- ``ConstanceProviderSignedRequestRequired``\n\n**ProviderSignedRequestRequired example**\n\n.. code-block:: python\n\n from rest_framework.viewsets import ModelViewSet\n\n from ska.contrib.django.ska.integration.drf.permissions import (\n ProviderSignedRequestRequired\n )\n\n from .models import FooItem\n from .serializers import FooItemSerializer\n\n class FooItemViewSet(ModelViewSet):\n \"\"\"FooItem model viewset.\"\"\"\n\n permission_classes = (ProviderSignedRequestRequired,)\n queryset = FooItem.objects.all()\n serializer_class = FooItemSerializer\n\n**Signing requests**\n\nRequests are signed the same way. Sample code:\n\n.. code-block:: python\n\n # Given that we have `auth_user`, `auth_user_email`, `provider_name`\n # (and the rest), the code would look as follows:\n\n from ska import sign_url\n from ska.defaults import DEFAULT_PROVIDER_PARAM\n\n extra = {\n 'email': auth_user_email,\n 'first_name': first_name,\n 'last_name': last_name,\n }\n\n if provider_name:\n extra.update({DEFAULT_PROVIDER_PARAM: provider_name})\n\n signed_url = sign_url(\n auth_user=auth_user,\n secret_key=secret_key,\n url=url,\n extra=extra\n )\n\nJWT tokens for authentication\n+++++++++++++++++++++++++++++\nFor obtaining JWT tokens for authentication. Also works with\n``django-constance``.\n\n**settings example**\n\n.. code-block:: python\n\n REST_FRAMEWORK = {\n 'DEFAULT_AUTHENTICATION_CLASSES': (\n 'rest_framework_jwt.authentication.JSONWebTokenAuthentication',\n 'rest_framework.authentication.SessionAuthentication',\n 'rest_framework.authentication.BasicAuthentication',\n ),\n }\n\n**urls example**\n\n.. code-block:: python\n\n urlpatterns = [\n # ...\n url(\n r'^ska-rest/',\n include('ska.contrib.django.ska.integration.drf.urls.jwt_token')\n ),\n ]\n\n**Sample request**\n\n.. code-block:: text\n\n http://localhost:8008/ska-rest/obtain-jwt-token/\n ?signature=P92KWDDe0U84Alvu0tvmYoi8e8s%3D\n &auth_user=test_ska_user\n &valid_until=1548195246.0\n &extra=email%2Cfirst_name%2Clast_name\n &email=test_ska_user%40mail.example.com\n &first_name=John\n &last_name=Doe\n\n**Sample response**\n\n.. code-block:: text\n\n HTTP 200 OK\n Allow: GET, HEAD, OPTIONS\n Content-Type: application/json\n Vary: Accept\n\n.. code-block:: javascript\n\n {\n \"token\": \"eyJ0eXAiO.eyJ1c2VyX2lkIjo.m_saOvyKBO3\"\n }\n\nTesting\n=======\nSimply type:\n\n.. code-block:: sh\n\n pytest\n\nOr use tox:\n\n.. code-block:: sh\n\n tox\n\nOr use tox to check specific env:\n\n.. code-block:: sh\n\n tox -e py39\n\nOr run Django tests:\n\n.. code-block:: sh\n\n python examples/simple/manage.py test ska --settings=settings.testing\n\nWriting documentation\n=====================\nKeep the following hierarchy.\n\n.. code-block:: text\n\n =====\n title\n =====\n\n header\n ======\n\n sub-header\n ----------\n\n sub-sub-header\n ~~~~~~~~~~~~~~\n\n sub-sub-sub-header\n ++++++++++++++++++\n\n sub-sub-sub-sub-header\n ^^^^^^^^^^^^^^^^^^^^^^\n\n sub-sub-sub-sub-sub-header\n **************************\n\nLicense\n=======\nGPL-2.0-only OR LGPL-2.1-or-later\n\nSupport\n=======\nFor any issues contact me at the e-mail given in the `Author`_ section.\n\nAuthor\n======\nArtur Barseghyan <artur.barseghyan@gmail.com>\n",
"bugtrack_url": null,
"license": "GPL-2.0-only OR LGPL-2.1-or-later",
"summary": "Sign- and validate- data (dictionaries, strings) using symmetric-key algorithm.",
"version": "1.10",
"project_urls": {
"Bug Tracker": "https://github.com/barseghyanartur/ska/issues",
"Changelog": "https://ska.readthedocs.io/en/latest/changelog.html",
"Documentation": "https://ska.readthedocs.io/",
"Homepage": "https://github.com/barseghyanartur/ska",
"Source Code": "https://github.com/barseghyanartur/ska"
},
"split_keywords": [
"sign data",
"sign (http) request",
"symmetric-key algorithm encryption",
"sign url",
"python",
"django",
"password-less login django",
"password-less authentication backend django"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "29ce055b3f8f8cec7212fba54f0d7c694c7da5f64088a76be20c72cb3f008ede",
"md5": "d8037baf37a5bf24d95eb18374effc9f",
"sha256": "c92194ccaec0eb92b393a8d27a1efb7ab9dd62afadbb489f176a7eb64b351b1a"
},
"downloads": -1,
"filename": "ska-1.10-py2.py3-none-any.whl",
"has_sig": false,
"md5_digest": "d8037baf37a5bf24d95eb18374effc9f",
"packagetype": "bdist_wheel",
"python_version": "py2.py3",
"requires_python": null,
"size": 96008,
"upload_time": "2023-08-26T23:32:19",
"upload_time_iso_8601": "2023-08-26T23:32:19.890825Z",
"url": "https://files.pythonhosted.org/packages/29/ce/055b3f8f8cec7212fba54f0d7c694c7da5f64088a76be20c72cb3f008ede/ska-1.10-py2.py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "67229789fade2645882af81174ca1adafdac40a01cddc6492a4a772ae254db33",
"md5": "f8242a9f407ecfc319cee045afcba2bb",
"sha256": "7ac0be5d5dde47e60e01d6e375cd3b351280f97d2a49aeb8aac8320a94854e4e"
},
"downloads": -1,
"filename": "ska-1.10.tar.gz",
"has_sig": false,
"md5_digest": "f8242a9f407ecfc319cee045afcba2bb",
"packagetype": "sdist",
"python_version": "source",
"requires_python": null,
"size": 92940,
"upload_time": "2023-08-26T23:32:22",
"upload_time_iso_8601": "2023-08-26T23:32:22.726939Z",
"url": "https://files.pythonhosted.org/packages/67/22/9789fade2645882af81174ca1adafdac40a01cddc6492a4a772ae254db33/ska-1.10.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2023-08-26 23:32:22",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "barseghyanartur",
"github_project": "ska",
"travis_ci": false,
"coveralls": true,
"github_actions": true,
"tox": true,
"lcname": "ska"
}