diff --git a/requirements.in b/requirements.in index bc115a755..5a99bb696 100644 --- a/requirements.in +++ b/requirements.in @@ -3,6 +3,8 @@ djangorestframework djangorestframework-jsonp django-filter django-modeltranslation +django-two-factor-auth +django-two-factor-auth[phonenumbers] flake8 requests requests_cache diff --git a/requirements.txt b/requirements.txt index 130ee0fbb..32912bbcb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -40,10 +40,14 @@ django==5.1 # django-cors-headers # django-extensions # django-filter + # django-formtools # django-js-asset # django-modeltranslation # django-munigeo + # django-otp + # django-phonenumber-field # django-polymorphic + # django-two-factor-auth # djangorestframework # drf-spectacular django-cors-headers==4.4.0 @@ -54,6 +58,8 @@ django-extensions==3.2.3 # via -r requirements.in django-filter==24.3 # via -r requirements.in +django-formtools==2.5.1 + # via django-two-factor-auth django-js-asset==2.2.0 # via django-mptt django-modeltranslation==0.19.7 @@ -66,8 +72,14 @@ django-mptt==0.16.0 # django-munigeo django-munigeo @ git+https://github.com/City-of-Helsinki/django-munigeo@v0.2.86 # via -r requirements.in +django-otp==1.5.2 + # via django-two-factor-auth +django-phonenumber-field==8.0.0 + # via django-two-factor-auth django-polymorphic==3.1.0 # via -r requirements.in +django-two-factor-auth[phonenumbers]==1.17.0 + # via -r requirements.in djangorestframework==3.15.2 # via # -r requirements.in @@ -125,6 +137,8 @@ pathspec==0.12.1 # via black pep8-naming==0.14.1 # via -r requirements.in +phonenumbers==8.13.43 + # via django-two-factor-auth pip-tools==7.4.1 # via -r requirements.in platformdirs==4.2.2 @@ -139,6 +153,8 @@ pycodestyle==2.12.1 # via flake8 pyflakes==3.2.0 # via flake8 +pypng==0.20220715.0 + # via qrcode pyproject-hooks==1.1.0 # via # build @@ -159,6 +175,8 @@ pyyaml==6.0.2 # via # django-munigeo # drf-spectacular +qrcode==7.4.2 + # via django-two-factor-auth referencing==0.35.1 # via # jsonschema @@ -202,6 +220,7 @@ typing-extensions==4.12.2 # black # cattrs # django-modeltranslation + # qrcode tzdata==2024.1 # via -r requirements.in uritemplate==4.1.1 diff --git a/services/templates/two_factor/_base.html b/services/templates/two_factor/_base.html new file mode 100644 index 000000000..cd525901b --- /dev/null +++ b/services/templates/two_factor/_base.html @@ -0,0 +1,30 @@ + + + + {% block title %}{% endblock %} + + + + + +{% block extra_media %}{% endblock %} + + + {% block content_wrapper %} +
+ {% block content %}{% endblock %} +
+ {% endblock %} + + diff --git a/smbackend/settings.py b/smbackend/settings.py index 51d7389ee..1e2b67547 100644 --- a/smbackend/settings.py +++ b/smbackend/settings.py @@ -45,6 +45,13 @@ SEARCH_LOG_LEVEL=(str, "INFO"), GEO_SEARCH_LOCATION=(str, ""), GEO_SEARCH_API_KEY=(str, ""), + EMAIL_USE_TLS=(bool, True), + EMAIL_HOST=(str, None), + EMAIL_PORT=(int, None), + EMAIL_TIMEOUT=(int, None), + EMAIL_HOST_USER=(str, None), + EMAIL_HOST_PASSWORD=(str, None), + OTP_EMAIL_SENDER=(str, None), ) env_path = BASE_DIR / ".env" @@ -60,6 +67,13 @@ DJANGO_LOG_LEVEL = env("DJANGO_LOG_LEVEL") IMPORT_LOG_LEVEL = env("IMPORT_LOG_LEVEL") SEARCH_LOG_LEVEL = env("SEARCH_LOG_LEVEL") +EMAIL_USE_TLS = env("EMAIL_USE_TLS") +EMAIL_HOST = env("EMAIL_HOST") +EMAIL_PORT = env("EMAIL_PORT") +EMAIL_TIMEOUT = env("EMAIL_TIMEOUT") +EMAIL_HOST_USER = env("EMAIL_HOST_USER") +EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD") +OTP_EMAIL_SENDER = env("OTP_EMAIL_SENDER") # Application definition INSTALLED_APPS = [ @@ -82,6 +96,13 @@ "services.apps.ServicesConfig", "observations", "drf_spectacular", + # Two-factor authentication + "django_otp", + "django_otp.plugins.otp_static", + "django_otp.plugins.otp_totp", + "django_otp.plugins.otp_email", + "two_factor", + "two_factor.plugins.email", ] if env("ADDITIONAL_INSTALLED_APPS"): @@ -100,6 +121,7 @@ "django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", + "django_otp.middleware.OTPMiddleware", ] if env("ADDITIONAL_MIDDLEWARE"): @@ -252,6 +274,7 @@ def gettext(s): }, "handlers": { "console": { + "level": "DEBUG", "class": "logging.StreamHandler", "formatter": "timestamped_named", }, @@ -260,6 +283,10 @@ def gettext(s): "django": {"handlers": ["console"], "level": DJANGO_LOG_LEVEL}, "services.search": {"handlers": ["console"], "level": SEARCH_LOG_LEVEL}, "services.management": {"handlers": ["console"], "level": IMPORT_LOG_LEVEL}, + "two_factor": { + "handlers": ["console"], + "level": "INFO", + }, }, } @@ -287,3 +314,12 @@ def gettext(s): "VERSION": None, "SERVE_INCLUDE_SCHEMA": False, } + +# Two-factor authentication settings +LOGIN_URL = "two_factor:login" +OTP_EMAIL_SUBJECT = "Palvelukartta - kirjautumistunniste" + +if DEBUG: + EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" +else: + EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" diff --git a/smbackend/urls.py b/smbackend/urls.py index bf0879025..56586ae7b 100644 --- a/smbackend/urls.py +++ b/smbackend/urls.py @@ -9,6 +9,7 @@ ) from munigeo.api import all_views as munigeo_views from rest_framework import routers +from two_factor.urls import urlpatterns as tf_urls from observations.api import views as observations_views from observations.views import obtain_auth_token @@ -77,4 +78,5 @@ def readiness(*args, **kwargs): "schema/redoc/", SpectacularRedocView.as_view(url_name="schema"), name="redoc" ), re_path(r"", include(shortcutter_urls)), + re_path("", include(tf_urls)), ]