Example #1
0
def _is_ratelimited(request):
    """Ratelimiting helper for kbforum threads and replies.

    They are ratelimited together with the same key.
    """
    return (
        is_ratelimited(request, increment=True, rate='4/m', ip=False,
                       keys=user_or_ip('kbforum-post-min')) or
        is_ratelimited(request, increment=True, rate='50/d', ip=False,
                       keys=user_or_ip('kbforum-post-day')))
Example #2
0
 def _wrapped(request, *args, **kw):
     request.limited = getattr(request, 'limited', False)
     if skip_if is None or not skip_if(request):
         ratelimited = is_ratelimited(request=request, increment=False,
                                      ip=ip, method=method, field=field,
                                      rate=rate, keys=keys)
         if ratelimited and block:
             raise Ratelimited()
     return_val, success = fn(request, *args, **kw)
     if success:
         is_ratelimited(request=request, increment=True, ip=ip,
                        method=method, field=field, rate=rate, keys=keys)
     return return_val
Example #3
0
def new_message(request, template):
    """Send a new private message."""
    to = request.GET.get('to')
    if to:
        try:
            User.objects.get(username=to)
        except User.DoesNotExist:
            contrib_messages.add_message(
                request, contrib_messages.ERROR,
                _('Invalid username provided. Enter a new username below.'))
            return HttpResponseRedirect(reverse('messages.new'))

    form = MessageForm(request.POST or None, initial={'to': to})

    if (request.method == 'POST' and form.is_valid() and
            not is_ratelimited(request, increment=True, rate='50/d', ip=False,
                           keys=user_or_ip('private-message-day'))):
        send_message(form.cleaned_data['to'], form.cleaned_data['message'],
                     request.user)
        if form.cleaned_data['in_reply_to']:
            irt = form.cleaned_data['in_reply_to']
            try:
                m = InboxMessage.objects.get(pk=irt, to=request.user)
                m.update(replied=True)
            except InboxMessage.DoesNotExist:
                pass
        contrib_messages.add_message(request, contrib_messages.SUCCESS,
                                     _('Your message was sent!'))
        return HttpResponseRedirect(reverse('messages.inbox'))

    return render(request, template, {'form': form})
Example #4
0
 def do_increment(request):
     return is_ratelimited(request,
                           increment=True,
                           ip=False,
                           method=None,
                           keys=[get_keys],
                           rate='1/m')
Example #5
0
def new_message(request, template):
    """Send a new private message."""
    to = request.GET.get('to')
    if to:
        try:
            User.objects.get(username=to)
        except User.DoesNotExist:
            contrib_messages.add_message(
                request, contrib_messages.ERROR,
                _('Invalid username provided. Enter a new username below.'))
            return HttpResponseRedirect(reverse('messages.new'))

    form = MessageForm(request.POST or None, initial={'to': to})

    if (request.method == 'POST' and form.is_valid()
            and not is_ratelimited(request,
                                   increment=True,
                                   rate='50/d',
                                   ip=False,
                                   keys=user_or_ip('private-message-day'))):
        send_message(form.cleaned_data['to'], form.cleaned_data['message'],
                     request.user)
        if form.cleaned_data['in_reply_to']:
            irt = form.cleaned_data['in_reply_to']
            try:
                m = InboxMessage.objects.get(pk=irt, to=request.user)
                m.update(replied=True)
            except InboxMessage.DoesNotExist:
                pass
        contrib_messages.add_message(request, contrib_messages.SUCCESS,
                                     _('Your message was sent!'))
        return HttpResponseRedirect(reverse('messages.inbox'))

    return render(request, template, {'form': form})
Example #6
0
File: views.py Project: jdm/kitsune
def new_message(request, template):
    """Send a new private message."""
    to = request.GET.get("to")
    if to:
        try:
            User.objects.get(username=to)
        except User.DoesNotExist:
            contrib_messages.add_message(
                request, contrib_messages.ERROR, _("Invalid username provided. Enter a new username below.")
            )
            return HttpResponseRedirect(reverse("messages.new"))

    form = MessageForm(request.POST or None, initial={"to": to})

    if (
        request.method == "POST"
        and form.is_valid()
        and not is_ratelimited(request, increment=True, rate="50/d", ip=False, keys=user_or_ip("private-message-day"))
    ):
        send_message(form.cleaned_data["to"], form.cleaned_data["message"], request.user)
        if form.cleaned_data["in_reply_to"]:
            irt = form.cleaned_data["in_reply_to"]
            try:
                m = InboxMessage.objects.get(pk=irt, to=request.user)
                m.update(replied=True)
            except InboxMessage.DoesNotExist:
                pass
        contrib_messages.add_message(request, contrib_messages.SUCCESS, _("Your message was sent!"))
        return HttpResponseRedirect(reverse("messages.inbox"))

    return render(request, template, {"form": form})
