diff --git a/CHANGELOG.md b/CHANGELOG.md index 824bf25..182523f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 2.2.0 (Not released) + * Added: MFA_REDIRECT_AFTER_REGISTRATION settings parameter + ## 2.1.2 * Fixed: Getting timestamp on Python 3.7 as ("%s") is raising an exception * Upgraded to FIDO 0.9.1 diff --git a/README.md b/README.md index c777bb4..77a147a 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,8 @@ Depends on MFA_UNALLOWED_METHODS=() # Methods that shouldn't be allowed for the user MFA_LOGIN_CALLBACK="" # A function that should be called by username to login the user in session MFA_RECHECK=True # Allow random rechecking of the user + MFA_REDIRECT_AFTER_REGISTRATION="mfa_home" # Allows Changing the page after successful registeration + MFA_SUCCESS_REGISTRATION_MSG = "Go to Security Home" # The text of the link MFA_RECHECK_MIN=10 # Minimum interval in seconds MFA_RECHECK_MAX=30 # Maximum in seconds MFA_QUICKLOGIN=True # Allow quick login for returning users by provide only their 2FA @@ -91,6 +93,8 @@ Depends on **Notes**: * Starting version 1.1, ~~FIDO_LOGIN_URL~~ isn't required for FIDO2 anymore. * Starting version 1.7.0, Key owners can be specified. + * Starting version 2.2.0 + * Added: `MFA_SUCCESS_REGISTRATION_MSG` & `MFA_REDIRECT_AFTER_REGISTRATION` 1. Break your login function Usually your login function will check for username and password, log the user in if the username and password are correct and create the user session, to support mfa, this has to change diff --git a/docs/change_login.md b/docs/change_login.md index 32e3617..d1425e6 100644 --- a/docs/change_login.md +++ b/docs/change_login.md @@ -9,15 +9,15 @@ Usually your login function will check for username and password, log the user i * if user has mfa then redirect to mfa page * if user doesn't have mfa then call your function to create the user session - - def login(request): # this function handles the login form POST - user = auth.authenticate(username=username, password=password) - if user is not None: # if the user object exist - from mfa.helpers import has_mfa - res = has_mfa(username = username,request=request) # has_mfa returns false or HttpResponseRedirect - if res: - return res - return log_user_in(request,username=user.username) +```python +def login(request): # this function handles the login form POST + user = auth.authenticate(username=username, password=password) + if user is not None: # if the user object exist + from mfa.helpers import has_mfa + res = has_mfa(username = username,request=request) # has_mfa returns false or HttpResponseRedirect + if res: + return res + return log_user_in(request,username=user.username) #log_user_in is a function that handles creatung user session, it should be in the setting file as MFA_CALLBACK - +``` diff --git a/example/example/settings.py b/example/example/settings.py index fe33941..37735a2 100644 --- a/example/example/settings.py +++ b/example/example/settings.py @@ -77,10 +77,8 @@ WSGI_APPLICATION = 'example.wsgi.application' DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.mysql', - 'NAME': 'mfa', - 'USER': 'root', - 'PASSWORD': 'password', + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': 'test_db', } } @@ -141,7 +139,9 @@ MFA_RECHECK=True # Allow random rechecking of the user MFA_RECHECK_MIN=10 # Minimum interval in seconds MFA_RECHECK_MAX=30 # Maximum in seconds MFA_QUICKLOGIN=True # Allow quick login for returning users by provide only their 2FA -MFA_HIDE_DISABLE=('FIDO2',) # Can the user disable his key (Added in 1.2.0). +MFA_HIDE_DISABLE=('',) # Can the user disable his key (Added in 1.2.0). +MFA_REDIRECT_AFTER_REGISTRATION="registered" +MFA_SUCCESS_REGISTRATION_MSG="Go to Home" TOKEN_ISSUER_NAME="PROJECT_NAME" #TOTP Issuer name diff --git a/example/example/templates/home.html b/example/example/templates/home.html index b76df9e..c4bdbbd 100644 --- a/example/example/templates/home.html +++ b/example/example/templates/home.html @@ -12,6 +12,9 @@ + {% if registered %} +
Registered Successfully
+ {% endif %}

Welcome {{ request.user.username }}!


