Skip to content

Commit

Permalink
django admin two factor auth is ready
Browse files Browse the repository at this point in the history
  • Loading branch information
imankarimi committed Oct 12, 2021
1 parent 29663e3 commit ad55072
Show file tree
Hide file tree
Showing 46 changed files with 1,313 additions and 3 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,4 @@ dmypy.json

# Pyre type checker
.pyre/
.idea/
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.
5 changes: 5 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
include LICENSE
include README.rst
recursive-include admin_two_factor/static *
recursive-include admin_two_factor/templates *
recursive-include docs *
90 changes: 88 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,88 @@
# django-admin-two-factor-auth
Two factor authentication for Django admin
# Django Admin Two Factor Authentication

**Django Admin Two-Factor Authentication**, allows you to login django admin with google authenticator.

<br>

## Why Django Log Reader?

- Using google authenticator to login your Django admin.
- Used jquery confirm dialog to get code.
- Simple interface
- Easy integration

<br />

[comment]: <> (![Django Log Reader]&#40;https://raw.githubusercontent.com/imankarimi/django-log-reader/main/screenshots/django_log_reader.png&#41;)

[comment]: <> (<br />)

## How to use it

* Download and install last version of **Django Admin Two-Factor Authentication**:

```bash
$ pip install git+https://github.com/imankarimi/django-admin-two-factor-auth.git
$ # or
$ easy_install git+https://github.com/imankarimi/django-admin-two-factor-auth.git
```

* Add 'admin_two_factor' application to the INSTALLED_APPS setting of your Django project `settings.py` file (note it should be before 'django.contrib.admin'):

```python
INSTALLED_APPS = (
'admin_two_factor.apps.TwoStepVerificationConfig',
'django.contrib.admin',
# ...
)
```

* Migrate `admin_two_factor`:

```bash
$ python manage.py migrate admin_two_factor
$ # or
$ python manage.py syncdb
```

* Add `‍‍‍‍ADMIN_TWO_FACTOR_NAME` in your `settings.py`. This value will be displayed in [Google Authenticator](https://support.google.com/accounts/answer/1066447?hl=en).

```python
ADMIN_TWO_FACTOR_NAME = 'PROJECT_NAME'
```

* Include the **Admin Two Factor** URL config in `PROJECT_CORE/urls.py`:

```python
urlpatterns = [
path('admin/', admin.site.urls),
path('two_factor/', include(('admin_two_factor.urls', 'admin_two_factor'), namespace='two_factor')),
# ...
]
```

* Collect static if you are in production environment:

```bash
$ python manage.py collectstatic
```

* Clear your browser cache

<br />

## Start the app

```bash
$ # Set up the database
$ python manage.py makemigrations
$ python manage.py migrate
$
$ # Create the superuser
$ python manage.py createsuperuser
$
$ # Start the application (development mode)
$ python manage.py runserver # default port 8000
```

* Access the `admin` section in the browser: `http://127.0.0.1:8000/`
Empty file added admin_two_factor/__init__.py
Empty file.
44 changes: 44 additions & 0 deletions admin_two_factor/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from django.contrib import admin, messages
from django.shortcuts import redirect
from django.template.loader import render_to_string
from django.utils.translation import gettext as _
from admin_two_factor.models import TwoFactorVerification
from admin_two_factor.utils import set_expire


@admin.register(TwoFactorVerification)
class TwoStepVerificationAdmin(admin.ModelAdmin):
list_display = ['user', 'is_active', 'created_time']
raw_id_fields = ['user']
list_filter = ['is_active', 'created_time']
fieldsets = (("", {'fields': ('user', 'code', 'is_active', 'qrcode'), }),)
readonly_fields = ['qrcode']

def qrcode(self, obj):
secret_key, qrcode = obj.get_qrcode
if qrcode:
return render_to_string('two_step_verification/admin/qrcode.html', {'qrcode': qrcode})

qrcode.short_description = _('Two Step QR Code')

def get_fieldsets(self, request, obj=None):
fieldsets = self.fieldsets
if not obj:
fieldsets = (("", {'fields': ('user',), }),)
elif obj and obj.secret:
fieldsets = (("", {'fields': ('user', 'code', 'is_active'), }),)
return fieldsets

def response_add(self, request, obj, post_url_continue=None):
self.message_user(request, _('user added successfully'), level=messages.SUCCESS)
return redirect('admin:admin_two_factor_twofactorverification_change', obj.id)

def response_change(self, request, obj):
request.session['two_step_%s' % request.user.id] = {'expire': set_expire().get('time')}
return super(TwoStepVerificationAdmin, self).response_change(request, obj)

def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
extra_context = extra_context or {}
if not self.get_object(request, object_id):
extra_context['show_save_and_add_another'] = False
return super(TwoStepVerificationAdmin, self).changeform_view(request, object_id, form_url, extra_context)
6 changes: 6 additions & 0 deletions admin_two_factor/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class TwoStepVerificationConfig(AppConfig):
name = 'admin_two_factor'
verbose_name = 'two factor'
Empty file.
16 changes: 16 additions & 0 deletions admin_two_factor/context_processors/two_factor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from django.contrib.auth import logout

from admin_two_factor.utils import is_expired


def verification(request):
user = request.user
can_redirect = False
if user.is_authenticated and user.is_staff and hasattr(user, 'two_step') and user.two_step.is_active:
key = 'two_step_%s' % user.id
user_session = request.session[key] if key in request.session else None
if not user_session or is_expired(user_session['expire']):
if user.id:
logout(request)
can_redirect = True
return {'can_redirect': can_redirect}
33 changes: 33 additions & 0 deletions admin_two_factor/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Generated by Django 3.1.3 on 2020-11-09 14:35

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

initial = True

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name='TwoFactorVerification',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('secret', models.CharField(blank=True, editable=False, max_length=20, null=True, unique=True, verbose_name='secret key')),
('code', models.CharField(blank=True, help_text='You must enter the code here to active/deactivate two step verification.', max_length=8, null=True, verbose_name='code')),
('is_active', models.BooleanField(default=False, verbose_name='is active?')),
('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')),
('updated_time', models.DateTimeField(auto_now=True, verbose_name='updated time')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.DO_NOTHING, related_name='two_step', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'two factor verification',
'verbose_name_plural': 'two factor verifications',
},
),
]
Empty file.
77 changes: 77 additions & 0 deletions admin_two_factor/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import base64
import os
from io import BytesIO

