Exemple #1
0
    def put(self, request, **kwargs):
        token = request.data.get('token')
        password = request.data.get('password')

        matches = re.search(r'([0-9A-Za-z]+)-(.*)', token)
        if not matches:
            return Response(status=HTTP_404_NOT_FOUND)
        uidb36 = matches.group(1)
        key = matches.group(2)
        if not (uidb36 and key):
            return Response(status=HTTP_404_NOT_FOUND)

        user = self._get_user(uidb36)
        if user is None:
            return Response(status=HTTP_404_NOT_FOUND)

        if not default_token_generator.check_token(user, key):
            return Response(status=HTTP_404_NOT_FOUND)

        try:
            validate_password(password)
        except DjangoValidationError as e:
            return Response(dict(password=e.messages), status=HTTP_400_BAD_REQUEST)

        cache.set(backoff_cache_key(user.email, None), None)

        user.set_password(password)
        user.save()
        return self.get_response()
Exemple #2
0
    def put(self, request, **kwargs):
        token = request.data.get('token')
        password = request.data.get('password')

        matches = re.search(r'([0-9A-Za-z]+)-(.*)', token)
        if not matches:
            return Response(status=HTTP_404_NOT_FOUND)
        uidb36 = matches.group(1)
        key = matches.group(2)
        if not (uidb36 and key):
            return Response(status=HTTP_404_NOT_FOUND)

        user = self._get_user(uidb36)
        if user is None:
            return Response(status=HTTP_404_NOT_FOUND)

        if not default_token_generator.check_token(user, key):
            return Response(status=HTTP_404_NOT_FOUND)

        try:
            validate_password(password)
        except DjangoValidationError as e:
            return Response(dict(password=e.messages), status=HTTP_400_BAD_REQUEST)

        cache.set(backoff_cache_key(user.email, None), None)

        user.set_password(password)
        user.save()
        return self.get_response()
Exemple #3
0
    def test_user_can_request_forgot_password(self):
        self.client.logout()
        cache.clear()

        # dont send recovery to unknown emails
        response = self.client.post('/v1/user/forgot-password', {
            'email': '*****@*****.**',
        })
        self.assertEqual(response.status_code, 200, "Does not leak information about account emails")
        self.assertEqual(len(mail.outbox), 0)

        # successfully send recovery email
        response = self.client.post('/v1/user/forgot-password', {
            'email': self.first_user_email
        })

        backoff_key = backoff_cache_key(self.first_user_email)
        backoff_data = {'127.0.0.1': {'count': 6, 'until': timezone.now() + timezone.timedelta(seconds=60)}}
        cache.set(backoff_key, backoff_data)
        self.assertEqual(cache.get(backoff_key), backoff_data)

        self.assertEqual(response.status_code, 200)
        # received email with recovery url
        self.assertEqual(len(mail.outbox), 1)
        matches = re.search(r'/v1/user/forgot-password\?token=([-0-9a-zA-Z]*)', mail.outbox[0].body)
        self.assertIsNotNone(matches)
        token = matches.group(1)
        new_password = '******'

        # able to use token received in email to reset password
        response = self.client.put('/v1/user/forgot-password', {
            'token': token,
            'password': new_password
        })
        self.assertEqual(response.status_code, 200)

        backoff_data = cache.get(backoff_key)
        self.assertIsNone(backoff_data)

        application = Application.objects.create(
            client_id='public',
            client_secret='',
            user=None,
            authorization_grant_type=Application.GRANT_PASSWORD,
            name='public'
        )
        ApplicationInfo.objects.create(
            application=application,
            allowed_scopes='rw:issuer rw:backpack rw:profile'
        )

        response = self.client.post('/o/token', {
            'username': self.first_user.email,
            'password': new_password,
        })
        self.assertEqual(response.status_code, 200)
Exemple #4
0
 def login_backoff(self, obj):
     cache_key = backoff_cache_key(obj.client_id)
     backoff = cache.get(cache_key)
     if backoff is not None:
         backoff_data = "</li><li>".join(["{ip}: {until} ({count} attempts)".format(
             ip=key,
             until=backoff[key].get('until').astimezone(timezone.get_current_timezone()).strftime("%Y-%m-%d %H:%M:%S"),
             count=backoff[key].get('count')
         ) for key in backoff.keys()])
         return format_html("<ul><li>{}</li></ul>".format(backoff_data))
     return "None"
