def validate(self): if not super().validate(): # FIXME-identity if (set(self.errors.keys()) - set( self.security_utils_service.get_identity_attributes())): return False self.user = self.security_utils_service.user_loader(self.email.data) if self.user is None: self.email.errors.append( _('flask_unchained.bundles.security:error.user_does_not_exist') ) return False if not self.user.password: self.password.errors.append( _('flask_unchained.bundles.security:error.password_not_set')) return False if not self.security_utils_service.verify_and_update_password( self.password.data, self.user): self.password.errors.append( _('flask_unchained.bundles.security:error.invalid_password')) return False if (not self.security_service.security.login_without_confirmation and self.security_service.security.confirmable and self.user.confirmed_at is None): self.email.errors.append( _('flask_unchained.bundles.security:error.confirmation_required' )) return False if not self.user.active: self.email.errors.append( _('flask_unchained.bundles.security:error.disabled_account')) return False return True
class ChangePasswordForm(BaseForm): class Meta: model = User model_fields = { 'new_password': '******', 'new_password_confirm': 'password' } password = fields.PasswordField( _('flask_unchained.bundles.security:form_field.password'), validators=[password_required]) new_password = fields.PasswordField( _('flask_unchained.bundles.security:form_field.new_password'), validators=[password_required]) new_password_confirm = fields.PasswordField( _('flask_unchained.bundles.security:form_field.retype_password'), validators=[new_password_equal, password_required]) submit = fields.SubmitField( _('flask_unchained.bundles.security:form_submit.change_password')) def validate(self): result = super().validate() if not security_utils_service.verify_password(current_user, self.password.data): self.password.errors.append( _('flask_unchained.bundles.security:error.invalid_password')) return False elif self.password.data == self.new_password.data: self.new_password.errors.append( _('flask_unchained.bundles.security:error.password_is_the_same' )) return False return result
def process_login_errors(self, form): """ An opportunity to modify the login form's error messages before returning the response to the user. The idea is to try not to leak excess account info without being too unfriendly to actually-valid-users. :param form: An instance of the config option `SECURITY_LOGIN_FORM` class. """ account_disabled = _( 'flask_unchained.bundles.security:error.disabled_account') confirmation_required = _( 'flask_unchained.bundles.security:error.confirmation_required') if account_disabled in form.errors.get('email', []): error = account_disabled elif confirmation_required in form.errors.get('email', []): error = confirmation_required else: identity_attrs = app.config.get( 'SECURITY_USER_IDENTITY_ATTRIBUTES') error = f"Invalid {', '.join(identity_attrs)} and/or password." # wipe out all individual field errors, we just want a single form-level error form._errors = {'_error': [error]} for field in form._fields.values(): field.errors = None return form
class PasswordFormMixin: password = fields.PasswordField( _('flask_unchained.bundles.security:form_field.password'), validators=[password_required]) password_confirm = fields.PasswordField( _('flask_unchained.bundles.security:form_field.retype_password'), validators=[password_equal, password_required])
class ForgotPasswordForm(BaseForm): class Meta: model = User user = None email = StringField(_('flask_unchained.bundles.security:form_field.email'), validators=[valid_user_email]) submit = fields.SubmitField( _('flask_unchained.bundles.security:form_submit.recover_password'))
def reset_password(self, token): """ View function verify a users reset password token from the email we sent to them. It also handles the form for them to set a new password. Supports html and json requests. """ expired, invalid, user = \ self.security_utils_service.reset_password_token_status(token) if invalid: self.flash(_( 'flask_unchained.bundles.security:flash.invalid_reset_password_token' ), category='error') return self.redirect('SECURITY_INVALID_RESET_TOKEN_REDIRECT') elif expired: self.security_service.send_reset_password_instructions(user) self.flash(_( 'flask_unchained.bundles.security:flash.password_reset_expired', email=user.email, within=app.config.get('SECURITY_RESET_PASSWORD_WITHIN')), category='error') return self.redirect('SECURITY_EXPIRED_RESET_TOKEN_REDIRECT') spa_redirect = app.config.get( 'SECURITY_API_RESET_PASSWORD_HTTP_GET_REDIRECT') if request.method == 'GET' and spa_redirect: return self.redirect(spa_redirect, token=token, _external=True) form = self._get_form('SECURITY_RESET_PASSWORD_FORM') if form.validate_on_submit(): self.security_service.reset_password(user, form.password.data) self.security_service.login_user(user) self.after_this_request(self._commit) self.flash( _('flask_unchained.bundles.security:flash.password_reset'), category='success') if request.is_json: return self.jsonify({ 'token': user.get_auth_token(), 'user': user }) return self.redirect('SECURITY_POST_RESET_REDIRECT_ENDPOINT', 'SECURITY_POST_LOGIN_REDIRECT_ENDPOINT') elif form.errors and request.is_json: return self.errors(form.errors) return self.render('reset_password', reset_password_form=form, reset_password_token=token, **self.security.run_ctx_processor('reset_password'))
def validate(self): result = super().validate() if not self.security_utils_service.verify_and_update_password( self.password.data, current_user): self.password.errors.append( _('flask_unchained.bundles.security:error.invalid_password')) return False if self.password.data == self.new_password.data: self.new_password.errors.append( _('flask_unchained.bundles.security:error.password_is_the_same' )) return False return result
def authorized(self, remote_app): provider = getattr(self.oauth, remote_app) resp = provider.authorized_response() if resp is None or resp.get('access_token') is None: abort( HTTPStatus.UNAUTHORIZED, 'errorCode={error} error={description}'.format( error=request.args['error'], description=request.args['error_description'], )) session['oauth_token'] = resp['access_token'] email, data = self.oauth_service.get_user_details(provider) user, created = self.user_manager.get_or_create(email=email, defaults=data, commit=True) if created: self.security_service.register_user( user, _force_login_without_confirmation=True) else: self.security_service.login_user(user, force=True) self.oauth_service.on_authorized(provider) self.flash(_('flask_unchained.bundles.security:flash.login'), category='success') return self.redirect('SECURITY_POST_LOGIN_REDIRECT_ENDPOINT')
def login(self): """ View function to log a user in. Supports html and json requests. """ form = self._get_form('SECURITY_LOGIN_FORM') if (form.validate_on_submit() and self.security_service.login_user( form.user, form.remember.data)): self.after_this_request(self._commit) if request.is_json: return self.jsonify({ 'token': form.user.get_auth_token(), 'user': form.user }) self.flash(_('flask_unchained.bundles.security:flash.login'), category='success') return self.redirect('SECURITY_POST_LOGIN_REDIRECT_ENDPOINT') elif form.errors: form = self.security_service.process_login_errors(form) if request.is_json: return self.jsonify({'error': form.errors.get('_error')[0]}, code=HTTPStatus.UNAUTHORIZED) return self.render('login', login_user_form=form, **self.security.run_ctx_processor('login'))
def change_password(self): """ View function for a user to change their password. Supports html and json requests. """ form = self._get_form('SECURITY_CHANGE_PASSWORD_FORM') if form.validate_on_submit(): self.security_service.change_password( current_user._get_current_object(), form.new_password.data) self.after_this_request(self._commit) self.flash( _('flask_unchained.bundles.security:flash.password_change'), category='success') if request.is_json: return self.jsonify({'token': current_user.get_auth_token()}) return self.redirect('SECURITY_POST_CHANGE_REDIRECT_ENDPOINT', 'SECURITY_POST_LOGIN_REDIRECT_ENDPOINT') elif form.errors and request.is_json: return self.errors(form.errors) return self.render( 'change_password', change_password_form=form, **self.security.run_ctx_processor('change_password'))
class ResetPasswordForm(BaseForm, PasswordFormMixin): class Meta: model = User model_fields = {'password_confirm': 'password'} submit = SubmitField( _('flask_unchained.bundles.security:form_submit.reset_password'))
def validate(self): if not super(SendConfirmationForm, self).validate(): return False if self.user.confirmed_at is not None: self.email.errors.append( _('flask_unchained.bundles.security:error.already_confirmed')) return False return True
class LoginForm(BaseForm, NextFormMixin): """The default login form""" class Meta: model = User email = fields.StringField( _('flask_unchained.bundles.security:form_field.email')) password = fields.PasswordField( _('flask_unchained.bundles.security:form_field.password')) remember = fields.BooleanField( _('flask_unchained.bundles.security:form_field.remember_me')) submit = fields.SubmitField( _('flask_unchained.bundles.security:form_submit.login')) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.user = None if not self.next.data: self.next.data = request.args.get('next', '') self.remember.default = app.config.SECURITY_DEFAULT_REMEMBER_ME def validate(self): if not super().validate(): # FIXME-identity if (set(self.errors.keys()) - set(security_utils_service.get_identity_attributes())): return False self.user = security_utils_service.user_loader(self.email.data) if self.user is None: self.email.errors.append( _('flask_unchained.bundles.security:error.user_does_not_exist') ) return False elif not self.password.data: self.password.errors.append( _('flask_unchained.bundles.security:password_required')) return False elif not security_utils_service.verify_password( self.user, self.password.data): self.password.errors.append( _('flask_unchained.bundles.security:error.invalid_password')) return False return True
def confirm_email(self, token): """ View function to confirm a user's token from the confirmation email send to them. Supports html and json requests. """ expired, invalid, user = \ self.security_utils_service.confirm_email_token_status(token) if not user or invalid: invalid = True self.flash(_( 'flask_unchained.bundles.security:flash.invalid_confirmation_token' ), category='error') already_confirmed = user is not None and user.confirmed_at is not None if expired and not already_confirmed: self.security_service.send_email_confirmation_instructions(user) self.flash(_( 'flask_unchained.bundles.security:flash.confirmation_expired', email=user.email, within=app.config.SECURITY_CONFIRM_EMAIL_WITHIN), category='error') if invalid or (expired and not already_confirmed): return self.redirect( 'SECURITY_CONFIRM_ERROR_REDIRECT_ENDPOINT', 'security_controller.send_confirmation_email') if self.security_service.confirm_user(user): self.after_this_request(self._commit) self.flash( _('flask_unchained.bundles.security:flash.email_confirmed'), category='success') else: self.flash( _('flask_unchained.bundles.security:flash.already_confirmed'), category='info') if user != current_user: self.security_service.logout_user() self.security_service.login_user(user) return self.redirect('SECURITY_POST_CONFIRM_REDIRECT_ENDPOINT', 'SECURITY_POST_LOGIN_REDIRECT_ENDPOINT')
class RegisterForm(BaseForm, PasswordFormMixin, NextFormMixin): class Meta: model = User email = StringField(_('flask_unchained.bundles.security:form_field.email'), validators=[unique_user_email]) submit = SubmitField( _('flask_unchained.bundles.security:form_submit.register')) field_order = ('email', 'password', 'password_confirm', 'submit') def to_dict(self): def is_field_and_user_attr(member): return isinstance(member, Field) and hasattr( self.Meta.model, member.name) fields = inspect.getmembers(self, is_field_and_user_attr) return dict((key, value.data) for key, value in fields)
class ChangePasswordForm(BaseForm): class Meta: model = User model_fields = { 'new_password': '******', 'new_password_confirm': 'password' } password = fields.PasswordField( _('flask_unchained.bundles.security:form_field.password')) new_password = fields.PasswordField( _('flask_unchained.bundles.security:form_field.new_password')) new_password_confirm = fields.PasswordField( _('flask_unchained.bundles.security:form_field.retype_password'), validators=[new_password_equal]) submit = fields.SubmitField( _('flask_unchained.bundles.security:form_submit.change_password')) def __init__(self, *args, security_utils_service: SecurityUtilsService = injectable, **kwargs): super().__init__(*args, **kwargs) self.security_utils_service = security_utils_service def validate(self): result = super().validate() if not self.security_utils_service.verify_and_update_password( self.password.data, current_user): self.password.errors.append( _('flask_unchained.bundles.security:error.invalid_password')) return False if self.password.data == self.new_password.data: self.new_password.errors.append( _('flask_unchained.bundles.security:error.password_is_the_same' )) return False return result
def logout(self): """ View function to log a user out. Supports html and json requests. """ if current_user.is_authenticated: self.security_service.logout_user() if request.is_json: return '', HTTPStatus.NO_CONTENT self.flash(_('flask_unchained.bundles.security:flash.logout'), category='success') return self.redirect('SECURITY_POST_LOGOUT_REDIRECT_ENDPOINT')
class SendConfirmationForm(BaseForm): class Meta: model = User user = None email = StringField(_('flask_unchained.bundles.security:form_field.email'), validators=[valid_user_email]) submit = SubmitField( _('flask_unchained.bundles.security:form_submit.send_confirmation')) def __init__(self, *args, **kwargs): super(SendConfirmationForm, self).__init__(*args, **kwargs) if request.method == 'GET': self.email.data = request.args.get('email', None) def validate(self): if not super(SendConfirmationForm, self).validate(): return False if self.user.confirmed_at is not None: self.email.errors.append( _('flask_unchained.bundles.security:error.already_confirmed')) return False return True
def _get_login_manager( self, app: FlaskUnchained, anonymous_user: AnonymousUser, ) -> LoginManager: """ Get an initialized instance of Flask Login's :class:`~flask_login.LoginManager`. """ login_manager = LoginManager() login_manager.anonymous_user = anonymous_user or AnonymousUser login_manager.localize_callback = _ login_manager.request_loader(self._request_loader) login_manager.user_loader( lambda *a, **kw: self.security_utils_service.user_loader(*a, **kw)) login_manager.login_view = 'security_controller.login' login_manager.login_message = _( 'flask_unchained.bundles.security:error.login_required') login_manager.login_message_category = 'info' login_manager.needs_refresh_message = _( 'flask_unchained.bundles.security:error.fresh_login_required') login_manager.needs_refresh_message_category = 'info' login_manager.init_app(app) return login_manager
def validate(self): if not super().validate(): # FIXME-identity if (set(self.errors.keys()) - set(security_utils_service.get_identity_attributes())): return False self.user = security_utils_service.user_loader(self.email.data) if self.user is None: self.email.errors.append( _('flask_unchained.bundles.security:error.user_does_not_exist') ) return False elif not self.password.data: self.password.errors.append( _('flask_unchained.bundles.security:password_required')) return False elif not security_utils_service.verify_password( self.user, self.password.data): self.password.errors.append( _('flask_unchained.bundles.security:error.invalid_password')) return False return True
def register_user(self, user, allow_login=None, send_email=None): """ Service method to register a user. Sends signal `user_registered`. Returns True if the user has been logged in, False otherwise. """ should_login_user = (not self.security.confirmable or self.security.login_without_confirmation) should_login_user = (should_login_user if allow_login is None else allow_login and should_login_user) if should_login_user: user.active = True # confirmation token depends on having user.id set, which requires # the user be committed to the database self.user_manager.save(user, commit=True) confirmation_link, token = None, None if self.security.confirmable: token = self.security_utils_service.generate_confirmation_token( user) confirmation_link = url_for('security_controller.confirm_email', token=token, _external=True) user_registered.send(app._get_current_object(), user=user, confirm_token=token) if (send_email or (send_email is None and app.config.get('SECURITY_SEND_REGISTER_EMAIL'))): self.send_mail( _('flask_unchained.bundles.security:email_subject.register'), to=user.email, template='security/email/welcome.html', user=user, confirmation_link=confirmation_link) if should_login_user: return self.login_user(user) return False
def send_reset_password_instructions(self, user): """ Sends the reset password instructions email for the specified user. Sends signal `reset_password_instructions_sent`. :param user: The user to send the instructions to. """ token = self.security_utils_service.generate_reset_password_token(user) reset_link = url_for('security_controller.reset_password', token=token, _external=True) self.send_mail( _('flask_unchained.bundles.security:email_subject.reset_password_instructions'), to=user.email, template='security/email/reset_password_instructions.html', user=user, reset_link=reset_link) reset_password_instructions_sent.send(app._get_current_object(), user=user, token=token)
def __call__(self, value): super().__call__(value) if not value: return message = self.msg if message is None: message = _('flask_unchained.bundles.security:email_invalid') if not value or '@' not in value: raise ValidationError(message) user_part, domain_part = value.rsplit('@', 1) if not self.user_regex.match(user_part): raise ValidationError(message) if not self.validate_hostname(domain_part): raise ValidationError(message)
def send_email_confirmation_instructions(self, user): """ Sends the confirmation instructions email for the specified user. Sends signal `confirm_instructions_sent`. :param user: The user to send the instructions to. """ token = self.security_utils_service.generate_confirmation_token(user) confirmation_link = url_for('security_controller.confirm_email', token=token, _external=True) self.send_mail( _('flask_unchained.bundles.security:email_subject.email_confirmation_instructions'), to=user.email, template='security/email/email_confirmation_instructions.html', user=user, confirmation_link=confirmation_link) confirm_instructions_sent.send(app._get_current_object(), user=user, token=token)
def reset_password(self, user, password): """ Service method to reset a user's password. The same as :meth:`change_password` except we this method sends a different notification email. Sends signal `password_reset`. :param user: :param password: :return: """ user.password = password self.user_manager.save(user) if app.config.SECURITY_SEND_PASSWORD_RESET_NOTICE_EMAIL: self.send_mail( _('flask_unchained.bundles.security:email_subject.password_reset_notice'), to=user.email, template='security/email/password_reset_notice.html', user=user) password_reset.send(app._get_current_object(), user=user)
def forgot_password(self): """ View function to request a password recovery email with a reset token. Supports html and json requests. """ form = self._get_form('SECURITY_FORGOT_PASSWORD_FORM') if form.validate_on_submit(): self.security_service.send_reset_password_instructions(form.user) self.flash(_( 'flask_unchained.bundles.security:flash.password_reset_request', email=form.user.email), category='info') if request.is_json: return '', HTTPStatus.NO_CONTENT elif form.errors and request.is_json: return self.errors(form.errors) return self.render( 'forgot_password', forgot_password_form=form, **self.security.run_ctx_processor('forgot_password'))
def send_confirmation_email(self): """ View function which sends confirmation token and instructions to a user. """ form = self._get_form('SECURITY_SEND_CONFIRMATION_FORM') if form.validate_on_submit(): self.security_service.send_email_confirmation_instructions( form.user) self.flash(_( 'flask_unchained.bundles.security:flash.confirmation_request', email=form.user.email), category='info') if request.is_json: return '', HTTPStatus.NO_CONTENT elif form.errors and request.is_json: return self.errors(form.errors) return self.render( 'send_confirmation_email', send_confirmation_form=form, **self.security.run_ctx_processor('send_confirmation_email'))
def change_password(self, user, password, send_email=None): """ Service method to change a user's password. Sends signal `password_changed`. :param user: The :class:`User`'s password to change. :param password: The new password. :param send_email: Whether or not to override the config option ``SECURITY_SEND_PASSWORD_CHANGED_EMAIL`` and force either sending or not sending an email. """ user.password = password self.user_manager.save(user) if send_email or (app.config.SECURITY_SEND_PASSWORD_CHANGED_EMAIL and send_email is None): self.send_mail( _('flask_unchained.bundles.security:email_subject.password_changed_notice'), to=user.email, template='security/email/password_changed_notice.html', user=user) password_changed.send(app._get_current_object(), user=user)
def _get_validators(cls, column_name): rv = [] col = cls.__table__.c.get(column_name) validators = cls.__validators__.get(column_name, []) for validator in validators: if isinstance(validator, str) and hasattr(cls, validator): rv.append(getattr(cls, validator)) else: if inspect.isclass(validator): validator = validator() rv.append(validator) if col is not None: not_null = not col.primary_key and not col.nullable required_msg = col.info and col.info.get('required', None) if not_null or required_msg: if isinstance(required_msg, bool): required_msg = None elif isinstance(required_msg, str): required_msg = _(required_msg) rv.append(Required(required_msg or None)) return rv
def login(self): """ View function to log a user in. Supports html and json requests. """ form = self._get_form('SECURITY_LOGIN_FORM') if form.validate_on_submit(): try: self.security_service.login_user(form.user, form.remember.data) except AuthenticationError as e: form._errors = {'_error': [str(e)]} else: self.after_this_request(self._commit) if request.is_json: return self.jsonify({ 'token': form.user.get_auth_token(), 'user': form.user }) self.flash(_('flask_unchained.bundles.security:flash.login'), category='success') return self.redirect('SECURITY_POST_LOGIN_REDIRECT_ENDPOINT') else: # FIXME-identity identity_attrs = app.config.SECURITY_USER_IDENTITY_ATTRIBUTES msg = f"Invalid {', '.join(identity_attrs)} and/or password." # we just want a single top-level form error form._errors = {'_error': [msg]} for field in form._fields.values(): field.errors = None if form.errors and request.is_json: return self.jsonify({'error': form.errors.get('_error')[0]}, code=HTTPStatus.UNAUTHORIZED) return self.render('login', login_user_form=form, **self.security.run_ctx_processor('login'))