import pyotp
import qrcode
from admin_two_factor import settings
from django.contrib.auth.models import User
from django.core.cache import cache
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import gettext as _


class TwoFactorVerification(models.Model):
user = models.OneToOneField(User, on_delete=models.DO_NOTHING, related_name='two_step')
secret = models.CharField(_('secret key'), max_length=20, null=True, blank=True, unique=True, editable=False)
code = models.CharField(_('code'), max_length=8, null=True, blank=True,
help_text=_('You must enter the code here to active/deactivate two step verification.'))
is_active = models.BooleanField(_('is active?'), default=False)

created_time = models.DateTimeField(_('created time'), auto_now_add=True)
updated_time = models.DateTimeField(_('updated time'), auto_now=True)

class Meta:
verbose_name = _('two factor verification')
verbose_name_plural = _('two factor verifications')

def clean(self):
if self.is_active and not self.secret and not self.code:
raise ValidationError("The code field is required.")
if not self.is_active and self.secret and not self.code:
raise ValidationError("The code field is required.")

if self.code:
if self.is_active and not self.secret:
secret = cache.get('user_secret_key_%s' % self.user.id)

is_verify = self.is_verify(code=self.code, secret=secret)
if not is_verify:
raise ValidationError("The code is wrong. please try again.")

self.secret = secret

elif not self.is_active and self.secret:
is_verify = self.is_verify(code=self.code)
if not is_verify:
raise ValidationError("The code is wrong. please try again.")
self.secret = None

self.code = None

@property
def get_qrcode(self):
if self.secret or not self.user:
return self.secret, None

secret_key = base64.b32encode(os.urandom(10)).decode()
username = self.user.get_full_name() if self.user.get_full_name() else self.user.username
query = pyotp.totp.TOTP(secret_key).provisioning_uri(username,
issuer_name=settings.ADMIN_TWO_FACTOR_NAME)
qr_img = qrcode.make(query)
buffered = BytesIO()
qr_img.save(buffered, format="JPEG")
link = base64.b64encode(buffered.getvalue()).decode('UTF-8')

cache.set('user_secret_key_%s' % self.user_id, secret_key, 300)

return secret_key, link

def is_verify(self, code, secret=None):
secret = secret if secret else self.secret
if secret:
totp = pyotp.TOTP(secret)
if code == totp.now():
return True
return False
12 changes: 12 additions & 0 deletions admin_two_factor/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from django.conf import settings

# Two factor name
ADMIN_TWO_FACTOR_NAME = getattr(settings, 'ADMIN_TWO_FACTOR_NAME', None)

# two factor session expire time (second)
SESSION_COOKIE_AGE = getattr(settings, 'SESSION_COOKIE_AGE', 300)

# Two factor context processors
settings.TEMPLATES[0]['OPTIONS']['context_processors'].append(
'admin_two_factor.context_processors.two_factor.verification'
)

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
build
.sizecache.json
*.log
17 changes: 17 additions & 0 deletions admin_two_factor/static/two_step_assets/jquery-cookie/.jshintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"boss": true,
"browser": true,
"curly": true,
"eqeqeq": true,
"eqnull": true,
"expr": true,
"evil": true,
"newcap": true,
"noarg": true,
"undef": true,
"globals": {
"define": true,
"jQuery": true,
"require": true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
softTabs = false
tabSize = 2

[ text.plain ]
softWrap = true
wrapColumn = "Use Window Frame"
softTabs = true
tabSize = 4

[ "*.md" ]
fileType = "text.plain"
10 changes: 10 additions & 0 deletions admin_two_factor/static/two_step_assets/jquery-cookie/.travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
language: node_js
node_js:
- 0.8
before_script:
- npm install -g grunt-cli
script: grunt ci --verbose
env:
global:
- secure: HRae0kyIDDuhonvMi2SfEl1WJb4K/wX8WmzT9YkxFbmWwLjiOMkmqyuEyi76DbTC1cb9o7WwGVgbP1DhSm6n6m0Lz+PSzpprBN4QZuJc56jcc+tBA6gM81hyUufaTT0yUWz112Bu06kWIAs44w5PtG0FYZR0CuIN8fQvZi8fXCQ=
- secure: c+M5ECIfxDcVrr+ZlqgpGjv8kVM/hxiz3ACMCn4ZkDiaeq4Rw0wWIGZYL6aV5fhsoHgzEQ/XQPca8xKs3Umr7R3b6Vr3AEyFnW+LP67K/1Qbz4Pi3PvhDH/h4rvK7fOoTqTDCVVDEH3v4pefsz2VaKemG4iBKxrcof5aR4Rjopk=
Loading

0 comments on commit ad55072

Please sign in to comment.