From b1ad3f843ba698c2e3321bd836dcb683208a6bb8 Mon Sep 17 00:00:00 2001 From: Eduardo Robles Date: Tue, 27 Jun 2023 17:35:43 +0200 Subject: [PATCH] Improve authentication messages to users (#294) Parent issue: https://github.com/sequentech/epi/issues/28 --- iam/api/tests.py | 12 +- iam/api/views.py | 50 ++++--- iam/authmethods/m_dnie.py | 17 ++- iam/authmethods/m_email.py | 132 ++++++++++--------- iam/authmethods/m_email_otp.py | 135 ++++++++++--------- iam/authmethods/m_emailpwd.py | 77 +++++++++-- iam/authmethods/m_openidconnect.py | 44 ++++++- iam/authmethods/m_pwd.py | 84 +++++++++--- iam/authmethods/m_smart_link.py | 204 ++++++++++++----------------- iam/authmethods/m_sms.py | 143 ++++++++++---------- iam/authmethods/m_sms_otp.py | 138 ++++++++++--------- iam/authmethods/tests.py | 4 +- iam/authmethods/utils.py | 14 +- iam/iam/test_settings.py | 2 + iam/utils.py | 36 +++-- 15 files changed, 625 insertions(+), 467 deletions(-) diff --git a/iam/api/tests.py b/iam/api/tests.py index d76da191..b50e410a 100644 --- a/iam/api/tests.py +++ b/iam/api/tests.py @@ -30,7 +30,7 @@ from .models import ACL, AuthEvent, Action, BallotBox, TallySheet, SuccessfulLogin from authmethods.models import Code, MsgLog, OneTimeLink from authmethods import m_sms_otp -from utils import HMACToken, verifyhmac, reproducible_json_dumps +from utils import HMACToken, verifyhmac, reproducible_json_dumps, ErrorCodes from authmethods.utils import get_cannonical_tlf, get_user_code def flush_db_load_fixture(ffile="initial.json"): @@ -1863,7 +1863,7 @@ def test_authenticate_authevent_email_invalid_code(self): response = c.authenticate(self.aeid, data) self.assertEqual(response.status_code, 400) r = parse_json_response(response) - self.assertEqual(r['error_codename'], 'invalid_credentials') + self.assertEqual(r['error_codename'], ErrorCodes.INVALID_CODE) @override_settings(**override_celery_data) def test_authenticate_authevent_email_fields(self): @@ -2332,7 +2332,7 @@ def test_authenticate_authevent_sms_invalid_code(self): response = c.authenticate(self.aeid, data) self.assertEqual(response.status_code, 400) r = parse_json_response(response) - self.assertEqual(r['error_codename'], 'invalid_credentials') + self.assertEqual(r['error_codename'], ErrorCodes.INVALID_CODE) def _test_authenticate_authevent_sms_fields(self): c = JClient() @@ -3932,7 +3932,7 @@ def test_census_delete_ok(self): ) self.assertEqual(response.status_code, 400) r = parse_json_response(response) - self.assertEqual(r['error_codename'], 'invalid_credentials') + self.assertEqual(r['error_codename'], ErrorCodes.USER_NOT_FOUND) # voter does not exist in database anymore self.assertEqual(User.objects.filter(id=self.census_user_id).count(), 0) @@ -4028,7 +4028,7 @@ def test_census_delete_voted_ok(self): ) self.assertEqual(response.status_code, 400) r = parse_json_response(response) - self.assertEqual(r['error_codename'], 'invalid_credentials') + self.assertEqual(r['error_codename'], ErrorCodes.USER_NOT_FOUND) # voter does not exist in database anymore self.assertEqual(User.objects.filter(id=self.census_user_id).count(), 0) @@ -4132,7 +4132,7 @@ def test_activation(self): response = c.authenticate(self.aeid, test_data.auth_email_default) self.assertEqual(response.status_code, 400) r = parse_json_response(response) - self.assertEqual(r['error_codename'], 'invalid_credentials') + self.assertEqual(r['error_codename'], ErrorCodes.USER_NOT_FOUND) # admin login response = c.authenticate(self.aeid, self.admin_auth_data) diff --git a/iam/api/views.py b/iam/api/views.py index e5aa7285..bb358096 100644 --- a/iam/api/views.py +++ b/iam/api/views.py @@ -599,26 +599,41 @@ class Authenticate(View): def post(self, request, pk): try: - e = get_object_or_404( + auth_event = get_object_or_404( AuthEvent, pk=pk ) except: - return json_response(status=400, error_codename=ErrorCodes.BAD_REQUEST) + return json_response( + status=400, + error_codename=ErrorCodes.AUTH_EVENT_NOT_FOUND + ) - if (e.status != AuthEvent.STARTED and - e.status != AuthEvent.RESUMED and - e.auth_method_config.get("config", dict()).get("show_pdf") != True): - return json_response(status=400, error_codename=ErrorCodes.BAD_REQUEST) + if (auth_event.status != AuthEvent.STARTED and + auth_event.status != AuthEvent.RESUMED and + auth_event.auth_method_config.get("config", dict()).get("show_pdf") != True): + return json_response( + status=400, + error_codename=ErrorCodes.AUTH_EVENT_NOT_STARTED + ) if not hasattr(request.user, 'account'): - error_kwargs = plugins.call("extend_auth", e) + error_kwargs = plugins.call("extend_auth", auth_event) if error_kwargs: return json_response(**error_kwargs[0]) try: - data = auth_authenticate(e, request) - except: - return json_response(status=400, error_codename=ErrorCodes.BAD_REQUEST) + data = auth_authenticate(auth_event, request) + except Exception as error: + LOGGER.error( + "Authenticate.post\n" + + f"req '{request}'\n" + + f"error '{error}'\n" + + f"Stack trace:\n {stack_trace_str()}" + ) + return json_response( + status=500, + error_codename=ErrorCodes.INTERNAL_SERVER_ERROR + ) if data and 'status' in data and data['status'] == 'ok': user = User.objects.get(username=data['username']) @@ -627,16 +642,21 @@ def post(self, request, pk): receiver=user, action_name='user:authenticate', event=user.userdata.event, - metadata=dict()) + metadata=dict() + ) action.save() - data["show-pdf"] = e.auth_method_config.get("config", dict()).get("show_pdf", False) + data["show-pdf"] = auth_event\ + .auth_method_config\ + .get("config", dict())\ + .get("show_pdf", False) return json_response(data) else: return json_response( - status=400, - error_codename=data.get('error_codename'), - message=data.get('msg', '-')) + status=400, + error_codename=data.get('error_codename'), + message=data.get('msg', '-') + ) authenticate = Authenticate.as_view() diff --git a/iam/authmethods/m_dnie.py b/iam/authmethods/m_dnie.py index bfc11b56..5a8d679b 100644 --- a/iam/authmethods/m_dnie.py +++ b/iam/authmethods/m_dnie.py @@ -15,12 +15,8 @@ import json from . import register_method -from utils import genhmac from django.shortcuts import get_object_or_404, redirect -from django.conf import settings -from django.contrib.auth.models import User from django.conf.urls import url -from django.db.models import Q from django.http import Http404 from authmethods.utils import check_pipeline, give_perms @@ -116,6 +112,19 @@ class DNIE: ) dni_definition = { "name": "dni", "type": "text", "required": True, "min": 2, "max": 200, "required_on_authentication": True } + def error( + self, msg, auth_event=None, error_codename=None, internal_error=None + ): + data = {'status': 'nok', 'msg': msg, 'error_codename': error_codename} + LOGGER.error(\ + "DNIE.error\n"\ + f"internal_error '{internal_error}'\n"\ + f"error_codename '{error_codename}'\n"\ + f"returning error '{data}'\n"\ + f"auth_event '{auth_event}'\n"\ + f"Stack trace: \n{stack_trace_str()}" + ) + return data def authenticate_error(self): d = {'status': 'nok'} diff --git a/iam/authmethods/m_email.py b/iam/authmethods/m_email.py index e6acdad8..ac389901 100644 --- a/iam/authmethods/m_email.py +++ b/iam/authmethods/m_email.py @@ -18,6 +18,7 @@ from django.conf import settings from django.contrib.auth.models import User from utils import ( + ErrorCodes, constant_time_compare, send_codes, get_client_ip, @@ -438,13 +439,17 @@ def census(self, auth_event, request): req, validation, auth_event, ret, stack_trace_str()) return ret - def error(self, msg, error_codename): + def error( + self, msg, auth_event=None, error_codename=None, internal_error=None + ): data = {'status': 'nok', 'msg': msg, 'error_codename': error_codename} LOGGER.error(\ "Email.error\n"\ - "error '%r'\n"\ - "Stack trace: \n%s",\ - data, stack_trace_str() + f"internal_error '{internal_error}'\n"\ + f"error_codename '{error_codename}'\n"\ + f"returning error '{data}'\n"\ + f"auth_event '{auth_event}'\n"\ + f"Stack trace: \n{stack_trace_str()}" ) return data @@ -679,8 +684,9 @@ def authenticate(self, auth_event, request): if verified: if not verify_num_successful_logins(auth_event, 'Email', user, req): return self.error( - "Incorrect data", - error_codename="invalid_credentials" + ErrorCodes.CANT_VOTE_MORE_TIMES, + auth_event=auth_event, + error_codename=ErrorCodes.CANT_VOTE_MORE_TIMES ) return return_auth_data('Email', req, request, user) @@ -688,59 +694,64 @@ def authenticate(self, auth_event, request): msg = '' if auth_event.parent is not None: msg += 'you can only authenticate to parent elections' - LOGGER.error(\ - "Email.authenticate error\n"\ - "error '%r'"\ - "authevent '%r'\n"\ - "request '%r'\n"\ - "Stack trace: \n%s",\ - msg, auth_event, req, stack_trace_str()) - return self.error("Incorrect data", error_codename="invalid_credentials") + return self.error( + msg, + auth_event=auth_event, + error_codename=ErrorCodes.CANT_AUTHENTICATE_TO_PARENT + ) msg += check_field_type(self.code_definition, req.get('code'), 'authenticate') msg += check_field_value(self.code_definition, req.get('code'), 'authenticate') msg += check_fields_in_request(req, auth_event, 'authenticate') if msg: - LOGGER.error(\ - "Email.authenticate error\n"\ - "error '%r'"\ - "authevent '%r'\n"\ - "request '%r'\n"\ - "Stack trace: \n%s",\ - msg, auth_event, req, stack_trace_str()) - return self.error("Incorrect data", error_codename="invalid_credentials") + return self.error( + msg="", + internal_error=msg, + auth_event=auth_event, + error_codename=ErrorCodes.INVALID_FIELD_VALIDATION + ) msg = check_pipeline(request, auth_event, 'authenticate') if msg: - LOGGER.error(\ - "Email.authenticate error\n"\ - "error '%r'\n"\ - "authevent '%r'\n"\ - "request '%r'\n"\ - "Stack trace: \n%s",\ - msg, auth_event, req, stack_trace_str()) - return self.error("Incorrect data", error_codename="invalid_credentials") + return self.error( + msg="", + internal_error=msg, + auth_event=auth_event, + error_codename=ErrorCodes.PIPELINE_INVALID_CREDENTIALS + ) + msg = "" try: q = get_base_auth_query(auth_event) q = get_required_fields_on_auth(req, auth_event, q) user = User.objects.get(q) + except Exception as error: + msg += f"can't find user with query: `{str(q)}`\nexception: `{error}`\n" + return self.error( + msg="", + internal_error=msg, + auth_event=auth_event, + error_codename=ErrorCodes.USER_NOT_FOUND + ) + try: otp_field_code = post_verify_fields_on_auth(user, req, auth_event) - except: - LOGGER.error(\ - "Email.authenticate error\n"\ - "user not found with these characteristics: \n"\ - "authevent '%r'\n"\ - "is_active True\n"\ - "request '%r'\n"\ - "Stack trace: \n%s",\ - auth_event, req, stack_trace_str()) - return self.error("Incorrect data", error_codename="invalid_credentials") + except Exception as error: + msg += f"exception: `{error}`\n" + return self.error( + msg="", + internal_error=msg, + auth_event=auth_event, + error_codename=ErrorCodes.INVALID_PASSWORD_OR_CODE + ) user_auth_event = user.userdata.event if not verify_num_successful_logins(user_auth_event, 'Email', user, req): - return self.error("Incorrect data", error_codename="invalid_credentials") + return self.error( + ErrorCodes.CANT_VOTE_MORE_TIMES, + auth_event=auth_event, + error_codename=ErrorCodes.CANT_VOTE_MORE_TIMES + ) if otp_field_code is not None: code = otp_field_code @@ -750,31 +761,22 @@ def authenticate(self, auth_event, request): timeout_seconds=None ) if not code: - LOGGER.error(\ - "Email.authenticate error\n"\ - "Code not found on db for user '%r'\n"\ - "and code '%r'\n"\ - "authevent '%r'\n"\ - "request '%r'\n"\ - "Stack trace: \n%s",\ - user.userdata,\ - req.get('code').upper(),\ - auth_event, req, stack_trace_str()) - return self.error("Incorrect data", error_codename="invalid_credentials") - - if not constant_time_compare(req.get('code').upper(), code.code): - LOGGER.error(\ - "Email.authenticate error\n"\ - "Code mismatch for user '%r'\n"\ - "Code received '%r'\n"\ - "and latest code in the db for the user '%r'\n"\ - "authevent '%r'\n"\ - "request '%r'\n"\ - "Stack trace: \n%s",\ - user.userdata, req.get('code').upper(), code.code, auth_event, req,\ - stack_trace_str()) + msg += f"code not found in the database for user `{user.userdata}` and requested code `{req.get('code').upper()}`\n" + return self.error( + msg="", + internal_error=msg, + auth_event=auth_event, + error_codename=ErrorCodes.INVALID_CODE + ) - return self.error("Incorrect data", error_codename="invalid_credentials") + if not constant_time_compare(req.get('code').upper(), code.code): + msg += f"code mismatch for user `{user.userdata}`: [dbcode = `{code.code}`] != [requested code = `{req.get('code').upper()}`]\n" + return self.error( + msg="", + internal_error=msg, + auth_event=auth_event, + error_codename=ErrorCodes.INVALID_CODE + ) return return_auth_data('Email', req, request, user) diff --git a/iam/authmethods/m_email_otp.py b/iam/authmethods/m_email_otp.py index e3b70df4..8c3e4770 100644 --- a/iam/authmethods/m_email_otp.py +++ b/iam/authmethods/m_email_otp.py @@ -19,11 +19,12 @@ from django.db.models import Q from django.contrib.auth.models import User from utils import ( + ErrorCodes, constant_time_compare, send_codes, get_client_ip, is_valid_url, - verify_admin_generated_auth_code + verify_admin_generated_auth_code, ) from . import register_method from authmethods.utils import ( @@ -441,13 +442,18 @@ def census(self, auth_event, request): req, validation, auth_event, ret, stack_trace_str()) return ret - def error(self, msg, error_codename): + def error( + self, msg, auth_event=None, error_codename=None, internal_error=None + ): data = {'status': 'nok', 'msg': msg, 'error_codename': error_codename} LOGGER.error(\ "EmailOtp.error\n"\ - "error '%r'\n"\ - "Stack trace: \n%s",\ - data, stack_trace_str()) + f"internal_error '{internal_error}'\n"\ + f"error_codename '{error_codename}'\n"\ + f"returning error '{data}'\n"\ + f"auth_event '{auth_event}'\n"\ + f"Stack trace: \n{stack_trace_str()}" + ) return data def register(self, auth_event, request): @@ -678,8 +684,9 @@ def authenticate(self, auth_event, request): if verified: if not verify_num_successful_logins(auth_event, 'EmailOtp', user, req): return self.error( - "Incorrect data", - error_codename="invalid_credentials" + ErrorCodes.CANT_VOTE_MORE_TIMES, + auth_event=auth_event, + error_codename=ErrorCodes.CANT_VOTE_MORE_TIMES ) return return_auth_data('EmailOtp', req, request, user) @@ -693,57 +700,62 @@ def authenticate(self, auth_event, request): if auth_event.parent is not None: msg += 'you can only authenticate to parent elections' - LOGGER.error(\ - "EmailOtp.authenticate error\n"\ - "error '%r'"\ - "authevent '%r'\n"\ - "request '%r'\n"\ - "Stack trace: \n%s",\ - msg, auth_event, req, stack_trace_str()) - return self.error("Incorrect data", error_codename="invalid_credentials") + return self.error( + msg, + auth_event=auth_event, + error_codename=ErrorCodes.CANT_AUTHENTICATE_TO_PARENT + ) msg += check_field_type(self.code_definition, req.get('code'), 'authenticate') msg += check_field_value(self.code_definition, req.get('code'), 'authenticate') msg += check_fields_in_request(req, auth_event, 'authenticate') if msg: - LOGGER.error(\ - "EmailOtp.authenticate error\n"\ - "error '%r'"\ - "authevent '%r'\n"\ - "request '%r'\n"\ - "Stack trace: \n%s",\ - msg, auth_event, req, stack_trace_str()) - return self.error("Incorrect data", error_codename="invalid_credentials") + return self.error( + msg="", + internal_error=msg, + auth_event=auth_event, + error_codename=ErrorCodes.INVALID_FIELD_VALIDATION + ) msg = check_pipeline(request, auth_event, 'authenticate') if msg: - LOGGER.error(\ - "EmailOtp.authenticate error\n"\ - "error '%r'\n"\ - "authevent '%r'\n"\ - "request '%r'\n"\ - "Stack trace: \n%s",\ - msg, auth_event, req, stack_trace_str()) - return self.error("Incorrect data", error_codename="invalid_credentials") + return self.error( + msg="", + internal_error=msg, + auth_event=auth_event, + error_codename=ErrorCodes.PIPELINE_INVALID_CREDENTIALS + ) + msg = "" try: q = get_base_auth_query(auth_event) q = get_required_fields_on_auth(req, auth_event, q) user = User.objects.get(q) + except Exception as error: + msg += f"can't find user with query: `{str(q)}`\nexception: `{error}`\n" + return self.error( + msg="", + internal_error=msg, + auth_event=auth_event, + error_codename=ErrorCodes.USER_NOT_FOUND + ) + try: otp_field_code = post_verify_fields_on_auth(user, req, auth_event) - except: - LOGGER.error(\ - "EmailOtp.authenticate error\n"\ - "user not found with these characteristics: email '%r'\n"\ - "authevent '%r'\n"\ - "is_active True\n"\ - "request '%r'\n"\ - "Stack trace: \n%s",\ - email, auth_event, req, stack_trace_str()) - return self.error("Incorrect data", error_codename="invalid_credentials") + except Exception as error: + msg += f"exception: `{error}`\n" + return self.error( + msg="", + internal_error=msg, + auth_event=auth_event, + error_codename=ErrorCodes.INVALID_PASSWORD_OR_CODE + ) if not verify_num_successful_logins(auth_event, 'EmailOtp', user, req): - return self.error("Incorrect data", error_codename="invalid_credentials") + return self.error( + ErrorCodes.CANT_VOTE_MORE_TIMES, + auth_event=auth_event, + error_codename=ErrorCodes.CANT_VOTE_MORE_TIMES + ) if otp_field_code is not None: code = otp_field_code @@ -753,20 +765,12 @@ def authenticate(self, auth_event, request): timeout_seconds=settings.SMS_OTP_EXPIRE_SECONDS ) if not code: - LOGGER.error( - "EmailOtp.authenticate error\n"\ - "Code not found on db for user '%r'\n"\ - "and time between now and '%r' seconds earlier\n"\ - "authevent '%r'\n"\ - "request '%r'\n"\ - "Stack trace: \n%s", - user.userdata, - settings.SMS_OTP_EXPIRE_SECONDS, - auth_event, req, stack_trace_str() - ) + msg += f"code not found in the database for user `{user.userdata}` and requested code `{req.get('code').upper()}` with expiration less than `{settings.SMS_OTP_EXPIRE_SECONDS}`\n" return self.error( - "Incorrect data", - error_codename="invalid_credentials" + msg="", + internal_error=msg, + auth_event=auth_event, + error_codename=ErrorCodes.INVALID_CODE ) # if otp_field_code is not None then post_verify_fields_on_auth already @@ -774,19 +778,14 @@ def authenticate(self, auth_event, request): if otp_field_code is None: disable_previous_user_codes(user) - if not constant_time_compare(req.get('code').upper(), code.code): - LOGGER.error(\ - "EmailOtp.authenticate error\n"\ - "Code mismatch for user '%r'\n"\ - "Code received '%r'\n"\ - "and latest code in the db for the user '%r'\n"\ - "authevent '%r'\n"\ - "request '%r'\n"\ - "Stack trace: \n%s",\ - user.userdata, req.get('code').upper(), code.code, auth_event, req,\ - stack_trace_str()) - - return self.error("Incorrect data", error_codename="invalid_credentials") + if not constant_time_compare(req.get('code').upper(), code.code): + msg += f"code mismatch for user `{user.userdata}`: [dbcode = `{code.code}`] != [requested code = `{req.get('code').upper()}`]\n" + return self.error( + msg="", + internal_error=msg, + auth_event=auth_event, + error_codename=ErrorCodes.INVALID_CODE + ) return return_auth_data('EmailOtp', req, request, user) diff --git a/iam/authmethods/m_emailpwd.py b/iam/authmethods/m_emailpwd.py index 049635d4..c658acdc 100644 --- a/iam/authmethods/m_emailpwd.py +++ b/iam/authmethods/m_emailpwd.py @@ -20,6 +20,7 @@ from . import register_method from utils import ( + ErrorCodes, verify_admin_generated_auth_code ) from authmethods.utils import ( @@ -167,7 +168,7 @@ def census(self, auth_event, request): req, validation, auth_event, ret, stack_trace_str()) return ret - def authenticate_error(self, error, req, ae): + def authenticate_error(self, error, req, ae, error_codename=None): d = {'status': 'nok'} LOGGER.error( "EmailPassword.census error\n"\ @@ -179,6 +180,20 @@ def authenticate_error(self, error, req, ae): ) return d + def error( + self, msg, auth_event=None, error_codename=None, internal_error=None + ): + data = {'status': 'nok', 'msg': msg, 'error_codename': error_codename} + LOGGER.error(\ + "EmailPassword.error\n"\ + f"internal_error '{internal_error}'\n"\ + f"error_codename '{error_codename}'\n"\ + f"returning error '{data}'\n"\ + f"auth_event '{auth_event}'\n"\ + f"Stack trace: \n{stack_trace_str()}" + ) + return data + def authenticate(self, auth_event, request, mode='authenticate'): return_data = {'status': 'ok'} req = json.loads(request.body.decode('utf-8')) @@ -195,30 +210,66 @@ def authenticate(self, auth_event, request, mode='authenticate'): user, req ): - return self.authenticate_error( - "invalid_num_successful_logins_allowed", req, auth_event + return self.error( + ErrorCodes.CANT_VOTE_MORE_TIMES, + auth_event=auth_event, + error_codename=ErrorCodes.CANT_VOTE_MORE_TIMES ) return return_auth_data('EmailPassword', req, request, user) msg = "" + if auth_event.parent is not None: + msg += 'you can only authenticate to parent elections' + return self.error( + msg, + auth_event=auth_event, + error_codename=ErrorCodes.CANT_AUTHENTICATE_TO_PARENT + ) + msg += check_fields_in_request(req, auth_event, 'authenticate') if msg: - return self.authenticate_error("invalid-fields-check", req, auth_event) + return self.error( + msg="", + internal_error=msg, + auth_event=auth_event, + error_codename=ErrorCodes.INVALID_FIELD_VALIDATION + ) + + msg = check_pipeline(request, auth_event, 'authenticate') + if msg: + return self.error( + msg="", + internal_error=msg, + auth_event=auth_event, + error_codename=ErrorCodes.PIPELINE_INVALID_CREDENTIALS + ) + msg = "" try: q = get_base_auth_query(auth_event) q = get_required_fields_on_auth(req, auth_event, q) user = User.objects.get(q) + except Exception as error: + msg += f"can't find user with query: `{str(q)}`\nexception: `{error}`\n" + return self.error( + msg="", + internal_error=msg, + auth_event=auth_event, + error_codename=ErrorCodes.USER_NOT_FOUND + ) + try: if mode == 'authenticate': post_verify_fields_on_auth(user, req, auth_event) - except: - return self.authenticate_error("user-not-found", req, auth_event) - - msg = check_pipeline(request, auth_event, 'authenticate') - if msg: - return self.authenticate_error("invalid-pipeline", req, auth_event) + except Exception as error: + msg += f"exception: `{error}`\n" + return self.error( + msg="", + internal_error=msg, + auth_event=auth_event, + error_codename=ErrorCodes.INVALID_PASSWORD_OR_CODE + ) if mode == "authenticate": if not verify_num_successful_logins( @@ -227,8 +278,10 @@ def authenticate(self, auth_event, request, mode='authenticate'): user, req ): - return self.authenticate_error( - "invalid_num_successful_logins_allowed", req, auth_event + return self.error( + ErrorCodes.CANT_VOTE_MORE_TIMES, + auth_event=auth_event, + error_codename=ErrorCodes.CANT_VOTE_MORE_TIMES ) LOGGER.debug( diff --git a/iam/authmethods/m_openidconnect.py b/iam/authmethods/m_openidconnect.py index 01b23d4c..0206c57d 100644 --- a/iam/authmethods/m_openidconnect.py +++ b/iam/authmethods/m_openidconnect.py @@ -16,6 +16,7 @@ from . import register_method from utils import ( + ErrorCodes, verify_admin_generated_auth_code ) from authmethods.utils import ( @@ -138,6 +139,20 @@ def authenticate_error(self, error, req, ae, message=""): error, message, req, ae, stack_trace_str()) return d + def error( + self, msg, auth_event=None, error_codename=None, internal_error=None + ): + data = {'status': 'nok', 'msg': msg, 'error_codename': error_codename} + LOGGER.error(\ + "OpenIdConnect.error\n"\ + f"internal_error '{internal_error}'\n"\ + f"error_codename '{error_codename}'\n"\ + f"returning error '{data}'\n"\ + f"auth_event '{auth_event}'\n"\ + f"Stack trace: \n{stack_trace_str()}" + ) + return data + def authenticate(self, auth_event, request, mode='authenticate'): ret_data = {'status': 'ok'} req = json.loads(request.body.decode('utf-8')) @@ -154,12 +169,21 @@ def authenticate(self, auth_event, request, mode='authenticate'): user, req ): - return self.authenticate_error( - "invalid_num_successful_logins_allowed", req, auth_event + return self.error( + ErrorCodes.CANT_VOTE_MORE_TIMES, + auth_event=auth_event, + error_codename=ErrorCodes.CANT_VOTE_MORE_TIMES ) return return_auth_data('OpenIdConnect', req, request, user) + if auth_event.parent is not None: + return self.error( + msg, + auth_event=auth_event, + error_codename=ErrorCodes.CANT_AUTHENTICATE_TO_PARENT + ) + id_token = req.get('id_token', '') provider_id = req.get('provider', '') nonce = req.get('nonce', '') @@ -200,7 +224,6 @@ def authenticate(self, auth_event, request, mode='authenticate'): # get user_id and get/create user user_id = id_token_dict['sub'] try: - user_query = get_base_auth_query(auth_event) user_query["userdata__metadata__contains"]={"sub": user_id} user = User.objects.get(user_query) @@ -215,13 +238,20 @@ def authenticate(self, auth_event, request, mode='authenticate'): msg = check_pipeline(request, auth_event, 'authenticate') if msg: - return self.authenticate_error("invalid-pipeline", req, auth_event, - message=msg) + return self.error( + msg="", + internal_error=msg, + auth_event=auth_event, + error_codename=ErrorCodes.PIPELINE_INVALID_CREDENTIALS + ) + msg = "" if mode == "authenticate": if not verify_num_successful_logins(auth_event, 'OpenIdConnect', user, req): - return self.authenticate_error( - "invalid_num_successful_logins_allowed", req, auth_event + return self.error( + ErrorCodes.CANT_VOTE_MORE_TIMES, + auth_event=auth_event, + error_codename=ErrorCodes.CANT_VOTE_MORE_TIMES ) LOGGER.debug(\ diff --git a/iam/authmethods/m_pwd.py b/iam/authmethods/m_pwd.py index 312ab3fc..9fc64bf6 100644 --- a/iam/authmethods/m_pwd.py +++ b/iam/authmethods/m_pwd.py @@ -20,6 +20,7 @@ from django.conf.urls import url from utils import ( + ErrorCodes, verify_admin_generated_auth_code ) from authmethods.utils import ( @@ -201,6 +202,20 @@ def authenticate_error(self, error, req, ae): error, req, ae, stack_trace_str()) return d + def error( + self, msg, auth_event=None, error_codename=None, internal_error=None + ): + data = {'status': 'nok', 'msg': msg, 'error_codename': error_codename} + LOGGER.error(\ + "UserPassword.error\n"\ + f"internal_error '{internal_error}'\n"\ + f"error_codename '{error_codename}'\n"\ + f"returning error '{data}'\n"\ + f"auth_event '{auth_event}'\n"\ + f"Stack trace: \n{stack_trace_str()}" + ) + return data + def authenticate(self, auth_event, request, mode="authenticate"): ret_data = {'status': 'ok'} req = json.loads(request.body.decode('utf-8')) @@ -217,41 +232,72 @@ def authenticate(self, auth_event, request, mode="authenticate"): user, req ): - return self.authenticate_error( - "invalid_num_successful_logins_allowed", req, auth_event + return self.error( + ErrorCodes.CANT_VOTE_MORE_TIMES, + auth_event=auth_event, + error_codename=ErrorCodes.CANT_VOTE_MORE_TIMES ) return return_auth_data('UserPassword', req, request, user) msg = "" + if auth_event.parent is not None: + msg += 'you can only authenticate to parent elections' + return self.error( + msg, + auth_event=auth_event, + error_codename=ErrorCodes.CANT_AUTHENTICATE_TO_PARENT + ) + msg += check_fields_in_request(req, auth_event, mode) if msg: - LOGGER.error(\ - "UserPassword.authenticate error\n"\ - "error '%r'"\ - "authevent '%r'\n"\ - "request '%r'\n"\ - "Stack trace: \n%s",\ - msg, auth_event, req, stack_trace_str()) - return self.authenticate_error("invalid-fields-check", req, auth_event) + return self.error( + msg="", + internal_error=msg, + auth_event=auth_event, + error_codename=ErrorCodes.INVALID_FIELD_VALIDATION + ) + + msg = check_pipeline(request, auth_event, 'authenticate') + if msg: + return self.error( + msg="", + internal_error=msg, + auth_event=auth_event, + error_codename=ErrorCodes.PIPELINE_INVALID_CREDENTIALS + ) + msg = "" try: q = get_base_auth_query(auth_event) q = get_required_fields_on_auth(req, auth_event, q) user = User.objects.get(q) - if mode == "authenticate": + except Exception as error: + msg += f"can't find user with query: `{str(q)}`\nexception: `{error}`\n" + return self.error( + msg="", + internal_error=msg, + auth_event=auth_event, + error_codename=ErrorCodes.USER_NOT_FOUND + ) + try: + if mode == 'authenticate': post_verify_fields_on_auth(user, req, auth_event) - except: - return self.authenticate_error("user-not-found", req, auth_event) - - msg = check_pipeline(request, auth_event, 'authenticate') - if msg: - return self.authenticate_error("invalid-pipeline", req, auth_event) + except Exception as error: + msg += f"exception: `{error}`\n" + return self.error( + msg="", + internal_error=msg, + auth_event=auth_event, + error_codename=ErrorCodes.INVALID_PASSWORD_OR_CODE + ) if mode == "authenticate": if not verify_num_successful_logins(auth_event, 'UserPassword', user, req): - return self.authenticate_error( - "invalid_num_successful_logins_allowed", req, auth_event + return self.error( + ErrorCodes.CANT_VOTE_MORE_TIMES, + auth_event=auth_event, + error_codename=ErrorCodes.CANT_VOTE_MORE_TIMES ) LOGGER.debug(\ diff --git a/iam/authmethods/m_smart_link.py b/iam/authmethods/m_smart_link.py index 3d52b492..66abc054 100644 --- a/iam/authmethods/m_smart_link.py +++ b/iam/authmethods/m_smart_link.py @@ -17,15 +17,13 @@ import logging from . import register_method from utils import ( + ErrorCodes, verifyhmac, HMACToken, verify_admin_generated_auth_code ) from django.conf import settings from django.contrib.auth.models import User -from utils import ( - verify_admin_generated_auth_code -) from authmethods.utils import ( verify_children_election_info, check_fields_in_request, @@ -52,6 +50,14 @@ from contracts.base import check_contract from contracts import CheckException +class SmartLinkErrorCodes: + AUTH_TOKEN_NOT_FOUND = "AUTH_TOKEN_NOT_FOUND" + INVALID_USER_ID = "INVALID_USER_ID" + MISMATCHED_AUTH_EVENT = "MISMATCHED_AUTH_EVENT" + INVALID_PERMISSION = "INVALID_PERMISSION" + EXPIRED_AUTH_TOKEN = "EXPIRED_AUTH_TOKEN" + INVALID_AUTH_TOKEN_VERIFICATION = "INVALID_AUTH_TOKEN_VERIFICATION" + AUTH_TOKEN_VERIFICATION_EXCEPTION = "AUTH_TOKEN_VERIFICATION_EXCEPTION" LOGGER = logging.getLogger('iam') @@ -242,15 +248,20 @@ def census(self, auth_event, request): ) return ret - def error(self, msg, error_codename): - data = {'status': 'nok', 'msg': msg, 'error_codename': error_codename} - LOGGER.error(\ - "SmartLink.error\n"\ - "error '%r'\n"\ - "Stack trace: \n%s",\ - data, stack_trace_str() - ) - return data + def error( + self, msg, auth_event=None, error_codename=None, internal_error=None + ): + data = {'status': 'nok', 'msg': msg, 'error_codename': error_codename} + LOGGER.error(\ + "SmartLink.error\n"\ + f"internal_error '{internal_error}'\n"\ + f"error_codename '{error_codename}'\n"\ + f"returning error '{data}'\n"\ + f"auth_event '{auth_event}'\n"\ + f"Stack trace: \n{stack_trace_str()}" + ) + return data + def authenticate(self, auth_event, request): req = json.loads(request.body.decode('utf-8')) @@ -262,8 +273,9 @@ def authenticate(self, auth_event, request): if verified: if not verify_num_successful_logins(auth_event, 'Email', user, req): return self.error( - "Incorrect data", - error_codename="invalid_credentials" + ErrorCodes.CANT_VOTE_MORE_TIMES, + auth_event=auth_event, + error_codename=ErrorCodes.CANT_VOTE_MORE_TIMES ) return return_auth_data('Email', req, request, user) @@ -271,16 +283,10 @@ def authenticate(self, auth_event, request): msg = '' auth_token = req.get('auth-token') if not auth_token or not isinstance(auth_token, str): - LOGGER.error(\ - "SmartLink.authenticate auth-token not found or not a string\n"\ - "authevent '%r'\n"\ - "request '%r'\n"\ - "Stack trace: \n%s",\ - auth_event, req, stack_trace_str() - ) return self.error( - msg="Incorrect data", - error_codename="invalid_credentials" + SmartLinkErrorCodes.AUTH_TOKEN_NOT_FOUND, + auth_event=auth_event, + error_codename=SmartLinkErrorCodes.AUTH_TOKEN_NOT_FOUND ) # we will obtain it from auth_token @@ -289,53 +295,31 @@ def authenticate(self, auth_event, request): hmac_token = HMACToken(auth_token) user_id, perm_obj, auth_event_id, perm_action, _timestamp = hmac_token.msg.split(':') if len(user_id) == 0: - LOGGER.error(\ - "SmartLink.authenticate auth-token: invalid user_id\n"\ - "authevent '%r'\n"\ - "request '%r'\n"\ - "Stack trace: \n%s",\ - auth_event, req, stack_trace_str() - ) return self.error( - msg="Incorrect data", - error_codename="invalid_credentials" + SmartLinkErrorCodes.INVALID_USER_ID, + auth_event=auth_event, + error_codename=SmartLinkErrorCodes.INVALID_USER_ID ) + if auth_event_id != str(auth_event.id): - LOGGER.error(\ - "SmartLink.authenticate auth-token: mismatched auth_event_id\n"\ - "authevent '%r'\n"\ - "request '%r'\n"\ - "Stack trace: \n%s",\ - auth_event, req, stack_trace_str() - ) return self.error( - msg="Incorrect data", - error_codename="invalid_credentials" + SmartLinkErrorCodes.MISMATCHED_AUTH_EVENT, + auth_event=auth_event, + error_codename=SmartLinkErrorCodes.MISMATCHED_AUTH_EVENT ) + if perm_obj != 'AuthEvent' or perm_action != 'vote': - LOGGER.error(\ - "SmartLink.authenticate auth-token: invalid permission\n"\ - "authevent '%r'\n"\ - "request '%r'\n"\ - "Stack trace: \n%s",\ - auth_event, req, stack_trace_str() - ) return self.error( - msg="Incorrect data", - error_codename="invalid_credentials" + SmartLinkErrorCodes.INVALID_PERMISSION, + auth_event=auth_event, + error_codename=SmartLinkErrorCodes.INVALID_PERMISSION ) if not hmac_token.check_expiration(settings.TIMEOUT): - LOGGER.error(\ - "SmartLink.authenticate auth-token: expired\n"\ - "authevent '%r'\n"\ - "request '%r'\n"\ - "Stack trace: \n%s",\ - auth_event, req, stack_trace_str() - ) return self.error( - msg="Incorrect data", - error_codename="invalid_credentials" + SmartLinkErrorCodes.EXPIRED_AUTH_TOKEN, + auth_event=auth_event, + error_codename=SmartLinkErrorCodes.EXPIRED_AUTH_TOKEN ) shared_secret = settings.SHARED_SECRET @@ -359,84 +343,68 @@ def authenticate(self, auth_event, request): ) if not verified: - LOGGER.error(\ - "SmartLink.authenticate auth-token: invalid verification\n"\ - "authevent '%r'\n"\ - "request '%r'\n"\ - "Stack trace: \n%s",\ - auth_event, req, stack_trace_str() + return self.error( + SmartLinkErrorCodes.INVALID_AUTH_TOKEN_VERIFICATION, + auth_event=auth_event, + error_codename=SmartLinkErrorCodes.INVALID_AUTH_TOKEN_VERIFICATION ) + except Exception as error: + msg += f"exception: `{error}`\n" return self.error( - msg="Incorrect data", - error_codename="invalid_credentials" + msg="", + internal_error=msg, + auth_event=auth_event, + error_codename=SmartLinkErrorCodes.AUTH_TOKEN_VERIFICATION_EXCEPTION ) - except Exception as e: - LOGGER.error(\ - "SmartLink.authenticate auth-token: invalid exception\n"\ - "error: '%r'\n"\ - "authevent '%r'\n"\ - "request '%r'\n"\ - "Stack trace: \n%s",\ - e, auth_event, req, stack_trace_str() - ) - return self.error( - msg="Incorrect data", - error_codename="invalid_credentials" - ) if auth_event.parent is not None: msg += 'you can only authenticate to parent elections' - LOGGER.error(\ - "SmartLink.authenticate error\n"\ - "error '%r'"\ - "authevent '%r'\n"\ - "request '%r'\n"\ - "Stack trace: \n%s",\ - msg, auth_event, req, stack_trace_str() + return self.error( + msg, + auth_event=auth_event, + error_codename=ErrorCodes.CANT_AUTHENTICATE_TO_PARENT ) - return self.error("Incorrect data", error_codename="invalid_credentials") msg = check_pipeline(request, auth_event, 'authenticate') if msg: - LOGGER.error(\ - "SmartLink.authenticate error\n"\ - "error '%r'\n"\ - "authevent '%r'\n"\ - "request '%r'\n"\ - "Stack trace: \n%s",\ - msg, auth_event, req, stack_trace_str() + return self.error( + msg="", + internal_error=msg, + auth_event=auth_event, + error_codename=ErrorCodes.PIPELINE_INVALID_CREDENTIALS ) - return self.error("Incorrect data", error_codename="invalid_credentials") - + msg = "" try: # enforce user_id to match the token user_id in the request req['user_id'] = user_id user_query = get_base_auth_query(auth_event) user_query = get_required_fields_on_auth(req, auth_event, user_query) user = User.objects.get(user_query) - post_verify_fields_on_auth(user, req, auth_event) - except: - LOGGER.error(\ - "SmartLink.authenticate error\n"\ - "user not found with these characteristics: user-id '%r'\n"\ - "authevent '%r'\n"\ - "is_active True\n"\ - "request '%r'\n"\ - "Stack trace: \n%s",\ - user_id, auth_event, req, stack_trace_str() - ) - return self.error("Incorrect data", error_codename="invalid_credentials") + except Exception as error: + msg += f"can't find user with query: `{str(user_query)}`\nexception: `{error}`\n" + return self.error( + msg="", + internal_error=msg, + auth_event=auth_event, + error_codename=ErrorCodes.USER_NOT_FOUND + ) + try: + post_verify_fields_on_auth(user, req, auth_event) + except Exception as error: + msg += f"exception: `{error}`\n" + return self.error( + msg="", + internal_error=msg, + auth_event=auth_event, + error_codename=ErrorCodes.INVALID_PASSWORD_OR_CODE + ) if not verify_num_successful_logins(auth_event, 'SmartLink', user, req): - LOGGER.error(\ - "SmartLink.authenticate error too many logins\n"\ - "authevent '%r'\n"\ - "is_active True\n"\ - "request '%r'\n"\ - "Stack trace: \n%s",\ - auth_event, req, stack_trace_str() - ) - return self.error("Incorrect data", error_codename="invalid_credentials") + return self.error( + ErrorCodes.CANT_VOTE_MORE_TIMES, + auth_event=auth_event, + error_codename=ErrorCodes.CANT_VOTE_MORE_TIMES + ) return return_auth_data('SmartLink', req, request, user, auth_event) diff --git a/iam/authmethods/m_sms.py b/iam/authmethods/m_sms.py index 18155fb4..ce86dd52 100644 --- a/iam/authmethods/m_sms.py +++ b/iam/authmethods/m_sms.py @@ -19,6 +19,7 @@ from django.db.models import Q from django.contrib.auth.models import User from utils import ( + ErrorCodes, constant_time_compare, send_codes, get_client_ip, @@ -308,14 +309,19 @@ class Sms: } ] - def error(self, msg, error_codename): - d = {'status': 'nok', 'msg': msg, 'error_codename': error_codename} + def error( + self, msg, auth_event=None, error_codename=None, internal_error=None + ): + data = {'status': 'nok', 'msg': msg, 'error_codename': error_codename} LOGGER.error(\ "Sms.error\n"\ - "error '%r'\n"\ - "Stack trace: \n%s",\ - d, stack_trace_str()) - return d + f"internal_error '{internal_error}'\n"\ + f"error_codename '{error_codename}'\n"\ + f"returning error '{data}'\n"\ + f"auth_event '{auth_event}'\n"\ + f"Stack trace: \n{stack_trace_str()}" + ) + return data def check_config(self, config): """ Check config when creating auth-event. """ @@ -679,8 +685,9 @@ def authenticate(self, auth_event, request): if verified: if not verify_num_successful_logins(auth_event, 'Sms', user, req): return self.error( - "Incorrect data", - error_codename="invalid_credentials" + ErrorCodes.CANT_VOTE_MORE_TIMES, + auth_event=auth_event, + error_codename=ErrorCodes.CANT_VOTE_MORE_TIMES ) return return_auth_data('Sms', req, request, user) @@ -694,46 +701,62 @@ def authenticate(self, auth_event, request): if auth_event.parent is not None: msg += 'you can only authenticate to parent elections' - LOGGER.error(\ - "Sms.authenticate error\n"\ - "error '%r'"\ - "authevent '%r'\n"\ - "request '%r'\n"\ - "Stack trace: \n%s",\ - msg, auth_event, req, stack_trace_str()) - return self.error("Incorrect data", error_codename="invalid_credentials") + return self.error( + msg, + auth_event=auth_event, + error_codename=ErrorCodes.CANT_AUTHENTICATE_TO_PARENT + ) msg += check_field_type(self.code_definition, req.get('code'), 'authenticate') msg += check_field_value(self.code_definition, req.get('code'), 'authenticate') msg += check_fields_in_request(req, auth_event, 'authenticate') if msg: - LOGGER.error(\ - "Sms.authenticate error\n"\ - "error '%r'"\ - "authevent '%r'\n"\ - "request '%r'\n"\ - "Stack trace: \n%s",\ - msg, auth_event, req, stack_trace_str()) - return self.error("Incorrect data", error_codename="invalid_credentials") + return self.error( + msg="", + internal_error=msg, + auth_event=auth_event, + error_codename=ErrorCodes.INVALID_FIELD_VALIDATION + ) + + msg = check_pipeline(request, auth_event, 'authenticate') + if msg: + return self.error( + msg="", + internal_error=msg, + auth_event=auth_event, + error_codename=ErrorCodes.PIPELINE_INVALID_CREDENTIALS + ) + msg = "" try: q = get_base_auth_query(auth_event) q = get_required_fields_on_auth(req, auth_event, q) user = User.objects.get(q) + except Exception as error: + msg += f"can't find user with query: `{str(q)}`\nexception: `{error}`\n" + return self.error( + msg="", + internal_error=msg, + auth_event=auth_event, + error_codename=ErrorCodes.USER_NOT_FOUND + ) + try: otp_field_code = post_verify_fields_on_auth(user, req, auth_event) - except: - LOGGER.error(\ - "Sms.authenticate error\n"\ - "user not found with these characteristics:\n tlf '%r'\n"\ - "authevent '%r'\n"\ - "is_active True\n"\ - "request '%r'\n"\ - "Stack trace: \n%s",\ - tlf, auth_event, req, stack_trace_str()) - return self.error("Incorrect data", error_codename="invalid_credentials") + except Exception as error: + msg += f"exception: `{error}`\n" + return self.error( + msg="", + internal_error=msg, + auth_event=auth_event, + error_codename=ErrorCodes.INVALID_PASSWORD_OR_CODE + ) if not verify_num_successful_logins(auth_event, 'Sms', user, req): - return self.error("Incorrect data", error_codename="invalid_credentials") + return self.error( + ErrorCodes.CANT_VOTE_MORE_TIMES, + auth_event=auth_event, + error_codename=ErrorCodes.CANT_VOTE_MORE_TIMES + ) if otp_field_code is not None: code = otp_field_code @@ -743,42 +766,22 @@ def authenticate(self, auth_event, request): timeout_seconds=None ) if not code: - LOGGER.error(\ - "Sms.authenticate error\n"\ - "Code not found on db for user '%r'\n"\ - "and code '%r'\n"\ - "authevent '%r'\n"\ - "request '%r'\n"\ - "Stack trace: \n%s",\ - user.userdata,\ - req.get('code').upper(),\ - auth_event, req, stack_trace_str()) - return self.error("Incorrect data", error_codename="invalid_credentials") - - if not constant_time_compare(req.get('code').upper(), code.code): - LOGGER.error(\ - "Sms.authenticate error\n"\ - "Code mismatch for user '%r'\n"\ - "Code received '%r'\n"\ - "and latest code in the db for the user '%r'\n"\ - "authevent '%r'\n"\ - "request '%r'\n"\ - "Stack trace: \n%s",\ - user.userdata, req.get('code').upper(), code.code, auth_event, req,\ - stack_trace_str()) - - return self.error("Incorrect data", error_codename="invalid_credentials") + msg += f"code not found in the database for user `{user.userdata}` and requested code `{req.get('code').upper()}`\n" + return self.error( + msg="", + internal_error=msg, + auth_event=auth_event, + error_codename=ErrorCodes.INVALID_CODE + ) - msg = check_pipeline(request, auth_event, 'authenticate') - if msg: - LOGGER.error(\ - "Sms.authenticate error\n"\ - "error '%r'\n"\ - "authevent '%r'\n"\ - "request '%r'\n"\ - "Stack trace: \n%s",\ - msg, auth_event, req, stack_trace_str()) - return self.error("Incorrect data", error_codename="invalid_credentials") + if not constant_time_compare(req.get('code').upper(), code.code): + msg += f"code mismatch for user `{user.userdata}`: [dbcode = `{code.code}`] != [requested code = `{req.get('code').upper()}`]\n" + return self.error( + msg="", + internal_error=msg, + auth_event=auth_event, + error_codename=ErrorCodes.INVALID_CODE + ) return return_auth_data('Sms', req, request, user) diff --git a/iam/authmethods/m_sms_otp.py b/iam/authmethods/m_sms_otp.py index f981d317..ab88d5a7 100644 --- a/iam/authmethods/m_sms_otp.py +++ b/iam/authmethods/m_sms_otp.py @@ -19,6 +19,7 @@ from django.db.models import Q from django.contrib.auth.models import User from utils import ( + ErrorCodes, constant_time_compare, send_codes, get_client_ip, @@ -309,14 +310,19 @@ class SmsOtp: } ] - def error(self, msg, error_codename): - d = {'status': 'nok', 'msg': msg, 'error_codename': error_codename} + def error( + self, msg, auth_event=None, error_codename=None, internal_error=None + ): + data = {'status': 'nok', 'msg': msg, 'error_codename': error_codename} LOGGER.error(\ "SmsOtp.error\n"\ - "error '%r'\n"\ - "Stack trace: \n%s",\ - d, stack_trace_str()) - return d + f"internal_error '{internal_error}'\n"\ + f"error_codename '{error_codename}'\n"\ + f"returning error '{data}'\n"\ + f"auth_event '{auth_event}'\n"\ + f"Stack trace: \n{stack_trace_str()}" + ) + return data def check_config(self, config): """ Check config when create auth-event. """ @@ -677,10 +683,13 @@ def authenticate(self, auth_event, request): log_prefix="SmsOtp" ) if verified: - if not verify_num_successful_logins(auth_event, 'SmsOtp', user, req): + if not verify_num_successful_logins( + auth_event, 'SmsOtp', user, req + ): return self.error( - "Incorrect data", - error_codename="invalid_credentials" + ErrorCodes.CANT_VOTE_MORE_TIMES, + auth_event=auth_event, + error_codename=ErrorCodes.CANT_VOTE_MORE_TIMES ) return return_auth_data('SmsOtp', req, request, user) @@ -694,27 +703,32 @@ def authenticate(self, auth_event, request): if auth_event.parent is not None: msg += 'you can only authenticate to parent elections' - LOGGER.error(\ - "SmsOtp.authenticate error\n"\ - "error '%r'"\ - "authevent '%r'\n"\ - "request '%r'\n"\ - "Stack trace: \n%s",\ - msg, auth_event, req, stack_trace_str()) - return self.error("Incorrect data", error_codename="invalid_credentials") + return self.error( + msg, + auth_event=auth_event, + error_codename=ErrorCodes.CANT_AUTHENTICATE_TO_PARENT + ) msg += check_field_type(self.code_definition, req.get('code'), 'authenticate') msg += check_field_value(self.code_definition, req.get('code'), 'authenticate') msg += check_fields_in_request(req, auth_event, 'authenticate') if msg: - LOGGER.error(\ - "SmsOtp.authenticate error\n"\ - "error '%r'"\ - "authevent '%r'\n"\ - "request '%r'\n"\ - "Stack trace: \n%s",\ - msg, auth_event, req, stack_trace_str()) - return self.error("Incorrect data", error_codename="invalid_credentials") + return self.error( + msg="", + internal_error=msg, + auth_event=auth_event, + error_codename=ErrorCodes.INVALID_FIELD_VALIDATION + ) + + msg = check_pipeline(request, auth_event, 'authenticate') + if msg: + return self.error( + msg="", + internal_error=msg, + auth_event=auth_event, + error_codename=ErrorCodes.PIPELINE_INVALID_CREDENTIALS + ) + msg = "" try: q = get_base_auth_query( @@ -723,20 +737,31 @@ def authenticate(self, auth_event, request): ) q = get_required_fields_on_auth(req, auth_event, q) user = User.objects.get(q) + except Exception as error: + msg += f"can't find user with query: `{str(q)}`\nexception: `{error}`\n" + return self.error( + msg="", + internal_error=msg, + auth_event=auth_event, + error_codename=ErrorCodes.USER_NOT_FOUND + ) + try: otp_field_code = post_verify_fields_on_auth(user, req, auth_event) - except: - LOGGER.error(\ - "SmsOtp.authenticate error\n"\ - "user not found with these characteristics: tlf '%r'\n"\ - "authevent '%r'\n"\ - "is_active True\n"\ - "request '%r'\n"\ - "Stack trace: \n%s",\ - tlf, auth_event, req, stack_trace_str()) - return self.error("Incorrect data", error_codename="invalid_credentials") + except Exception as error: + msg += f"exception: `{error}`\n" + return self.error( + msg="", + internal_error=msg, + auth_event=auth_event, + error_codename=ErrorCodes.INVALID_PASSWORD_OR_CODE + ) if not verify_num_successful_logins(auth_event, 'SmsOtp', user, req): - return self.error("Incorrect data", error_codename="invalid_credentials") + return self.error( + ErrorCodes.CANT_VOTE_MORE_TIMES, + auth_event=auth_event, + error_codename=ErrorCodes.CANT_VOTE_MORE_TIMES + ) if otp_field_code is not None: code = otp_field_code @@ -745,21 +770,13 @@ def authenticate(self, auth_event, request): user, timeout_seconds=settings.SMS_OTP_EXPIRE_SECONDS ) - if not code: - LOGGER.error( - "SmsOtp.authenticate error\n"\ - "Code not found on db for user '%r'\n"\ - "and time between now and '%r' seconds earlier\n"\ - "authevent '%r'\n"\ - "request '%r'\n"\ - "Stack trace: \n%s", - user.userdata, - settings.SMS_OTP_EXPIRE_SECONDS, - auth_event, req, stack_trace_str() - ) + if not code: + msg += f"code not found in the database for user `{user.userdata}` and requested code `{req.get('code').upper()}` with expiration less than `{settings.SMS_OTP_EXPIRE_SECONDS}`\n" return self.error( - "Incorrect data", - error_codename="invalid_credentials" + msg="", + internal_error=msg, + auth_event=auth_event, + error_codename=ErrorCodes.INVALID_CODE ) # if otp_field_code is not None then post_verify_fields_on_auth already @@ -767,19 +784,14 @@ def authenticate(self, auth_event, request): if otp_field_code is None: disable_previous_user_codes(user) - if not constant_time_compare(req.get('code').upper(), code.code): - LOGGER.error(\ - "SmsOtp.authenticate error\n"\ - "Code mismatch for user '%r'\n"\ - "Code received '%r'\n"\ - "and latest code in the db for the user '%r'\n"\ - "authevent '%r'\n"\ - "request '%r'\n"\ - "Stack trace: \n%s",\ - user.userdata, req.get('code').upper(), code.code, auth_event, req,\ - stack_trace_str()) - - return self.error("Incorrect data", error_codename="invalid_credentials") + if not constant_time_compare(req.get('code').upper(), code.code): + msg += f"code mismatch for user `{user.userdata}`: [dbcode = `{code.code}`] != [requested code = `{req.get('code').upper()}`]\n" + return self.error( + msg="", + internal_error=msg, + auth_event=auth_event, + error_codename=ErrorCodes.INVALID_CODE + ) return return_auth_data('SmsOtp', req, request, user) diff --git a/iam/authmethods/tests.py b/iam/authmethods/tests.py index 6047418d..70ca5fae 100644 --- a/iam/authmethods/tests.py +++ b/iam/authmethods/tests.py @@ -28,7 +28,7 @@ from .m_email import Email from .m_sms import Sms from .models import Message, Code -from utils import genhmac +from utils import genhmac, ErrorCodes class AuthMethodTestCase(TestCase): @@ -414,7 +414,7 @@ def test_method_sms_invalid_code(self): response = self.c.authenticate(self.aeid, data) self.assertEqual(response.status_code, 400) r = json.loads(response.content.decode('utf-8')) - self.assertEqual(r['error_codename'], 'invalid_credentials') + self.assertEqual(r['error_codename'], ErrorCodes.INVALID_CODE) def test_method_sms_get_perm(self): # Fix auth = { 'tlf': '+34666666666', 'code': 'AAAAAAAA', diff --git a/iam/authmethods/utils.py b/iam/authmethods/utils.py index 13f26e11..10240b8d 100644 --- a/iam/authmethods/utils.py +++ b/iam/authmethods/utils.py @@ -33,6 +33,7 @@ from captcha.decorators import valid_captcha from contracts import CheckException, JSONContractEncoder from utils import ( + ErrorCodes, json_response, get_client_ip, constant_time_compare, @@ -1064,12 +1065,13 @@ def post_verify_fields_on_auth(user, req, auth_event, mode="auth"): mode != "resend-auth" and type_field != 'otp-code' ): - raise Exception() + raise Exception(f"field_name {field_name} missing") field_value = req.get(field_name, '') if type_field == 'password': if not user.check_password(field_value): - raise Exception() + raise Exception("Invalid Password") + # we do not verify otp-code in mode 'resend-auth', since the # whole point is to send the auth-code before being able to verify # it @@ -1088,7 +1090,7 @@ def post_verify_fields_on_auth(user, req, auth_event, mode="auth"): f"field name '{field_name}'\n" + f"Stack trace: \n{stack_trace_str()}" ) - raise Exception() + raise Exception('Error running parse_otp_code_field') # get the field value, because in otp-code it's always under the # key 'code' @@ -1102,7 +1104,7 @@ def post_verify_fields_on_auth(user, req, auth_event, mode="auth"): f"field name '{field_name}'\n" + f"Stack trace: \n{stack_trace_str()}" ) - raise Exception() + raise Exception('Error: code is not a string') timeout = settings.SMS_OTP_EXPIRE_SECONDS if otp_field_code is None: @@ -1118,7 +1120,7 @@ def post_verify_fields_on_auth(user, req, auth_event, mode="auth"): f"field name '{field_name}'\n" + f"Stack trace: \n{stack_trace_str()}" ) - raise Exception() + raise Exception(f"Code not found on db for user '{user.userdata}'") if not constant_time_compare( field_value.upper(), @@ -1134,7 +1136,7 @@ def post_verify_fields_on_auth(user, req, auth_event, mode="auth"): f"field name '{field_name}'\n" + f"Stack trace: \n{stack_trace_str()}" ) - raise Exception() + raise Exception(f"Code mismatch for user '{user.userdata}'") # disable the user code if any if otp_field_code is not None: diff --git a/iam/iam/test_settings.py b/iam/iam/test_settings.py index a7d16e66..0657283e 100644 --- a/iam/iam/test_settings.py +++ b/iam/iam/test_settings.py @@ -25,6 +25,8 @@ from datetime import timedelta from kombu import Exchange, Queue +TESTING = True + # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(__file__)) # Celery config diff --git a/iam/utils.py b/iam/utils.py index 4845f8af..45660d97 100644 --- a/iam/utils.py +++ b/iam/utils.py @@ -39,7 +39,6 @@ from django.conf import settings from django.http import HttpResponse from django.utils import timezone -from enum import IntEnum, unique from string import ascii_lowercase, digits, ascii_letters from random import choice from pipelines import PipeReturnvalue @@ -62,15 +61,30 @@ def stack_trace_str(): return "\n".join(stack_trace[:-1]) + "\n" + traceback.format_exc() -@unique -class ErrorCodes(IntEnum): - BAD_REQUEST = 1 - INVALID_REQUEST = 2 - INVALID_CODE = 3 - INVALID_PERMS = 4 - GENERAL_ERROR = 5 - MAX_CONNECTION = 6 - BLACKLIST = 7 +class ErrorCodes: + BAD_REQUEST = "BAD_REQUEST" + INVALID_REQUEST = "INVALID_REQUEST" + INTERNAL_SERVER_ERROR = "INTERNAL_SERVER_ERROR" + GENERAL_ERROR = "GENERAL_ERROR" + AUTH_EVENT_NOT_FOUND = "AUTH_EVENT_NOT_FOUND" + AUTH_EVENT_NOT_STARTED = "AUTH_EVENT_NOT_STARTED" + CANT_VOTE_MORE_TIMES = "CANT_VOTE_MORE_TIMES" + CANT_AUTHENTICATE_TO_PARENT = "CANT_AUTHENTICATE_TO_PARENT" + INVALID_FIELD_VALIDATION = "INVALID_FIELD_VALIDATION" + PIPELINE_INVALID_CREDENTIALS = "PIPELINE_INVALID_CREDENTIALS" + + # Note that for security reasons the following three error codes are the + # same. This is to prevent enumeration attacks. More information: + # https://web.archive.org/web/20230203194955/https://www.techtarget.com/searchsecurity/tip/What-enumeration-attacks-are-and-how-to-prevent-them + INVALID_CODE = "INVALID_USER_CREDENTIALS" + USER_NOT_FOUND = "INVALID_USER_CREDENTIALS" + INVALID_PASSWORD_OR_CODE = "INVALID_USER_CREDENTIALS" + +# .. but during testing we want to test the different between those three: +if hasattr(settings, 'TESTING') and settings.TESTING: + ErrorCodes.INVALID_CODE = "INVALID_CODE" + ErrorCodes.USER_NOT_FOUND = "USER_NOT_FOUND" + ErrorCodes.INVALID_PASSWORD_OR_CODE = "INVALID_PASSWORD_OR_CODE" def reproducible_json_dumps(s): return json.dumps(s, indent=4, ensure_ascii=False, sort_keys=True, separators=(',', ': ')) @@ -86,8 +100,6 @@ def json_response(data=None, status=200, message="", field=None, error_codename= if status != 200: if not error_codename: error_codename = ErrorCodes.GENERAL_ERROR - if isinstance(error_codename, ErrorCodes): - error_codename = error_codename.value error_data = dict( message=message, field=field,