Skip to content

Commit

Permalink
Merged v2.9.0
Browse files Browse the repository at this point in the history
  • Loading branch information
mkalioby committed May 27, 2024
1 parent 77905b0 commit d90c40b
Show file tree
Hide file tree
Showing 36 changed files with 1,021 additions and 637 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.idea
.pyre/
example/venv
# Byte-compiled / optimized / DLL files
__pycache__/
Expand Down
16 changes: 16 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
repos:
# Using this mirror lets us use mypyc-compiled black, which is about 2x faster
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 23.12.1
hooks:
- id: black
# It is recommended to specify the latest version of Python
# supported by your project here, or alternatively use
# pre-commit's default_language_version, see
# https://pre-commit.com/#top_level-default_language_version
language_version: python3.11

- repo: https://github.com/fsouza/pre-commit-pyre-check
rev: '199dc59' # Use the sha / tag you want to point at
hooks:
- id: pyre-check
15 changes: 15 additions & 0 deletions .pyre_configuration
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"source_directories": [
"."
],
"search_path": [
"env/lib/python3.11/site-packages/"
],
"ignore_all_errors":[
"*env*/*",
"example/venv/*",
"build/*",
"example/*"

]
}
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
# Change Log

## 2.9.0
* Add: Set black as code formatter
* Add: Add Pyre as a type checker
* Add: Add pre-commit hooks
* Upgrade: fido to be 1.1.0 as minimum

## 2.8.0
* Support For Django 4.0+ JSONField
* Removed jsonfield package from requirements
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
A Django app that handles MFA, it supports TOTP, U2F, FIDO2 U2F (Web Authn), Email Tokens , Trusted Devices and backup codes.

