How to override Flask-Security default messages?

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) }}`

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.