Question

Flask-Security takes a lot of the grunt work out of authentication and authorization for Python Flask web application development. I've run into one snag, though.

On the login page, messages are flashed in response to various invalid inputs. Examples include: - Specified user does not exist - Invalid password - Account is disabled

This isn't in accordance with security best practices. You should not divulge to the user the details of why his or her login attempt was rejected. The above messages make it easier for a hacker to identify valid usernames.

I'd like to override these standard Flask-Security messages, replacing them all with something like "Invalid username or password." However, I haven't found a convenient way to do so.

These messages are stored in _default_messages in site-packages/flask_security/core.py. I could modify that file, but that's not a good solution: it will break if I reinstall or update Flask-Security.

I know I can customize Flask-Security's default views. But the views contain helpful code such as

{{ render_field_with_errors(login_user_form.email) }}

that hides the implementation details of the login form. I wouldn't want to discard all that helpful code and rewrite most of it just to change a few messages.

Does anyone know a better way to customize Flask-Security's login messages?

Was it helpful?

Solution 2

From reading through the code, it looks like it sets config defaults on all of the messages:

_default_messages = {
    'INVALID_PASSWORD': ('Invalid password', 'error'),
}

... later on in the init method ....

for key, value in _default_messages.items():
        app.config.setdefault('SECURITY_MSG_' + key, value)

To change the message, it looks like all you'd have to do is set this in your app.config:

SECURITY_MSG_INVALID_PASSWORD = ('Your username and password do not match our records', 'error'),

If it doesn't, it looks like it would not be difficult to refactor the module to use babel, or something similar. Would be a great thing to do as a good FOSS citizen.

OTHER TIPS

Rachel is partly right (btw, all the messages are in core.py in flask-security). By changing the default security messages you can make the error messages broader. However, assuming that you are using the standard rendering of fields, the error message will still be attached to the form element that caused the problem. So it is not difficult to understand that the problem is in the user name or in the password.

