From b849fcd0fa2d254eed2dc388b73ade8e80cd6203 Mon Sep 17 00:00:00 2001 From: gotlium Date: Thu, 5 Jun 2014 02:46:35 +0500 Subject: [PATCH] fixes and improvements * security fixes * logging confirmation method * protection for login_confirmation view * totp name settings * pep8 * translation * registration phone require fix --- README.rst | 10 +- demo/demo/settings.py | 4 +- demo/demo/templates/base.html | 2 +- .../demo/templates/registration/activate.html | 19 +-- secureauth/auth_forms.py | 13 +- secureauth/defaults.py | 11 +- secureauth/forms.py | 28 ++-- secureauth/locale/ru/LC_MESSAGES/django.mo | Bin 11553 -> 12534 bytes secureauth/locale/ru/LC_MESSAGES/django.po | 44 +++++- ...d_field_userauthactivity_confirm_method.py | 133 ++++++++++++++++++ secureauth/models.py | 27 ++-- secureauth/registration/views.py | 5 +- secureauth/tables.py | 2 +- .../secureauth/codes_settings/was_done.html | 2 +- secureauth/views.py | 15 +- setup.py | 4 +- 16 files changed, 253 insertions(+), 66 deletions(-) create mode 100644 secureauth/migrations/0006_auto__add_field_userauthactivity_confirm_method.py diff --git a/README.rst b/README.rst index b280ef9..0d557e9 100644 --- a/README.rst +++ b/README.rst @@ -25,14 +25,14 @@ Open settings: $ vim demo/settings.py -Replace Twilio credentials in ``AUTH_SMS_BACKEND_AUTH`` to your demo account settings. +Replace Twilio credentials in ``AUTH_SMS_BACKEND_AUTH`` to your demo account settings or change SMS_FORCE to False. -Run RabbitMQ server and Celery daemon(or remove 'djcelery' from ``INSTALLED_APPS``): +Run Redis server and Celery daemon(or remove 'djcelery' from ``INSTALLED_APPS``): .. code-block:: bash - $ rabbitmq-server -detached - $ python manage.py celeryd --loglevel=info >& /dev/null & + $ redis-server >& /dev/null & + $ python manage.py celeryd --loglevel=info >& /tmp/celery.log & Run test server: @@ -45,7 +45,7 @@ Run test server: Now you can open http://127.0.0.1:8000/accounts/register/ and register new account and setup all available authentication methods. -*Note: activation link will be output on console.* +*Note: activation link will be output to console.* Screenshots diff --git a/demo/demo/settings.py b/demo/demo/settings.py index b53cf66..b06cdc5 100644 --- a/demo/demo/settings.py +++ b/demo/demo/settings.py @@ -83,7 +83,7 @@ 'django_extensions', #'debug_toolbar', 'bootstrap', - # 'djcelery', + 'djcelery', 'django_tables2', 'registration', 'south', @@ -120,7 +120,7 @@ EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' # IMPORTANT: django-secure-auth required configs -AUTH_SMS_FORCE = False +AUTH_SMS_FORCE = True AUTH_SMS_BACKEND = 'Twilio' # You can register new account on https://www.twilio.com/ and enter diff --git a/demo/demo/templates/base.html b/demo/demo/templates/base.html index 9a0bad0..7fed9a6 100644 --- a/demo/demo/templates/base.html +++ b/demo/demo/templates/base.html @@ -95,7 +95,7 @@

{% block title %}{% endblock %}


diff --git a/demo/demo/templates/registration/activate.html b/demo/demo/templates/registration/activate.html index 2c14c02..8cb5081 100644 --- a/demo/demo/templates/registration/activate.html +++ b/demo/demo/templates/registration/activate.html @@ -4,17 +4,10 @@ {% block title %}{% trans 'Registration' %}{% endblock %} {% block content %} - -{% if account %} - -

{% trans "Account successfully activated" %}

- -

{% trans "Log in" %}

- -{% else %} - -

{% trans "Account activation failed" %}

- -{% endif %} - + {% if account %} +

{% trans "Account successfully activated" %}

+

{% trans "Log in" %}

+ {% else %} +

{% trans "Account activation failed" %}