Example #7
0
def new_thread(request, forum_slug):
    """Start a new thread."""
    forum = get_object_or_404(Forum, slug=forum_slug)
    user = request.user
    if not forum.allows_posting_by(user):
        if forum.allows_viewing_by(user):
            raise PermissionDenied
        else:
            raise Http404

    if request.method == 'GET':
        form = NewThreadForm()
        return render(request, 'forums/new_thread.html', {
            'form': form,
            'forum': forum
        })

    form = NewThreadForm(request.POST)
    post_preview = None
    if form.is_valid():
        if 'preview' in request.POST:
            thread = Thread(creator=request.user,
                            title=form.cleaned_data['title'])
            post_preview = Post(thread=thread,
                                author=request.user,
                                content=form.cleaned_data['content'])
            post_preview.author_post_count = \
                post_preview.author.post_set.count()
        elif (_skip_post_ratelimit(request)
              or not is_ratelimited(request,
                                    increment=True,
                                    rate='5/d',
                                    ip=False,
                                    keys=user_or_ip('forum-post'))):
            thread = forum.thread_set.create(creator=request.user,
                                             title=form.cleaned_data['title'])
            thread.save()
            statsd.incr('forums.thread')
            post = thread.new_post(author=request.user,
                                   content=form.cleaned_data['content'])
            post.save()

            NewThreadEvent(post).fire(exclude=post.author)

            # Add notification automatically if needed.
            if Setting.get_for_user(request.user, 'forums_watch_new_thread'):
                NewPostEvent.notify(request.user, thread)

            url = reverse('forums.posts', args=[forum_slug, thread.id])
            return HttpResponseRedirect(urlparams(url, last=post.id))

    return render(request, 'forums/new_thread.html', {
        'form': form,
        'forum': forum,
        'post_preview': post_preview
    })
Example #8
0
        def _wrapped(request, *args, **kwargs):
            already_limited = getattr(request, 'limited', False)
            ratelimited = is_ratelimited(
                request=request, increment=True, ip=False, method=['POST'],
                field=None, rate=rate, keys=keyfun)

            if not already_limited and ratelimited:
                statsd.incr('throttled.' + rulename)

            return fn(request, *args, **kwargs)
Example #9
0
    def allow_request(self, request, view):
        ratelimited = is_ratelimited(
            request=request, increment=True, ip=False, method=self.methods,
            field=None, rate=self.rate, keys=self.keyfun)

        if ratelimited:
            # Failed rate-limiting, so this request is not allowed.
            statsd.incr('throttled.' + self.rulename)
            return self.throttle_failure()

        # Did not trigger rate-limiting, so this request is allowed.
        return self.throttle_success()
Example #10
0
 def _wrapped(request, *args, **kw):
     request.limited = getattr(request, 'limited', False)
     if skip_if is None or not skip_if(request):
         ratelimited = is_ratelimited(request=request,
                                      increment=True,
                                      ip=ip,
                                      method=method,
                                      field=field,
                                      rate=rate,
                                      keys=keys)
         if ratelimited and block:
             raise Ratelimited()
     return fn(request, *args, **kw)
Example #11
0
        def _wrapped(request, *args, **kwargs):
            already_limited = getattr(request, 'limited', False)
            ratelimited = is_ratelimited(request=request,
                                         increment=True,
                                         ip=False,
                                         method=['POST'],
                                         field=None,
                                         rate=rate,
                                         keys=keyfun)

            if not already_limited and ratelimited:
                statsd.incr('throttled.' + rulename)

            return fn(request, *args, **kwargs)