Exemple #5
0
    def test_can_reset_failed_login_backoff(self):
        """
        This test is time sensitive down to a second or two. You may get unexpected failures if you run in a debugger
        and pause at certain times, allowing throttle backoffs of 2sec to expire.
        """
        cache.clear()
        password = '******'
        user = self.setup_user(authenticate=False,
                               password=password,
                               email='*****@*****.**')
        backoff_key = backoff_cache_key(user.email)
        application = Application.objects.create(
            client_id='public',
            client_secret='',
            user=None,
            authorization_grant_type=Application.GRANT_PASSWORD,
            name='public')
        ApplicationInfo.objects.create(
            application=application,
            allowed_scopes='rw:issuer rw:backpack rw:profile')

        post_data = {'username': user.email, 'password': password}
        response = self.client.post('/o/token', data=post_data)
        self.assertEqual(response.status_code, 200)
        backoff_data = cache.get(backoff_key)
        self.assertIsNone(backoff_data)

        post_data['password'] = '******'
        response = self.client.post('/o/token', data=post_data)
        self.assertEqual(response.status_code, 400)
        backoff_data = cache.get(backoff_key)['127.0.0.1']
        self.assertEqual(backoff_data['count'], 1)
        backoff_time = backoff_data['until']

        post_data['password'] = password  # Now try the correct password
        response = self.client.post('/o/token', data=post_data)
        self.assertEqual(response.status_code, 429)
        backoff_data = cache.get(backoff_key)['127.0.0.1']
        self.assertEqual(
            backoff_data['count'], 1,
            "Count does not increase if correct password sent too soon")
        self.assertGreaterEqual(backoff_data['until'], backoff_time,
                                "backoff time should not increase.")

        backoff_data['until'] = backoff_time - timezone.timedelta(
            seconds=3)  # reset to a time in the past
        cache.set(backoff_key, {'127.0.0.1': backoff_data})

        response = self.client.post('/o/token', data=post_data)
        self.assertEqual(response.status_code, 200)
        backoff_data = cache.get(backoff_key)
        self.assertIsNone(backoff_data)
Exemple #6
0
 def login_backoff(self, obj):
     out = []
     for email in obj.all_verified_recipient_identifiers:
         cache_key = backoff_cache_key(username=email)
         backoff = cache.get(cache_key)
         if backoff is not None:
             out.append("<div><strong>{email}</strong>: <span>{until}</span> <span>({count} attempts)</span></div>".format(
                 email=email,
                 until=backoff.get('until').astimezone(timezone.get_current_timezone()).strftime("%Y-%m-%d %H:%M:%S"),
                 count=backoff.get('count')
             ))
     if len(out):
         return "".join(out)
Exemple #7
0
 def login_backoff(self, obj):
     out = []
     for email in obj.all_recipient_identifiers:
         cache_key = backoff_cache_key(username=email)
         backoff = cache.get(cache_key)
         if backoff is not None:
             out.append("<div><strong>{email}</strong>: <span>{until}</span> <span>({count} attempts)</span></div>".format(
                 email=email,
                 until=backoff.get('until').astimezone(timezone.get_current_timezone()).strftime("%Y-%m-%d %H:%M:%S"),
                 count=backoff.get('count')
             ))
     if len(out):
         return "".join(out)
Exemple #8
0
 def login_backoff(self, obj):
     blocks = []
     for email in obj.all_verified_recipient_identifiers:
         cache_key = backoff_cache_key(email)
         backoff = cache.get(cache_key)
         if backoff is not None:
             blocks += ["{email} - {ip}: {until} ({count} attempts)".format(
                     email=email, ip=key,
                     until=backoff[key].get('until').astimezone(timezone.get_current_timezone()).strftime("%Y-%m-%d %H:%M:%S"),
                     count=backoff[key].get('count')
                 ) for key in backoff.keys()]
     if len(blocks):
         return format_html("<ul><li>{}</li></ul>".format("</li><li>".join(blocks)))
     return "None"
