From c95e097f3471a2dbd87ff32ff80592b6797919b4 Mon Sep 17 00:00:00 2001 From: Gabriel Augendre Date: Sun, 26 Dec 2021 23:24:33 +0100 Subject: [PATCH] Add TOTP --- poetry.lock | 139 +++++++++++++++++-- pyproject.toml | 1 + src/articles/static/login.css | 14 ++ src/articles/templates/two_factor/_base.html | 5 + src/blog/settings.py | 12 +- src/blog/urls.py | 2 + 6 files changed, 161 insertions(+), 12 deletions(-) create mode 100644 src/articles/static/login.css create mode 100644 src/articles/templates/two_factor/_base.html diff --git a/poetry.lock b/poetry.lock index 6b615d3..489344e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -97,7 +97,7 @@ unicode_backport = ["unicodedata2"] name = "colorama" version = "0.4.4" description = "Cross-platform colored terminal text." -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" @@ -200,13 +200,83 @@ python-versions = ">=3.6" Django = ">=2.2" sqlparse = ">=0.2.0" +[[package]] +name = "django-formtools" +version = "2.3" +description = "A set of high-level abstractions for Django forms" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +Django = ">=2.2" + +[[package]] +name = "django-otp" +version = "1.1.3" +description = "A pluggable framework for adding two-factor authentication to Django using one-time passwords." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +django = ">=2.2" + +[package.extras] +qrcode = ["qrcode"] + +[[package]] +name = "django-phonenumber-field" +version = "5.2.0" +description = "An international phone number field for django models." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +Django = ">=2.2" + +[package.extras] +phonenumbers = ["phonenumbers (>=7.0.2)"] +phonenumberslite = ["phonenumberslite (>=7.0.2)"] + +[[package]] +name = "django-two-factor-auth" +version = "1.13" +description = "" +category = "main" +optional = false +python-versions = "*" +develop = false + +[package.dependencies] +Django = ">=2.2" +django-formtools = "*" +django_otp = ">=0.8.0" +django-phonenumber-field = ">=1.1.0,<6" +phonenumberslite = {version = ">=7.0.9,<8.99", optional = true, markers = "extra == \"phonenumberslite\""} +qrcode = ">=4.0.0,<6.99" + +[package.extras] +call = ["twilio (>=6.0)"] +sms = ["twilio (>=6.0)"] +yubikey = ["django-otp-yubikey"] +phonenumbers = ["phonenumbers (>=7.0.9,<8.99)"] +phonenumberslite = ["phonenumberslite (>=7.0.9,<8.99)"] + +[package.source] +type = "git" +url = "https://github.com/Bouke/django-two-factor-auth.git" +reference = "ffe4422e" +resolved_reference = "ffe4422e6f68bfb84ad2b44ca83c15abb0af5e7c" + [[package]] name = "filelock" -version = "3.4.0" +version = "3.4.2" description = "A platform independent file lock." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.extras] docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] @@ -340,6 +410,14 @@ python-versions = ">=3.6" [package.dependencies] pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" +[[package]] +name = "phonenumberslite" +version = "8.12.40" +description = "Python version of Google's common library for parsing, formatting, storing and validating international phone numbers." +category = "main" +optional = false +python-versions = "*" + [[package]] name = "pillow" version = "8.4.0" @@ -350,11 +428,11 @@ python-versions = ">=3.6" [[package]] name = "platformdirs" -version = "2.4.0" +version = "2.4.1" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.extras] docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] @@ -559,6 +637,24 @@ category = "dev" optional = false python-versions = ">=3.6" +[[package]] +name = "qrcode" +version = "6.1" +description = "QR Code image generator" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} +six = "*" + +[package.extras] +dev = ["tox", "pytest", "mock"] +maintainer = ["zest.releaser"] +pil = ["pillow"] +test = ["pytest", "pytest-cov", "mock"] + [[package]] name = "rcssmin" version = "1.1.0" @@ -621,7 +717,7 @@ python-versions = ">=3.6.*" name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" @@ -748,7 +844,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "8ce7193640d549da552b67f5d485214f3e74931c801116164ad21614be32846e" +content-hash = "30053d4662f7e86ae956249a199beeaed34b338fe420d09ad2db0ae4f9d2d8bd" [metadata.files] asgiref = [ @@ -898,9 +994,22 @@ django-debug-toolbar = [ {file = "django-debug-toolbar-3.2.4.tar.gz", hash = "sha256:644bbd5c428d3283aa9115722471769cac1bec189edf3a0c855fd8ff870375a9"}, {file = "django_debug_toolbar-3.2.4-py3-none-any.whl", hash = "sha256:6b633b6cfee24f232d73569870f19aa86c819d750e7f3e833f2344a9eb4b4409"}, ] +django-formtools = [ + {file = "django-formtools-2.3.tar.gz", hash = "sha256:9663b6eca64777b68d6d4142efad8597fe9a685924673b25aa8a1dcff4db00c3"}, + {file = "django_formtools-2.3-py3-none-any.whl", hash = "sha256:4699937e19ee041d803943714fe0c1c7ad4cab802600eb64bbf4cdd0a1bfe7d9"}, +] +django-otp = [ + {file = "django-otp-1.1.3.tar.gz", hash = "sha256:f002c71d4ea7f514590be00492980d3c87397b73dc20542e1c4fc00b66f2dda1"}, + {file = "django_otp-1.1.3-py3-none-any.whl", hash = "sha256:8637be826c0465d0fd1710e4472efe9fc83883853a2141fefdbace9358d20003"}, +] +django-phonenumber-field = [ + {file = "django-phonenumber-field-5.2.0.tar.gz", hash = "sha256:52b2e5970133ec5ab701218b802f7ab237229854dc95fd239b7e9e77dc43731d"}, + {file = "django_phonenumber_field-5.2.0-py3-none-any.whl", hash = "sha256:5547fb2b2cc690a306ba77a5038419afc8fa8298a486fb7895008e9067cc7e75"}, +] +django-two-factor-auth = [] filelock = [ - {file = "filelock-3.4.0-py3-none-any.whl", hash = "sha256:2e139a228bcf56dd8b2274a65174d005c4a6b68540ee0bdbb92c76f43f29f7e8"}, - {file = "filelock-3.4.0.tar.gz", hash = "sha256:93d512b32a23baf4cac44ffd72ccf70732aeff7b8050fcaf6d3ec406d954baf4"}, + {file = "filelock-3.4.2-py3-none-any.whl", hash = "sha256:cf0fc6a2f8d26bd900f19bf33915ca70ba4dd8c56903eeb14e1e7a2fd7590146"}, + {file = "filelock-3.4.2.tar.gz", hash = "sha256:38b4f4c989f9d06d44524df1b24bd19e167d851f19b50bf3e3559952dddc5b80"}, ] gunicorn = [ {file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"}, @@ -1083,6 +1192,10 @@ packaging = [ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] +phonenumberslite = [ + {file = "phonenumberslite-8.12.40-py2.py3-none-any.whl", hash = "sha256:e6fe6cad1091f8928e34a98570cade4758f4cf4e70e9e32ff7eca517ce98e273"}, + {file = "phonenumberslite-8.12.40.tar.gz", hash = "sha256:153885eefec397058c8ce91fb987f55545b2cfa945f22a904584ddf162aa82b1"}, +] pillow = [ {file = "Pillow-8.4.0-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:81f8d5c81e483a9442d72d182e1fb6dcb9723f289a57e8030811bac9ea3fef8d"}, {file = "Pillow-8.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3f97cfb1e5a392d75dd8b9fd274d205404729923840ca94ca45a0af57e13dbe6"}, @@ -1127,8 +1240,8 @@ pillow = [ {file = "Pillow-8.4.0.tar.gz", hash = "sha256:b8e2f83c56e141920c39464b852de3719dfbfb6e3c99a2d8da0edf4fb33176ed"}, ] platformdirs = [ - {file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"}, - {file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"}, + {file = "platformdirs-2.4.1-py3-none-any.whl", hash = "sha256:1d7385c7db91728b83efd0ca99a5afb296cab9d0ed8313a45ed8ba17967ecfca"}, + {file = "platformdirs-2.4.1.tar.gz", hash = "sha256:440633ddfebcc36264232365d7840a970e75e1018d15b4327d11f91909045fda"}, ] pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, @@ -1227,6 +1340,10 @@ pyyaml = [ {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, ] +qrcode = [ + {file = "qrcode-6.1-py2.py3-none-any.whl", hash = "sha256:3996ee560fc39532910603704c82980ff6d4d5d629f9c3f25f34174ce8606cf5"}, + {file = "qrcode-6.1.tar.gz", hash = "sha256:505253854f607f2abf4d16092c61d4e9d511a3b4392e60bff957a68592b04369"}, +] rcssmin = [ {file = "rcssmin-1.1.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:2211a5c91ea14a5937b57904c9121f8bfef20987825e55368143da7d25446e3b"}, {file = "rcssmin-1.1.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:7085d1b51dd2556f3aae03947380f6e9e1da29fb1eeadfa6766b7f105c54c9ff"}, diff --git a/pyproject.toml b/pyproject.toml index caf09a6..53ceea6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,7 @@ django-debug-toolbar = "^3.2" whitenoise = {extras = ["brotli"], version = "^5.2.0"} rcssmin = "^1.0.6" django-csp = "^3.7" +django-two-factor-auth = {extras = ["phonenumberslite"], git = "https://github.com/Bouke/django-two-factor-auth.git", rev = "ffe4422e"} [tool.poetry.dev-dependencies] pre-commit = "^2.7" diff --git a/src/articles/static/login.css b/src/articles/static/login.css new file mode 100644 index 0000000..137a3c7 --- /dev/null +++ b/src/articles/static/login.css @@ -0,0 +1,14 @@ +.d-none { + display: none !important; +} + +.float-right { + float: right; +} + +td, th, tr, tbody, tr:nth-child(2n) { + background-color: inherit; + border: none; + padding-left: 0; + padding-right: 0; +} diff --git a/src/articles/templates/two_factor/_base.html b/src/articles/templates/two_factor/_base.html new file mode 100644 index 0000000..b0052b7 --- /dev/null +++ b/src/articles/templates/two_factor/_base.html @@ -0,0 +1,5 @@ +{% extends "articles/base.html" %} +{% load static %} +{% block append_css %} + +{% endblock %} diff --git a/src/blog/settings.py b/src/blog/settings.py index a28e52d..6498c2c 100644 --- a/src/blog/settings.py +++ b/src/blog/settings.py @@ -71,6 +71,10 @@ INSTALLED_APPS = [ "anymail", "django_cleanup.apps.CleanupConfig", "debug_toolbar", + "django_otp", + "django_otp.plugins.otp_static", + "django_otp.plugins.otp_totp", + "two_factor", ] MIDDLEWARE = [ @@ -82,6 +86,7 @@ MIDDLEWARE = [ "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", + "django_otp.middleware.OTPMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", "csp.middleware.CSPMiddleware", @@ -207,7 +212,12 @@ SHORTPIXEL_RESIZE_HEIGHT = int(os.getenv("SHORTPIXEL_RESIZE_HEIGHT", 10000)) GOATCOUNTER_DOMAIN = os.getenv("GOATCOUNTER_DOMAIN") -LOGIN_URL = "admin:login" +LOGIN_URL = "two_factor:login" +LOGIN_REDIRECT_URL = "two_factor:profile" +LOGOUT_REDIRECT_URL = "articles-list" +TWO_FACTOR_REMEMBER_COOKIE_AGE = 86400 * 30 +TWO_FACTOR_REMEMBER_COOKIE_SECURE = True +TWO_FACTOR_REMEMBER_COOKIE_SAMESITE = "Strict" SECURE_REFERRER_POLICY = "strict-origin-when-cross-origin" SECURE_HSTS_INCLUDE_SUBDOMAINS = True diff --git a/src/blog/urls.py b/src/blog/urls.py index 0e71792..f572e6d 100644 --- a/src/blog/urls.py +++ b/src/blog/urls.py @@ -18,10 +18,12 @@ from django.conf.urls.static import static from django.contrib import admin from django.urls import include, path from django.views.generic import TemplateView +from two_factor.urls import urlpatterns as tf_urls from blog import settings urlpatterns = [ + path("", include(tf_urls)), path( "robots.txt", TemplateView.as_view(