def authenticate_with_emergency(request):
    """ TODO: emergency code authentication """
    ret = {}
    get_params = request.GET.dict()
    codes = request.browser.user.get_emergency_codes()
    if not codes:
        ret["no_codes_generated"] = True
        custom_log(request, "No codes generated. Can't authenticate with emergency codes.", level="info")
        return render_to_response("login_frontend/no_emergency_available.html", {}, context_instance=RequestContext(request))
    if not codes.valid():
        ret["no_codes_available"] = True
        custom_log(request, "No codes available. Can't authenticate with emergency codes.", level="info")
        return render_to_response("login_frontend/no_emergency_available.html", {}, context_instance=RequestContext(request))

    if request.method == 'POST':
        otp = request.POST.get("otp", "")
        if otp:
            # whitespace is not important, but printed passwords include spaces for readability.
            otp = otp.replace(" ", "")
        if is_ratelimited(request, True, True, ["POST"], None, "30/30s", [request.user.username], "30s_emergency"):
            ret["ratelimited"] = True
            ret["ratelimit_wait_until"] = timezone.now() + datetime.timedelta(seconds=120)
            custom_log(request, "2f-emergency: ratelimited", level="warn")
        elif codes.use_code(otp):
            # Proper code was provided.
            familiar_device = request.browser.user_is_familiar(request.browser.user, Browser.L_STRONG)
            emergency_used_notify(request, codes, familiar_device=familiar_device)
            custom_log(request, "Authenticated with emergency code", level="info")
            add_user_log(request, "Second-factor authentication with emergency code succeeded.", "lock")
            request.browser.save_browser = False # emergency codes are only for temporary authentication
            request.browser.set_auth_level(Browser.L_STRONG)
            request.browser.set_auth_state(Browser.S_AUTHENTICATED)
            return redir_to_sso(request)
        else:
            if re.match("^[0-9]{5,7}$", otp):
                custom_log(request, "Tried to enter SMS/authenticator code", level="info")
                ret["twostep_code"] = True
            if re.match("^[0-9]{8}$", otp):
                custom_log(request, "Tried to use Google apps emergency code", level="info")
                ret["gapps_code"] = True
            custom_log(request, "Incorrect emergency code", level="info")
            ret["invalid_otp"] = True

    ret["generated_at"] = str(codes.generated_at)
    ret["should_timesync"] = request.browser.should_timesync()
    ret["get_params"] = urllib.urlencode(get_params)
    ret["emergency_code_id"] = codes.current_code.code_id
    return render_to_response("login_frontend/authenticate_with_emergency_code.html", ret, context_instance=RequestContext(request))
Example #13
0
def new_thread(request, forum_slug):
    """Start a new thread."""
    forum = get_object_or_404(Forum, slug=forum_slug)
    user = request.user
    if not forum.allows_posting_by(user):
        if forum.allows_viewing_by(user):
            raise PermissionDenied
        else:
            raise Http404

    if request.method == 'GET':
        form = NewThreadForm()
        return render(request, 'forums/new_thread.html', {
            'form': form, 'forum': forum})

    form = NewThreadForm(request.POST)
    post_preview = None
    if form.is_valid():
        if 'preview' in request.POST:
            thread = Thread(creator=request.user,
                            title=form.cleaned_data['title'])
            post_preview = Post(thread=thread, author=request.user,
                                content=form.cleaned_data['content'])
            post_preview.author_post_count = \
                post_preview.author.post_set.count()
        elif (_skip_post_ratelimit(request) or
              not is_ratelimited(request, increment=True, rate='5/d', ip=False,
                                 keys=user_or_ip('forum-post'))):
            thread = forum.thread_set.create(creator=request.user,
                                             title=form.cleaned_data['title'])
            thread.save()
            statsd.incr('forums.thread')
            post = thread.new_post(author=request.user,
                                   content=form.cleaned_data['content'])
            post.save()

            NewThreadEvent(post).fire(exclude=post.author)

            # Add notification automatically if needed.
            if Setting.get_for_user(request.user, 'forums_watch_new_thread'):
                NewPostEvent.notify(request.user, thread)

            url = reverse('forums.posts', args=[forum_slug, thread.id])
            return HttpResponseRedirect(urlparams(url, last=post.id))

    return render(request, 'forums/new_thread.html', {
        'form': form, 'forum': forum,
        'post_preview': post_preview})
