M nlotp/checks.py +14 -13
@@ 26,30 26,31 @@ REQUIRED_INSTALLED_APPS = (
REQUIRED_SETTINGS = (
{
- "key": "NLOTP_EXCLUDED_URLS",
+ "key": "NLOTP_VERIFY_EXCLUDED_URLS",
"code": "E004",
"hint": (
- "A list of urls excluded from redirect if user "
- "is not OTP verified."
+ "A list of urls excluded from redirect to verify page "
+ "if user is not OTP verified."
+ ),
+ },
+ {
+ "key": "NLOTP_SETUP_EXCLUDED_URLS",
+ "code": "E005",
+ "hint": (
+ "A list of urls excluded from redirect to setup page "
+ "if user didn't setup 2FA and it's required "
+ "(NLOTP_2FA_SETUP_REQUIRED = True)."
),
},
)
REQUIRED_MIDDLEWARES = (
{
- "key": "django_otp.middleware.OTPMiddleware",
- "code": "E005",
- "hint": (
- "Must be installed after "
- "`django.contrib.auth.middleware.AuthenticationMiddleware` "
- "and before `nlotp.middleware.OTPCheckMiddleware`."
- ),
- },
- {
"key": "nlotp.middleware.OTPCheckMiddleware",
"code": "E006",
"hint": (
- "Must be installed after `django_otp.middleware.OTPMiddleware`."
+ "Must be installed after "
+ "`django.contrib.auth.middleware.AuthenticationMiddleware`."
),
},
)
M nlotp/middleware.py +27 -17
@@ 1,23 1,33 @@
-from django.conf import settings
-from django.http import HttpResponseRedirect
-from django.urls import reverse_lazy
-from django.utils.deprecation import MiddlewareMixin
-from django_otp import user_has_device
+import functools
-NLOTP_LOGIN_URL = getattr(
- settings,
- "NLOTP_LOGIN_URL",
- reverse_lazy("nlotp:verify-otp"),
-)
+from django.http import HttpResponseRedirect
+from django.utils.deprecation import MiddlewareMixin
+from django.utils.functional import SimpleLazyObject
+from django_otp import user_has_device
+from django_otp.middleware import OTPMiddleware
+
+from . import settings
-class OTPCheckMiddleware(MiddlewareMixin):
+class OTPCheckMiddleware(OTPMiddleware, MiddlewareMixin):
+ def __call__(self, request):
+ return MiddlewareMixin.__call__(self, request)
+
def process_request(self, request):
- user = request.user
- if user.is_authenticated:
- if user.is_verified() or not user_has_device(user):
+ request.user = SimpleLazyObject(
+ functools.partial(self._verify_user, request, request.user)
+ )
+ if request.user.is_authenticated:
+ if request.user.is_verified():
return None
- else:
- if request.path not in settings.NLOTP_EXCLUDED_URLS:
- return HttpResponseRedirect(NLOTP_LOGIN_URL)
+ if not user_has_device(request.user):
+ if (
+ settings.NLOTP_2FA_SETUP_REQUIRED
+ and request.path not in settings.NLOTP_SETUP_EXCLUDED_URLS
+ ):
+ return HttpResponseRedirect(settings.NLOTP_2FA_SETUP_URL)
+ else:
+ return None
+ if request.path not in settings.NLOTP_VERIFY_EXCLUDED_URLS:
+ return HttpResponseRedirect(settings.NLOTP_VERIFY_URL)
return None
A => nlotp/settings.py +58 -0
@@ 0,0 1,58 @@
+from django.conf import settings
+from django.urls import reverse_lazy
+
+
+# 2FA verify page url
+NLOTP_VERIFY_URL = getattr(
+ settings,
+ "NLOTP_VERIFY_URL",
+ reverse_lazy("nlotp:verify-otp"),
+)
+
+
+# list of urls excluded from redirect to verify page if user is not verified
+# NOTE: login and logout views should be added to this list
+NLOTP_VERIFY_EXCLUDED_URLS = getattr(
+ settings,
+ "NLOTP_VERIFY_EXCLUDED_URLS",
+ (
+ NLOTP_VERIFY_URL,
+ ),
+)
+
+
+# if 2FA setup is mandatory
+NLOTP_2FA_SETUP_REQUIRED = getattr(
+ settings,
+ "NLOTP_2FA_SETUP_REQUIRED",
+ False,
+)
+
+
+# 2FA setup page url
+NLOTP_2FA_SETUP_URL = getattr(
+ settings,
+ "NLOTP_2FA_SETUP_URL",
+ reverse_lazy("nlotp:two-factor-auth"),
+)
+
+
+# user TOTP device QR code generation url
+NLOTP_QR_CODE_URL = getattr(
+ settings,
+ "NLOTP_QR_CODE_URL",
+ reverse_lazy("nlotp:qr-code"),
+)
+
+
+# list of urls excluded from redirect to 2FA setup page
+# when `NLOTP_2FA_SETUP_REQUIRED` is enabled and user didn't setup 2FA
+# NOTE: login and logout views should be added to this list
+NLOTP_SETUP_EXCLUDED_URLS = getattr(
+ settings,
+ "NLOTP_SETUP_EXCLUDED_URLS",
+ (
+ NLOTP_2FA_SETUP_URL,
+ NLOTP_QR_CODE_URL,
+ ),
+)
M nlotp/templates/nlotp/two_factor_auth.html +6 -2
@@ 14,7 14,7 @@
{% endfor %}
</ul>
{% endif %}
- <form method="POST" action="{% url 'nlotp:two-factor-auth' %}" id="manage-two-factor-auth-form" autocomplete="on">
+ <form method="POST" action="." id="manage-two-factor-auth-form" autocomplete="on">
{% csrf_token %}
{{ form.non_field_errors }}
@@ 37,6 37,10 @@
{% endwith %}
<hr/>
{% endif %}
+
+ {% if setup_required %}
+ <p><b>You need to enable 2FA to continue.</b></p>
+ {% endif %}
<b>Status:</b> <span>{{ totp_device.confirmed|yesno:"Enabled,Disabled" }}</span>
@@ 52,7 56,7 @@
{% if not totp_device.confirmed %}
<p>Please scan the QR code or manually enter the key, then enter an authentication code from your app in order to complete setup.</p>
<p id="qrcode">
- <img width="200" height="200" src="{% url 'nlotp:qr-code' totp_device.id %}" />
+ <img width="200" height="200" src="{{ qr_code_url}}" />
</p>
<p>
<label>Key:</label> {{ totp_device.bin_key|b32encode_val }}
M nlotp/templates/nlotp/verify_otp.html +1 -1
@@ 6,7 6,7 @@
<title>Two-Step Verification (2FA) Verification</title>
</head>
<body>
- <form action="{% url 'nlotp:verify-otp' %}?next={{ next }}" method="POST" autocomplete="on">
+ <form action="?next={{ next }}" method="POST" autocomplete="on">
{% csrf_token %}
<h2>Two-Step Verification (2FA)</h2>
M nlotp/urls.py +1 -1
@@ 6,7 6,7 @@ app_name = 'nlotp'
urlpatterns = [
path(
- 'qr-code/<int:device_id>/',
+ 'qr-code/',
views.QRCodeView.as_view(),
name='qr-code',
),
M nlotp/views.py +10 -9
@@ 6,19 6,18 @@ from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.urls import reverse_lazy
from django.views.generic.edit import FormView, View
+from django_otp import DEVICE_ID_SESSION_KEY
from django_otp import login as otp_login
from django_otp.views import LoginView
-from . import forms, utils
+from . import forms, settings, utils
class QRCodeView(LoginRequiredMixin, View):
- """ Generates QR code for the TOTP device """
+ """ Generates QR code for the logged in user TOTP device """
- def get(self, request, device_id, *args, **kwargs):
- device = get_object_or_404(
- request.user.totpdevice_set.all(), id=device_id
- )
+ def get(self, request, *args, **kwargs):
+ device = utils.get_user_totp_device(request.user)
img = qrcode.make(
device.config_url, image_factory=qrcode.image.svg.SvgImage
)
@@ 39,7 38,7 @@ class TwoFactorAuthView(LoginRequiredMix
template_name = "nlotp/two_factor_auth.html"
form_class = forms.TwoFactorAuthForm
- success_url = reverse_lazy("nlotp:two-factor-auth")
+ success_url = settings.NLOTP_2FA_SETUP_URL
def setup(self, request, *args, **kwargs):
super().setup(request, *args, **kwargs)
@@ 55,6 54,8 @@ class TwoFactorAuthView(LoginRequiredMix
"totp_device": self.totp_device,
"static_device": self.static_device,
"show_codes": self.show_codes,
+ "qr_code_url": settings.NLOTP_QR_CODE_URL,
+ "setup_required": settings.NLOTP_2FA_SETUP_REQUIRED,
}
)
return context
@@ 102,9 103,9 @@ class TwoFactorAuthView(LoginRequiredMix
self.totp_device = utils.get_user_totp_device(self.request.user)
self.static_device.confirmed = False
self.static_device.save()
+ del self.request.session[DEVICE_ID_SESSION_KEY]
messages.success(
- self.request,
- "Two-Step Verification has been disabled.",
+ self.request, "Two-Step Verification has been disabled.",
)
context = self.get_context_data()
context["form"] = self.form_class(