1# Copyright (c) 2012-2016 Seafile Ltd. 2import hashlib 3import re 4import logging 5from datetime import datetime 6from importlib import import_module 7 8from constance import config 9 10from django.conf import settings 11from django.urls import reverse 12from django.http import HttpResponseRedirect, Http404 13from django.utils.translation import ugettext as _ 14from django.views.decorators.cache import never_cache 15from django.contrib.sites.shortcuts import get_current_site 16from django.shortcuts import redirect 17from django.utils.http import is_safe_url 18from django.views.decorators.debug import sensitive_post_parameters 19 20from formtools.wizard.views import SessionWizardView 21 22 23from seahub.auth import REDIRECT_FIELD_NAME, get_backends 24from seahub.auth import login as auth_login 25from seahub.base.accounts import User 26from seahub.utils.ip import get_remote_ip 27 28from seahub.profile.models import Profile 29 30from seahub.two_factor import login as two_factor_login 31from seahub.two_factor.models import (StaticDevice, TOTPDevice, default_device, 32 user_has_device) 33 34from seahub.two_factor.forms import TOTPTokenAuthForm, BackupTokenAuthForm, AuthenticationTokenForm 35from seahub.two_factor.views.utils import class_view_decorator 36 37from seahub.utils.auth import get_login_bg_image_path 38 39 40# Get an instance of a logger 41logger = logging.getLogger(__name__) 42 43@class_view_decorator(sensitive_post_parameters()) 44@class_view_decorator(never_cache) 45class TwoFactorVerifyView(SessionWizardView): 46 """ 47 View for handling the login process, including OTP verification. 48 49 The login process is composed like a wizard. The first step asks for the 50 user's credentials. If the credentials are correct, the wizard proceeds to 51 the OTP verification step. If the user has a default OTP device 52 configured, that device is asked to generate a token and the user is asked 53 to provide the generated token. 54 """ 55 template_name = 'two_factor/core/login.html' 56 storage_name = 'seahub.two_factor.views.utils.ExtraSessionStorage' 57 58 form_list = ( 59 ('token', AuthenticationTokenForm), 60 ('backup', BackupTokenAuthForm), 61 ) 62 63 def has_token_step(self): 64 return default_device(self.get_user_from_request(self.request)) 65 66 def has_backup_step(self): 67 return StaticDevice.objects.device_for_user(self.user.username) and \ 68 not self.storage.get_step_data('token') 69 70 # This class attribute must be defined after `has_token_step` and 71 # `has_backup_step` methods because it makes use of them. 72 condition_dict = { 73 'token': has_token_step, 74 'backup': has_backup_step, 75 } 76 redirect_field_name = REDIRECT_FIELD_NAME 77 78 def __init__(self, **kwargs): 79 super(TwoFactorVerifyView, self).__init__(**kwargs) 80 self.user = None 81 self.device_cache = None 82 83 def reset_two_factor_session(self): 84 for key in (SESSION_KEY_TWO_FACTOR_AUTH_USERNAME, 85 SESSION_KEY_TWO_FACTOR_REDIRECT_URL, 86 SESSION_KEY_TWO_FACTOR_FAILED_ATTEMPT): 87 self.request.session.pop(key, '') 88 89 def dispatch(self, request, *a, **kw): 90 self.user = self.get_user_from_request(request) 91 if not self.user: 92 return HttpResponseRedirect(settings.LOGIN_URL) 93 response = super(TwoFactorVerifyView, self).dispatch(request, *a, **kw) 94 if self.request.session.get(SESSION_KEY_TWO_FACTOR_FAILED_ATTEMPT, 0) >= settings.LOGIN_ATTEMPT_LIMIT: 95 self.reset_two_factor_session() 96 return response 97 98 def done(self, form_list, **kwargs): 99 """ 100 Login the user and redirect to the desired page. 101 """ 102 redirect_to = self.request.session.get(SESSION_KEY_TWO_FACTOR_REDIRECT_URL, '') \ 103 or self.request.GET.get(self.redirect_field_name, '') 104 105 auth_login(self.request, self.user) 106 107 self.reset_two_factor_session() 108 109 if not is_safe_url(url=redirect_to, allowed_hosts=self.request.get_host()): 110 redirect_to = str(settings.LOGIN_REDIRECT_URL) 111 112 res = HttpResponseRedirect(redirect_to) 113 if form_list[0].is_valid(): 114 remember_me = form_list[0].cleaned_data['remember_me'] 115 if remember_me: 116 s = remember_device(self.user.username) 117 res.set_cookie( 118 'S2FA', s.session_key, 119 max_age=settings.TWO_FACTOR_DEVICE_REMEMBER_DAYS * 24 * 60 * 60, 120 domain=settings.SESSION_COOKIE_DOMAIN, 121 path=settings.SESSION_COOKIE_PATH, 122 secure=settings.SESSION_COOKIE_SECURE or None, 123 httponly=settings.SESSION_COOKIE_HTTPONLY or None) 124 return res 125 126 def get_form_kwargs(self, step=None): 127 if step in ('token', 'backup'): 128 return { 129 'user': self.user, 130 'request': self.request, 131 } 132 return {} 133 134 def get_device(self, step=None): 135 """ 136 Returns the OTP device selected by the user, or his default device. 137 """ 138 if not self.device_cache: 139 if step == 'backup': 140 try: 141 self.device_cache = StaticDevice.objects.get( 142 user=self.user.username, name='backup') 143 except StaticDevice.DoesNotExist: 144 pass 145 if not self.device_cache: 146 self.device_cache = default_device(self.user) 147 return self.device_cache 148 149 def render(self, form=None, **kwargs): 150 """ 151 If the user selected a device, ask the device to generate a challenge; 152 either making a phone call or sending a text message. 153 """ 154 if self.steps.current == 'token': 155 self.get_device().generate_challenge() 156 return super(TwoFactorVerifyView, self).render(form, **kwargs) 157 158 def get_user_from_request(self, request): 159 username = request.session.get(SESSION_KEY_TWO_FACTOR_AUTH_USERNAME, None) 160 if not username: 161 return None 162 username = Profile.objects.get_username_by_login_id(username) or username 163 try: 164 user = User.objects.get(email=username) 165 except User.DoesNotExist: 166 self.request.session.pop(SESSION_KEY_TWO_FACTOR_AUTH_USERNAME, '') 167 return None 168 for backend in get_backends(): 169 user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__) 170 return user 171 172 def get_context_data(self, form, **kwargs): 173 """ 174 Adds user's default and backup OTP devices to the context. 175 """ 176 context = super(TwoFactorVerifyView, self).get_context_data(form, **kwargs) 177 if self.steps.current == 'token': 178 context['device'] = self.get_device() 179 device = StaticDevice.objects.device_for_user(self.user.username) 180 context['backup_tokens'] = device.token_set.count() if device else 0 181 182 context['cancel_url'] = '/accounts/logout/' 183 context['form_prefix'] = '%s-' % self.steps.current 184 login_bg_image_path = get_login_bg_image_path() 185 context['login_bg_image_path'] = login_bg_image_path 186 context['remember_days'] = settings.TWO_FACTOR_DEVICE_REMEMBER_DAYS 187 188 return context 189 190 def render_done(self, form, **kwargs): 191 final_form_list = [] 192 # walk through the form list and try to validate the data again. 193 for form_key in self.get_form_list(): 194 form_obj = self.get_form( 195 step=form_key, 196 data=self.storage.get_step_data(form_key), 197 files=self.storage.get_step_files( 198 form_key) 199 ) 200 final_form_list.append(form_obj) 201 202 done_response = self.done(final_form_list, **kwargs) 203 self.storage.reset() 204 return done_response 205 206def two_factor_auth_enabled(user): 207 return config.ENABLE_TWO_FACTOR_AUTH and user_has_device(user) 208 209SESSION_KEY_TWO_FACTOR_AUTH_USERNAME = '2fa-username' 210SESSION_KEY_TWO_FACTOR_REDIRECT_URL = '2fa-redirect-url' 211SESSION_KEY_TWO_FACTOR_FAILED_ATTEMPT = '2fa-failed-attempt' 212def handle_two_factor_auth(request, user, redirect_to): 213 request.session[SESSION_KEY_TWO_FACTOR_AUTH_USERNAME] = user.username 214 request.session[SESSION_KEY_TWO_FACTOR_REDIRECT_URL] = redirect_to 215 request.session[SESSION_KEY_TWO_FACTOR_FAILED_ATTEMPT] = 0 216 return redirect(reverse('two_factor_auth')) 217 218def verify_two_factor_token(user, token): 219 """ 220 This function is called when doing the api authentication. 221 Backup token is not supported, because if the user has the backup token, 222 he can always login the website and re-setup the totp. 223 """ 224 device = default_device(user) 225 if device: 226 return device.verify_token(token) 227 228def remember_device(s_data): 229 SessionStore = import_module(settings.SESSION_ENGINE).SessionStore 230 s = SessionStore() 231 s.set_expiry(settings.TWO_FACTOR_DEVICE_REMEMBER_DAYS * 24 * 60 * 60) 232 s['2fa-skip'] = s_data 233 s.create() 234 return s 235 236def is_device_remembered(request_header, user): 237 if not request_header: 238 return False 239 240 # User must be authenticated, otherwise this function is wrong used. 241 assert user.is_authenticated 242 243 SessionStore = import_module(settings.SESSION_ENGINE).SessionStore 244 s = SessionStore(request_header) 245 try: 246 username = s['2fa-skip'] 247 return username == user.username 248 except KeyError: 249 return False 250