Controlling TOTP Authetication

This commit is contained in:
Mohamed El-Kalioby
2021-06-24 18:42:33 +03:00
parent 0b83758625
commit ca49e1623d
8 changed files with 91 additions and 29 deletions

View File

@@ -1,5 +1,9 @@
# Change Log
## 2.3.0
* Some code cleanup thanks to @xi
* Added: MFA_TOTP_FAILURE_WINDOW & MFA_TOTP_FAILURE_LIMIT
## 2.2.0
* Added: MFA_REDIRECT_AFTER_REGISTRATION settings parameter
* Fixed: Deprecation error for NullBooleanField

View File

@@ -78,7 +78,9 @@ Depends on
MFA_OWNED_BY_ENTERPRISE = FALSE # Who owns security keys
TOKEN_ISSUER_NAME="PROJECT_NAME" #TOTP Issuer name
MFA_TOTP_FAILURE_LIMIT = 3 # Allowed TOTP Failures / user
MFA_TOTP_FAILURE_WINDOW = 5 # The number of minutes to check failed logins against.
U2F_APPID="https://localhost" #URL For U2F
FIDO_SERVER_ID=u"localehost" # Server rp id for FIDO2, it the full domain of your project
FIDO_SERVER_NAME=u"PROJECT_NAME"
@@ -96,6 +98,8 @@ Depends on
* Starting version 1.7.0, Key owners can be specified.
* Starting version 2.2.0
* Added: `MFA_SUCCESS_REGISTRATION_MSG` & `MFA_REDIRECT_AFTER_REGISTRATION`
* Starting version 2.3.0
* Added: `MFA_TOTP_FAILURE_LIMIT` & `MFA_TOTP_FAILURE_WINDOW`
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

View File

@@ -86,20 +86,20 @@ DATABASES = {
# Password validation
# https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]
# AUTH_PASSWORD_VALIDATORS = [
# {
# "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
# },
# {
# "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
# },
# {
# "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
# },
# {
# "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
# },
# ]
# Internationalization

View File

@@ -6,12 +6,12 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('mfa', '0011_auto_20210530_0622'),
("mfa", "0011_auto_20210530_0622"),
]
operations = [
migrations.RenameModel(
old_name='User_Keys',
new_name='UserKey',
old_name="User_Keys",
new_name="UserKey",
),
]

View File

@@ -0,0 +1,35 @@
# Generated by Django 2.2 on 2021-06-24 15:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("mfa", "0012_rename_user_keys_userkey"),
]
operations = [
migrations.CreateModel(
name="OTPTracker",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("actor", models.CharField(max_length=50)),
("value", models.CharField(max_length=6)),
("success", models.BooleanField(blank=True)),
("done_on", models.DateTimeField(auto_now=True)),
],
),
migrations.AddIndex(
model_name="otptracker",
index=models.Index(fields=["actor"], name="mfa_otptrac_usernam_1f423f_idx"),
),
]

View File

@@ -33,11 +33,13 @@ class UserKey(models.Model):
class OTPTracker(models.Model):
username = models.CharField(max_length=50)
value = models.CharField(max_length = 6)
actor = models.CharField(
max_length=50, help_text="Username"
) # named this way for indexing purpose.
value = models.CharField(max_length=6)
success = models.BooleanField(blank=True)
done_on = models.DateTimeField(auto_now=True)
class Meta:
app_label = 'mfa'
indexes = [models.Index(fields=['username'])]
app_label = "mfa"
indexes = [models.Index(fields=["actor"])]

View File

@@ -28,7 +28,7 @@
{% csrf_token %}
{% if invalid %}
<div class="alert alert-danger">
Sorry, The provided token is not valid.
{{ invalid_msg }}
</div>
{% endif %}
{% if quota %}

View File

@@ -16,17 +16,34 @@ from .views import login
def verify_login(request, username, token):
FAILURE_LIMIT = getattr("settings", "MFA_TOTP_FAILURE_LIMIT", 3)
start_time = timezone.now() + datetime.timedelta(
minutes=-1 * getattr(settings, "MFA_TOTP_FAILURE_WINDOW", 5)
)
if (
OTPTracker.objects.filter(
done_on__gt=start_time, actor=username, success=0
).count()
>= FAILURE_LIMIT
):
return [
False,
"Using this method is temporarily suspended on your account, use another method, or later again later ",
]
for key in UserKey.objects.filter(username=username, key_type="TOTP"):
totp = pyotp.TOTP(key.properties["secret_key"])
if totp.verify(token, valid_window=30):
if OTPTracker.objects.filter(username=username, value=token).exists():
return [False, "Used Before, please generate another token"]
TOTP_Tracker.objects.create(username=username,value=token, success=True)
if OTPTracker.objects.filter(actor=username, value=token).exists():
return [
False,
"This code is used before, please generate another token",
]
OTPTracker.objects.create(actor=username, value=token, success=True)
key.last_used = timezone.now()
key.save()
return [True, key.id]
TOTP_Tracker.objects.create(username = username, value = token, success = False)
return [False,"Invalid Token"]
OTPTracker.objects.create(actor=username, value=token, success=False)
return [False, "Invalid Token"]
def recheck(request):
@@ -68,7 +85,7 @@ def auth(request):
return render(request, "TOTP/Auth.html", context)
def getToken(request):
def get_token(request):
secret_key = pyotp.random_base32()
totp = pyotp.TOTP(secret_key)
request.session["new_mfa_answer"] = totp.now()