Exemple #9
0
    def test_can_reset_failed_login_backoff(self):
        cache.clear()
        password = '******'
        user = self.setup_user(authenticate=False,
                               password=password,
                               email='*****@*****.**')
        backoff_key = backoff_cache_key(user.email, None)
        application = Application.objects.create(
            client_id='public',
            client_secret='',
            user=None,
            authorization_grant_type=Application.GRANT_PASSWORD,
            name='public')
        ApplicationInfo.objects.create(
            application=application,
            allowed_scopes='rw:issuer rw:backpack rw:profile')

        post_data = {'username': user.email, 'password': password}
        response = self.client.post('/o/token', data=post_data)
        self.assertEqual(response.status_code, 200)
        backoff_data = cache.get(backoff_key)
        self.assertIsNone(backoff_data)

        post_data['password'] = '******'
        response = self.client.post('/o/token', data=post_data)
        self.assertEqual(response.status_code, 401)
        backoff_data = cache.get(backoff_key)
        self.assertEqual(backoff_data['count'], 1)
        backoff_time = backoff_data['until']

        post_data['password'] = password
        response = self.client.post('/o/token', data=post_data)
        self.assertEqual(response.status_code, 401)
        backoff_data = cache.get(backoff_key)
        self.assertEqual(
            backoff_data['count'], 2,
            "Count increases even if sent too soon even if password is right")
        self.assertGreaterEqual(
            backoff_data['until'],
            backoff_time + timezone.timedelta(seconds=2),
            "backoff time should increase by at least two seconds")

        backoff_data['until'] = backoff_time - timezone.timedelta(
            seconds=3)  # reset to a time in the past
        cache.set(backoff_key, backoff_data)

        response = self.client.post('/o/token', data=post_data)
        self.assertEqual(response.status_code, 200)
        backoff_data = cache.get(backoff_key)
        self.assertIsNone(backoff_data)
Exemple #10
0
    def test_user_can_request_forgot_password(self):
        self.client.logout()
        cache.clear()

        # dont send recovery to unknown emails
        response = self.client.post('/v1/user/forgot-password', {
            'email': '*****@*****.**',
        })
        self.assertEqual(response.status_code, 200,
                         "Does not leak information about account emails")
        self.assertEqual(len(mail.outbox), 0)

        with self.settings(BADGR_APP_ID=self.badgr_app.id):
            # successfully send recovery email
            response = self.client.post('/v1/user/forgot-password',
                                        {'email': self.first_user_email})

            backoff_key = backoff_cache_key(self.first_user_email, None)
            backoff_data = {
                'count': 6,
                'until': timezone.now() + timezone.timedelta(seconds=60)
            }
            cache.set(backoff_key, backoff_data)
            self.assertEqual(cache.get(backoff_key), backoff_data)

            self.assertEqual(response.status_code, 200)
            # received email with recovery url
            self.assertEqual(len(mail.outbox), 1)
            matches = re.search(
                r'/v1/user/forgot-password\?token=([-0-9a-zA-Z]*)',
                mail.outbox[0].body)
            self.assertIsNotNone(matches)
            token = matches.group(1)
            new_password = '******'

            # able to use token received in email to reset password
            response = self.client.put('/v1/user/forgot-password', {
                'token': token,
                'password': new_password
            })
            self.assertEqual(response.status_code, 200)

            backoff_data = cache.get(backoff_key)
            self.assertIsNone(backoff_data)

            response = self.client.post('/api-auth/token', {
                'username': self.first_user.username,
                'password': new_password,
            })
            self.assertEqual(response.status_code, 200)
    def test_can_reset_failed_login_backoff(self):
        cache.clear()
        password = '******'
        user = self.setup_user(authenticate=False, password=password, email='*****@*****.**')
        backoff_key = backoff_cache_key(user.email, None)
        application = Application.objects.create(
            client_id='public',
            client_secret='',
            user=None,
            authorization_grant_type=Application.GRANT_PASSWORD,
            name='public'
        )
        ApplicationInfo.objects.create(
            application=application,
            allowed_scopes='rw:issuer rw:backpack rw:profile'
        )

        post_data = {
            'username': user.email,
            'password': password
        }
        response = self.client.post('/o/token', data=post_data)
        self.assertEqual(response.status_code, 200)
        backoff_data = cache.get(backoff_key)
        self.assertIsNone(backoff_data)

        post_data['password'] = '******'
        response = self.client.post('/o/token', data=post_data)
        self.assertEqual(response.status_code, 401)
        backoff_data = cache.get(backoff_key)
        self.assertEqual(backoff_data['count'], 1)
        backoff_time = backoff_data['until']

        post_data['password'] = password
        response = self.client.post('/o/token', data=post_data)
        self.assertEqual(response.status_code, 401)
        backoff_data = cache.get(backoff_key)
        self.assertEqual(backoff_data['count'], 2, "Count increases even if sent too soon even if password is right")
        self.assertGreaterEqual(backoff_data['until'], backoff_time + timezone.timedelta(seconds=2),
                                "backoff time should increase by at least two seconds")

        backoff_data['until'] = backoff_time - timezone.timedelta(seconds=3)  # reset to a time in the past
        cache.set(backoff_key, backoff_data)

        response = self.client.post('/o/token', data=post_data)
        self.assertEqual(response.status_code, 200)
        backoff_data = cache.get(backoff_key)
        self.assertIsNone(backoff_data)
