Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to override Flask-Security default messages?

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?

like image 239
Steve Saporta Avatar asked Apr 17 '14 14:04

Steve Saporta


1 Answers

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) }}`
like image 66
udiw Avatar answered Sep 21 '22 19:09

udiw