Example #14
0
def reply(request, forum_slug, thread_id):
    """Reply to a thread."""
    forum = get_object_or_404(Forum, slug=forum_slug)
    user = request.user
    if not forum.allows_posting_by(user):
        if forum.allows_viewing_by(user):
            raise PermissionDenied
        else:
            raise Http404

    form = ReplyForm(request.POST)
    post_preview = None
    if form.is_valid():
        thread = get_object_or_404(Thread, pk=thread_id, forum=forum)

        if not thread.is_locked:
            reply_ = form.save(commit=False)
            reply_.thread = thread
            reply_.author = request.user
            if 'preview' in request.POST:
                post_preview = reply_
                post_preview.author_post_count = \
                    reply_.author.post_set.count()
            elif (_skip_post_ratelimit(request)
                  or not is_ratelimited(request,
                                        increment=True,
                                        rate='15/d',
                                        ip=False,
                                        keys=user_or_ip('forum-post'))):
                reply_.save()
                statsd.incr('forums.reply')

                # Subscribe the user to the thread.
                if Setting.get_for_user(request.user,
                                        'forums_watch_after_reply'):
                    NewPostEvent.notify(request.user, thread)

                # Send notifications to thread/forum watchers.
                NewPostEvent(reply_).fire(exclude=reply_.author)

                return HttpResponseRedirect(thread.get_last_post_url())

    return posts(request,
                 forum_slug,
                 thread_id,
                 form,
                 post_preview,
                 is_reply=True)
Example #15
0
    def allow_request(self, request, view):
        ratelimited = is_ratelimited(request=request,
                                     increment=True,
                                     ip=False,
                                     method=self.methods,
                                     field=None,
                                     rate=self.rate,
                                     keys=self.keyfun)

        if ratelimited:
            # Failed rate-limiting, so this request is not allowed.
            statsd.incr('throttled.' + self.rulename)
            return self.throttle_failure()

        # Did not trigger rate-limiting, so this request is allowed.
        return self.throttle_success()
Example #16
0
def reply(request, forum_slug, thread_id):
    """Reply to a thread."""
    forum = get_object_or_404(Forum, slug=forum_slug)
    user = request.user
    if not forum.allows_posting_by(user):
        if forum.allows_viewing_by(user):
            raise PermissionDenied
        else:
            raise Http404

    form = ReplyForm(request.POST)
    post_preview = None
    if form.is_valid():
        thread = get_object_or_404(Thread, pk=thread_id, forum=forum)

        if not thread.is_locked:
            reply_ = form.save(commit=False)
            reply_.thread = thread
            reply_.author = request.user
            if 'preview' in request.POST:
                post_preview = reply_
                post_preview.author_post_count = \
                    reply_.author.post_set.count()
            elif (_skip_post_ratelimit(request) or
                  not is_ratelimited(request, increment=True, rate='5/d',
                                     ip=False,
                                     keys=user_or_ip('forum-post'))):
                reply_.save()
                statsd.incr('forums.reply')

                # Subscribe the user to the thread.
                if Setting.get_for_user(request.user,
                                        'forums_watch_after_reply'):
                    NewPostEvent.notify(request.user, thread)

                # Send notifications to thread/forum watchers.
                NewPostEvent(reply_).fire(exclude=reply_.author)

                return HttpResponseRedirect(thread.get_last_post_url())

    return posts(request, forum_slug, thread_id, form, post_preview,
                 is_reply=True)
Example #17
0
 def do_increment(request):
     return is_ratelimited(request, increment=True, ip=False,
                           method=None, keys=[get_keys], rate='1/m')
