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'), + )