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( 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, application = '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)) return { 'message': 'security.webauthn_register_success', 'credentials': compile_credential_list(security_user) }
def test_fillup_attributes(self): for context in self.plugin_contexts: security_user = SecurityUser(data=self.user_data) context.private_db.save(security_user) self.assertDictEqual( attribute_fetcher(context, security_user.user_id), { '$set': { 'passwords': [{ 'credential_id': u'112345678901234567890123', 'salt': '$NDNv1H1$9c810d852430b62a9a7c6159d5d64c41c3831846f81b6799b54e1e8922f11545$32$32$', }], 'nins': [{ 'number': '123456781235', 'primary': True, 'verified': True }], 'phone': [{ 'number': '+46700011336', 'primary': True, 'verified': True }] }, '$unset': { 'terminated': None } } )
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 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': True, 'message': 'security.u2f.missing_enrollment_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(version=device['version'], keyhandle=device['keyHandle'], app_id=device['appId'], public_key=device['publicKey'], attest_cert=pem_cert, description=description, application='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') return { 'message': 'security.u2f_register_success', 'credentials': compile_credential_list(security_user) }
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 add_token_to_user(self, eppn): user = self.app.central_userdb.get_user_by_eppn(eppn) u2f_token = U2F(version='version', keyhandle='keyHandle', app_id='appId', public_key='publicKey', attest_cert='cert', description='description', application='eduid_security', created_ts=True) user.credentials.add(u2f_token) self.app.central_userdb.save(user) return SecurityUser.from_user(user, self.app.private_userdb)
def account_terminated(user): """ The account termination action, removes all credentials for the terminated account from the VCCS service, flags the account as terminated, sends an email to the address in the terminated account, and logs out the session. :type user: eduid_userdb.user.User """ security_user = SecurityUser.from_user(user, current_app.private_userdb) authn_ts = session.get('reauthn-for-termination', None) if authn_ts is None: abort(400) now = datetime.utcnow() delta = now - datetime.fromtimestamp(authn_ts) if int(delta.total_seconds()) > 600: return error_response(message=SecurityMsg.stale_reauthn) del session['reauthn-for-termination'] # revoke all user passwords revoke_all_credentials(current_app.config.vccs_url, security_user) # Skip removing old passwords from the user at this point as a password reset will do that anyway. # This fixes the problem with loading users for a password reset as users without passwords triggers # the UserHasNotCompletedSignup check in eduid-userdb. # TODO: Needs a decision on how to handle unusable user passwords # for p in security_user.credentials.filter(Password).to_list(): # security_user.passwords.remove(p.key) # flag account as terminated security_user.terminated = True try: save_and_sync_user(security_user) except UserOutOfSync: return error_response(message=CommonMsg.out_of_sync) current_app.stats.count(name='security_account_terminated', value=1) current_app.logger.info('Terminated user account') # email the user try: send_termination_mail(security_user) except MsgTaskFailed as e: current_app.logger.error( f'Failed to send account termination mail: {e}') current_app.logger.error( 'Account will be terminated successfully anyway.') current_app.logger.debug(f'Logging out (terminated) user {user}') return redirect( f'{current_app.config.logout_endpoint}?next={current_app.config.termination_redirect_url}' )
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') return { 'message': 'security.u2f-token-removed', 'credentials': compile_credential_list(security_user) }
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 account_terminated(user): """ The account termination action, removes all credentials for the terminated account from the VCCS service, flags the account as terminated, sends an email to the address in the terminated account, and logs out the session. :type user: eduid_userdb.user.User """ security_user = SecurityUser.from_user(user, current_app.private_userdb) authn_ts = session.get('reauthn-for-termination', None) if authn_ts is None: abort(400) now = datetime.utcnow() delta = now - datetime.fromtimestamp(authn_ts) if int(delta.total_seconds()) > 600: return error_message('security.stale_authn_info') del session['reauthn-for-termination'] # revoke all user passwords revoke_all_credentials(current_app.config.get('VCCS_URL'), security_user) # Skip removing old passwords from the user at this point as a password reset will do that anyway. # This fixes the problem with loading users for a password reset as users without passwords triggers # the UserHasNotCompletedSignup check in eduid-userdb. # TODO: Needs a decision on how to handle unusable user passwords #for p in security_user.credentials.filter(Password).to_list(): # security_user.passwords.remove(p.key) # flag account as terminated security_user.terminated = True try: save_and_sync_user(security_user) except UserOutOfSync: return error_message('user-out-of-sync') current_app.stats.count(name='security_account_terminated', value=1) current_app.logger.info('Terminated user account') # email the user send_termination_mail(security_user) session.invalidate() current_app.logger.info('Invalidated session for user') site_url = current_app.config.get("EDUID_SITE_URL") current_app.logger.info('Redirection user to user {}'.format(site_url)) # TODO: Add a account termination completed view to redirect to return redirect(site_url)
def reset_user_password(state, password): """ :param state: Password reset state :type state: PasswordResetState :param password: Plain text password :type password: six.string_types :return: None :rtype: None """ vccs_url = current_app.config.vccs_url user = current_app.central_userdb.get_user_by_eppn(state.eppn, raise_on_missing=False) security_user = SecurityUser.from_user( user, private_userdb=current_app.private_userdb) # If no extra security is all verified information (except email addresses) is set to not verified if not extra_security_used(state): current_app.logger.info('No extra security used by user {}'.format( state.eppn)) # Phone numbers verified_phone_numbers = security_user.phone_numbers.verified.to_list() if verified_phone_numbers: current_app.logger.info( 'Unverifying phone numbers for user {}'.format(state.eppn)) security_user.phone_numbers.primary.is_primary = False for phone_number in verified_phone_numbers: phone_number.is_verified = False current_app.logger.debug('Phone number {} unverified'.format( phone_number.number)) # NINs verified_nins = security_user.nins.verified.to_list() if verified_nins: current_app.logger.info('Unverifying nins for user {}'.format( state.eppn)) security_user.nins.primary.is_primary = False for nin in verified_nins: nin.is_verified = False current_app.logger.debug('NIN {} unverified'.format( nin.number)) security_user = reset_password(security_user, new_password=password, application='security', vccs_url=vccs_url) security_user.terminated = False save_and_sync_user(security_user) current_app.stats.count(name='security_password_reset', value=1) current_app.logger.info('Reset password successful for user {}'.format( security_user.eppn))
def modify(user, credential_key, description): security_user = SecurityUser.from_user(user, current_app.private_userdb) token_to_modify = security_user.credentials.filter(U2F).find(credential_key) if not token_to_modify: current_app.logger.error('Did not find requested U2F token for user.') return {'_error': True, 'message': 'security.u2f.missing_token'} if len(description) > current_app.config['U2F_MAX_DESCRIPTION_LENGTH']: current_app.logger.error('User tried to set a U2F token description longer than {}.'.format( current_app.config['U2F_MAX_DESCRIPTION_LENGTH'])) return {'_error': True, 'message': 'security.u2f.description_to_long'} token_to_modify.description = description save_and_sync_user(security_user) current_app.stats.count(name='u2f_token_modify') return { 'credentials': compile_credential_list(security_user) }
def modify(user, credential_key, description): security_user = SecurityUser.from_user(user, current_app.private_userdb) token_to_modify = security_user.credentials.filter(U2F).find( credential_key) if not token_to_modify: current_app.logger.error('Did not find requested U2F token for user.') return error_response(message=SecurityMsg.no_token) if len(description) > current_app.config.u2f_max_description_length: current_app.logger.error( 'User tried to set a U2F token description longer than {}.'.format( current_app.config.u2f_max_description_length)) return error_response(message=SecurityMsg.long_desc) token_to_modify.description = description save_and_sync_user(security_user) current_app.stats.count(name='u2f_token_modify') return {'credentials': compile_credential_list(security_user)}
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 {'_status': 'error', 'success': False, 'message': 'nins.verified_no_rm'} try: remove_nin_from_user(security_user, nin) return {'success': True, 'message': 'nins.success_removal', 'nins': security_user.nins.to_list_of_dicts()} except AmTaskFailed as e: current_app.logger.error('Removing nin from user failed'.format(nin, security_user)) current_app.logger.error('{}'.format(e)) return {'_status': 'error', 'message': 'Temporary technical problems'}
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 remove(user, credential_key): security_user = SecurityUser.from_user(user, current_app.private_userdb) tokens = security_user.credentials.filter(FidoCredential) if tokens.count <= 1: return {'_error': True, 'message': 'security.webauthn-noremove-last'} token_to_remove = security_user.credentials.find(credential_key) if token_to_remove: security_user.credentials.remove(credential_key) save_and_sync_user(security_user) current_app.stats.count(name='webauthn_token_remove') current_app.logger.info(f'User {security_user} has removed a security token: {credential_key}') message = 'security.webauthn-token-removed' else: current_app.logger.info(f'User {security_user} has tried to remove a' f' missing security token: {credential_key}') message = 'security.webauthn-token-notfound' return { 'message': message, 'credentials': compile_credential_list(security_user) }
def change_password(user, old_password, new_password): """ View to change the password """ security_user = SecurityUser.from_user(user, current_app.private_userdb) authn_ts = session.get('reauthn-for-chpass', None) if authn_ts is None: return error_message('chpass.no_reauthn') now = datetime.utcnow() delta = now - datetime.fromtimestamp(authn_ts) timeout = current_app.config.get('CHPASS_TIMEOUT', 600) if int(delta.total_seconds()) > timeout: return error_message('chpass.stale_reauthn') vccs_url = current_app.config.get('VCCS_URL') added = add_credentials(vccs_url, old_password, new_password, security_user, source='security') if not added: current_app.logger.debug('Problem verifying the old credentials for {}'.format(user)) return error_message('chpass.unable-to-verify-old-password') security_user.terminated = False try: save_and_sync_user(security_user) except UserOutOfSync: return error_message('user-out-of-sync') del session['reauthn-for-chpass'] current_app.stats.count(name='security_password_changed', value=1) current_app.logger.info('Changed password for user {}'.format(security_user.eppn)) next_url = current_app.config.get('DASHBOARD_URL', '/profile') credentials = { 'next_url': next_url, 'credentials': compile_credential_list(security_user), 'message': 'chpass.password-changed' } return CredentialList().dump(credentials).data
def remove(user, credential_key): security_user = SecurityUser.from_user(user, current_app.private_userdb) tokens = security_user.credentials.filter(FidoCredential) if tokens.count <= 1: return {'_error': True, 'message': SecurityMsg.no_last.value} token_to_remove = security_user.credentials.find(credential_key) if token_to_remove: security_user.credentials.remove(credential_key) save_and_sync_user(security_user) current_app.stats.count(name='webauthn_token_remove') current_app.logger.info(f'User {security_user} has removed a security token: {credential_key}') message = SecurityMsg.rm_webauthn else: current_app.logger.info( f'User {security_user} has tried to remove a' f' missing security token: {credential_key}' ) message = SecurityMsg.no_webauthn credentials = compile_credential_list(security_user) return {'message': message, 'credentials': credentials}
def reset_user_password(state, password): """ :param state: Password reset state :type state: PasswordResetState :param password: Plain text password :type password: six.string_types :return: None :rtype: None """ vccs_url = current_app.config.get('VCCS_URL') user = current_app.central_userdb.get_user_by_eppn(state.eppn, raise_on_missing=False) security_user = SecurityUser.from_user(user, private_userdb=current_app.private_userdb) # If no extra security is all verified information (except email addresses) is set to not verified if not extra_security_used(state): current_app.logger.info('No extra security used by user {}'.format(state.eppn)) # Phone numbers verified_phone_numbers = security_user.phone_numbers.verified.to_list() if verified_phone_numbers: current_app.logger.info('Unverifying phone numbers for user {}'.format(state.eppn)) security_user.phone_numbers.primary.is_primary = False for phone_number in verified_phone_numbers: phone_number.is_verified = False current_app.logger.debug('Phone number {} unverified'.format(phone_number.number)) # NINs verified_nins = security_user.nins.verified.to_list() if verified_nins: current_app.logger.info('Unverifying nins for user {}'.format(state.eppn)) security_user.nins.primary.is_primary = False for nin in verified_nins: nin.is_verified = False current_app.logger.debug('NIN {} unverified'.format(nin.number)) security_user = reset_password(security_user, new_password=password, application='security', vccs_url=vccs_url) security_user.terminated = False save_and_sync_user(security_user) current_app.stats.count(name='security_password_reset', value=1) current_app.logger.info('Reset password successful for user {}'.format(security_user.eppn))
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 change_password(user): """ View to change the password """ security_user = SecurityUser.from_user(user, current_app.private_userdb) min_entropy = current_app.config.password_entropy schema = ChangePasswordSchema(zxcvbn_terms=get_zxcvbn_terms( security_user.eppn), min_entropy=int(min_entropy)) if not request.data: return error_response(message='chpass.no-data') try: form = schema.load(json.loads(request.data)) current_app.logger.debug(form) except ValidationError as e: current_app.logger.error(e) return error_response(message='chpass.weak-password') else: old_password = form.get('old_password') new_password = form.get('new_password') if session.get_csrf_token() != form['csrf_token']: return error_response(message='csrf.try_again') authn_ts = session.get('reauthn-for-chpass', None) if authn_ts is None: return error_response(message='chpass.no_reauthn') now = datetime.utcnow() delta = now - datetime.fromtimestamp(authn_ts) timeout = current_app.config.chpass_timeout if int(delta.total_seconds()) > timeout: return error_response(message='chpass.stale_reauthn') vccs_url = current_app.config.vccs_url added = add_credentials(vccs_url, old_password, new_password, security_user, source='security') if not added: current_app.logger.debug( 'Problem verifying the old credentials for {}'.format(user)) return error_response(message='chpass.unable-to-verify-old-password') security_user.terminated = False try: save_and_sync_user(security_user) except UserOutOfSync: return error_response(message='user-out-of-sync') del session['reauthn-for-chpass'] current_app.stats.count(name='security_password_changed', value=1) current_app.logger.info('Changed password for user {}'.format( security_user.eppn)) next_url = current_app.config.dashboard_url credentials = { 'next_url': next_url, 'credentials': compile_credential_list(security_user), 'message': 'chpass.password-changed', } return credentials