Skip to content

Commit

Permalink
✨ Fix urlconf, middleware and add sytem checks
Browse files Browse the repository at this point in the history
  • Loading branch information
sergei-maertens committed Jan 30, 2024
1 parent cab2ac0 commit a18bbe1
Show file tree
Hide file tree
Showing 8 changed files with 146 additions and 9 deletions.
1 change: 1 addition & 0 deletions maykin_2fa/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ class Maykin2FaConfig(AppConfig):
name = "maykin_2fa"

def ready(self):
from . import checks # noqa
from . import signals # noqa
102 changes: 102 additions & 0 deletions maykin_2fa/checks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
from django.apps import apps
from django.conf import settings
from django.contrib import admin
from django.core.checks import Error, Warning, register
from django.urls import NoReverseMatch, reverse

from .admin import MFARequired


@register()
def check_urlconf(app_configs, **kwargs):
errors = []
try:
reverse("maykin_2fa:login")
except NoReverseMatch:
example_code = 'path("admin/", include((urlpatterns, "maykin_2fa"))),'
errors.append(
Error(
"Broken URL config - could not resolve the 'maykin_2fa:login' URL.",
hint=f"Try including `{example_code}` in your root `urls.py`.",
id="maykin_2fa.E001",
)
)

if apps.is_installed("two_factor.plugins.webauthn"):
try:
reverse("two_factor:webauthn:create_credential")
except NoReverseMatch:
example_code = (
'path("admin/", include((webauthn_urlpatterns, "two_factor"))),'
)
errors.append(
Error(
"Broken URL config - could not resolve the "
"'two_factor:webauthn:create_credential' URL.",
hint=f"Try including `{example_code}` in your root `urls.py`.",
id="maykin_2fa.E002",
)
)

return errors


@register()
def check_authentication_backends(app_configs, **kwargs):
bypass_backends = getattr(settings, "MAYKIN_2FA_ALLOW_MFA_BYPASS_BACKENDS", [])
unknown_backends = [
backend
for backend in bypass_backends
if backend not in settings.AUTHENTICATION_BACKENDS
]
if not unknown_backends:
return []

return [
Warning(
"MAYKIN_2FA_ALLOW_MFA_BYPASS_BACKENDS contains backends not present in the "
f"AUTHENTICATION_BACKENDS setting: {', '.join(unknown_backends)}",
hint="Check for typos or add the backend(s) to settings.AUTHENTICATION_BACKENDS",
id="maykin_2fa.W001",
)
]


@register()
def check_middleware(app_configs, **kwargs):
errors = []

if "maykin_2fa.middleware.OTPMiddleware" not in settings.MIDDLEWARE:
errors.append(
Error(
"`maykin_2fa.middleware.OTPMiddleware` is missing from the middleware.",
hint="Add the maykin_2fa middleware instead of the django_otp one.",
id="maykin_2fa.E003",
)
)

if "django_otp.middleware.OTPMiddleware" in settings.MIDDLEWARE:
errors.append(
Error(
"Found `django_otp.middleware.OTPMiddleware` in the middleware - this is obsolete.",
hint="Remove the django_otp middleware (instead, use the maykin_2fa one).",
id="maykin_2fa.E004",
)
)

return errors


@register()
def check_admin_patched(app_configs, **kwargs):
cls = admin.site.__class__
if cls is MFARequired or issubclass(cls, MFARequired):
return []

return [
Error(
"The default admin site is not monkeypatched.",
hint="Call 'maykin_2fa.monkeypatch_admin' in your root urls.py",
id="maykin_2fa.E005",
)
]
2 changes: 1 addition & 1 deletion maykin_2fa/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,6 @@ def _verify_user(self, request: HttpRequest, user: AnyUser):
# call the super but replace the `is_verified` callable
user = super()._verify_user(request, user)
# this is *not* persisted on the user object after authenticate
user.backend = request.session[BACKEND_SESSION_KEY]
user.backend = request.session.get(BACKEND_SESSION_KEY)
user.is_verified = functools.partial(is_verified, user) # type: ignore
return user
13 changes: 13 additions & 0 deletions maykin_2fa/templates/maykin_2fa/setup_complete.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{% extends "two_factor/_base_focus.html" %}
{% load i18n %}