What I did:

  1. Change the messages in your config file to broad messages. I have changed the following messages:

    SECURITY_MSG_INVALID_PASSWORD = ("Bad username or password", "error")
    SECURITY_MSG_PASSWORD_NOT_PROVIDED = ("Bad username or password", "error")
    SECURITY_MSG_USER_DOES_NOT_EXIST = ("Bad username or password", "error")
    
  2. Used the macro that renders the fields without the error messages (render_field). If you are using flask-bootstrap, then there is no such macro, so I created one of my own (very easy, just remove the error block, and also the class that colors the form element). The below is just copy+pasted from flask-bootstrap, and only removed the field error code:

    {% macro bootstrap_form_field_no_errors(field,
                        form_type="basic",
                        horizontal_columns=('lg', 2, 10),
                        button_map={}) %}
    {% if field.widget.input_type == 'checkbox' %}
      {% call _hz_form_wrap(horizontal_columns, form_type, True) %}
        <div class="checkbox">
          <label>
            {{field()|safe}} {{field.label.text|safe}}
          </label>
        </div>
      {% endcall %}
    {%- elif field.type == 'RadioField' -%}
      {# note: A cleaner solution would be rendering depending on the widget,
         this is just a hack for now, until I can think of something better #}
      {% call _hz_form_wrap(horizontal_columns, form_type, True) %}
        {% for item in field -%}
          <div class="radio">
            <label>
              {{item|safe}} {{item.label.text|safe}}
            </label>
          </div>
        {% endfor %}
      {% endcall %}
    {%- elif field.type == 'SubmitField' -%}
      {# note: same issue as above - should check widget, not field type #}
      {% call _hz_form_wrap(horizontal_columns, form_type, True) %}
        {{field(class='btn btn-%s' % button_map.get(field.name, 'default'))}}
      {% endcall %}
    {%- elif field.type == 'FormField' -%}
    {# note: FormFields are tricky to get right and complex setups requiring
       these are probably beyond the scope of what this macro tries to do.
       the code below ensures that things don't break horribly if we run into
       one, but does not try too hard to get things pretty. #}
      <fieldset>
        <legend>{{field.label}}</legend>
        {%- for subfield in field %}
          {% if not bootstrap_is_hidden_field(subfield) -%}
            {{ form_field(subfield,
                          form_type=form_type,
                          horizontal_columns=horizontal_columns,
                          button_map=button_map) }}
          {%- endif %}
        {%- endfor %}
      </fieldset>
    {% else -%}
      <div class="form-group">
          {%- if form_type == "inline" %}
            {{field.label(class="sr-only")|safe}}
            {{field(class="form-control", placeholder=field.description, **kwargs)|safe}}
          {% elif form_type == "horizontal" %}
            {{field.label(class="control-label " + (
              " col-%s-%s" % horizontal_columns[0:2]
            ))|safe}}
            <div class=" col-{{horizontal_columns[0]}}-{{horizontal_columns[2]}}">
              {{field(class="form-control", **kwargs)|safe}}
            </div>
            {%- if field.description -%}
              {% call _hz_form_wrap(horizontal_columns, form_type) %}
                <p class="help-block">{{field.description|safe}}</p>
              {% endcall %}
            {%- endif %}
          {%- else -%}
            {{field.label(class="control-label")|safe}}
            {{field(class="form-control", **kwargs)|safe}}
    
            {%- if field.errors %}
              {%- for error in field.errors %}
                <p class="help-block">{{error}}</p>
              {%- endfor %}
            {%- elif field.description -%}
              <p class="help-block">{{field.description|safe}}</p>
            {%- endif %}
          {%- endif %}
      </div>
    {% endif %}
    {% endmacro %}
    
  3. Created a new macro that renders all the errors of all the fields, and placed it in the top of the form. I have not seen that multiple errors are generated simultaneously, but you don't really know which field caused the error. Again, my code matches the flask-bootstrap style, but you can easily remove the bootstrap-specific elements.

    {% macro fields_errors() %}
    {% for field in varargs %}
    {% if field.errors %}
      {% for error in field.errors %}
        {% call _hz_form_wrap(horizontal_columns, form_type) %}
          <div class="alert alert-danger">{{error}}</div>
        {% endcall %}
      {% endfor %}
    {% endif %}
    {% endfor %}
    {% endmacro %}
    

In the form itself, you call this macro with all the form fields:

    {{ fields_errors(login_user_form.email, login_user_form.password, login_user_form.remember) }}`

At first I used Rachel's answer, but because the error is still attached to form field the end user can tell if they have a valid email and an incorrect password... So, as I use flash messages for pretty much all feedback, I found editing flask_security itself was the easiest approach for me.

If you open the file

site-packages\flask_security\forms.py

you'll find the following code:

    if self.user is None:
        self.email.errors.append(get_message('USER_DOES_NOT_EXIST')[0])
        return False
    if not self.user.password:
        self.password.errors.append(get_message('PASSWORD_NOT_SET')[0])
        return False
    if not verify_and_update_password(self.password.data, self.user):
        self.password.errors.append(get_message('INVALID_PASSWORD')[0])
        return False
    if requires_confirmation(self.user):
        self.email.errors.append(get_message('CONFIRMATION_REQUIRED')[0])
        return False
    if not self.user.is_active:
        self.email.errors.append(get_message('DISABLED_ACCOUNT')[0])
        return False

You can see that it appends an error the relevant field (email or password), You could comment those out to simply provide no feedback on failure - personally I flashed an error as below:

    if self.user is None:
        #self.email.errors.append(get_message('USER_DOES_NOT_EXIST')[0])
        flash('Error: There was an issue logging you in')
        return False
    if not self.user.password:
        #self.password.errors.append(get_message('PASSWORD_NOT_SET')[0])
        flash('Error: There was an issue logging you in')
        return False
    if not verify_and_update_password(self.password.data, self.user):
        #self.password.errors.append(get_message('INVALID_PASSWORD')[0])
        flash('Error: There was an issue logging you in')
        return False
    if requires_confirmation(self.user):
        #self.email.errors.append(get_message('CONFIRMATION_REQUIRED')[0])
        flash('Error: You have not confirmed your email')
        return False
    if not self.user.is_active:
        #self.email.errors.append(get_message('DISABLED_ACCOUNT')[0])
        flash('Error: There was an issue logging you in')
        return False

I'm not sure if there is a better way to implement the change I've put in than editing the package directly (I'm sure it's bad practice), but it worked for me, and I understand it.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top