Compare commits
25 Commits
v2.5b2
...
Better_TOT
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca49e1623d | ||
|
|
0b83758625 | ||
|
|
6d59258393 | ||
|
|
bb88f680a0 | ||
|
|
62bb50307e | ||
|
|
68e257d60e | ||
|
|
ba4e7f9a17 | ||
|
|
f4d8934ef5 | ||
|
|
81675207d3 | ||
|
|
daece24c6d | ||
|
|
f654debb98 | ||
|
|
3d133b3fff | ||
|
|
714fb68a65 | ||
|
|
ec16539c34 | ||
|
|
e8ce96c404 | ||
|
|
b18dfe2bb6 | ||
|
|
84f93444a3 | ||
|
|
34fcca57c8 | ||
|
|
174dba2878 | ||
|
|
0945561136 | ||
|
|
6b132683a7 | ||
|
|
d54cd20d9b | ||
|
|
007872bd8a | ||
|
|
8911a4f0b6 | ||
|
|
9c126f06b5 |
13
.github/workflows/main.yml
vendored
Normal file
13
.github/workflows/main.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
on: [push]
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: python
|
||||||
|
run: |
|
||||||
|
sudo apt-get install python3
|
||||||
|
- name: lint
|
||||||
|
run: |
|
||||||
|
pip install black
|
||||||
|
black --check --exclude migrations mfa/ example/ setup.py
|
||||||
20
CHANGELOG.md
20
CHANGELOG.md
@@ -1,8 +1,12 @@
|
|||||||
# Change Log
|
# Change Log
|
||||||
|
|
||||||
|
## 2.3.0
|
||||||
|
* Some code cleanup thanks to @xi
|
||||||
|
* Added: MFA_TOTP_FAILURE_WINDOW & MFA_TOTP_FAILURE_LIMIT
|
||||||
|
|
||||||
## 2.2.0
|
## 2.2.0
|
||||||
* Added: MFA_REDIRECT_AFTER_REGISTRATION settings parameter
|
* Added: MFA_REDIRECT_AFTER_REGISTRATION settings parameter
|
||||||
* Fixed: Deprecation error for NULBooleanField
|
* Fixed: Deprecation error for NullBooleanField
|
||||||
|
|
||||||
## 2.1.2
|
## 2.1.2
|
||||||
* Fixed: Getting timestamp on Python 3.7 as ("%s") is raising an exception
|
* Fixed: Getting timestamp on Python 3.7 as ("%s") is raising an exception
|
||||||
@@ -11,7 +15,7 @@
|
|||||||
|
|
||||||
## 2.1.1
|
## 2.1.1
|
||||||
* Fixed: FIDO2 version in requirements.txt file.
|
* Fixed: FIDO2 version in requirements.txt file.
|
||||||
|
|
||||||
## 2.1.0
|
## 2.1.0
|
||||||
* Added Support for Touch ID for Mac OSx and iOS 14 on Safari
|
* Added Support for Touch ID for Mac OSx and iOS 14 on Safari
|
||||||
|
|
||||||
@@ -26,27 +30,27 @@
|
|||||||
* Fixed: __version__ to show correct version
|
* Fixed: __version__ to show correct version
|
||||||
|
|
||||||
## 2.0.2
|
## 2.0.2
|
||||||
* Added: A missing migration
|
* Added: A missing migration
|
||||||
thnks to @swainn
|
thnks to @swainn
|
||||||
|
|
||||||
## 2.0.1
|
## 2.0.1
|
||||||
* Fixed: issue in migration between Postgres and SQLite
|
* Fixed: issue in migration between Postgres and SQLite
|
||||||
thnks to @swainn and @willingham
|
thnks to @swainn and @willingham
|
||||||
|
|
||||||
## 2.0
|
## 2.0
|
||||||
* Dropped support to djangp-1.8 and Python 2.7
|
* Dropped support to djangp-1.8 and Python 2.7
|
||||||
* Added: never-cache decorator
|
* Added: never-cache decorator
|
||||||
* Fixes to Make Email Method More Robust
|
* Fixes to Make Email Method More Robust
|
||||||
* Addresses several structure and style issues with TOTP and Email dialogs
|
* Addresses several structure and style issues with TOTP and Email dialogs
|
||||||
* Updated to fido2 0.8.1
|
* Updated to fido2 0.8.1
|
||||||
|
|
||||||
Thanks to @swainn
|
Thanks to @swainn
|
||||||
|
|
||||||
## v1.9.1
|
## v1.9.1
|
||||||
* Fixed: is_authenticated #13
|
* Fixed: is_authenticated #13
|
||||||
* Fixed: is_anonymous #6
|
* Fixed: is_anonymous #6
|
||||||
|
|
||||||
thanks to @d3cline,
|
thanks to @d3cline,
|
||||||
|
|
||||||
## v1.7
|
## v1.7
|
||||||
* Better Error Management
|
* Better Error Management
|
||||||
|
|||||||
@@ -5,4 +5,4 @@
|
|||||||
1. activate env `source venv/bin/activate`
|
1. activate env `source venv/bin/activate`
|
||||||
1. install requirements `pip install -r requirements.txt`
|
1. install requirements `pip install -r requirements.txt`
|
||||||
1. migrate `python manage.py migrate`
|
1. migrate `python manage.py migrate`
|
||||||
1. create super user 'python manage.py createsuperuser'
|
1. create super user 'python manage.py createsuperuser'
|
||||||
103
README.md
103
README.md
@@ -1,14 +1,15 @@
|
|||||||
# django-mfa2
|
# django-mfa2
|
||||||
A Django app that handles MFA, it supports TOTP, U2F, FIDO2 U2F (Web Authn), Email Tokens , and Trusted Devices
|
A Django app that handles MFA, it supports TOTP, U2F, FIDO2 U2F (Web Authn), Email Tokens , and Trusted Devices
|
||||||
|
|
||||||
|

