def post_email(user, email, verified, primary): proofing_user = ProofingUser.from_user(user, current_app.private_userdb) current_app.logger.debug('Trying to save unconfirmed email {!r} ' 'for user {}'.format(email, proofing_user)) new_mail = MailAddress.from_dict( dict(email=email, created_by='email', verified=False, primary=False)) try: proofing_user.mail_addresses.add(new_mail) except DuplicateElementViolation: return error_response(message=EmailMsg.dupe) try: save_and_sync_user(proofing_user) except UserOutOfSync: current_app.logger.debug('Couldnt save email {} for user {}, ' 'data out of sync'.format( email, proofing_user)) return error_response(message=CommonMsg.out_of_sync) current_app.logger.info('Saved unconfirmed email {!r} ' 'for user {}'.format(email, proofing_user)) current_app.stats.count(name='email_save_unconfirmed_email', value=1) sent = send_verification_code(email, proofing_user) emails = {'emails': proofing_user.mail_addresses.to_list_of_dicts()} email_list = EmailListPayload().dump(emails) if not sent: return success_response(payload=email_list, message=EmailMsg.added_and_throttled) current_app.stats.count(name='email_send_verification_code', value=1) return success_response(payload=email_list, message=EmailMsg.saved)
def _do_action(): action_type = session.get('current_plugin') if not action_type: abort(403) plugin_obj = current_app.plugins[action_type]() old_format = 'user_oid' in session['current_action'] action = Action(data=session['current_action'], old_format=old_format) try: data = plugin_obj.perform_step(action) except plugin_obj.ActionError as exc: return _aborted(action, exc) except plugin_obj.ValidationError as exc: errors = exc.args[0] current_app.logger.info( 'Validation error {} for step {} of action {}'.format( errors, session['current_step'], action)) session['current_step'] -= 1 return error_response(payload={'errors': errors}, message=CommonMsg.form_errors) eppn = session.get('user_eppn') if session['total_steps'] == session['current_step']: current_app.logger.info( 'Finished pre-login action {} for eppn {}'.format( action.action_type, eppn)) return success_response(payload=dict(data=data), message=ActionsMsg.action_completed) current_app.logger.info( 'Performed step {} for action {} for eppn {}'.format( action.action_type, session['current_step'], eppn)) session['current_step'] += 1 return success_response(payload={'data': data}, message=None)
def get_groups(user: User) -> FluxData: scim_user = get_scim_user_by_eppn(user.eppn) if not scim_user: current_app.logger.info('User does not exist in scimapi_userdb') # As the user does not exist return empty group lists return success_response(payload={}) payload = get_all_group_data(scim_user) return success_response(payload=payload)
def add_nin(user, nin): security_user = SecurityUser.from_user(user, current_app.private_userdb) current_app.logger.info('Removing NIN from user') current_app.logger.debug('NIN: {}'.format(nin)) nin_obj = security_user.nins.find(nin) if nin_obj: current_app.logger.info('NIN already added.') return error_response(message=SecurityMsg.already_exists) try: nin_element = NinProofingElement.from_dict( dict(number=nin, created_by='security', verified=False)) proofing_state = NinProofingState.from_dict({ 'eduPersonPrincipalName': security_user.eppn, 'nin': nin_element.to_dict() }) add_nin_to_user(user, proofing_state, user_class=SecurityUser) return success_response( payload=dict(nins=security_user.nins.to_list_of_dicts()), message=SecurityMsg.add_success) except AmTaskFailed as e: current_app.logger.error('Adding nin to user failed') current_app.logger.debug(f'NIN: {nin}') current_app.logger.error('{}'.format(e)) return error_response(message=CommonMsg.temp_problem)
def registration_complete(user, credential_id, attestation_object, client_data, description): security_user = SecurityUser.from_user(user, current_app.private_userdb) server = get_webauthn_server(current_app.config.fido2_rp_id) att_obj = AttestationObject(urlsafe_b64decode(attestation_object)) cdata_obj = ClientData(urlsafe_b64decode(client_data)) state = session['_webauthn_state_'] auth_data = server.register_complete(state, cdata_obj, att_obj) cred_data = auth_data.credential_data current_app.logger.debug('Proccessed Webauthn credential data: {}.'.format(cred_data)) credential = Webauthn.from_dict( dict( keyhandle=credential_id, credential_data=base64.urlsafe_b64encode(cred_data).decode('ascii'), app_id=current_app.config.fido2_rp_id, attest_obj=base64.b64encode(attestation_object.encode('utf-8')).decode('ascii'), description=description, created_by='security', ) ) security_user.credentials.add(credential) save_and_sync_user(security_user) current_app.stats.count(name='webauthn_register_complete') current_app.logger.info('User {} has completed registration of a webauthn token'.format(security_user)) credentials = compile_credential_list(security_user) return success_response(payload=dict(credentials=credentials), message=SecurityMsg.webauthn_success)
def resend_code(user, number): """ view to resend a new verification code for one of the (unverified) phone numbers of the logged in user. Returns a listing of all phones for the logged in user. """ current_app.logger.debug( 'Trying to send new verification code for phone number ' ' {!r} for user {}'.format(number, user)) if not user.phone_numbers.find(number): current_app.logger.warning( 'Unknown phone in resend_code_action, user {}'.format(user)) return error_response(message=CommonMsg.out_of_sync) sent = send_verification_code(user, number) if not sent: return error_response(message=PhoneMsg.still_valid_code) current_app.logger.debug('New verification code sent to ' 'phone number {!r} for user {}'.format( number, user)) current_app.stats.count(name='mobile_resend_code', value=1) phones = {'phones': user.phone_numbers.to_list_of_dicts()} return success_response(payload=phones, message=PhoneMsg.resend_success)
def post_phone(user, number, verified, primary): """ view to add a new phone to the user data of the currently logged in user. Returns a listing of all phones for the logged in user. """ proofing_user = ProofingUser.from_user(user, current_app.private_userdb) current_app.logger.debug('Trying to save unconfirmed phone number {!r} ' 'for user {}'.format(number, proofing_user)) new_phone = PhoneNumber.from_dict( dict(number=number, created_by='phone', verified=False, primary=False)) proofing_user.phone_numbers.add(new_phone) try: save_and_sync_user(proofing_user) except UserOutOfSync: current_app.logger.debug('Couldnt save phone number {!r} for user {}, ' 'data out of sync'.format( number, proofing_user)) return error_response(message=CommonMsg.out_of_sync) current_app.logger.info('Saved unconfirmed phone number {!r} ' 'for user {}'.format(number, proofing_user)) current_app.stats.count(name='mobile_save_unconfirmed_mobile', value=1) send_verification_code(proofing_user, number) current_app.stats.count(name='mobile_send_verification_code', value=1) phones = {'phones': proofing_user.phone_numbers.to_list_of_dicts()} return success_response(payload=phones, message=PhoneMsg.save_success)
def init_reset_pw(email: str) -> FluxData: """ View that receives an email address to initiate a reset password process. It returns a message informing of the result of the operation. Preconditions required for the call to succeed: * There is a valid user corresponding to the received email address. As side effects, this view will: * Create a PasswordResetEmailState in the password_reset_state_db (holding the email address, the eppn of the user associated to the email address in the central userdb, and a freshly generated random hash as an identifier code for the created state); * Email the generated code to the received email address. The operation can fail due to: * The email address does not correspond to any valid user in the central db; * There is some problem sending the email. """ current_app.logger.info(f'Trying to send password reset email to {email}') try: send_password_reset_mail(email) except BadCode as error: current_app.logger.error( f'Sending password reset e-mail for {email} failed: {error}') return error_response(message=error.msg) return success_response(message=ResetPwMsg.send_pw_success)
def set_new_pw_extra_security_phone(code: str, password: str, phone_code: str) -> FluxData: """ View that receives an emailed reset password code, an SMS'ed reset password code, and a password, and sets the password as credential for the user, with extra security. Preconditions required for the call to succeed: * A PasswordResetEmailAndPhoneState object in the password_reset_state_db keyed by the received codes. * A flag in said state object indicating that the emailed code has already been verified. As side effects, this view will: * Compare the received password with the hash in the session to mark it accordingly (as suggested or as custom); * Revoke all password credentials the user had; This operation may fail due to: * The codes do not correspond to a valid state in the db; * Any of the codes have expired; * No valid user corresponds to the eppn stored in the state; * Communication problems with the VCCS backend; * Synchronization problems with the central user db. """ if not password: return error_response(message=ResetPwMsg.chpass_no_data) data = _load_data(code, password) if data.error: return error_response(message=data.error) if not isinstance(data.state, ResetPasswordEmailAndPhoneState): raise TypeError( f'State is not ResetPasswordEmailAndPhoneState ({type(data.state)})' ) if phone_code == data.state.phone_code.code: if not verify_phone_number(data.state): current_app.logger.info( f'Could not verify phone code for {data.state.eppn}') return error_response(message=ResetPwMsg.phone_invalid) current_app.logger.info(f'Phone code verified for {data.state.eppn}') current_app.stats.count( name='reset_password_extra_security_phone_success') else: current_app.logger.info( f'Could not verify phone code for {data.state.eppn}') return error_response(message=ResetPwMsg.unknown_phone_code) current_app.logger.info(f'Resetting password for user {data.user}') reset_user_password(data.user, data.state, password) current_app.logger.info( f'Password reset done, removing state for {data.user}') current_app.password_reset_state_db.remove_state(data.state) return success_response(message=ResetPwMsg.pw_resetted)
def resend_email_verification(email): """ The user has not yet verified the email address. Send a verification message to the address so it can be verified. """ current_app.logger.debug("Resend email confirmation to {!s}".format(email)) send_verification_mail(email) current_app.stats.count(name='resend_code') return success_response(message=SignupMsg.resent_success)
def config_reset_pw(code: str) -> FluxData: """ View that receives an emailed reset password code and returns the configuration needed for the reset password form. Preconditions required for the call to succeed: * A PasswordResetEmailState object in the password_reset_state_db keyed by the received code. The configuration returned (in case of success) will include: * The received code; * A newly generated suggested password; * In case the user corresponding to the email address has verified phone numbers, these will be sent (masked) to allow the user to use extra security. (If the user does not use extra security, any verified NIN or phone number will be unverified upon resetting the password). As side effects, this view will: * Create a MailAddressProofing element in the proofing_log; * Set the email_code.is_verified flag in the PasswordResetEmailState object; * Set a hash of the generated password in the session. This operation may fail due to: * The code does not correspond to a valid state in the db; * The code has expired; * No valid user corresponds to the eppn stored in the state. """ current_app.logger.info(f'Configuring password reset form for {code}') try: state = get_pwreset_state(code) except BadCode as e: return error_response(message=e.msg) verify_email_address(state) new_password = generate_suggested_password() session.reset_password.generated_password_hash = hash_password( new_password) user = current_app.central_userdb.get_user_by_eppn(state.eppn) alternatives = get_extra_security_alternatives(user, SESSION_PREFIX) state.extra_security = alternatives current_app.password_reset_state_db.save(state) return success_response(payload={ 'csrf_token': session.get_csrf_token(), 'suggested_password': new_password, 'email_code': state.email_code.code, 'email_address': state.email_address, 'extra_security': mask_alternatives(alternatives), 'password_entropy': current_app.config.password_entropy, 'password_length': current_app.config.password_length, 'password_service_url': current_app.config.password_service_url, 'zxcvbn_terms': get_zxcvbn_terms(state.eppn), }, )
def get_suggested(user) -> FluxData: """ View to get a suggested password for the logged user. """ current_app.logger.debug(f'Sending new generated password for {user}') password = generate_suggested_password() session.reset_password.generated_password_hash = hash_password(password) return success_response(payload={'suggested_password': password}, message=None)
def complete_registration(signup_user) -> FluxData: """ After a successful registration: * record acceptance of TOU * generate a password, * add it to the user record, * update the attribute manager db with the new account, * create authn token for the dashboard, * return information to be sent to the user. :param signup_user: SignupUser instance :type signup_user: SignupUser :return: registration status info """ current_app.logger.info("Completing registration for user " "{}".format(signup_user)) context = {} password_id = ObjectId() (password, salt) = generate_password(str(password_id), signup_user) credential = Password.from_dict( dict(credential_id=password_id, salt=salt, is_generated=True, created_by='signup')) signup_user.passwords.add(credential) # Record the acceptance of the terms of use record_tou(signup_user, 'signup') try: save_and_sync_user(signup_user) except UserOutOfSync: current_app.logger.error('Couldnt save user {}, ' 'data out of sync'.format(signup_user)) return error_response(message='user-out-of-sync') timestamp = datetime.datetime.fromtimestamp(int(time.time())) if session.common is not None: # please mypy session.common.eppn = signup_user.eppn if session.signup is not None: # please mypy session.signup.ts = timestamp context.update({ "status": 'verified', "password": password, "email": signup_user.mail_addresses.primary.email, "dashboard_url": current_app.config.signup_authn_url, }) current_app.stats.count(name='signup_complete') current_app.logger.info( "Signup process for new user {} complete".format(signup_user)) return success_response(payload=context)
def get_all_data(user: User) -> FluxData: payload = {} scim_user = get_scim_user_by_eppn(user.eppn) if scim_user: # The user can only have group data if there is a scim user payload.update(get_all_group_data(scim_user)) # Update payload with incoming and outgoing invites payload.update({ 'incoming': get_incoming_invites(user), 'outgoing': get_outgoing_invites(user) }) return success_response(payload=payload)
def remove(user, credential_key): security_user = SecurityUser.from_user(user, current_app.private_userdb) token_to_remove = security_user.credentials.filter(U2F).find( credential_key) if token_to_remove: security_user.credentials.remove(credential_key) save_and_sync_user(security_user) current_app.stats.count(name='u2f_token_remove') credentials = compile_credential_list(security_user) return success_response(payload=dict(credentials=credentials), message=SecurityMsg.rm_u2f_success)
def to_response(self): """ Create a response with information about the users current proofing state (or an error).""" if self.error: return error_response(message=self.message) return success_response( { 'letter_sent': self.sent, 'letter_expires': self.expires, 'letter_expired': self.is_expired, }, message=self.message, )
def trycaptcha(email, recaptcha_response, tou_accepted): """ Kantara requires a check for humanness even at level AL1. """ if not tou_accepted: return error_response(message=SignupMsg.no_tou) config = current_app.config recaptcha_verified = False # add a backdoor to bypass recaptcha checks for humanness, # to be used in testing environments for automated integration tests. if check_magic_cookie(config): current_app.logger.info( 'Using BACKDOOR to verify reCaptcha during signup!') recaptcha_verified = True # common path with no backdoor if not recaptcha_verified: remote_ip = request.remote_addr recaptcha_public_key = config.recaptcha_public_key if recaptcha_public_key: recaptcha_private_key = config.recaptcha_private_key recaptcha_verified = verify_recaptcha(recaptcha_private_key, recaptcha_response, remote_ip) else: recaptcha_verified = False current_app.logger.info('Missing configuration for reCaptcha!') if recaptcha_verified: next = check_email_status(email) if next == 'new': # Workaround for failed earlier sync of user to userdb: Remove any signup_user with this e-mail address. remove_users_with_mail_address(email) send_verification_mail(email) return success_response(payload=dict(next=next), message=SignupMsg.reg_new) elif next == 'resend-code': return {'next': next} elif next == 'address-used': current_app.stats.count(name='address_used_error') return error_response(payload=dict(next=next), message=SignupMsg.email_used) return error_response(message=SignupMsg.no_recaptcha)
def post_remove(user, email): proofing_user = ProofingUser.from_user(user, current_app.private_userdb) current_app.logger.debug('Trying to remove email address {!r} ' 'from user {}'.format(email, proofing_user)) emails = proofing_user.mail_addresses.to_list() verified_emails = proofing_user.mail_addresses.verified.to_list() # Do not let the user remove all mail addresses if len(emails) == 1: current_app.logger.debug( 'Cannot remove the last address: {}'.format(email)) return error_response(message=EmailMsg.cannot_remove_last) # Do not let the user remove all verified mail addresses if len(verified_emails) == 1 and verified_emails[0].email == email: current_app.logger.debug( 'Cannot remove last verified address: {}'.format(email)) return error_response(message=EmailMsg.cannot_remove_last_verified) try: proofing_user.mail_addresses.remove(email) except PrimaryElementViolation: # Trying to remove the primary mail address, set next verified mail address as primary other_verified = [ address for address in verified_emails if address.email != email ] proofing_user.mail_addresses.primary = other_verified[0].email # Now remove the unwanted and previous primary mail address proofing_user.mail_addresses.remove(email) try: save_and_sync_user(proofing_user) except UserOutOfSync: current_app.logger.debug( 'Could not remove email {} for user, data out of sync'.format( email)) return error_response(message=CommonMsg.out_of_sync) current_app.logger.info('Email address {} removed'.format(email)) current_app.stats.count(name='email_remove_success', value=1) emails = {'emails': proofing_user.mail_addresses.to_list_of_dicts()} email_list = EmailListPayload().dump(emails) return success_response(payload=email_list, message=EmailMsg.removal_success)
def verify(user, code, number): """ view to mark one of the (unverified) phone numbers of the logged in user as verified. Returns a listing of all phones for the logged in user. """ proofing_user = ProofingUser.from_user(user, current_app.private_userdb) current_app.logger.debug( 'Trying to save phone number {} as verified'.format(number)) db = current_app.proofing_statedb try: state = db.get_state_by_eppn_and_mobile(proofing_user.eppn, number) timeout = current_app.config.phone_verification_timeout if state.is_expired(timeout): current_app.logger.info( "Proofing state is expired. Removing the state.") current_app.logger.debug("Proofing state: {!r}".format(state)) current_app.proofing_statedb.remove_state(state) return error_response(message=PhoneMsg.code_invalid) except DocumentDoesNotExist: current_app.logger.info( "Could not find proofing state for number {}".format(number)) return error_response(message=PhoneMsg.unknown_phone) if code == state.verification.verification_code: try: verify_phone_number(state, proofing_user) current_app.logger.info('Phone number successfully verified') current_app.logger.debug('Phone number: {}'.format(number)) phones = { 'phones': proofing_user.phone_numbers.to_list_of_dicts(), } return success_response(payload=phones, message=PhoneMsg.verify_success) except UserOutOfSync: current_app.logger.info( 'Could not confirm phone number, data out of sync') current_app.logger.debug('Phone number: {}'.format(number)) return error_response(message=CommonMsg.out_of_sync) current_app.logger.info("Invalid verification code") current_app.logger.debug("Proofing state: {!r}".format(state)) return error_response(message=PhoneMsg.code_invalid)
def resend_code(user, email): current_app.logger.debug('Trying to send new verification code for email ' 'address {} for user {}'.format(email, user)) if not user.mail_addresses.find(email): current_app.logger.debug('Unknown email {!r} in resend_code_action,' ' user {}'.format(email, user)) return error_response(message=CommonMsg.out_of_sync) sent = send_verification_code(email, user) if not sent: return error_response(message=EmailMsg.still_valid_code) current_app.logger.debug('New verification code sent to ' 'address {} for user {}'.format(email, user)) current_app.stats.count(name='email_resend_code', value=1) emails = {'emails': user.mail_addresses.to_list_of_dicts()} return success_response(payload=emails, message=EmailMsg.code_sent)
def verify(user, code, email): """ """ proofing_user = ProofingUser.from_user(user, current_app.private_userdb) current_app.logger.debug( 'Trying to save email address {} as verified'.format(email)) db = current_app.proofing_statedb try: state = db.get_state_by_eppn_and_email(proofing_user.eppn, email) timeout = current_app.config.email_verification_timeout if state.is_expired(timeout): current_app.logger.info( "Verification code is expired. Removing the state") current_app.logger.debug("Proofing state: {}".format(state)) current_app.proofing_statedb.remove_state(state) return error_response(message=EmailMsg.invalid_code) except DocumentDoesNotExist: current_app.logger.info( 'Could not find proofing state for email {}'.format(email)) return error_response(message=EmailMsg.unknown_email) if code == state.verification.verification_code: try: verify_mail_address(state, proofing_user) current_app.logger.info('Email successfully verified') current_app.logger.debug('Email address: {}'.format(email)) emails = { 'emails': proofing_user.mail_addresses.to_list_of_dicts(), } email_list = EmailListPayload().dump(emails) return success_response(payload=email_list, message=EmailMsg.verify_success) except UserOutOfSync: current_app.logger.info( 'Could not confirm email, data out of sync') current_app.logger.debug('Mail address: {}'.format(email)) return error_response(message=CommonMsg.out_of_sync) current_app.logger.info("Invalid verification code") current_app.logger.debug("Email address: {}".format( state.verification.email)) return error_response(message=EmailMsg.invalid_code)
def post_user(user, given_name, surname, display_name, language): personal_data_user = PersonalDataUser.from_user(user, current_app.private_userdb) current_app.logger.debug('Trying to save user {}'.format(user)) personal_data_user.given_name = given_name personal_data_user.surname = surname personal_data_user.display_name = display_name personal_data_user.language = language try: save_and_sync_user(personal_data_user) except UserOutOfSync: return error_response(message=CommonMsg.out_of_sync) current_app.stats.count(name='personal_data_saved', value=1) current_app.logger.info( 'Saved personal data for user {}'.format(personal_data_user)) personal_data = personal_data_user.to_dict() return success_response(payload=personal_data, message=PDataMsg.save_success)
def remove_nin(user, nin): security_user = SecurityUser.from_user(user, current_app.private_userdb) current_app.logger.info('Removing NIN from user') current_app.logger.debug('NIN: {}'.format(nin)) nin_obj = security_user.nins.find(nin) if nin_obj and nin_obj.is_verified: current_app.logger.info('NIN verified. Will not remove it.') return error_response(message=SecurityMsg.rm_verified) try: remove_nin_from_user(security_user, nin) return success_response( payload=dict(nins=security_user.nins.to_list_of_dicts()), message=SecurityMsg.rm_success) except AmTaskFailed as e: current_app.logger.error('Removing nin from user failed') current_app.logger.debug(f'NIN: {nin}') current_app.logger.error('{}'.format(e)) return error_response(message=CommonMsg.temp_problem)
def post_remove(user, number): """ view to remove one of the phone numbers of the logged in user. Returns a listing of all phones for the logged in user. """ proofing_user = ProofingUser.from_user(user, current_app.private_userdb) current_app.logger.debug('Trying to remove phone number {!r} ' 'from user {}'.format(number, proofing_user)) try: proofing_user.phone_numbers.remove(number) except PrimaryElementViolation: current_app.logger.info('Removing primary phone number') current_app.logger.debug('Phone number: {}.'.format(number)) verified = proofing_user.phone_numbers.verified.to_list() new_index = 1 if verified[0].number == number else 0 proofing_user.phone_numbers.primary = verified[new_index].number proofing_user.phone_numbers.remove(number) except UserDBValueError: current_app.logger.info('Tried to remove a non existing phone number') current_app.logger.debug('Phone number: {}.'.format(number)) return error_response(message=PhoneMsg.unknown_phone) try: save_and_sync_user(proofing_user) except UserOutOfSync: current_app.logger.debug('Couldnt remove phone number {!r} for user' ' {}, data out of sync'.format( number, proofing_user)) return error_response(message=CommonMsg.out_of_sync) current_app.logger.info('Phone number {!r} removed ' 'for user {}'.format(number, proofing_user)) current_app.stats.count(name='mobile_remove_success', value=1) phones = {'phones': proofing_user.phone_numbers.to_list_of_dicts()} return success_response(payload=phones, message=PhoneMsg.removal_success)
def post_primary(user, number): """ view to mark one of the (verified) phone numbers of the logged in user as the primary phone number. Returns a listing of all phones for the logged in user. """ proofing_user = ProofingUser.from_user(user, current_app.private_userdb) current_app.logger.debug( 'Trying to save phone number {} as primary'.format(number)) phone_element = proofing_user.phone_numbers.find(number) if not phone_element: current_app.logger.debug( 'Could not save phone number {} as primary, data out of sync'. format(number)) return error_response(message=CommonMsg.out_of_sync) if not phone_element.is_verified: current_app.logger.debug( 'Could not save phone number {} as primary, phone number unconfirmed' .format(number)) return error_response(message=PhoneMsg.unconfirmed_primary) proofing_user.phone_numbers.primary = phone_element.number try: save_and_sync_user(proofing_user) except UserOutOfSync: current_app.logger.debug( 'Could not save phone number {} as primary, data out of sync'. format(number)) return error_response(message=CommonMsg.out_of_sync) current_app.logger.info('Phone number {} made primary'.format(number)) current_app.stats.count(name='mobile_set_primary', value=1) phones = {'phones': proofing_user.phone_numbers.to_list_of_dicts()} return success_response(payload=phones, message=PhoneMsg.primary_success)
def set_new_pw(code: str, password: str) -> FluxData: """ View that receives an emailed reset password code and a password, and sets the password as credential for the user, with no extra security. Preconditions required for the call to succeed: * A PasswordResetEmailState object in the password_reset_state_db keyed by the received code. * A flag in said state object indicating that the emailed code has already been verified. As side effects, this view will: * Compare the received password with the hash in the session to mark it accordingly (as suggested or as custom); * Revoke all password credentials the user had; * Unverify any verified phone number or NIN the user previously had. This operation may fail due to: * The code does not correspond to a valid state in the db; * The code has expired; * No valid user corresponds to the eppn stored in the state; * Communication problems with the VCCS backend; * Synchronization problems with the central user db. """ if not password or not code: return error_response(message=ResetPwMsg.missing_data) data = _load_data(code, password) if data.error: return error_response(message=data.error) current_app.logger.info(f'Resetting password for user {data.user}') reset_user_password(data.user, data.state, password) current_app.logger.info( f'Password reset done, removing state for {data.user}') current_app.password_reset_state_db.remove_state(data.state) return success_response(message=ResetPwMsg.pw_resetted)
def bind(user, version, registration_data, client_data, description=''): security_user = SecurityUser.from_user(user, current_app.private_userdb) enrollment_data = session.pop('_u2f_enroll_', None) if not enrollment_data: current_app.logger.error('Found no U2F enrollment data in session.') return error_response(message=SecurityMsg.missing_data) data = { 'version': version, 'registrationData': registration_data, 'clientData': client_data } device, der_cert = complete_registration(enrollment_data, data, current_app.config.u2f_facets) cert = x509.load_der_x509_certificate(der_cert, default_backend()) pem_cert = crypto.dump_certificate(crypto.FILETYPE_PEM, cert) if not isinstance(pem_cert, six.string_types): pem_cert = pem_cert.decode('utf-8') u2f_token = U2F.from_dict( dict( version=device['version'], keyhandle=device['keyHandle'], app_id=device['appId'], public_key=device['publicKey'], attest_cert=pem_cert, description=description, created_by='eduid_security', created_ts=True, )) security_user.credentials.add(u2f_token) save_and_sync_user(security_user) current_app.stats.count(name='u2f_token_bind') credentials = compile_credential_list(security_user) return success_response(payload=dict(credentials=credentials), message=SecurityMsg.u2f_registered)
def post_primary(user, email): proofing_user = ProofingUser.from_user(user, current_app.private_userdb) current_app.logger.debug( 'Trying to save email address {!r} as primary for user {}'.format( email, proofing_user)) try: mail = proofing_user.mail_addresses.find(email) except IndexError: current_app.logger.debug( 'Couldnt save email {!r} as primary for user {}, data out of sync'. format(email, proofing_user)) return error_response(message=CommonMsg.out_of_sync) if not mail.is_verified: current_app.logger.debug( 'Couldnt save email {!r} as primary for user {}, email unconfirmed' .format(email, proofing_user)) return error_response(message=EmailMsg.unconfirmed_not_primary) proofing_user.mail_addresses.primary = mail.email try: save_and_sync_user(proofing_user) except UserOutOfSync: current_app.logger.debug('Couldnt save email {!r} as primary for user' ' {}, data out of sync'.format( email, proofing_user)) return error_response(message=CommonMsg.out_of_sync) current_app.logger.info('Email address {!r} made primary ' 'for user {}'.format(email, proofing_user)) current_app.stats.count(name='email_set_primary', value=1) emails = {'emails': proofing_user.mail_addresses.to_list_of_dicts()} email_list = EmailListPayload().dump(emails) return success_response(payload=email_list, message=EmailMsg.success_primary)
def set_new_pw_extra_security_token( code: str, password: str, tokenResponse: Optional[str] = None, authenticatorData: Optional[str] = None, clientDataJSON: Optional[str] = None, credentialId: Optional[str] = None, signature: Optional[str] = None, ) -> FluxData: """ View that receives an emailed reset password code, hw token data, and a password, and sets the password as credential for the user, with extra security. Preconditions required for the call to succeed: * A PasswordResetEmailAndTokenState object in the password_reset_state_db keyed by the received code. * A flag in said state object indicating that the emailed code has already been verified. As side effects, this view will: * Compare the received password with the hash in the session to mark it accordingly (as suggested or as custom); * Revoke all password credentials the user had; This operation may fail due to: * The codes do not correspond to a valid state in the db; * Any of the codes have expired; * No valid user corresponds to the eppn stored in the state; * Communication problems with the VCCS backend; * Synchronization problems with the central user db. """ data = _load_data(code, password) if data.error: return error_response(message=data.error) # Process POSTed data success = False if tokenResponse: # CTAP1/U2F token_response = request.get_json().get('tokenResponse', '') current_app.logger.debug(f'U2F token response: {token_response}') _challenge = session.get(SESSION_PREFIX + '.u2f.challenge') if not isinstance(_challenge, bytes): raise TypeError( f'U2F challenge in session is not bytes {repr(_challenge)}') current_app.logger.debug(f'Challenge: {_challenge!r}') result = fido_tokens.verify_u2f(data.user, _challenge, token_response) if result is not None: success = result['success'] elif not success and authenticatorData: # CTAP2/Webauthn try: result = fido_tokens.verify_webauthn( data.user, dict( credentialId=credentialId, clientDataJSON=clientDataJSON, authenticatorData=authenticatorData, signature=signature, ), SESSION_PREFIX, ) except fido_tokens.VerificationProblem: pass else: success = result['success'] else: current_app.logger.error( f'Neither U2F nor Webauthn data in request to authn {data.user}') if not success: return error_response(message=ResetPwMsg.fido_token_fail) current_app.logger.info(f'Resetting password for user {data.user}') reset_user_password(data.user, data.state, password) current_app.logger.info( f'Password reset done, removing state for {data.user}') current_app.password_reset_state_db.remove_state(data.state) return success_response(message=ResetPwMsg.pw_resetted)
def choose_extra_security_phone(code: str, phone_index: int) -> FluxData: """ View called when the user chooses extra security (she can do that when she has some verified phone number). It receives an emailed reset password code and an index for one of the verified phone numbers, and returns info on the result of the attempted operation. Preconditions required for the call to succeed: * A PasswordResetEmailState object in the password_reset_state_db keyed by the received code. * A flag in said state object indicating that the emailed code has already been verified. * The user referenced in the state has at least phone_index (number) of verified phone numbers. As side effects, this operation will: * Copy the data in the PasswordResetEmailState to a new PasswordResetEmailAndPhoneState; * Create a new random hash as identifier code for the new state; * Store this code in the new state; * Send an SMS message with the code to the phone number corresponding to the received phone_index; This operation may fail due to: * The code does not correspond to a valid state in the db; * The code has expired; * No valid user corresponds to the eppn stored in the state; * Problems sending the SMS message """ try: state = get_pwreset_state(code) except BadCode as e: return error_response(message=e.msg) if isinstance(state, ResetPasswordEmailAndPhoneState): now = int(time.time()) if not isinstance(state.modified_ts, datetime): raise TypeError( f'Modified timestamp in state is not a datetime ({repr(state.modified_ts)})' ) if int(state.modified_ts.timestamp() ) > now - current_app.config.throttle_sms_seconds: current_app.logger.info( f'Throttling reset password SMSs for: {state.eppn}') return error_response(message=ResetPwMsg.send_sms_throttled) current_app.logger.info( f'Password reset: choose_extra_security for user with eppn {state.eppn}' ) # Check that the email code has been validated if not state.email_code.is_verified: current_app.logger.info( f'User with eppn {state.eppn} has not verified their email address' ) return error_response(message=ResetPwMsg.email_not_validated) phone_number = state.extra_security['phone_numbers'][phone_index] current_app.logger.info( f'Trying to send password reset sms to user with eppn {state.eppn}') try: send_verify_phone_code(state, phone_number["number"]) except MsgTaskFailed as e: current_app.logger.error(f'Sending sms failed: {e}') return error_response(message=ResetPwMsg.send_sms_failure) current_app.stats.count(name='reset_password_extra_security_phone') return success_response(message=ResetPwMsg.send_sms_success)