Skip to content

Commit

Permalink
Alot of fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
mkalioby committed Jul 4, 2024
1 parent 3a7b578 commit 0c3a1e9
Show file tree
Hide file tree
Showing 19 changed files with 307 additions and 262 deletions.
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
# Change Log

## 3.0 (Beta)

This is a major cleanup and CSS adjustments so please test before deployment.

* Updated to fido2==1.1.3
* Removed: CBOR and exchange is done in JSON now
* Removed: CBOR and exchange is done in JSON now.
* Removed: `simplejson` package from dependencies.
* Email OTP is always 6 numbers.
* Better support for bootstrap 4 and 5.
* Added: the following settings
* `MFA_FIDO2_RESIDENT_KEY`: Defaults to `Discouraged` which was the old behaviour
* `MFA_FIDO2_AUTHENTICATOR_ATTACHMENT`: If you like to have a PLATFORM Authenticator, Defaults to NONE
* `MFA_FIDO2_USER_VERIFICATION`: If you need User Verification
* `MFA_FIDO2_ATTESTATION_PREFERENCE`: If you like to have an Attention
* `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.
* `MFA_SHOW_OTP_IN_EMAIL_SUBJECT`: If you like to show the OTP in the email subject
* `MFA_OTP_EMAIL_SUBJECT`: The subject of the email after the token allows placeholder '%s' for otp

## 2.9.0
* Add: Set black as code formatter
Expand Down
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ Depends on

```python
from django.conf.global_settings import PASSWORD_HASHERS as DEFAULT_PASSWORD_HASHERS #Preferably at the same place where you import your other modules

MFA_UNALLOWED_METHODS=() # Methods that shouldn't be allowed for the user e.g ('TOTP','U2F',)
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
Expand All @@ -91,7 +92,7 @@ Depends on
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).
MFA_RENAME_METHODS={} #Rename the methods in a more user-friendly way e.g {"RECOVERY":"Backup Codes"} (Added in 2.6.0)
MFA_HIDE_DISABLE=('FIDO2',) # Can the user disable his key (Added in 1.2.0).
MFA_OWNED_BY_ENTERPRISE = FALSE # Who owns security keys
MFA_OWNED_BY_ENTERPRISE = False # Who owns security keys
PASSWORD_HASHERS = DEFAULT_PASSWORD_HASHERS # Comment if PASSWORD_HASHER already set in your settings.py
PASSWORD_HASHERS += ['mfa.recovery.Hash']
RECOVERY_ITERATION = 350000 #Number of iteration for recovery code, higher is more secure, but uses more resources for generation and check...
Expand All @@ -101,10 +102,16 @@ Depends on
U2F_APPID="https://localhost" #URL For U2F
FIDO_SERVER_ID=u"localehost" # Server rp id for FIDO2, it is the full domain of your project
FIDO_SERVER_NAME=u"PROJECT_NAME"