def authenticate_with_url(request, **kwargs):
    """ Authenticates user with URL sent via SMS """
    def sid_cleanup(sid):
        keys = ["urlauth-%s-%s" % (k, sid) for k in ("params", "user", "bid")]
        dcache.delete(keys)

    if is_ratelimited(request, True, True, ["POST"], None, "10/30s", [request.user.username], "30s_url"):
        ret["ratelimited"] = True
        ret["ratelimit_wait_until"] = timezone.now() + datetime.timedelta(seconds=120)
        custom_log(request, "2f-url: ratelimited")
        return render_to_response(template_name, ret, context_instance=RequestContext(request))

    template_name = "login_frontend/authenticate_with_url.html"
    sid = kwargs.get("sid")
    ret = {}
    if not hasattr(request, "browser") or not request.browser or not request.browser.user:
        custom_log(request, "2f-url: No browser object / no signed-in user", level="warn")
        ret["invalid_request"] = True
        return render_to_response(template_name, ret, context_instance=RequestContext(request))

    if not dcache.get("urlauth-params-%s" % sid):
        custom_log(request, "2f-url: sid does not exist, or it expired.", level="warn")
        ret["invalid_sid"] = True
        return render_to_response(template_name, ret, context_instance=RequestContext(request))

    username = dcache.get("urlauth-user-%s" % sid)
    if username != request.browser.user.username:
        custom_log(request, "2f-url: Tried to access SID that belongs to another user.", level="warn")
        ret["invalid_request"] = True
        return render_to_response(template_name, ret, context_instance=RequestContext(request))

    bid_public = dcache.get("urlauth-bid-%s" % sid)
    if bid_public != request.browser.bid_public:
        custom_log(request, "2f-url: Tried to access SID with wrong browser. Probably the phone opens SMS links to different browser, or it was actually another phone.", level="warn")
        ret["wrong_browser"] = True
        return render_to_response(template_name, ret, context_instance=RequestContext(request))

    get_params = dcache.get("urlauth-params-%s" % sid)
    try:
        get_params_dict = json.loads(get_params)
    except (ValueError, EOFError):
        custom_log(request, "2f-url: Invalid get_params json from cache", level="warn")
        ret["invalid_request"] = True
        return render_to_response(template_name, ret, context_instance=RequestContext(request))

    if request.browser.is_authenticated():
        custom_log(request, "2f-url: User is already signed in. Redirect to secondstepauth: %s" % get_params, level="info")
        sid_cleanup(sid)
        request.browser.revoke_sms()
        return redirect_with_get_params("login_frontend.authentication_views.secondstepauth", get_params_dict)

    if not request.browser.get_auth_level() >= Browser.L_BASIC or not request.browser.get_auth_state() == Browser.S_REQUEST_STRONG:
        custom_log(request, "2f-url: Browser is in wrong authentication state", level="warn")
        ret["invalid_auth_state"] = True
        return render_to_response(template_name, ret, context_instance=RequestContext(request))

    # Everything is fine:
    # - sid is valid
    # - browser matches
    # - user is authenticated
    # set authentication state and redirect through secondstepauth.
    # TODO: determine these automatically
    request.browser.set_auth_level(Browser.L_STRONG)
    request.browser.set_auth_state(Browser.S_AUTHENTICATED)
    sid_cleanup(sid)
    request.browser.revoke_sms()
    custom_log(request, "2f-url: Successfully authenticated with URL. Redirecting to secondstepauth", level="info")
    add_user_log(request, "Successfully authenticated with URL.", "lock")
    return redirect_with_get_params("login_frontend.authentication_views.secondstepauth", get_params_dict)
