# -*- coding: utf-8 -*-
from setuptools import setup

package_dir = \
{'': 'src'}

packages = \
['sesame']

package_data = \
{'': ['*']}

install_requires = \
['django>=2.2']

extras_require = \
{'ua': ['ua-parser>=0.10']}

setup_kwargs = {
    'name': 'django-sesame',
    'version': '2.3',
    'description': 'Frictionless authentication with "Magic Links" for your Django project.',
    'long_description': '.. image:: logo/horizontal.svg\n   :width: 400px\n   :alt: django-sesame\n\n#############\ndjango-sesame\n#############\n\n`django-sesame`_ provides frictionless authentication with "Magic Links" for\nyour Django project.\n\n.. _django-sesame: https://github.com/aaugustin/django-sesame\n\nIt generates URLs containing authentication tokens such as:\nhttps://example.com/?sesame=zxST9d0XT9xgfYLvoa9e2myN\n\nThen it authenticates users based on tokens found in URLs.\n\nTable of contents\n=================\n\n* `Use cases`_\n* `(In)security`_\n* `User guide`_\n\n  * `Requirements`_\n  * `Getting started`_\n  * `Generating URLs`_\n  * `Tokens lifecycle`_\n  * `Per-view authentication`_\n  * `Scoped tokens`_\n  * `Override expiration`_\n\n* `Advanced topics`_\n\n  * `Safari issues`_\n  * `Tokens security`_\n  * `Custom primary keys`_\n  * `Stateless authentication`_\n\n* `Infrequently asked questions`_\n* `Contributing`_\n* `Changelog`_\n\nUse cases\n=========\n\nKnown use cases for django-sesame include:\n\n1. Login by email, an increasingly attractive option on mobile where\n   typing passwords is uncomfortable. This technique is prominently\n   deployed by Slack.\n\n   If you\'re doing this, you should define a small ``SESAME_MAX_AGE``, perhaps\n   10 minutes.\n\n2. Authenticated links, typically if you\'re generating a report offline, then\n   emailing a link to access it when it\'s ready. An authenticated link works\n   even if the user isn\'t logged in on the device where they\'re opening it.\n\n   Likewise, you should configure an appropriate ``SESAME_MAX_AGE``, probably\n   no more than a few days.\n\n   Since emails may be forwarded, authenticated links shouldn\'t log the user\n   in. They should only allow access to specific views, as described in\n   `Per-view authentication`_.\n\n3. Sharing links, which are a variant of authenticated links. When a user\n   shares content with a guest, you can create a phantom account for the guest\n   and generate an authenticated link tied to that account.\n\n   Email forwarding is even more likely in this context. If you\'re doing this,\n   make sure authenticated links don\'t log the user in.\n\n4. Non-critical private websites, for example for a family or club site,\n   where users don\'t expect to manage a personal account with a password.\n   Authorized users can bookmark personalized authenticated URLs.\n\n   Here you can rely on the default settings because that\'s the original —\n   and, admittedly, niche — use case for which django-sesame was built.\n\n(In)security\n============\n\n.. warning::\n\n    **Before using django-sesame in your project, please review the following\n    advice carefully.** (Also, please don\'t use security-sensitive libraries\n    published by strangers on the Internet without checking what they do.)\n\nThe major security weakness in django-sesame is a direct consequence of the\nfeature it implements: **whoever obtains an authentication token will be able\nto authenticate to your website.**\n\nURLs end up in countless insecure places: emails, referer headers, proxy logs,\nbrowser history, etc. You can\'t avoid that. At best you can mitigate it by\ncreating short-lived or single-use tokens, as described below.\n\nOtherwise, a reasonable attempt was made to provide a secure solution. Tokens\nare secured with modern cryptography. There are configurable options for token\nexpiration and invalidation.\n\nUser guide\n==========\n\nRequirements\n------------\n\ndjango-sesame is tested with:\n\n- Django 2.2 (LTS), 3.0, and 3.1;\n- Python ≥ 3.6\n\nIt builds upon ``django.contrib.auth``. It supports custom user models,\nprovided they have ``password`` and ``last_login`` fields. Most custom user\nmodels inherit these fields from ``AbstractBaseUser``.\n\ndjango-sesame is released under the BSD license, like Django itself.\n\nGetting started\n---------------\n\n1. Install django-sesame:\n\n   .. code:: bash\n\n    $ pip install django-sesame[ua]\n\n   The ``ua`` extra is optional. See `Safari issues`_ for details.\n\n2. Add ``"sesame.backends.ModelBackend"`` to ``AUTHENTICATION_BACKENDS``:\n\n   .. code:: python\n\n    AUTHENTICATION_BACKENDS += ["sesame.backends.ModelBackend"]\n\n3. Add ``"sesame.middleware.AuthenticationMiddleware"`` to ``MIDDLEWARE``:\n\n   .. code:: python\n\n    MIDDLEWARE += ["sesame.middleware.AuthenticationMiddleware"]\n\n   The best position for ``sesame.middleware.AuthenticationMiddleware`` is\n   just after ``django.contrib.auth.middleware.AuthenticationMiddleware``.\n\nGenerating URLs\n---------------\n\ndjango-sesame provides functions to generate authenticated URLs in the\n``sesame.utils`` module.\n\nLoad a user from the database:\n\n.. code:: pycon\n\n    >>> from django.contrib.auth import get_user_model\n    >>> User = get_user_model()\n    >>> user = User.objects.first()\n\nNow you can create a query string that you can append to any URL to enable\none-click login:\n\n.. code:: pycon\n\n    >>> from sesame.utils import get_query_string\n    >>> get_query_string(user)\n    \'?sesame=zxST9d0XT9xgfYLvoa9e2myN\'\n\nYou can also obtain a ``dict`` of URL parameters rather than ready-to-use\nquery string:\n\n.. code:: pycon\n\n    >>> from sesame.utils import get_parameters\n    >>> get_parameters(user)\n    {\'sesame\': \'zxST9d0XT9xgfYLvoa9e2myN\'}\n\nThen you can add other URL parameters to this ``dict`` before serializing it\nto a query string.\n\nFinally, here\'s how to get only the token:\n\n.. code:: pycon\n\n    >>> from sesame.utils import get_token\n    >>> get_token(user)\n    \'zxST9d0XT9xgfYLvoa9e2myN\'\n\nShare the resulting URLs with your users though an adequately confidential\nchannel for your use case.\n\nBy default, the URL parameter is named ``sesame``. You can change this with\nthe ``SESAME_TOKEN_NAME`` setting. Make sure that it doesn\'t conflict with\nother query string parameters used by your application.\n\n*Changed in 2.0:* the URL parameter used to be named ``url_auth_token``.\n\nTokens lifecycle\n----------------\n\nBy default, tokens don\'t expire but are tied to the password of the user.\nChanging the password invalidates the token. When the authentication backend\nuses salted passwords — that\'s been the default in Django for a long time —\nthe token is invalidated even if the new password is identical to the old one.\n\nIf you want tokens to expire after a given amount of time, set the\n``SESAME_MAX_AGE`` setting to a duration in seconds or a\n``datetime.timedelta``. Then each token will contain the time it was generated\nat and django-sesame will check if it\'s still valid at each login attempt.\n\nIf you want tokens to be usable only once, set the ``SESAME_ONE_TIME`` setting\nto ``True``. Then tokens are valid only if the last login date hasn\'t changed\nsince they were generated. Since logging in changes the last login date, such\ntokens are usable at most once. If you\'re intending to send links by email, be\naware that some email providers scan links for security reasons, which\nconsumes single-use tokens prematurely. Tokens with a short expiry are more\nreliable.\n\nIf you don\'t want tokens to be invalidated by password changes, set the\n``SESAME_INVALIDATE_ON_PASSWORD_CHANGE`` setting to ``False``. **This is\ndiscouraged because it becomes impossible to invalidate a single token.** Your\nonly option if a token is compromised is to invalidate all tokens at once. If\nyou\'re doing it anyway, you should set ``SESAME_MAX_AGE`` to a short value to\nminimize risks. This option may be useful for generating tokens during a\nsign up process, when you don\'t know if the token will be used before or after\ninitializing the password.\n\nFinally, if the ``is_active`` attribute of a user is set to ``False``,\ndjango-sesame rejects authentication tokens for this user.\n\nTokens must be verified with the same settings that were used for generating\nthem. Changing settings invalidates previously generated tokens. The only\nexception to this rule is ``SESAME_MAX_AGE``: as long as it isn\'t ``None``,\nyou can change its value and the new value will apply even to previously\ngenerated tokens.\n\nPer-view authentication\n-----------------------\n\nThe configuration described in `Getting started`_ enables a middleware that\nlooks for a token in every request and, if there is a valid token, logs the\nuser in. It\'s as if they had submitted their username and password in a login\nform. This provides compatibility with APIs like the ``login_required``\ndecorator and the ``LoginRequired`` mixin.\n\nSometimes this behavior is too blunt. For example, you may want to build a\nMagic Link that gives access to a specific view but doesn\'t log the user in\npermanently.\n\nTo achieve this, remove ``"sesame.middleware.AuthenticationMiddleware"`` from\nthe ``MIDDLEWARE`` setting and authenticate the user with django-sesame in a\nview as follows:\n\n.. code:: python\n\n    from django.core.exceptions import PermissionDenied\n    from django.http import HttpResponse\n\n    from sesame.utils import get_user\n\n    def hello(request):\n        user = get_user(request)\n        if user is None:\n            raise PermissionDenied\n        return HttpResponse("Hello {}!".format(user))\n\nWhen ``get_user()`` returns ``None``, it means that the token was missing,\ninvalid, expired, or that the user account is inactive. Then you can show an\nappropriate error message or redirect to a login form.\n\nWhen ``SESAME_ONE_TIME`` is enabled, ``get_user()`` updates the user\'s last\nlogin date in order to invalidate the token. When ``SESAME_ONE_TIME`` isn\'t\nenabled, it doesn\'t, because making a database write for every call to\n``get_user()`` could degrade performance. You can override this behavior with\nthe ``update_last_login`` keyword argument:\n\n.. code:: python\n\n    get_user(request, update_last_login=True)   # always update last_login\n    get_user(request, update_last_login=False)  # never update last_login\n\n``get_user()`` is a thin wrapper around the low-level ``authenticate()``\nfunction from ``django.contrib.auth``. It\'s also possible to verify an\nauthentication token directly with  ``authenticate()``. To do so, the\n``sesame.backends.ModelBackend`` authentication backend expects an\n``sesame`` argument:\n\n.. code:: python\n\n    from django.contrib.auth import authenticate\n\n    user = authenticate(sesame=...)\n\n*Changed in 2.0:* the argument used to be named ``url_auth_token``.\n\nIf you decide to use ``authenticate()`` instead of ``get_user()``, you must\nupdate ``user.last_login`` to invalidate one-time tokens. Indeed, in\n``django.contrib.auth``, ``authenticate()`` is a low-level function. The\ncaller, usually the higher-level ``login()`` function, is responsible for\nupdating ``user.last_login``.\n\nScoped tokens\n-------------\n\nIf your application uses tokens for multiple purposes, you should prevent a\ntoken created from one purpose from being reused for another purpose.\n\nAdd a ``scope`` to generate authenticated URLs valid only in that scope:\n\n.. code:: pycon\n\n    >>> from sesame.utils import get_query_string\n    >>> get_query_string(user, scope="sharing")\n    \'?sesame=jISWHmrXr4zg8FHVZZuxhpHs\'\n\nSimilar to ``get_query_string()``, ``get_parameters()`` and ``get_token()``\naccept an optional ``scope`` argument. ``scope`` must be a string.\n\nThen you can verify the token with the same scope:\n\n.. code:: python\n\n    from sesame.utils import get_user\n\n    def share(request):\n        user = get_user(request, scope="sharing")\n        if user is None:\n            raise PermissionDenied\n        ...\n\nIf the scope doesn\'t match, the token is invalid and ``get_user()`` returns\n``None``. ``get_user()`` is the only way to verify a scoped token.\n\nThe default scope is ``""``. ``"sesame.middleware.AuthenticationMiddleware"``\nconsiders a token generated with a non-default scope to be invalid and doesn\'t\nlog the user in, even if the token is valid in that scope.\n\nOverride expiration\n-------------------\n\nIf you have several use cases inside the same application and they require\ndifferent expiry durations, you can override ``SESAME_MAX_AGE``:\n\n.. code:: python\n\n    from sesame.utils import get_user\n\n    def recover(request):\n        user = get_user(request, max_age=120)\n        if user is None:\n            raise PermissionDenied\n        ...\n\nThis doesn\'t work when ``SESAME_MAX_AGE`` is ``None`` — because tokens don\'t\ncontain a timestamp in that case. In other words, changing the expiry duration\nis supported, but switching between expiring and non-expiring tokens isn\'t.\n\nAdvanced topics\n===============\n\nSafari issues\n-------------\n\nThe django-sesame middleware removes the token from the URL with a HTTP 302\nRedirect after authenticating a user successfully. Unfortunately, in some\nscenarios, this triggers Safari\'s "Protection Against First Party Bounce\nTrackers". In that case, Safari clears cookies and the user is logged out.\n\nTo avoid this problem, django-sesame doesn\'t perform the redirect when it\ndetects that the browser is Safari. This relies on the ua-parser package,\nwhich is an optional dependency. If it isn\'t installed, django-sesame always\nredirects.\n\nTokens security\n---------------\n\ndjango-sesame builds authentication tokens as follows:\n\n- Encode the primary key of the user for which they were generated;\n- Assemble a revocation key which will be used for invalidating tokens;\n- If ``SESAME_MAX_AGE`` is enabled, encode the token generation timestamp;\n- Add a message authentication code (MAC) to prevent tampering with the token.\n\nThe revocation key is derived from:\n\n- The password of the user, unless ``SESAME_INVALIDATE_ON_PASSWORD_CHANGE`` is\n  disabled;\n- The last login date of the user, if ``SESAME_ONE_TIME`` is enabled.\n\nPrimary keys are in clear text. If this is a concern, you can write a custom\npacker to encrypt them. See `Custom primary keys`_ for details.\n\ndjango-sesame provides two token formats:\n\n- v1 is the original format, which still works as designed;\n- v2 is a better, cleaner, faster design that produces shorter tokens.\n\nThe ``SESAME_TOKENS`` setting lists supported formats in order of decreasing\npreference. The first item defines the format of newly created tokens. Other\nitems define other acceptable formats, if any.\n\n``SESAME_TOKENS`` defaults to ``["sesame.tokens_v2", "sesame.tokens_v1"]``\nwhich means "generate tokens v2, accept tokens v2 and v1".\n\nTokens v2\n.........\n\nThey contain a primary key, an optional timestamp, and a signature.\n\nThe signature covers the primary key, the optional timestamp, and the\nrevocation key. If the revocation key changes, the signature becomes invalid.\nAs a consequence, there\'s no need to include the revocation key in tokens.\n\nThe signature algorithm is Blake2 in keyed mode. A unique key is derived by\nhashing the ``SECRET_KEY`` setting and relevant ``SESAME_*`` settings.\n\nBy default the signature length is 10 bytes. You can adjust it to any value\nbetween 1 and 64 bytes with the ``SESAME_SIGNATURE_SIZE`` setting.\n\nIf you need to invalidate all tokens, set the ``SESAME_KEY`` setting to a new\nvalue. This will change the unique key and, as a consequence, invalidate all\nsignatures.\n\nTokens v1\n.........\n\nTokens v1 contain a primary key and a revocation key, plus an optional\ntimestamp and a signature generated by Django\'s built-in ``Signer`` or\n``TimestampSigner``.\n\nThe signature algorithm is HMAC-SHA1.\n\nIf you need to invalidate all tokens, you can set the ``SESAME_SALT`` setting\nto a new value. This will change all signatures.\n\nCustom primary keys\n-------------------\n\nWhen generating a token for a user, django-sesame stores the primary key of\nthat user in the token. In order to keep tokens short, django-sesame creates\ncompact binary representations of primary keys, according to their type.\n\nIf you\'re using integer or UUID primary keys, you\'re fine. If you\'re using\nanother type of primary key, for example a string created by a unique ID\ngeneration algorithm, the default representation may be suboptimal.\n\nFor example, let\'s say primary keys are strings containing 24 hexadecimal\ncharacters. The default packer represents them with 25 bytes. You can reduce\nthem to 12 bytes with this custom packer:\n\n.. code:: python\n\n    from sesame.packers import BasePacker\n\n    class Packer(BasePacker):\n\n        @staticmethod\n        def pack_pk(user_pk):\n            assert len(user_pk) == 24\n            return bytes.fromhex(user_pk)\n\n        @staticmethod\n        def unpack_pk(data):\n            return data[:12].hex(), data[12:]\n\nThen, set the ``SESAME_PACKER`` setting to the dotted Python path to your\ncustom packer class.\n\nFor details, read ``help(BasePacker)`` and look at built-in packers defined in\nthe ``sesame.packers`` module.\n\nStateless authentication\n------------------------\n\nTheoretically, django-sesame can provide stateless authenticated navigation\nwithout ``django.contrib.sessions``, provided all internal links include the\nauthentication token. That increases the security concerns and it\'s unclear\nthat it meets any practical use case.\n\nIn a scenario where ``django.contrib.sessions.middleware.SessionMiddleware``\nand ``django.contrib.auth.middleware.AuthenticationMiddleware`` aren\'t\nenabled, ``sesame.middleware.AuthenticationMiddleware`` still sets\n``request.user`` to the currently logged-in user or ``AnonymousUser()``.\n\nInfrequently asked questions\n============================\n\n**Is django-sesame usable without passwords?**\n\nYes, it is.\n\nYou should call ``user.set_unusable_password()`` when you create users.\n\n**How do I understand why a token is invalid?**\n\nEnable debug logs by setting the ``sesame`` logger to the ``DEBUG`` level.\n\n.. code:: python\n\n    import logging\n    logger = logging.getLogger("sesame")\n    logger.setLevel(logging.DEBUG)\n    logger.addHandler(logging.StreamHandler())\n\nThen you should get a hint in logs.\n\nDepending on how logging is set up in your project, there may by another way\nto enable this configuration.\n\n**Why does upgrading Django invalidate tokens?**\n\nEach release of Django increases the work factor of password hashers. After\ndeploying a new version of Django, when a user logs in with their password,\nDjango upgrades the password hash. This invalidates the user\'s token.\n\nThis problem occurs only when a user logs in alternatively with a long-lived\ntoken and with a password, which isn\'t frequent in practice. If you\'re facing\nit, you should regenerate and redistribute tokens after upgrading Django.\n\nOther workarounds, such as disabling token invalidation on password change or\nusing a custom hasher to keep the work factor constant, are discouraged\nbecause they create security concerns.\n\nContributing\n============\n\nPrepare a development environment:\n\n* Install Poetry_.\n* Run ``poetry install --extras ua``.\n* Run ``poetry shell`` to load the development environment.\n\nMake changes:\n\n* Make changes to the code, tests, or docs.\n* Run ``make style`` and fix any flake8 violations.\n* Run ``make test`` or ``make coverage`` to run the set suite — it\'s fast!\n\nIterate until you\'re happy.\n\nCheck quality and submit your changes:\n\n* Install tox_.\n* Run ``tox`` to test across Python and Django versions — it\'s quite slow.\n* Submit a pull request.\n\n.. _Poetry: https://python-poetry.org/\n.. _tox: https://tox.readthedocs.io/\n\nChangelog\n=========\n\n2.3\n---\n\n* Support overriding max_age. This feature is only available for v2 tokens.\n\n2.2\n---\n\n* Fixed crash on truncated v2 tokens.\n\n2.1\n---\n\n* Added scoped tokens. This feature is only available for v2 tokens.\n\n2.0\n---\n\n* Introduced a faster and shorter token format (v2). The previous format (v1)\n  is still supported. See `Tokens security`_.\n* Added the ``get_token()`` function to generate a token.\n* **Backwards-incompatible** Changed the default URL parameter to ``sesame``.\n  If you need to preserve existing URLs, you can set\n  ``SESAME_TOKEN_NAME = "url_auth_token"``.\n* **Backwards-incompatible** Changed the argument expected by\n  ``authenticate()`` to ``sesame``. You\'re affected only if you\'re explicitly\n  calling ``authenticate(url_auth_token=...)``. If so, change this call to\n  ``authenticate(sesame=...)``.\n* ``SESAME_MAX_AGE`` can be a ``datetime.timedelta``.\n* Improved documentation.\n\n1.8\n---\n\n* Added compatibility with custom user models with most types of primary keys,\n  including ``BigAutoField``, ``SmallAutoField``, other integer fields,\n  ``CharField`` and ``BinaryField``.\n* Added the ability to customize how primary keys are stored in tokens.\n* Added compatibility with Django ≥ 3.0.\n\n1.7\n---\n\n* Fixed invalidation of one-time tokens in ``get_user()``.\n\n1.6\n---\n\n* Fixed detection of Safari on iOS.\n\n1.5\n---\n\n* Added support for single use tokens with the ``SESAME_ONE_TIME`` setting.\n* Added support for not invalidating tokens on password change with the\n  ``SESAME_INVALIDATE_ON_PASSWORD_CHANGE`` setting.\n* Added compatibility with custom user models where the primary key is a\n  ``UUIDField``.\n* Added the ``get_user()`` function to obtain a user instance from a request.\n* Improved error message for preexisting tokens when changing the\n  ``SESAME_MAX_AGE`` setting.\n* Fixed authentication on Safari by disabling the redirect which triggers ITP.\n\n1.4\n---\n\n* Added a redirect to the same URL with the query string parameter removed.\n\n1.3\n---\n\n* Added compatibility with Django ≥ 2.0.\n\n1.2\n---\n\n* Added the ability to rename the query string parameter with the\n  ``SESAME_TOKEN_NAME`` setting.\n* Added compatibility with Django ≥ 1.8.\n\n1.1\n---\n\n* Added support for expiring tokens with the ``SESAME_MAX_AGE`` setting.\n\n1.0\n---\n\n* Initial release.\n',
    'author': 'Aymeric Augustin',
    'author_email': 'aymeric.augustin@m4x.org',
    'maintainer': None,
    'maintainer_email': None,
    'url': 'https://github.com/aaugustin/django-sesame',
    'package_dir': package_dir,
    'packages': packages,
    'package_data': package_data,
    'install_requires': install_requires,
    'extras_require': extras_require,
    'python_requires': '>=3.6,<4.0',
}


setup(**setup_kwargs)