|
||||||
### Pip Stats
|
### Pip Stats
|
||||||
[](https://badge.fury.io/py/django-mfa2)
|
[](https://badge.fury.io/py/django-mfa2)
|
||||||
[](https://pepy.tech/project/django-mfa2)
|
[](https://pepy.tech/project/django-mfa2)
|
||||||
|
|
||||||
### Conda Stats
|
### Conda Stats
|
||||||
[](https://anaconda.org/conda-forge/django-mfa2)
|
[](https://anaconda.org/conda-forge/django-mfa2)
|
||||||
[](https://anaconda.org/conda-forge/django-mfa2)
|
[](https://anaconda.org/conda-forge/django-mfa2)
|
||||||
[](https://anaconda.org/conda-forge/django-mfa2)
|
[](https://anaconda.org/conda-forge/django-mfa2)
|
||||||
|
|
||||||
Web Authencation API (WebAuthn) is state-of-the art techology that is expected to replace passwords.
|
Web Authencation API (WebAuthn) is state-of-the art techology that is expected to replace passwords.
|
||||||
|
|
||||||
@@ -39,17 +40,17 @@ Depends on
|
|||||||
* fido2==0.9.0
|
* fido2==0.9.0
|
||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
1. using pip
|
1. using pip
|
||||||
|
|
||||||
`pip install django-mfa2`
|
`pip install django-mfa2`
|
||||||
2. Using Conda forge
|
2. Using Conda forge
|
||||||
|
|
||||||
`conda config --add channels conda-forge`
|
`conda config --add channels conda-forge`
|
||||||
|
|
||||||
`conda install django-mfa2`
|
`conda install django-mfa2`
|
||||||
|
|
||||||
For more info, see the conda-forge repo (https://github.com/conda-forge/django-mfa2-feedstock)
|
For more info, see the conda-forge repo (https://github.com/conda-forge/django-mfa2-feedstock)
|
||||||
|
|
||||||
Thanks for [swainn](https://github.com/swainn) for adding package to conda-forge
|
Thanks for [swainn](https://github.com/swainn) for adding package to conda-forge
|
||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
@@ -64,7 +65,7 @@ Depends on
|
|||||||
`python manage.py collectstatic`
|
`python manage.py collectstatic`
|
||||||
1. Add the following settings to your file
|
1. Add the following settings to your file
|
||||||
|
|
||||||
```python
|
```python
|
||||||
MFA_UNALLOWED_METHODS=() # Methods that shouldn't be allowed for the user
|
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_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_RECHECK=True # Allow random rechecking of the user
|
||||||
@@ -74,10 +75,12 @@ Depends on
|
|||||||
MFA_RECHECK_MAX=30 # Maximum in seconds
|
MFA_RECHECK_MAX=30 # Maximum in seconds
|
||||||
MFA_QUICKLOGIN=True # Allow quick login for returning users by provide only their 2FA
|
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=('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
|
||||||
|
|
||||||
TOKEN_ISSUER_NAME="PROJECT_NAME" #TOTP Issuer name
|
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
|
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_ID=u"localehost" # Server rp id for FIDO2, it the full domain of your project
|
||||||
FIDO_SERVER_NAME=u"PROJECT_NAME"
|
FIDO_SERVER_NAME=u"PROJECT_NAME"
|
||||||
@@ -89,44 +92,48 @@ Depends on
|
|||||||
* TOTP
|
* TOTP
|
||||||
* Trusted_Devices
|
* Trusted_Devices
|
||||||
* Email
|
* Email
|
||||||
|
|
||||||
**Notes**:
|
**Notes**:
|
||||||
* Starting version 1.1, ~~FIDO_LOGIN_URL~~ isn't required for FIDO2 anymore.
|
* 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 1.7.0, Key owners can be specified.
|
||||||
* Starting version 2.2.0
|
* Starting version 2.2.0
|
||||||
* Added: `MFA_SUCCESS_REGISTRATION_MSG` & `MFA_REDIRECT_AFTER_REGISTRATION`
|
* 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
|
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
|
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
|
||||||
|
|
||||||
* authenticate the user
|
* authenticate the user
|
||||||
* if username and password are correct , check if the user has mfa or not
|
* if username and password are correct , check if the user has mfa or not
|
||||||
* if user has mfa then redirect to mfa page
|
* if user has mfa then redirect to mfa page
|
||||||
* if user doesn't have mfa then call your function to create the user session
|
* if user doesn't have mfa then call your function to create the user session
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def login(request): # this function handles the login form POST
|
from mfa.helpers import has_mfa
|
||||||
user = auth.authenticate(username=username, password=password)
|
|
||||||
if user is not None: # if the user object exist
|
def login(request): # this function handles the login form POST
|
||||||
from mfa.helpers import has_mfa
|
user = auth.authenticate(username=username, password=password)
|
||||||
res = has_mfa(username = username,request=request) # has_mfa returns false or HttpResponseRedirect
|
if user is not None: # if the user object exist
|
||||||
|
res = has_mfa(username=username, request=request) # has_mfa returns false or HttpResponseRedirect
|
||||||
if res:
|
if res:
|
||||||
return res
|
return res
|
||||||
return log_user_in(request,username=user.username)
|
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
|
# log_user_in is a function that handles creating user session, it should be in the setting file as MFA_CALLBACK
|
||||||
```
|
```
|
||||||
1. Add mfa to urls.py
|
1. Add mfa to urls.py
|
||||||
```python
|
```python
|
||||||
import mfa
|
import mfa
|
||||||
import mfa.TrustedDevice
|
import mfa.TrustedDevice
|
||||||
urls_patterns= [
|
|
||||||
'...',
|
urls_patterns = [
|
||||||
url(r'^mfa/', include('mfa.urls')),
|
'...',
|
||||||
url(r'devices/add$', mfa.TrustedDevice.add,name="mfa_add_new_trusted_device"), # This short link to add new trusted device
|
url(r'^mfa/', include('mfa.urls')),
|
||||||
'....',
|
url(r'devices/add$', mfa.TrustedDevice.add,name="mfa_add_new_trusted_device"), # This short link to add new trusted device
|
||||||
]
|
'....',
|
||||||
```
|
]
|
||||||
1. Provide `mfa_auth_base.html` in your templaes with block called 'head' and 'content'
|
```
|
||||||
|
1. Provide `mfa_auth_base.html` in your templates with block called 'head' and 'content'
|
||||||
The template will be included during the user login.
|
The template will be included during the user login.
|
||||||
If you will use Email Token method, then you have to provide template named `mfa_email_token_template.html` that will content the format of the email with parameter named `user` and `otp`.
|
If you will use Email Token method, then you have to provide template named `mfa_email_token_template.html` that will content the format of the email with parameter named `user` and `otp`.
|
||||||
1. To match the look and feel of your project, MFA includes `base.html` but it needs blocks named `head` & `content` to added its content to it.
|
1. To match the look and feel of your project, MFA includes `base.html` but it needs blocks named `head` & `content` to added its content to it.
|
||||||
@@ -139,40 +146,42 @@ For Example, See https://github.com/mkalioby/AutoDeploy/commit/5f1d94b1804e0aa33
|
|||||||
|
|
||||||
To be able to go passwordless for returning users, create a cookie named 'base_username' containing username as shown in snippet below
|
To be able to go passwordless for returning users, create a cookie named 'base_username' containing username as shown in snippet below
|
||||||
```python
|
```python
|
||||||
response = render(request, 'Dashboard.html', context))
|
response = render(request, 'Dashboard.html', context))
|
||||||
if request.session.get("mfa",{}).get("verified",False) and getattr(settings,"MFA_QUICKLOGIN",False):
|
if request.session.get("mfa", {}).get("verified", False) and getattr(settings, "MFA_QUICKLOGIN", False):
|
||||||
if request.session["mfa"]["method"]!="Trusted Device":
|
if request.session["mfa"]["method"] != "Trusted Device":
|
||||||
response.set_cookie("base_username", request.user.username, path="/",max_age = 15*24*60*60)
|
response.set_cookie("base_username", request.user.username, path="/", max_age=15 * 24 * 60 * 60)
|
||||||
return response
|
return response
|
||||||
```
|
```
|
||||||
|
|
||||||
Second, update the GET part of your login view
|
Second, update the GET part of your login view
|
||||||
```python
|
```python
|
||||||
if "mfa" in settings.INSTALLED_APPS and getattr(settings,"MFA_QUICKLOGIN",False) and request.COOKIES.get('base_username'):
|
from mfa.helpers import has_mfa
|
||||||
username=request.COOKIES.get('base_username')
|
|
||||||
from mfa.helpers import has_mfa
|
if "mfa" in settings.INSTALLED_APPS and getattr(settings, "MFA_QUICKLOGIN", False) and request.COOKIES.get('base_username'):
|
||||||
res = has_mfa(username = username,request=request,)
|
username=request.COOKIES.get('base_username')
|
||||||
if res: return res
|
res = has_mfa(username=username, request=request)
|
||||||
## continue and return the form.
|
if res:
|
||||||
|
return res
|
||||||
|
# continue and return the form.
|
||||||
```
|
```
|
||||||
# Checking MFA on Client Side
|
# Checking MFA on Client Side
|
||||||
|
|
||||||
Sometimes you like to verify that the user is still there so simple you can ask django-mfa2 to check that for you
|
Sometimes you like to verify that the user is still there so simple you can ask django-mfa2 to check that for you
|
||||||
|
|
||||||
```html
|
```html
|
||||||
{% include 'mfa_check.html' %}
|
{% include 'mfa_check.html' %}
|
||||||
```
|
```
|
||||||
````js
|
````js
|
||||||
function success_func() {
|
function success_func() {
|
||||||
//logic if mfa check succeeds
|
// logic if mfa check succeeds
|
||||||
}
|
}
|
||||||
function fail_func() {
|
function fail_func() {
|
||||||
//logic if mfa check fails
|
// logic if mfa check fails
|
||||||
}
|
}
|
||||||
function some_func() {
|
function some_func() {
|
||||||
recheck_mfa(success_func,fail_func,MUST_BE_MFA)
|
recheck_mfa(success_func, fail_func, MUST_BE_MFA)
|
||||||
//MUST_BE_MFA true or false, if the user must has with MFA
|
// MUST_BE_MFA true or false, if the user must has with MFA
|
||||||
}
|
}
|
||||||
|
|
||||||
````
|
````
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
## Break your login function
|
## 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
|
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
|
||||||
|
|
||||||
* authenticate the user
|
* authenticate the user
|
||||||
* if username and password are correct , check if the user has mfa or not
|
* if username and password are correct , check if the user has mfa or not
|
||||||
* if user has mfa then redirect to mfa page
|
* if user has mfa then redirect to mfa page
|
||||||
@@ -11,13 +11,13 @@ Usually your login function will check for username and password, log the user i
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
def login(request): # this function handles the login form POST
|
def login(request): # this function handles the login form POST
|
||||||
user = auth.authenticate(username=username, password=password)
|
user = auth.authenticate(username=username, password=password)
|
||||||
if user is not None: # if the user object exist
|
if user is not None: # if the user object exist
|
||||||
from mfa.helpers import has_mfa
|
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:
|
if res:
|
||||||
return res
|
return res
|
||||||
return log_user_in(request,username=user.username)
|
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
|
#log_user_in is a function that handles creatung user session, it should be in the setting file as MFA_CALLBACK
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Installation & Configuration
|
# Installation & Configuration
|
||||||
1. Install the package
|
1. Install the package
|
||||||
```sh
|
```sh
|
||||||
pip install django-mfa2
|
pip install django-mfa2
|
||||||
```
|
```
|
||||||
@@ -17,27 +17,27 @@
|
|||||||
MFA_RECHECK=True # Allow random rechecking of the user
|
MFA_RECHECK=True # Allow random rechecking of the user
|
||||||
MFA_RECHECK_MIN=10 # Minimum interval in seconds
|
MFA_RECHECK_MIN=10 # Minimum interval in seconds
|
||||||
MFA_RECHECK_MAX=30 # Maximum in seconds
|
MFA_RECHECK_MAX=30 # Maximum in seconds
|
||||||
MFA_QUICKLOGIN=True # Allow quick login for returning users by provide only their 2FA
|
MFA_QUICKLOGIN=True # Allow quick login for returning users by provide only their 2FA
|
||||||
|
|
||||||
TOKEN_ISSUER_NAME="PROJECT_NAME" #TOTP Issuer name
|
TOKEN_ISSUER_NAME="PROJECT_NAME" #TOTP Issuer name
|
||||||
|
|
||||||
U2F_APPID="https://localhost" #URL For U2
|
U2F_APPID="https://localhost" #URL For U2
|
||||||
FIDO_SERVER_ID=u"localehost" # Server rp id for FIDO2, it the full domain of your project
|
FIDO_SERVER_ID=u"localehost" # Server rp id for FIDO2, it the full domain of your project
|
||||||
FIDO_SERVER_NAME=u"PROJECT_NAME"
|
FIDO_SERVER_NAME=u"PROJECT_NAME"
|
||||||
FIDO_LOGIN_URL=BASE_URL
|
FIDO_LOGIN_URL=BASE_URL
|
||||||
```
|
```
|
||||||
|
|
||||||
**Method Names**
|
**Method Names**
|
||||||
* U2F
|
* U2F
|
||||||
* FIDO2
|
* FIDO2
|
||||||
* TOTP
|
* TOTP
|
||||||
* Trusted_Devices
|
* Trusted_Devices
|
||||||
* Email
|
* Email
|
||||||
|
|
||||||
**Note**: Starting version 1.1, ~~FIDO_LOGIN_URL~~ isn't required for FIDO2 anymore.
|
**Note**: Starting version 1.1, ~~FIDO_LOGIN_URL~~ isn't required for FIDO2 anymore.
|
||||||
|
|
||||||
1. Add mfa to urls.py
|
1. Add mfa to urls.py
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import mfa
|
import mfa
|
||||||
import mfa.TrustedDevice
|
import mfa.TrustedDevice
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
'....',
|
'....',
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
1. Provide `mfa_auth_base.html` in your templaes with block called 'head' and 'content'
|
1. Provide `mfa_auth_base.html` in your templates with block called 'head' and 'content'
|
||||||
The template will be included during the user login.
|
The template will be included during the user login.
|
||||||
If you will use Email Token method, then you have to provide template named `mfa_email_token_template.html` that will content the format of the email with parameter named `user` and `otp`.
|
If you will use Email Token method, then you have to provide template named `mfa_email_token_template.html` that will content the format of the email with parameter named `user` and `otp`.
|
||||||
1. To match the look and feel of your project, MFA includes `base.html` but it needs blocks named `head` & `content` to added its content to it.
|
1. To match the look and feel of your project, MFA includes `base.html` but it needs blocks named `head` & `content` to added its content to it.
|
||||||
|
|||||||
@@ -1,30 +1,36 @@
|
|||||||
from django.shortcuts import render
|
from django.contrib.auth import authenticate, login, logout
|
||||||
from django.http import HttpResponseRedirect
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.contrib.auth import authenticate,login,logout
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
|
from django.shortcuts import render
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from mfa.helpers import has_mfa
|
||||||
|
|
||||||
|
|
||||||
def loginView(request):
|
def loginView(request):
|
||||||
context={}
|
context = {}
|
||||||
if request.method=="POST":
|
if request.method == "POST":
|
||||||
username=request.POST["username"]
|
username = request.POST["username"]
|
||||||
password=request.POST["password"]
|
password = request.POST["password"]
|
||||||
user=authenticate(username=username,password=password)
|
user = authenticate(username=username, password=password)
|
||||||
if user:
|
if user:
|
||||||
from mfa.helpers import has_mfa
|
res = has_mfa(
|
||||||
res = has_mfa(username = username, request = request) # has_mfa returns false or HttpResponseRedirect
|
username=username, request=request
|
||||||
|
) # has_mfa returns false or HttpResponseRedirect
|
||||||
if res:
|
if res:
|
||||||
return res
|
return res
|
||||||
return create_session(request,user.username)
|
return create_session(request, user.username)
|
||||||
context["invalid"]=True
|
context["invalid"] = True
|
||||||
return render(request, "login.html", context)
|
return render(request, "login.html", context)
|
||||||
|
|
||||||
def create_session(request,username):
|
|
||||||
user=User.objects.get(username=username)
|
def create_session(request, username):
|
||||||
user.backend='django.contrib.auth.backends.ModelBackend'
|
user = User.objects.get(username=username)
|
||||||
|
user.backend = "django.contrib.auth.backends.ModelBackend"
|
||||||
login(request, user)
|
login(request, user)
|
||||||
return HttpResponseRedirect(reverse('home'))
|
return HttpResponseRedirect(reverse("home"))
|
||||||
|
|
||||||
|
|
||||||
def logoutView(request):
|
def logoutView(request):
|
||||||
logout(request)
|
logout(request)
|
||||||
return render(request,"logout.html",{})
|
return render(request, "logout.html", {})
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|||||||
# See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
# 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!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
@@ -31,54 +31,54 @@ ALLOWED_HOSTS = []
|
|||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
'django.contrib.admin',
|
"django.contrib.admin",
|
||||||
'django.contrib.auth',
|
"django.contrib.auth",
|
||||||
'django.contrib.contenttypes',
|
"django.contrib.contenttypes",
|
||||||
'django.contrib.sessions',
|
"django.contrib.sessions",
|
||||||
'django.contrib.messages',
|
"django.contrib.messages",
|
||||||
'django.contrib.staticfiles',
|
"django.contrib.staticfiles",
|
||||||
'mfa',
|
"mfa",
|
||||||
'sslserver'
|
"sslserver",
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
'django.middleware.security.SecurityMiddleware',
|
"django.middleware.security.SecurityMiddleware",
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
'django.middleware.common.CommonMiddleware',
|
"django.middleware.common.CommonMiddleware",
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
]
|
]
|
||||||
|
|
||||||
ROOT_URLCONF = 'example.urls'
|
ROOT_URLCONF = "example.urls"
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
'DIRS': [os.path.join(BASE_DIR ,'example','templates' )],
|
"DIRS": [os.path.join(BASE_DIR, "example", "templates")],
|
||||||
'APP_DIRS': True,
|
"APP_DIRS": True,
|
||||||
'OPTIONS': {
|
"OPTIONS": {
|
||||||
'context_processors': [
|
"context_processors": [
|
||||||
'django.template.context_processors.debug',
|
"django.template.context_processors.debug",
|
||||||
'django.template.context_processors.request',
|
"django.template.context_processors.request",
|
||||||
'django.contrib.auth.context_processors.auth',
|
"django.contrib.auth.context_processors.auth",
|
||||||
'django.contrib.messages.context_processors.messages',
|
"django.contrib.messages.context_processors.messages",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
WSGI_APPLICATION = 'example.wsgi.application'
|
WSGI_APPLICATION = "example.wsgi.application"
|
||||||
|
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
# https://docs.djangoproject.com/en/2.0/ref/settings/#databases
|
# https://docs.djangoproject.com/en/2.0/ref/settings/#databases
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
"default": {
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
"ENGINE": "django.db.backends.sqlite3",
|
||||||
'NAME': 'test_db',
|
"NAME": "test_db",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,28 +86,28 @@ DATABASES = {
|
|||||||
# Password validation
|
# Password validation
|
||||||
# https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators
|
# https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
AUTH_PASSWORD_VALIDATORS = [
|
# 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
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/2.0/topics/i18n/
|
# 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
|
USE_I18N = True
|
||||||
|
|
||||||
@@ -119,32 +119,33 @@ USE_TZ = True
|
|||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/2.0/howto/static-files/
|
# https://docs.djangoproject.com/en/2.0/howto/static-files/
|
||||||
|
|
||||||
STATIC_URL = '/static/'
|
STATIC_URL = "/static/"
|
||||||
#STATIC_ROOT=(os.path.join(BASE_DIR,'static'))
|
# STATIC_ROOT=(os.path.join(BASE_DIR,'static'))
|
||||||
STATICFILES_DIRS=[os.path.join(BASE_DIR,'static')]
|
STATICFILES_DIRS = [os.path.join(BASE_DIR, "static")]
|
||||||
LOGIN_URL="/auth/login"
|
LOGIN_URL = "/auth/login"
|
||||||
|
|
||||||
EMAIL_FROM='Test App'
|
EMAIL_FROM = "Test App"
|
||||||
EMAIL_HOST="smtp.gmail.com"
|
EMAIL_HOST = "smtp.gmail.com"
|
||||||
EMAIL_PORT=587
|
EMAIL_PORT = 587
|
||||||
EMAIL_HOST_USER=""
|
EMAIL_HOST_USER = ""
|
||||||
EMAIL_HOST_PASSWORD=''
|
EMAIL_HOST_PASSWORD = ""
|
||||||
EMAIL_USE_TLS=True
|
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_UNALLOWED_METHODS=() # Methods that shouldn't be allowed for the user
|
TOKEN_ISSUER_NAME = "PROJECT_NAME" # TOTP Issuer name
|
||||||
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"
|
|
||||||
|
|
||||||
TOKEN_ISSUER_NAME="PROJECT_NAME" #TOTP Issuer name
|
U2F_APPID = "https://localhost" # URL For U2F
|
||||||
|
FIDO_SERVER_ID = (
|
||||||
U2F_APPID="https://localhost" #URL For U2F
|
u"localhost" # Server rp id for FIDO2, it the full domain of your project
|
||||||
FIDO_SERVER_ID=u"localhost" # Server rp id for FIDO2, it the full domain of your project
|
)
|
||||||
FIDO_SERVER_NAME=u"PROJECT_NAME"
|
FIDO_SERVER_NAME = u"PROJECT_NAME"
|
||||||
|
|||||||
@@ -14,14 +14,15 @@ Including another URLconf
|
|||||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
"""
|
"""
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path,re_path,include
|
from django.urls import include, path
|
||||||
from . import views,auth
|
|
||||||
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"),
|
|
||||||
|
|
||||||
re_path('^$',views.home,name='home'),
|
from . import auth, views
|
||||||
path('registered/',views.registered,name='registered')
|
|
||||||
|
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("", views.home, name="home"),
|
||||||
|
path("registered/", views.registered, name="registered"),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
from django.shortcuts import render
|
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
|
||||||
@login_required()
|
@login_required()
|
||||||
def home(request):
|
def home(request):
|
||||||
return render(request,"home.html",{})
|
return render(request, "home.html", {})
|
||||||
|
|
||||||
|
|
||||||
@login_required()
|
@login_required()
|
||||||
def registered(request):
|
def registered(request):
|
||||||
return render(request,"home.html",{"registered":True})
|
return render(request, "home.html", {"registered": True})
|
||||||
|
|||||||
@@ -1,19 +1,22 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.mail import EmailMessage
|
from django.core.mail import EmailMessage
|
||||||
try:
|
from django.urls import reverse
|
||||||
from django.urls import reverse
|
|
||||||
except:
|
|
||||||
from django.core.urlresolver import reverse
|
|
||||||
|
|
||||||
def send(to,subject,body):
|
|
||||||
|
def send(to, subject, body):
|
||||||
from_email_address = settings.EMAIL_HOST_USER
|
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_email_address = settings.DEFAULT_FROM_EMAIL
|
||||||
From = "%s <%s>" % (settings.EMAIL_FROM, from_email_address)
|
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"
|
email.content_subtype = "html"
|
||||||
return email.send(False)
|
return email.send(False)
|
||||||
|
|
||||||
|
|
||||||
def get_redirect_url():
|
def get_redirect_url():
|
||||||
return {"redirect_html": reverse(getattr(settings, 'MFA_REDIRECT_AFTER_REGISTRATION', 'mfa_home')),
|
return {
|
||||||
"reg_success_msg":getattr(settings,"MFA_SUCCESS_REGISTRATION_MSG")}
|
"redirect_html": reverse(
|
||||||
|
getattr(settings, "MFA_REDIRECT_AFTER_REGISTRATION", "mfa_home")
|
||||||
|
),
|
||||||
|
"reg_success_msg": getattr(settings, "MFA_SUCCESS_REGISTRATION_MSG"),
|
||||||
|
}
|
||||||
|
|||||||
102
mfa/Email.py
102
mfa/Email.py
@@ -1,66 +1,90 @@
|
|||||||
from django.shortcuts import render
|
import datetime
|
||||||
from django.views.decorators.cache import never_cache
|
import random
|
||||||
from django.template.context_processors import csrf
|
|
||||||
import datetime,random
|
|
||||||
from random import randint
|
from random import randint
|
||||||
from .models import *
|
|
||||||
#from django.template.context import RequestContext
|
|
||||||
from .views import login
|
|
||||||
from .Common import send
|
|
||||||
|
|
||||||
def sendEmail(request,username,secret):
|
from django.conf import settings
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
|
from django.shortcuts import render
|
||||||
|
from django.template.context_processors import csrf
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.views.decorators.cache import never_cache
|
||||||
|
|
||||||
|
from .Common import send
|
||||||
|
from .models import UserKey
|
||||||
|
from .views import login
|
||||||
|
|
||||||
|
|
||||||
|
def send_email(request, username, secret):
|
||||||
"""Send Email to the user after rendering `mfa_email_token_template`"""
|
"""Send Email to the user after rendering `mfa_email_token_template`"""
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
key = getattr(User, 'USERNAME_FIELD', 'username')
|
key = getattr(User, "USERNAME_FIELD", "username")
|
||||||
kwargs = {key: username}
|
kwargs = {key: username}
|
||||||
user = User.objects.get(**kwargs)
|
user = User.objects.get(**kwargs)
|
||||||
res=render(request,"mfa_email_token_template.html",{"request":request,"user":user,'otp':secret})
|
res = render(
|
||||||
return send([user.email],"OTP", res.content.decode())
|
request,
|
||||||
|
"mfa_email_token_template.html",
|
||||||
|
{"request": request, "user": user, "otp": secret},
|
||||||
|
)
|
||||||
|
return send([user.email], "OTP", res.content.decode())
|
||||||
|
|
||||||
|
|
||||||
@never_cache
|
@never_cache
|
||||||
def start(request):
|
def start(request):
|
||||||
"""Start adding email as a 2nd factor"""
|
"""Start adding email as a 2nd factor"""
|
||||||
context = csrf(request)
|
context = csrf(request)
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
if request.session["email_secret"] == request.POST["otp"]: #if successful
|
if request.session["email_secret"] == request.POST["otp"]: # if successful
|
||||||
uk=User_Keys()
|
uk = UserKey()
|
||||||
uk.username=request.user.username
|
uk.username = request.user.username
|
||||||
uk.key_type="Email"
|
uk.key_type = "Email"
|
||||||
uk.enabled=1
|
uk.enabled = 1
|
||||||
uk.save()
|
uk.save()
|
||||||
from django.http import HttpResponseRedirect
|
return HttpResponseRedirect(
|
||||||
try:
|
reverse(
|
||||||
from django.core.urlresolvers import reverse
|
getattr(settings, "MFA_REDIRECT_AFTER_REGISTRATION", "mfa_home")
|
||||||
except:
|
)
|
||||||
from django.urls import reverse
|
)
|
||||||
return HttpResponseRedirect(reverse(getattr(settings,'MFA_REDIRECT_AFTER_REGISTRATION','mfa_home')))
|
|
||||||
context["invalid"] = True
|
context["invalid"] = True
|
||||||
else:
|
else:
|
||||||
request.session["email_secret"] = str(randint(0,100000)) #generate a random integer
|
request.session["email_secret"] = str(
|
||||||
if sendEmail(request, request.user.username, request.session["email_secret"]):
|
randint(0, 100000)
|
||||||
|
) # generate a random integer
|
||||||
|
if send_email(request, request.user.username, request.session["email_secret"]):
|
||||||
context["sent"] = True
|
context["sent"] = True
|
||||||
return render(request,"Email/Add.html", context)
|
return render(request, "Email/Add.html", context)
|
||||||
|
|
||||||
|
|
||||||
@never_cache
|
@never_cache
|
||||||
def auth(request):
|
def auth(request):
|
||||||
"""Authenticating the user by email."""
|
"""Authenticating the user by email."""
|
||||||
context=csrf(request)
|
context = csrf(request)
|
||||||
if request.method=="POST":
|
if request.method == "POST":
|
||||||
if request.session["email_secret"]==request.POST["otp"].strip():
|
if request.session["email_secret"] == request.POST["otp"].strip():
|
||||||
uk = User_Keys.objects.get(username=request.session["base_username"], key_type="Email")
|
uk = UserKey.objects.get(
|
||||||
mfa = {"verified": True, "method": "Email","id":uk.id}
|
username=request.session["base_username"], key_type="Email"
|
||||||
|
)
|
||||||
|
mfa = {"verified": True, "method": "Email", "id": uk.id}
|
||||||
if getattr(settings, "MFA_RECHECK", False):
|
if getattr(settings, "MFA_RECHECK", False):
|
||||||
mfa["next_check"] = datetime.datetime.timestamp(datetime.datetime.now() + datetime.timedelta(
|
mfa["next_check"] = datetime.datetime.timestamp(
|
||||||
seconds = random.randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX)))
|
datetime.datetime.now()
|
||||||
|
+ datetime.timedelta(
|
||||||
|
seconds=random.randint(
|
||||||
|
settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
request.session["mfa"] = mfa
|
request.session["mfa"] = mfa
|
||||||
|
|
||||||
from django.utils import timezone
|
uk.last_used = timezone.now()
|
||||||
uk.last_used=timezone.now()
|
|
||||||
uk.save()
|
uk.save()
|
||||||
return login(request)
|
return login(request)
|
||||||
context["invalid"]=True
|
context["invalid"] = True
|
||||||
else:
|
else:
|
||||||
request.session["email_secret"] = str(randint(0, 100000))
|
request.session["email_secret"] = str(randint(0, 100000))
|
||||||
if sendEmail(request, request.session["base_username"], request.session["email_secret"]):
|
if send_email(
|
||||||
|
request, request.session["base_username"], request.session["email_secret"]
|
||||||
|
):
|
||||||
context["sent"] = True
|
context["sent"] = True
|
||||||
return render(request,"Email/Auth.html", context)
|
return render(request, "Email/Auth.html", context)
|
||||||
|
|||||||
190
mfa/FIDO2.py
190
mfa/FIDO2.py
@@ -1,21 +1,23 @@
|
|||||||
from fido2.client import ClientData
|
|
||||||
from fido2.server import Fido2Server, PublicKeyCredentialRpEntity
|
|
||||||
from fido2.ctap2 import AttestationObject, AuthenticatorData
|
|
||||||
from django.template.context_processors import csrf
|
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
|
||||||
from django.shortcuts import render
|
|
||||||
# from django.template.context import RequestContext
|
|
||||||
import simplejson
|
|
||||||
from fido2 import cbor
|
|
||||||
from django.http import HttpResponse
|
|
||||||
from django.conf import settings
|
|
||||||
from .models import *
|
|
||||||
from fido2.utils import websafe_decode, websafe_encode
|
|
||||||
from fido2.ctap2 import AttestedCredentialData
|
|
||||||
from .views import login, reset_cookie
|
|
||||||
import datetime
|
import datetime
|
||||||
from .Common import get_redirect_url
|
import random
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.http import HttpResponse, JsonResponse
|
||||||
|
from django.shortcuts import render
|
||||||
|
from django.template.context_processors import csrf
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from fido2 import cbor
|
||||||
|
from fido2.client import ClientData
|
||||||
|
from fido2.ctap2 import AttestationObject, AttestedCredentialData, AuthenticatorData
|
||||||
|
from fido2.server import Fido2Server, PublicKeyCredentialRpEntity
|
||||||
|
from fido2.utils import websafe_decode, websafe_encode
|
||||||
|
|
||||||
|
from .Common import get_redirect_url
|
||||||
|
from .models import UserKey
|
||||||
|
from .views import login, reset_cookie
|
||||||
|
|
||||||
|
|
||||||
def recheck(request):
|
def recheck(request):
|
||||||
@@ -26,7 +28,7 @@ def recheck(request):
|
|||||||
return render(request, "FIDO2/recheck.html", context)
|
return render(request, "FIDO2/recheck.html", context)
|
||||||
|
|
||||||
|
|
||||||
def getServer():
|
def get_server():
|
||||||
"""Get Server Info from settings and returns a Fido2Server"""
|
"""Get Server Info from settings and returns a Fido2Server"""
|
||||||
rp = PublicKeyCredentialRpEntity(settings.FIDO_SERVER_ID, settings.FIDO_SERVER_NAME)
|
rp = PublicKeyCredentialRpEntity(settings.FIDO_SERVER_ID, settings.FIDO_SERVER_NAME)
|
||||||
return Fido2Server(rp)
|
return Fido2Server(rp)
|
||||||
@@ -34,15 +36,20 @@ def getServer():
|
|||||||
|
|
||||||
def begin_registeration(request):
|
def begin_registeration(request):
|
||||||
"""Starts registering a new FIDO Device, called from API"""
|
"""Starts registering a new FIDO Device, called from API"""
|
||||||
server = getServer()
|
server = get_server()
|
||||||
registration_data, state = server.register_begin({
|
registration_data, state = server.register_begin(
|
||||||
u'id': request.user.username.encode("utf8"),
|
{
|
||||||
u'name': (request.user.first_name + " " + request.user.last_name),
|
u"id": request.user.username.encode("utf8"),
|
||||||
u'displayName': request.user.username,
|
u"name": (request.user.first_name + " " + request.user.last_name),
|
||||||
}, getUserCredentials(request.user.username))
|
u"displayName": request.user.username,
|
||||||
request.session['fido_state'] = state
|
},
|
||||||
|
get_user_credentials(request.user.username),
|
||||||
|
)
|
||||||
|
request.session["fido_state"] = state
|
||||||
|
|
||||||
return HttpResponse(cbor.encode(registration_data), content_type = 'application/octet-stream')
|
return HttpResponse(
|
||||||
|
cbor.encode(registration_data), content_type="application/octet-stream"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
@@ -51,29 +58,28 @@ def complete_reg(request):
|
|||||||
try:
|
try:
|
||||||
data = cbor.decode(request.body)
|
data = cbor.decode(request.body)
|
||||||
|
|
||||||
client_data = ClientData(data['clientDataJSON'])
|
client_data = ClientData(data["clientDataJSON"])
|
||||||
att_obj = AttestationObject((data['attestationObject']))
|
att_obj = AttestationObject((data["attestationObject"]))
|
||||||
server = getServer()
|
server = get_server()
|
||||||
auth_data = server.register_complete(
|
auth_data = server.register_complete(
|
||||||
request.session['fido_state'],
|
request.session["fido_state"], client_data, att_obj
|
||||||
client_data,
|
|
||||||
att_obj
|
|
||||||
)
|
)
|
||||||
encoded = websafe_encode(auth_data.credential_data)
|
encoded = websafe_encode(auth_data.credential_data)
|
||||||
uk = User_Keys()
|
uk = UserKey()
|
||||||
uk.username = request.user.username
|
uk.username = request.user.username
|
||||||
uk.properties = {"device": encoded, "type": att_obj.fmt, }
|
uk.properties = {
|
||||||
|
"device": encoded,
|
||||||
|
"type": att_obj.fmt,
|
||||||
|
}
|
||||||
uk.owned_by_enterprise = getattr(settings, "MFA_OWNED_BY_ENTERPRISE", False)
|
uk.owned_by_enterprise = getattr(settings, "MFA_OWNED_BY_ENTERPRISE", False)
|
||||||
uk.key_type = "FIDO2"
|
uk.key_type = "FIDO2"
|
||||||
uk.save()
|
uk.save()
|
||||||
return HttpResponse(simplejson.dumps({'status': 'OK'}))
|
return JsonResponse({"status": "OK"})
|
||||||
except Exception as exp:
|
except Exception as exp:
|
||||||
try:
|
print(traceback.format_exc())
|
||||||
from raven.contrib.django.raven_compat.models import client
|
return JsonResponse(
|
||||||
client.captureException()
|
{"status": "ERR", "message": "Error on server, please try again later"}
|
||||||
except:
|
)
|
||||||
pass
|
|
||||||
return HttpResponse(simplejson.dumps({'status': 'ERR', "message": "Error on server, please try again later"}))
|
|
||||||
|
|
||||||
|
|
||||||
def start(request):
|
def start(request):
|
||||||
@@ -83,10 +89,12 @@ def start(request):
|
|||||||
return render(request, "FIDO2/Add.html", context)
|
return render(request, "FIDO2/Add.html", context)
|
||||||
|
|
||||||
|
|
||||||
def getUserCredentials(username):
|
def get_user_credentials(username):
|
||||||
credentials = []
|
credentials = []
|
||||||
for uk in User_Keys.objects.filter(username = username, key_type = "FIDO2"):
|
for uk in UserKey.objects.filter(username=username, key_type="FIDO2"):
|
||||||
credentials.append(AttestedCredentialData(websafe_decode(uk.properties["device"])))
|
credentials.append(
|
||||||
|
AttestedCredentialData(websafe_decode(uk.properties["device"]))
|
||||||
|
)
|
||||||
return credentials
|
return credentials
|
||||||
|
|
||||||
|
|
||||||
@@ -96,11 +104,13 @@ def auth(request):
|
|||||||
|
|
||||||
|
|
||||||
def authenticate_begin(request):
|
def authenticate_begin(request):
|
||||||
server = getServer()
|
server = get_server()
|
||||||
credentials = getUserCredentials(request.session.get("base_username", request.user.username))
|
credentials = get_user_credentials(
|
||||||
|
request.session.get("base_username", request.user.username)
|
||||||
|
)
|
||||||
auth_data, state = server.authenticate_begin(credentials)
|
auth_data, state = server.authenticate_begin(credentials)
|
||||||
request.session['fido_state'] = state
|
request.session["fido_state"] = state
|
||||||
return HttpResponse(cbor.encode(auth_data), content_type = "application/octet-stream")
|
return HttpResponse(cbor.encode(auth_data), content_type="application/octet-stream")
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
@@ -108,64 +118,70 @@ def authenticate_complete(request):
|
|||||||
try:
|
try:
|
||||||
credentials = []
|
credentials = []
|
||||||
username = request.session.get("base_username", request.user.username)
|
username = request.session.get("base_username", request.user.username)
|
||||||
server = getServer()
|
server = get_server()
|
||||||
credentials = getUserCredentials(username)
|
credentials = get_user_credentials(username)
|
||||||
data = cbor.decode(request.body)
|
data = cbor.decode(request.body)
|
||||||
credential_id = data['credentialId']
|
credential_id = data["credentialId"]
|
||||||
client_data = ClientData(data['clientDataJSON'])
|
client_data = ClientData(data["clientDataJSON"])
|
||||||
auth_data = AuthenticatorData(data['authenticatorData'])
|
auth_data = AuthenticatorData(data["authenticatorData"])
|
||||||
signature = data['signature']
|
signature = data["signature"]
|
||||||
try:
|
try:
|
||||||
cred = server.authenticate_complete(
|
cred = server.authenticate_complete(
|
||||||
request.session.pop('fido_state'),
|
request.session.pop("fido_state"),
|
||||||
credentials,
|
credentials,
|
||||||
credential_id,
|
credential_id,
|
||||||
client_data,
|
client_data,
|
||||||
auth_data,
|
auth_data,
|
||||||
signature
|
signature,
|
||||||
)
|
)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return HttpResponse(simplejson.dumps({'status': "ERR",
|
return JsonResponse(
|
||||||
"message": "Wrong challenge received, make sure that this is your security and try again."}),
|
{
|
||||||
content_type = "application/json")
|
"status": "ERR",
|
||||||
|
"message": "Wrong challenge received, make sure that this is your security and try again.",
|
||||||
|
}
|
||||||
|
)
|
||||||
except Exception as excep:
|
except Exception as excep:
|
||||||
try:
|
print(traceback.format_exc())
|
||||||
from raven.contrib.django.raven_compat.models import client
|
return JsonResponse({"status": "ERR", "message": excep.message})
|
||||||
client.captureException()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
return HttpResponse(simplejson.dumps({'status': "ERR",
|
|
||||||
"message": excep.message}),
|
|
||||||
content_type = "application/json")
|
|
||||||
|
|
||||||
if request.session.get("mfa_recheck", False):
|
if request.session.get("mfa_recheck", False):
|
||||||
import time
|
|
||||||
request.session["mfa"]["rechecked_at"] = time.time()
|
request.session["mfa"]["rechecked_at"] = time.time()
|
||||||
return HttpResponse(simplejson.dumps({'status': "OK"}),
|
return JsonResponse({"status": "OK"})
|
||||||
content_type = "application/json")
|
|
||||||
else:
|
else:
|
||||||
import random
|
keys = UserKey.objects.filter(
|
||||||
keys = User_Keys.objects.filter(username = username, key_type = "FIDO2", enabled = 1)
|
username=username, key_type="FIDO2", enabled=1
|
||||||
|
)
|
||||||
for k in keys:
|
for k in keys:
|
||||||
if AttestedCredentialData(websafe_decode(k.properties["device"])).credential_id == cred.credential_id:
|
if (
|
||||||
|
AttestedCredentialData(
|
||||||
|
websafe_decode(k.properties["device"])
|
||||||
|
).credential_id
|
||||||
|
== cred.credential_id
|
||||||
|
):
|
||||||
k.last_used = timezone.now()
|
k.last_used = timezone.now()
|
||||||
k.save()
|
k.save()
|
||||||
mfa = {"verified": True, "method": "FIDO2", 'id': k.id}
|
mfa = {"verified": True, "method": "FIDO2", "id": k.id}
|
||||||
if getattr(settings, "MFA_RECHECK", False):
|
if getattr(settings, "MFA_RECHECK", False):
|
||||||
mfa["next_check"] = datetime.datetime.timestamp((datetime.datetime.now() + datetime.timedelta(
|
mfa["next_check"] = datetime.datetime.timestamp(
|
||||||
seconds = random.randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX))))
|
(
|
||||||
|
datetime.datetime.now()
|
||||||
|
+ datetime.timedelta(
|
||||||
|
seconds=random.randint(
|
||||||
|
settings.MFA_RECHECK_MIN,
|
||||||
|
settings.MFA_RECHECK_MAX,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
request.session["mfa"] = mfa
|
request.session["mfa"] = mfa
|
||||||
try:
|
if not request.user.is_authenticated:
|
||||||
authenticated = request.user.is_authenticated
|
|
||||||
except:
|
|
||||||
authenticated = request.user.is_authenticated()
|
|
||||||
if not authenticated:
|
|
||||||
res = login(request)
|
res = login(request)
|
||||||
if not "location" in res: return reset_cookie(request)
|
if "location" not in res:
|
||||||
return HttpResponse(simplejson.dumps({'status': "OK", "redirect": res["location"]}),
|
return reset_cookie(request)
|
||||||
content_type = "application/json")
|
return JsonResponse(
|
||||||
return HttpResponse(simplejson.dumps({'status': "OK"}),
|
{"status": "OK", "redirect": res["location"]}
|
||||||
content_type = "application/json")
|
)
|
||||||
|
return JsonResponse({"status": "OK"})
|
||||||
except Exception as exp:
|
except Exception as exp:
|
||||||
return HttpResponse(simplejson.dumps({'status': "ERR", "message": exp.message}),
|
return JsonResponse({"status": "ERR", "message": str(exp)})
|
||||||
content_type = "application/json")
|
|
||||||
|
|||||||
@@ -1,134 +1,162 @@
|
|||||||
import string
|
|
||||||
import random
|
import random
|
||||||
from django.shortcuts import render
|
import string
|
||||||
from django.http import HttpResponse
|
from datetime import datetime, timedelta
|
||||||
from django.template.context import RequestContext
|
|
||||||
from django.template.context_processors import csrf
|
|
||||||
from .models import *
|
|
||||||
import user_agents
|
import user_agents
|
||||||
|
from django.conf import settings
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.shortcuts import render
|
||||||
|
from django.template.context_processors import csrf
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from jose import jwt
|
||||||
|
|
||||||
|
from .Common import send
|
||||||
|
from .models import UserKey
|
||||||
|
|
||||||
|
|
||||||
def id_generator(size=6, chars=string.ascii_uppercase + string.digits):
|
def id_generator(size=6, chars=string.ascii_uppercase + string.digits):
|
||||||
x=''.join(random.choice(chars) for _ in range(size))
|
x = "".join(random.choice(chars) for _ in range(size))
|
||||||
if not User_Keys.objects.filter(properties__shas="$.key="+x).exists(): return x
|
if not UserKey.objects.filter(properties__shas="$.key=" + x).exists():
|
||||||
else: return id_generator(size,chars)
|
return x
|
||||||
|
else:
|
||||||
|
return id_generator(size, chars)
|
||||||
|
|
||||||
def getUserAgent(request):
|
|
||||||
id=id=request.session.get("td_id",None)
|
def get_user_agent(request):
|
||||||
|
id = id = request.session.get("td_id", None)
|
||||||
if id:
|
if id:
|
||||||
tk=User_Keys.objects.get(id=id)
|
tk = UserKey.objects.get(id=id)
|
||||||
if tk.properties.get("user_agent","")!="":
|
if tk.properties.get("user_agent", "") != "":
|
||||||
ua = user_agents.parse(tk.properties["user_agent"])
|
ua = user_agents.parse(tk.properties["user_agent"])
|
||||||
res = render(None, "TrustedDevices/user-agent.html", context={"ua":ua})
|
res = render(None, "TrustedDevices/user-agent.html", context={"ua": ua})
|
||||||
return HttpResponse(res)
|
return HttpResponse(res)
|
||||||
return HttpResponse("")
|
return HttpResponse("")
|
||||||
|
|
||||||
|
|
||||||
def trust_device(request):
|
def trust_device(request):
|
||||||
tk = User_Keys.objects.get(id=request.session["td_id"])
|
tk = UserKey.objects.get(id=request.session["td_id"])
|
||||||
tk.properties["status"]="trusted"
|
tk.properties["status"] = "trusted"
|
||||||
tk.save()
|
tk.save()
|
||||||
del request.session["td_id"]
|
del request.session["td_id"]
|
||||||
return HttpResponse("OK")
|
return HttpResponse("OK")
|
||||||
|
|
||||||
def checkTrusted(request):
|
|
||||||
|
def check_trusted(request):
|
||||||
res = ""
|
res = ""
|
||||||
id=request.session.get("td_id","")
|
id = request.session.get("td_id", "")
|
||||||
if id!="":
|
if id != "":
|
||||||
try:
|
try:
|
||||||
tk = User_Keys.objects.get(id=id)
|
tk = UserKey.objects.get(id=id)
|
||||||
if tk.properties["status"] == "trusted": res = "OK"
|
if tk.properties["status"] == "trusted":
|
||||||
|
res = "OK"
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
return HttpResponse(res)
|
return HttpResponse(res)
|
||||||
|
|
||||||
def getCookie(request):
|
|
||||||
tk = User_Keys.objects.get(id=request.session["td_id"])
|
def get_cookie(request):
|
||||||
|
tk = UserKey.objects.get(id=request.session["td_id"])
|
||||||
|
|
||||||
if tk.properties["status"] == "trusted":
|
if tk.properties["status"] == "trusted":
|
||||||
context={"added":True}
|
context = {"added": True}
|
||||||
response = render(request,"TrustedDevices/Done.html", context)
|
response = render(request, "TrustedDevices/Done.html", context)
|
||||||
from datetime import datetime, timedelta
|
|
||||||
expires = datetime.now() + timedelta(days=180)
|
expires = datetime.now() + timedelta(days=180)
|
||||||
tk.expires=expires
|
tk.expires = expires
|
||||||
tk.save()
|
tk.save()
|
||||||
response.set_cookie("deviceid", tk.properties["signature"], expires=expires)
|
response.set_cookie("deviceid", tk.properties["signature"], expires=expires)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
def add(request):
|
def add(request):
|
||||||
context=csrf(request)
|
context = csrf(request)
|
||||||
if request.method=="GET":
|
if request.method == "GET":
|
||||||
return render(request,"TrustedDevices/Add.html",context)
|
return render(request, "TrustedDevices/Add.html", context)
|
||||||
else:
|
else:
|
||||||
key=request.POST["key"].replace("-","").replace(" ","").upper()
|
key = request.POST["key"].replace("-", "").replace(" ", "").upper()
|
||||||
context["username"] = request.POST["username"]
|
context["username"] = request.POST["username"]
|
||||||
context["key"] = request.POST["key"]
|
context["key"] = request.POST["key"]
|
||||||
trusted_keys=User_Keys.objects.filter(username=request.POST["username"],properties__has="$.key="+key)
|
trusted_keys = UserKey.objects.filter(
|
||||||
cookie=False
|
username=request.POST["username"], properties__has="$.key=" + key
|
||||||
|
)
|
||||||
|
cookie = False
|
||||||
if trusted_keys.exists():
|
if trusted_keys.exists():
|
||||||
tk=trusted_keys[0]
|
tk = trusted_keys[0]
|
||||||
request.session["td_id"]=tk.id
|
request.session["td_id"] = tk.id
|
||||||
ua=request.META['HTTP_USER_AGENT']
|
ua = request.META["HTTP_USER_AGENT"]
|
||||||
agent=user_agents.parse(ua)
|
agent = user_agents.parse(ua)
|
||||||
if agent.is_pc:
|
if agent.is_pc:
|
||||||
context["invalid"]="This is a PC, it can't used as a trusted device."
|
context["invalid"] = "This is a PC, it can't used as a trusted device."
|
||||||
else:
|
else:
|
||||||
tk.properties["user_agent"]=ua
|
tk.properties["user_agent"] = ua
|
||||||
tk.save()
|
tk.save()
|
||||||
context["success"]=True
|
context["success"] = True
|
||||||
# tk.properties["user_agent"]=ua
|
|
||||||
# tk.save()
|
|
||||||
# context["success"]=True
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
context["invalid"]="The username or key is wrong, please check and try again."
|
context[
|
||||||
|
"invalid"
|
||||||
|
] = "The username or key is wrong, please check and try again."
|
||||||
|
|
||||||
|
return render(request, "TrustedDevices/Add.html", context)
|
||||||
|
|
||||||
return render(request,"TrustedDevices/Add.html", context)
|
|
||||||
|
|
||||||
def start(request):
|
def start(request):
|
||||||
if User_Keys.objects.filter(username=request.user.username,key_type="Trusted Device").count()>= 2:
|
if (
|
||||||
return render(request,"TrustedDevices/start.html",{"not_allowed":True})
|
UserKey.objects.filter(
|
||||||
td=None
|
username=request.user.username, key_type="Trusted Device"
|
||||||
if not request.session.get("td_id",None):
|
).count()
|
||||||
td=User_Keys()
|
>= 2
|
||||||
td.username=request.user.username
|
):
|
||||||
td.properties={"key":id_generator(),"status":"adding"}
|
return render(request, "TrustedDevices/start.html", {"not_allowed": True})
|
||||||
td.key_type="Trusted Device"
|
td = None
|
||||||
|
if not request.session.get("td_id", None):
|
||||||
|
td = UserKey()
|
||||||
|
td.username = request.user.username
|
||||||
|
td.properties = {"key": id_generator(), "status": "adding"}
|
||||||
|
td.key_type = "Trusted Device"
|
||||||
td.save()
|
td.save()
|
||||||
request.session["td_id"]=td.id
|
request.session["td_id"] = td.id
|
||||||
try:
|
try:
|
||||||
if td==None: td=User_Keys.objects.get(id=request.session["td_id"])
|
if td is None:
|
||||||
context={"key":td.properties["key"]}
|
td = UserKey.objects.get(id=request.session["td_id"])
|
||||||
|
context = {"key": td.properties["key"]}
|
||||||
except:
|
except:
|
||||||
del request.session["td_id"]
|
del request.session["td_id"]
|
||||||
return start(request)
|
return start(request)
|
||||||
return render(request,"TrustedDevices/start.html",context)
|
return render(request, "TrustedDevices/start.html", context)
|
||||||
|
|
||||||
|
|
||||||
def send_email(request):
|
def send_email(request):
|
||||||
body=render(request,"TrustedDevices/email.html",{}).content
|
body = render(request, "TrustedDevices/email.html", {}).content
|
||||||
from .Common import send
|
e = request.user.email
|
||||||
e=request.user.email
|
if e == "":
|
||||||
if e=="":
|
e = request.session.get("user", {}).get("email", "")
|
||||||
e=request.session.get("user",{}).get("email","")
|
if e == "":
|
||||||
if e=="":
|
|
||||||
res = "User has no email on the system."
|
res = "User has no email on the system."
|
||||||
elif send([e],"Add Trusted Device Link",body):
|
elif send([e], "Add Trusted Device Link", body):
|
||||||
res="Sent Successfully"
|
res = "Sent Successfully"
|
||||||
else:
|
else:
|
||||||
res="Error occured, please try again later."
|
res = "Error occured, please try again later."
|
||||||
return HttpResponse(res)
|
return HttpResponse(res)
|
||||||
|
|
||||||
|
|
||||||
def verify(request):
|
def verify(request):
|
||||||
if request.COOKIES.get('deviceid',None):
|
if request.COOKIES.get("deviceid", None):
|
||||||
from jose import jwt
|
json = jwt.decode(request.COOKIES.get("deviceid"), settings.SECRET_KEY)
|
||||||
json= jwt.decode(request.COOKIES.get('deviceid'),settings.SECRET_KEY)
|
if json["username"].lower() == request.session["base_username"].lower():
|
||||||
if json["username"].lower()== request.session['base_username'].lower():
|
|
||||||
try:
|
try:
|
||||||
uk = User_Keys.objects.get(username=request.POST["username"].lower(), properties__has="$.key=" + json["key"])
|
uk = UserKey.objects.get(
|
||||||
|
username=request.POST["username"].lower(),
|
||||||
|
properties__has="$.key=" + json["key"],
|
||||||
|
)
|
||||||
if uk.enabled and uk.properties["status"] == "trusted":
|
if uk.enabled and uk.properties["status"] == "trusted":
|
||||||
uk.last_used=timezone.now()
|
uk.last_used = timezone.now()
|
||||||
uk.save()
|
uk.save()
|
||||||
request.session["mfa"] = {"verified": True, "method": "Trusted Device","id":uk.id}
|
request.session["mfa"] = {
|
||||||
|
"verified": True,
|
||||||
|
"method": "Trusted Device",
|
||||||
|
"id": uk.id,
|
||||||
|
}
|
||||||
return True
|
return True
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|||||||
136
mfa/U2F.py
136
mfa/U2F.py
@@ -1,111 +1,141 @@
|
|||||||
|
import datetime
|
||||||
|
import hashlib
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
|
||||||
from u2flib_server.u2f import (begin_registration, begin_authentication,
|
import simplejson
|
||||||
complete_registration, complete_authentication)
|
|
||||||
from cryptography import x509
|
from cryptography import x509
|
||||||
from cryptography.hazmat.backends import default_backend
|
from cryptography.hazmat.backends import default_backend
|
||||||
from cryptography.hazmat.primitives.serialization import Encoding
|
from cryptography.hazmat.primitives.serialization import Encoding
|
||||||
from django.shortcuts import render
|
|
||||||
import simplejson
|
|
||||||
#from django.template.context import RequestContext
|
|
||||||
from django.template.context_processors import csrf
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse, JsonResponse
|
||||||
from .models import *
|
from django.shortcuts import render
|
||||||
from .views import login
|
from django.template.context_processors import csrf
|
||||||
import datetime
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from u2flib_server.u2f import (
|
||||||
|
begin_authentication,
|
||||||
|
begin_registration,
|
||||||
|
complete_authentication,
|
||||||
|
complete_registration,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .Common import get_redirect_url
|
||||||
|
from .models import UserKey
|
||||||
|
from .views import login
|
||||||
|
|
||||||
|
|
||||||
def recheck(request):
|
def recheck(request):
|
||||||
context = csrf(request)
|
context = csrf(request)
|
||||||
context["mode"]="recheck"
|
context["mode"] = "recheck"
|
||||||
s = sign(request.user.username)
|
s = sign(request.user.username)
|
||||||
request.session["_u2f_challenge_"] = s[0]
|
request.session["_u2f_challenge_"] = s[0]
|
||||||
context["token"] = s[1]
|
context["token"] = s[1]
|
||||||
request.session["mfa_recheck"]=True
|
request.session["mfa_recheck"] = True
|
||||||
return render(request,"U2F/recheck.html", context)
|
return render(request, "U2F/recheck.html", context)
|
||||||
|
|
||||||
|
|
||||||
def process_recheck(request):
|
def process_recheck(request):
|
||||||
x=validate(request,request.user.username)
|
x = validate(request, request.user.username)
|
||||||
if x==True:
|
if x is True:
|
||||||
import time
|
|
||||||
request.session["mfa"]["rechecked_at"] = time.time()
|
request.session["mfa"]["rechecked_at"] = time.time()
|
||||||
return HttpResponse(simplejson.dumps({"recheck":True}),content_type="application/json")
|
return JsonResponse({"recheck": True})
|
||||||
return x
|
return x
|
||||||
|
|
||||||
|
|
||||||
def check_errors(request, data):
|
def check_errors(request, data):
|
||||||
if "errorCode" in data:
|
if "errorCode" in data:
|
||||||
if data["errorCode"] == 0: return True
|
if data["errorCode"] == 0:
|
||||||
|
return True
|
||||||
if data["errorCode"] == 4:
|
if data["errorCode"] == 4:
|
||||||
return HttpResponse("Invalid Security Key")
|
return HttpResponse("Invalid Security Key")
|
||||||
if data["errorCode"] == 1:
|
if data["errorCode"] == 1:
|
||||||
return auth(request)
|
return auth(request)
|
||||||
return True
|
return True
|
||||||
def validate(request,username):
|
|
||||||
import datetime, random
|
|
||||||
|
|
||||||
|
|
||||||
|
def validate(request, username):
|
||||||
data = simplejson.loads(request.POST["response"])
|
data = simplejson.loads(request.POST["response"])
|
||||||
|
|
||||||
res= check_errors(request,data)
|
res = check_errors(request, data)
|
||||||
if res!=True:
|
if res is not True:
|
||||||
return res
|
return res
|
||||||
|
|
||||||
challenge = request.session.pop('_u2f_challenge_')
|
challenge = request.session.pop("_u2f_challenge_")
|
||||||
device, c, t = complete_authentication(challenge, data, [settings.U2F_APPID])
|
device, c, t = complete_authentication(challenge, data, [settings.U2F_APPID])
|
||||||
|
|
||||||
key=User_Keys.objects.get(username=username,properties__shas="$.device.publicKey=%s"%device["publicKey"])
|
key = UserKey.objects.get(
|
||||||
key.last_used=timezone.now()
|
username=username,
|
||||||
|
properties__shas="$.device.publicKey=%s" % device["publicKey"],
|
||||||
|
)
|
||||||
|
key.last_used = timezone.now()
|
||||||
key.save()
|
key.save()
|
||||||
mfa = {"verified": True, "method": "U2F","id":key.id}
|
mfa = {"verified": True, "method": "U2F", "id": key.id}
|
||||||
if getattr(settings, "MFA_RECHECK", False):
|
if getattr(settings, "MFA_RECHECK", False):
|
||||||
mfa["next_check"] = datetime.datetime.timestamp((datetime.datetime.now()
|
mfa["next_check"] = datetime.datetime.timestamp(
|
||||||
+ datetime.timedelta(
|
(
|
||||||
seconds=random.randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX))))
|
datetime.datetime.now()
|
||||||
|
+ datetime.timedelta(
|
||||||
|
seconds=random.randint(
|
||||||
|
settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
request.session["mfa"] = mfa
|
request.session["mfa"] = mfa
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def auth(request):
|
|
||||||
context=csrf(request)
|
|
||||||
s=sign(request.session["base_username"])
|
|
||||||
request.session["_u2f_challenge_"]=s[0]
|
|
||||||
context["token"]=s[1]
|
|
||||||
|
|
||||||
return render(request,"U2F/Auth.html")
|
def auth(request):
|
||||||
|
context = csrf(request)
|
||||||
|
s = sign(request.session["base_username"])
|
||||||
|
request.session["_u2f_challenge_"] = s[0]
|
||||||
|
context["token"] = s[1]
|
||||||
|
|
||||||
|
return render(request, "U2F/Auth.html")
|
||||||
|
|
||||||
|
|
||||||
def start(request):
|
def start(request):
|
||||||
enroll = begin_registration(settings.U2F_APPID, [])
|
enroll = begin_registration(settings.U2F_APPID, [])
|
||||||
request.session['_u2f_enroll_'] = enroll.json
|
request.session["_u2f_enroll_"] = enroll.json
|
||||||
context=csrf(request)
|
context = csrf(request)
|
||||||
context["token"]=simplejson.dumps(enroll.data_for_client)
|
context["token"] = simplejson.dumps(enroll.data_for_client)
|
||||||
context.update(get_redirect_url())
|
context.update(get_redirect_url())
|
||||||
return render(request,"U2F/Add.html",context)
|
return render(request, "U2F/Add.html", context)
|
||||||
|
|
||||||
|
|
||||||
def bind(request):
|
def bind(request):
|
||||||
import hashlib
|
enroll = request.session["_u2f_enroll_"]
|
||||||
enroll = request.session['_u2f_enroll_']
|
data = simplejson.loads(request.POST["response"])
|
||||||
data=simplejson.loads(request.POST["response"])
|
|
||||||
device, cert = complete_registration(enroll, data, [settings.U2F_APPID])
|
device, cert = complete_registration(enroll, data, [settings.U2F_APPID])
|
||||||
cert = x509.load_der_x509_certificate(cert, default_backend())
|
cert = x509.load_der_x509_certificate(cert, default_backend())
|
||||||
cert_hash=hashlib.md5(cert.public_bytes(Encoding.PEM)).hexdigest()
|
cert_hash = hashlib.md5(cert.public_bytes(Encoding.PEM)).hexdigest()
|
||||||
q=User_Keys.objects.filter(key_type="U2F", properties__icontains= cert_hash)
|
q = UserKey.objects.filter(key_type="U2F", properties__icontains=cert_hash)
|
||||||
if q.exists():
|
if q.exists():
|
||||||
return HttpResponse("This key is registered before, it can't be registered again.")
|
return HttpResponse(
|
||||||
User_Keys.objects.filter(username=request.user.username,key_type="U2F").delete()
|
"This key is registered before, it can't be registered again."
|
||||||
uk = User_Keys()
|
)
|
||||||
|
UserKey.objects.filter(username=request.user.username, key_type="U2F").delete()
|
||||||
|
uk = UserKey()
|
||||||
uk.username = request.user.username
|
uk.username = request.user.username
|
||||||
uk.owned_by_enterprise = getattr(settings, "MFA_OWNED_BY_ENTERPRISE", False)
|
uk.owned_by_enterprise = getattr(settings, "MFA_OWNED_BY_ENTERPRISE", False)
|
||||||
uk.properties = {"device":simplejson.loads(device.json),"cert":cert_hash}
|
uk.properties = {"device": simplejson.loads(device.json), "cert": cert_hash}
|
||||||
uk.key_type = "U2F"
|
uk.key_type = "U2F"
|
||||||
uk.save()
|
uk.save()
|
||||||
return HttpResponse("OK")
|
return HttpResponse("OK")
|
||||||
|
|
||||||
|
|
||||||
def sign(username):
|
def sign(username):
|
||||||
u2f_devices=[d.properties["device"] for d in User_Keys.objects.filter(username=username,key_type="U2F")]
|
u2f_devices = [
|
||||||
|
d.properties["device"]
|
||||||
|
for d in UserKey.objects.filter(username=username, key_type="U2F")
|
||||||
|
]
|
||||||
challenge = begin_authentication(settings.U2F_APPID, u2f_devices)
|
challenge = begin_authentication(settings.U2F_APPID, u2f_devices)
|
||||||
return [challenge.json,simplejson.dumps(challenge.data_for_client)]
|
return [challenge.json, simplejson.dumps(challenge.data_for_client)]
|
||||||
|
|
||||||
|
|
||||||
def verify(request):
|
def verify(request):
|
||||||
x= validate(request,request.session["base_username"])
|
x = validate(request, request.session["base_username"])
|
||||||
if x==True:
|
if x is True:
|
||||||
return login(request)
|
return login(request)
|
||||||
else: return x
|
else:
|
||||||
|
return x
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__="2.2.0"
|
__version__ = "2.2.0"
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
# Register your models here.
|
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class myAppNameConfig(AppConfig):
|
class myAppNameConfig(AppConfig):
|
||||||
name = 'mfa'
|
name = "mfa"
|
||||||
verbose_name = 'A Much Better Name'
|
verbose_name = "A Much Better Name"
|
||||||
|
|||||||
@@ -1,32 +1,32 @@
|
|||||||
import pyotp
|
from django.http import JsonResponse
|
||||||
from .models import *
|
|
||||||
from . import TrustedDevice, U2F, FIDO2, totp
|
from . import FIDO2, U2F, TrustedDevice, totp
|
||||||
import simplejson
|
from .models import UserKey
|
||||||
from django.shortcuts import HttpResponse
|
from .views import verify
|
||||||
from mfa.views import verify,goto
|
|
||||||
def has_mfa(request,username):
|
|
||||||
if User_Keys.objects.filter(username=username,enabled=1).count()>0:
|
def has_mfa(request, username):
|
||||||
|
if UserKey.objects.filter(username=username, enabled=1).count() > 0:
|
||||||
return verify(request, username)
|
return verify(request, username)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def is_mfa(request,ignore_methods=[]):
|
|
||||||
if request.session.get("mfa",{}).get("verified",False):
|
def is_mfa(request, ignore_methods=[]):
|
||||||
if not request.session.get("mfa",{}).get("method",None) in ignore_methods:
|
if request.session.get("mfa", {}).get("verified", False):
|
||||||
|
if not request.session.get("mfa", {}).get("method", None) in ignore_methods:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def recheck(request):
|
def recheck(request):
|
||||||
method=request.session.get("mfa",{}).get("method",None)
|
method = request.session.get("mfa", {}).get("method", None)
|
||||||
if not method:
|
if not method:
|
||||||
return HttpResponse(simplejson.dumps({"res":False}),content_type="application/json")
|
return JsonResponse({"res": False})
|
||||||
if method=="Trusted Device":
|
if method == "Trusted Device":
|
||||||
return HttpResponse(simplejson.dumps({"res":TrustedDevice.verify(request)}),content_type="application/json")
|
return JsonResponse({"res": TrustedDevice.verify(request)})
|
||||||
elif method=="U2F":
|
elif method == "U2F":
|
||||||
return HttpResponse(simplejson.dumps({"html": U2F.recheck(request).content}), content_type="application/json")
|
return JsonResponse({"html": U2F.recheck(request).content})
|
||||||
elif method == "FIDO2":
|
elif method == "FIDO2":
|
||||||
return HttpResponse(simplejson.dumps({"html": FIDO2.recheck(request).content}), content_type="application/json")
|
return JsonResponse({"html": FIDO2.recheck(request).content})
|
||||||
elif method=="TOTP":
|
elif method == "TOTP":
|
||||||
return HttpResponse(simplejson.dumps({"html": totp.recheck(request).content}), content_type="application/json")
|
return JsonResponse({"html": totp.recheck(request).content})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
import time
|
import time
|
||||||
from django.http import HttpResponseRedirect
|
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
|
||||||
def process(request):
|
def process(request):
|
||||||
next_check=request.session.get('mfa',{}).get("next_check",False)
|
next_check = request.session.get("mfa", {}).get("next_check", False)
|
||||||
if not next_check: return None
|
if not next_check:
|
||||||
now=int(time.time())
|
return None
|
||||||
|
now = int(time.time())
|
||||||
if now >= next_check:
|
if now >= next_check:
|
||||||
method=request.session["mfa"]["method"]
|
method = request.session["mfa"]["method"]
|
||||||
path = request.META["PATH_INFO"]
|
path = request.META["PATH_INFO"]
|
||||||
return HttpResponseRedirect(reverse(method+"_auth")+"?next=%s"%(settings.BASE_URL + path).replace("//", "/"))
|
return HttpResponseRedirect(
|
||||||
return None
|
reverse(method + "_auth")
|
||||||
|
+ "?next=%s" % (settings.BASE_URL + path).replace("//", "/")
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|||||||
@@ -6,17 +6,24 @@ from django.db import models, migrations
|
|||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = []
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='User_Keys',
|
name="User_Keys",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
(
|
||||||
('username', models.CharField(max_length=50)),
|
"id",
|
||||||
('secret_key', models.CharField(max_length=15)),
|
models.AutoField(
|
||||||
('added_on', models.DateTimeField(auto_now_add=True)),
|
verbose_name="ID",
|
||||||
|
serialize=False,
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("username", models.CharField(max_length=50)),
|
||||||
|
("secret_key", models.CharField(max_length=15)),
|
||||||
|
("added_on", models.DateTimeField(auto_now_add=True)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ from django.db import models, migrations
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('mfa', '0001_initial'),
|
("mfa", "0001_initial"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='user_keys',
|
model_name="user_keys",
|
||||||
name='key_type',
|
name="key_type",
|
||||||
field=models.CharField(default=b'TOTP', max_length=25),
|
field=models.CharField(default=b"TOTP", max_length=25),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ from django.db import models, migrations
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('mfa', '0002_user_keys_key_type'),
|
("mfa", "0002_user_keys_key_type"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='user_keys',
|
model_name="user_keys",
|
||||||
name='secret_key',
|
name="secret_key",
|
||||||
field=models.CharField(max_length=32),
|
field=models.CharField(max_length=32),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ from django.db import models, migrations
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('mfa', '0003_auto_20181114_2159'),
|
("mfa", "0003_auto_20181114_2159"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='user_keys',
|
model_name="user_keys",
|
||||||
name='enabled',
|
name="enabled",
|
||||||
field=models.BooleanField(default=True),
|
field=models.BooleanField(default=True),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -7,24 +7,25 @@ import jsonfield.fields
|
|||||||
|
|
||||||
def modify_json(apps, schema_editor):
|
def modify_json(apps, schema_editor):
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
if "mysql" in settings.DATABASES.get("default", {}).get("engine", ""):
|
if "mysql" in settings.DATABASES.get("default", {}).get("engine", ""):
|
||||||
migrations.RunSQL("alter table mfa_user_keys modify column properties json;")
|
migrations.RunSQL("alter table mfa_user_keys modify column properties json;")
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('mfa', '0004_user_keys_enabled'),
|
("mfa", "0004_user_keys_enabled"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name='user_keys',
|
model_name="user_keys",
|
||||||
name='secret_key',
|
name="secret_key",
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='user_keys',
|
model_name="user_keys",
|
||||||
name='properties',
|
name="properties",
|
||||||
field=jsonfield.fields.JSONField(null=True),
|
field=jsonfield.fields.JSONField(null=True),
|
||||||
),
|
),
|
||||||
migrations.RunPython(modify_json)
|
migrations.RunPython(modify_json),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -7,21 +7,29 @@ from django.db import models, migrations
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('mfa', '0005_auto_20181115_2014'),
|
("mfa", "0005_auto_20181115_2014"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Trusted_Devices',
|
name="Trusted_Devices",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
(
|
||||||
('signature', models.CharField(max_length=255)),
|
"id",
|
||||||
('key', models.CharField(max_length=6)),
|
models.AutoField(
|
||||||
('username', models.CharField(max_length=50)),
|
verbose_name="ID",
|
||||||
('user_agent', models.CharField(max_length=255)),
|
serialize=False,
|
||||||
('status', models.CharField(default=b'adding', max_length=255)),
|
auto_created=True,
|
||||||
('added_on', models.DateTimeField(auto_now_add=True)),
|
primary_key=True,
|
||||||
('last_used', models.DateTimeField(default=None, null=True)),
|
),
|
||||||
|
),
|
||||||
|
("signature", models.CharField(max_length=255)),
|
||||||
|
("key", models.CharField(max_length=6)),
|
||||||
|
("username", models.CharField(max_length=50)),
|
||||||
|
("user_agent", models.CharField(max_length=255)),
|
||||||
|
("status", models.CharField(default=b"adding", max_length=255)),
|
||||||
|
("added_on", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("last_used", models.DateTimeField(default=None, null=True)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -7,16 +7,16 @@ from django.db import models, migrations
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('mfa', '0006_trusted_devices'),
|
("mfa", "0006_trusted_devices"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.DeleteModel(
|
migrations.DeleteModel(
|
||||||
name='Trusted_Devices',
|
name="Trusted_Devices",
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='user_keys',
|
model_name="user_keys",
|
||||||
name='expires',
|
name="expires",
|
||||||
field=models.DateTimeField(default=None, null=True, blank=True),
|
field=models.DateTimeField(default=None, null=True, blank=True),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ from django.db import models, migrations
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('mfa', '0007_auto_20181230_1549'),
|
("mfa", "0007_auto_20181230_1549"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='user_keys',
|
model_name="user_keys",
|
||||||
name='last_used',
|
name="last_used",
|
||||||
field=models.DateTimeField(default=None, null=True, blank=True),
|
field=models.DateTimeField(default=None, null=True, blank=True),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,21 +6,23 @@ from django.conf import settings
|
|||||||
|
|
||||||
|
|
||||||
def update_owned_by_enterprise(apps, schema_editor):
|
def update_owned_by_enterprise(apps, schema_editor):
|
||||||
user_keys = apps.get_model('mfa', 'user_keys')
|
user_keys = apps.get_model("mfa", "user_keys")
|
||||||
user_keys.objects.filter(key_type='FIDO2').update(owned_by_enterprise=getattr(settings,"MFA_OWNED_BY_ENTERPRISE",False))
|
user_keys.objects.filter(key_type="FIDO2").update(
|
||||||
|
owned_by_enterprise=getattr(settings, "MFA_OWNED_BY_ENTERPRISE", False)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('mfa', '0008_user_keys_last_used'),
|
("mfa", "0008_user_keys_last_used"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='user_keys',
|
model_name="user_keys",
|
||||||
name='owned_by_enterprise',
|
name="owned_by_enterprise",
|
||||||
field=models.NullBooleanField(default=None),
|
field=models.NullBooleanField(default=None),
|
||||||
),
|
),
|
||||||
migrations.RunPython(update_owned_by_enterprise)
|
migrations.RunPython(update_owned_by_enterprise),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ from django.db import migrations, models
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('mfa', '0009_user_keys_owned_by_enterprise'),
|
("mfa", "0009_user_keys_owned_by_enterprise"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='user_keys',
|
model_name="user_keys",
|
||||||
name='key_type',
|
name="key_type",
|
||||||
field=models.CharField(default='TOTP', max_length=25),
|
field=models.CharField(default="TOTP", max_length=25),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ from django.db import migrations, models
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('mfa', '0010_auto_20201110_0557'),
|
("mfa", "0010_auto_20201110_0557"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='user_keys',
|
model_name="user_keys",
|
||||||
name='owned_by_enterprise',
|
name="owned_by_enterprise",
|
||||||
field=models.BooleanField(blank=True, default=None, null=True),
|
field=models.BooleanField(blank=True, default=None, null=True),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
17
mfa/migrations/0012_rename_user_keys_userkey.py
Normal file
17
mfa/migrations/0012_rename_user_keys_userkey.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 3.2.4 on 2021-06-23 07:10
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("mfa", "0011_auto_20210530_0622"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameModel(
|
||||||
|
old_name="User_Keys",
|
||||||
|
new_name="UserKey",
|
||||||
|
),
|
||||||
|
]
|
||||||
35
mfa/migrations/0013_auto_20210624_1505.py
Normal file
35
mfa/migrations/0013_auto_20210624_1505.py
Normal 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"),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,32 +1,45 @@
|
|||||||
from django.db import models
|
|
||||||
from jsonfield import JSONField
|
|
||||||
from jose import jwt
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
#from jsonLookup import shasLookup, hasLookup
|
from django.db import models
|
||||||
# JSONField.register_lookup(shasLookup)
|
from jose import jwt
|
||||||
# JSONField.register_lookup(hasLookup)
|
from jsonfield import JSONField
|
||||||
|
|
||||||
|
|
||||||
class User_Keys(models.Model):
|
class UserKey(models.Model):
|
||||||
username=models.CharField(max_length = 50)
|
username = models.CharField(max_length=50)
|
||||||
properties=JSONField(null = True)
|
properties = JSONField(null=True)
|
||||||
added_on=models.DateTimeField(auto_now_add = True)
|
added_on = models.DateTimeField(auto_now_add=True)
|
||||||
key_type=models.CharField(max_length = 25,default = "TOTP")
|
key_type = models.CharField(max_length=25, default="TOTP")
|
||||||
enabled=models.BooleanField(default=True)
|
enabled = models.BooleanField(default=True)
|
||||||
expires=models.DateTimeField(null=True,default=None,blank=True)
|
expires = models.DateTimeField(null=True, default=None, blank=True)
|
||||||
last_used=models.DateTimeField(null=True,default=None,blank=True)
|
last_used = models.DateTimeField(null=True, default=None, blank=True)
|
||||||
owned_by_enterprise=models.BooleanField(default=None,null=True,blank=True)
|
owned_by_enterprise = models.BooleanField(default=None, null=True, blank=True)
|
||||||
|
|
||||||
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
|
def save(self, *args, **kwargs):
|
||||||
if self.key_type == "Trusted Device" and self.properties.get("signature","") == "":
|
if (
|
||||||
self.properties["signature"]= jwt.encode({"username": self.username, "key": self.properties["key"]}, settings.SECRET_KEY)
|
self.key_type == "Trusted Device"
|
||||||
super(User_Keys, self).save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
|
and self.properties.get("signature", "") == ""
|
||||||
|
):
|
||||||
def __unicode__(self):
|
self.properties["signature"] = jwt.encode(
|
||||||
return "%s -- %s"%(self.username,self.key_type)
|
{"username": self.username, "key": self.properties["key"]},
|
||||||
|
settings.SECRET_KEY,
|
||||||
|
)
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.__unicode__()
|
return "%s -- %s" % (self.username, self.key_type)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
app_label='mfa'
|
app_label = "mfa"
|
||||||
|
|
||||||
|
|
||||||
|
class OTPTracker(models.Model):
|
||||||
|
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=["actor"])]
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% if invalid %}
|
{% if invalid %}
|
||||||
<div class="alert alert-danger">
|
<div class="alert alert-danger">
|
||||||
Sorry, The provided token is not valid.
|
{{ invalid_msg }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if quota %}
|
{% if quota %}
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
||||||
140
mfa/totp.py
140
mfa/totp.py
@@ -1,76 +1,120 @@
|
|||||||
from django.shortcuts import render
|
|
||||||
from django.views.decorators.cache import never_cache
|
|
||||||
from django.http import HttpResponse
|
|
||||||
from .models import *
|
|
||||||
from django.template.context_processors import csrf
|
|
||||||
import simplejson
|
|
||||||
from django.template.context import RequestContext
|
|
||||||
from django.conf import settings
|
|
||||||
import pyotp
|
|
||||||
from .views import login
|
|
||||||
import datetime
|
import datetime
|
||||||
from django.utils import timezone
|
|
||||||
import random
|
import random
|
||||||
def verify_login(request,username,token):
|
import time
|
||||||
for key in User_Keys.objects.filter(username=username,key_type = "TOTP"):
|
|
||||||
|
import pyotp
|
||||||
|
from django.conf import settings
|
||||||
|
from django.http import HttpResponse, JsonResponse
|
||||||
|
from django.shortcuts import render
|
||||||
|
from django.template.context_processors import csrf
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.views.decorators.cache import never_cache
|
||||||
|
|
||||||
|
from .Common import get_redirect_url
|
||||||
|
from .models import UserKey, OTPTracker
|
||||||
|
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"])
|
totp = pyotp.TOTP(key.properties["secret_key"])
|
||||||
if totp.verify(token,valid_window = 30):
|
if totp.verify(token, valid_window=30):
|
||||||
key.last_used=timezone.now()
|
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()
|
key.save()
|
||||||
return [True,key.id]
|
return [True, key.id]
|
||||||
return [False]
|
OTPTracker.objects.create(actor=username, value=token, success=False)
|
||||||
|
return [False, "Invalid Token"]
|
||||||
|
|
||||||
|
|
||||||
def recheck(request):
|
def recheck(request):
|
||||||
context = csrf(request)
|
context = csrf(request)
|
||||||
context["mode"]="recheck"
|
context["mode"] = "recheck"
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
if verify_login(request,request.user.username, token=request.POST["otp"]):
|
if verify_login(request, request.user.username, token=request.POST["otp"]):
|
||||||
import time
|
|
||||||
request.session["mfa"]["rechecked_at"] = time.time()
|
request.session["mfa"]["rechecked_at"] = time.time()
|
||||||
return HttpResponse(simplejson.dumps({"recheck": True}), content_type="application/json")
|
return JsonResponse({"recheck": True})
|
||||||
else:
|
else:
|
||||||
return HttpResponse(simplejson.dumps({"recheck": False}), content_type="application/json")
|
return JsonResponse({"recheck": False})
|
||||||
return render(request,"TOTP/recheck.html", context)
|
return render(request, "TOTP/recheck.html", context)
|
||||||
|
|
||||||
|
|
||||||
@never_cache
|
@never_cache
|
||||||
def auth(request):
|
def auth(request):
|
||||||
context=csrf(request)
|
context = csrf(request)
|
||||||
if request.method=="POST":
|
if request.method == "POST":
|
||||||
res=verify_login(request,request.session["base_username"],token = request.POST["otp"])
|
res = verify_login(
|
||||||
|
request, request.session["base_username"], token=request.POST["otp"]
|
||||||
|
)
|
||||||
if res[0]:
|
if res[0]:
|
||||||
mfa = {"verified": True, "method": "TOTP","id":res[1]}
|
mfa = {"verified": True, "method": "TOTP", "id": res[1]}
|
||||||
if getattr(settings, "MFA_RECHECK", False):
|
if getattr(settings, "MFA_RECHECK", False):
|
||||||
mfa["next_check"] = datetime.datetime.timestamp((datetime.datetime.now()
|
mfa["next_check"] = datetime.datetime.timestamp(
|
||||||
+ datetime.timedelta(
|
(
|
||||||
seconds=random.randint(settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX))))
|
datetime.datetime.now()
|
||||||
|
+ datetime.timedelta(
|
||||||
|
seconds=random.randint(
|
||||||
|
settings.MFA_RECHECK_MIN, settings.MFA_RECHECK_MAX
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
request.session["mfa"] = mfa
|
request.session["mfa"] = mfa
|
||||||
return login(request)
|
return login(request)
|
||||||
context["invalid"]=True
|
context["invalid"] = True
|
||||||
return render(request,"TOTP/Auth.html", context)
|
context["invalid_msg"] = res[1]
|
||||||
|
return render(request, "TOTP/Auth.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
def get_token(request):
|
||||||
def getToken(request):
|
secret_key = pyotp.random_base32()
|
||||||
secret_key=pyotp.random_base32()
|
|
||||||
totp = pyotp.TOTP(secret_key)
|
totp = pyotp.TOTP(secret_key)
|
||||||
request.session["new_mfa_answer"]=totp.now()
|
request.session["new_mfa_answer"] = totp.now()
|
||||||
return HttpResponse(simplejson.dumps({"qr":pyotp.totp.TOTP(secret_key).provisioning_uri(str(request.user.username), issuer_name = settings.TOKEN_ISSUER_NAME),
|
return JsonResponse(
|
||||||
"secret_key": secret_key}))
|
{
|
||||||
|
"qr": pyotp.totp.TOTP(secret_key).provisioning_uri(
|
||||||
|
str(request.user.username), issuer_name=settings.TOKEN_ISSUER_NAME
|
||||||
|
),
|
||||||
|
"secret_key": secret_key,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def verify(request):
|
def verify(request):
|
||||||
answer=request.GET["answer"]
|
answer = request.GET["answer"]
|
||||||
secret_key=request.GET["key"]
|
secret_key = request.GET["key"]
|
||||||
totp = pyotp.TOTP(secret_key)
|
totp = pyotp.TOTP(secret_key)
|
||||||
if totp.verify(answer,valid_window = 60):
|
if totp.verify(answer, valid_window=60):
|
||||||
uk=User_Keys()
|
uk = UserKey()
|
||||||
uk.username=request.user.username
|
uk.username = request.user.username
|
||||||
uk.properties={"secret_key":secret_key}
|
uk.properties = {"secret_key": secret_key}
|
||||||
#uk.name="Authenticatior #%s"%User_Keys.objects.filter(username=user.username,type="TOTP")
|
uk.key_type = "TOTP"
|
||||||
uk.key_type="TOTP"
|
|
||||||
uk.save()
|
uk.save()
|
||||||
return HttpResponse("Success")
|
return HttpResponse("Success")
|
||||||
else: return HttpResponse("Error")
|
else:
|
||||||
|
return HttpResponse("Error")
|
||||||
|
|
||||||
|
|
||||||
@never_cache
|
@never_cache
|
||||||
def start(request):
|
def start(request):
|
||||||
"""Start Adding Time One Time Password (TOTP)"""
|
"""Start Adding Time One Time Password (TOTP)"""
|
||||||
return render(request,"TOTP/Add.html",get_redirect_url())
|
return render(request, "TOTP/Add.html", get_redirect_url())
|
||||||
|
|||||||
87
mfa/urls.py
87
mfa/urls.py
@@ -1,50 +1,41 @@
|
|||||||
from . import views,totp,U2F,TrustedDevice,helpers,FIDO2,Email
|
from django.urls import path
|
||||||
#app_name='mfa'
|
|
||||||
|
from . import FIDO2, U2F, Email, TrustedDevice, helpers, totp, views
|
||||||
|
|
||||||
try:
|
|
||||||
from django.urls import re_path as url
|
|
||||||
except:
|
|
||||||
from django.conf.urls import url
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'totp/start/', totp.start , name="start_new_otop"),
|
path("totp/start/", totp.start, name="start_new_otop"),
|
||||||
url(r'totp/getToken', totp.getToken , name="get_new_otop"),
|
path("totp/getToken/", totp.get_token, name="get_new_otop"),
|
||||||
url(r'totp/verify', totp.verify, name="verify_otop"),
|
path("totp/verify/", totp.verify, name="verify_otop"),
|
||||||
url(r'totp/auth', totp.auth, name="totp_auth"),
|
path("totp/auth/", totp.auth, name="totp_auth"),
|
||||||
url(r'totp/recheck', totp.recheck, name="totp_recheck"),
|
path("totp/recheck/", totp.recheck, name="totp_recheck"),
|
||||||
|
path("email/start/", Email.start, name="start_email"),
|
||||||
url(r'email/start/', Email.start , name="start_email"),
|
path("email/auth/", Email.auth, name="email_auth"),
|
||||||
url(r'email/auth/', Email.auth , name="email_auth"),
|
path("u2f/", U2F.start, name="start_u2f"),
|
||||||
|
path("u2f/bind/", U2F.bind, name="bind_u2f"),
|
||||||
url(r'u2f/$', U2F.start, name="start_u2f"),
|
path("u2f/auth/", U2F.auth, name="u2f_auth"),
|
||||||
url(r'u2f/bind', U2F.bind, name="bind_u2f"),
|
path("u2f/process_recheck/", U2F.process_recheck, name="u2f_recheck"),
|
||||||
url(r'u2f/auth', U2F.auth, name="u2f_auth"),
|
path("u2f/verify/", U2F.verify, name="u2f_verify"),
|
||||||
url(r'u2f/process_recheck', U2F.process_recheck, name="u2f_recheck"),
|
path("fido2/", FIDO2.start, name="start_fido2"),
|
||||||
url(r'u2f/verify', U2F.verify, name="u2f_verify"),
|
path("fido2/auth/", FIDO2.auth, name="fido2_auth"),
|
||||||
|
path("fido2/begin_auth/", FIDO2.authenticate_begin, name="fido2_begin_auth"),
|
||||||
url(r'fido2/$', FIDO2.start, name="start_fido2"),
|
path(
|
||||||
url(r'fido2/auth', FIDO2.auth, name="fido2_auth"),
|
"fido2/complete_auth/", FIDO2.authenticate_complete, name="fido2_complete_auth"
|
||||||
url(r'fido2/begin_auth', FIDO2.authenticate_begin, name="fido2_begin_auth"),
|
),
|
||||||
url(r'fido2/complete_auth', FIDO2.authenticate_complete, name="fido2_complete_auth"),
|
path("fido2/begin_reg/", FIDO2.begin_registeration, name="fido2_begin_reg"),
|
||||||
url(r'fido2/begin_reg', FIDO2.begin_registeration, name="fido2_begin_reg"),
|
path("fido2/complete_reg/", FIDO2.complete_reg, name="fido2_complete_reg"),
|
||||||
url(r'fido2/complete_reg', FIDO2.complete_reg, name="fido2_complete_reg"),
|
path("fido2/recheck/", FIDO2.recheck, name="fido2_recheck"),
|
||||||
url(r'fido2/recheck', FIDO2.recheck, name="fido2_recheck"),
|
path("td/", TrustedDevice.start, name="start_td"),
|
||||||
|
path("td/add/", TrustedDevice.add, name="add_td"),
|
||||||
|
path("td/send_link/", TrustedDevice.send_email, name="td_sendemail"),
|
||||||
url(r'td/$', TrustedDevice.start, name="start_td"),
|
path("td/get-ua/", TrustedDevice.get_user_agent, name="td_get_useragent"),
|
||||||
url(r'td/add', TrustedDevice.add, name="add_td"),
|
path("td/trust/", TrustedDevice.trust_device, name="td_trust_device"),
|
||||||
url(r'td/send_link', TrustedDevice.send_email, name="td_sendemail"),
|
path("u2f/checkTrusted/", TrustedDevice.check_trusted, name="td_checkTrusted"),
|
||||||
url(r'td/get-ua', TrustedDevice.getUserAgent, name="td_get_useragent"),
|
path("u2f/secure_device", TrustedDevice.get_cookie, name="td_securedevice"),
|
||||||
url(r'td/trust', TrustedDevice.trust_device, name="td_trust_device"),
|
path("", views.index, name="mfa_home"),
|
||||||
url(r'u2f/checkTrusted', TrustedDevice.checkTrusted, name="td_checkTrusted"),
|
path("goto/<method>/", views.goto, name="mfa_goto"),
|
||||||
url(r'u2f/secure_device', TrustedDevice.getCookie, name="td_securedevice"),
|
path("selct_method/", views.show_methods, name="mfa_methods_list"),
|
||||||
|
path("recheck/", helpers.recheck, name="mfa_recheck"),
|
||||||
url(r'^$', views.index, name="mfa_home"),
|
path("toggleKey/", views.toggle_key, name="toggle_key"),
|
||||||
url(r'goto/(.*)', views.goto, name="mfa_goto"),
|
path("delete/", views.del_key, name="mfa_delKey"),
|
||||||
url(r'selct_method', views.show_methods, name="mfa_methods_list"),
|
path("reset/", views.reset_cookie, name="mfa_reset_cookie"),
|
||||||
url(r'recheck', helpers.recheck, name="mfa_recheck"),
|
]
|
||||||
url(r'toggleKey', views.toggleKey, name="toggle_key"),
|
|
||||||
url(r'delete', views.delKey, name="mfa_delKey"),
|
|
||||||
url(r'reset', views.reset_cookie, name="mfa_reset_cookie"),
|
|
||||||
|
|
||||||
]
|
|
||||||
# print(urlpatterns)
|
|
||||||
|
|||||||
98
mfa/views.py
98
mfa/views.py
@@ -1,90 +1,97 @@
|
|||||||
from django.shortcuts import render
|
import importlib
|
||||||
from django.http import HttpResponse,HttpResponseRedirect
|
|
||||||
from .models import *
|
|
||||||
try:
|
|
||||||
from django.urls import reverse
|
|
||||||
except:
|
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
from django.template.context_processors import csrf
|
|
||||||
from django.template.context import RequestContext
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from . import TrustedDevice
|
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.http import HttpResponse, HttpResponseRedirect
|
||||||
|
from django.shortcuts import render
|
||||||
|
from django.urls import reverse
|
||||||
from user_agents import parse
|
from user_agents import parse
|
||||||
|
|
||||||
|
from . import TrustedDevice
|
||||||
|
from .models import UserKey
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def index(request):
|
def index(request):
|
||||||
keys=[]
|
keys = []
|
||||||
context={"keys":User_Keys.objects.filter(username=request.user.username),"UNALLOWED_AUTHEN_METHODS":settings.MFA_UNALLOWED_METHODS
|
context = {
|
||||||
,"HIDE_DISABLE":getattr(settings,"MFA_HIDE_DISABLE",[])}
|
"keys": UserKey.objects.filter(username=request.user.username),
|
||||||
|
"UNALLOWED_AUTHEN_METHODS": settings.MFA_UNALLOWED_METHODS,
|
||||||
|
"HIDE_DISABLE": getattr(settings, "MFA_HIDE_DISABLE", []),
|
||||||
|
}
|
||||||
for k in context["keys"]:
|
for k in context["keys"]:
|
||||||
if k.key_type =="Trusted Device" :
|
if k.key_type == "Trusted Device":
|
||||||
setattr(k,"device",parse(k.properties.get("user_agent","-----")))
|
setattr(k, "device", parse(k.properties.get("user_agent", "-----")))
|
||||||
elif k.key_type == "FIDO2":
|
elif k.key_type == "FIDO2":
|
||||||
setattr(k,"device",k.properties.get("type","----"))
|
setattr(k, "device", k.properties.get("type", "----"))
|
||||||
keys.append(k)
|
keys.append(k)
|
||||||
context["keys"]=keys
|
context["keys"] = keys
|
||||||
return render(request,"MFA.html",context)
|
return render(request, "MFA.html", context)
|
||||||
|
|
||||||
def verify(request,username):
|
|
||||||
|
def verify(request, username):
|
||||||
request.session["base_username"] = username
|
request.session["base_username"] = username
|
||||||
#request.session["base_password"] = password
|
keys = UserKey.objects.filter(username=username, enabled=1)
|
||||||
keys=User_Keys.objects.filter(username=username,enabled=1)
|
methods = list(set([k.key_type for k in keys]))
|
||||||
methods=list(set([k.key_type for k in keys]))
|
|
||||||
|
|
||||||
if "Trusted Device" in methods and not request.session.get("checked_trusted_device",False):
|
if "Trusted Device" in methods and not request.session.get(
|
||||||
|
"checked_trusted_device", False
|
||||||
|
):
|
||||||
if TrustedDevice.verify(request):
|
if TrustedDevice.verify(request):
|
||||||
return login(request)
|
return login(request)
|
||||||
methods.remove("Trusted Device")
|
methods.remove("Trusted Device")
|
||||||
request.session["mfa_methods"] = methods
|
request.session["mfa_methods"] = methods
|
||||||
if len(methods)==1:
|
if len(methods) == 1:
|
||||||
return HttpResponseRedirect(reverse(methods[0].lower()+"_auth"))
|
return HttpResponseRedirect(reverse(methods[0].lower() + "_auth"))
|
||||||
return show_methods(request)
|
return show_methods(request)
|
||||||
|
|
||||||
|
|
||||||
def show_methods(request):
|
def show_methods(request):
|
||||||
return render(request,"select_mfa_method.html", {})
|
return render(request, "select_mfa_method.html", {})
|
||||||
|
|
||||||
|
|
||||||
def reset_cookie(request):
|
def reset_cookie(request):
|
||||||
response=HttpResponseRedirect(settings.LOGIN_URL)
|
response = HttpResponseRedirect(settings.LOGIN_URL)
|
||||||
response.delete_cookie("base_username")
|
response.delete_cookie("base_username")
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
def login(request):
|
def login(request):
|
||||||
from django.contrib import auth
|
|
||||||
from django.conf import settings
|
|
||||||
callable_func = __get_callable_function__(settings.MFA_LOGIN_CALLBACK)
|
callable_func = __get_callable_function__(settings.MFA_LOGIN_CALLBACK)
|
||||||
return callable_func(request,username=request.session["base_username"])
|
return callable_func(request, username=request.session["base_username"])
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def delKey(request):
|
def del_key(request):
|
||||||
key=User_Keys.objects.get(id=request.GET["id"])
|
key = UserKey.objects.get(id=request.GET["id"])
|
||||||
if key.username == request.user.username:
|
if key.username == request.user.username:
|
||||||
key.delete()
|
key.delete()
|
||||||
return HttpResponse("Deleted Successfully")
|
return HttpResponse("Deleted Successfully")
|
||||||
else:
|
else:
|
||||||
return HttpResponse("Error: You own this token so you can't delete it")
|
return HttpResponse("Error: You own this token so you can't delete it")
|
||||||
|
|
||||||
|
|
||||||
def __get_callable_function__(func_path):
|
def __get_callable_function__(func_path):
|
||||||
import importlib
|
if "." not in func_path:
|
||||||
if not '.' in func_path:
|
|
||||||
raise Exception("class Name should include modulename.classname")
|
raise Exception("class Name should include modulename.classname")
|
||||||
|
|
||||||
parsed_str = func_path.split(".")
|
parsed_str = func_path.split(".")
|
||||||
module_name , func_name = ".".join(parsed_str[:-1]) , parsed_str[-1]
|
module_name, func_name = ".".join(parsed_str[:-1]), parsed_str[-1]
|
||||||
imported_module = importlib.import_module(module_name)
|
imported_module = importlib.import_module(module_name)
|
||||||
callable_func = getattr(imported_module,func_name)
|
callable_func = getattr(imported_module, func_name)
|
||||||
if not callable_func:
|
if not callable_func:
|
||||||
raise Exception("Module does not have requested function")
|
raise Exception("Module does not have requested function")
|
||||||
return callable_func
|
return callable_func
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def toggleKey(request):
|
def toggle_key(request):
|
||||||
id=request.GET["id"]
|
id = request.GET["id"]
|
||||||
q=User_Keys.objects.filter(username=request.user.username, id=id)
|
q = UserKey.objects.filter(username=request.user.username, id=id)
|
||||||
if q.count()==1:
|
if q.count() == 1:
|
||||||
key=q[0]
|
key = q[0]
|
||||||
if not key.key_type in settings.MFA_HIDE_DISABLE:
|
if key.key_type not in settings.MFA_HIDE_DISABLE:
|
||||||
key.enabled=not key.enabled
|
key.enabled = not key.enabled
|
||||||
key.save()
|
key.save()
|
||||||
return HttpResponse("OK")
|
return HttpResponse("OK")
|
||||||
else:
|
else:
|
||||||
@@ -92,5 +99,6 @@ def toggleKey(request):
|
|||||||
else:
|
else:
|
||||||
return HttpResponse("Error")
|
return HttpResponse("Error")
|
||||||
|
|
||||||
def goto(request,method):
|
|
||||||
return HttpResponseRedirect(reverse(method.lower()+"_auth"))
|
def goto(request, method):
|
||||||
|
return HttpResponseRedirect(reverse(method.lower() + "_auth"))
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ site_name: MkLorum
|
|||||||
nav:
|
nav:
|
||||||
- Home: index.md
|
- Home: index.md
|
||||||
- Installation: installation.md
|
- Installation: installation.md
|
||||||
- Code Changes: change_login.md
|
- Code Changes: change_login.md
|
||||||
theme: readthedocs
|
theme: readthedocs
|
||||||
markdown_extensions:
|
markdown_extensions:
|
||||||
- fenced_code
|
- fenced_code
|
||||||
|
|||||||
43
setup.py
43
setup.py
@@ -3,33 +3,32 @@
|
|||||||
from setuptools import find_packages, setup
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='django-mfa2',
|
name="django-mfa2",
|
||||||
version='2.2.0',
|
version="2.2.0",
|
||||||
description='Allows user to add 2FA to their accounts',
|
description="Allows user to add 2FA to their accounts",
|
||||||
long_description=open("README.md").read(),
|
long_description=open("README.md").read(),
|
||||||
long_description_content_type="text/markdown",
|
long_description_content_type="text/markdown",
|
||||||
|
author="Mohamed El-Kalioby",
|
||||||
author='Mohamed El-Kalioby',
|
author_email="mkalioby@mkalioby.com",
|
||||||
author_email = 'mkalioby@mkalioby.com',
|
url="https://github.com/mkalioby/django-mfa2/",
|
||||||
url = 'https://github.com/mkalioby/django-mfa2/',
|
download_url="https://github.com/mkalioby/django-mfa2/",
|
||||||
download_url='https://github.com/mkalioby/django-mfa2/',
|
license="MIT",
|
||||||
license='MIT',
|
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'django >= 2.0',
|
"django >= 2.0",
|
||||||
'jsonfield',
|
"jsonfield",
|
||||||
'simplejson',
|
"simplejson",
|
||||||
'pyotp',
|
"pyotp",
|
||||||
'python-u2flib-server',
|
"python-u2flib-server",
|
||||||
'ua-parser',
|
"ua-parser",
|
||||||
'user-agents',
|
"user-agents",
|
||||||
'python-jose',
|
"python-jose",
|
||||||
'fido2 == 0.9.1',
|
"fido2 == 0.9.1",
|
||||||
'jsonLookup'
|
"jsonLookup",
|
||||||
],
|
],
|
||||||
python_requires=">=3.5",
|
python_requires=">=3.5",
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
zip_safe=False, # because we're including static files
|
zip_safe=False, # because we're including static files
|
||||||
classifiers=[
|
classifiers=[
|
||||||
"Development Status :: 5 - Production/Stable",
|
"Development Status :: 5 - Production/Stable",
|
||||||
"Environment :: Web Environment",
|
"Environment :: Web Environment",
|
||||||
@@ -48,5 +47,5 @@ setup(
|
|||||||
"Programming Language :: Python :: 3.7",
|
"Programming Language :: Python :: 3.7",
|
||||||
"Programming Language :: Python :: 3.8",
|
"Programming Language :: Python :: 3.8",
|
||||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||||
]
|
],
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user