totp: rename tfa to totp

This commit is contained in:
Jens Langhammer 2018-12-14 10:09:57 +01:00
parent 52d1920914
commit fbf58801ec
17 changed files with 79 additions and 78 deletions

View File

@ -74,7 +74,7 @@ INSTALLED_APPS = [
'passbook.oauth_client', 'passbook.oauth_client',
'passbook.oauth_provider', 'passbook.oauth_provider',
'passbook.saml_idp', 'passbook.saml_idp',
'passbook.tfa', 'passbook.totp',
] ]
# Message Tag fix for bootstrap CSS Classes # Message Tag fix for bootstrap CSS Classes

View File

@ -1,3 +0,0 @@
"""passbook tfa Header"""
__version__ = '0.0.1-alpha'
default_app_config = 'passbook.tfa.apps.PassbookTFAConfig'

View File

@ -1,11 +0,0 @@
"""passbook 2FA AppConfig"""
from django.apps.config import AppConfig
class PassbookTFAConfig(AppConfig):
"""passbook TFA AppConfig"""
name = 'passbook.tfa'
label = 'passbook_tfa'
mountpoint = 'user/tfa/'

View File

@ -1,14 +0,0 @@
"""passbook 2FA Urls"""
from django.urls import path
from passbook.tfa import views
urlpatterns = [
path('', views.index, name='tfa-index'),
path('qr/', views.qr_code, name='tfa-qr'),
path('verify/', views.verify, name='tfa-verify'),
# path('enable/', views.TFASetupView.as_view(), name='tfa-enable'),
path('disable/', views.disable, name='tfa-disable'),
path('user_settings/', views.user_settings, name='tfa-user_settings'),
]

View File

@ -0,0 +1,3 @@
"""passbook totp Header"""
__version__ = '0.0.1-alpha'
default_app_config = 'passbook.totp.apps.PassbookTOTPConfig'

11
passbook/totp/apps.py Normal file
View File

@ -0,0 +1,11 @@
"""passbook TOTP AppConfig"""
from django.apps.config import AppConfig
class PassbookTOTPConfig(AppConfig):
"""passbook TOTP AppConfig"""
name = 'passbook.totp'
label = 'passbook_totp'
mountpoint = 'user/totp/'

View File

