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