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

packages = \
['magic_link', 'magic_link.migrations']

package_data = \
{'': ['*'], 'magic_link': ['templates/magic_link/*']}

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

setup_kwargs = {
    'name': 'django-magic-link',
    'version': '0.4',
    'description': "Django app for managing tokenised 'magic link' logins.",
    'long_description': '# Django Magic Link\n\nOpinionated Django app for managing "magic link" logins.\n\n**WARNING**\n\nIf you send a login link to the wrong person, they will gain full access to the user\'s account. Use\nwith extreme caution, and do not use this package without reading the source code and ensuring that\nyou are comfortable with it. If you have an internal security team, ask them to look at it before\nusing it. If your clients have security sign-off on your application, ask them to look at it before\nusing it.\n\n**/WARNING**\n\nThis app is not intended for general purpose URL tokenisation; it is designed to support a single\nuse case - so-called "magic link" logins.\n\nThere are lots of alternative apps that can support this use case, including the project from which\nthis has been extracted -\n[`django-request-token`](https://github.com/yunojuno/django-request-token). The reason for yet\nanother one is to handle the real-world challenge of URL caching / pre-fetch, where intermediaries\nuse URLs with unintended consequences.\n\nThis packages supports a very specific model:\n\n1. User is sent a link to log them in automatically.\n2. User clicks on the link, and which does a GET request to the URL.\n3. User is presented with a confirmation page, but is _not_ logged in.\n4. User clicks on a button and performs a POST to the same page.\n5. The POST request authenticates the user, and deactivates the token.\n\nThe advantage of this is the email clients do not support POST links, and any prefetch that attempts\na POST will fail the CSRF checks.\n\nThe purpose is to ensure that someone actively, purposefully, clicked on a link to authenticate\nthemselves. This enables instant deactivation of the token, so that it can no longer be used.\n\nIn practice, without this check, valid magic links may be requested a number of times via GET\nrequest before the intended recipient even sees the link. If you use a "max uses" restriction to\nlock down the link you may find this limit is hit, and the end user then finds that the link is\ninactive. The alternative to this is to remove the use limit and rely instead on an expiry window.\nThis risks leaving the token active even after the user has logged in. This package is targeted at\nthis situation.\n\n## Use\n\n### Prerequisite: Override the default templates.\n\nThis package has two HTML templates that must be overridden in your local application.\n\n**logmein.html**\n\nThis is the landing page that a user sees when they click on the magic link. You can add any content\nyou like to this page - the only requirement is that must contains a simple form with a csrf token\nand a submit button. This form must POST back to the link URL. The template render context includes\nthe `link` which has a `get_absolute_url` method to simplify this:\n\n```html\n<form method="POST" action="{{ link.get_absolute_url }}>\n    {% csrf_token %}\n    <button type="submit">Log me in</button>\n</form>\n```\n\n**error.html**\n\nIf the link has expired, been used, or is being accessed by someone who is already logged in, then\nthe `error.html` template will be rendered. The template context includes `link` and `error`.\n\n```html\n<p>Error handling magic link {{ link }}: {{ error }}.</p>\n```\n\n### 1. Create a new login link\n\nThe first step in managing magic links is to create one. Links are bound to a user, and can have a\ncustom expiry and post-login redirect URL.\n\n```python\n# create a link with the default expiry and redirect\nlink = MagicLink.objects.create(user=user)\n\n# create a link with a specific redirect\nlink = MagicLink.objects.create(user=user, redirect_to="/foo")\n\n# create a link with a specific expiry (in seconds)\nlink = MagicLink.objects.create(user=user, expiry=60)\n```\n\n### 3. Send the link to the user\n\nThis package does not handle the sending on your behalf - it is your responsibility to ensure that\nyou send the link to the correct user. If you send the link to the wrong user, they will have full\naccess to the link user\'s account. **YOU HAVE BEEN WARNED**.\n\n## Auditing\n\nA core requirement of this package is to be able to audit the use of links - for monitoring and\nanalysis. To enable this we have a second model, `MagicLinkUse`, and we create a new object for\nevery request to a link URL, _regardless of outcome_. Questions that we want to have answers for\ninclude:\n\n-   How long does it take for users to click on a link?\n-   How many times is a link used before the POST login?\n-   How often is a link used _after_ a successful login?\n-   How often does a link expire before a successful login?\n-   Can we identify common non-user client requests (email caches, bots, etc)?\n-   Should we disable links after X non-POST requests?\n\nIn order to facilitate this analysis we denormalise a number of timestamps from the `MagicLinkUse`\nobject back onto the `MagicLink` itself:\n\n-   `created_at` - when the record was created in the database\n-   `accessed_at` - the first GET request to the link URL\n-   `logged_in_at` - the successful POST\n-   `expires_at` - the link expiry, set when the link is created.\n\nNote that the expiry timestamp is **not** updated when the link is used. This is by design, to\nretain the original expiry timestamp.\n\n### Link validation\n\nIn addition to the timestamp fields, there is a separate boolean flag, `is_active`. This acts as a\n"kill switch" that overrides any other attribute, and it allows a link to be disabled without having\nto edit (or destroy) existing timestamp values. You can deactivate all links in one hit by calling\n`MagicLink.objects.deactivate()`.\n\nA link\'s `is_valid` property combines both `is_active` and timestamp data to return a bool value\nthat defines whether a link can used, based on the following criteria:\n\n1. The link is active (`is_active`)\n2. The link has not expired (`expires_at`)\n3. The link has not already been used (`logged_in_at`)\n\nIn addition to checking the property `is_valid`, the `validate()` method will raise an exception\nbased on the specific condition that failed. This is used by the link view to give feedback to the\nuser on the nature of the failure.\n\n### Request authorization\n\nIf the link\'s `is_valid` property returns `True`, then the link _can_ be used. However, this does\nnot mean that the link can be used by anyone. We do not allow authenticated users to login using\nsomeone else\'s magic link. The `authorize()` method takes a `User` argument and determines whether\nthey are authorized to use the link. If the user is authenticated, and does not match the\n`link.user`, then a `PermissionDenied` exception is raised.\n\n### Putting it together\n\nCombining the validation, authorization and auditing, we get a simplified flow that looks something\nlike this:\n\n```python\ndef get(request, token):\n    """Render login page."""\n    link = get_object_or_404(MagicLink, token=token)\n    link.validate()\n    link.authorize(request.user)\n    link.audit()\n    return render("logmein.html")\n\ndef post(request, token):\n    """Handle the login POST."""\n    link = get_object_or_404(MagicLink, token=token)\n    link.validate()\n    link.authorize(request.user)\n    link.login(request)\n    link.disable()\n    return redirect(link.redirect_to)\n```\n\n## Settings\n\nSettings are read from a `django.conf.settings` settings dictionary called `MAGIC_LINK`.\n\nDefault settings show below:\n\n```python\n# settings.py\nMAGIC_LINK = {\n    # link expiry, in seconds\n    "DEFAULT_EXPIRY": 300,\n    # default link redirect\n    "DEFAULT_REDIRECT": "/",\n    # the preferred authorization backend to use, in the case where you have more\n    # than one specified in the `settings.AUTHORIZATION_BACKENDS` setting.\n    "AUTHENTICATION_BACKEND": "django.contrib.auth.backends.ModelBackend",\n    # SESSION_COOKIE_AGE override for magic-link logins - in seconds (default is 1 week)\n    "SESSION_EXPIRY": 7 * 24 * 60 * 60\n}\n```\n\n## Screenshots\n\n**Default landing page (`logmein.html`)**\n\n<img src="https://raw.githubusercontent.com/yunojuno/django-magic-link/master/screenshots/landing-page.png" width=600 alt="Screenshot of default landing page" />\n\n**Default error page (`error.html`)**\n\n<img src="https://raw.githubusercontent.com/yunojuno/django-magic-link/master/screenshots/error-page.png" width=600 alt="Screenshot of default error page" />\n\n**Admin view of magic link uses**\n\n<img src="https://raw.githubusercontent.com/yunojuno/django-magic-link/master/screenshots/admin-inline.png" width=600 alt="Screenshot of MagicLinkUseInline" />\n',
    'author': 'YunoJuno',
    'author_email': 'code@yunojuno.com',
    'maintainer': 'YunoJuno',
    'maintainer_email': 'code@yunojuno.com',
    'url': 'https://github.com/yunojuno/django-magic-link',
    'packages': packages,
    'package_data': package_data,
    'install_requires': install_requires,
    'python_requires': '>=3.7,<4.0',
}


setup(**setup_kwargs)
