Skip to content

Commit 0c3a1e9

Browse files
committed
Alot of fixes
1 parent 3a7b578 commit 0c3a1e9

File tree

19 files changed

+307
-262
lines changed

19 files changed

+307
-262
lines changed

CHANGELOG.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
# Change Log
22

33
## 3.0 (Beta)
4+
5+
This is a major cleanup and CSS adjustments so please test before deployment.
6+
47
* Updated to fido2==1.1.3
5-
* Removed: CBOR and exchange is done in JSON now
8+
* Removed: CBOR and exchange is done in JSON now.
9+
* Removed: `simplejson` package from dependencies.
10+
* Email OTP is always 6 numbers.
11+
* Better support for bootstrap 4 and 5.
612
* Added: the following settings
713
* `MFA_FIDO2_RESIDENT_KEY`: Defaults to `Discouraged` which was the old behaviour
814
* `MFA_FIDO2_AUTHENTICATOR_ATTACHMENT`: If you like to have a PLATFORM Authenticator, Defaults to NONE
915
* `MFA_FIDO2_USER_VERIFICATION`: If you need User Verification
1016
* `MFA_FIDO2_ATTESTATION_PREFERENCE`: If you like to have an Attention
17+
* `MFA_ENFORCE_EMAIL_TOKEN`: if you want the user to receive OTP by email without enrolling, if this the case, the system admins shall make sure that emails are valid.
18+
* `MFA_SHOW_OTP_IN_EMAIL_SUBJECT`: If you like to show the OTP in the email subject
19+
* `MFA_OTP_EMAIL_SUBJECT`: The subject of the email after the token allows placeholder '%s' for otp
1120

1221
## 2.9.0
1322
* Add: Set black as code formatter

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ Depends on
8080

8181
```python
8282
from django.conf.global_settings import PASSWORD_HASHERS as DEFAULT_PASSWORD_HASHERS #Preferably at the same place where you import your other modules
83+
8384
MFA_UNALLOWED_METHODS=() # Methods that shouldn't be allowed for the user e.g ('TOTP','U2F',)
8485
MFA_LOGIN_CALLBACK="" # A function that should be called by username to login the user in session
8586
MFA_RECHECK=True # Allow random rechecking of the user
@@ -91,7 +92,7 @@ Depends on
9192
MFA_ALWAYS_GO_TO_LAST_METHOD = False # Always redirect the user to the last method used to save a click (Added in 2.6.0).
9293
MFA_RENAME_METHODS={} #Rename the methods in a more user-friendly way e.g {"RECOVERY":"Backup Codes"} (Added in 2.6.0)
9394
MFA_HIDE_DISABLE=('FIDO2',) # Can the user disable his key (Added in 1.2.0).
94-
MFA_OWNED_BY_ENTERPRISE = FALSE # Who owns security keys
95+
MFA_OWNED_BY_ENTERPRISE = False # Who owns security keys
9596
PASSWORD_HASHERS = DEFAULT_PASSWORD_HASHERS # Comment if PASSWORD_HASHER already set in your settings.py
9697
PASSWORD_HASHERS += ['mfa.recovery.Hash']
9798
RECOVERY_ITERATION = 350000 #Number of iteration for recovery code, higher is more secure, but uses more resources for generation and check...
@@ -101,10 +102,16 @@ Depends on
101102
U2F_APPID="https://localhost" #URL For U2F
102103
FIDO_SERVER_ID=u"localehost" # Server rp id for FIDO2, it is the full domain of your project
103104
FIDO_SERVER_NAME=u"PROJECT_NAME"
105+
106+
import mfa
104107
MFA_FIDO2_RESIDENT_KEY = mfa.ResidentKey.DISCOURAGED # Resident Key allows a special User Handle
105108
MFA_FIDO2_AUTHENTICATOR_ATTACHMENT = None # Let the user choose
106109
MFA_FIDO2_USER_VERIFICATION = None # Verify User Presence
107110
MFA_FIDO2_ATTESTATION_PREFERENCE = mfa.AttestationPreference.NONE
111+
112+
MFA_ENFORCE_EMAIL_TOKEN = False # If you want the user to receive OTP by email without enrolling, if this the case, the system admins shall make sure that emails are valid.
113+
MFA_SHOW_OTP_IN_EMAIL_SUBJECT = False #If you like to show the OTP in the email subject
114+
MFA_OTP_EMAIL_SUBJECT= "OTP" # The subject of the email after the token
108115
```
109116
**Method Names**
110117
* U2F
@@ -123,6 +130,7 @@ Depends on
123130
* Added: `MFA_ALWAYS_GO_TO_LAST_METHOD`, `MFA_RENAME_METHODS`, `MFA_ENFORCE_RECOVERY_METHOD` & `RECOVERY_ITERATION`
124131
* Starting version 3.0
125132
* Added: `MFA_FIDO2_RESIDENT_KEY`, `MFA_FIDO2_AUTHENTICATOR_ATTACHMENT`, `MFA_FIDO2_USER_VERIFICATION`, `MFA_FIDO2_ATTESTATION_PREFERENCE`
133+
* Added: `MFA_ENFORCE_EMAIL_TOKEN`, `MFA_SHOW_OTP_IN_EMAIL_SUBJECT`, `MFA_OTP_EMAIL_SUBJECT`
126134
4. Break your login function
127135

