def get_actions(): user = current_app.central_userdb.get_user_by_eppn( session.get('user_eppn')) actions = get_next_action(user) if not actions['action']: return json.dumps({ 'action': False, 'url': actions['idp_url'], 'payload': { 'csrf_token': session.new_csrf_token() } }) action_type = session['current_plugin'] 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) current_app.logger.info('Starting pre-login action {} ' 'for user {}'.format(action.action_type, user)) try: url = plugin_obj.get_url_for_bundle(action) return json.dumps({ 'action': True, 'url': url, 'payload': { 'csrf_token': session.new_csrf_token() } }) except plugin_obj.ActionError as exc: _aborted(action, exc) abort(500)
def get_actions(): user = current_app.central_userdb.get_user_by_eppn(session.get('user_eppn')) actions = get_next_action(user) if not actions['action']: return json.dumps({'action': False, 'url': actions['idp_url'], 'payload': { 'csrf_token': session.new_csrf_token() } }) action_type = session['current_plugin'] 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) current_app.logger.info('Starting pre-login action {} ' 'for user {}'.format(action.action_type, user)) try: url = plugin_obj.get_url_for_bundle(action) return json.dumps({'action': True, 'url': url, 'payload': { 'csrf_token': session.new_csrf_token() }}) except plugin_obj.ActionError as exc: _aborted(action, exc) abort(500)
def registration_begin(user, authenticator): user_webauthn_tokens = user.credentials.filter(FidoCredential) if user_webauthn_tokens.count >= current_app.config.webauthn_max_allowed_tokens: current_app.logger.error( 'User tried to register more than {} tokens.'.format(current_app.config.webauthn_max_allowed_tokens) ) return error_response(message=SecurityMsg.max_webauthn) creds = make_credentials(user_webauthn_tokens.to_list()) server = get_webauthn_server(current_app.config.fido2_rp_id) if user.given_name is None or user.surname is None or user.display_name is None: return error_response(message=SecurityMsg.no_pdata) registration_data, state = server.register_begin( { 'id': str(user.eppn).encode('ascii'), 'name': "{} {}".format(user.given_name, user.surname), 'displayName': user.display_name, }, credentials=creds, user_verification=USER_VERIFICATION.DISCOURAGED, authenticator_attachment=authenticator, ) session['_webauthn_state_'] = state current_app.logger.info('User {} has started registration of a webauthn token'.format(user)) current_app.logger.debug('Webauthn Registration data: {}.'.format(registration_data)) current_app.stats.count(name='webauthn_register_begin') encoded_data = base64.urlsafe_b64encode(cbor.encode(registration_data)).decode('ascii') encoded_data = encoded_data.rstrip('=') return {'csrf_token': session.new_csrf_token(), 'registration_data': encoded_data}
def post_action(): if not request.data or session.get_csrf_token() != json.loads(request.data)['csrf_token']: abort(400) ret = _do_action() # Add a new csrf token as this is a POST request ret['csrf_token'] = session.new_csrf_token() return ret
def extra_security_phone_number(state): current_app.logger.info('Password reset: verify_phone_number {}'.format(request.method)) view_context = { 'heading': _('Verify phone number'), 'text': _('Enter the code you received via SMS'), 'action': url_for('reset_password.extra_security_phone_number', email_code=state.email_code.code), 'form_post_fail_msg': None, 'retry_url': url_for('reset_password.choose_extra_security', email_code=state.email_code.code), 'retry_url_txt': _('Resend code or try another way'), 'csrf_token': session.get_csrf_token(), 'errors': [], } if request.method == 'POST': form = ResetPasswordVerifyPhoneNumberSchema().load(request.form) if not form.errors and session.get_csrf_token() == form.data['csrf']: current_app.logger.info('Trying to verify phone code') if form.data.get('phone_code', '') == state.phone_code.code: if not verify_phone_number(state): current_app.logger.info('Could not validated phone code for {}'.format(state.eppn)) view_context = { 'heading': _('Temporary technical problem'), 'text': _('Please try again.'), 'retry_url': url_for('reset_password.choose_extra_security', email_code=state.email_code.code), 'retry_url_txt': _('Try again'), } return render_template('error.jinja2', view_context=view_context) current_app.logger.info('Phone code verified redirecting user to set password view') return redirect(url_for('reset_password.new_password', email_code=state.email_code.code)) view_context['form_post_fail_msg'] = _('Invalid code. Please try again.') view_context['errors'] = form.errors view_context['csrf_token'] = session.new_csrf_token() return render_template('reset_password_verify_phone.jinja2', view_context=view_context)
def registration_begin(user, authenticator): user_webauthn_tokens = user.credentials.filter(FidoCredential) if user_webauthn_tokens.count >= current_app.config['WEBAUTHN_MAX_ALLOWED_TOKENS']: current_app.logger.error('User tried to register more than {} tokens.'.format( current_app.config['WEBAUTHN_MAX_ALLOWED_TOKENS'])) return {'_status': 'error', 'message': 'security.webauthn.max_allowed_tokens'} creds = make_credentials(user_webauthn_tokens.to_list()) server = get_webauthn_server(current_app.config['FIDO2_RP_ID']) if user.given_name is None or user.surname is None or user.display_name is None: return {'_status': 'error', 'message': 'security.webauthn-missing-pdata'} registration_data, state = server.register_begin( { 'id': str(user.eppn).encode('ascii'), 'name': "{} {}".format(user.given_name, user.surname), 'displayName': user.display_name }, credentials=creds, user_verification=USER_VERIFICATION.DISCOURAGED, authenticator_attachment=authenticator, ) session['_webauthn_state_'] = state current_app.logger.info('User {} has started registration of a webauthn token'.format(user)) current_app.logger.debug('Webauthn Registration data: {}.'.format(registration_data)) current_app.stats.count(name='webauthn_register_begin') encoded_data = base64.urlsafe_b64encode(cbor.dumps(registration_data)).decode('ascii') encoded_data = encoded_data.rstrip('=') return { 'csrf_token': session.new_csrf_token(), 'registration_data': encoded_data }
def choose_extra_security(state): current_app.logger.info('Password reset: choose_extra_security {}'.format(request.method)) view_context = { 'heading': _('Extra security'), 'text': _('Choose an option to enhance the security'), 'action': url_for('reset_password.choose_extra_security', email_code=state.email_code.code), 'csrf_token': session.get_csrf_token(), 'form_post_success': False, 'errors': [], } # Check that the email code has been validated if not state.email_code.is_verified: current_app.logger.info('User {} has not verified their email address'.format(state.eppn)) view_context = { 'heading': _('Email address not validated'), 'text': _('Please use the password reset link that you have in your email.'), } return render_template('error.jinja2', view_context=view_context) if request.method == 'POST': form = ResetPasswordExtraSecuritySchema().load(request.form) if not form.errors and session.get_csrf_token() == form.data['csrf']: if form.data.get('no_extra_security'): current_app.logger.info('Redirecting user to reset password with NO extra security') return redirect(url_for('reset_password.new_password', email_code=state.email_code.code)) if form.data.get('phone_number_index'): phone_number_index = int(form.data.get('phone_number_index')) phone_number = state.extra_security['phone_numbers'][phone_number_index] current_app.logger.info('Trying to send password reset sms to user {}'.format(state.eppn)) send_verify_phone_code(state, phone_number) current_app.logger.info('Redirecting user to verify phone number view') return redirect(url_for('reset_password.extra_security_phone_number', email_code=state.email_code.code)) view_context['errors'] = form.errors view_context['csrf_token'] = session.new_csrf_token() try: alternatives = get_extra_security_alternatives(state.eppn) state.extra_security = alternatives current_app.password_reset_state_db.save(state) view_context['security_alternatives'] = mask_alternatives(alternatives) except DocumentDoesNotExist: current_app.logger.error('User {} not found'.format(state.eppn)) view_context = { 'heading': _('Something went wrong'), 'text': _('Please restart the password reset procedure.'), 'retry_url': url_for('reset_password.reset_password'), 'retry_url_txt': _('Reset your password'), } return render_template('error.jinja2', view_context=view_context) if not alternatives: # The user has no options for extra security, redirect to setting a new password return redirect(url_for('reset_password.new_password', email_code=state.email_code.code)) return render_template('reset_password_extra_security.jinja2', view_context=view_context)
def get_config(): try: action_type = session['current_plugin'] except KeyError: abort(403) plugin_obj = current_app.plugins[action_type]() action = Action(data=session['current_action']) try: config = plugin_obj.get_config_for_bundle(action) config['csrf_token'] = session.new_csrf_token() return config except plugin_obj.ActionError as exc: return error_response(message=exc.args[0])
def get_config(): try: action_type = session['current_plugin'] except KeyError: abort(403) plugin_obj = current_app.plugins[action_type]() action = Action(data=session['current_action']) try: config = plugin_obj.get_config_for_bundle(action) config['csrf_token'] = session.new_csrf_token() return config except plugin_obj.ActionError as exc: return { '_status': 'error', 'message': exc.args[0] }
def reset_password(): current_app.logger.info('Password reset: reset_password {}'.format( request.method)) view_context = { 'heading': _('Reset password'), 'text': _('Enter an email address registered to your account below'), 'action': url_for('reset_password.reset_password'), 'csrf_token': session.get_csrf_token(), 'form_post_success': False, 'form_post_success_msg': _('Reset password message sent. Check your email to continue.'), 'errors': [], } if request.method == 'POST': try: form = ResetPasswordEmailSchema().load(request.form) except ValidationError as e: current_app.logger.error(e) view_context['errors'] = e.messages else: if session.get_csrf_token() == form['csrf']: current_app.logger.info( 'Trying to send password reset email to {}'.format( form['email'])) try: send_password_reset_mail(form['email']) except MailTaskFailed as e: current_app.logger.error( 'Sending e-mail failed: {}'.format(e)) view_context = { 'heading': _('Temporary technical problem'), 'text': _('Please try again.'), 'retry_url': url_for('reset_password.reset_password'), 'retry_url_txt': _('Try again'), } return render_template('error.jinja2', view_context=view_context) view_context['form_post_success'] = True view_context['text'] = '' view_context['csrf_token'] = session.new_csrf_token() return render_template('reset_password.jinja2', view_context=view_context)
def new_password(state): current_app.logger.info('Password reset: new_password {}'.format(request.method)) view_context = { 'heading': _('New password'), 'text': _(''' Please choose a new password for your eduID account. A strong password has been generated for you. You can accept the generated password by clicking "Change password" or you can opt to choose your own password by clicking "Custom Password". '''), 'action': url_for('reset_password.new_password', email_code=state.email_code.code), 'csrf_token': session.get_csrf_token(), 'active_pane': 'generated', 'zxcvbn_terms': json.dumps(get_zxcvbn_terms(state.eppn)), 'errors': [], } if request.method == 'POST': min_entropy = current_app.config['PASSWORD_ENTROPY'] form = ResetPasswordNewPasswordSchema( zxcvbn_terms=view_context['zxcvbn_terms'], min_entropy=int(min_entropy)).load(request.form) current_app.logger.debug(form) if not form.errors and session.get_csrf_token() == form.data['csrf']: if form.data.get('use_generated_password'): password = state.generated_password current_app.logger.info('Generated password used') else: password = form.data.get('custom_password') current_app.logger.info('Custom password used') current_app.logger.info('Resetting password for user {}'.format(state.eppn)) reset_user_password(state, password) current_app.logger.info('Password reset done removing state for user {}'.format(state.eppn)) current_app.password_reset_state_db.remove_state(state) view_context['form_post_success'] = True view_context['login_url'] = current_app.config['EDUID_SITE_URL'] return render_template('reset_password_new_password.jinja2', view_context=view_context) view_context['errors'] = form.errors view_context['active_pane'] = 'custom' # Generate a random good password # TODO: Hash the password using VCCSPasswordFactor before saving it to db state.generated_password = generate_suggested_password() view_context['generated_password'] = state.generated_password current_app.password_reset_state_db.save(state) view_context['csrf_token'] = session.new_csrf_token() return render_template('reset_password_new_password.jinja2', view_context=view_context)
def reset_password(): current_app.logger.info('Password reset: reset_password {}'.format(request.method)) view_context = { 'heading': _('Reset password'), 'text': _('Enter an email address registered to your account below'), 'action': url_for('reset_password.reset_password'), 'csrf_token': session.get_csrf_token(), 'form_post_success': False, 'form_post_success_msg': _('Reset password message sent. Check your email to continue.'), 'errors': [], } if request.method == 'POST': form = ResetPasswordEmailSchema().load(request.form) if not form.errors and session.get_csrf_token() == form.data['csrf']: current_app.logger.info('Trying to send password reset email to {}'.format(form.data['email'])) send_password_reset_mail(form.data['email']) view_context['form_post_success'] = True view_context['text'] = '' view_context['errors'] = form.errors view_context['csrf_token'] = session.new_csrf_token() return render_template('reset_password.jinja2', view_context=view_context)
def new_password(state): current_app.logger.info('Password reset: new_password {}'.format( request.method)) view_context = { 'heading': _('New password'), 'text': _(''' Please choose a new password for your eduID account. A strong password has been generated for you. You can accept the generated password by clicking "Change password" or you can opt to choose your own password by clicking "Custom Password". '''), 'action': url_for('reset_password.new_password', email_code=state.email_code.code), 'csrf_token': session.get_csrf_token(), 'active_pane': 'generated', 'zxcvbn_terms': json.dumps(get_zxcvbn_terms(state.eppn)), 'errors': [], } if request.method == 'POST': min_entropy = current_app.config.password_entropy try: form = ResetPasswordNewPasswordSchema( zxcvbn_terms=view_context['zxcvbn_terms'], min_entropy=int(min_entropy)).load(request.form) current_app.logger.debug(form) except ValidationError as e: current_app.logger.error(e) view_context['errors'] = e.messages view_context['active_pane'] = 'custom' else: if session.get_csrf_token() == form['csrf']: if form.get('use_generated_password'): password = state.generated_password current_app.logger.info('Generated password used') current_app.stats.count( name='reset_password_generated_password_used') else: password = form.get('custom_password') current_app.logger.info('Custom password used') current_app.stats.count( name='reset_password_custom_password_used') current_app.logger.info( 'Resetting password for user {}'.format(state.eppn)) reset_user_password(state, password) current_app.logger.info( 'Password reset done removing state for user {}'.format( state.eppn)) current_app.password_reset_state_db.remove_state(state) view_context['form_post_success'] = True view_context['login_url'] = current_app.config.eduid_site_url return render_template('reset_password_new_password.jinja2', view_context=view_context) # Generate a random good password # TODO: Hash the password using VCCSPasswordFactor before saving it to db state.generated_password = generate_suggested_password() view_context['generated_password'] = state.generated_password current_app.password_reset_state_db.save(state) view_context['csrf_token'] = session.new_csrf_token() return render_template('reset_password_new_password.jinja2', view_context=view_context)
def extra_security_phone_number(state): current_app.logger.info('Password reset: verify_phone_number {}'.format( request.method)) view_context = { 'heading': _('Verify phone number'), 'text': _('Enter the code you received via SMS'), 'action': url_for('reset_password.extra_security_phone_number', email_code=state.email_code.code), 'form_post_fail_msg': None, 'retry_url': url_for('reset_password.choose_extra_security', email_code=state.email_code.code), 'retry_url_txt': _('Resend code or try another way'), 'csrf_token': session.get_csrf_token(), 'errors': [], } if request.method == 'POST': try: form = ResetPasswordVerifyPhoneNumberSchema().load(request.form) except ValidationError as e: current_app.logger.error(e) view_context['errors'] = e.messages else: if session.get_csrf_token() == form['csrf']: current_app.logger.info('Trying to verify phone code') phone_code = form.get('phone_code', '') if phone_code == state.phone_code.code: if not verify_phone_number(state): current_app.logger.info( 'Could not validated phone code for {}'.format( state.eppn)) view_context = { 'heading': _('Temporary technical problem'), 'text': _('Please try again.'), 'retry_url': url_for('reset_password.choose_extra_security', email_code=state.email_code.code), 'retry_url_txt': _('Try again'), } return render_template('error.jinja2', view_context=view_context) current_app.logger.info( 'Phone code verified redirecting user to set password view' ) current_app.stats.count( name='reset_password_extra_security_phone_success') return redirect( url_for('reset_password.new_password', email_code=state.email_code.code)) view_context['form_post_fail_msg'] = _( 'Invalid code. Please try again.') view_context['csrf_token'] = session.new_csrf_token() return render_template('reset_password_verify_phone.jinja2', view_context=view_context)
def choose_extra_security(state): current_app.logger.info('Password reset: choose_extra_security {}'.format( request.method)) view_context = { 'heading': _('Extra security'), 'text': _('Choose an option to enhance the security'), 'action': url_for('reset_password.choose_extra_security', email_code=state.email_code.code), 'csrf_token': session.get_csrf_token(), 'form_post_success': False, 'errors': [], } # Check that the email code has been validated if not state.email_code.is_verified: current_app.logger.info( 'User {} has not verified their email address'.format(state.eppn)) view_context = { 'heading': _('Email address not validated'), 'text': _('Please use the password reset link that you have in your email.' ), } return render_template('error.jinja2', view_context=view_context) if request.method == 'POST': try: form = ResetPasswordExtraSecuritySchema().load(request.form) except ValidationError as e: current_app.logger.error(e) view_context['errors'] = e.messages else: if session.get_csrf_token() == form['csrf']: if form.get('no_extra_security'): current_app.logger.info( 'Redirecting user to reset password with NO extra security' ) current_app.stats.count( name='reset_password_no_extra_security') return redirect( url_for('reset_password.new_password', email_code=state.email_code.code)) if form.get('phone_number_index'): phone_number_index = int(form.get('phone_number_index')) phone_number = state.extra_security['phone_numbers'][ phone_number_index] current_app.logger.info( 'Trying to send password reset sms to user {}'.format( state.eppn)) try: send_verify_phone_code(state, phone_number) except MsgTaskFailed as e: current_app.logger.error( 'Sending sms failed: {}'.format(e)) view_context = { 'heading': _('Temporary technical problem'), 'text': _('Please try again.'), 'retry_url': url_for('reset_password.choose_extra_security', email_code=state.email_code.code), 'retry_url_txt': _('Try again'), } return render_template('error.jinja2', view_context=view_context) current_app.logger.info( 'Redirecting user to verify phone number view') current_app.stats.count( name='reset_password_extra_security_phone') return redirect( url_for('reset_password.extra_security_phone_number', email_code=state.email_code.code)) view_context['csrf_token'] = session.new_csrf_token() try: alternatives = get_extra_security_alternatives(state.eppn) state.extra_security = alternatives current_app.password_reset_state_db.save(state) view_context['security_alternatives'] = mask_alternatives(alternatives) except DocumentDoesNotExist: current_app.logger.error('User {} not found'.format(state.eppn)) view_context = { 'heading': _('Something went wrong'), 'text': _('Please restart the password reset procedure.'), 'retry_url': url_for('reset_password.reset_password'), 'retry_url_txt': _('Reset your password'), } return render_template('error.jinja2', view_context=view_context) if not alternatives: # The user has no options for extra security, redirect to setting a new password return redirect( url_for('reset_password.new_password', email_code=state.email_code.code)) return render_template('reset_password_extra_security.jinja2', view_context=view_context)