def post_verification_token(email, token): """ Gives call to party service to add a verification token for the respondent and increase the password reset counter :param email: the respondent's email :param token: the verification token """ logger.info( "Attempting to add respondent verification token and increase password reset counter", email=obfuscate_email(email), ) party_id = get_respondent_by_email(email)["id"] url = f"{app.config['PARTY_URL']}/party-api/v1/respondents/{party_id}/password-verification-token" payload = { "token": token, } response = requests.post(url, auth=app.config["BASIC_AUTH"], json=payload) try: response.raise_for_status() except requests.exceptions.HTTPError: logger.error( "Failed to add respondent verification token or increase password reset counter", email=obfuscate_email(email), ) raise ApiError(logger, response) logger.info( "Successfully added respondent verification token and password reset counter", email=obfuscate_email(email)) return response.json()
def delete_verification_token(token): """ Gives call to party service to delete a verification token for the respondent :param token: the verification token """ email = decode_email_token(token) logger.info("Attempting to delete respondent verification token", email=obfuscate_email(email)) party_id = get_respondent_by_email(email)["id"] url = f"{app.config['PARTY_URL']}/party-api/v1/respondents/{party_id}/password-verification-token/{token}" response = requests.delete(url, auth=app.config["BASIC_AUTH"]) try: response.raise_for_status() except requests.exceptions.HTTPError: if response.status_code == 404: logger.error("Verification token not found") raise NotFound("Token not found") logger.error("Failed to delete respondent verification token", email=obfuscate_email(email)) raise ApiError(logger, response) logger.info("Successfully deleted respondent verification token", email=obfuscate_email(email)) return response.json()
def sign_in(username, password): """ Checks if the users credentials are valid. On success it returns an empty dict (a hangover from when this function used to call a different authentication application). :param username: The username. Should be an email address :param password: The password :raises AuthError: Raised if the credentials provided are incorrect :raises ApiError: Raised on any other non-401 error status code :return: An empty dict if credentials are valid. An exception is raised otherwise """ if app.config["CANARY_GENERATE_ERRORS"]: logger.error("Canary experiment running this error can be ignored", status=500) logger.info("Attempting to sign in", email=obfuscate_email(username)) url = f"{app.config['AUTH_URL']}/api/v1/tokens/" data = { "username": username, "password": password, } headers = { "Accept": "application/json", "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", } response = requests.post(url, headers=headers, auth=app.config["BASIC_AUTH"], data=data) try: response.raise_for_status() except requests.exceptions.HTTPError: if response.status_code == 401: auth_error = response.json().get("detail", "") message = response.json().get("title", "") raise AuthError(logger, response, log_level="warning", message=message, auth_error=auth_error) else: logger.error("Failed to authenticate", email=obfuscate_email(username)) raise ApiError(logger, response) logger.info("Successfully signed in", email=obfuscate_email(username)) return {}
def change_password(session): form = ChangePasswordFrom(request.values) party_id = session.get_party_id() respondent_details = party_controller.get_respondent_party_by_id(party_id) if request.method == 'POST' and form.validate(): username = respondent_details['emailAddress'] password = request.form.get('password') new_password = request.form.get('new_password') if new_password == password: return render_template('account/account-change-password.html', form=form, errors={"new_password": ["Your new password is the same as your old password"]}) bound_logger = logger.bind(email=obfuscate_email(username)) bound_logger.info("Attempting to find user in auth service") try: # We call the sign in function to verify that the password provided is correct auth_controller.sign_in(username, password) bound_logger.info("Attempting to change password via party service") party_controller.change_password(username, new_password) bound_logger.info("password changed via party service") flash('Your password has been changed.') return redirect(url_for('surveys_bp.get_survey_list', tag='todo')) except AuthError as exc: error_message = exc.auth_error if BAD_CREDENTIALS_ERROR in error_message: bound_logger.info('Bad credentials provided') return render_template('account/account-change-password.html', form=form, errors={"password": ["Incorrect current password"]}) else: errors = form.errors return render_template('account/account-change-password.html', form=form, errors=errors)
def notify_party_and_respondent_account_locked(respondent_id, email_address, status=None): bound_logger = logger.bind(respondent_id=respondent_id, email=obfuscate_email(email_address), status=status) bound_logger.info( "Notifying respondent and party service that account is locked") url = f'{app.config["PARTY_URL"]}/party-api/v1/respondents/edit-account-status/{respondent_id}' data = { "respondent_id": respondent_id, "email_address": email_address, "status_change": status } response = requests.put(url, json=data, auth=app.config["BASIC_AUTH"]) try: response.raise_for_status() except requests.exceptions.HTTPError: bound_logger.error("Failed to notify party") bound_logger.unbind("respondent_id", "email", "status") raise ApiError(logger, response) bound_logger.info( "Successfully notified respondent and party service that account is locked" ) bound_logger.unbind("respondent_id", "email", "status")
def create_account(registration_data: dict) -> None: obfuscated_email = obfuscate_email(registration_data["emailAddress"]) enrolment_code = registration_data["enrolmentCode"] logger.info("Attempting to create account", email=obfuscated_email, enrolment_code=enrolment_code) url = f"{app.config['PARTY_URL']}/party-api/v1/respondents" registration_data["status"] = "CREATED" response = requests.post(url, auth=app.config["BASIC_AUTH"], json=registration_data) try: response.raise_for_status() except requests.exceptions.HTTPError: if response.status_code == 409: logger.info("Email has already been used", email=obfuscated_email, enrolment_code=enrolment_code) else: logger.error("Failed to create account", email=obfuscated_email, enrolment_code=enrolment_code) raise ApiError(logger, response, message=response.json()) logger.info("Successfully created account", email=obfuscated_email, enrolment_code=enrolment_code)
def change_password(email, password): bound_logger = logger.bind(email=obfuscate_email(email)) bound_logger.info( "Attempting to change password through the party service") data = {"email_address": email, "new_password": password} url = f"{app.config['PARTY_URL']}/party-api/v1/respondents/change_password" response = requests.put(url, auth=app.config["BASIC_AUTH"], json=data) try: response.raise_for_status() except requests.exceptions.HTTPError: bound_logger.error( "Failed to send change password request to party service") raise ApiError(logger, response) bound_logger.info( "Successfully changed password through the party service")
def test_obfuscate_email(self): """Tests the output of obfuscate email with both valid and invalid strings""" testAddresses = { "*****@*****.**": "e*****e@e*********m", "*****@*****.**": "p****x@d**********k", "*****@*****.**": "f********e@p**********k", "*****@*****.**": "m*********n@g*******m", "*****@*****.**": "a***********e@e*********m", "*****@*****.**": "j**************6@l****************k", "[email protected]": "m**?@e*********m", "*****@*****.**": "m@m***m", "joe.bloggs": "j********s", "joe.bloggs@": "j********s", "@gmail.com": "@g*******m" } for test in testAddresses: self.assertEqual(obfuscate_email(test), testAddresses[test])
def delete_account(username: str): bound_logger = logger.bind(email=obfuscate_email(username)) bound_logger.info("Attempting to delete account") url = f'{app.config["AUTH_URL"]}/api/account/user' # force_delete will always be true if deletion is initiated by user form_data = {"username": username, "force_delete": True} response = requests.delete(url, data=form_data, auth=app.config["BASIC_AUTH"]) try: response.raise_for_status() except requests.exceptions.HTTPError: bound_logger.error("Failed to delete account") bound_logger.unbind("email") raise ApiError(logger, response) bound_logger.info("Successfully deleted account") bound_logger.unbind("email") return response
def reset_password_request(username): bound_logger = logger.bind(email=obfuscate_email(username)) bound_logger.info( "Attempting to send reset password request to party service") url = f"{app.config['PARTY_URL']}/party-api/v1/respondents/request_password_change" data = {"email_address": username} response = requests.post(url, auth=app.config["BASIC_AUTH"], json=data) try: response.raise_for_status() except requests.exceptions.HTTPError: if response.status_code == 404: raise UserDoesNotExist("User does not exist in party service") bound_logger.error( "Failed to send reset password request to party service") raise ApiError(logger, response) bound_logger.info( "Successfully sent reset password request to party service")
def get_respondent_by_email(email): bound_logger = logger.bind(email=obfuscate_email(email)) bound_logger.info("Attempting to find respondent party by email") url = f"{app.config['PARTY_URL']}/party-api/v1/respondents/email" response = requests.get(url, json={"email": email}, auth=app.config["BASIC_AUTH"]) if response.status_code == 404: bound_logger.info("Failed to retrieve party by email") return try: response.raise_for_status() except requests.exceptions.HTTPError: bound_logger.error("Error retrieving respondent by email") raise ApiError(logger, response) bound_logger.info("Successfully retrieved respondent by email") return response.json()
def sign_in(username, password): if app.config["CANARY_GENERATE_ERRORS"]: logger.error("Canary experiment running this error can be ignored", status=500) bound_logger = logger.bind(email=obfuscate_email(username)) bound_logger.info('Attempting to sign in') url = f"{app.config['AUTH_URL']}/api/v1/tokens/" data = { 'username': username, 'password': password, } headers = { 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', } response = requests.post(url, headers=headers, auth=app.config['BASIC_AUTH'], data=data) try: response.raise_for_status() except requests.exceptions.HTTPError: if response.status_code == 401: auth_error = response.json().get('detail', '') message = response.json().get('title', '') raise AuthError(logger, response, log_level='warning', message=message, auth_error=auth_error) else: bound_logger.error('Failed to authenticate') raise ApiError(logger, response) bound_logger.info('Successfully signed in') return {}
def change_password(session): form = ChangePasswordFrom(request.values) party_id = session.get_party_id() respondent_details = party_controller.get_respondent_party_by_id(party_id) if request.method == "POST" and form.validate(): username = respondent_details["emailAddress"] password = request.form.get("password") new_password = request.form.get("new_password") if new_password == password: return render_template( "account/account-change-password.html", form=form, errors={"new_password": ["Your new password is the same as your old password"]}, ) bound_logger = logger.bind(email=obfuscate_email(username)) bound_logger.info("Attempting to find user in auth service") try: # We call the sign in function to verify that the password provided is correct auth_controller.sign_in(username, password) bound_logger.info("Attempting to change password via party service") party_controller.change_password(username, new_password) bound_logger.info("password changed via party service") flash("Your password has been changed. Please login with your new password.", "success") return redirect(url_for("sign_in_bp.logout")) except AuthError as exc: error_message = exc.auth_error if BAD_CREDENTIALS_ERROR in error_message: bound_logger.info("Bad credentials provided") return render_template( "account/account-change-password.html", form=form, errors={"password": ["Incorrect current password"]}, ) else: errors = form.errors return render_template("account/account-change-password.html", form=form, errors=errors)
def login(): # noqa: C901 form = LoginForm(request.form) form.username.data = form.username.data.strip() account_activated = request.args.get('account_activated', None) secure = app.config['WTF_CSRF_ENABLED'] if request.method == 'POST' and form.validate(): username = form.username.data password = request.form.get('password') bound_logger = logger.bind(email=obfuscate_email(username)) bound_logger.info("Attempting to find user in auth service") try: auth_controller.sign_in(username, password) except AuthError as exc: error_message = exc.auth_error party_json = party_controller.get_respondent_by_email(username) party_id = party_json.get('id') if party_json else None bound_logger = bound_logger.bind(party_id=party_id) if USER_ACCOUNT_LOCKED in error_message: # pylint: disable=no-else-return if not party_id: bound_logger.error("Respondent account locked in auth but doesn't exist in party") return render_template('sign-in/sign-in.html', form=form, data={"error": {"type": "failed"}}) bound_logger.info('User account is locked on the Auth server', status=party_json['status']) if party_json['status'] == 'ACTIVE' or party_json['status'] == 'CREATED': notify_party_and_respondent_account_locked(respondent_id=party_id, email_address=username, status='SUSPENDED') return render_template('sign-in/sign-in.account-locked.html', form=form) elif NOT_VERIFIED_ERROR in error_message: bound_logger.info('User account is not verified on the Auth server') return render_template('sign-in/sign-in.account-not-verified.html', party_id=party_id) elif BAD_AUTH_ERROR in error_message: bound_logger.info('Bad credentials provided') elif UNKNOWN_ACCOUNT_ERROR in error_message: bound_logger.info('User account does not exist in auth service') else: bound_logger.error('Unexpected error was returned from Auth service', auth_error=error_message) return render_template('sign-in/sign-in.html', form=form, data={"error": {"type": "failed"}}, next=request.args.get('next')) bound_logger.info("Successfully found user in auth service. Attempting to find user in party service") party_json = party_controller.get_respondent_by_email(username) if not party_json or 'id' not in party_json: bound_logger.error("Respondent has an account in auth but not in party") return render_template('sign-in/sign-in.html', form=form, data={"error": {"type": "failed"}}) party_id = party_json['id'] bound_logger = bound_logger.bind(party_id=party_id) if request.args.get('next'): response = make_response(redirect(request.args.get('next'))) else: response = make_response(redirect(url_for('surveys_bp.get_survey_list', tag='todo', _external=True, _scheme=getenv('SCHEME', 'http')))) bound_logger.info("Successfully found user in party service") bound_logger.info('Creating session') session = Session.from_party_id(party_id) response.set_cookie('authorization', value=session.session_key, expires=session.get_expires_in(), secure=secure, httponly=secure) count = conversation_controller.get_message_count_from_api(session) session.set_unread_message_total(count) bound_logger.info('Successfully created session', session_key=session.session_key) return response template_data = { "error": { "type": form.errors, "logged_in": "False" }, 'account_activated': account_activated } if request.args.get('next'): return render_template('sign-in/sign-in.html', form=form, data=template_data, next=request.args.get('next')) return render_template('sign-in/sign-in.html', form=form, data=template_data)
def register_enter_your_details(): # Get and decrypt enrolment code encrypted_enrolment_code = request.args.get("encrypted_enrolment_code") try: enrolment_code = cryptographer.decrypt( encrypted_enrolment_code.encode()).decode() except AttributeError: logger.error("No enrolment code supplied", exc_info=True, url=request.url) raise form = RegistrationForm(request.values, enrolment_code=encrypted_enrolment_code) if form.email_address.data is not None: form.email_address.data = form.email_address.data.strip() # Validate enrolment code before rendering or checking the form iac_controller.validate_enrolment_code(enrolment_code) if request.method == "POST" and form.validate(): email_address = form.email_address.data registration_data = { "emailAddress": email_address, "firstName": request.form.get("first_name"), "lastName": request.form.get("last_name"), "password": request.form.get("password"), "telephone": request.form.get("phone_number"), "enrolmentCode": enrolment_code, } try: party_controller.create_account(registration_data) except ApiError as exc: if exc.status_code == 400: # If party returns an error, we should just log out the error with as much detail as possible, and # put a generic message up for the user as we don't want to show them any potentially ugly messages # from party logger.info( "Party returned an error", email=obfuscate_email(email_address), enrolment_code=enrolment_code, error=exc.message, ) flash("Something went wrong, please try again or contact us", "error") return render_template( "register/register.enter-your-details.html", form=form, errors=form.errors) elif exc.status_code == 409: error = { "email_address": [ "This email has already been used to register an account" ] } return render_template( "register/register.enter-your-details.html", form=form, errors=error) else: logger.error("Failed to create account", status=exc.status_code, error=exc.message) raise exc return render_template("register/register.almost-done.html", email=email_address) else: return render_template("register/register.enter-your-details.html", form=form, errors=form.errors)
def login(): # noqa: C901 form = LoginForm(request.form) if form.username.data is not None: form.username.data = form.username.data.strip() if request.method == "POST" and form.validate(): username = form.username.data password = request.form.get("password") bound_logger = logger.bind(email=obfuscate_email(username)) bound_logger.info("Attempting to find user in auth service") try: auth_controller.sign_in(username, password) except AuthError as exc: error_message = exc.auth_error party_json = party_controller.get_respondent_by_email(username) party_id = party_json.get("id") if party_json else None bound_logger = bound_logger.bind(party_id=party_id) if USER_ACCOUNT_LOCKED in error_message: if not party_id: bound_logger.error( "Respondent account locked in auth but doesn't exist in party" ) return render_template("sign-in/sign-in.html", form=form, data={"error": { "type": "failed" }}) bound_logger.info("User account is locked on the Auth server", status=party_json["status"]) if party_json["status"] == "ACTIVE" or party_json[ "status"] == "CREATED": notify_party_and_respondent_account_locked( respondent_id=party_id, email_address=username, status="SUSPENDED") return render_template("sign-in/sign-in.account-locked.html", form=form) elif NOT_VERIFIED_ERROR in error_message: bound_logger.info( "User account is not verified on the Auth server") return render_template( "sign-in/sign-in.account-not-verified.html", party_id=party_id) elif BAD_AUTH_ERROR in error_message: bound_logger.info("Bad credentials provided") elif UNKNOWN_ACCOUNT_ERROR in error_message: bound_logger.info( "User account does not exist in auth service") elif USER_ACCOUNT_DELETED in error_message: bound_logger.info("User account is marked for deletion") else: bound_logger.error( "Unexpected error was returned from Auth service", auth_error=error_message) logger.unbind("email") return render_template("sign-in/sign-in.html", form=form, data={"error": { "type": "failed" }}) bound_logger.info( "Successfully found user in auth service. Attempting to find user in party service" ) party_json = party_controller.get_respondent_by_email(username) if not party_json or "id" not in party_json: bound_logger.error( "Respondent has an account in auth but not in party") return render_template("sign-in/sign-in.html", form=form, data={"error": { "type": "failed" }}) party_id = party_json["id"] bound_logger = bound_logger.bind(party_id=party_id) if session.get("next"): response = make_response(redirect(session.get("next"))) session.pop("next") else: response = make_response( redirect( url_for("surveys_bp.get_survey_list", tag="todo", _external=True, _scheme=getenv("SCHEME", "http")))) bound_logger.info("Successfully found user in party service") bound_logger.info("Creating session") redis_session = Session.from_party_id(party_id) secure = app.config["WTF_CSRF_ENABLED"] response.set_cookie( "authorization", value=redis_session.session_key, expires=redis_session.get_expires_in(), secure=secure, httponly=secure, samesite="strict", ) count = conversation_controller.get_message_count_from_api( redis_session) redis_session.set_unread_message_total(count) bound_logger.info("Successfully created session", session_key=redis_session.session_key) bound_logger.unbind("email") return response account_activated = request.args.get("account_activated", None) template_data = { "error": { "type": form.errors, "logged_in": "False" }, "account_activated": account_activated } return render_template("sign-in/sign-in.html", form=form, data=template_data)