diff --git a/example/example/urls.py b/example/example/urls.py index 3691a81..bd0dc4d 100644 --- a/example/example/urls.py +++ b/example/example/urls.py @@ -22,5 +22,6 @@ urlpatterns = [ path('auth/login',auth.loginView,name="login"), path('auth/logout',auth.logoutView,name="logout"), - re_path('^$',views.home,name='home') + re_path('^$',views.home,name='home'), + path('registered/',views.registered,name='registered') ] diff --git a/example/example/views.py b/example/example/views.py index f615692..98e80e9 100644 --- a/example/example/views.py +++ b/example/example/views.py @@ -5,3 +5,7 @@ from django.contrib.auth.decorators import login_required @login_required() def home(request): return render(request,"home.html",{}) + +@login_required() +def registered(request): + return render(request,"home.html",{"registered":True}) diff --git a/example/requiremnts.txt b/example/requiremnts.txt index 9a1fda4..ee8e548 100644 --- a/example/requiremnts.txt +++ b/example/requiremnts.txt @@ -1,2 +1,2 @@ -django==2.0 -django-sslserver +django >= 2.2 +django_ssl diff --git a/mfa/Common.py b/mfa/Common.py index 340c82b..1504340 100644 --- a/mfa/Common.py +++ b/mfa/Common.py @@ -1,5 +1,9 @@ from django.conf import settings from django.core.mail import EmailMessage +try: + from django.urls import reverse +except: + from django.core.urlresolver import reverse def send(to,subject,body): from_email_address = settings.EMAIL_HOST_USER @@ -8,4 +12,8 @@ def send(to,subject,body): From = "%s <%s>" % (settings.EMAIL_FROM, from_email_address) email = EmailMessage(subject,body,From,to) email.content_subtype = "html" - return email.send(False) \ No newline at end of file + return email.send(False) + +def get_redirect_url(): + return {"redirect_html": reverse(getattr(settings, 'MFA_REDIRECT_AFTER_REGISTRATION', 'mfa_home')), + "reg_success_msg":getattr(settings,"MFA_SUCCESS_REGISTRATION_MSG")} diff --git a/mfa/Email.py b/mfa/Email.py index 9533c5d..010d6d5 100644 --- a/mfa/Email.py +++ b/mfa/Email.py @@ -7,7 +7,9 @@ from .models import * #from django.template.context import RequestContext from .views import login from .Common import send + def sendEmail(request,username,secret): + """Send Email to the user after rendering `mfa_email_token_template`""" from django.contrib.auth import get_user_model User = get_user_model() key = getattr(User, 'USERNAME_FIELD', 'username') @@ -18,9 +20,10 @@ def sendEmail(request,username,secret): @never_cache def start(request): + """Start adding email as a 2nd factor""" context = csrf(request) if request.method == "POST": - if request.session["email_secret"] == request.POST["otp"]: + if request.session["email_secret"] == request.POST["otp"]: #if successful uk=User_Keys() uk.username=request.user.username uk.key_type="Email" @@ -31,15 +34,16 @@ def start(request): from django.core.urlresolvers import reverse except: from django.urls import reverse - return HttpResponseRedirect(reverse('mfa_home')) + return HttpResponseRedirect(reverse(getattr(settings,'MFA_REDIRECT_AFTER_REGISTRATION','mfa_home'))) context["invalid"] = True else: - request.session["email_secret"] = str(randint(0,100000)) + request.session["email_secret"] = str(randint(0,100000)) #generate a random integer if sendEmail(request, request.user.username, request.session["email_secret"]): context["sent"] = True return render(request,"Email/Add.html", context) @never_cache def auth(request): + """Authenticating the user by email.""" context=csrf(request) if request.method=="POST": if request.session["email_secret"]==request.POST["otp"].strip(): diff --git a/mfa/FIDO2.py b/mfa/FIDO2.py index addda1c..98a6e5e 100644 --- a/mfa/FIDO2.py +++ b/mfa/FIDO2.py @@ -14,10 +14,12 @@ from fido2.utils import websafe_decode, websafe_encode from fido2.ctap2 import AttestedCredentialData from .views import login, reset_cookie import datetime +from .Common import get_redirect_url from django.utils import timezone def recheck(request): + """Starts FIDO2 recheck""" context = csrf(request) context["mode"] = "recheck" request.session["mfa_recheck"] = True @@ -25,11 +27,13 @@ def recheck(request): def getServer(): + """Get Server Info from settings and returns a Fido2Server""" rp = PublicKeyCredentialRpEntity(settings.FIDO_SERVER_ID, settings.FIDO_SERVER_NAME) return Fido2Server(rp) def begin_registeration(request): + """Starts registering a new FIDO Device, called from API""" server = getServer() registration_data, state = server.register_begin({ u'id': request.user.username.encode("utf8"), @@ -43,6 +47,7 @@ def begin_registeration(request): @csrf_exempt def complete_reg(request): + """Completes the registeration, called by API""" try: data = cbor.decode(request.body) @@ -72,7 +77,9 @@ def complete_reg(request): def start(request): + """Start Registeration a new FIDO Token""" context = csrf(request) + context.update(get_redirect_url()) return render(request, "FIDO2/Add.html", context) diff --git a/mfa/U2F.py b/mfa/U2F.py index f7da5dc..fe5ea19 100644 --- a/mfa/U2F.py +++ b/mfa/U2F.py @@ -76,6 +76,7 @@ def start(request): request.session['_u2f_enroll_'] = enroll.json context=csrf(request) context["token"]=simplejson.dumps(enroll.data_for_client) + context.update(get_redirect_url()) return render(request,"U2F/Add.html",context) diff --git a/mfa/__init__.py b/mfa/__init__.py index 0f81ac0..e5dafea 100644 --- a/mfa/__init__.py +++ b/mfa/__init__.py @@ -1 +1 @@ -__version__="2.1.2" +__version__="2.2.0b1" diff --git a/mfa/models.py b/mfa/models.py index b97123d..2f0d682 100644 --- a/mfa/models.py +++ b/mfa/models.py @@ -2,9 +2,9 @@ from django.db import models from jsonfield import JSONField from jose import jwt from django.conf import settings -from jsonLookup import shasLookup, hasLookup -JSONField.register_lookup(shasLookup) -JSONField.register_lookup(hasLookup) +#from jsonLookup import shasLookup, hasLookup +# JSONField.register_lookup(shasLookup) +# JSONField.register_lookup(hasLookup) class User_Keys(models.Model): diff --git a/mfa/templates/FIDO2/Add.html b/mfa/templates/FIDO2/Add.html index 6ff10b9..74381cf 100644 --- a/mfa/templates/FIDO2/Add.html +++ b/mfa/templates/FIDO2/Add.html @@ -32,7 +32,7 @@ }).then(function (res) { if (res["status"] =='OK') - $("#res").html("
Registered Successfully, Go to Security Home
") + $("#res").html("
Registered Successfully, {{reg_success_msg}}
") else $("#res").html("
Registeration Failed as " + res["message"] + ", try again or Go to Security Home
") diff --git a/mfa/templates/TOTP/Add.html b/mfa/templates/TOTP/Add.html index 5bae45f..d303f96 100644 --- a/mfa/templates/TOTP/Add.html +++ b/mfa/templates/TOTP/Add.html @@ -43,7 +43,7 @@ else { alert("Your authenticator is added successfully.") - window.location.href="{% url 'mfa_home' %}" + window.location.href="{{ redirect_html }}" } } }) diff --git a/mfa/templates/U2F/Add.html b/mfa/templates/U2F/Add.html index c354509..0eefcd4 100644 --- a/mfa/templates/U2F/Add.html +++ b/mfa/templates/U2F/Add.html @@ -24,7 +24,7 @@ if (data == "OK") { alert("Your device is added successfully.") - window.location.href="{% url 'mfa_home' %}" + window.location.href="{{ redirect_html }}" } } }) diff --git a/mfa/totp.py b/mfa/totp.py index c0c008a..96617c7 100644 --- a/mfa/totp.py +++ b/mfa/totp.py @@ -72,4 +72,5 @@ def verify(request): @never_cache def start(request): - return render(request,"TOTP/Add.html",{}) + """Start Adding Time One Time Password (TOTP)""" + return render(request,"TOTP/Add.html",get_redirect_url()) diff --git a/requirements.txt b/requirements.txt index da1c07d..3b3941a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -django >= 1.7 +django >= 2.0 jsonfield simplejson pyotp @@ -6,5 +6,5 @@ python-u2flib-server ua-parser user-agents python-jose -fido2 == 0.9.0 +fido2 == 0.9.1 jsonLookup diff --git a/setup.py b/setup.py index 4dac42d..8958ee5 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import find_packages, setup setup( name='django-mfa2', - version='2.1.2', + version='2.2.0b1', description='Allows user to add 2FA to their accounts', long_description=open("README.md").read(), long_description_content_type="text/markdown", @@ -31,10 +31,9 @@ setup( include_package_data=True, zip_safe=False, # because we're including static files classifiers=[ - "Development Status :: 5 - Production/Stable", + "Development Status :: 4 - Beta", "Environment :: Web Environment", "Framework :: Django", - "Framework :: Django :: 1.11", "Framework :: Django :: 2.0", "Framework :: Django :: 2.1", "Framework :: Django :: 2.2",