Exemple #12
0
    def test_user_can_request_forgot_password(self):
        self.client.logout()
        cache.clear()

        # dont send recovery to unknown emails
        response = self.client.post('/v1/user/forgot-password', {
            'email': '*****@*****.**',
        })
        self.assertEqual(response.status_code, 200, "Does not leak information about account emails")
        self.assertEqual(len(mail.outbox), 0)

        with self.settings(BADGR_APP_ID=self.badgr_app.id):
            # successfully send recovery email
            response = self.client.post('/v1/user/forgot-password', {
                'email': self.first_user_email
            })

            backoff_key = backoff_cache_key(self.first_user_email, None)
            backoff_data = {'count': 6, 'until': timezone.now() + timezone.timedelta(seconds=60)}
            cache.set(backoff_key, backoff_data)
            self.assertEqual(cache.get(backoff_key), backoff_data)

            self.assertEqual(response.status_code, 200)
            # received email with recovery url
            self.assertEqual(len(mail.outbox), 1)
            matches = re.search(r'/v1/user/forgot-password\?token=([-0-9a-zA-Z]*)', mail.outbox[0].body)
            self.assertIsNotNone(matches)
            token = matches.group(1)
            new_password = '******'

            # able to use token received in email to reset password
            response = self.client.put('/v1/user/forgot-password', {
                'token': token,
                'password': new_password
            })
            self.assertEqual(response.status_code, 200)

            backoff_data = cache.get(backoff_key)
            self.assertIsNone(backoff_data)

            response = self.client.post('/api-auth/token', {
                'username': self.first_user.username,
                'password': new_password,
            })
            self.assertEqual(response.status_code, 200)
    def post(self, request, *args, **kwargs):
        _backoff_period = getattr(settings, 'TOKEN_BACKOFF_PERIOD_SECONDS', 2)
        _max_backoff = getattr(settings, 'TOKEN_BACKOFF_MAXIMUM_SECONDS', 3600)  # max is 1 hour

        grant_type = request.POST.get('grant_type', 'password')
        username = request.POST.get('username')
        client_ip = client_ip_from_request(request)

        if grant_type == 'password' and _backoff_period is not None:
            # check for existing backoff for password attempts
            backoff = cache.get(backoff_cache_key(username, client_ip))
            if backoff is not None:
                backoff_until = backoff.get('until', None)
                backoff_count = backoff.get('count', 1)
                if backoff_until > timezone.now():
                    backoff_count += 1
                    backoff_seconds = min(_max_backoff, _backoff_period ** backoff_count)
                    backoff_until = timezone.now() + datetime.timedelta(seconds=backoff_seconds)
                    cache.set(backoff_cache_key(username, client_ip), dict(until=backoff_until, count=backoff_count), timeout=None)
                    # return the same error as a failed login attempt
                    return HttpResponse(json.dumps({
                        "error_description": "Too many login attempts. Please wait and try again.",
                        "error": "login attempts throttled",
                        "expires": backoff_seconds,
                    }), status=HTTP_401_UNAUTHORIZED)

        # pre-validate scopes requested
        client_id = request.POST.get('client_id', None)
        requested_scopes = [s for s in scope_to_list(request.POST.get('scope', '')) if s]
        if client_id:
            try:
                oauth_app = Application.objects.get(client_id=client_id)
            except Application.DoesNotExist:
                return HttpResponse(json.dumps({"error": "invalid client_id"}), status=HTTP_400_BAD_REQUEST)

            try:
                allowed_scopes = oauth_app.applicationinfo.scope_list
            except ApplicationInfo.DoesNotExist:
                allowed_scopes = ['r:profile']

            # handle rw:issuer:* scopes
            if 'rw:issuer:*' in allowed_scopes:
                issuer_scopes = filter(lambda x: x.startswith(r'rw:issuer:'), requested_scopes)
                allowed_scopes.extend(issuer_scopes)

            filtered_scopes = set(allowed_scopes) & set(requested_scopes)
            if len(filtered_scopes) < len(requested_scopes):
                return HttpResponse(json.dumps({"error": "invalid scope requested"}), status=HTTP_400_BAD_REQUEST)

        # let parent method do actual authentication
        response = super(TokenView, self).post(request, *args, **kwargs)

        if grant_type == "password" and response.status_code == 401:
            # failed password login attempt
            username = request.POST.get('username', None)
            badgrlogger.event(badgrlog.FailedLoginAttempt(request, username, endpoint='/o/token'))

            if _backoff_period is not None:
                # update backoff for failed logins
                backoff = cache.get(backoff_cache_key(username, client_ip))
                if backoff is None:
                    backoff = {'count': 0}
                backoff['count'] += 1
                backoff['until'] = timezone.now() + datetime.timedelta(seconds=_backoff_period ** backoff['count'])
                cache.set(backoff_cache_key(username, client_ip), backoff, timeout=None)
        elif response.status_code == 200:
            # successful login
            cache.set(backoff_cache_key(username, client_ip), None)  # clear any existing backoff

        return response