+ {% endif %} {% endblock %} diff --git a/secureauth/auth_forms.py b/secureauth/auth_forms.py index c75467b..d5957bc 100644 --- a/secureauth/auth_forms.py +++ b/secureauth/auth_forms.py @@ -7,15 +7,7 @@ from defaults import CAPTCHA_ATTEMPT, CAPTCHA_ENABLED from captcha.fields import CaptchaField - - -AUTH_TYPES = ( - ('', '---'), - ('code', _('by code')), - ('token', _('by token')), - ('phone', _('by sms')), - ('question', _('by question')), -) +from models import AUTH_TYPES def get_available_auth_methods(user): @@ -55,12 +47,13 @@ def clean(self): if auth_type and code: from secureauth.backend import AuthBackend + backend = AuthBackend() self.user_cache = backend.auth(self.credentials, auth_type, code) if self.user_cache is None: raise forms.ValidationError(_("Please enter correct code")) elif not self.user_cache.is_active: - raise forms.ValidationError(_("This account is inactive.")) + raise forms.ValidationError(_("This account is inactive")) return self.cleaned_data def get_user(self): diff --git a/secureauth/defaults.py b/secureauth/defaults.py index 184695e..deff562 100644 --- a/secureauth/defaults.py +++ b/secureauth/defaults.py @@ -1,35 +1,38 @@ # -*- coding: utf-8 -*- +from django.utils.translation import ugettext as _ from django.conf import settings def get_settings(key, default): return getattr(settings, key, default) + SMS_FORCE = get_settings('AUTH_SMS_FORCE', False) SMS_BACKEND = get_settings('AUTH_SMS_BACKEND', 'Twilio') SMS_BACKEND_AUTH = get_settings('AUTH_SMS_BACKEND_AUTH', [ 'ACc73704107c6a5426b2157e279c485d32', 'a2949613dc22aa3df58ea813a6e0747f' ]) SMS_FROM = get_settings('AUTH_SMS_FROM', '+12242315966') -SMS_MESSAGE = get_settings('AUTH_SMS_MESSAGE', 'Your code is: %s') +SMS_MESSAGE = get_settings('AUTH_SMS_MESSAGE', _('Your code is: %s')) SMS_CODE_LEN = get_settings('AUTH_SMS_CODE_LEN', 4) SMS_AGE = get_settings('AUTH_SMS_AGE', 60) SMS_ASCII = get_settings('AUTH_SMS_ASCII', False) CODE_RANGES = get_settings('AUTH_CODE_RANGES', 20) CODE_LEN = get_settings('AUTH_CODE_LEN', 6) +TOTP_NAME = get_settings('AUTH_TOTP_NAME', "%(username)s@%(domain)s") # Available: code, token, phone, question DEFAULT_AUTH_TYPE = get_settings('AUTH_DEFAULT_TYPE', 'phone') # Notification when user is authenticated on site SMS_NOTIFICATION_SUBJECT = get_settings( - 'AUTH_SMS_NOTIFICATION_SUBJECT', 'Auth activity') + 'AUTH_SMS_NOTIFICATION_SUBJECT', _('Auth activity')) CODES_SUBJECT = get_settings( - 'AUTH_CODES_SUBJECT', 'Your security codes') + 'AUTH_CODES_SUBJECT', _('Your security codes')) SMS_NOTIFICATION_MESSAGE = get_settings( 'AUTH_SMS_NOTIFICATION_MESSAGE', - "Authorization was made. If it's not you, then contact with us.") + _("Authorization was made. If it's not you, then contact with us.")) USE_CELERY = get_settings( 'AUTH_USE_CELERY', 'djcelery' in settings.INSTALLED_APPS) diff --git a/secureauth/forms.py b/secureauth/forms.py index 7afc7b8..2a69623 100644 --- a/secureauth/forms.py +++ b/secureauth/forms.py @@ -35,7 +35,7 @@ def __init__(self, request, model, *args, **kwargs): def clean_current_password(self): current_password = self.cleaned_data.get('current_password', '') if not self.request.user.check_password(current_password): - raise forms.ValidationError(_(u'Invalid password!')) + raise forms.ValidationError(_(u'Invalid password')) def save(self): if not self.user: @@ -101,26 +101,32 @@ class QuestionForm(BasicForm): question = forms.CharField(label=_('Question'), required=True) code = forms.CharField(label=_('code'), required=True, max_length=16) - def __init__(self, *args, **kwargs): + def __init__(self, request, *args, **kwargs): self.decrypt('code', **kwargs) self.decrypt('question', **kwargs) - super(QuestionForm, self).__init__(*args, **kwargs) + super(QuestionForm, self).__init__(request, *args, **kwargs) - if kwargs.get('initial') or (args[2] and args[2].get('code')): - self.fields['code'].widget = forms.HiddenInput() self.fields['code'].label = _('Answer') + try: + UserAuthQuestion.objects.get(user=request.user) + self.fields.pop('code') + except UserAuthQuestion.DoesNotExist: + pass + def save(self): model = super(QuestionForm, self).save() return model.set_data( self.cleaned_data.get('question'), self.cleaned_data.get('code')) -class PasswordCheckForm(forms.ModelForm): +class BaseSettingsForm(forms.ModelForm): + enabled = forms.BooleanField(label=_('Enabled'), required=False) + def __init__(self, request, *args, **kwargs): self.request = request - super(PasswordCheckForm, self).__init__(*args, **kwargs) + super(BaseSettingsForm, self).__init__(*args, **kwargs) if CHECK_PASSWORD is True: self.fields['current_password'] = forms.CharField( @@ -129,16 +135,16 @@ def __init__(self, request, *args, **kwargs): def clean_current_password(self): current_password = self.cleaned_data.get('current_password', '') if not self.request.user.check_password(current_password): - raise forms.ValidationError(_(u'Invalid password!')) + raise forms.ValidationError(_(u'Invalid password')) -class NotificationForm(PasswordCheckForm): +class NotificationForm(BaseSettingsForm): class Meta: model = UserAuthNotification exclude = ('user',) -class LoggingForm(PasswordCheckForm): +class LoggingForm(BaseSettingsForm): class Meta: model = UserAuthLogging exclude = ('user',) @@ -184,7 +190,7 @@ def get_status(model): def clean_current_password(self): current_password = self.cleaned_data.get('current_password', '') if not self._request.user.check_password(current_password): - raise forms.ValidationError(_(u'Invalid password!')) + raise forms.ValidationError(_(u'Invalid password')) def save(self): def set_status(model, key): diff --git a/secureauth/locale/ru/LC_MESSAGES/django.mo b/secureauth/locale/ru/LC_MESSAGES/django.mo index 021c6e1e187f59e6ede3bdfe0ecf59d020bd2c06..381e18bce7f4150cc316316cf6404c06d9a739ed 100644 GIT binary patch delta 2739 zcmZA1dvHuw9Ki7tuaZa_5fveF1WmgZ!kr^B>0TE?S)wcp?F?PHp`xu0{-J?H#>=W+MI zqWiAK?=yPrRQ!ebAHx6P?((G6#7F+sOK}p#I~RDhewe?19CYhU0NGPQwD?tK}4Apm*&CAET^9C-%c@CHgc&K zxXF7TO8+-e27Cw0a34y)t0)t?gB%l;#4HOj8J$?xPxhZeH5IY~tGP+TttgrA#$ot@ zU4IdCdA3lN^mpV^e{dUxDH%#Vf#Xr?m!OQZ9A$-8p=?nr%0#zju>P|5`>Bu_ovIy!Ylr6fCTq>`T*0NNk zCEN&W($46H5DEP(H)^NEemCGD)IkD4(AP8!(Jh@D`S0A%iu_OuQ6c!4NLOD<~Nj zFp4-6nY;?1EV+S|_z}vfzJs#&-MLA?B9yOXGETw(K81(v`kN@@WI0&>=O`385}(~l zWc$=+q>Vae*MEnS$Zsf_XYlLsB39vi+>bNy9!|%pe0!4E29(cr4@$rocHt$Qh?|OL zBz`C^k?yNhe2Yv*Rr1lZnks~;xCdpA4`B+PL0RgH*b8qL$5!-8?rGGete*T#G*S}` z`B%6i{$N1YyTZEGH^b>%7>tI9?T&{0k%mM_W!EM0(KRfcyT%o$_Iiv}83$8KoW-Ti z;u1Y(TzPRx%-O#(DXAnD&8+X!bC20+o-p?34m5twx$N0ywVEB;JZG(;y50Q5JZ_#e zPw;&zS9nv9_VUr2KeXt5utwG?I8niPa&Iq>{Gwrdb68j3VtA@g5@)%sfGM0uSUy zzqUkv1lwXP)CGgzXN=QUv$cWzPf6pW@yT>p8_SI*Js~UJUt7)O(rI>B>j-?7&YNSW z^n`9h_R>2sE_wW~zi~V7qVZKh{{bEJjL}CH>A2m0OYG$EUwh8h=IMBL_8cTvYn`>$ zYF6gH|4fvXu{J96AWLR9|2F|2S% delta 1828 zcmYM!4`|e79LMqR&7GTbx7+R3bk1$wF89x!`*X|Xno*s3M5EkVMz=}KHe+q2#%vVT zL2S`qBY!ruNN_=akf8A+MIk~XGo}?L(I8_hqh*jrF{p@of7}lmH(t;4-0%55-{<>0 zznkwpluCbBkol_dx1Il${I@Ss&+LiC|JzUVN%}kR2i%8uWtmOl9G=A2vdy}2>k_jg zcnrsJXO3AhW-T?l1|wL8Ef_XS+ZGyH>p=|QE?kajT!#BS-}gL?Otcxy$3Ku9%%8ik zKmar8M==kpaSgU$E#8lszvt@rX|o9$AqFNpPNI(BQ&gN;RORL{tq1<3p#VWX>a^DRG>vXq{`%@ zj-(7()1s)I#Zd{jVgq(zJ&vL7)CZ`u{~WdM_aW-9K$jRO#LK9iWfm;l(m3h}I#Cmz zLhhSAgIaJeYNrQKaVGE!pV`~^68#Z^y6C6ORg53QQJke*ZMZ#6S|@nmP0~&<(7(cL zBfiUdv@rfF?_?Y+OZgsCSceBucVG&Y_}AEhzj*x(yki>gL~Y;!jG}|8>`~N_re|n0 z(YTD-Q3VG>46DK0Fo{Wg1r=c0`~DxSqF=IV;p`Jg?v_Fw)oxT}o<>#l1>AT;Ix&gs2vMg_hP^}8^HI?GYK2lt~AyyEo>>F5V-2nl7+ zp(=LR^9Xiu|Lg<}U8?Z43%4?Yx`a35Rvbi4{0#M$oX1MMf=X=J>V;C?f{)Sfz?*Oi z*I_OXEAa&O;ATwWdl+GT%VxXEuoj7F?Wosj7bfvpRLQ;fF5Zud&ufbDn# zKJSg6LdBWK+cChdxR-V>QY0I~v<6R9K%g`4wXnTS?G1DMJF*ux?{jPO+I-GPu*ba^{4m46d)q?;!_L`)Vy7rv+>qM{5 baDHBW%AKyf\n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" @@ -17,7 +17,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Language: \n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" -"X-Translated-Using: django-rosetta 0.7.3\n" +"X-Translated-Using: django-rosetta 0.7.4\n" msgid "Russian" msgstr "Русский" @@ -125,7 +125,7 @@ msgstr "код" msgid "Please enter correct code" msgstr "Пожалуйста, введите правильный код" -msgid "This account is inactive." +msgid "This account is inactive" msgstr "Эта учетная запись неактивна." msgid "Enabled" @@ -373,7 +373,7 @@ msgid "SMS was sent!" msgstr "Код был выслан к Вам на мобильный телефон" msgid "Default backend can not be removed" -msgstr "Нельзя удалить настройки" +msgstr "Нельзя удалить настройки по умолчанию" msgid "Auth method was disabled" msgstr "Один из методов аутентификации был отключен" @@ -395,3 +395,39 @@ msgstr "Уведомления отключены. Если это не Вы, п msgid "Answer" msgstr "Ответ" + +msgid "Current password:" +msgstr "Текущий пароль:" + +msgid "Logging settings" +msgstr "Настройка логирования" + +msgid "Confirm Method" +msgstr "Метод подтверждения" + +msgid "Invalid password" +msgstr "Неверный пароль" + +msgid "Successfully saved" +msgstr "Успешно сохранено" + +msgid "Your settings has changed" +msgstr "Настройки изменились" + +msgid "Codes were sent to the email" +msgstr "Коды отправлены на email" + +msgid "Send codes to email" +msgstr "Отправить коды на почту" + +msgid "Your code is: %s" +msgstr "Ваш код: %s" + +msgid "Auth activity" +msgstr "Код авторизации" + +msgid "Your security codes" +msgstr "Ваши коды безопасности" + +msgid "Authorization was made. If it's not you, then contact with us." +msgstr "Был произведен вход. Если это не Вы, то свяжитесь с нами." diff --git a/secureauth/migrations/0006_auto__add_field_userauthactivity_confirm_method.py b/secureauth/migrations/0006_auto__add_field_userauthactivity_confirm_method.py new file mode 100644 index 0000000..51e3785 --- /dev/null +++ b/secureauth/migrations/0006_auto__add_field_userauthactivity_confirm_method.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'UserAuthActivity.confirm_method' + db.add_column(u'secureauth_userauthactivity', 'confirm_method', + self.gf('django.db.models.fields.CharField')(max_length=10, null=True, blank=True), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'UserAuthActivity.confirm_method' + db.delete_column(u'secureauth_userauthactivity', 'confirm_method') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'secureauth.userauthactivity': { + 'Meta': {'ordering': "('-id',)", 'object_name': 'UserAuthActivity'}, + 'agent': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'confirm_method': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'geo': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip': ('django.db.models.fields.CharField', [], {'max_length': '40', 'db_index': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}) + }, + u'secureauth.userauthattempt': { + 'Meta': {'object_name': 'UserAuthAttempt'}, + 'attempt': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'auto_now_add': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}) + }, + u'secureauth.userauthcode': { + 'Meta': {'object_name': 'UserAuthCode'}, + 'code': ('django.db.models.fields.TextField', [], {}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_verified': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'number': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'}) + }, + u'secureauth.userauthlogging': { + 'Meta': {'object_name': 'UserAuthLogging'}, + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'}) + }, + u'secureauth.userauthnotification': { + 'Meta': {'object_name': 'UserAuthNotification'}, + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'}) + }, + u'secureauth.userauthphone': { + 'Meta': {'object_name': 'UserAuthPhone'}, + 'code': ('django.db.models.fields.TextField', [], {}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_verified': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'phone': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'}) + }, + u'secureauth.userauthquestion': { + 'Meta': {'object_name': 'UserAuthQuestion'}, + 'code': ('django.db.models.fields.TextField', [], {}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_verified': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'question': ('django.db.models.fields.TextField', [], {}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'}) + }, + u'secureauth.userauthtoken': { + 'Meta': {'object_name': 'UserAuthToken'}, + 'code': ('django.db.models.fields.TextField', [], {}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_verified': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'}) + } + } + + complete_apps = ['secureauth'] \ No newline at end of file diff --git a/secureauth/models.py b/secureauth/models.py index bfcb382..3cf409a 100644 --- a/secureauth/models.py +++ b/secureauth/models.py @@ -7,6 +7,7 @@ from django.utils.translation import ugettext as _ from django.core.exceptions import ValidationError +from django.forms.models import model_to_dict from django.contrib.sites.models import Site from django.utils.timezone import now from django.contrib import messages @@ -24,7 +25,16 @@ from secureauth.defaults import ( SMS_MESSAGE, SMS_CODE_LEN, SMS_AGE, SMS_FROM, CODE_RANGES, CODE_LEN, SMS_NOTIFICATION_MESSAGE, SMS_NOTIFICATION_SUBJECT, - LOGIN_ATTEMPT, BAN_TIME, CHECK_ATTEMPT, CODES_SUBJECT) + LOGIN_ATTEMPT, BAN_TIME, CHECK_ATTEMPT, CODES_SUBJECT, TOTP_NAME) + + +AUTH_TYPES = ( + ('', '---'), + ('code', _('by code')), + ('token', _('by token')), + ('phone', _('by sms')), + ('question', _('by question')), +) class UserAuthAbstract(models.Model): @@ -115,11 +125,9 @@ def send_codes(cls, request): class UserAuthToken(UserAuthAbstract): def get_google_url(self): - return get_google_url( - Sign().unsign(self.code), - "%s@%s" % ( - self.user.get_full_name(), Site.objects.get_current().domain) - ) + data = model_to_dict(Site.objects.get_current()) + data.update(model_to_dict(self.user)) + return get_google_url(Sign().unsign(self.code), TOTP_NAME % data) def _code_is_valid(self, code): return check_seed(Sign().unsign(self.code), int(code)) @@ -204,12 +212,15 @@ class UserAuthActivity(models.Model): date = models.DateTimeField(_('Date'), auto_now_add=True) agent = models.CharField( _('Browser'), max_length=255, null=True, blank=True) + confirm_method = models.CharField( + _('Confirm method'), max_length=10, choices=AUTH_TYPES, + null=True, blank=True) class Meta: ordering = ('-id',) @classmethod - def log_auth(cls, request): + def log_auth(cls, request, confirm_method=''): ip = get_ip(request) user_agent = request.META.get('HTTP_USER_AGENT') if user_agent is not None: @@ -220,7 +231,7 @@ def log_auth(cls, request): browser.get('name', ""), browser.get('version', "")) cls.objects.create( user=request.user, ip=get_ip(request), geo=get_geo(ip), - agent=user_agent + agent=user_agent, confirm_method=confirm_method ) @classmethod diff --git a/secureauth/registration/views.py b/secureauth/registration/views.py index 1e0e2fd..9380a10 100644 --- a/secureauth/registration/views.py +++ b/secureauth/registration/views.py @@ -8,9 +8,9 @@ from registration.backends.default import views from secureauth.forms import ActivatePhoneForm, CodeForm +from secureauth.defaults import SMS_AGE, SMS_FORCE from secureauth.models import UserAuthPhone from secureauth.utils.sign import Sign -from secureauth.defaults import SMS_AGE def _get_user(**kwargs): @@ -83,3 +83,6 @@ def activate(self, request, activation_key): def get_success_url(self, request, user): return ('registration_activation_complete_view', (), {}) + + +ActivationView = ActivationView if SMS_FORCE else views.ActivationView diff --git a/secureauth/tables.py b/secureauth/tables.py index 35c7570..a6119d5 100644 --- a/secureauth/tables.py +++ b/secureauth/tables.py @@ -9,6 +9,6 @@ class UserAuthActivityTable(tables.Table): class Meta: model = UserAuthActivity - fields = ('ip', 'geo', 'date', 'agent') + fields = ('ip', 'geo', 'date', 'agent', 'confirm_method') order_by = ('id',) per_page = ACTIVITY_PER_PAGE diff --git a/secureauth/templates/secureauth/codes_settings/was_done.html b/secureauth/templates/secureauth/codes_settings/was_done.html index c94fcca..535ffc6 100644 --- a/secureauth/templates/secureauth/codes_settings/was_done.html +++ b/secureauth/templates/secureauth/codes_settings/was_done.html @@ -6,5 +6,5 @@ {% block content %} {% trans 'Codes authentication successfully enabled.' %}
- {% trans 'Send codes to email.' %} + {% trans 'Send codes to email' %} {% endblock content %} diff --git a/secureauth/views.py b/secureauth/views.py index cdf3e2a..df685bd 100644 --- a/secureauth/views.py +++ b/secureauth/views.py @@ -137,14 +137,18 @@ def login_confirmation(request, template_name='secureauth/confirmation.html', if UserAuthLogging.is_enabled(request): UserAuthActivity.check_location(request) - UserAuthActivity.log_auth(request) + UserAuthActivity.log_auth( + request, form.cleaned_data.get('auth_type')) UserAuthNotification.notify(request) UserAuthAttempt.remove(request) return HttpResponseRedirect(data.get('redirect_to')) else: - raise Http404('ERROR') + return HttpResponseBadRequest() + elif CHECK_ATTEMPT is True: + UserAuthAttempt.clean() + UserAuthAttempt.store(request) else: form = authentication_form(data) @@ -339,9 +343,13 @@ def codes_settings(request): @login_required @never_cache def send_codes(request): - if UserAuthCode.send_codes(request): + if request.session.get('step') != 3: + raise Http404 + elif UserAuthCode.send_codes(request): messages.info(request, _('Codes were sent to the email')) UserAuthNotification.notify(request, _('Codes were sent to the email')) + if request.session.get('step'): + del request.session['step'] return redirect('codes_settings') @@ -381,6 +389,7 @@ def _settings_view(request, model_class, form_class, template): request, _('Your settings has changed'), force=True) return render(request, template, {'form': form}) + @login_required @never_cache def notify_settings(request): diff --git a/setup.py b/setup.py index a7ecb8c..4afa23b 100644 --- a/setup.py +++ b/setup.py @@ -8,10 +8,10 @@ name='django-secure-auth', version=get_version(), description='Secure authentication by TOTP, SMS, Codes & Question', - keywords='django secure auth totp sms codes question', + keywords='django secure auth protection totp sms codes question', long_description=open('README.rst').read(), classifiers=[ - 'Development Status :: 4 - Beta', + 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', 'Framework :: Django', 'Intended Audience :: Developers',