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()
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)
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"
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)
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)
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)
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"
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)
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)
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
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)
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
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)
def clear_login_backoff(self, request, obj): cache_key = backoff_cache_key(obj.client_id) cache.delete(cache_key)