@ -1,12 +1,12 @@
"""passbook 2FA Forms""" """passbook TOTP Forms"""
from django import forms from django import forms
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
TFA_CODE_VALIDATOR = RegexValidator(r'^[0-9a-z]{6,8}$', TOTP_CODE_VALIDATOR = RegexValidator(r'^[0-9a-z]{6,8}$',
_('Only alpha-numeric characters are allowed.')) _('Only alpha-numeric characters are allowed.'))
class PictureWidget(forms.widgets.Widget): class PictureWidget(forms.widgets.Widget):
@ -16,37 +16,37 @@ class PictureWidget(forms.widgets.Widget):
return mark_safe("<img src=\"%s\" />" % value) # nosec return mark_safe("<img src=\"%s\" />" % value) # nosec
class TFAVerifyForm(forms.Form): class TOTPVerifyForm(forms.Form):
"""Simple Form to verify 2FA Code""" """Simple Form to verify TOTP Code"""
order = ['code'] order = ['code']
code = forms.CharField(label=_('Code'), validators=[TFA_CODE_VALIDATOR], code = forms.CharField(label=_('Code'), validators=[TOTP_CODE_VALIDATOR],
widget=forms.TextInput(attrs={'autocomplete': 'off'})) widget=forms.TextInput(attrs={'autocomplete': 'off'}))
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(TFAVerifyForm, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# This is a little helper so the field is focused by default # This is a little helper so the field is focused by default
self.fields['code'].widget.attrs.update({'autofocus': 'autofocus'}) self.fields['code'].widget.attrs.update({'autofocus': 'autofocus'})
class TFASetupInitForm(forms.Form): class TOTPSetupInitForm(forms.Form):
"""Initial 2FA Setup form""" """Initial TOTP Setup form"""
title = _('Set up 2FA') title = _('Set up TOTP')
device = None device = None
confirmed = False confirmed = False
qr_code = forms.CharField(widget=PictureWidget, disabled=True, required=False, qr_code = forms.CharField(widget=PictureWidget, disabled=True, required=False,
label=_('Scan this Code with your 2FA App.')) label=_('Scan this Code with your TOTP App.'))
code = forms.CharField(label=_('Code'), validators=[TFA_CODE_VALIDATOR]) code = forms.CharField(label=_('Code'), validators=[TOTP_CODE_VALIDATOR])
def clean_code(self): def clean_code(self):
"""Check code with new totp device""" """Check code with new totp device"""
if self.device is not None: if self.device is not None:
if not self.device.verify_token(int(self.cleaned_data.get('code'))) \ if not self.device.verify_token(int(self.cleaned_data.get('code'))) \
and not self.confirmed: and not self.confirmed:
raise forms.ValidationError(_("2FA Code does not match")) raise forms.ValidationError(_("TOTP Code does not match"))
return self.cleaned_data.get('code') return self.cleaned_data.get('code')
class TFASetupStaticForm(forms.Form): class TOTPSetupStaticForm(forms.Form):
"""Static form to show generated static tokens""" """Static form to show generated static tokens"""
tokens = forms.MultipleChoiceField(disabled=True, required=False) tokens = forms.MultipleChoiceField(disabled=True, required=False)

View File

@ -1,4 +1,4 @@
"""passbook 2FA Middleware to force users with 2FA set up to verify""" """passbook TOTP Middleware to force users with TOTP set up to verify"""
from django.shortcuts import redirect from django.shortcuts import redirect
from django.urls import reverse from django.urls import reverse
@ -6,24 +6,25 @@ from django.utils.http import urlencode
from django_otp import user_has_device from django_otp import user_has_device
def tfa_force_verify(get_response): def totp_force_verify(get_response):
"""Middleware to force 2FA Verification""" """Middleware to force TOTP Verification"""
def middleware(request): def middleware(request):
"""Middleware to force 2FA Verification""" """Middleware to force TOTP Verification"""
# pylint: disable=too-many-boolean-expressions # pylint: disable=too-many-boolean-expressions
if request.user.is_authenticated and \ if request.user.is_authenticated and \
user_has_device(request.user) and \ user_has_device(request.user) and \
not request.user.is_verified() and \ not request.user.is_verified() and \
request.path != reverse('passbook_tfa:tfa-verify') and \ request.path != reverse('passbook_totp:totp-verify') and \
request.path != reverse('account-logout') and \ request.path != reverse('account-logout') and \
not request.META.get('HTTP_AUTHORIZATION', '').startswith('Bearer'): not request.META.get('HTTP_AUTHORIZATION', '').startswith('Bearer'):
# User has 2FA set up but is not verified # User has TOTP set up but is not verified
# At this point the request is already forwarded to the target destination # At this point the request is already forwarded to the target destination
# So we just add the current request's path as next parameter # So we just add the current request's path as next parameter
args = '?%s' % urlencode({'next': request.get_full_path()}) args = '?%s' % urlencode({'next': request.get_full_path()})
return redirect(reverse('passbook_tfa:tfa-verify') + args) return redirect(reverse('passbook_totp:totp-verify') + args)
response = get_response(request) response = get_response(request)
return response return response

View File

@ -1,4 +1,4 @@
"""passbook 2FA Settings""" """passbook TOTP Settings"""
OTP_LOGIN_URL = 'passbook_tfa:tfa-verify' OTP_LOGIN_URL = 'passbook_tfa:tfa-verify'
OTP_TOTP_ISSUER = 'passbook' OTP_TOTP_ISSUER = 'passbook'

View File

@ -31,9 +31,9 @@
</p> </p>
<p> <p>
{% if not state %} {% if not state %}
<a href="{% url 'passbook_tfa:tfa-enable' %}" class="btn btn-success btn-sm">{% trans "Enable 2FA" %}</a> <a href="{% url 'passbook_tfa:tfa-enable' %}" class="btn btn-success btn-sm">{% trans "Enable TOTP" %}</a>
{% else %} {% else %}
<a href="{% url 'passbook_tfa:tfa-disable' %}" class="btn btn-danger btn-sm">{% trans "Disable 2FA" %}</a> <a href="{% url 'passbook_tfa:tfa-disable' %}" class="btn btn-danger btn-sm">{% trans "Disable TOTP" %}</a>
{% endif %} {% endif %}
</p> </p>
</div> </div>

View File

@ -7,7 +7,7 @@
{% endblock %} {% endblock %}
{% block form %} {% block form %}
<label for="">Keep these tokens somewhere safe. These are to be used if you loose your primary 2FA device.</label> <label for="">Keep these tokens somewhere safe. These are to be used if you loose your primary TOTP device.</label>
{% for field in wizard.form %} {% for field in wizard.form %}
{% if field.field.widget|fieldtype == 'SelectMultiple' %} {% if field.field.widget|fieldtype == 'SelectMultiple' %}
<ul class="list"> <ul class="list">

View File

@ -1,4 +1,4 @@
"""passbook Mod 2FA Middleware Test""" """passbook TOTP Middleware Test"""
import os import os
@ -7,19 +7,19 @@ from django.test import RequestFactory, TestCase
from django.urls import reverse from django.urls import reverse
from passbook.core.views import overview from passbook.core.views import overview
from passbook.tfa.middleware import tfa_force_verify from passbook.totp.middleware import totp_force_verify
class TestMiddleware(TestCase): class TestMiddleware(TestCase):
"""passbook 2FA Middleware Test""" """passbook TOTP Middleware Test"""
def setUp(self): def setUp(self):
os.environ['RECAPTCHA_TESTING'] = 'True' os.environ['RECAPTCHA_TESTING'] = 'True'
self.factory = RequestFactory() self.factory = RequestFactory()
def test_tfa_force_verify_anon(self): def test_totp_force_verify_anon(self):
"""Test Anonymous TFA Force""" """Test Anonymous TFA Force"""
request = self.factory.get(reverse('passbook_core:overview')) request = self.factory.get(reverse('passbook_core:overview'))
request.user = AnonymousUser() request.user = AnonymousUser()
response = tfa_force_verify(overview.OverviewView.as_view())(request) response = totp_force_verify(overview.OverviewView.as_view())(request)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)