[![Works with PassKeys](https://github.com/mkalioby/django-mfa2/raw/master/img/Works%20with%20PassKeys-black.png)](https://fidoalliance.org/passkeys/)

[![Code Style Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![Type Checker By Pyre](https://img.shields.io/badge/type%20checker-pyre-orange)](https://pyre-check.org/)
### Pip Stats
[![PyPI version](https://badge.fury.io/py/django-mfa2.svg)](https://badge.fury.io/py/django-mfa2)
[![Downloads Count](https://static.pepy.tech/personalized-badge/django-mfa2?period=total&units=international_system&left_color=black&right_color=green&left_text=Downloads)](https://pepy.tech/project/django-mfa2)
Expand Down
34 changes: 20 additions & 14 deletions example/example/auth.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,36 @@
from django.shortcuts import render
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.contrib.auth import authenticate,login,logout
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.models import User


def loginView(request):
context={}
if request.method=="POST":
username=request.POST["username"]
password=request.POST["password"]
user=authenticate(username=username,password=password)
context = {}
if request.method == "POST":
username = request.POST["username"]
password = request.POST["password"]
user = authenticate(username=username, password=password)
if user:
from mfa.helpers import has_mfa
res = has_mfa(username = username, request = request) # has_mfa returns false or HttpResponseRedirect

res = has_mfa(
username=username, request=request
) # has_mfa returns false or HttpResponseRedirect
if res:
return res
return create_session(request,user.username)
context["invalid"]=True
return create_session(request, user.username)
context["invalid"] = True
return render(request, "login.html", context)

def create_session(request,username):
user=User.objects.get(username=username)
user.backend='django.contrib.auth.backends.ModelBackend'

def create_session(request, username):
user = User.objects.get(username=username)
user.backend = "django.contrib.auth.backends.ModelBackend"
login(request, user)
return HttpResponseRedirect(reverse('home'))
return HttpResponseRedirect(reverse("home"))


def logoutView(request):
logout(request)
return render(request,"logout.html",{})
return render(request, "logout.html", {})
139 changes: 70 additions & 69 deletions example/example/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,65 +21,65 @@
# See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '#9)q!_i3@pr-^3oda(e^3$x!kq3b4f33#5l@+=+&vuz+p6gb3g'
SECRET_KEY = "#9)q!_i3@pr-^3oda(e^3$x!kq3b4f33#5l@+=+&vuz+p6gb3g"

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = ['*']
ALLOWED_HOSTS = ["*"]


# Application definition

INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'mfa',
'sslserver'
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"mfa",
"sslserver",
]

MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]

ROOT_URLCONF = 'example.urls'
ROOT_URLCONF = "example.urls"

TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR ,'example','templates' )],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [os.path.join(BASE_DIR, "example", "templates")],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]

WSGI_APPLICATION = 'example.wsgi.application'
WSGI_APPLICATION = "example.wsgi.application"


# Database
# https://docs.djangoproject.com/en/2.0/ref/settings/#databases

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': 'test_db',
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": "test_db",
}
}

Expand All @@ -89,26 +89,26 @@

AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]


# Internationalization
# https://docs.djangoproject.com/en/2.0/topics/i18n/

LANGUAGE_CODE = 'en-us'
LANGUAGE_CODE = "en-us"

TIME_ZONE = 'UTC'
TIME_ZONE = "UTC"

USE_I18N = True

Expand All @@ -120,37 +120,38 @@
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.0/howto/static-files/

STATIC_URL = '/static/'
#STATIC_ROOT=(os.path.join(BASE_DIR,'static'))
STATICFILES_DIRS=[os.path.join(BASE_DIR,'static')]
LOGIN_URL="/auth/login"

EMAIL_FROM='Test App'
EMAIL_HOST="smtp.gmail.com"
EMAIL_PORT=587
EMAIL_HOST_USER=""
EMAIL_HOST_PASSWORD=''
EMAIL_USE_TLS=True



MFA_UNALLOWED_METHODS=() # Methods that shouldn't be allowed for the user
MFA_LOGIN_CALLBACK="example.auth.create_session" # A function that should be called by username to login the user in session
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=('',) # Can the user disable his key (Added in 1.2.0).
MFA_REDIRECT_AFTER_REGISTRATION="registered"
MFA_SUCCESS_REGISTRATION_MSG="Go to Home"
STATIC_URL = "/static/"
# STATIC_ROOT=(os.path.join(BASE_DIR,'static'))
STATICFILES_DIRS = [os.path.join(BASE_DIR, "static")]
LOGIN_URL = "/auth/login"

EMAIL_FROM = "Test App"
EMAIL_HOST = "smtp.gmail.com"
EMAIL_PORT = 587
EMAIL_HOST_USER = ""
EMAIL_HOST_PASSWORD = ""
EMAIL_USE_TLS = True


MFA_UNALLOWED_METHODS = () # Methods that shouldn't be allowed for the user
MFA_LOGIN_CALLBACK = "example.auth.create_session" # A function that should be called by username to login the user in session
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 = ("",) # Can the user disable his key (Added in 1.2.0).
MFA_REDIRECT_AFTER_REGISTRATION = "registered"
MFA_SUCCESS_REGISTRATION_MSG = "Go to Home"
MFA_ALWAYS_GO_TO_LAST_METHOD = True
MFA_ENFORCE_RECOVERY_METHOD = True
MFA_RENAME_METHODS = {"RECOVERY":"Backup Codes","FIDO2":"Biometric Authentication"}
PASSWORD_HASHERS = DEFAULT_PASSWORD_HASHERS #Comment if PASSWORD_HASHER already set
PASSWORD_HASHERS += ['mfa.recovery.Hash']
RECOVERY_ITERATION = 1 #Number of iteration for recovery code, higher is more secure, but uses more resources for generation and check...
TOKEN_ISSUER_NAME="PROJECT_NAME" #TOTP Issuer name

U2F_APPID="https://localhost:9000" #URL For U2F
FIDO_SERVER_ID="localhost" # Server rp id for FIDO2, it the full domain of your project
FIDO_SERVER_NAME="TestApp"
MFA_RENAME_METHODS = {"RECOVERY": "Backup Codes", "FIDO2": "Biometric Authentication"}
PASSWORD_HASHERS = DEFAULT_PASSWORD_HASHERS # Comment if PASSWORD_HASHER already set
PASSWORD_HASHERS += ["mfa.recovery.Hash"]
RECOVERY_ITERATION = 1 # Number of iteration for recovery code, higher is more secure, but uses more resources for generation and check...
TOKEN_ISSUER_NAME = "PROJECT_NAME" # TOTP Issuer name

U2F_APPID = "https://localhost:9000" # URL For U2F
FIDO_SERVER_ID = (
"localhost" # Server rp id for FIDO2, it the full domain of your project
)
FIDO_SERVER_NAME = "TestApp"
19 changes: 10 additions & 9 deletions example/example/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,16 @@
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path,re_path,include
from . import views,auth
from django.urls import path, re_path, include
from . import views, auth
from mfa import TrustedDevice

urlpatterns = [
path('admin/', admin.site.urls),
path('mfa/', include('mfa.urls')),
path('auth/login',auth.loginView,name="login"),
path('auth/logout',auth.logoutView,name="logout"),
path('devices/add/', TrustedDevice.add,name="add_trusted_device"),
re_path('^$',views.home,name='home'),
path('registered/',views.registered,name='registered')
path("admin/", admin.site.urls),
path("mfa/", include("mfa.urls")),
path("auth/login", auth.loginView, name="login"),
path("auth/logout", auth.logoutView, name="logout"),
path("devices/add/", TrustedDevice.add, name="add_trusted_device"),
re_path("^$", views.home, name="home"),
path("registered/", views.registered, name="registered"),
]
5 changes: 3 additions & 2 deletions example/example/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@

@login_required()
def home(request):
return render(request,"home.html",{})
return render(request, "home.html", {})


@login_required()
def registered(request):
return render(request,"home.html",{"registered":True})
return render(request, "home.html", {"registered": True})
21 changes: 14 additions & 7 deletions mfa/Common.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
from django.conf import settings
from django.core.mail import EmailMessage

try:
from django.urls import reverse
except:
from django.core.urlresolver import reverse
except ImportError:
from django.core.urlresolver import reverse # pyre-ignore[21]


def send(to,subject,body):
def send(to, subject, body):
from_email_address = settings.EMAIL_HOST_USER
if '@' not in from_email_address:
if "@" not in from_email_address:
from_email_address = settings.DEFAULT_FROM_EMAIL
From = "%s <%s>" % (settings.EMAIL_FROM, from_email_address)
email = EmailMessage(subject,body,From,to)
email = EmailMessage(subject, body, From, to)
email.content_subtype = "html"
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")}
return {
"redirect_html": reverse(
getattr(settings, "MFA_REDIRECT_AFTER_REGISTRATION", "mfa_home")
),
"reg_success_msg": getattr(settings, "MFA_SUCCESS_REGISTRATION_MSG"),
}
Loading

0 comments on commit d90c40b

Please sign in to comment.