import mfa
MFA_FIDO2_RESIDENT_KEY = mfa.ResidentKey.DISCOURAGED # Resident Key allows a special User Handle
MFA_FIDO2_AUTHENTICATOR_ATTACHMENT = None # Let the user choose
MFA_FIDO2_USER_VERIFICATION = None # Verify User Presence
MFA_FIDO2_ATTESTATION_PREFERENCE = mfa.AttestationPreference.NONE

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.
MFA_SHOW_OTP_IN_EMAIL_SUBJECT = False #If you like to show the OTP in the email subject
MFA_OTP_EMAIL_SUBJECT= "OTP" # The subject of the email after the token
```
**Method Names**
* U2F
Expand All @@ -123,6 +130,7 @@ Depends on
* Added: `MFA_ALWAYS_GO_TO_LAST_METHOD`, `MFA_RENAME_METHODS`, `MFA_ENFORCE_RECOVERY_METHOD` & `RECOVERY_ITERATION`
* Starting version 3.0
* Added: `MFA_FIDO2_RESIDENT_KEY`, `MFA_FIDO2_AUTHENTICATOR_ATTACHMENT`, `MFA_FIDO2_USER_VERIFICATION`, `MFA_FIDO2_ATTESTATION_PREFERENCE`
* Added: `MFA_ENFORCE_EMAIL_TOKEN`, `MFA_SHOW_OTP_IN_EMAIL_SUBJECT`, `MFA_OTP_EMAIL_SUBJECT`
4. 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
Expand Down
21 changes: 21 additions & 0 deletions mfa/Common.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import datetime
from random import randint

from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.mail import EmailMessage

try:
Expand All @@ -24,3 +28,20 @@ def get_redirect_url():
),
"reg_success_msg": getattr(settings, "MFA_SUCCESS_REGISTRATION_MSG"),
}


def get_username_field():
User = get_user_model()
USERNAME_FIELD = getattr(User, "USERNAME_FIELD", "username")
return User, USERNAME_FIELD


def set_next_recheck():
if getattr(settings, "MFA_RECHECK", False):
delta = datetime.timedelta(
seconds=randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX)
)
return {
"next_check": datetime.datetime.timestamp(datetime.datetime.now() + delta)
}
return {}
52 changes: 30 additions & 22 deletions mfa/Email.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,26 @@
from .models import User_Keys

from .views import login
from .Common import send
from .Common import send, get_username_field, set_next_recheck


def sendEmail(request, username, secret):
"""Send Email to the user after rendering `mfa_email_token_template`"""

User = get_user_model()
key = getattr(User, "USERNAME_FIELD", "username")
kwargs = {key: username}
User, UsernameField = get_username_field()
kwargs = {UsernameField: username}
user = User.objects.get(**kwargs)
res = render(
request,
"mfa_email_token_template.html",
{"request": request, "user": user, "otp": secret},
)
return send([user.email], "OTP", res.content.decode())
subject = getattr(settings, "MFA_OTP_EMAIL_SUBJECT", "OTP")
if getattr(settings, "MFA_SHOW_OTP_IN_EMAIL_SUBJECT", False):
if "%s" in subject:
subject = subject % secret
else:
subject = secret + " " + subject
return send([user.email], subject, res.content.decode())


@never_cache
Expand All @@ -35,7 +39,8 @@ def start(request):
if request.method == "POST":
if request.session["email_secret"] == request.POST["otp"]: # if successful
uk = User_Keys()
uk.username = request.user.username
User, USERNAME_FIELD = get_username_field()
uk.username = USERNAME_FIELD
uk.key_type = "Email"
uk.enabled = 1
uk.save()
Expand Down Expand Up @@ -64,8 +69,8 @@ def start(request):
)
context["invalid"] = True
else:
request.session["email_secret"] = str(
randint(0, 100000)
request.session["email_secret"] = str(randint(0, 1000000)).zfill(
6
) # generate a random integer

if sendEmail(request, request.user.username, request.session["email_secret"]):
Expand All @@ -78,20 +83,23 @@ def auth(request):
"""Authenticating the user by email."""
context = csrf(request)
if request.method == "POST":
username = request.session["base_username"]

if request.session["email_secret"] == request.POST["otp"].strip():
uk = User_Keys.objects.get(
username=request.session["base_username"], key_type="Email"
)
email_keys = User_Keys.objects.filter(username=username, key_type="Email")
if email_keys.exists():
uk = email_keys.first()
elif getattr(settings, "MFA_ENFORCE_EMAIL_TOKEN", False):
uk = User_Keys()
uk.username = username
uk.key_type = "Email"
uk.enabled = 1
uk.save()
else:
raise Exception("Email is not a valid method for this user")

mfa = {"verified": True, "method": "Email", "id": uk.id}
if getattr(settings, "MFA_RECHECK", False):
mfa["next_check"] = datetime.datetime.timestamp(
datetime.datetime.now()
+ datetime.timedelta(
seconds=random.randint(
settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX
)
)
)
mfa.update(set_next_recheck())
request.session["mfa"] = mfa

from django.utils import timezone
Expand All @@ -101,7 +109,7 @@ def auth(request):
return login(request)
context["invalid"] = True
else:
request.session["email_secret"] = str(randint(0, 100000))
request.session["email_secret"] = str(randint(0, 1000000)).zfill(6)
if sendEmail(
request, request.session["base_username"], request.session["email_secret"]
):
Expand Down
Loading

0 comments on commit 0c3a1e9

Please sign in to comment.