1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-26 07:19:58 +00:00

[2fa] Remember device for certain days

This commit is contained in:
zhengxie
2017-12-15 11:33:00 +08:00
parent f5daddfef3
commit c767931fd3
8 changed files with 97 additions and 6 deletions

View File

@@ -8,6 +8,7 @@ from seahub.auth import authenticate
from seahub.api2.models import DESKTOP_PLATFORMS from seahub.api2.models import DESKTOP_PLATFORMS
from seahub.api2.utils import get_token_v1, get_token_v2 from seahub.api2.utils import get_token_v1, get_token_v2
from seahub.profile.models import Profile from seahub.profile.models import Profile
from seahub.two_factor.views.login import is_device_remembered
from seahub.utils.two_factor_auth import has_two_factor_auth, \ from seahub.utils.two_factor_auth import has_two_factor_auth, \
two_factor_auth_enabled, verify_two_factor_token two_factor_auth_enabled, verify_two_factor_token
@@ -108,6 +109,11 @@ class AuthTokenSerializer(serializers.Serializer):
def _two_factor_auth(self, request, user): def _two_factor_auth(self, request, user):
if not has_two_factor_auth() or not two_factor_auth_enabled(user): if not has_two_factor_auth() or not two_factor_auth_enabled(user):
return return
if is_device_remembered(request.META.get('HTTP_X_SEAFILE_S2FA', ''),
user):
return
token = request.META.get('HTTP_X_SEAFILE_OTP', '') token = request.META.get('HTTP_X_SEAFILE_OTP', '')
if not token: if not token:
self.two_factor_auth_failed = True self.two_factor_auth_failed = True
@@ -118,6 +124,7 @@ class AuthTokenSerializer(serializers.Serializer):
msg = 'Two factor auth token is invalid.' msg = 'Two factor auth token is invalid.'
raise serializers.ValidationError(msg) raise serializers.ValidationError(msg)
class AccountSerializer(serializers.Serializer): class AccountSerializer(serializers.Serializer):
email = serializers.EmailField() email = serializers.EmailField()
password = serializers.CharField() password = serializers.CharField()

View File

@@ -3,6 +3,7 @@
import logging import logging
import os import os
import stat import stat
from importlib import import_module
import json import json
import datetime import datetime
import posixpath import posixpath
@@ -18,6 +19,7 @@ from rest_framework.permissions import IsAuthenticated, IsAdminUser
from rest_framework.reverse import reverse from rest_framework.reverse import reverse
from rest_framework.response import Response from rest_framework.response import Response
from django.conf import settings as dj_settings
from django.contrib.auth.hashers import check_password from django.contrib.auth.hashers import check_password
from django.contrib.sites.models import RequestSite from django.contrib.sites.models import RequestSite
from django.db import IntegrityError from django.db import IntegrityError
@@ -180,12 +182,45 @@ class ObtainAuthToken(APIView):
renderer_classes = (renderers.JSONRenderer,) renderer_classes = (renderers.JSONRenderer,)
def post(self, request): def post(self, request):
headers = {}
context = { 'request': request } context = { 'request': request }
serializer = AuthTokenSerializer(data=request.data, context=context) serializer = AuthTokenSerializer(data=request.data, context=context)
if serializer.is_valid(): if serializer.is_valid():
key = serializer.validated_data key = serializer.validated_data
trust_dev = False
try:
trust_dev_header = int(request.META.get('HTTP_X_SEAFILE_2FA_TRUST_DEVICE', ''))
trust_dev = True if trust_dev_header == 1 else False
except ValueError:
trust_dev = False
skip_2fa_header = request.META.get('HTTP_X_SEAFILE_S2FA', None)
if skip_2fa_header is None:
if trust_dev:
# 2fa login with trust device,
# create new session, and return session id.
pass
else:
# No 2fa login or 2fa login without trust device,
# return token only.
return Response({'token': key}) return Response({'token': key})
headers = {} else:
# 2fa login without OTP token,
# get or create session, and return session id
pass
SessionStore = import_module(dj_settings.SESSION_ENGINE).SessionStore
s = SessionStore(skip_2fa_header)
if not s.exists(skip_2fa_header) or s.is_empty():
from seahub.two_factor.views.login import remember_device
s = remember_device(request.data['username'])
headers = {
'X-SEAFILE-S2FA': s.session_key
}
return Response({'token': key}, headers=headers)
if serializer.two_factor_auth_failed: if serializer.two_factor_auth_failed:
# Add a special response header so the client knows to ask the user # Add a special response header so the client knows to ask the user
# for the 2fa token. # for the 2fa token.

View File