{% block content %}
<h1>{% block title %}{% trans "Enable Two-Factor Authentication" %}{% endblock %}</h1>

<p>{% blocktrans trimmed %}Congratulations, you've successfully enabled two-factor
authentication.{% endblocktrans %}</p>

<p><a href="#TODO"
class="btn btn-block btn-secondary">{% trans "Back to Account Security" %}</a></p>

{% endblock %}
11 changes: 8 additions & 3 deletions maykin_2fa/urls.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from django.urls import path
from django.urls import include, path

from .views import (
AdminLoginView,
Expand All @@ -8,8 +8,6 @@
SetupCompleteView,
)

app_name = "maykin_2fa"

# See two_factor/urls.py for a reference of the (core) urls

urlpatterns = [
Expand All @@ -19,3 +17,10 @@
path("mfa/setup/complete/", SetupCompleteView.as_view(), name="setup_complete"),
path("mfa/backup/tokens/", BackupTokensView.as_view(), name="backup_tokens"),
]

webauthn_urlpatterns = [
path(
"mfa/webauthn/",
include("two_factor.plugins.webauthn.urls", namespace="two_factor_webauthn"),
),
]
21 changes: 17 additions & 4 deletions maykin_2fa/views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from django.contrib.auth import logout
from django.shortcuts import resolve_url

from two_factor.utils import default_device
from two_factor.views import (
BackupTokensView as _BackupTokensView,
LoginView as _LoginView,
Expand All @@ -11,16 +13,27 @@

class AdminLoginView(_LoginView):
template_name = "two_factor/core/login.html"
redirect_authenticated_user = True
redirect_authenticated_user = False

def get_redirect_url(self):
# after succesful authentication, check if the user needs to set up 2FA. If MFA
# was configured already, login flow takes care of the OTP step.
user = self.request.user

if user.is_authenticated and not user.is_verified():
return resolve_url("maykin_2fa:setup")
return super().get_redirect_url()
# if no device is set up, redirect to the setup.
device = default_device(user)
if device is None:
return resolve_url("maykin_2fa:setup")

# a device is configured, but wasn't used - this may have been an aborted
# authentication process. Log the user out and have the go through the login
# flow again.
logout(self.request)
return resolve_url("maykin_2fa:login")

admin_index = resolve_url("admin:index")
return super().get_redirect_url() or admin_index


class AdminSetupView(_SetupView):
Expand All @@ -36,7 +49,7 @@ class BackupTokensView(_BackupTokensView):


class SetupCompleteView(_SetupCompleteView):
template_name = "two_factor/core/setup_complete.html"
template_name = "maykin_2fa/setup_complete.html"


class QRGeneratorView(_QRGeneratorView):
Expand Down
1 change: 1 addition & 0 deletions testapp/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,4 @@

# Custom settings
MAYKIN_2FA_ALLOW_MFA_BYPASS_BACKENDS = ["testapp.backends.No2FAModelBackend"]
# MAYKIN_2FA_ALLOW_MFA_BYPASS_BACKENDS = AUTHENTICATION_BACKENDS
4 changes: 3 additions & 1 deletion testapp/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
from django.urls import include, path

from maykin_2fa import monkeypatch_admin
from maykin_2fa.urls import urlpatterns, webauthn_urlpatterns

monkeypatch_admin()

urlpatterns = [
path("admin/", include("maykin_2fa.urls")),
path("admin/", include((urlpatterns, "maykin_2fa"))),
path("admin/", include((webauthn_urlpatterns, "two_factor"))),
path("admin/", admin.site.urls),
]

0 comments on commit a18bbe1

Please sign in to comment.