Skip to content
This repository has been archived by the owner on May 5, 2020. It is now read-only.

Commit

Permalink
feat: Add NOPASSWORD_LOGIN_ON_GET setting
Browse files Browse the repository at this point in the history
  • Loading branch information
rubengrill authored and relekang committed Sep 14, 2018
1 parent a8d1522 commit 627267e
Show file tree
Hide file tree
Showing 8 changed files with 36 additions and 8 deletions.
11 changes: 7 additions & 4 deletions nopassword/backends/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,13 @@ def authenticate(self, request, username=None, code=None, **kwargs):

timeout = getattr(settings, 'NOPASSWORD_LOGIN_CODE_TIMEOUT', 900)
timestamp = datetime.now() - timedelta(seconds=timeout)
login_code = LoginCode.objects.get(user=user, code=code, timestamp__gt=timestamp)
user = login_code.user
user.code = login_code
login_code.delete()

# We don't delete the login code when authenticating,
# as that is done during validation of the login form
# and validation should not have any side effects.
# It is the responsibility of the view/form to delete the token
# as soon as the login was successfull.
user.login_code = LoginCode.objects.get(user=user, code=code, timestamp__gt=timestamp)

return user

Expand Down
3 changes: 3 additions & 0 deletions nopassword/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,6 @@ def clean_code(self):

def get_user(self):
return self.cleaned_data.get('user')

def save(self):
self.cleaned_data['code'].delete()
3 changes: 3 additions & 0 deletions nopassword/rest/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ def validate(self, data):

return self.form.cleaned_data

def save(self):
self.form.save()


class TokenSerializer(serializers.ModelSerializer):

Expand Down
1 change: 1 addition & 0 deletions nopassword/rest/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ def get_response(self):
def post(self, request, *args, **kwargs):
self.serializer = self.get_serializer(data=request.data)
self.serializer.is_valid(raise_exception=True)
self.serializer.save()
self.login()
return self.get_response()

Expand Down
7 changes: 6 additions & 1 deletion nopassword/views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
from django.conf import settings
from django.contrib.auth.views import LoginView as DjangoLoginView
from django.contrib.auth.views import LogoutView as DjangoLogoutView
from django.urls import reverse_lazy
Expand Down Expand Up @@ -27,10 +28,14 @@ class LoginView(DjangoLoginView):
form_class = forms.LoginForm

def get(self, request, *args, **kwargs):
if request.method == 'GET' and 'code' in self.request.GET:
if 'code' in self.request.GET and getattr(settings, 'NOPASSWORD_LOGIN_ON_GET', False):
return super(LoginView, self).post(request, *args, **kwargs)
return super(LoginView, self).get(request, *args, **kwargs)

def form_valid(self, form):
form.save()
return super(LoginView, self).form_valid(form)

def get_form_kwargs(self):
kwargs = super(LoginView, self).get_form_kwargs()

Expand Down
1 change: 0 additions & 1 deletion tests/test_backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ def setUp(self):
self.code = LoginCode.create_code_for_user(self.user, next='/secrets/')
self.assertEqual(len(self.code.code), 20)
self.assertIsNotNone(authenticate(username=self.user.username, code=self.code.code))
self.assertEqual(LoginCode.objects.filter(user=self.user, code=self.code.code).count(), 0)

@patch('nopassword.backends.sms.TwilioRestClient')
def test_twilio_backend(self, mock_object):
Expand Down
1 change: 0 additions & 1 deletion tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ def setUp(self):
def test_login_backend(self):
self.assertEqual(len(self.code.code), 20)
self.assertIsNotNone(authenticate(username=self.user.username, code=self.code.code))
self.assertEqual(LoginCode.objects.filter(user=self.user, code=self.code.code).count(), 0)
self.assertIsNone(LoginCode.create_code_for_user(self.inactive_user))

@override_settings(NOPASSWORD_CODE_LENGTH=8)
Expand Down
17 changes: 16 additions & 1 deletion tests/test_views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# -*- coding: utf8 -*-
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.test import TestCase, override_settings

from nopassword.models import LoginCode

Expand Down Expand Up @@ -63,6 +63,7 @@ def test_login_post(self):
self.assertEqual(response.status_code, 302)
self.assertEqual(response['Location'], '/accounts/profile/')
self.assertEqual(response.wsgi_request.user, self.user)
self.assertFalse(LoginCode.objects.filter(pk=login_code.pk).exists())

def test_login_get(self):
login_code = LoginCode.objects.create(user=self.user, code='foobar')
Expand All @@ -71,9 +72,23 @@ def test_login_get(self):
'code': login_code.code,
})

self.assertEqual(response.status_code, 200)
self.assertEqual(response.context['form'].cleaned_data['code'], login_code)
self.assertTrue(response.wsgi_request.user.is_anonymous)
self.assertTrue(LoginCode.objects.filter(pk=login_code.pk).exists())

@override_settings(NOPASSWORD_LOGIN_ON_GET=True)
def test_login_get_non_idempotent(self):
login_code = LoginCode.objects.create(user=self.user, code='foobar')

response = self.client.get('/accounts/login/', {
'code': login_code.code,
})

self.assertEqual(response.status_code, 302)
self.assertEqual(response['Location'], '/accounts/profile/')
self.assertEqual(response.wsgi_request.user, self.user)
self.assertFalse(LoginCode.objects.filter(pk=login_code.pk).exists())

def test_login_missing_code_post(self):
response = self.client.post('/accounts/login/')
Expand Down

0 comments on commit 627267e

Please sign in to comment.