14
passbook/totp/urls.py Normal file
View File

@ -0,0 +1,14 @@
"""passbook TOTP Urls"""
from django.urls import path
from passbook.totp import views
urlpatterns = [
path('', views.index, name='totp-index'),
path('qr/', views.qr_code, name='totp-qr'),
path('verify/', views.verify, name='totp-verify'),
# path('enable/', views.TFASetupView.as_view(), name='totp-enable'),
path('disable/', views.disable, name='totp-disable'),
path('user_settings/', views.user_settings, name='totp-user_settings'),
]

View File

@ -1,4 +1,4 @@
"""passbook Mod 2FA Utils""" """passbook Mod TOTP Utils"""
from django.conf import settings from django.conf import settings
from django.utils.http import urlencode from django.utils.http import urlencode

View File

@ -1,4 +1,4 @@
"""passbook 2FA Views""" """passbook TOTP Views"""
# from base64 import b32encode # from base64 import b32encode
# from binascii import unhexlify # from binascii import unhexlify
@ -19,8 +19,8 @@ from qrcode.image.svg import SvgPathImage
from passbook.lib.decorators import reauth_required from passbook.lib.decorators import reauth_required
# from passbook.core.models import Event # from passbook.core.models import Event
# from passbook.core.views.wizards import BaseWizardView # from passbook.core.views.wizards import BaseWizardView
from passbook.tfa.forms import TFAVerifyForm from passbook.totp.forms import TOTPVerifyForm
from passbook.tfa.utils import otpauth_url from passbook.totp.utils import otpauth_url
TFA_SESSION_KEY = 'passbook_2fa_key' TFA_SESSION_KEY = 'passbook_2fa_key'
@ -30,22 +30,22 @@ TFA_SESSION_KEY = 'passbook_2fa_key'
def index(request: HttpRequest) -> HttpResponse: def index(request: HttpRequest) -> HttpResponse:
"""Show empty index page""" """Show empty index page"""
return render(request, 'core/generic.html', { return render(request, 'core/generic.html', {
'text': 'Test 2FA passed' 'text': 'Test TOTP passed'
}) })
@login_required @login_required
def verify(request: HttpRequest) -> HttpResponse: def verify(request: HttpRequest) -> HttpResponse:
"""Verify 2FA Token""" """Verify TOTP Token"""
if not user_has_device(request.user): if not user_has_device(request.user):
messages.error(request, _("You don't have 2-Factor Authentication set up.")) messages.error(request, _("You don't have 2-Factor Authentication set up."))
if request.method == 'POST': if request.method == 'POST':
form = TFAVerifyForm(request.POST) form = TOTPVerifyForm(request.POST)
if form.is_valid(): if form.is_valid():
device = match_token(request.user, form.cleaned_data.get('code')) device = match_token(request.user, form.cleaned_data.get('code'))
if device: if device:
login(request, device) login(request, device)
messages.success(request, _('Successfully validated 2FA Token.')) messages.success(request, _('Successfully validated TOTP Token.'))
# Check if there is a next GET parameter and redirect to that # Check if there is a next GET parameter and redirect to that
if 'next' in request.GET: if 'next' in request.GET:
return redirect(request.GET.get('next')) return redirect(request.GET.get('next'))
@ -53,7 +53,7 @@ def verify(request: HttpRequest) -> HttpResponse:
return redirect(reverse('common-index')) return redirect(reverse('common-index'))
messages.error(request, _('Invalid 2-Factor Token.')) messages.error(request, _('Invalid 2-Factor Token.'))
else: else:
form = TFAVerifyForm() form = TOTPVerifyForm()
return render(request, 'generic/form_login.html', { return render(request, 'generic/form_login.html', {
'form': form, 'form': form,
@ -67,13 +67,13 @@ def verify(request: HttpRequest) -> HttpResponse:
@login_required @login_required
def user_settings(request: HttpRequest) -> HttpResponse: def user_settings(request: HttpRequest) -> HttpResponse:
"""View for user settings to control 2FA""" """View for user settings to control TOTP"""
static = get_object_or_404(StaticDevice, user=request.user, confirmed=True) static = get_object_or_404(StaticDevice, user=request.user, confirmed=True)
static_tokens = StaticToken.objects.filter(device=static).order_by('token') static_tokens = StaticToken.objects.filter(device=static).order_by('token')
finished_totp_devices = TOTPDevice.objects.filter(user=request.user, confirmed=True) finished_totp_devices = TOTPDevice.objects.filter(user=request.user, confirmed=True)
finished_static_devices = StaticDevice.objects.filter(user=request.user, confirmed=True) finished_static_devices = StaticDevice.objects.filter(user=request.user, confirmed=True)
state = finished_totp_devices.exists() and finished_static_devices.exists() state = finished_totp_devices.exists() and finished_static_devices.exists()
return render(request, 'tfa/user_settings.html', { return render(request, 'totp/user_settings.html', {
'static_tokens': static_tokens, 'static_tokens': static_tokens,
'state': state, 'state': state,
}) })
@ -83,7 +83,7 @@ def user_settings(request: HttpRequest) -> HttpResponse:
@reauth_required @reauth_required
@otp_required @otp_required
def disable(request: HttpRequest) -> HttpResponse: def disable(request: HttpRequest) -> HttpResponse:
"""Disable 2FA for user""" """Disable TOTP for user"""
# Delete all the devices for user # Delete all the devices for user
static = get_object_or_404(StaticDevice, user=request.user, confirmed=True) static = get_object_or_404(StaticDevice, user=request.user, confirmed=True)
static_tokens = StaticToken.objects.filter(device=static).order_by('token') static_tokens = StaticToken.objects.filter(device=static).order_by('token')
@ -92,11 +92,11 @@ def disable(request: HttpRequest) -> HttpResponse:
totp.delete() totp.delete()
for token in static_tokens: for token in static_tokens:
token.delete() token.delete()
messages.success(request, 'Successfully disabled 2FA') messages.success(request, 'Successfully disabled TOTP')
# Create event with email notification # Create event with email notification
# Event.create( # Event.create(
# user=request.user, # user=request.user,
# message=_('You disabled 2FA.'), # message=_('You disabled TOTP.'),
# current=True, # current=True,
# request=request, # request=request,
# send_notification=True) # send_notification=True)
@ -108,7 +108,7 @@ def disable(request: HttpRequest) -> HttpResponse:
# class TFASetupView(BaseWizardView): # class TFASetupView(BaseWizardView):
# """Wizard to create a Mail Account""" # """Wizard to create a Mail Account"""
# title = _('Set up 2FA') # title = _('Set up TOTP')
# form_list = [TFASetupInitForm, TFASetupStaticForm] # form_list = [TFASetupInitForm, TFASetupStaticForm]
# totp_device = None # totp_device = None
@ -117,15 +117,15 @@ def disable(request: HttpRequest) -> HttpResponse:
# def get_template_names(self): # def get_template_names(self):
# if self.steps.current == '1': # if self.steps.current == '1':
# return 'tfa/wizard_setup_static.html' # return 'totp/wizard_setup_static.html'
# return self.template_name # return self.template_name
# def handle_request(self, request: HttpRequest): # def handle_request(self, request: HttpRequest):
# # Check if user has 2FA setup already # # Check if user has TOTP setup already
# finished_totp_devices = TOTPDevice.objects.filter(user=request.user, confirmed=True) # finished_totp_devices = TOTPDevice.objects.filter(user=request.user, confirmed=True)
# finished_static_devices = StaticDevice.objects.filter(user=request.user, confirmed=True) # finished_static_devices = StaticDevice.objects.filter(user=request.user, confirmed=True)
# if finished_totp_devices.exists() or finished_static_devices.exists(): # if finished_totp_devices.exists() or finished_static_devices.exists():
# messages.error(request, _('You already have 2FA enabled!')) # messages.error(request, _('You already have TOTP enabled!'))
# return redirect(reverse('common-index')) # return redirect(reverse('common-index'))
# # Check if there's an unconfirmed device left to set up # # Check if there's an unconfirmed device left to set up
# totp_devices = TOTPDevice.objects.filter(user=request.user, confirmed=False) # totp_devices = TOTPDevice.objects.filter(user=request.user, confirmed=False)
@ -182,7 +182,7 @@ def disable(request: HttpRequest) -> HttpResponse:
# # Create event with email notification # # Create event with email notification
# Event.create( # Event.create(
# user=self.request.user, # user=self.request.user,
# message=_('You activated 2FA.'), # message=_('You activated TOTP.'),
# current=True, # current=True,
# request=self.request, # request=self.request,
# send_notification=True) # send_notification=True)