def authenticate_with_authenticator(request):
    """ Authenticates user with Google Authenticator """

    custom_log(request, "2f-auth: Requested authentication with Authenticator", level="debug")

    # If already authenticated with L_STRONG, redirect back to SSO / frontpage
    if request.browser.is_authenticated():
        custom_log(request, "2f-auth: User is already authenticated. Redirect back to SSO", level="info")
        return redir_to_sso(request)

    ret = {}
    user = request.browser.user
    assert user != None, "Browser is authenticated but no User object exists."

    skips_available = user.strong_skips_available
    ret["skips_available"] = skips_available

    if not user.strong_authenticator_secret:
        # Authenticator is not configured. Redirect back to secondstep main screen
        custom_log(request, "2f-auth: Authenticator is not configured, but user accessed Authenticator view. Redirect back to secondstepauth", level="error")
        messages.warning(request, "You tried to authenticate with Authenticator. However, according to our records, you don't have it configured. Please sign in and go to settings to do that.")
        return redirect_with_get_params("login_frontend.authentication_views.secondstepauth", request.GET)

    if not user.strong_authenticator_used:
        custom_log(request, "2f-auth: Authenticator has not been used. Generated at %s" % user.strong_authenticator_generated_at, level="debug")
        ret["authenticator_not_used"] = True
        ret["authenticator_generated"] = user.strong_authenticator_generated_at

    emergency_codes = user.get_emergency_codes()
    if emergency_codes and emergency_codes.valid():
        ret["can_use_emergency"] = True

    if request.method == "POST" and request.POST.get("skip"):
        if skips_available > 0:
            user.strong_skips_available -= 1
            user.save()
            add_user_log(request, "Skipped strong authentication: %s left" % user.strong_skips_available, "meh-o")
            custom_log(request, "2f-auth: Skipped strong authentication: %s left" % user.strong_skips_available)
            # TODO: determine the levels automatically.
            request.browser.set_auth_level(Browser.L_STRONG_SKIPPED)
            request.browser.set_auth_state(Browser.S_AUTHENTICATED)
            request.browser.set_auth_level_valid_until = timezone.now() + datetime.timedelta(hours=12)
            request.browser.save()
            custom_log(request, "2f-auth: Redirecting back to SSO provider", level="debug")
            return redir_to_sso(request)
        else:
            messages.warning(request, "You can't skip strong authentication anymore.")
            custom_log(request, "2f-auth: Tried to skip strong authentication with no skips available", level="warn")

    if request.browser.name:
        ret["browser_name"] = True

    if request.method == "POST" and not request.session.test_cookie_worked():
        custom_log(request, "2f-auth: cookies do not work properly", level="warn")
        ret["enable_cookies"] = True

    elif request.method == "POST":
        browser_name = request.POST.get("name")
        ret["browser_name_value"] = browser_name
        request.session.delete_test_cookie()

        custom_log(request, "2f-auth: POST request", level="debug")
        if is_ratelimited(request, True, False, ["POST"], None, "30/30s", [request.user.username], "30s_2f"):
            ret["ratelimited"] = True
            ret["ratelimit_wait_until"] = timezone.now() + datetime.timedelta(seconds=120)
            custom_log(request, "2f-auth: ratelimited", level="warn")
        elif request.POST.get("otp"):
            custom_log(request, "2f-auth: Form is valid", level="debug")
            otp = request.POST.get("otp")
            otp = otp.replace(" ", "") # spaces doesn't matter
            custom_log(request, "2f-auth: Testing OTP code %s at %s" % (otp, time.time()), level="debug")
            (status, message) = user.validate_authenticator_code(otp, request)

            save_browser = False
            if request.POST.get("my_computer"):
                save_browser = True
            if request.browser.save_browser != save_browser:
                request.browser.save_browser = save_browser
                request.browser.save()
                if save_browser:
                    custom_log(request, "2f-auth: Marked browser as remembered", level="info")
                    add_user_log(request, "Marked browser as remembered", "eye")
                else:
                    custom_log(request, "2f-auth: Marked browser as not remembered", level="info")
                    add_user_log(request, "Marked browser as not remembered", "eye-slash")

            if not status:
                # If authenticator code did not match, also try latest SMS (if available).
                custom_log(request, "2f-auth: Authenticator code did not match. Testing SMS", level="info")
                status, _ = request.browser.validate_sms(otp)

            if not status:
                # If neither SMS or Authenticator matched, test Yubikey.
                custom_log(request, "2f-auth: OTP from SMS/Authenticator did not match. Try Yubikey.", level="info")
                (status, yubikey_message) = validate_yubikey(user, otp)
                if yubikey_message:
                    message = yubikey_message

            if status:
                request.browser.twostep_last_entered_at = timezone.now()
                if browser_name and browser_name != request.browser.name:
                    request.browser.name = browser_name

                new_device_notify(request, "authenticator")

                custom_log(request, "2f-auth: Second-factor authentication with Authenticator succeeded")
                add_user_log(request, "Second-factor authentication with Authenticator succeeded", "lock")
                # Mark authenticator configuration as valid. User might have configured
                # authenticator but aborted without entering validation code.
                user.strong_authenticator_used = True
                user.strong_configured = True
                user.save()

                if request.POST.get("timing_data"):
                    custom_log(request, "2f-auth: Saving timing data", level="debug")
                    timing_data = request.POST.get("timing_data")
                    save_timing_data(request, user, timing_data)


                # TODO: determine the levels automatically.
                request.browser.set_auth_level(Browser.L_STRONG)
                request.browser.set_auth_state(Browser.S_AUTHENTICATED)

                if request.browser.name:
                    request.browser.auth_state_changed()
                    custom_log(request, "2f-auth: Redirecting back to SSO provider", level="debug")
                    return redir_to_sso(request)
                else:
                    custom_log(request, "2f-auth: Browser name is not set. Redirect to naming view", level="debug")
                    # Don't send auth_state_changed(), as it would redirect all browser windows to name form
                    get_params = request.GET.dict()
                    get_params["_sc"] = "on"
                    return redirect_with_get_params("login_frontend.views.name_your_browser", get_params)
            else:
                custom_log(request, "2f-auth: Incorrect Authenticator OTP provided: %s" % message, level="warn")
                add_user_log(request, "Incorrect Authenticator OTP provided: %s" % message, "warning")
                if not re.match("^[0-9]{5,6}$", otp):
                    ret["is_invalid_otp"] = True
                ret["invalid_otp"] = message
        else:
            custom_log(request, "2f-auth: form was not valid", level="debug")
            messages.warning(request, "One-time password field is mandatory.")
    else:
        custom_log(request, "2f-auth: GET request", level="debug")

    ret["user"] = user
    ret["authenticator_id"] = user.get_authenticator_id()
    ret["get_params"] = urllib.urlencode(request.GET)
    ret["my_computer"] = request.browser.save_browser
    ret["should_timesync"] = request.browser.should_timesync()
    request.session.set_test_cookie()

    response = render_to_response("login_frontend/authenticate_with_authenticator.html", ret, context_instance=RequestContext(request))
    return response