Exemple #14
0
 def clear_login_backoff(self, request, obj):
     for email in obj.all_verified_recipient_identifiers:
         cache_key = backoff_cache_key(username=email)
         cache.delete(cache_key)
Exemple #15
0
    def post(self, request, *args, **kwargs):
        _backoff_period = getattr(settings, 'TOKEN_BACKOFF_PERIOD_SECONDS', 2)
        _max_backoff = getattr(settings, 'TOKEN_BACKOFF_MAXIMUM_SECONDS',
                               3600)  # max is 1 hour

        grant_type = request.POST.get('grant_type', 'password')
        username = request.POST.get('username')
        client_ip = client_ip_from_request(request)

        if grant_type == 'password' and _backoff_period is not None:
            # check for existing backoff for password attempts
            backoff = cache.get(backoff_cache_key(username, client_ip))
            if backoff is not None:
                backoff_until = backoff.get('until', None)
                backoff_count = backoff.get('count', 1)
                if backoff_until > timezone.now():
                    backoff_count += 1
                    backoff_seconds = min(_max_backoff,
                                          _backoff_period**backoff_count)
                    backoff_until = timezone.now() + datetime.timedelta(
                        seconds=backoff_seconds)
                    cache.set(backoff_cache_key(username, client_ip),
                              dict(until=backoff_until, count=backoff_count),
                              timeout=None)
                    # return the same error as a failed login attempt
                    return HttpResponse(json.dumps({
                        "error_description":
                        "Too many login attempts. Please wait and try again.",
                        "error": "login attempts throttled",
                        "expires": backoff_seconds,
                    }),
                                        status=HTTP_401_UNAUTHORIZED)

        # pre-validate scopes requested
        client_id = request.POST.get('client_id', None)
        requested_scopes = [
            s for s in scope_to_list(request.POST.get('scope', '')) if s
        ]
        if client_id:
            try:
                oauth_app = Application.objects.get(client_id=client_id)
            except Application.DoesNotExist:
                return HttpResponse(json.dumps({"error": "invalid client_id"}),
                                    status=HTTP_400_BAD_REQUEST)

            try:
                allowed_scopes = oauth_app.applicationinfo.scope_list
            except ApplicationInfo.DoesNotExist:
                allowed_scopes = ['r:profile']

            # handle rw:issuer:* scopes
            if 'rw:issuer:*' in allowed_scopes:
                issuer_scopes = filter(lambda x: x.startswith(r'rw:issuer:'),
                                       requested_scopes)
                allowed_scopes.extend(issuer_scopes)

            filtered_scopes = set(allowed_scopes) & set(requested_scopes)
            if len(filtered_scopes) < len(requested_scopes):
                return HttpResponse(json.dumps(
                    {"error": "invalid scope requested"}),
                                    status=HTTP_400_BAD_REQUEST)

        # let parent method do actual authentication
        response = super(TokenView, self).post(request, *args, **kwargs)

        if grant_type == "password" and response.status_code == 401:
            # failed password login attempt
            username = request.POST.get('username', None)
            badgrlogger.event(
                badgrlog.FailedLoginAttempt(request,
                                            username,
                                            endpoint='/o/token'))

            if _backoff_period is not None:
                # update backoff for failed logins
                backoff = cache.get(backoff_cache_key(username, client_ip))
                if backoff is None:
                    backoff = {'count': 0}
                backoff['count'] += 1
                backoff['until'] = timezone.now() + datetime.timedelta(
                    seconds=_backoff_period**backoff['count'])
                cache.set(backoff_cache_key(username, client_ip),
                          backoff,
                          timeout=None)
        elif response.status_code == 200:
            # successful login
            cache.set(backoff_cache_key(username, client_ip),
                      None)  # clear any existing backoff

        return response
Exemple #16
0
 def clear_login_backoff(self, request, obj):
     for email in obj.all_recipient_identifiers:
         cache_key = backoff_cache_key(username=email)
         cache.delete(cache_key)
Exemple #17
0
 def clear_login_backoff(self, request, obj):
     cache_key = backoff_cache_key(obj.client_id)
     cache.delete(cache_key)