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

packages = \
['request_token', 'request_token.migrations', 'request_token.templatetags']

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

install_requires = \
['django>=2.2,<4.0', 'pyjwt>=2.0,<3.0']

setup_kwargs = {
    'name': 'django-request-token',
    'version': '0.15',
    'description': 'JWT-backed Django app for managing querystring tokens.',
    'long_description': 'Supported versions\n------------------\n\nAs of v0.10 this project supports Django 2.2 and above, and Python 3.7 and above only on the master branch. For previous verions see the relevant tag / branch.\n\nDjango Request Token\n--------------------\n\nDjango app that uses JWT to manage one-time and expiring tokens to protect URLs.\n\nThis app currently requires the use of PostgreSQL.\n\nBackground\n==========\n\nThis project was borne out of our experiences at YunoJuno with \'expiring links\' -\nwhich is a common use case of providing users with a URL that performs a single\naction, and may bypass standard authentication. A well-known use of of this is\nthe ubiquitous \'unsubscribe\' link you find at the bottom of newsletters. You click\non the link and it immediately unsubscribes you, irrespective of whether you are\nalready authenticated or not.\n\nIf you google "temporary url", "one-time link" or something similar you will find\nlots of StackOverflow articles on supporting this in Django - it\'s pretty obvious,\nyou have a dedicated token url, and you store the tokens in a model - when they\nare used you expire the token, and it can\'t be used again. This works well, but\nit falls down in a number of areas:\n\n* Hard to support multiple endpoints (views)\n\nIf you want to support the same functionality (expiring links) for more than\none view in your project, you either need to have multiple models and token\nhandlers, or you need to store the specific view function and args\nin the model; neither of these is ideal.\n\n* Hard to debug\n\nIf you use have a single token url view that proxies view functions, you need\nto store the function name, args and it then becomes hard to support - when\nsomeone claims that they clicked on example.com/t/<token>, you can\'t tell what\nthat would resolve to without looking it up in the database - which doesn\'t\nwork for customer support.\n\n* Hard to support multiple scenarios\n\nSome links expire, others have usage quotas - some have both. Links may be\nfor use by a single user, or multiple users.\n\nThis project is intended to provide an easy-to-support mechanism for \'tokenising\'\nURLs without having to proxy view functions - you can build well-formed Django\nURLs and views, and then add request token support afterwards.\n\nUse Cases\n=========\n\nThis project supports three core use cases, each of which is modelled using\nthe ``login_mode`` attribute of a request token:\n\n1. Public link with payload\n2. Single authenticated request\n3. Auto-login\n\n**Public Link** (``RequestToken.LOGIN_MODE_NONE``)\n\nIn this mode (the default for a new token), there is no authentication, and no\nassigned user. The token is used as a mechanism for attaching a payload\nto the link. An example of this might be a custom registration or affiliate link,\nthat renders the standard template with additional information extracted from\nthe token - e.g. the name of the affiliate, or the person who invited you to\nregister.\n\n.. code:: python\n\n    # a token that can be used to access a public url, without authenticating\n    # as a user, but carrying a payload (affiliate_id).\n    token = RequestToken.objects.create_token(\n        scope="foo",\n        login_mode=RequestToken.LOGIN_MODE_NONE,\n        data={\n            \'affiliate_id\': 1\n        }\n    )\n\n    ...\n\n    @use_request_token(scope="foo")\n    function view_func(request):\n        # extract the affiliate id from an token _if_ one is supplied\n        affiliate_id = (\n            request.token.data[\'affiliate_id\']\n            if hasattr(request, \'token\')\n            else None\n        )\n\n\n**Single Request** (``RequestToken.LOGIN_MODE_REQUEST``)\n\nIn Request mode, the request.user property is overridden by the user specified\nin the token, but only for a single request. This is useful for responding to\na single action (e.g. RSVP, unsubscribe). If the user then navigates onto another\npage on the site, they will not be authenticated. If the user is already\nauthenticated, but as a different user to the one in the token, then they will\nreceive a 403 response.\n\n.. code:: python\n\n    # this token will identify the request.user as a given user, but only for\n    # a single request - not the entire session.\n    token = RequestToken.objects.create_token(\n        scope="foo",\n        login_mode=RequestToken.LOGIN_MODE_REQUEST,\n        user=User.objects.get(username="hugo")\n    )\n\n    ...\n\n    @use_request_token(scope="foo")\n    function view_func(request):\n        assert request.user == User.objects.get(username="hugo")\n\n**Auto-login** (``RequestToken.LOGIN_MODE_SESSION``)\n\nThis is the nuclear option, and must be treated with extreme care. Using a\nSession token will automatically log the user in for an entire session, giving\nthe user who clicks on the link full access the token user\'s account. This is\nuseful for automatic logins. A good example of this is the email login process\non medium.com, which takes an email address (no password) and sends out a login\nlink.\n\nSession tokens have a default expiry of ten minutes.\n\n.. code:: python\n\n    # this token will log in as the given user for the entire session -\n    # NB use with caution.\n    token = RequestToken.objects.create_token(\n        scope="foo",\n        login_mode=RequestToken.LOGIN_MODE_SESSION,\n        user=User.objects.get(username="hugo")\n    )\n\nImplementation\n==============\n\nThe project contains middleware and a view function decorator that together\nvalidate request tokens added to site URLs.\n\n**request_token.models.RequestToken** - stores the token details\n\nStep 1 is to create a ``RequestToken`` - this has various attributes that can\nbe used to modify its behaviour, and mandatory property - ``scope``. This is a\ntext value - it can be anything you like - it is used by the function decorator\n(described below) to confirm that the token given matches the function being\ncalled - i.e. the ``token.scope`` must match the function decorator scope kwarg:\n\n.. code:: python\n\n    token = RequestToken(scope="foo")\n\n    # this will raise a 403 without even calling the function\n    @use_request_token(scope="bar")\n    def incorrect_scope(request):\n        pass\n\n    # this will call the function as expected\n    @use_request_token(scope="foo")\n    def correct_scope(request):\n        pass\n\nThe token itself - the value that must be appended to links as a querystring\nargument - is a JWT - and comes from the ``RequestToken.jwt()`` method. For example,\nif you were sending out an email, you might render the email as an HTML template\nlike this:\n\n.. code:: html\n\n    {% if token %}\n        <a href="{{url}}?rt={{token.jwt}}>click here</a>\n    {% else %}\n        <a href="{{url}}">click here</a>\n    {% endif %}\n\nIf you haven\'t come across JWT before you can find out more on the `jwt.io <https://jwt.io/>`_ website. The token produced will include the following JWT claims (available as the property ``RequestToken.claims``:\n\n* ``max``: maximum times the token can be used\n* ``sub``: the scope\n* ``mod``: the login mode\n* ``jti``: the token id\n* ``aud``: (optional) the user the token represents\n* ``exp``: (optional) the expiration time of the token\n* ``iat``: (optional) the time the token was issued\n* ``ndf``: (optional) the not-before-time of the token\n\n**request_token.middleware.RequestTokenMiddleware** - decodes and verifies tokens\n\nThe ``RequestTokenMiddleware`` will look for a querystring token value (the argument name defaults to \'rt\' and can overridden using the ``JWT_QUERYSTRING_ARG`` setting), and if it finds one it will verify the token (using the JWT decode verification). If the token is verified, it will fetch the token object from the database and perform additional validation against the token attributes. If the token checks out it is added to the incoming request as a ``token`` attribute. This way you can add arbitrary data (stored on the token) to incoming requests.\n\nIf the token has a user specified, then the ``request.user`` is updated to\nreflect this. The middleware must run after the Django auth middleware, and\nbefore any custom middleware that inspects / monkey-patches the ``request.user``.\n\nIf the token cannot be verified it returns a 403.\n\n**request_token.decorators.use_request_token** - applies token permissions to views\n\nA function decorator that takes one mandatory kwargs (``scope``) and one optional\nkwargs (``required``). The ``scope`` is used to match tokens to view functions -\nit\'s just a straight text match - the value can be anything you like, but if the\ntoken scope is \'foo\', then the corresponding view function decorator scope must\nmatch. The ``required`` kwarg is used to indicate whether the view **must** have\na token in order to be used, or not. This defaults to False - if a token **is**\nprovided, then it will be validated, if not, the view function is called as is.\n\nIf the scopes do not match then a 403 is returned.\n\nIf required is True and no token is provided the a 403 is returned.\n\nInstallation\n============\n\nDownload / install the app using pip:\n\n.. code:: shell\n\n    pip install django-request-token\n\nAdd the app ``request_token`` to your ``INSTALLED_APPS`` Django setting:\n\n.. code:: python\n\n    # settings.py\n    INSTALLED_APPS = (\n        \'django.contrib.admin\',\n        \'django.contrib.auth\',\n        \'django.contrib.contenttypes\',\n        \'django.contrib.sessions\',\n        \'django.contrib.messages\',\n        \'django.contrib.staticfiles\',\n        \'request_token\',\n        ...\n    )\n\nAdd the middleware to your settings, **after** the standard authentication middleware,\nand before any custom middleware that uses the ``request.user``.\n\n.. code:: python\n\n    MIDDLEWARE_CLASSES = [\n        # default django middleware\n        \'django.contrib.sessions.middleware.SessionMiddleware\',\n        \'django.middleware.common.CommonMiddleware\',\n        \'django.middleware.csrf.CsrfViewMiddleware\',\n        \'django.contrib.auth.middleware.AuthenticationMiddleware\',\n        \'django.contrib.messages.middleware.MessageMiddleware\',\n        \'request_token.middleware.RequestTokenMiddleware\',\n    ]\n\nYou can now add ``RequestToken`` objects, either via the shell (or within your\napp) or through the admin interface. Once you have added a ``RequestToken`` you\ncan add the token JWT to your URLs (using the ``jwt()`` method):\n\n.. code:: python\n\n    >>> token = RequestToken.objects.create_token(scope="foo")\n    >>> url = "https://example.com/foo?rt=" + token.jwt()\n\nYou now have a request token enabled URL. You can use this token to protect a\nview function using the view decorator:\n\n.. code:: python\n\n    @use_request_token(scope="foo")\n    function foo(request):\n        pass\n\nNB The \'scope\' argument to the decorator is used to bind the function to the\nincoming token - if someone tries to use a valid token on another URL, this\nwill return a 403.\n\n**NB this currently supports only view functions - not class-based views.**\n\nSettings\n========\n\nSettings are read in from the environment or Django settings:\n\n.. code:: python\n\n    os.getenv(\'SETTING_NAME\') or django.conf.settings.get(\'SETTING_NAME\', default)\n\n* ``REQUEST_TOKEN_QUERYSTRING``\n\nThe querystring argument name used to extract the token from incoming\nrequests, defaults to **rt**.\n\n* ``REQUEST_TOKEN_EXPIRY``\n\nSession tokens have a default expiry interval, specified in minutes.\nThe primary use case (above) dictates that the expiry should be no longer\nthan it takes to receive and open an email, defaults to **10** (minutes).\n\n* ``REQUEST_TOKEN_403_TEMPLATE``\n\nSpecifying the 403-template so that for prettyfying the 403-response,\nin production with a setting like:\n\n.. code:: python\n\n    FOUR03_TEMPLATE = os.path.join(BASE_DIR,\'...\',\'403.html\')\n\n* ``REQUEST_TOKEN_LOG_TOKEN_ERRORS``\n\nIf an ``InvalidTokenError`` is raised by the decorator or middleware, the error\nwill be logged as a ``RequestTokenErrorLog`` object. This makes debugging\neasier, which is important in production as often the first you will know about\na token problem is an angry customer who says "my link doesn\'t work". Being\nable to diagnose issues from the admin site is useful, however if the volume\nor errors is a problem this can be disabled by setting this value to anything\nother than \'True\' or \'1\'.\n\n\nLogging\n=======\n\nDebugging middleware and decorators can be complex, so the project is verbose\nin its logging (by design). If you feel it\'s providing too much logging, you\ncan adjust it by setting the standard Django logging for ``request_token``.\n\nYou can turn off formal logging in the database of token errors by using the\nsetting ``REQUEST_TOKEN_LOG_TOKEN_ERRORS``.\n\nTests\n=====\n\nThere is a set of ``tox`` tests.\n\nLicense\n=======\n\nMIT\n\nContributing\n============\n\nThis is by no means complete, however, it\'s good enough to be of value, hence releasing it.\nIf you would like to contribute to the project, usual Github rules apply:\n\n1. Fork the repo to your own account\n2. Submit a pull request\n3. Add tests for any new code\n4. Follow coding style of existing project\n\nAcknowledgements\n================\n\n@jpadilla for `PyJWT <https://github.com/jpadilla/pyjwt/>`_\n',
    'author': 'YunoJuno',
    'author_email': 'code@yunojuno.com',
    'maintainer': 'YunoJuno',
    'maintainer_email': 'code@yunojuno.com',
    'url': 'https://github.com/yunojuno/django-request-token',
    'packages': packages,
    'package_data': package_data,
    'install_requires': install_requires,
    'python_requires': '>=3.7,<4.0',
}


setup(**setup_kwargs)