def authenticate_with_sms(request):
    """ Authenticate user with SMS.
    Accepts Authenticator codes too.
    """
    # If already authenticated with L_STRONG, redirect back to SSO / frontpage
    if request.browser.is_authenticated():
        custom_log(request, "2f-sms: User is already authenticated. Redirect back to SSO service", level="debug")
        return redir_to_sso(request)

    custom_log(request, "2f-sms: authenticate_with_sms", level="debug")

    user = request.browser.user
    ret = {}
    if not (user.primary_phone or user.secondary_phone):
        # Phone numbers are not available.
        custom_log(request, "2f-sms: No phone number available - unable to authenticate.", level="error")
        ret["should_timesync"] = request.browser.should_timesync()
        return render_to_response("login_frontend/no_phone_available.html", ret, context_instance=RequestContext(request))

    skips_available = user.strong_skips_available
    ret["skips_available"] = skips_available

    if request.method == "POST" and request.POST.get("skip"):
        if skips_available > 0:
            user.strong_skips_available -= 1
            user.save()
            add_user_log(request, "Skipped strong authentication: %s left" % user.strong_skips_available, "meh-o")
            custom_log(request, "2f-sms: Skipped strong authentication: %s left" % user.strong_skips_available)
            # TODO: determine the levels automatically.
            request.browser.set_auth_level(Browser.L_STRONG)
            request.browser.set_auth_state(Browser.S_AUTHENTICATED)
            request.browser.set_auth_level_valid_until = timezone.now() + datetime.timedelta(hours=12)
            request.browser.save()
            request.browser.auth_state_changed()
            custom_log(request, "2f-sms: Redirecting back to SSO provider", level="debug")
            return redir_to_sso(request)
        else:
            messages.warning(request, "You can't skip strong authentication anymore.")
            custom_log(request, "2f-sms: Tried to skip strong authentication with no skips available", level="warn")

    emergency_codes = user.get_emergency_codes()
    if emergency_codes and emergency_codes.valid():
        ret["can_use_emergency"] = True

    if user.strong_configured:
        if user.strong_authenticator_secret:
            ret["can_use_authenticator"] = True
            if not user.strong_authenticator_used:
                ret["authenticator_generated"] = True
    else:
        custom_log(request, "2f-sms: Strong authentication is not configured yet.", level="debug")
        # No strong authentication is configured.
        ret["strong_not_configured"] = True
        if user.strong_authenticator_secret:
            ret["authenticator_generated"] = True
            ret["can_use_authenticator"] = True


    if user.primary_phone_changed:
        custom_log(request, "2f-sms: Phone number has changed.", level="debug")
        # Phone number changed. For security reasons...
        ret["primary_phone_changed"] = True

    if request.browser.name:
        ret["browser_name"] = True

    if request.method == "POST":
        custom_log(request, "2f-sms: POST request", level="debug")
        browser_name = request.POST.get("name")
        ret["browser_name_value"] = browser_name
        if is_ratelimited(request, True, True, ["POST"], None, "60/15m", [request.user.username], "30s_sms"):
            ret["ratelimited"] = True
            ret["ratelimit_wait_until"] = timezone.now() + datetime.timedelta(seconds=900)
            custom_log(request, "2f-sms: ratelimited", level="warn")

        elif request.POST.get("otp"):
            custom_log(request, "2f-sms: Form is valid", level="debug")
            otp = request.POST.get("otp")
            otp = otp.replace(" ", "") # spaces doesn't matter
            status, message = request.browser.validate_sms(otp)

            save_browser = False
            if request.POST.get("my_computer"):
                save_browser = True
            if request.browser.save_browser != save_browser:
                request.browser.save_browser = save_browser
                request.browser.save()
                if save_browser:
                    custom_log(request, "2f-sms: Marked browser as remembered", level="info")
                    add_user_log(request, "Marked browser as remembered", "eye")
                else:
                    custom_log(request, "2f-sms: Marked browser as not remembered", level="info")
                    add_user_log(request, "Marked browser as not remembered", "eye-slash")

            if not status:
                # If OTP from SMS did not match, also test for Authenticator OTP.
                custom_log(request, "2f-sms: OTP from SMS did not match, testing Authenticator", level="info")
                (status, _) = request.browser.user.validate_authenticator_code(otp, request)

            if not status:
                # If neither SMS or Authenticator matched, test Yubikey.
                custom_log(request, "2f-sms: OTP from SMS/Authenticator did not match. Try Yubikey.", level="info")
                (status, yubikey_message) = validate_yubikey(user, otp)
                if yubikey_message:
                    message = yubikey_message

            if status:
                request.browser.twostep_last_entered_at = timezone.now()
                if browser_name and browser_name != request.browser.name:
                    request.browser.name = browser_name

                new_device_notify(request, "sms")

                # Authentication succeeded.
                custom_log(request, "2f-sms: Second-factor authentication with SMS succeeded")
                add_user_log(request, "Second-factor authentication with SMS succeeded", "lock")
                # TODO: determine the levels automatically.
                request.browser.set_auth_level(Browser.L_STRONG)
                request.browser.set_auth_state(Browser.S_AUTHENTICATED)
                if user.primary_phone_changed:
                    user.primary_phone_changed = False
                    user.save()

                if request.POST.get("timing_data"):
                    custom_log(request, "2f-sms: Saved timing data", level="debug")
                    timing_data = request.POST.get("timing_data")
                    save_timing_data(request, user, timing_data)


                if not user.strong_configured:
                    # Strong authentication is not configured. Go to configuration view.
                    custom_log(request, "2f-sms: User has not configured strong authentication. Redirect to configuration view", level="info")
                    return redirect_with_get_params("login_frontend.views.configure", request.GET)
                # Redirect back to SSO service
                if request.browser.name:
                    request.browser.auth_state_changed()
                    custom_log(request, "2f-sms: Redirecting back to SSO provider", level="debug")
                    return redir_to_sso(request)
                else:
                    custom_log(request, "2f-sms: Browser name is not set. Redirect to naming view", level="debug")
                    # Don't send auth_state_changed(), as it would redirect all browser windows to name form
                    get_params = request.GET.dict()
                    get_params["_sc"] = "on"
                    return redirect_with_get_params("login_frontend.views.name_your_browser", get_params)
            else:
                if message:
                    ret["message"] = message
                    custom_log(request, "2f-sms: SMS OTP login failed: %s" % message, level="warn")
                    add_user_log(request, "SMS OTP login failed: %s" % message, "warning")
                else:
                    custom_log(request, "2f-sms: Incorrect SMS OTP", level="warn")
                    add_user_log(request, "Incorrect SMS OTP", "warning")
                    ret["authentication_failed"] = True
        else:
            messages.warning(request, "Invalid input")
    else:
        custom_log(request, "2f-sms: GET request", level="debug")

    if not request.browser.valid_sms_exists(180) or request.POST.get("regen_sms"):
        custom_log(request, "2f-sms: Generating a new SMS code", level="info")
        sms_text = request.browser.generate_sms_text(request=request)
        for phone in (user.primary_phone, user.secondary_phone):
            if phone:
                status = send_sms(phone, sms_text)
                if not status:
                    messages.warning(request, "Sending SMS to %s failed." % phone)
                    custom_log(request, "2f-sms: Sending SMS to %s failed" % phone, level="warn")
                    add_user_log(request, "Sending SMS to %s failed" % phone)
                else:
                    custom_log(request, "2f-sms: Sent OTP to %s" % phone)
                    add_user_log(request, "Sent OTP code to %s" % phone, "info")
                    phone_redacted = "%s...%s" % (phone[0:6], phone[-4:])
                    messages.info(request, mark_safe("Sent SMS to <span class='tooltip-link' title='This is redacted to protect your privacy'>%s</span>" % phone_redacted))
        if request.method == "POST":
            # Redirect to avoid duplicate SMSes on reload.
            return redirect_with_get_params("login_frontend.authentication_views.authenticate_with_sms", request.GET)

    ret["sms_valid_until"] = request.browser.sms_code_generated_at + datetime.timedelta(seconds=900)
    ret["expected_sms_id"] = request.browser.sms_code_id
    ret["get_params"] = urllib.urlencode(request.GET)
    ret["my_computer"] = request.browser.save_browser
    ret["should_timesync"] = request.browser.should_timesync()

    response = render_to_response("login_frontend/authenticate_with_sms.html", ret, context_instance=RequestContext(request))
    return response