128136
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

mfa/Common.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1+
import datetime
2+
from random import randint
3+
14
from django.conf import settings
5+
from django.contrib.auth import get_user_model
26
from django.core.mail import EmailMessage
37

48
try:
@@ -24,3 +28,20 @@ def get_redirect_url():
2428
),
2529
"reg_success_msg": getattr(settings, "MFA_SUCCESS_REGISTRATION_MSG"),
2630
}
31+
32+
33+
def get_username_field():
34+
User = get_user_model()
35+
USERNAME_FIELD = getattr(User, "USERNAME_FIELD", "username")
36+
return User, USERNAME_FIELD
37+
38+
39+
def set_next_recheck():
40+
if getattr(settings, "MFA_RECHECK", False):
41+
delta = datetime.timedelta(
42+
seconds=randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX)
43+
)
44+
return {
45+
"next_check": datetime.datetime.timestamp(datetime.datetime.now() + delta)
46+
}
47+
return {}

mfa/Email.py

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,26 @@
1010
from .models import User_Keys
1111

1212
from .views import login
13-
from .Common import send
13+
from .Common import send, get_username_field, set_next_recheck
1414

1515

1616
def sendEmail(request, username, secret):
1717
"""Send Email to the user after rendering `mfa_email_token_template`"""
18-
19-
User = get_user_model()
20-
key = getattr(User, "USERNAME_FIELD", "username")
21-
kwargs = {key: username}
18+
User, UsernameField = get_username_field()
19+
kwargs = {UsernameField: username}
2220
user = User.objects.get(**kwargs)
2321
res = render(
2422
request,
2523
"mfa_email_token_template.html",
2624
{"request": request, "user": user, "otp": secret},
2725
)
28-
return send([user.email], "OTP", res.content.decode())
26+
subject = getattr(settings, "MFA_OTP_EMAIL_SUBJECT", "OTP")
27+
if getattr(settings, "MFA_SHOW_OTP_IN_EMAIL_SUBJECT", False):
28+
if "%s" in subject:
29+
subject = subject % secret
30+
else:
31+
subject = secret + " " + subject
32+
return send([user.email], subject, res.content.decode())
2933

3034

3135
@never_cache
@@ -35,7 +39,8 @@ def start(request):
3539
if request.method == "POST":
3640
if request.session["email_secret"] == request.POST["otp"]: # if successful
3741
uk = User_Keys()
38-
uk.username = request.user.username
42+
User, USERNAME_FIELD = get_username_field()
43+
uk.username = USERNAME_FIELD
3944
uk.key_type = "Email"
4045
uk.enabled = 1
4146
uk.save()
@@ -64,8 +69,8 @@ def start(request):
6469
)
6570
context["invalid"] = True
6671
else:
67-
request.session["email_secret"] = str(
68-
randint(0, 100000)
72+
request.session["email_secret"] = str(randint(0, 1000000)).zfill(
73+
6
6974
) # generate a random integer
7075

7176
if sendEmail(request, request.user.username, request.session["email_secret"]):
@@ -78,20 +83,23 @@ def auth(request):
7883
"""Authenticating the user by email."""
7984
context = csrf(request)
8085
if request.method == "POST":
86+
username = request.session["base_username"]
87+
8188
if request.session["email_secret"] == request.POST["otp"].strip():
82-
uk = User_Keys.objects.get(
83-
username=request.session["base_username"], key_type="Email"
84-
)
89+
email_keys = User_Keys.objects.filter(username=username, key_type="Email")
90+
if email_keys.exists():
91+
uk = email_keys.first()
92+
elif getattr(settings, "MFA_ENFORCE_EMAIL_TOKEN", False):
93+
uk = User_Keys()
94+
uk.username = username
95+
uk.key_type = "Email"
96+
uk.enabled = 1
97+
uk.save()
98+
else:
99+
raise Exception("Email is not a valid method for this user")
100+
85101
mfa = {"verified": True, "method": "Email", "id": uk.id}
86-
if getattr(settings, "MFA_RECHECK", False):
87-
mfa["next_check"] = datetime.datetime.timestamp(
88-
datetime.datetime.now()
89-
+ datetime.timedelta(
90-
seconds=random.randint(
91-
settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX
92-
)
93-
)
94-
)
102+
mfa.update(set_next_recheck())
95103
request.session["mfa"] = mfa
96104

97105
from django.utils import timezone
@@ -101,7 +109,7 @@ def auth(request):
101109
return login(request)
102110
context["invalid"] = True
103111
else:
104-
request.session["email_secret"] = str(randint(0, 100000))
112+
request.session["email_secret"] = str(randint(0, 1000000)).zfill(6)
105113
if sendEmail(
106114
request, request.session["base_username"], request.session["email_secret"]
107115
):

0 commit comments

Comments
 (0)