diff --git a/requirements.txt b/requirements.txt
index 52f6ab755e..14900b9467 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -11,3 +11,5 @@ djangorestframework==3.3.1
git+git://github.com/haiwen/django-constance.git@751f7f8b60651a2828e4a535a47fc05b907883da#egg=django-constance[database]
openpyxl==2.3.0
pytz==2015.7
+django-formtools
+qrcode
diff --git a/seahub/api2/serializers.py b/seahub/api2/serializers.py
index 2e0315a2db..d087321c02 100644
--- a/seahub/api2/serializers.py
+++ b/seahub/api2/serializers.py
@@ -4,6 +4,7 @@ from seahub.auth import authenticate
from seahub.api2.models import Token, TokenV2, DESKTOP_PLATFORMS
from seahub.api2.utils import get_token_v1, get_token_v2
from seahub.profile.models import Profile
+from seahub.utils.two_factor_auth import has_two_factor_auth, two_factor_auth_enabled, verify_two_factor_token
def all_none(values):
for value in values:
@@ -32,6 +33,10 @@ class AuthTokenSerializer(serializers.Serializer):
client_version = serializers.CharField(required=False, default='')
platform_version = serializers.CharField(required=False, default='')
+ def __init__(self, *a, **kw):
+ super(AuthTokenSerializer, self).__init__(*a, **kw)
+ self.two_factor_auth_failed = False
+
def validate(self, attrs):
login_id = attrs.get('username')
password = attrs.get('password')
@@ -66,8 +71,9 @@ class AuthTokenSerializer(serializers.Serializer):
else:
raise serializers.ValidationError('Must include "username" and "password"')
- # Now user is authenticated
+ self._two_factor_auth(self.context['request'], username)
+ # Now user is authenticated
if v2:
token = get_token_v2(self.context['request'], username, platform, device_id, device_name,
client_version, platform_version)
@@ -75,6 +81,19 @@ class AuthTokenSerializer(serializers.Serializer):
token = get_token_v1(username)
return token.key
+ def _two_factor_auth(self, request, username):
+ if not has_two_factor_auth() or not two_factor_auth_enabled(username):
+ return
+ token = request.META.get('HTTP_X_SEAFILE_OTP', '')
+ if not token:
+ self.two_factor_auth_failed = True
+ msg = 'Two factor auth token is missing.'
+ raise serializers.ValidationError(msg)
+ if not verify_two_factor_token(username, token):
+ self.two_factor_auth_failed = True
+ msg = 'Two factor auth token is invalid.'
+ raise serializers.ValidationError(msg)
+
class AccountSerializer(serializers.Serializer):
email = serializers.EmailField()
password = serializers.CharField()
diff --git a/seahub/api2/views.py b/seahub/api2/views.py
index af05283945..a2c6585e55 100644
--- a/seahub/api2/views.py
+++ b/seahub/api2/views.py
@@ -190,8 +190,17 @@ class ObtainAuthToken(APIView):
if serializer.is_valid():
key = serializer.validated_data
return Response({'token': key})
+ headers = {}
+ if serializer.two_factor_auth_failed:
+ # Add a special response header so the client knows to ask the user
+ # for the 2fa token.
+ headers = {
+ 'X-Seafile-OTP': 'required',
+ }
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+ return Response(serializer.errors,
+ status=status.HTTP_400_BAD_REQUEST,
+ headers=headers)
########## Accounts
class Accounts(APIView):
diff --git a/seahub/api2/views_auth.py b/seahub/api2/views_auth.py
index d7db551151..a338861f49 100644
--- a/seahub/api2/views_auth.py
+++ b/seahub/api2/views_auth.py
@@ -10,6 +10,7 @@ from seahub.api2.authentication import TokenAuthentication
from seahub.api2.models import Token, TokenV2
from seahub.base.models import ClientLoginToken
from seahub.utils import gen_token
+from seahub.utils.two_factor_auth import has_two_factor_auth, two_factor_auth_enabled
class LogoutDeviceView(APIView):
"""Removes the api token of a device that has already logged in. If the device
@@ -41,6 +42,8 @@ class ClientLoginTokenView(APIView):
@json_response
def post(self, request, format=None):
+ if has_two_factor_auth() and two_factor_auth_enabled(request.user.username):
+ return {}
randstr = gen_token(max_length=32)
token = ClientLoginToken(randstr, request.user.username)
token.save()
diff --git a/seahub/auth/views.py b/seahub/auth/views.py
index 4ca6c2f360..3dea05e32b 100644
--- a/seahub/auth/views.py
+++ b/seahub/auth/views.py
@@ -28,6 +28,7 @@ from seahub.profile.models import Profile
from seahub.utils import is_ldap_user
from seahub.utils.http import is_safe_url
from seahub.utils.ip import get_remote_ip
+from seahub.utils.two_factor_auth import two_factor_auth_enabled, handle_two_factor_auth
from constance import config
@@ -43,14 +44,17 @@ def log_user_in(request, user, redirect_to):
if not is_safe_url(url=redirect_to, host=request.get_host()):
redirect_to = settings.LOGIN_REDIRECT_URL
- # Okay, security checks complete. Log the user in.
- auth_login(request, user)
-
if request.session.test_cookie_worked():
request.session.delete_test_cookie()
_clear_login_failed_attempts(request)
+ if two_factor_auth_enabled(user):
+ return handle_two_factor_auth(request, user, redirect_to)
+
+ # Okay, security checks complete. Log the user in.
+ auth_login(request, user)
+
return HttpResponseRedirect(redirect_to)
def _get_login_failed_attempts(username=None, ip=None):
diff --git a/seahub/profile/templates/profile/set_profile.html b/seahub/profile/templates/profile/set_profile.html
index 39fc873073..651a30f07c 100644
--- a/seahub/profile/templates/profile/set_profile.html
+++ b/seahub/profile/templates/profile/set_profile.html
@@ -15,6 +15,9 @@
{% trans "Default Library" %}
{% endif %}
{% trans "Delete Account" %}
+ {% if two_factor_auth_enabled %}
+ {% trans "Security" %}
+ {% endif %}
diff --git a/seahub/profile/urls.py b/seahub/profile/urls.py
index 0ba3aee908..ca6aecede4 100644
--- a/seahub/profile/urls.py
+++ b/seahub/profile/urls.py
@@ -1,4 +1,5 @@
-from django.conf.urls import patterns, url
+from django.conf.urls import patterns, url, include
+from seahub.utils.two_factor_auth import HAS_TWO_FACTOR_AUTH
urlpatterns = patterns('seahub.profile.views',
# url(r'^list_user/$', 'list_userids', name="list_userids"),
@@ -6,7 +7,14 @@ urlpatterns = patterns('seahub.profile.views',
url(r'^(?P[^/]+)/get/$', 'get_user_profile', name="get_user_profile"),
url(r'^delete/$', 'delete_user_account', name="delete_user_account"),
url(r'^default-repo/$', 'default_repo', name="default_repo"),
-
- url(r'^(?P[^/]*)/$', 'user_profile', name="user_profile"),
-# url(r'^logout/$', 'logout_relay', name="logout_relay"),
+)
+
+if HAS_TWO_FACTOR_AUTH:
+ urlpatterns += patterns('',
+ (r'^two_factor_authentication/', include('seahub_extra.two_factor.urls', 'two_factor')),
+ )
+
+# Move the catch-all pattern to the end.
+urlpatterns += patterns('seahub.profile.views',
+ url(r'^(?P[^/]*)/$', 'user_profile', name="user_profile"),
)
diff --git a/seahub/profile/views.py b/seahub/profile/views.py
index 9ba174b7b3..030010f185 100644
--- a/seahub/profile/views.py
+++ b/seahub/profile/views.py
@@ -1,4 +1,5 @@
# encoding: utf-8
+from constance import config
from django.conf import settings
import json
from django.core.urlresolvers import reverse
@@ -21,6 +22,7 @@ from seahub.base.templatetags.seahub_tags import email2nickname
from seahub.contacts.models import Contact
from seahub.options.models import UserOptions, CryptoOptionNotSetError
from seahub.utils import is_ldap_user
+from seahub.utils.two_factor_auth import has_two_factor_auth
from seahub.views import get_owned_repo_list
@login_required
@@ -54,13 +56,13 @@ def edit_profile(request):
init_dict['department'] = d_profile.department
init_dict['telephone'] = d_profile.telephone
form = form_class(init_dict)
-
+
# common logic
try:
server_crypto = UserOptions.objects.is_server_crypto(username)
except CryptoOptionNotSetError:
# Assume server_crypto is ``False`` if this option is not set.
- server_crypto = False
+ server_crypto = False
sub_lib_enabled = UserOptions.objects.is_sub_lib_enabled(username)
@@ -82,6 +84,7 @@ def edit_profile(request):
'owned_repos': owned_repos,
'is_pro': is_pro_version(),
'is_ldap_user': is_ldap_user(request.user),
+ 'two_factor_auth_enabled': has_two_factor_auth(),
}, context_instance=RequestContext(request))
@login_required
@@ -116,14 +119,14 @@ def get_user_profile(request, user):
'user_intro': '',
'err_msg': '',
'new_user': ''
- }
+ }
content_type = 'application/json; charset=utf-8'
try:
user_check = User.objects.get(email=user)
except User.DoesNotExist:
user_check = None
-
+
if user_check:
profile = Profile.objects.filter(user=user)
if profile:
diff --git a/seahub/settings.py b/seahub/settings.py
index 9945c6eed8..c891be6a25 100644
--- a/seahub/settings.py
+++ b/seahub/settings.py
@@ -533,6 +533,8 @@ ADD_REPLY_TO_HEADER = False
CLOUD_DEMO_USER = 'demo@seafile.com'
+ENABLE_TWO_FACTOR_AUTH = False
+
#####################
# External settings #
#####################
@@ -631,4 +633,5 @@ CONSTANCE_CONFIG = {
'USER_PASSWORD_STRENGTH_LEVEL': (USER_PASSWORD_STRENGTH_LEVEL,''),
'SHARE_LINK_PASSWORD_MIN_LENGTH': (SHARE_LINK_PASSWORD_MIN_LENGTH,''),
+ 'ENABLE_TWO_FACTOR_AUTH': (ENABLE_TWO_FACTOR_AUTH,''),
}
diff --git a/seahub/templates/sysadmin/settings.html b/seahub/templates/sysadmin/settings.html
index 75c94215c9..0fa897a5c3 100644
--- a/seahub/templates/sysadmin/settings.html
+++ b/seahub/templates/sysadmin/settings.html
@@ -73,6 +73,12 @@
{% endwith %}
+ {% if has_two_factor_auth %}
+ {% with type="checkbox" setting_display_name="enable two factor authentication" help_tip="Enable two factor authentication" setting_name="ENABLE_TWO_FACTOR_AUTH" setting_val=config_dict.ENABLE_TWO_FACTOR_AUTH %}
+ {% include "snippets/web_settings_form.html" %}
+ {% endwith %}
+ {% endif %}
+
Library
diff --git a/seahub/utils/two_factor_auth.py b/seahub/utils/two_factor_auth.py
new file mode 100644
index 0000000000..2132462c5e
--- /dev/null
+++ b/seahub/utils/two_factor_auth.py
@@ -0,0 +1,19 @@
+# encoding: utf-8
+from constance import config
+
+try:
+ from seahub_extra.two_factor.views.login import (
+ two_factor_auth_enabled,
+ handle_two_factor_auth,
+ verify_two_factor_token,
+ )
+ HAS_TWO_FACTOR_AUTH = True
+except ImportError:
+ two_factor_auth_enabled = lambda *a: False
+ handle_two_factor_auth = None
+ verify_two_factor_token = None
+ HAS_TWO_FACTOR_AUTH = False
+
+
+def has_two_factor_auth():
+ return HAS_TWO_FACTOR_AUTH and config.ENABLE_TWO_FACTOR_AUTH
diff --git a/seahub/views/sysadmin.py b/seahub/views/sysadmin.py
index 574ba876d2..d073429627 100644
--- a/seahub/views/sysadmin.py
+++ b/seahub/views/sysadmin.py
@@ -67,6 +67,7 @@ try:
from seahub.settings import MULTI_TENANCY
except ImportError:
MULTI_TENANCY = False
+from seahub.utils.two_factor_auth import HAS_TWO_FACTOR_AUTH
logger = logging.getLogger(__name__)
@@ -1871,7 +1872,7 @@ def sys_settings(request):
if not dj_settings.ENABLE_SETTINGS_VIA_WEB:
raise Http404
- DIGIT_WEB_SETTINGS = (
+ DIGIT_WEB_SETTINGS = [
'DISABLE_SYNC_WITH_ANY_FOLDER', 'ENABLE_SIGNUP',
'ACTIVATE_AFTER_REGISTRATION', 'REGISTRATION_SEND_MAIL',
'LOGIN_REMEMBER_DAYS', 'REPO_PASSWORD_MIN_LENGTH',
@@ -1880,7 +1881,10 @@ def sys_settings(request):
'USER_PASSWORD_STRENGTH_LEVEL', 'SHARE_LINK_PASSWORD_MIN_LENGTH',
'ENABLE_USER_CREATE_ORG_REPO', 'FORCE_PASSWORD_CHANGE',
'LOGIN_ATTEMPT_LIMIT', 'FREEZE_USER_ON_LOGIN_FAILED',
- )
+ ]
+
+ if HAS_TWO_FACTOR_AUTH:
+ DIGIT_WEB_SETTINGS.append('ENABLE_TWO_FACTOR_AUTH')
STRING_WEB_SETTINGS = ('SERVICE_URL', 'FILE_SERVER_ROOT',)
@@ -1927,6 +1931,7 @@ def sys_settings(request):
return render_to_response('sysadmin/settings.html', {
'config_dict': config_dict,
+ 'has_two_factor_auth': HAS_TWO_FACTOR_AUTH,
}, context_instance=RequestContext(request))
@login_required_ajax
diff --git a/thirdpart/registration/auth_urls.py b/thirdpart/registration/auth_urls.py
index 38d5425f8c..93a1f26bc4 100644
--- a/thirdpart/registration/auth_urls.py
+++ b/thirdpart/registration/auth_urls.py
@@ -27,6 +27,7 @@ from django.conf import settings
from django.conf.urls import patterns, url
from seahub.auth import views as auth_views
+from seahub.utils.two_factor_auth import HAS_TWO_FACTOR_AUTH
urlpatterns = patterns('',
url(r'^password/change/$',
@@ -72,3 +73,11 @@ else:
{'template_name': 'registration/logout.html'},
name='auth_logout'),
)
+
+ if HAS_TWO_FACTOR_AUTH:
+ from seahub_extra.two_factor.views.login import TwoFactorVerifyView
+ urlpatterns += patterns('',
+ url(r'^login/two-factor-auth/$',
+ TwoFactorVerifyView.as_view(),
+ name='two_factor_auth'),
+ )