@@ -29,6 +29,7 @@ from seahub.auth.utils import (
from seahub.base.accounts import User from seahub.base.accounts import User
from seahub.options.models import UserOptions from seahub.options.models import UserOptions
from seahub.profile.models import Profile from seahub.profile.models import Profile
from seahub.two_factor.views.login import is_device_remembered
from seahub.utils import is_ldap_user from seahub.utils import is_ldap_user
from seahub.utils.ip import get_remote_ip from seahub.utils.ip import get_remote_ip
from seahub.utils.file_size import get_quota_from_string from seahub.utils.file_size import get_quota_from_string
@@ -54,7 +55,8 @@ def log_user_in(request, user, redirect_to):
clear_login_failed_attempts(request, user.username) clear_login_failed_attempts(request, user.username)
if two_factor_auth_enabled(user): if two_factor_auth_enabled(user) and \
not is_device_remembered(request.COOKIES.get('S2FA', ''), user):
return handle_two_factor_auth(request, user, redirect_to) return handle_two_factor_auth(request, user, redirect_to)
# Okay, security checks complete. Log the user in. # Okay, security checks complete. Log the user in.

View File

@@ -634,6 +634,7 @@ CLOUD_DEMO_USER = 'demo@seafile.com'
ENABLE_TWO_FACTOR_AUTH = False ENABLE_TWO_FACTOR_AUTH = False
OTP_LOGIN_URL = '/profile/two_factor_authentication/setup/' OTP_LOGIN_URL = '/profile/two_factor_authentication/setup/'
TWO_FACTOR_DEVICE_REMEMBER_DAYS = 90
# Enable personal wiki, group wiki # Enable personal wiki, group wiki
ENABLE_WIKI = False ENABLE_WIKI = False

View File

@@ -295,6 +295,7 @@ class OTPAuthenticationFormMixin(object):
class AuthenticationTokenForm(OTPAuthenticationFormMixin, Form): class AuthenticationTokenForm(OTPAuthenticationFormMixin, Form):
otp_token = forms.IntegerField(label=_("Token"), min_value=1, otp_token = forms.IntegerField(label=_("Token"), min_value=1,
max_value=int('9' * totp_digits())) max_value=int('9' * totp_digits()))
remember_me = forms.BooleanField(required=False)
def __init__(self, user, request=None, *args, **kwargs): def __init__(self, user, request=None, *args, **kwargs):
""" """

View File

@@ -22,6 +22,11 @@
<label for="token">{% trans "Authentication token" %}</label> <label for="token">{% trans "Authentication token" %}</label>
<input id="token" type="text" name="{{form_prefix}}otp_token" value="" class="input two-factor-auth-login-token-input" autocomplete="off" /> <input id="token" type="text" name="{{form_prefix}}otp_token" value="" class="input two-factor-auth-login-token-input" autocomplete="off" />
<label class="checkbox-label remember">
<input type="checkbox" name="{{form_prefix}}remember_me" class="vam remember-input" />
<span class="vam">{% blocktrans %}Remember this computer for {{remember_days}} days{% endblocktrans %}</span>
</label>
{% if form.errors %} {% if form.errors %}
<p class="error">{% trans "Incorrect code" %}</p> <p class="error">{% trans "Incorrect code" %}</p>
{% else %} {% else %}

View File

@@ -3,6 +3,7 @@ import hashlib
import re import re
import logging import logging
from datetime import datetime from datetime import datetime
from importlib import import_module
from constance import config from constance import config
@@ -108,7 +109,20 @@ class TwoFactorVerifyView(SessionWizardView):
if not is_safe_url(url=redirect_to, host=self.request.get_host()): if not is_safe_url(url=redirect_to, host=self.request.get_host()):
redirect_to = str(settings.LOGIN_REDIRECT_URL) redirect_to = str(settings.LOGIN_REDIRECT_URL)
return redirect(redirect_to)
res = HttpResponseRedirect(redirect_to)
if form_list[0].is_valid():
remember_me = form_list[0].cleaned_data['remember_me']
if remember_me:
s = remember_device(self.user.username)
res.set_cookie(
'S2FA', s.session_key,
max_age=settings.TWO_FACTOR_DEVICE_REMEMBER_DAYS * 24 * 60 * 60,
domain=settings.SESSION_COOKIE_DOMAIN,
path=settings.SESSION_COOKIE_PATH,
secure=settings.SESSION_COOKIE_SECURE or None,
httponly=settings.SESSION_COOKIE_HTTPONLY or None)
return res
def get_form_kwargs(self, step=None): def get_form_kwargs(self, step=None):
if step in ('token', 'backup'): if step in ('token', 'backup'):
@@ -168,9 +182,9 @@ class TwoFactorVerifyView(SessionWizardView):
context['cancel_url'] = settings.LOGOUT_URL context['cancel_url'] = settings.LOGOUT_URL
context['form_prefix'] = '%s-' % self.steps.current context['form_prefix'] = '%s-' % self.steps.current
login_bg_image_path = get_login_bg_image_path() login_bg_image_path = get_login_bg_image_path()
context['login_bg_image_path'] = login_bg_image_path context['login_bg_image_path'] = login_bg_image_path
context['remember_days'] = settings.TWO_FACTOR_DEVICE_REMEMBER_DAYS
return context return context
@@ -212,3 +226,26 @@ def verify_two_factor_token(username, token):
device = TOTPDevice.objects.device_for_user(username) device = TOTPDevice.objects.device_for_user(username)
if device: if device:
return device.verify_token(token) return device.verify_token(token)
def remember_device(s_data):
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
s = SessionStore()
s.set_expiry(settings.TWO_FACTOR_DEVICE_REMEMBER_DAYS * 24 * 60 * 60)
s['2fa-skip'] = s_data
s.create()
return s
def is_device_remembered(request_header, user):
if not request_header:
return False
# User must be authenticated, otherwise this function is wrong used.
assert user.is_authenticated()
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
s = SessionStore(request_header)
try:
username = s['2fa-skip']
return username == user.username
except KeyError:
return False

View File

@@ -58,4 +58,7 @@ class DisableView(CheckTwoFactorEnabledMixin, FormView):
def form_valid(self, form): def form_valid(self, form):
for device in devices_for_user(self.request.user): for device in devices_for_user(self.request.user):
device.delete() device.delete()
return HttpResponseRedirect(reverse('edit_profile'))
resp = HttpResponseRedirect(reverse('edit_profile'))
resp.delete_cookie('S2FA', domain=settings.SESSION_COOKIE_DOMAIN)
return resp