summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2020-07-31 15:22:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2020-07-31 15:22:43 +0000
commitffbddd57eaf4104b504f44407d187712e7a1faac (patch)
tree506c190fb60aa5187ad0e143b48600e6493bd79d
parentInitial commit. (diff)
downloadflask-security-ffbddd57eaf4104b504f44407d187712e7a1faac.tar.xz
flask-security-ffbddd57eaf4104b504f44407d187712e7a1faac.zip
Adding upstream version 3.4.2.upstream/3.4.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
-rw-r--r--.editorconfig34
-rw-r--r--AUTHORS52
-rw-r--r--CHANGES.rst700
-rw-r--r--CONTRIBUTING.rst149
-rw-r--r--Flask_Security_Too.egg-info/PKG-INFO112
-rw-r--r--Flask_Security_Too.egg-info/SOURCES.txt168
-rw-r--r--Flask_Security_Too.egg-info/dependency_links.txt1
-rw-r--r--Flask_Security_Too.egg-info/not-zip-safe1
-rw-r--r--Flask_Security_Too.egg-info/requires.txt114
-rw-r--r--Flask_Security_Too.egg-info/top_level.txt1
-rw-r--r--LICENSE21
-rw-r--r--MANIFEST.in26
-rw-r--r--PKG-INFO112
-rw-r--r--README.rst75
-rw-r--r--babel.ini10
-rw-r--r--docs/Makefile153
-rw-r--r--docs/_static/logo-owl-105.pngbin0 -> 20399 bytes
-rw-r--r--docs/_static/logo-owl-68.pngbin0 -> 9445 bytes
-rw-r--r--docs/_static/logo-owl-full-240.pngbin0 -> 68319 bytes
-rw-r--r--docs/_static/logo-owl-full.pngbin0 -> 29491 bytes
-rw-r--r--docs/_static/openapi_view.html20
-rw-r--r--docs/api.rst252
-rw-r--r--docs/authors.rst1
-rw-r--r--docs/changelog.rst1
-rw-r--r--docs/conf.py270
-rw-r--r--docs/configuration.rst1223
-rw-r--r--docs/contributing.rst1
-rw-r--r--docs/customizing.rst425
-rw-r--r--docs/features.rst244
-rw-r--r--docs/index.rst89
-rw-r--r--docs/models.rst129
-rw-r--r--docs/patterns.rst312
-rw-r--r--docs/quickstart.rst455
-rw-r--r--docs/requirements.txt1
-rw-r--r--docs/spa.rst189
-rw-r--r--docs/two_factor_configurations.rst120
-rw-r--r--flask_security/__init__.py104
-rw-r--r--flask_security/async_compat.py18
-rw-r--r--flask_security/babel.py22
-rw-r--r--flask_security/cache.py43
-rw-r--r--flask_security/changeable.py46
-rw-r--r--flask_security/cli.py187
-rw-r--r--flask_security/confirmable.py100
-rw-r--r--flask_security/core.py1411
-rw-r--r--flask_security/datastore.py733
-rw-r--r--flask_security/decorators.py581
-rw-r--r--flask_security/forms.py611
-rw-r--r--flask_security/models/__init__.py12
-rw-r--r--flask_security/models/fsqla.py164
-rw-r--r--flask_security/models/fsqla_v2.py53
-rw-r--r--flask_security/passwordless.py62
-rw-r--r--flask_security/phone_util.py60
-rw-r--r--flask_security/quart_compat.py37
-rw-r--r--flask_security/recoverable.py107
-rw-r--r--flask_security/registerable.py72
-rw-r--r--flask_security/signals.py43
-rw-r--r--flask_security/templates/security/_macros.html28
-rw-r--r--flask_security/templates/security/_menu.html20
-rw-r--r--flask_security/templates/security/_messages.html9
-rw-r--r--flask_security/templates/security/base.html30
-rw-r--r--flask_security/templates/security/change_password.html14
-rw-r--r--flask_security/templates/security/email/change_notice.html4
-rw-r--r--flask_security/templates/security/email/change_notice.txt5
-rw-r--r--flask_security/templates/security/email/confirmation_instructions.html3
-rw-r--r--flask_security/templates/security/email/confirmation_instructions.txt3
-rw-r--r--flask_security/templates/security/email/login_instructions.html5
-rw-r--r--flask_security/templates/security/email/login_instructions.txt5
-rw-r--r--flask_security/templates/security/email/reset_instructions.html1
-rw-r--r--flask_security/templates/security/email/reset_instructions.txt3
-rw-r--r--flask_security/templates/security/email/reset_notice.html1
-rw-r--r--flask_security/templates/security/email/reset_notice.txt1
-rw-r--r--flask_security/templates/security/email/two_factor_instructions.html3
-rw-r--r--flask_security/templates/security/email/two_factor_instructions.txt3
-rw-r--r--flask_security/templates/security/email/two_factor_rescue.html1
-rw-r--r--flask_security/templates/security/email/two_factor_rescue.txt1
-rw-r--r--flask_security/templates/security/email/us_instructions.html9
-rw-r--r--flask_security/templates/security/email/us_instructions.txt10
-rw-r--r--flask_security/templates/security/email/welcome.html7
-rw-r--r--flask_security/templates/security/email/welcome.txt7
-rw-r--r--flask_security/templates/security/forgot_password.html13
-rw-r--r--flask_security/templates/security/login_user.html16
-rw-r--r--flask_security/templates/security/register_user.html17
-rw-r--r--flask_security/templates/security/reset_password.html14
-rw-r--r--flask_security/templates/security/send_confirmation.html13
-rw-r--r--flask_security/templates/security/send_login.html13
-rw-r--r--flask_security/templates/security/two_factor_setup.html38
-rw-r--r--flask_security/templates/security/two_factor_verify_code.html26
-rw-r--r--flask_security/templates/security/two_factor_verify_password.html13
-rw-r--r--flask_security/templates/security/us_setup.html47
-rw-r--r--flask_security/templates/security/us_signin.html29
-rw-r--r--flask_security/templates/security/us_verify.html27
-rw-r--r--flask_security/templates/security/verify.html13
-rw-r--r--flask_security/totp.py106
-rw-r--r--flask_security/translations/ca_ES/LC_MESSAGES/flask_security.mobin0 -> 6571 bytes
-rw-r--r--flask_security/translations/ca_ES/LC_MESSAGES/flask_security.po628
-rw-r--r--flask_security/translations/da_DK/LC_MESSAGES/flask_security.mobin0 -> 6091 bytes
-rw-r--r--flask_security/translations/da_DK/LC_MESSAGES/flask_security.po626
-rw-r--r--flask_security/translations/de_DE/LC_MESSAGES/flask_security.mobin0 -> 6309 bytes
-rw-r--r--flask_security/translations/de_DE/LC_MESSAGES/flask_security.po628
-rw-r--r--flask_security/translations/es_ES/LC_MESSAGES/flask_security.mobin0 -> 6574 bytes
-rw-r--r--flask_security/translations/es_ES/LC_MESSAGES/flask_security.po629
-rw-r--r--flask_security/translations/flask_security.pot607
-rw-r--r--flask_security/translations/fr_FR/LC_MESSAGES/flask_security.mobin0 -> 6684 bytes
-rw-r--r--flask_security/translations/fr_FR/LC_MESSAGES/flask_security.po630
-rw-r--r--flask_security/translations/ja_JP/LC_MESSAGES/flask_security.mobin0 -> 6641 bytes
-rw-r--r--flask_security/translations/ja_JP/LC_MESSAGES/flask_security.po616
-rw-r--r--flask_security/translations/nl_NL/LC_MESSAGES/flask_security.mobin0 -> 8973 bytes
-rw-r--r--flask_security/translations/nl_NL/LC_MESSAGES/flask_security.po638
-rw-r--r--flask_security/translations/pt_BR/LC_MESSAGES/flask_security.mobin0 -> 6015 bytes
-rw-r--r--flask_security/translations/pt_BR/LC_MESSAGES/flask_security.po622
-rw-r--r--flask_security/translations/pt_PT/LC_MESSAGES/flask_security.mobin0 -> 6300 bytes
-rw-r--r--flask_security/translations/pt_PT/LC_MESSAGES/flask_security.po626
-rw-r--r--flask_security/translations/ru_RU/LC_MESSAGES/flask_security.mobin0 -> 7981 bytes
-rw-r--r--flask_security/translations/ru_RU/LC_MESSAGES/flask_security.po623
-rw-r--r--flask_security/translations/tr_TR/LC_MESSAGES/flask_security.mobin0 -> 6093 bytes
-rw-r--r--flask_security/translations/tr_TR/LC_MESSAGES/flask_security.po621
-rw-r--r--flask_security/translations/zh_Hans_CN/LC_MESSAGES/flask_security.mobin0 -> 5511 bytes
-rw-r--r--flask_security/translations/zh_Hans_CN/LC_MESSAGES/flask_security.po616
-rw-r--r--flask_security/twofactor.py192
-rw-r--r--flask_security/unified_signin.py963
-rw-r--r--flask_security/utils.py1166
-rw-r--r--flask_security/views.py1178
-rw-r--r--pytest.ini6
-rw-r--r--requirements.txt3
-rw-r--r--setup.cfg40
-rwxr-xr-xsetup.py118
-rw-r--r--tests/conftest.py700
-rw-r--r--tests/templates/_messages.html9
-rw-r--r--tests/templates/_nav.html20
-rw-r--r--tests/templates/custom_security/change_password.html3
-rw-r--r--tests/templates/custom_security/forgot_password.html3
-rw-r--r--tests/templates/custom_security/login_user.html3
-rw-r--r--tests/templates/custom_security/register_user.html3
-rw-r--r--tests/templates/custom_security/reset_password.html3
-rw-r--r--tests/templates/custom_security/send_confirmation.html3
-rw-r--r--tests/templates/custom_security/send_login.html3
-rw-r--r--tests/templates/custom_security/tf_setup.html3
-rw-r--r--tests/templates/custom_security/tf_verify.html3
-rw-r--r--tests/templates/custom_security/tfc.html3
-rw-r--r--tests/templates/custom_security/us_setup.html3
-rw-r--r--tests/templates/custom_security/us_signin.html3
-rw-r--r--tests/templates/custom_security/us_verify.html3
-rw-r--r--tests/templates/custom_security/verify.html3
-rw-r--r--tests/templates/index.html3
-rw-r--r--tests/templates/register.html11
-rw-r--r--tests/templates/security/email/reset_instructions.html3
-rw-r--r--tests/templates/unauthorized.html3
-rw-r--r--tests/test_cache.py133
-rw-r--r--tests/test_changeable.py329
-rw-r--r--tests/test_cli.py153
-rw-r--r--tests/test_common.py726
-rw-r--r--tests/test_configuration.py45
-rw-r--r--tests/test_confirmable.py434
-rw-r--r--tests/test_context_processors.py221
-rw-r--r--tests/test_csrf.py559
-rw-r--r--tests/test_datastore.py509
-rw-r--r--tests/test_entities.py115
-rw-r--r--tests/test_hashing.py192
-rw-r--r--tests/test_misc.py913
-rw-r--r--tests/test_passwordless.py194
-rw-r--r--tests/test_recoverable.py526
-rw-r--r--tests/test_registerable.py347
-rw-r--r--tests/test_response.py164
-rw-r--r--tests/test_trackable.py94
-rw-r--r--tests/test_two_factor.py964
-rw-r--r--tests/test_unified_signin.py1425
-rw-r--r--tests/utils.py202
-rw-r--r--tests/view_scaffold.py268
-rw-r--r--tox.ini25
169 files changed, 31590 insertions, 0 deletions
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..8b942d4
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+
+root = true
+
+[*]
+indent_style = space
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+charset = utf-8
+
+# Python files
+[*.py]
+indent_size = 4
+# isort plugin configuration
+known_first_party = flask_security
+multi_line_output = 2
+default_section = THIRDPARTY
+
+# RST files (used by sphinx)
+[*.rst]
+indent_size = 4
+
+# CSS, HTML, JS, JSON, YML
+[*.{css,html,js,json,yml}]
+indent_size = 2
+
+# Matches the exact files either package.json or .travis.yml
+[{package.json,.travis.yml}]
+indent_size = 2
+
+# Dockerfile
+[Dockerfile]
+indent_size = 4
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..c94c7aa
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,52 @@
+Flask-Security was written by Matt Wright and various contributors.
+
+Flask-Security-Too is an independently maintained repo:
+
+Development Lead
+````````````````
+
+- Chris Wagner <jwag956@github.com>
+
+Maintainer
+``````````
+
+- Chris Wagner <jwag956@github.com>
+
+Patches and Suggestions
+```````````````````````
+
+Alexander Sukharev
+Alexey Poryadin
+Andrew J. Camenga
+Anthony Plunkett
+Artem Andreev
+Catherine Wise
+Chris Haines
+Christophe Simonis
+David Ignacio
+Eric Butler
+Eskil Heyn Olsen
+Iuri de Silvio
+Jay Goel
+Jiri Kuncar
+Joe Esposito
+Joe Hand
+Josh Purvis
+Kostyantyn Leschenko
+Luca Invernizzi
+Manuel Ebert
+Martin Maillard
+Paweł Krześniak
+Robert Clark
+Rodrigue Cloutier
+Rotem Yaari
+Srijan Choudhary
+Tristan Escalada
+Vadim Kotov
+Walt Askew
+John Paraskevopoulos
+Chris Wagner
+Eric Regnier
+Gal Stainfeld
+Ivan Piskunov
+Tyler Baur
diff --git a/CHANGES.rst b/CHANGES.rst
new file mode 100644
index 0000000..f5947b4
--- /dev/null
+++ b/CHANGES.rst
@@ -0,0 +1,700 @@
+Flask-Security Changelog
+========================
+
+Here you can see the full list of changes between each Flask-Security release.
+
+Version 4.0.0
+-------------
+
+Release Target 2020
+
+- Removal of python 2.7 and <3.6 support
+- Removal of token caching feature (a relatively new feature that has some systemic issues)
+- Other possible breaking changes tracked `here`_
+
+.. _here: https://github.com/Flask-Middleware/flask-security/issues/85
+
+Version 3.4.2
+-------------
+
+Released May x, 2020
+
+Only change is to move repo to the Flask-Middleware github organization.
+
+Version 3.4.1
+--------------
+
+Released April 22, 2020
+
+Fix a bunch of bugs in new unified sign in along with a couple other major issues.
+
+Fixed
++++++
+- (:issue:`298`) Alternative ID feature ran afoul of postgres/psycopg2 finickiness.
+- (:issue:`300`) JSON 401 responses had WWW-Authenticate Header attached - that caused
+ browsers to pop up their own login/password form. Not what applications want.
+- (:issue:`280`) Allow admin/api to setup TFA (and unified sign in) out of band.
+ Please see :meth:`.UserDatastore.tf_set`, :meth:`.UserDatastore.tf_reset`,
+ :meth:`.UserDatastore.us_set`, :meth:`.UserDatastore.us_reset` and
+ :meth:`.UserDatastore.reset_user_access`.
+- (:pr:`305`) We used form._errors which wasn't very pythonic, and it was
+ removed in WTForms 2.3.0.
+- (:pr:`310`) WTForms 2.3.0 made email_validator optional - we need it.
+
+
+Version 3.4.0
+-------------
+
+Released March 31, 2020
+
+Features
+++++++++
+- (:pr:`257`) Support a unified sign in feature. Please see :ref:`unified-sign-in`.
+- (:pr:`265`) Add phone number validation class. This is used in both unified sign in
+ as well as two-factor when using ``sms``.
+- (:pr:`274`) Add support for 'freshness' of caller's authentication. This permits endpoints
+ to be additionally protected by ensuring a recent authentication.
+- (:issue:`99`, :issue:`195`) Support pluggable password validators. Provide a default
+ validator that offers complexity and breached support.
+- (:issue:`266`) Provide interface to two-factor send_token so that applications
+ can provide error mitigation. Defaults to returning errors if can't send the verification code.
+- (:pr:`247`) Updated all-inclusive data models (fsqlaV2). Add fields necessary for the new unified sign in feature
+ and changed 'username' to be unique (but not required).
+- (:pr:`245`) Use fs_uniquifier as the default Flask-Login 'alternative token'. Basically
+ this means that changing the fs_uniquifier will cause outstanding auth tokens, session and remember me
+ cookies to be invalidated. So if an account gets compromised, an admin can easily stop access. Prior to this
+ cookies were storing the 'id' which is the user's primary key - difficult to change! (kishi85)
+
+Fixed
++++++
+- (:issue:`273`) Don't allow reset password for accounts that are disabled.
+- (:issue:`282`) Add configuration that disallows GET for logout. Allowing GET can
+ cause some denial of service issues. The default still allows GET for backwards
+ compatibility. (kantorii)
+- (:issue:`258`) Reset password wasn't integrated into the two-factor feature and therefore
+ two-factor auth could be bypassed.
+- (:issue:`254`) Allow lists and sets as underlying permissions. (pffs)
+- (:issue:`251`) Allow a registration form to have additional fields that aren't part of the user model
+ that are just passed to the user_registered.send signal, where the application can perform arbitrary
+ additional actions required during registration. (kuba-lilz)
+- (:issue:`249`) Add configuration to disable the 'role-joining' optimization for SQLAlchemy. (pffs)
+- (:issue:`238`) Fix more issues with atomically setting the new TOTP secret when setting up two-factor. (kishi85)
+- (:pr:`240`) Fix Quart Compatibility. (ristellise)
+- (:issue:`232`) CSRF Cookie not being set when using 'Remember Me' cookie to re-sign in. (kishi85)
+- (:issue:`229`) Two-factor enabled accounts didn't work with the Remember Me feature. (kishi85)
+
+As part of adding unified sign in, there were many similarities with two-factor.
+Some refactoring was done to unify naming, configuration variables etc.
+It should all be backwards compatible.
+
+- In TWO_FACTOR_ENABLED_METHODS "mail" was changed to "email". "mail" will still
+ be honored if already stored in DB. Also "google_authenticator" is now just "authenticator".
+- TWO_FACTOR_SECRET, TWO_FACTOR_URI_SERVICE_NAME, TWO_FACTOR_SMS_SERVICE, and TWO_FACTOR_SMS_SERVICE_CONFIG
+ have all been deprecated in favor of names that are the same for two-factor and unified sign in.
+
+Other changes with possible backwards compatibility issues:
+
+- ``/tf-setup`` never did any phone number validation. Now it does.
+
+Version 3.3.3
+-------------
+
+Released February 11, 2020
+
+Minor changes required to work with latest released Werkzeug and Flask-Login.
+
+Version 3.3.2
+-------------
+
+Released December 7, 2019
+
+- (:issue:`215`) Fixed 2FA totp secret regeneration bug (kishi85)
+- (:issue:`172`) Fixed 'next' redirect error in login view
+- (:issue:`221`) Fixed regressions in login view when already authenticated user
+ again does a GET or POST.
+- (:issue:`219`) Added example code for unit testing FS protected routes.
+- (:issue:`223`) Integrated two-factor auth into registration and confirmation.
+
+Thanks to kuba-lilz and kishi85 for finding and providing detailed issue reports.
+
+In Flask-Security 3.3.0 the login view was changed to allow already authenticated
+users to access the view. Prior to 3.3.0, the login view was protected with
+@anonymous_user_required - so any access (via GET or POST) would simply redirect
+the user to the ``POST_LOGIN_VIEW``. With the 3.3.0 changes, both GET and POST
+behaved oddly. GET simply returned the login template, and POST attempted to
+log out the current user, and log in the new user. This was problematic since
+this couldn't possibly work with CSRF.
+The old behavior has been restored, with the subtle change that older Flask-Security
+releases did not look at "next" in the form or request for the redirect,
+and now, all redirects from the login view will honor "next".
+
+Version 3.3.1
+-------------
+
+Released November 16, 2019
+
+- (:pr:`197`) Add `Quart <https://gitlab.com/pgjones/quart/>`_ compatibility (Ristellise)
+- (:pr:`194`) Add Python 3.8 support into CI (jdevera)
+- (:pr:`196`) Improve docs around Single Page Applications and React (acidjunk)
+- (:issue:`201`) fsqla model was added to __init__.py making Sqlalchemy a required package.
+ That is wrong and has been removed. Applications must now explicitly import from ``flask_security.models``
+- (:pr:`204`) Fix/improve examples and quickstart to show one MUST call hash_password() when
+ creating users programmatically. Also show real SECRET_KEYs and PASSWORD_SALTs and how to generate them.
+- (:pr:`209`) Add argon2 as an allowable password hash.
+- (:pr:`210`) Improve integration with Flask-Admin. Actually - this PR improves localization support
+ by adding a method ``_fsdomain`` to jinja2's global environment. Added documentation
+ around localization.
+
+
+Version 3.3.0
+-------------
+
+Released September 26, 2019
+
+**There are several default behavior changes that might break existing applications.
+Most have configuration variables that restore prior behavior**.
+
+**If you use Authentication Tokens (rather than session cookies) you MUST make a (small) change.
+Please see below for details.**
+
+- (:pr:`120`) Native support for Permissions as part of Roles. Endpoints can be
+ protected via permissions that are evaluated based on role(s) that the user has.
+- (:issue:`126`, :issue:`93`, :issue:`96`) Revamp entire CSRF handling. This adds support for Single Page Applications
+ and having CSRF protection for browser(session) authentication but ignored for
+ token based authentication. Add extensive documentation about all the options.
+- (:issue:`156`) Token authentication is slow. Please see below for details on how to enable a new, fast implementation.
+- (:issue:`130`) Enable applications to provide their own :meth:`.render_json` method so that they can create
+ unified API responses.
+- (:issue:`121`) Unauthorization callback not quite right. Split into 2 different callbacks - one for
+ unauthorized and one for unauthenticated. Made default unauthenticated handler use Flask-Login's unauthenticated
+ method to make everything uniform. Extensive documentation added. `.Security.unauthorized_callback` has been deprecated.
+- (:pr:`120`) Add complete User and Role model mixins that support all features. Modify tests and Quickstart documentation
+ to show how to use these. Please see :ref:`responsetopic` for details.
+- Improve documentation for :meth:`.UserDatastore.create_user` to make clear that hashed password
+ should be passed in.
+- Improve documentation for :class:`.UserDatastore` and :func:`.verify_and_update_password`
+ to make clear that caller must commit changes to DB if using a session based datastore.
+- (:issue:`122`) Clarify when to use ``confirm_register_form`` rather than ``register_form``.
+- Fix bug in 2FA that didn't commit DB after using `verify_and_update_password`.
+- Fix bug(s) in UserDatastore where changes to user ``active`` flag weren't being added to DB.
+- (:issue:`127`) JSON response was failing due to LazyStrings in error response.
+- (:issue:`117`) Making a user inactive should stop all access immediately.
+- (:issue:`134`) Confirmation token can no longer be reused. Added
+ *SECURITY_AUTO_LOGIN_AFTER_CONFIRM* option for applications that don't want the user
+ to be automatically logged in after confirmation (defaults to True - existing behavior).
+- (:issue:`159`) The ``/register`` endpoint returned the Authentication Token even though
+ confirmation was required. This was a huge security hole - it has been fixed.
+- (:issue:`160`) The 2FA totp_secret would be regenerated upon submission, making QRCode not work. (malware-watch)
+- (:issue:`166`) `default_render_json` uses ``flask.make_response`` and forces the Content-Type to JSON for generating the response (koekie)
+- (:issue:`166`) *SECURITY_MSG_UNAUTHENTICATED* added to the configuration.
+- (:pr:`168`) When using the @auth_required or @auth_token_required decorators, the token
+ would be verified twice, and the DB would be queried twice for the user. Given how slow
+ token verification is - this was a significant issue. That has been fixed.
+- (:issue:`84`) The :func:`.anonymous_user_required` was not JSON friendly - always
+ performing a redirect. Now, if the request 'wants' a JSON response - it will receive a 400 with an error
+ message defined by *SECURITY_MSG_ANONYMOUS_USER_REQUIRED*.
+- (:pr:`145`) Improve 2FA templates to that they can be localized. (taavie)
+- (:issue:`173`) *SECURITY_UNAUTHORIZED_VIEW* didn't accept a url (just an endpoint). All other view
+ configurations did. That has been fixed.
+
+Possible compatibility issues
++++++++++++++++++++++++++++++
+
+- (:pr:`164`) In prior releases, the Authentication Token was returned as part of the JSON response to each
+ successful call to `/login`, `/change`, or `/reset/{token}` API call. This is not a great idea since
+ for browser-based UIs that used JSON request/response, and used session based authentication - they would
+ be sent this token - even though it was likely ignored. Since these tokens by default have no expiration time
+ this exposed a needless security hole. The new default behavior is to ONLY return the Authentication Token from those APIs
+ if the query param ``include_auth_token`` is added to the request. Prior behavior can be restored by setting
+ the *SECURITY_BACKWARDS_COMPAT_AUTH_TOKEN* configuration variable.
+
+- (:pr:`120`) :class:`.RoleMixin` now has a method :meth:`.get_permissions` which is called as part
+ each request to add Permissions to the authenticated user. It checks if the RoleModel
+ has a property ``permissions`` and assumes it is a comma separated string of permissions.
+ If your model already has such a property this will likely fail. You need to override :meth:`.get_permissions`
+ and simply return an emtpy set.
+
+- (:issue:`121`) Changes the default (failure) behavior for views protected with @auth_required, @token_auth_required,
+ or @http_auth_required. Before, a 401 was returned with some stock html. Now, Flask-Login.unauthorized() is
+ called (the same as @login_required does) - which by default redirects to a login page/view. If you had provided your own
+ `.Security.unauthorized_callback` there are no changes - that will still be called first. The old default
+ behavior can be restored by setting *SECURITY_BACKWARDS_COMPAT_UNAUTHN* to True. Please see :ref:`responsetopic` for details.
+
+- (:issue:`127`) Fix for LazyStrings in json error response. The fix for this has Flask-Security registering
+ its own JsonEncoder on its blueprint. If you registered your own JsonEncoder for your app - it will no
+ longer be called when serializing responses to Flask-Security endpoints. You can register your JsonEncoder
+ on Flask-Security's blueprint by sending it as `json_encoder_cls` as part of initialization. Be aware that your
+ JsonEncoder needs to handle LazyStrings (see speaklater).
+
+- (:issue:`84`) Prior to this fix - anytime the decorator :func:`.anonymous_user_required` failed, it caused a redirect to
+ the post_login_view. Now, if the caller wanted a JSON response, it will return a 400.
+
+- (:issue:`156`) Faster Authentication Token introduced the following non-backwards compatible behavior change:
+
+ * Since the old Authentication Token algorithm used the (hashed) user's password, those tokens would be invalidated
+ whenever the user changed their password. This is not likely to be what most users expect. Since the new
+ Authentication Token algorithm doesn't refer to the user's password, changing the user's password won't invalidate
+ outstanding Authentication Tokens. The method :meth:`.UserDatastore.set_uniquifier` can be used by an administrator
+ to change a user's ``fs_uniquifier`` - but nothing the user themselves can do to invalidate their Authentication Tokens.
+ Setting the *SECURITY_BACKWARDS_COMPAT_AUTH_TOKEN_INVALIDATE* configuration variable will cause the user's ``fs_uniquifier`` to
+ be changed when they change their password, thus restoring prior behavior.
+
+
+New fast authentication token implementation
+++++++++++++++++++++++++++++++++++++++++++++
+Current auth tokens are slow because they use the user's password (hashed) as a uniquifier (the
+user id isn't really enough since it might be reused). This requires checking the (hashed) password against
+what is in the token on EVERY request - however hashing is (on purpose) slow. So this can add almost a whole second
+to every request.
+
+To solve this, a new attribute in the User model was added - ``fs_uniquifier``. If this is present in your
+User model, then it will be used instead of the password for ensuring the token corresponds to the correct user.
+This is very fast. If that attribute is NOT present - then the behavior falls back to the existing (slow) method.
+
+
+DB Migration
+~~~~~~~~~~~~
+
+To use the new UserModel mixins or to add the column ``user.fs_uniquifier`` to speed up token
+authentication, a schema AND data migration needs to happen. If you are using Alembic the schema migration is
+easy - but you need to add ``fs_uniquifier`` values to all your existing data. You can
+add code like this to your migrations::update method::
+
+ # be sure to MODIFY this line to make nullable=True:
+ op.add_column('user', sa.Column('fs_uniquifier', sa.String(length=64), nullable=True))
+
+ # update existing rows with unique fs_uniquifier
+ import uuid
+ user_table = sa.Table('user', sa.MetaData(), sa.Column('id', sa.Integer, primary_key=True),
+ sa.Column('fs_uniquifier', sa.String))
+ conn = op.get_bind()
+ for row in conn.execute(sa.select([user_table.c.id])):
+ conn.execute(user_table.update().values(fs_uniquifier=uuid.uuid4().hex).where(user_table.c.id == row['id']))
+
+ # finally - set nullable to false
+ op.alter_column('user', 'fs_uniquifier', nullable=False)
+
+
+Version 3.2.0
+-------------
+
+Released June 26th 2019
+
+- (opr #839) Support caching of authentication token (eregnier).
+ This adds a new configuration variable *SECURITY_USE_VERIFY_PASSWORD_CACHE*
+ which enables a cache (with configurable TTL) for authentication tokens.
+ This is a big performance boost for those accessing Flask-Security via token
+ as opposed to session.
+- (:pr:`81`) Support for JSON/Single-Page-Application. This completes support
+ for non-form based access to Flask-Security. See PR for details. (jwag956)
+- (:pr:`79` Add POST logout to enhance JSON usage (jwag956).
+- (:pr:`73`) Fix get_user for various DBs (jwag956).
+ This is a more complete fix than in opr #633.
+- (:pr:`78`, :pr:`103`) Add formal openapi API spec (jwag956).
+- (:pr:`86`, :pr:`94`, :pr:`98`, :pr:`101`, :pr:`104`) Add Two-factor authentication (opr #842) (baurt, jwag956).
+- (:issue:`108`) Fix form field label translations (jwag956)
+- (:issue:`115`) Fix form error message translations (upstream #801) (jwag956)
+- (:issue:`87`) Convert entire repo to Black (baurt)
+
+Version 3.1.0
+-------------
+
+Released never
+
+- (opr #487) Use Security.render_template in mails too (noirbizarre)
+- (opr #679) Optimize DB accesses by using an SQL JOIN when retrieving a user. (nfvs)
+- (opr #697) Add base template to security templates (grihabor)
+- (opr #633) datastore: get user by numeric identity attribute (jirikuncar)
+- (opr #703) bugfix: support application factory pattern (briancappello)
+- (opr #714) Make SECURITY_PASSWORD_SINGLE_HASH a list of scheme ignoring double hash (noirbizarre )
+- (opr #717) Allow custom login_manager to be passed in to Flask-Security (jaza)
+- (opr #727) Docs for OAauth2-based custom login manager (jaza)
+- (opr #779) core: make the User model check the password (mklassen)
+- (opr #730) Customizable send_mail (abulte)
+- (opr #726) core: fix default for UNAUTHORIZED_VIEW (jirijunkar)
+
+These should all be backwards compatible.
+
+Possible compatibility issues:
+
+- #487 - prior to this, render_template() was overiddable for views, but not
+ emails. If anyone actually relied on this behavior, this has changed.
+- #703 - get factory pattern working again. There was a very complex dance between
+ Security() instantiation and init_app regarding kwargs. This has been rationalized (hopefully).
+- #679 - SqlAlchemy SQL improvement. It is possible you will get the following error::
+
+ Got exception during processing: <class 'sqlalchemy.exc.InvalidRequestError'> -
+ 'User.roles' does not support object population - eager loading cannot be applied.
+
+ This is likely solveable by removing ``lazy='dynamic'`` from your Role definition.
+
+
+Performance improvements:
+
+- #679 - for sqlalchemy, for each request, there would be 2 DB accesses - now
+ there is one.
+
+Testing:
+For datastores operations, Sqlalchemy, peewee, pony were all tested against sqlite,
+postgres, and mysql real databases.
+
+
+Version 3.0.2
+-------------
+
+Released April 30th 2019
+
+- (opr #439) HTTP Auth respects SECURITY_USER_IDENTITY_ATTRIBUTES (pnpnpn)
+- (opr #660) csrf_enabled` deprecation fix (abulte)
+- (opr #671) Fix referrer loop in _get_unauthorized_view(). (nfvs)
+- (opr #675) Fix AttributeError in _request_loader (sbagan)
+- (opr #676) Fix timing attack on login form (cript0nauta)
+- (opr #683) Close db connection after running tests (reambus)
+- (opr #691) docs: add password salt to SQLAlchemy app example (KshitijKarthick)
+- (opr #692) utils: fix incorrect email sender type (switowski)
+- (opr #696) Fixed broken Click link (williamhatcher)
+- (opr #722) Fix password recovery confirmation on deleted user (kesara)
+- (opr #747) Update login_user.html (rickwest)
+- (opr #748) i18n: configurable the dirname domain (escudero)
+- (opr #835) adds relevant user to reset password form for validation purposes (fuhrysteve)
+
+These are bug fixes and a couple very small additions.
+No change in behavior and no new functionality.
+'opr#' is the original pull request from https://github.com/mattupstate/flask-security
+
+Version 3.0.1
+--------------
+
+Released April 28th 2019
+
+- Support 3.7 as part of CI
+- Rebrand to this forked repo
+- (#15) Build docs and translations as part of CI
+- (#17) Move to msgcheck from pytest-translations
+- (opr #669) Fix for Read the Docs (jirikuncar)
+- (opr #710) Spanish translation (maukoquiroga)
+- (opr #712) i18n: improvements of German translations (eseifert)
+- (opr #713) i18n: add Portuguese (Brazilian) translation (dinorox)
+- (opr #719) docs: fix anchor links and typos (kesara)
+- (opr #751) i18n: fix missing space (abulte)
+- (opr #762) docs: fixed proxy import (lsmith)
+- (opr #767) Update customizing.rst (allanice001)
+- (opr #776) i18n: add Portuguese (Portugal) translation (micael-grilo)
+- (opr #791) Fix documentation for mattupstate#781 (fmerges)
+- (opr #796) Chinese translations (Steinkuo)
+- (opr #808) Clarify that a commit is needed after login_user (christophertull)
+- (opr #823) Add Turkish translation (Admicos)
+- (opr #831) Catalan translation (miceno)
+
+These are all documentation and i18n changes - NO code changes. All except the last 3 were accepted and reviewed by
+the original Flask-Security team.
+Thanks as always to all the contributors.
+
+Version 3.0.0
+-------------
+
+Released May 29th 2017
+
+- Fixed a bug when user clicking confirmation link after confirmation
+ and expiration causes confirmation email to resend. (see #556)
+- Added support for I18N.
+- Added options `SECURITY_EMAIL_PLAINTEXT` and `SECURITY_EMAIL_HTML`
+ for sending respecively plaintext and HTML version of email.
+- Fixed validation when missing login information.
+- Fixed condition for token extraction from JSON body.
+- Better support for universal bdist wheel.
+- Added port of CLI using Click configurable using options
+ `SECURITY_CLI_USERS_NAME` and `SECURITY_CLI_ROLES_NAME`.
+- Added new configuration option `SECURITY_DATETIME_FACTORY` which can
+ be used to force default timezone for newly created datetimes.
+ (see mattupstate/flask-security#466)
+- Better IP tracking if using Flask 0.12.
+- Renamed deprecated Flask-WFT base form class.
+- Added tests for custom forms configured using app config.
+- Added validation and tests for next argument in logout endpoint. (see #499)
+- Bumped minimal required versions of several packages.
+- Extended test matric on Travis CI for minimal and released package versions.
+- Added of .editorconfig and forced tests for code style.
+- Fixed a security bug when validating a confirmation token, also checks
+ if the email that the token was created with matches the user's current email.
+- Replaced token loader with request loader.
+- Changed trackable behavior of `login_user` when IP can not be detected from a request from 'untrackable' to `None` value.
+- Use ProxyFix instead of inspecting X-Forwarded-For header.
+- Fix identical problem with app as with datastore.
+- Removed always-failing assertion.
+- Fixed failure of init_app to set self.datastore.
+- Changed to new style flask imports.
+- Added proper error code when returning JSON response.
+- Changed obsolette Required validator from WTForms to DataRequired. Bumped Flask-WTF to 0.13.
+- Fixed missing `SECURITY_SUBDOMAIN` in config docs.
+- Added cascade delete in PeeweeDatastore.
+- Added notes to docs about `SECURITY_USER_IDENTITY_ATTRIBUTES`.
+- Inspect value of `SECURITY_UNAUTHORIZED_VIEW`.
+- Send password reset instructions if an attempt has expired.
+- Added "Forgot password?" link to LoginForm description.
+- Upgraded passlib, and removed bcrypt version restriction.
+- Removed a duplicate line ('retype_password': 'Retype Password') in forms.py.
+- Various documentation improvement.
+
+Version 1.7.5
+-------------
+
+Released December 2nd 2015
+
+- Added `SECURITY_TOKEN_MAX_AGE` configuration setting
+- Fixed calls to `SQLAlchemyUserDatastore.get_user(None)` (this now returns `False` instead of raising a `TypeError`
+- Fixed URL generation adding extra slashes in some cases (see GitHub #343)
+- Fixed handling of trackable IP addresses when the `X-Forwarded-For` header contains multiple values
+- Include WWW-Authenticate headers in `@auth_required` authentication checks
+- Fixed error when `check_token` function is used with a json list
+- Added support for custom `AnonymousUser` classes
+- Restricted `forgot_password` endpoint to anonymous users
+- Allowed unauthorized callback to be overridden
+- Fixed issue where passwords cannot be reset if currently set to `None`
+- Ensured that password reset tokens are invalidated after use
+- Updated `is_authenticated` and `is_active` functions to support Flask-Login changes
+- Various documentation improvements
+
+
+Version 1.7.4
+-------------
+
+Released October 13th 2014
+
+- Fixed a bug related to changing existing passwords from plaintext to hashed
+- Fixed a bug in form validation that did not enforce case insensivitiy
+- Fixed a bug with validating redirects
+
+
+Version 1.7.3
+-------------
+
+Released June 10th 2014
+
+- Fixed a bug where redirection to `SECURITY_POST_LOGIN_VIEW` was not respected
+- Fixed string encoding in various places to be friendly to unicode
+- Now using `werkzeug.security.safe_str_cmp` to check tokens
+- Removed user information from JSON output on `/reset` responses
+- Added Python 3.4 support
+
+
+Version 1.7.2
+-------------
+
+Released May 6th 2014
+
+- Updated IP tracking to check for `X-Forwarded-For` header
+- Fixed a bug regarding the re-hashing of passwords with a new algorithm
+- Fixed a bug regarding the `password_changed` signal.
+
+
+Version 1.7.1
+-------------
+
+Released January 14th 2014
+
+- Fixed a bug where passwords would fail to verify when specifying a password hash algorithm
+
+
+Version 1.7.0
+-------------
+
+Released January 10th 2014
+
+- Python 3.3 support!
+- Dependency updates
+- Fixed a bug when `SECURITY_LOGIN_WITHOUT_CONFIRMATION = True` did not allow users to log in
+- Added `SECURITY_SEND_PASSWORD_RESET_NOTICE_EMAIL` configuraiton option to optionally send password reset notice emails
+- Add documentation for `@security.send_mail_task`
+- Move to `request.get_json` as `request.json` is now deprecated in Flask
+- Fixed a bug when using AJAX to change a user's password
+- Added documentation for select functions in the `flask_security.utils` module
+- Fixed a bug in `flask_security.forms.NextFormMixin`
+- Added `CHANGE_PASSWORD_TEMPLATE` configuration option to optionally specify a different change password template
+- Added the ability to specify addtional fields on the user model to be used for identifying the user via the `USER_IDENTITY_ATTRIBUTES` configuration option
+- An error is now shown if a user tries to change their password and the password is the same as before. The message can be customed with the `SECURITY_MSG_PASSWORD_IS_SAME` configuration option
+- Fixed a bug in `MongoEngineUserDatastore` where user model would not be updated when using the `add_role_to_user` method
+- Added `SECURITY_SEND_PASSWORD_CHANGE_EMAIL` configuration option to optionally disable password change email from being sent
+- Fixed a bug in the `find_or_create_role` method of the PeeWee datastore
+- Removed pypy tests
+- Fixed some tests
+- Include CHANGES and LICENSE in MANIFEST.in
+- A bit of documentation cleanup
+- A bit of code cleanup including removal of unnecessary utcnow call and simplification of get_max_age method
+
+
+Version 1.6.9
+-------------
+
+Released August 20th 2013
+
+- Fix bug in SQLAlchemy datastore's `get_user` function
+- Fix bug in PeeWee datastore's `remove_role_from_user` function
+- Fixed import error caused by new Flask-WTF release
+
+
+Version 1.6.8
+-------------
+
+Released August 1st 2013
+
+- Fixed bug with case sensitivity of email address during login
+- Code cleanup regarding token_callback
+- Ignore validation errors in find_user function for MongoEngineUserDatastore
+
+
+Version 1.6.7
+-------------
+
+Released July 11th 2013
+
+- Made password length form error message configurable
+- Fixed email confirmation bug that prevented logged in users from confirming their email
+
+
+Version 1.6.6
+-------------
+
+Released June 28th 2013
+
+- Fixed dependency versions
+
+
+Version 1.6.5
+-------------
+
+Released June 20th 2013
+
+- Fixed bug in `flask.ext.security.confirmable.generate_confirmation_link`
+
+
+Version 1.6.4
+-------------
+
+Released June 18th 2013
+
+- Added `SECURITY_DEFAULT_REMEMBER_ME` configuration value to unify behavior between endpoints
+- Fixed Flask-Login dependency problem
+- Added optional `next` parameter to registration endpoint, similar to that of login
+
+
+Version 1.6.3
+-------------
+
+Released May 8th 2013
+
+- Fixed bug in regards to imports with latest version of MongoEngine
+
+
+Version 1.6.2
+-------------
+
+Released April 4th 2013
+
+- Fixed bug with http basic auth
+
+
+Version 1.6.1
+-------------
+
+Released April 3rd 2013
+
+- Fixed bug with signals
+
+
+Version 1.6.0
+-------------
+
+Released March 13th 2013
+
+- Added Flask-Pewee support
+- Password hashing is now more flexible and can be changed to a different type at will
+- Flask-Login messages are configurable
+- AJAX requests must now send a CSRF token for security reasons
+- Form messages are now configurable
+- Forms can now be extended with more fields
+- Added change password endpoint
+- Added the user to the request context when successfully authenticated via http basic and token auth
+- The Flask-Security blueprint subdomain is now configurable
+- Redirects to other domains are now not allowed during requests that may redirect
+- Template paths can be configured
+- The welcome/register email can now optionally be sent to the user
+- Passwords can now contain non-latin characters
+- Fixed a bug when confirming an account but the account has been deleted
+
+
+Version 1.5.4
+-------------
+
+Released January 6th 2013
+
+- Fix bug in forms with `csrf_enabled` parameter not accounting attempts to login using JSON data
+
+
+Version 1.5.3
+-------------
+
+Released December 23rd 2012
+
+- Change dependency requirement
+
+Version 1.5.2
+-------------
+
+Released December 11th 2012
+
+- Fix a small bug in `flask_security.utils.login_user` method
+
+Version 1.5.1
+-------------
+
+Released November 26th 2012
+
+- Fixed bug with `next` form variable
+- Added better documentation regarding Flask-Mail configuration
+- Added ability to configure email subjects
+
+Version 1.5.0
+-------------
+
+Released October 11th 2012
+
+- Major release. Upgrading from previous versions will require a bit of work to
+ accomodate API changes. See documentation for a list of new features and for
+ help on how to upgrade.
+
+Version 1.2.3
+-------------
+
+Released June 12th 2012
+
+- Fixed a bug in the RoleMixin eq/ne functions
+
+Version 1.2.2
+-------------
+
+Released April 27th 2012
+
+- Fixed bug where `roles_required` and `roles_accepted` did not pass the next
+ argument to the login view
+
+Version 1.2.1
+-------------
+
+Released March 28th 2012
+
+- Added optional user model mixin parameter for datastores
+- Added CreateRoleCommand to available Flask-Script commands
+
+Version 1.2.0
+-------------
+
+Released March 12th 2012
+
+- Added configuration option `SECURITY_FLASH_MESSAGES` which can be set to a
+ boolean value to specify if Flask-Security should flash messages or not.
+
+Version 1.1.0
+-------------
+
+Initial release
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
new file mode 100644
index 0000000..3f39c0b
--- /dev/null
+++ b/CONTRIBUTING.rst
@@ -0,0 +1,149 @@
+.. _contributing:
+
+===========================
+Contributing
+===========================
+
+
+.. highlight:: console
+
+Contributions are welcome. If you would like add features or fix bugs,
+please review the information below.
+
+One source of history or ideas are the `bug reports`_.
+There you can find ideas for requested features, or the remains of rejected
+ideas.
+
+If you have a 'big idea' - please file an issue first so it can be discussed
+prior to you spending a lot of time developing. New features need to be generally
+useful - if your feature has limited applicability, consider making a small
+change that ENABLES your feature, rather than trying to get the entire feature
+into Flask-Security.
+
+.. _bug reports: https://github.com/Flask-Middleware/flask-security/issues
+
+
+Checklist
+---------
+
+ * All new code and bug fixes need unit tests
+ * If you change/add to the external API be sure to update docs/openapi.yaml
+ * Additions to configuration variables and/or messages must be documented
+ * Make sure any new public API methods have good docstrings, are picked up by
+ the api.rst document, and are exposed in __init__.py if appropriate.
+ * Add appropriate info to CHANGES.rst
+
+
+Getting the code
+----------------
+
+The code is hosted on a GitHub repo at
+https://github.com/Flask-Middleware/flask-security. To get a working environment, follow
+these steps:
+
+ #. (Optional, but recommended) Create a Python 3.6 (or greater) virtualenv to work in,
+ and activate it.
+
+ #. Fork the repo `Flask-Security <https://github.com/Flask-Middleware/flask-security>`_
+ (look for the "Fork" button).
+
+ #. Clone your fork locally::
+
+ $ git clone https://github.com/<your-username>/flask-security
+
+ #. Create a branch for local development::
+
+ $ git checkout -b name-of-your-bugfix-or-feature
+
+ #. Change directory to flask_security::
+
+ $ cd flask_security
+
+ #. Install the requirements::
+
+ $ pip install -e .[tests]
+
+ #. Develop the Feature/Bug Fix and edit
+
+ #. Write Tests for your code in::
+
+ tests/
+
+ #. When done, verify unit tests, syntax etc. all pass::
+
+ $ python setup.py test
+ $ python setup.py build_sphinx compile_catalog
+
+ #. When the tests are successful, commit your changes
+ and push your branch to GitHub::
+
+ $ git add .
+ $ git commit -m "Your detailed description of your changes."
+ $ git push origin name-of-your-bugfix-or-feature
+
+ #. Submit a pull request through the GitHub website.
+
+ #. Be sure that the CI tests and coverage checks pass.
+
+Updating the Swagger API document
+----------------------------------
+When making changes to the external API, you need to update the openapi.yaml
+formal specification. To do this - install the swagger editor locally::
+
+ $ npm -g install swagger-editor-dist http-server
+
+Then in a browser navigate to::
+
+ file:///usr/local/lib/node_modules/swagger-editor-dist/index.html#
+
+
+Edit - it is a WYSIWYG editor and will show you errors. Once you save (as yaml) you
+need to look at what it will render as::
+
+ $ python setup.py build_sphinx
+ $ http-server -p 8081
+
+Then in your browser navigate to::
+
+ http://localhost:8081/docs/_build/html/index.html
+ or
+ http://localhost:8081/docs/_build/html/_static/openapi_view.html
+
+
+Please note that changing ``openapi.yaml`` won't re-trigger a docs build - so you might
+have to manually delete ``docs/_build``.
+
+Updating Translations
+---------------------
+If you change any translatable strings (such as new messages, modified forms, etc.)
+you need to re-generate the translations::
+
+ $ python setup.py extract_messages
+ $ python setup.py update_catalog
+ $ python setup.py compile_catalog
+
+Testing
+-------
+Unit tests are critical since Flask-Security is a piece of middleware. They also
+help other contributors understand any subtleties in the code and edge conditions that
+need to be handled.
+
+Datastore
++++++++++
+By default the unit tests use an in-memory sqlite DB to test datastores (except for
+MongoDatastore which uses mongomock). While this is sufficient for most changes, changes
+to the datastore layer require testing against a real DB (the CI tests test against
+postgres). It is easy to run the unit tests against a real DB instance. First
+of course install the DB locally then::
+
+ # For postgres
+ python setup.py test --realdburl postgres://<user>@localhost/
+ # For mysql
+ python setup.py test --realdburl "mysql+pymysql://root:<password>@localhost/"
+
+Views
++++++
+Much of Flask-Security is concerned with form-based views. These can be difficult to test
+especially translations etc. In the tests directory is a stand-alone Flask application
+``view_scaffold.py`` that can be run and you can point your browser to it and walk
+through the various views.
diff --git a/Flask_Security_Too.egg-info/PKG-INFO b/Flask_Security_Too.egg-info/PKG-INFO
new file mode 100644
index 0000000..92f8613
--- /dev/null
+++ b/Flask_Security_Too.egg-info/PKG-INFO
@@ -0,0 +1,112 @@
+Metadata-Version: 2.1
+Name: Flask-Security-Too
+Version: 3.4.2
+Summary: Simple security for Flask apps.
+Home-page: https://github.com/Flask-Middleware/flask-security
+Author: Matt Wright & Chris Wagner
+Author-email: jwag.wagner+github@gmail.com
+License: MIT
+Project-URL: Documentation, https://flask-security-too.readthedocs.io
+Project-URL: Releases, https://pypi.org/project/Flask-Security-Too/
+Project-URL: Code, https://github.com/Flask-Middleware/flask-security
+Project-URL: Issue tracker, https://github.com/Flask-Middleware/flask-security/issues
+Description: Flask-Security
+ ===================
+
+ .. image:: https://travis-ci.org/Flask-Middleware/flask-security.svg?branch=master
+ :target: https://travis-ci.org/Flask-Middleware/flask-security
+
+ .. image:: https://coveralls.io/repos/github/Flask-Middleware/flask-security/badge.svg?branch=master
+ :target: https://coveralls.io/github/Flask-Middleware/flask-security?branch=master
+
+ .. image:: https://img.shields.io/github/tag/Flask-Middleware/flask-security.svg
+ :target: https://github.com/Flask-Middleware/flask-security/releases
+
+ .. image:: https://img.shields.io/pypi/dm/flask-security-too.svg
+ :target: https://pypi.python.org/pypi/flask-security-too
+ :alt: Downloads
+
+ .. image:: https://img.shields.io/github/license/Flask-Middleware/flask-security.svg
+ :target: https://github.com/Flask-Middleware/flask-security/blob/master/LICENSE
+ :alt: License
+
+ .. image:: https://readthedocs.org/projects/flask-security-too/badge/?version=latest
+ :target: https://flask-security-too.readthedocs.io/en/latest/?badge=latest
+ :alt: Documentation Status
+
+ .. image:: https://img.shields.io/badge/code%20style-black-000000.svg
+ :target: https://github.com/python/black
+
+ Quickly add security features to your Flask application.
+
+ Notes on this repo
+ ------------------
+ This is a independently maintained version of Flask-Security based on the 3.0.0
+ version of the `Original <https://github.com/mattupstate/flask-security>`_
+
+ Goals
+ +++++
+ * Regain momentum for this critical piece of the Flask eco-system. To that end the
+ the plan is to put out small, frequent releases starting with pulling the simplest
+ and most obvious changes that have already been vetted in the upstream version, as
+ well as other pull requests. This was completed with the June 29 2019 3.2.0 release.
+ * Continue work to get Flask-Security to be usable from Single Page Applications,
+ such as those built with Vue and Angular, that have no html forms. This is true as of the 3.3.0
+ release.
+ * Use `OWASP <https://github.com/OWASP/ASVS>`_ to guide best practice and default configurations.
+ * Migrate to more modern paradigms such as using oauth2 and JWT for token acquisition.
+ * Be more opinionated and 'batteries' included by reducing reliance on abandoned projects and
+ bundling in support for common use cases.
+ * Follow the `Pallets <https://github.com/pallets>`_ lead on supported versions, documentation
+ standards and any other guidelines for extensions that they come up with.
+ * Any other great ideas.
+
+ Contributing
+ ++++++++++++
+ Issues and pull requests are welcome. Other maintainers are also welcome. Unlike
+ the original Flask-Security - issue pull requests against the *master* branch.
+ Please consult these `contributing`_ guidelines.
+
+ .. _contributing: https://github.com/Flask-Middleware/flask-security/blob/master/CONTRIBUTING.rst
+
+ Installing
+ ----------
+ Install and update using `pip <https://pip.pypa.io/en/stable/quickstart/>`_:
+
+ ::
+
+ pip install -U Flask-Security-Too
+
+
+ Resources
+ ---------
+
+ - `Documentation <https://flask-security-too.readthedocs.io/>`_
+ - `Releases <https://pypi.org/project/Flask-Security-Too/>`_
+ - `Issue Tracker <https://github.com/Flask-Middleware/flask-security/issues>`_
+ - `Code <https://github.com/Flask-Middleware/flask-security/>`_
+
+Keywords: flask security
+Platform: any
+Classifier: Environment :: Web Environment
+Classifier: Framework :: Flask
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
+Classifier: Development Status :: 4 - Beta
+Requires-Python: >=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*
+Provides-Extra: docs
+Provides-Extra: tests
+Provides-Extra: all
diff --git a/Flask_Security_Too.egg-info/SOURCES.txt b/Flask_Security_Too.egg-info/SOURCES.txt
new file mode 100644
index 0000000..0be8f06
--- /dev/null
+++ b/Flask_Security_Too.egg-info/SOURCES.txt
@@ -0,0 +1,168 @@
+.editorconfig
+AUTHORS
+CHANGES.rst
+CONTRIBUTING.rst
+LICENSE
+MANIFEST.in
+README.rst
+babel.ini
+pytest.ini
+requirements.txt
+setup.cfg
+setup.py
+tox.ini
+Flask_Security_Too.egg-info/PKG-INFO
+Flask_Security_Too.egg-info/SOURCES.txt
+Flask_Security_Too.egg-info/dependency_links.txt
+Flask_Security_Too.egg-info/not-zip-safe
+Flask_Security_Too.egg-info/requires.txt
+Flask_Security_Too.egg-info/top_level.txt
+docs/Makefile
+docs/api.rst
+docs/authors.rst
+docs/changelog.rst
+docs/conf.py
+docs/configuration.rst
+docs/contributing.rst
+docs/customizing.rst
+docs/features.rst
+docs/index.rst
+docs/models.rst
+docs/patterns.rst
+docs/quickstart.rst
+docs/requirements.txt
+docs/spa.rst
+docs/two_factor_configurations.rst
+docs/_static/logo-owl-105.png
+docs/_static/logo-owl-68.png
+docs/_static/logo-owl-full-240.png
+docs/_static/logo-owl-full.png
+docs/_static/openapi_view.html
+flask_security/__init__.py
+flask_security/async_compat.py
+flask_security/babel.py
+flask_security/cache.py
+flask_security/changeable.py
+flask_security/cli.py
+flask_security/confirmable.py
+flask_security/core.py
+flask_security/datastore.py
+flask_security/decorators.py
+flask_security/forms.py
+flask_security/passwordless.py
+flask_security/phone_util.py
+flask_security/quart_compat.py
+flask_security/recoverable.py
+flask_security/registerable.py
+flask_security/signals.py
+flask_security/totp.py
+flask_security/twofactor.py
+flask_security/unified_signin.py
+flask_security/utils.py
+flask_security/views.py
+flask_security/models/__init__.py
+flask_security/models/fsqla.py
+flask_security/models/fsqla_v2.py
+flask_security/templates/security/_macros.html
+flask_security/templates/security/_menu.html
+flask_security/templates/security/_messages.html
+flask_security/templates/security/base.html
+flask_security/templates/security/change_password.html
+flask_security/templates/security/forgot_password.html
+flask_security/templates/security/login_user.html
+flask_security/templates/security/register_user.html
+flask_security/templates/security/reset_password.html
+flask_security/templates/security/send_confirmation.html
+flask_security/templates/security/send_login.html
+flask_security/templates/security/two_factor_setup.html
+flask_security/templates/security/two_factor_verify_code.html
+flask_security/templates/security/two_factor_verify_password.html
+flask_security/templates/security/us_setup.html
+flask_security/templates/security/us_signin.html
+flask_security/templates/security/us_verify.html
+flask_security/templates/security/verify.html
+flask_security/templates/security/email/change_notice.html
+flask_security/templates/security/email/change_notice.txt
+flask_security/templates/security/email/confirmation_instructions.html
+flask_security/templates/security/email/confirmation_instructions.txt
+flask_security/templates/security/email/login_instructions.html
+flask_security/templates/security/email/login_instructions.txt
+flask_security/templates/security/email/reset_instructions.html
+flask_security/templates/security/email/reset_instructions.txt
+flask_security/templates/security/email/reset_notice.html
+flask_security/templates/security/email/reset_notice.txt
+flask_security/templates/security/email/two_factor_instructions.html
+flask_security/templates/security/email/two_factor_instructions.txt
+flask_security/templates/security/email/two_factor_rescue.html
+flask_security/templates/security/email/two_factor_rescue.txt
+flask_security/templates/security/email/us_instructions.html
+flask_security/templates/security/email/us_instructions.txt
+flask_security/templates/security/email/welcome.html
+flask_security/templates/security/email/welcome.txt
+flask_security/translations/flask_security.pot
+flask_security/translations/ca_ES/LC_MESSAGES/flask_security.mo
+flask_security/translations/ca_ES/LC_MESSAGES/flask_security.po
+flask_security/translations/da_DK/LC_MESSAGES/flask_security.mo
+flask_security/translations/da_DK/LC_MESSAGES/flask_security.po
+flask_security/translations/de_DE/LC_MESSAGES/flask_security.mo
+flask_security/translations/de_DE/LC_MESSAGES/flask_security.po
+flask_security/translations/es_ES/LC_MESSAGES/flask_security.mo
+flask_security/translations/es_ES/LC_MESSAGES/flask_security.po
+flask_security/translations/fr_FR/LC_MESSAGES/flask_security.mo
+flask_security/translations/fr_FR/LC_MESSAGES/flask_security.po
+flask_security/translations/ja_JP/LC_MESSAGES/flask_security.mo
+flask_security/translations/ja_JP/LC_MESSAGES/flask_security.po
+flask_security/translations/nl_NL/LC_MESSAGES/flask_security.mo
+flask_security/translations/nl_NL/LC_MESSAGES/flask_security.po
+flask_security/translations/pt_BR/LC_MESSAGES/flask_security.mo
+flask_security/translations/pt_BR/LC_MESSAGES/flask_security.po
+flask_security/translations/pt_PT/LC_MESSAGES/flask_security.mo
+flask_security/translations/pt_PT/LC_MESSAGES/flask_security.po
+flask_security/translations/ru_RU/LC_MESSAGES/flask_security.mo
+flask_security/translations/ru_RU/LC_MESSAGES/flask_security.po
+flask_security/translations/tr_TR/LC_MESSAGES/flask_security.mo
+flask_security/translations/tr_TR/LC_MESSAGES/flask_security.po
+flask_security/translations/zh_Hans_CN/LC_MESSAGES/flask_security.mo
+flask_security/translations/zh_Hans_CN/LC_MESSAGES/flask_security.po
+tests/conftest.py
+tests/test_cache.py
+tests/test_changeable.py
+tests/test_cli.py
+tests/test_common.py
+tests/test_configuration.py
+tests/test_confirmable.py
+tests/test_context_processors.py
+tests/test_csrf.py
+tests/test_datastore.py
+tests/test_entities.py
+tests/test_hashing.py
+tests/test_misc.py
+tests/test_passwordless.py
+tests/test_recoverable.py
+tests/test_registerable.py
+tests/test_response.py
+tests/test_trackable.py
+tests/test_two_factor.py
+tests/test_unified_signin.py
+tests/utils.py
+tests/view_scaffold.py
+tests/templates/_messages.html
+tests/templates/_nav.html
+tests/templates/index.html
+tests/templates/register.html
+tests/templates/unauthorized.html
+tests/templates/custom_security/change_password.html
+tests/templates/custom_security/forgot_password.html
+tests/templates/custom_security/login_user.html
+tests/templates/custom_security/register_user.html
+tests/templates/custom_security/reset_password.html
+tests/templates/custom_security/send_confirmation.html
+tests/templates/custom_security/send_login.html
+tests/templates/custom_security/tf_setup.html
+tests/templates/custom_security/tf_verify.html
+tests/templates/custom_security/tfc.html
+tests/templates/custom_security/us_setup.html
+tests/templates/custom_security/us_signin.html
+tests/templates/custom_security/us_verify.html
+tests/templates/custom_security/verify.html
+tests/templates/security/email/reset_instructions.html \ No newline at end of file
diff --git a/Flask_Security_Too.egg-info/dependency_links.txt b/Flask_Security_Too.egg-info/dependency_links.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/Flask_Security_Too.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/Flask_Security_Too.egg-info/not-zip-safe b/Flask_Security_Too.egg-info/not-zip-safe
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/Flask_Security_Too.egg-info/not-zip-safe
@@ -0,0 +1 @@
+
diff --git a/Flask_Security_Too.egg-info/requires.txt b/Flask_Security_Too.egg-info/requires.txt
new file mode 100644
index 0000000..19c7130
--- /dev/null
+++ b/Flask_Security_Too.egg-info/requires.txt
@@ -0,0 +1,114 @@
+Flask>=1.0.2
+Flask-Login>=0.4.1
+Flask-Mail>=0.9.1
+Flask-Principal>=0.4.0
+Flask-WTF>=0.14.2
+Flask-BabelEx>=0.9.3
+email-validator>=1.0.5
+itsdangerous>=1.1.0
+passlib>=1.7.1
+
+[all]
+Pallets-Sphinx-Themes>=1.2.0
+Sphinx>=1.8.5
+sphinx-issues>=1.2.0
+Flask-Mongoengine>=0.9.5
+peewee>=3.11.2
+Flask-SQLAlchemy>=2.3
+argon2_cffi>=19.1.0
+bcrypt>=3.1.5
+cachetools>=3.1.0
+check-manifest>=0.25
+coverage>=4.5.4
+cryptography>=2.3.1
+isort>=4.2.2
+mock>=1.3.0
+mongoengine>=0.15.3
+mongomock>=3.14.0
+msgcheck>=2.9
+pony>=0.7.11
+phonenumberslite>=8.11.1
+psycopg2>=2.8.4
+pydocstyle>=1.0.0
+pymysql>=0.9.3
+pyqrcode>=1.2
+pytest-black>=0.3.8
+pytest-cache>=1.0
+pytest-cov>=2.5.1
+pytest-flake8>=1.0.4
+pytest-mongo>=1.2.1
+pytest>=3.5.1
+sqlalchemy>=1.2.6
+sqlalchemy-utils>=0.33.0
+werkzeug>=0.15.5
+zxcvbn~=4.4.28
+Pallets-Sphinx-Themes>=1.2.0
+Sphinx>=1.8.5
+sphinx-issues>=1.2.0
+Flask-Mongoengine>=0.9.5
+peewee>=3.11.2
+Flask-SQLAlchemy>=2.3
+argon2_cffi>=19.1.0
+bcrypt>=3.1.5
+cachetools>=3.1.0
+check-manifest>=0.25
+coverage>=4.5.4
+cryptography>=2.3.1
+isort>=4.2.2
+mock>=1.3.0
+mongoengine>=0.15.3
+mongomock>=3.14.0
+msgcheck>=2.9
+pony>=0.7.11
+phonenumberslite>=8.11.1
+psycopg2>=2.8.4
+pydocstyle>=1.0.0
+pymysql>=0.9.3
+pyqrcode>=1.2
+pytest-black>=0.3.8
+pytest-cache>=1.0
+pytest-cov>=2.5.1
+pytest-flake8>=1.0.4
+pytest-mongo>=1.2.1
+pytest>=3.5.1
+sqlalchemy>=1.2.6
+sqlalchemy-utils>=0.33.0
+werkzeug>=0.15.5
+zxcvbn~=4.4.28
+
+[docs]
+Pallets-Sphinx-Themes>=1.2.0
+Sphinx>=1.8.5
+sphinx-issues>=1.2.0
+
+[tests]
+Flask-Mongoengine>=0.9.5
+peewee>=3.11.2
+Flask-SQLAlchemy>=2.3
+argon2_cffi>=19.1.0
+bcrypt>=3.1.5
+cachetools>=3.1.0
+check-manifest>=0.25
+coverage>=4.5.4
+cryptography>=2.3.1
+isort>=4.2.2
+mock>=1.3.0
+mongoengine>=0.15.3
+mongomock>=3.14.0
+msgcheck>=2.9
+pony>=0.7.11
+phonenumberslite>=8.11.1
+psycopg2>=2.8.4
+pydocstyle>=1.0.0
+pymysql>=0.9.3
+pyqrcode>=1.2
+pytest-black>=0.3.8
+pytest-cache>=1.0
+pytest-cov>=2.5.1
+pytest-flake8>=1.0.4
+pytest-mongo>=1.2.1
+pytest>=3.5.1
+sqlalchemy>=1.2.6
+sqlalchemy-utils>=0.33.0
+werkzeug>=0.15.5
+zxcvbn~=4.4.28
diff --git a/Flask_Security_Too.egg-info/top_level.txt b/Flask_Security_Too.egg-info/top_level.txt
new file mode 100644
index 0000000..dc4e699
--- /dev/null
+++ b/Flask_Security_Too.egg-info/top_level.txt
@@ -0,0 +1 @@
+flask_security
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..040c879
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (C) 2012-2019 by Matthew Wright
+Copyright (C) 2019-2019 by Chris Wagner
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..1666cc6
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,26 @@
+include .editorconfig
+include AUTHORS
+include CHANGES.rst
+include CONTRIBUTING.rst
+include LICENSE
+include README.rst
+include babel.ini
+include pytest.ini
+include tox.ini
+include requirements.txt
+recursive-include docs *.html
+recursive-include docs *.inc
+recursive-include docs *.png
+recursive-include docs *.py
+recursive-include docs *.rst
+recursive-include docs *.txt
+recursive-include docs Makefile
+recursive-include flask_security/templates *.*
+recursive-include flask_security/translations *.po *.pot *.mo
+recursive-include tests *.py
+recursive-include tests *.html
+exclude .coverage tests/.coverage
+recursive-exclude docs/_build *
+global-exclude *.pyc .DS_Store
+prune scripts
+prune examples
diff --git a/PKG-INFO b/PKG-INFO
new file mode 100644
index 0000000..92f8613
--- /dev/null
+++ b/PKG-INFO
@@ -0,0 +1,112 @@
+Metadata-Version: 2.1
+Name: Flask-Security-Too
+Version: 3.4.2
+Summary: Simple security for Flask apps.
+Home-page: https://github.com/Flask-Middleware/flask-security
+Author: Matt Wright & Chris Wagner
+Author-email: jwag.wagner+github@gmail.com
+License: MIT
+Project-URL: Documentation, https://flask-security-too.readthedocs.io
+Project-URL: Releases, https://pypi.org/project/Flask-Security-Too/
+Project-URL: Code, https://github.com/Flask-Middleware/flask-security
+Project-URL: Issue tracker, https://github.com/Flask-Middleware/flask-security/issues
+Description: Flask-Security
+ ===================
+
+ .. image:: https://travis-ci.org/Flask-Middleware/flask-security.svg?branch=master
+ :target: https://travis-ci.org/Flask-Middleware/flask-security
+
+ .. image:: https://coveralls.io/repos/github/Flask-Middleware/flask-security/badge.svg?branch=master
+ :target: https://coveralls.io/github/Flask-Middleware/flask-security?branch=master
+
+ .. image:: https://img.shields.io/github/tag/Flask-Middleware/flask-security.svg
+ :target: https://github.com/Flask-Middleware/flask-security/releases
+
+ .. image:: https://img.shields.io/pypi/dm/flask-security-too.svg
+ :target: https://pypi.python.org/pypi/flask-security-too
+ :alt: Downloads
+
+ .. image:: https://img.shields.io/github/license/Flask-Middleware/flask-security.svg
+ :target: https://github.com/Flask-Middleware/flask-security/blob/master/LICENSE
+ :alt: License
+
+ .. image:: https://readthedocs.org/projects/flask-security-too/badge/?version=latest
+ :target: https://flask-security-too.readthedocs.io/en/latest/?badge=latest
+ :alt: Documentation Status
+
+ .. image:: https://img.shields.io/badge/code%20style-black-000000.svg
+ :target: https://github.com/python/black
+
+ Quickly add security features to your Flask application.
+
+ Notes on this repo
+ ------------------
+ This is a independently maintained version of Flask-Security based on the 3.0.0
+ version of the `Original <https://github.com/mattupstate/flask-security>`_
+
+ Goals
+ +++++
+ * Regain momentum for this critical piece of the Flask eco-system. To that end the
+ the plan is to put out small, frequent releases starting with pulling the simplest
+ and most obvious changes that have already been vetted in the upstream version, as
+ well as other pull requests. This was completed with the June 29 2019 3.2.0 release.
+ * Continue work to get Flask-Security to be usable from Single Page Applications,
+ such as those built with Vue and Angular, that have no html forms. This is true as of the 3.3.0
+ release.
+ * Use `OWASP <https://github.com/OWASP/ASVS>`_ to guide best practice and default configurations.
+ * Migrate to more modern paradigms such as using oauth2 and JWT for token acquisition.
+ * Be more opinionated and 'batteries' included by reducing reliance on abandoned projects and
+ bundling in support for common use cases.
+ * Follow the `Pallets <https://github.com/pallets>`_ lead on supported versions, documentation
+ standards and any other guidelines for extensions that they come up with.
+ * Any other great ideas.
+
+ Contributing
+ ++++++++++++
+ Issues and pull requests are welcome. Other maintainers are also welcome. Unlike
+ the original Flask-Security - issue pull requests against the *master* branch.
+ Please consult these `contributing`_ guidelines.
+
+ .. _contributing: https://github.com/Flask-Middleware/flask-security/blob/master/CONTRIBUTING.rst
+
+ Installing
+ ----------
+ Install and update using `pip <https://pip.pypa.io/en/stable/quickstart/>`_:
+
+ ::
+
+ pip install -U Flask-Security-Too
+
+
+ Resources
+ ---------
+
+ - `Documentation <https://flask-security-too.readthedocs.io/>`_
+ - `Releases <https://pypi.org/project/Flask-Security-Too/>`_
+ - `Issue Tracker <https://github.com/Flask-Middleware/flask-security/issues>`_
+ - `Code <https://github.com/Flask-Middleware/flask-security/>`_
+
+Keywords: flask security
+Platform: any
+Classifier: Environment :: Web Environment
+Classifier: Framework :: Flask
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
+Classifier: Development Status :: 4 - Beta
+Requires-Python: >=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*
+Provides-Extra: docs
+Provides-Extra: tests
+Provides-Extra: all
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..d097ba5
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,75 @@
+Flask-Security
+===================
+
+.. image:: https://travis-ci.org/Flask-Middleware/flask-security.svg?branch=master
+ :target: https://travis-ci.org/Flask-Middleware/flask-security
+
+.. image:: https://coveralls.io/repos/github/Flask-Middleware/flask-security/badge.svg?branch=master
+ :target: https://coveralls.io/github/Flask-Middleware/flask-security?branch=master
+
+.. image:: https://img.shields.io/github/tag/Flask-Middleware/flask-security.svg
+ :target: https://github.com/Flask-Middleware/flask-security/releases
+
+.. image:: https://img.shields.io/pypi/dm/flask-security-too.svg
+ :target: https://pypi.python.org/pypi/flask-security-too
+ :alt: Downloads
+
+.. image:: https://img.shields.io/github/license/Flask-Middleware/flask-security.svg
+ :target: https://github.com/Flask-Middleware/flask-security/blob/master/LICENSE
+ :alt: License
+
+.. image:: https://readthedocs.org/projects/flask-security-too/badge/?version=latest
+ :target: https://flask-security-too.readthedocs.io/en/latest/?badge=latest
+ :alt: Documentation Status
+
+.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
+ :target: https://github.com/python/black
+
+Quickly add security features to your Flask application.
+
+Notes on this repo
+------------------
+This is a independently maintained version of Flask-Security based on the 3.0.0
+version of the `Original <https://github.com/mattupstate/flask-security>`_
+
+Goals
++++++
+* Regain momentum for this critical piece of the Flask eco-system. To that end the
+ the plan is to put out small, frequent releases starting with pulling the simplest
+ and most obvious changes that have already been vetted in the upstream version, as
+ well as other pull requests. This was completed with the June 29 2019 3.2.0 release.
+* Continue work to get Flask-Security to be usable from Single Page Applications,
+ such as those built with Vue and Angular, that have no html forms. This is true as of the 3.3.0
+ release.
+* Use `OWASP <https://github.com/OWASP/ASVS>`_ to guide best practice and default configurations.
+* Migrate to more modern paradigms such as using oauth2 and JWT for token acquisition.
+* Be more opinionated and 'batteries' included by reducing reliance on abandoned projects and
+ bundling in support for common use cases.
+* Follow the `Pallets <https://github.com/pallets>`_ lead on supported versions, documentation
+ standards and any other guidelines for extensions that they come up with.
+* Any other great ideas.
+
+Contributing
+++++++++++++
+Issues and pull requests are welcome. Other maintainers are also welcome. Unlike
+the original Flask-Security - issue pull requests against the *master* branch.
+Please consult these `contributing`_ guidelines.
+
+.. _contributing: https://github.com/Flask-Middleware/flask-security/blob/master/CONTRIBUTING.rst
+
+Installing
+----------
+Install and update using `pip <https://pip.pypa.io/en/stable/quickstart/>`_:
+
+::
+
+ pip install -U Flask-Security-Too
+
+
+Resources
+---------
+
+- `Documentation <https://flask-security-too.readthedocs.io/>`_
+- `Releases <https://pypi.org/project/Flask-Security-Too/>`_
+- `Issue Tracker <https://github.com/Flask-Middleware/flask-security/issues>`_
+- `Code <https://github.com/Flask-Middleware/flask-security/>`_
diff --git a/babel.ini b/babel.ini
new file mode 100644
index 0000000..4f00e4f
--- /dev/null
+++ b/babel.ini
@@ -0,0 +1,10 @@
+# Extraction from Python source files
+
+[python: **.py]
+encoding = utf-8
+
+# Extraction from Jinja2 templates
+
+[jinja2: **/templates/**.html]
+encoding = utf-8
+extensions = jinja2.ext.autoescape, jinja2.ext.with_
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 0000000..5b6f3d8
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,153 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+PAPER =
+BUILDDIR = _build
+
+# Internal variables.
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
+
+help:
+ @echo "Please use \`make <target>' where <target> is one of"
+ @echo " html to make standalone HTML files"
+ @echo " dirhtml to make HTML files named index.html in directories"
+ @echo " singlehtml to make a single large HTML file"
+ @echo " pickle to make pickle files"
+ @echo " json to make JSON files"
+ @echo " htmlhelp to make HTML files and a HTML help project"
+ @echo " qthelp to make HTML files and a qthelp project"
+ @echo " devhelp to make HTML files and a Devhelp project"
+ @echo " epub to make an epub"
+ @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+ @echo " latexpdf to make LaTeX files and run them through pdflatex"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " texinfo to make Texinfo files"
+ @echo " info to make Texinfo files and run them through makeinfo"
+ @echo " gettext to make PO message catalogs"
+ @echo " changes to make an overview of all changed/added/deprecated items"
+ @echo " linkcheck to check all external links for integrity"
+ @echo " doctest to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+ -rm -rf $(BUILDDIR)/*
+
+html:
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+ $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml:
+ $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+ @echo
+ @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+pickle:
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+ @echo
+ @echo "Build finished; now you can process the pickle files."
+
+json:
+ $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+ @echo
+ @echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+ $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+ @echo
+ @echo "Build finished; now you can run HTML Help Workshop with the" \
+ ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+ $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+ @echo
+ @echo "Build finished; now you can run "qcollectiongenerator" with the" \
+ ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+ @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Flask-Security.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Flask-Security.qhc"
+
+devhelp:
+ $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+ @echo
+ @echo "Build finished."
+ @echo "To view the help file:"
+ @echo "# mkdir -p $$HOME/.local/share/devhelp/Flask-Security"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Flask-Security"
+ @echo "# devhelp"
+
+epub:
+ $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+ @echo
+ @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+latex:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo
+ @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+ @echo "Run \`make' in that directory to run these through (pdf)latex" \
+ "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through pdflatex..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+ $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+ @echo
+ @echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+ $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+ @echo
+ @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+texinfo:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo
+ @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+ @echo "Run \`make' in that directory to run these through makeinfo" \
+ "(use \`make info' here to do that automatically)."
+
+info:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo "Running Texinfo files through makeinfo..."
+ make -C $(BUILDDIR)/texinfo info
+ @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+gettext:
+ $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+ @echo
+ @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+changes:
+ $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+ @echo
+ @echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+ $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+ @echo
+ @echo "Link check complete; look for any errors in the above output " \
+ "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+ $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+ @echo "Testing of doctests in the sources finished, look at the " \
+ "results in $(BUILDDIR)/doctest/output.txt."
diff --git a/docs/_static/logo-owl-105.png b/docs/_static/logo-owl-105.png
new file mode 100644
index 0000000..7693e09
--- /dev/null
+++ b/docs/_static/logo-owl-105.png
Binary files differ
diff --git a/docs/_static/logo-owl-68.png b/docs/_static/logo-owl-68.png
new file mode 100644
index 0000000..b7aba0c
--- /dev/null
+++ b/docs/_static/logo-owl-68.png
Binary files differ
diff --git a/docs/_static/logo-owl-full-240.png b/docs/_static/logo-owl-full-240.png
new file mode 100644
index 0000000..ddebf71
--- /dev/null
+++ b/docs/_static/logo-owl-full-240.png
Binary files differ
diff --git a/docs/_static/logo-owl-full.png b/docs/_static/logo-owl-full.png
new file mode 100644
index 0000000..621d21b
--- /dev/null
+++ b/docs/_static/logo-owl-full.png
Binary files differ
diff --git a/docs/_static/openapi_view.html b/docs/_static/openapi_view.html
new file mode 100644
index 0000000..5c4c2a3
--- /dev/null
+++ b/docs/_static/openapi_view.html
@@ -0,0 +1,20 @@
+ <!doctype html> <!-- Important: must specify -->
+ <html>
+ <head>
+ <meta charset="utf-8"> <!-- Important: rapi-doc uses utf8 charecters -->
+ <script src="https://unpkg.com/rapidoc/dist/rapidoc-min.js"></script>
+ </head>
+ <body>
+ <rapi-doc
+ spec-url="openapi.yaml"
+ allow-try="false"
+ allow-spec-url-load="false"
+ allow-spec-file-load="false"
+ heading-text="Flask Security External API">
+ <img
+ slot="logo"
+ src="logo-owl-68.png"
+ />
+ </rapi-doc>
+ </body>
+ </html>
diff --git a/docs/api.rst b/docs/api.rst
new file mode 100644
index 0000000..2853b2d
--- /dev/null
+++ b/docs/api.rst
@@ -0,0 +1,252 @@
+API
+===
+
+The external (json/form) API is described `here`_
+
+.. _here: _static/openapi_view.html
+
+
+Core
+----
+.. autoclass:: flask_security.Security
+ :members:
+
+.. data:: flask_security.current_user
+
+ A proxy for the current user.
+
+.. function:: flask_security.Security.unauthorized_handler
+
+ If an endpoint fails authentication or authorization from one of the decorators
+ described below
+ (except ``login_required``), a method annotated with this decorator will be called.
+ For ``login_required`` (which is implemented in Flask-Login) use
+ **flask_security.login_manager.unauthorized_handler**
+
+ .. deprecated:: 3.3.0
+
+Protecting Views
+----------------
+.. autofunction:: flask_security.anonymous_user_required
+
+.. autofunction:: flask_security.http_auth_required
+
+.. autofunction:: flask_security.auth_token_required
+
+.. autofunction:: flask_security.auth_required
+
+.. autofunction:: flask_security.login_required
+
+.. autofunction:: flask_security.roles_required
+
+.. autofunction:: flask_security.roles_accepted
+
+.. autofunction:: flask_security.permissions_required
+
+.. autofunction:: flask_security.permissions_accepted
+
+.. autofunction:: flask_security.unauth_csrf
+
+.. autofunction:: flask_security.handle_csrf
+
+User Object Helpers
+-------------------
+.. autoclass:: flask_security.UserMixin
+ :members:
+
+.. autoclass:: flask_security.RoleMixin
+ :members:
+
+.. autoclass:: flask_security.AnonymousUser
+ :members:
+
+
+Datastores
+----------
+.. autoclass:: flask_security.UserDatastore
+ :members:
+
+.. autoclass:: flask_security.SQLAlchemyUserDatastore
+ :members:
+ :inherited-members:
+
+.. autoclass:: flask_security.SQLAlchemySessionUserDatastore
+ :members:
+ :inherited-members:
+
+.. autoclass:: flask_security.MongoEngineUserDatastore
+ :members:
+ :inherited-members:
+
+.. autoclass:: flask_security.PeeweeUserDatastore
+ :members:
+ :inherited-members:
+
+.. autoclass:: flask_security.PonyUserDatastore
+ :members:
+ :inherited-members:
+
+Utils
+-----
+.. autofunction:: flask_security.login_user
+
+.. autofunction:: flask_security.logout_user
+
+.. autofunction:: flask_security.check_and_update_authn_fresh
+
+.. autofunction:: flask_security.get_hmac
+
+.. autofunction:: flask_security.verify_password
+
+.. autofunction:: flask_security.verify_and_update_password
+
+.. autofunction:: flask_security.hash_password
+
+.. autofunction:: flask_security.uia_phone_mapper
+
+.. autofunction:: flask_security.uia_email_mapper
+
+.. autofunction:: flask_security.url_for_security
+
+.. autofunction:: flask_security.send_mail
+
+.. autofunction:: flask_security.get_token_status
+
+.. autofunction:: flask_security.check_and_get_token_status
+
+.. autofunction:: flask_security.get_url
+
+.. autofunction:: flask_security.password_length_validator
+
+.. autofunction:: flask_security.password_complexity_validator
+
+.. autofunction:: flask_security.password_breached_validator
+
+.. autofunction:: flask_security.pwned
+
+.. autofunction:: flask_security.transform_url
+
+.. autofunction:: flask_security.us_send_security_token
+
+.. autofunction:: flask_security.tf_send_security_token
+
+.. autoclass:: flask_security.FsJsonEncoder
+
+.. autoclass:: flask_security.Totp
+ :members: get_last_counter, set_last_counter
+
+.. autoclass:: flask_security.PhoneUtil
+ :members:
+
+.. autoclass:: flask_security.SmsSenderBaseClass
+ :members: send_sms
+
+.. autoclass:: flask_security.SmsSenderFactory
+ :members: createSender
+
+Signals
+-------
+See the `Flask documentation on signals`_ for information on how to use these
+signals in your code.
+
+.. tip::
+
+ Remember to add ``**extra_args`` to your signature so that if we add
+ additional parameters in the future your code doesn't break.
+
+See the documentation for the signals provided by the Flask-Login and
+Flask-Principal extensions. In addition to those signals, Flask-Security
+sends the following signals.
+
+.. data:: user_authenticated
+
+ Sent when a user successfully authenticates. In addition to the app (which is the
+ sender), it is passed `user`, and `authn_via` arguments. The `authn_via` argument
+ specifies how the user authenticated - it will be a list with possible values
+ of ``password``, ``sms``, ``authenticator``, ``email``, ``confirm``, ``reset``,
+ ``register``.
+
+ .. versionadded:: 3.4.0
+
+.. data:: user_registered
+
+ Sent when a user registers on the site. In addition to the app (which is the
+ sender), it is passed `user`, `confirm_token` and `form_data` arguments.
+ `form_data` is a dictionary representation of registration form's content
+ received with registration request.
+
+.. data:: user_confirmed
+
+ Sent when a user is confirmed. In addition to the app (which is the
+ sender), it is passed a `user` argument.
+
+.. data:: confirm_instructions_sent
+
+ Sent when a user requests confirmation instructions. In addition to the app
+ (which is the sender), it is passed a `user` argument.
+
+.. data:: login_instructions_sent
+
+ Sent when passwordless login is used and user logs in. In addition to the app
+ (which is the sender), it is passed `user` and `login_token` arguments.
+
+.. data:: password_reset
+
+ Sent when a user completes a password reset. In addition to the app (which is
+ the sender), it is passed a `user` argument.
+
+.. data:: password_changed
+
+ Sent when a user completes a password change. In addition to the app (which is
+ the sender), it is passed a `user` argument.
+
+.. data:: reset_password_instructions_sent
+
+ Sent when a user requests a password reset. In addition to the app (which is
+ the sender), it is passed `user` and `token` arguments.
+
+.. data:: tf_code_confirmed
+
+ Sent when a user performs two-factor authentication login on the site. In
+ addition to the app (which is the sender), it is passed `user`
+ and `method` arguments.
+
+ .. versionadded:: 3.3.0
+
+.. data:: tf_profile_changed
+
+ Sent when two-factor is used and user logs in. In addition to the app
+ (which is the sender), it is passed `user` and `method` arguments.
+
+ .. versionadded:: 3.3.0
+
+.. data:: tf_disabled
+
+ Sent when two-factor is disabled. In addition to the app
+ (which is the sender), it is passed `user` argument.
+
+ .. versionadded:: 3.3.0
+
+.. data:: tf_security_token_sent
+
+ Sent when a two factor security/access code is sent. In addition to the app
+ (which is the sender), it is passed `user`, `method`, and `token` arguments.
+
+ .. versionadded:: 3.3.0
+
+.. data:: us_security_token_sent
+
+ Sent when a unified sign in access code is sent. In addition to the app
+ (which is the sender), it is passed `user`, `method`, `token`,
+ `phone_number`, and `send_magic_link` arguments.
+
+ .. versionadded:: 3.4.0
+
+.. data:: us_profile_changed
+
+ Sent when user completes changing their unified sign in profile. In addition to the app
+ (which is the sender), it is passed `user` and `method` arguments.
+
+ .. versionadded:: 3.4.0
+
+.. _Flask documentation on signals: https://flask.palletsprojects.com/en/1.1.x/signals/
diff --git a/docs/authors.rst b/docs/authors.rst
new file mode 100644
index 0000000..c29da5d
--- /dev/null
+++ b/docs/authors.rst
@@ -0,0 +1 @@
+.. include:: ../AUTHORS
diff --git a/docs/changelog.rst b/docs/changelog.rst
new file mode 100644
index 0000000..d9e113e
--- /dev/null
+++ b/docs/changelog.rst
@@ -0,0 +1 @@
+.. include:: ../CHANGES.rst
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..45d7605
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,270 @@
+# -*- coding: utf-8 -*-
+#
+# Flask-Security documentation build configuration file, created by
+# sphinx-quickstart on Mon Mar 12 15:35:21 2012.
+#
+# This file is execfile()d with the current directory set to its containing
+# dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import os
+import sys
+
+from pallets_sphinx_themes import ProjectLink
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+sys.path.insert(0, os.path.abspath(".."))
+
+# -- General configuration -----------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+# needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = [
+ "pallets_sphinx_themes",
+ "sphinx.ext.autodoc",
+ "sphinx.ext.intersphinx",
+ "sphinx_issues",
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ["_templates"]
+
+# The suffix of source filenames.
+source_suffix = ".rst"
+
+# The encoding of source files.
+# source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = "index"
+
+# General information about the project.
+project = u"Flask-Security"
+copyright = u"2012-2020"
+author = "Matt Wright & Chris Wagner"
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = "3.4.2"
+# The full version, including alpha/beta/rc tags.
+release = version
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+# language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+# today = ''
+# Else, today_fmt is used as the format for a strftime call.
+# today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ["_build"]
+
+# The reST default role (used for this markup: `text`) to use for all
+# documents.
+# default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+# add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+# add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+# show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+# pygments_style = "pocoo"
+
+# A list of ignored prefixes for module index sorting.
+# modindex_common_prefix = []
+
+nitpicky = True
+nitpick_ignore = [("py:attr", "LoginManager.unauthorized"), ("py:class", "function")]
+
+# -- Options for HTML output ---------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. Major themes that come with
+# Sphinx are currently 'default' and 'sphinxdoc'.
+html_theme = "flask"
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+html_theme_options = {"index_sidebar_logo": False}
+html_context = {
+ "project_links": [
+ ProjectLink("PyPI releases", "https://pypi.org/project/Flask-Security-Too/"),
+ ProjectLink(
+ "Source Code", "https://github.com/Flask-Middleware/flask-security/"
+ ),
+ ProjectLink(
+ "Issue Tracker",
+ "https://github.com/Flask-Middleware/flask-security/issues/",
+ ),
+ ]
+}
+
+# The name for this set of Sphinx documents. If None, it defaults to
+# "<project> v<release> documentation".
+# html_title = "Flask-Security Documentation ({}).format(version)"
+html_logo = "_static/logo-owl-105.png"
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+# html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ["_static", "openapi.yaml"]
+
+
+# Custom sidebar templates, maps document names to template names.
+html_sidebars = {
+ "index": ["project.html", "localtoc.html", "searchbox.html"],
+ "**": ["localtoc.html", "relations.html", "searchbox.html"],
+}
+singlehtml_sidebars = {"index": ["project.html", "localtoc.html"]}
+
+# If true, links to the reST sources are added to the pages.
+html_show_sourcelink = False
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = "Flask-Securitydoc"
+
+
+# -- Options for LaTeX output --------------------------------------------
+
+latex_elements = {
+ # The paper size ('letterpaper' or 'a4paper').
+ # 'papersize': 'letterpaper',
+ # The font size ('10pt', '11pt' or '12pt').
+ # 'pointsize': '10pt',
+ # Additional stuff for the LaTeX preamble.
+ # 'preamble': '',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass
+# [howto/manual]).
+latex_documents = [
+ ("index", "Flask-Security.tex", u"Flask-Security Documentation", author, "manual")
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+# latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+# latex_use_parts = False
+
+# If true, show page references after internal links.
+# latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+# latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+# latex_appendices = []
+
+# If false, no module index is generated.
+# latex_domain_indices = True
+
+
+# -- Options for Texinfo output ------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ (
+ "index",
+ "Flask-Security",
+ u"Flask-Security Documentation",
+ u"Matt Wright",
+ "Flask-Security",
+ "One line description of project.",
+ "Miscellaneous",
+ )
+]
+
+# Documents to append as an appendix to all manuals.
+# texinfo_appendices = []
+
+# If false, no module index is generated.
+# texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+# texinfo_show_urls = 'footnote'
+
+
+# -- Options for Epub output ---------------------------------------------
+
+# Bibliographic Dublin Core info.
+epub_title = u"Flask-Security"
+epub_author = u"Matt Wright"
+epub_publisher = u"J. Christopher Wagner"
+epub_copyright = u"2012-2019"
+
+# The language of the text. It defaults to the language option
+# or en if the language is not set.
+# epub_language = ''
+
+# The scheme of the identifier. Typical schemes are ISBN or URL.
+# epub_scheme = ''
+
+# The unique identifier of the text. This can be a ISBN number
+# or the project homepage.
+# epub_identifier = ''
+
+# A unique identification for the text.
+# epub_uid = ''
+
+# A tuple containing the cover image and cover page html template filenames.
+# epub_cover = ()
+
+# HTML files that should be inserted before the pages created by sphinx.
+# The format is a list of tuples containing the path and title.
+# epub_pre_files = []
+
+# HTML files shat should be inserted after the pages created by sphinx.
+# The format is a list of tuples containing the path and title.
+# epub_post_files = []
+
+# A list of files that should not be packed into the epub file.
+# epub_exclude_files = []
+
+# The depth of the table of contents in toc.ncx.
+# epub_tocdepth = 3
+
+# Allow duplicate toc entries.
+# epub_tocdup = True
+
+
+# Example configuration for intersphinx: refer to the Python standard library.
+intersphinx_mapping = {"https://docs.python.org/3": None}
+
+# -- Options for sphinx-issues ---------------------------------------------
+# Github repo
+issues_github_path = "Flask-Middleware/flask-security"
diff --git a/docs/configuration.rst b/docs/configuration.rst
new file mode 100644
index 0000000..252ec96
--- /dev/null
+++ b/docs/configuration.rst
@@ -0,0 +1,1223 @@
+Configuration
+=============
+
+The following configuration values are used by Flask-Security:
+
+Core
+--------------
+
+These configuration keys are used globally across all features.
+
+.. py:data:: SECRET_KEY
+
+ This is actually part of Flask - but is used by Flask-Security to sign all tokens.
+ It is critical this is set to a strong value. For python3 consider using: ``secrets.token_urlsafe()``
+
+.. py:data:: SECURITY_BLUEPRINT_NAME
+
+ Specifies the name for the Flask-Security blueprint.
+
+ Default: ``security``.
+
+.. py:data:: SECURITY_URL_PREFIX
+
+ Specifies the URL prefix for the Flask-Security blueprint.
+
+ Default: ``None``.
+
+.. py:data:: SECURITY_SUBDOMAIN
+
+ Specifies the subdomain for the Flask-Security blueprint.
+
+ Default: ``None``.
+.. py:data:: SECURITY_FLASH_MESSAGES
+
+ Specifies whether or not to flash messages during security procedures.
+
+ Default: ``True``.
+.. py:data:: SECURITY_I18N_DOMAIN
+
+ Specifies the name for domain used for translations.
+
+ Default: ``flask_security``.
+.. py:data:: SECURITY_I18N_DIRNAME
+
+ Specifies the directory containing the ``MO`` files used for translations.
+
+ Default: ``[PATH_LIB]/flask_security/translations``.
+
+.. py:data:: SECURITY_PASSWORD_HASH
+
+ Specifies the password hash algorithm to use when hashing passwords.
+ Recommended values for production systems are ``bcrypt``, ``argon2``, ``sha512_crypt``, or
+ ``pbkdf2_sha512``. Some algorithms require the installation of a backend package (e.g. `bcrypt`_, `argon2`_).
+
+ Default:``bcrypt``.
+
+.. py:data:: SECURITY_PASSWORD_SCHEMES
+
+ List of support password hash algorithms. ``SECURITY_PASSWORD_HASH``
+ must be from this list. Passwords encrypted with any of these schemes will be honored.
+
+.. py:data:: SECURITY_DEPRECATED_PASSWORD_SCHEMES
+
+ List of password hash algorithms that are considered weak and
+ will be accepted, however on first use, will be re-hashed to the current
+ setting of ``SECURITY_PASSWORD_HASH``.
+
+ Default: ``["auto"]`` which means any password found that wasn't
+ hashed using ``SECURITY_PASSWORD_HASH`` will be re-hashed.
+
+.. py:data:: SECURITY_PASSWORD_SALT
+
+ Specifies the HMAC salt. This is required for all schemes that
+ are configured for double hashing. A good salt can be generated using:
+ ``secrets.SystemRandom().getrandbits(128)``.
+
+ Default: ``None``.
+
+.. py:data:: SECURITY_PASSWORD_SINGLE_HASH
+
+ A list of schemes that should not be hashed twice. By default, passwords are
+ hashed twice, first with ``SECURITY_PASSWORD_SALT``, and then with a random salt.
+
+ Default: a list of known schemes not working with double hashing (`django_{digest}`, `plaintext`).
+
+.. py:data:: SECURITY_HASHING_SCHEMES
+
+ List of algorithms used for encrypting/hashing sensitive data within a token
+ (Such as is sent with confirmation or reset password).
+
+ Default: ``sha256_crypt``.
+.. py:data:: SECURITY_DEPRECATED_HASHING_SCHEMES
+
+ List of deprecated algorithms used for creating and validating tokens.
+
+ Default: ``hex_md5``.
+
+.. py:data:: SECURITY_PASSWORD_HASH_OPTIONS
+
+ Specifies additional options to be passed to the hashing method. This is deprecated as of passlib 1.7.
+
+ .. deprecated:: 3.4.0 see: :py:data:`SECURITY_PASSWORD_HASH_PASSLIB_OPTIONS`
+
+.. py:data:: SECURITY_PASSWORD_HASH_PASSLIB_OPTIONS
+
+ Pass additional options to the various hashing methods. This is a
+ dict of the form ``{<scheme>__<option>: <value>, ..}``
+ e.g. {"argon2__rounds": 10}.
+
+ .. versionadded:: 3.3.1
+
+.. py:data:: SECURITY_PASSWORD_LENGTH_MIN
+
+ Minimum required length for passwords.
+
+ Default: 8
+
+ .. versionadded:: 3.4.0
+.. py:data:: SECURITY_PASSWORD_COMPLEXITY_CHECKER
+
+ Set to complexity checker to use (Only ``zxcvbn`` supported).
+
+ Default: ``None``
+
+ .. versionadded:: 3.4.0
+.. py:data:: SECURITY_PASSWORD_CHECK_BREACHED
+
+ If not ``None`` new/changed passwords will be checked against the
+ database of breached passwords at https://api.pwnedpasswords.com.
+ If set to ``strict`` then if the site can't be reached, validation will fail.
+ If set to ``best-effort`` failure to reach the site will continue
+ with the rest of password validation.
+
+ Default: ``None``
+
+ .. versionadded:: 3.4.0
+.. py:data:: SECURITY_PASSWORD_BREACHED_COUNT
+
+ Passwords with counts greater than or equal to this value are considered breached.
+
+ Default: 1 - which might be to burdensome for some applications.
+
+ .. versionadded:: 3.4.0
+
+.. py:data:: SECURITY_TOKEN_AUTHENTICATION_KEY
+
+ Specifies the query string parameter to read when using token authentication.
+
+ Default: ``auth_token``.
+
+.. py:data:: SECURITY_TOKEN_AUTHENTICATION_HEADER
+
+ Specifies the HTTP header to read when using token authentication.
+
+ Default: ``Authentication-Token``.
+
+.. py:data:: SECURITY_TOKEN_MAX_AGE
+
+ Specifies the number of seconds before an authentication token expires.
+
+ Default: ``None``, meaning the token never expires.
+
+.. py:data:: SECURITY_DEFAULT_HTTP_AUTH_REALM
+
+ Specifies the default authentication realm when using basic HTTP auth.
+
+ Default: ``Login Required``
+
+.. py:data:: SECURITY_USE_VERIFY_PASSWORD_CACHE
+
+ If ``True`` enables cache for token verification, which speeds up further
+ calls to authenticated routes using authentication-token and slow hash algorithms (like bcrypt).
+ If you set this - you must ensure that `cachetools`_ is installed.
+ **Note: this will likely be deprecated and removed in 4.0. It**
+ **has known limitations, and there is now a better/faster way to**
+ **generate and verify auth tokens.**
+
+ Default: ``None``.
+
+.. py:data:: SECURITY_VERIFY_HASH_CACHE_MAX_SIZE
+
+ Limitation for token validation cache size. Rules are the ones of TTLCache of
+ cachetools package.
+
+ Default: ``500``
+
+.. py:data:: SECURITY_VERIFY_HASH_CACHE_TTL
+
+ Time to live for password check cache entries.
+
+ Default: ``300`` (5 minutes)
+
+.. py:data:: SECURITY_REDIRECT_BEHAVIOR
+
+ Passwordless login, confirmation, and reset password have GET endpoints that validate
+ the passed token and redirect to an action form.
+ For Single-Page-Applications style UIs which need to control their own internal URL routing these redirects
+ need to not contain forms, but contain relevant information as query parameters.
+ Setting this to ``spa`` will enable that behavior.
+
+ Default: ``None`` which is existing html-style form redirects.
+
+ .. versionadded:: 3.3.0
+
+.. py:data:: SECURITY_REDIRECT_HOST
+
+ Mostly for development purposes, the UI is often developed
+ separately and is running on a different port than the
+ Flask application. In order to test redirects, the `netloc`
+ of the redirect URL needs to be rewritten. Setting this to e.g. `localhost:8080` does that.
+
+ Default: ``None``.
+
+ .. versionadded:: 3.3.0
+
+.. py:data:: SECURITY_CSRF_PROTECT_MECHANISMS
+
+ Authentication mechanisms that require CSRF protection.
+ These are the same mechanisms as are permitted in the ``@auth_required`` decorator.
+
+ Default: ``("basic", "session", "token")``.
+
+.. py:data:: SECURITY_CSRF_IGNORE_UNAUTH_ENDPOINTS
+
+ If ``True`` then CSRF will not be required for endpoints
+ that don't require authentication (e.g. login, logout, register, forgot_password).
+
+ Default: ``False``.
+
+.. py:data:: SECURITY_CSRF_COOKIE
+
+ A dict that defines the parameters required to
+ set a CSRF cookie. At a minimum it requires a 'key'.
+ The complete set of parameters is described in Flask's `set_cookie`_ documentation.
+
+ Default: ``{"key": None}`` which means no cookie will sent.
+
+.. py:data:: SECURITY_CSRF_HEADER
+
+ The HTTP Header name that will contain the CSRF token. ``X-XSRF-Token``
+ is used by packages such as `axios`_.
+
+ Default: ``X-XSRF-Token``.
+
+.. py:data:: SECURITY_CSRF_COOKIE_REFRESH_EACH_REQUEST
+
+ By default, csrf_tokens have an expiration (controlled
+ by the configuration variable ``WTF_CSRF_TIME_LIMIT``.
+ This can cause CSRF failures if say an application is left
+ idle for a long time. You can set that time limit to ``None``
+ or have the CSRF cookie sent on every request (which will give
+ it a new expiration time).
+
+ Default: ``False``.
+
+.. py:data:: SECURITY_EMAIL_SENDER
+
+ Specifies the email address to send emails as.
+
+ Default: value set to ``MAIL_DEFAULT_SENDER`` if Flask-Mail is used otherwise ``no-reply@localhost``.
+
+.. py:data:: SECURITY_USER_IDENTITY_ATTRIBUTES
+
+ Specifies which attributes of the user object can be used for login.
+
+ Default: ``['email']``.
+
+ .. danger::
+ Make sure that any attributes listed here are marked Unique in your UserDataStore
+ model.
+
+.. py:data:: SECURITY_USER_IDENTITY_MAPPINGS
+
+ Defines the order and matching that will be applied when validating the
+ unified sign in form. This form has a single ``identity`` field
+ that is parsed using the information below - the FIRST match will then be
+ used to look up the user in the DB.
+
+ Default::
+
+ [
+ {"email": uia_email_mapper},
+ {"us_phone_number": uia_phone_mapper},
+ ],
+
+ Be aware that ONLY those attributes listed in :py:data:`SECURITY_USER_IDENTITY_ATTRIBUTES`
+ will be considered - regardless of the setting of this variable.
+
+ Mapping functions take a single argument - ``identity`` from the form
+ and should return ``None`` if the ``identity`` argument isn't in a format
+ suitable for the attribute. If the ``identity`` argument format matches, it
+ should be returned, optionally having had some canonicalization performed.
+ The returned result will be used to look up the identity in the UserDataStore.
+
+ The provided :meth:`flask_security.uia_phone_mapper` for example performs
+ phone number normalization using the ``phonenumbers`` package.
+
+ .. tip::
+ If your mapper performs any sort of canonicalization/normalization,
+ make sure you apply the exact same transformation in your form validator
+ when setting the field.
+
+ .. versionadded:: 3.4.0
+
+.. py:data:: SECURITY_DEFAULT_REMEMBER_ME
+
+ Specifies the default "remember me" value used when logging in a user.
+
+ Default: ``False``.
+
+.. py:data:: SECURITY_BACKWARDS_COMPAT_UNAUTHN
+
+ If set to ``True`` then the default behavior for authentication
+ failures from one of Flask-Security's decorators will be restored to
+ be compatible with releases prior to 3.3.0 (return 401 and some static html).
+
+ Default: ``False``.
+
+.. py:data:: SECURITY_BACKWARDS_COMPAT_AUTH_TOKEN
+
+ If set to ``True`` then an Authentication-Token will be returned
+ on every successful call to login, reset-password, change-password
+ as part of the JSON response. This was the default prior to release 3.3.0
+ - however sending Authentication-Tokens (which by default don't expire)
+ to session based UIs is a bad security practice.
+
+ Default: ``False``.
+
+.. py:data:: SECURITY_BACKWARDS_COMPAT_AUTH_TOKEN_INVALIDATE
+
+ When ``True`` changing the user's password will also change the user's
+ ``fs_uniquifier`` (if it exists) such that existing authentication tokens
+ will be rendered invalid. This restores pre 3.3.0 behavior.
+
+Core - Multi-factor
+-------------------
+These are used by the Two-Factor and Unified Signin features.
+
+.. py:data:: SECURITY_TOTP_SECRETS
+
+ Secret used to encrypt totp_password both into DB and in session cookie.
+ Best practice is to set this to:
+
+ .. code-block:: python
+
+ from passlib import totp
+ "{1: <result of totp.generate_secret()>}"
+
+ See: `Totp`_ for details.
+
+ .. versionadded:: 3.4.0
+
+.. py:data:: SECURITY_TOTP_ISSUER
+
+ Specifies the name of the service or application that the user is authenticating to.
+ This will be the name displayed by most authenticator apps.
+
+ Default: ``None``.
+
+ .. versionadded:: 3.4.0
+
+.. py:data:: SECURITY_SMS_SERVICE
+
+ Specifies the name of the sms service provider.
+
+ Default: ``Dummy`` which does nothing.
+
+ .. versionadded:: 3.4.0
+
+.. py:data:: SECURITY_SMS_SERVICE_CONFIG
+
+ Specifies a dictionary of basic configurations needed for use of a sms service.
+
+ Default: ``{'ACCOUNT_ID': NONE, 'AUTH_TOKEN':NONE, 'PHONE_NUMBER': NONE}``
+
+ .. versionadded:: 3.4.0
+
+.. py:data:: SECURITY_PHONE_REGION_DEFAULT
+
+ Assigns a default 'region' for phone numbers used for two-factor or
+ unified sign in. All other phone numbers will require a region prefix to
+ be accepted.
+
+ Default: ``US``
+
+ .. versionadded:: 3.4.0
+
+.. py:data:: SECURITY_FRESHNESS
+
+ A timedelta used to protect endpoints that alter sensitive information.
+ This is used to protect the endpoint: :py:data:`SECURITY_US_SETUP_URL`.
+ Refer to :meth:`flask_security.auth_required` for details.
+ Setting this to a negative number will disable any freshness checking and
+ the endpoints :py:data:`SECURITY_VERIFY_URL`, :py:data:`SECURITY_US_VERIFY_URL`
+ and :py:data:`SECURITY_US_VERIFY_SEND_CODE_URL` won't be registered.
+ Setting this to 0 results in undefined behavior.
+
+ Default: timedelta(hours=24)
+
+ .. versionadded:: 3.4.0
+
+.. py:data:: SECURITY_FRESHNESS_GRACE_PERIOD
+
+ A timedelta that provides a grace period when altering sensitive
+ information.
+ This is used to protect the endpoint: :py:data:`SECURITY_US_SETUP_URL`.
+ Refer to :meth:`flask_security.auth_required` for details.
+ N.B. To avoid strange behavior, be sure to set the grace period less than
+ the freshness period.
+ Please see :meth:`flask_security.check_and_update_authn_fresh` for details.
+
+ Default: timedelta(hours=1)
+
+ .. versionadded:: 3.4.0
+
+
+Core - rarely need changing
+----------------------------
+
+.. py:data:: SECURITY_DATETIME_FACTORY
+
+ Specifies the default datetime factory.
+
+ Default:``datetime.datetime.utcnow``.
+
+.. py:data:: SECURITY_CONFIRM_SALT
+
+ Specifies the salt value when generating confirmation links/tokens.
+
+ Default: ``"confirm-salt"``.
+
+.. py:data:: SECURITY_RESET_SALT
+
+ Specifies the salt value when generating password reset links/tokens.
+
+ Default: ``"reset-salt"``.
+
+.. py:data:: SECURITY_LOGIN_SALT
+
+ Specifies the salt value when generating login links/tokens.
+
+ Default: ``"login-salt"``.
+
+.. py:data:: SECURITY_REMEMBER_SALT
+
+ Specifies the salt value when generating remember tokens.
+ Remember tokens are used instead of user ID's as it is more secure.
+
+ Default: ``"remember-salt"``.
+.. py:data:: SECURITY_US_SETUP_SALT
+
+ Default: ``"us-setup-salt"``
+
+.. py:data:: SECURITY_EMAIL_PLAINTEXT
+
+ Sends email as plaintext using ``*.txt`` template.
+
+ Default: ``True``.
+
+.. py:data:: SECURITY_EMAIL_HTML
+
+ Sends email as HTML using ``*.html`` template.
+
+ Default: ``True``.
+
+.. py:data:: SECURITY_CLI_USERS_NAME
+
+ Specifies the name for the command managing users. Disable by setting ``False``.
+
+ Default: ``users``.
+
+.. py:data:: SECURITY_CLI_ROLES_NAME
+
+ Specifies the name for the command managing roles. Disable by setting ``False``.
+
+ Default: ``roles``.
+
+.. py:data:: SECURITY_JOIN_USER_ROLES
+
+ Specifies whether to set the ``UserModel.roles`` loading relationship to ``joined`` when a ``roles`` attribute
+ is present for a SQLAlchemy Datastore. Setting this to ``False`` restores pre 3.3.0 behavior and is required if the ``roles`` attribute
+ is not a joinable attribute on the ``UserModel``. The default setting improves performance by only requiring a single
+ DB call.
+
+ Default: ``True``.
+
+ .. versionadded:: 3.4.0
+
+.. _Totp: https://passlib.readthedocs.io/en/stable/narr/totp-tutorial.html#totp-encryption-setup
+.. _set_cookie: https://flask.palletsprojects.com/en/1.1.x/api/?highlight=set_cookie#flask.Response.set_cookie
+.. _axios: https://github.com/axios/axios
+.. _cachetools: https://pypi.org/project/cachetools/
+.. _bcrypt: https://pypi.org/project/bcrypt/
+.. _argon2: https://pypi.org/project/argon2-cffi/
+
+Login/Logout
+------------
+.. py:data:: SECURITY_LOGIN_URL
+
+ Specifies the login URL.
+
+ Default: ``"/login"``.
+
+.. py:data:: SECURITY_LOGOUT_URL
+
+ Specifies the logout URL.
+
+ Default:``"/logout"``.
+
+
+.. py:data:: SECURITY_LOGOUT_METHODS
+
+ Specifies the HTTP request methods that the logout URL accepts. Specify ``None`` to disable the logout URL (and implement your own).
+ Configuring with just ``["POST"]`` is slightly more secure. The default includes ``"GET"`` for backwards compatibility.
+
+ Default: ``["GET", "POST"]``.
+
+
+.. py:data:: SECURITY_POST_LOGIN_VIEW
+
+ Specifies the default view to redirect to after a user logs in. This value can be set to a URL
+ or an endpoint name.
+
+ Default: ``"/"``.
+
+.. py:data:: SECURITY_POST_LOGOUT_VIEW
+
+ Specifies the default view to redirect to after a user logs out.
+ This value can be set to a URL or an endpoint name.
+
+ Default: ``"/"``.
+
+
+.. py:data:: SECURITY_UNAUTHORIZED_VIEW
+
+ Specifies the view to redirect to if a user attempts to access a URL/endpoint that they do
+ not have permission to access. If this value is ``None``, the user is presented with a default
+ HTTP 403 response.
+
+ Default: ``None``.
+
+.. py:data:: SECURITY_LOGIN_USER_TEMPLATE
+
+ Specifies the path to the template for the user login page.
+
+ Default:``security/login_user.html``.
+
+.. py:data:: SECURITY_VERIFY_URL
+
+ Specifies the re-authenticate URL. If :py:data:`SECURITY_FRESHNESS` evaluates to < 0; this
+ endpoint won't be registered.
+
+ Default: ``"/verify"``
+
+.. py:data:: SECURITY_POST_VERIFY_URL
+
+ Specifies the default view to redirect to after a user successfully re-authenticates either via
+ the :py:data:`SECURITY_VERIFY_URL` or the :py:data:`SECURITY_US_VERIFY_URL`.
+ Normally this won't need to be set and after the verification/re-authentication, the referring
+ view (held in the ``next`` parameter) will be redirected to.
+
+ Default: ``None``.
+
+Registerable
+------------
+.. py:data:: SECURITY_REGISTERABLE
+
+ Specifies if Flask-Security should create a user registration endpoint.
+
+ Default: ``False``
+
+.. py:data:: SECURITY_SEND_REGISTER_EMAIL
+
+ Specifies whether registration email is sent.
+
+ Default: ``True``.
+.. py:data:: SECURITY_EMAIL_SUBJECT_REGISTER
+
+ Sets the subject for the confirmation email.
+
+ Default: ``Welcome``.
+.. py:data:: SECURITY_REGISTER_USER_TEMPLATE
+
+ Specifies the path to the template for the user registration page.
+
+ Default: ``security/register_user.html``.
+.. py:data:: SECURITY_POST_REGISTER_VIEW
+
+ Specifies the view to redirect to after a user successfully registers.
+ This value can be set to a URL or an endpoint name. If this value is
+ ``None``, the user is redirected to the value of ``SECURITY_POST_LOGIN_VIEW``.
+
+ Default: ``None``.
+.. py:data:: SECURITY_REGISTER_URL
+
+ Specifies the register URL.
+
+ Default: ``"/register"``.
+
+Confirmable
+-----------
+
+.. py:data:: SECURITY_CONFIRMABLE
+
+ Specifies if users are required to confirm their email address when
+ registering a new account. If this value is `True`, Flask-Security creates an endpoint to handle
+ confirmations and requests to resend confirmation instructions.
+
+ Default: ``False``.
+.. py:data:: SECURITY_CONFIRM_EMAIL_WITHIN
+
+ Specifies the amount of time a user has before their confirmation
+ link expires. Always pluralize the time unit for this value.
+
+ Default: ``5 days``.
+.. py:data:: SECURITY_CONFIRM_URL
+
+ Specifies the email confirmation URL.
+
+ Default: ``"/confirm"``.
+.. py:data:: SECURITY_SEND_CONFIRMATION_TEMPLATE
+
+ Specifies the path to the template for the resend confirmation instructions page.
+
+ Default: ``security/send_confirmation.html``.
+.. py:data:: SECURITY_EMAIL_SUBJECT_CONFIRM
+
+ Sets the subject for the email confirmation message.
+
+ Default: ``Please confirm your email``.
+.. py:data:: SECURITY_CONFIRM_ERROR_VIEW
+
+ Specifies the view to redirect to if a confirmation error occurs.
+ This value can be set to a URL or an endpoint name.
+ If this value is ``None``, the user is presented the default view
+ to resend a confirmation link. In the case of ``SECURITY_REDIRECT_BEHAVIOR`` == ``spa``
+ query params in the redirect will contain the error.
+
+ Default: ``None``.
+.. py:data:: SECURITY_POST_CONFIRM_VIEW
+
+ Specifies the view to redirect to after a user successfully confirms their email.
+ This value can be set to a URL or an endpoint name. If this value is ``None``, the user is redirected to the
+ value of ``SECURITY_POST_LOGIN_VIEW``.
+
+ Default: ``None``.
+.. py:data:: SECURITY_AUTO_LOGIN_AFTER_CONFIRM
+
+ If ``False`` then on confirmation the user will be required to login again.
+ Note that the confirmation token is not valid after being used once.
+ If ``True``, then the user corresponding to the
+ confirmation token will be automatically logged in.
+
+ Default: ``True``.
+.. py:data:: SECURITY_LOGIN_WITHOUT_CONFIRMATION
+
+ Specifies if a user may login before confirming their email when
+ the value of ``SECURITY_CONFIRMABLE`` is set to ``True``.
+
+ Default:``False``.
+
+Changeable
+----------
+Configuration variables for the ``SECURITY_CHANGEABLE`` feature:
+
+.. py:data:: SECURITY_CHANGEABLE
+
+ Specifies if Flask-Security should enable the change password endpoint.
+
+ Default: ``False``.
+.. py:data:: SECURITY_CHANGE_URL
+
+ Specifies the password change URL.
+
+ Default: ``"/change"``.
+.. py:data:: SECURITY_POST_CHANGE_VIEW
+
+ Specifies the view to redirect to after a user successfully changes their password.
+ This value can be set to a URL or an endpoint name.
+ If this value is ``None``, the user is redirected to the
+ value of ``SECURITY_POST_LOGIN_VIEW``.
+
+ Default: ``None``.
+.. py:data:: SECURITY_CHANGE_PASSWORD_TEMPLATE
+
+ Specifies the path to the template for the change password page.
+
+ Default: ``security/change_password.html``.
+
+.. py:data:: SECURITY_SEND_PASSWORD_CHANGE_EMAIL
+
+ Specifies whether password change email is sent.
+
+ Default: ``True``.
+
+.. py:data:: SECURITY_EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE
+
+ Sets the subject for the password change notice.
+
+ Default: ``Your password has been changed``.
+
+Recoverable
+-----------
+
+.. py:data:: SECURITY_RECOVERABLE
+
+ Specifies if Flask-Security should create a password reset/recover endpoint.
+
+ Default: ``False``.
+
+.. py:data:: SECURITY_RESET_URL
+
+ Specifies the password reset URL.
+
+ Default: ``"/reset"``.
+
+.. py:data:: SECURITY_RESET_PASSWORD_TEMPLATE
+
+ Specifies the path to the template for the reset password page.
+
+ Default: ``security/reset_password.html``.
+
+.. py:data:: SECURITY_FORGOT_PASSWORD_TEMPLATE
+
+ Specifies the path to the template for the forgot password page.
+
+ Default: ``security/forgot_password.html``.
+
+.. py:data:: SECURITY_POST_RESET_VIEW
+
+ Specifies the view to redirect to after a user successfully resets their password.
+ This value can be set to a URL or an endpoint name. If this
+ value is ``None``, the user is redirected to the value of ``SECURITY_POST_LOGIN_VIEW``.
+
+ Default: ``None``.
+
+.. py:data:: SECURITY_RESET_VIEW
+
+ Specifies the view/URL to redirect to after a GET reset-password link.
+ This is only valid if ``SECURITY_REDIRECT_BEHAVIOR`` == ``spa``.
+ Query params in the redirect will contain the ``token`` and ``email``.
+
+ Default: ``None``.
+
+.. py:data:: SECURITY_RESET_ERROR_VIEW
+
+ Specifies the view/URL to redirect to after a GET reset-password link when there is an error.
+ This is only valid if ``SECURITY_REDIRECT_BEHAVIOR`` == ``spa``.
+ Query params in the redirect will contain the error.
+
+ Default: ``None``.
+
+.. py:data:: SECURITY_RESET_PASSWORD_WITHIN
+
+ Specifies the amount of time a user has before their password reset link expires.
+ Always pluralize the time unit for this value.
+
+ Default: ``5 days``.
+
+.. py:data:: SECURITY_SEND_PASSWORD_RESET_EMAIL
+
+ Specifies whether password reset email is sent. These are instructions
+ including a link that can be clicked on.
+
+ Default: ``True``.
+
+.. py:data:: SECURITY_SEND_PASSWORD_RESET_NOTICE_EMAIL
+
+ Specifies whether password reset notice email is sent. This is sent once
+ a user's password was successfully reset.
+
+ Default: ``True``.
+
+.. py:data:: SECURITY_EMAIL_SUBJECT_PASSWORD_RESET
+
+ Sets the subject for the password reset email.
+
+ Default: ``Password reset instructions``.
+
+.. py:data:: SECURITY_EMAIL_SUBJECT_PASSWORD_NOTICE
+
+ Sets subject for the password notice.
+
+ Default: ``Your password has been reset``.
+
+Two-Factor
+-----------
+Configuration related to the two-factor authentication feature.
+
+.. versionadded:: 3.2.0
+
+.. py:data:: SECURITY_TWO_FACTOR
+
+ Specifies if Flask-Security should enable the two-factor login feature.
+ If set to ``True``, in addition to their passwords, users will be required to
+ enter a code that is sent to them. Note that unless
+ ``SECURITY_TWO_FACTOR_REQUIRED`` is set - this is opt-in.
+
+ Default: ``False``.
+.. py:data:: SECURITY_TWO_FACTOR_REQUIRED
+
+ If set to ``True`` then all users will be required to setup and use two factor authorization.
+
+ Default: ``False``.
+.. py:data:: SECURITY_TWO_FACTOR_ENABLED_METHODS
+
+ Specifies the default enabled methods for two-factor authentication.
+
+ Default: ``['email', 'authenticator', 'sms']`` which are the only currently supported methods.
+
+.. py:data:: SECURITY_TWO_FACTOR_SECRET
+
+ .. deprecated:: 3.4.0 see: :py:data:`SECURITY_TOTP_SECRETS`
+
+.. py:data:: SECURITY_TWO_FACTOR_URI_SERVICE_NAME
+
+ .. deprecated:: 3.4.0 see: :py:data:`SECURITY_TOTP_ISSUER`
+
+.. py:data:: SECURITY_TWO_FACTOR_SMS_SERVICE
+
+ .. deprecated:: 3.4.0 see: :py:data:`SECURITY_SMS_SERVICE`
+
+.. py:data:: SECURITY_TWO_FACTOR_SMS_SERVICE_CONFIG
+
+ .. deprecated:: 3.4.0 see: :py:data:`SECURITY_SMS_SERVICE_CONFIG`
+
+.. py:data:: SECURITY_TWO_FACTOR_AUTHENTICATOR_VALIDITY
+
+ Specifies the number of seconds access token is valid.
+
+ Default: ``2 minutes``.
+.. py:data:: SECURITY_TWO_FACTOR_MAIL_VALIDITY
+
+ Specifies the number of seconds access token is valid.
+
+ Default: ``5 minutes``.
+.. py:data:: SECURITY_TWO_FACTOR_SMS_VALIDITY
+
+ Specifies the number of seconds access token is valid.
+
+ Default: ``2 minutes``.
+.. py:data:: SECURITY_TWO_FACTOR_RESCUE_MAIL
+
+ Specifies the email address users send mail to when they can't complete the
+ two-factor authentication login.
+
+ Default: ``no-reply@localhost``.
+
+.. py:data:: SECURITY_EMAIL_SUBJECT_TWO_FACTOR
+
+ Sets the subject for the two factor feature.
+
+ Default: ``Two-factor Login``
+.. py:data:: SECURITY_EMAIL_SUBJECT_TWO_FACTOR_RESCUE
+
+ Sets the subject for the two factor help function.
+
+ Default: ``Two-factor Rescue``
+.. py:data:: SECURITY_TWO_FACTOR_VERIFY_CODE_TEMPLATE
+
+ Specifies the path to the template for the verify code page for the two-factor authentication process.
+
+ Default: ``security/two_factor_verify_code.html``.
+.. py:data:: SECURITY_TWO_FACTOR_SETUP_TEMPLATE
+
+ Specifies the path to the template for the setup page for the two factor authentication process.
+
+ Default: ``security/two_factor_setup.html``.
+.. py:data:: SECURITY_TWO_FACTOR_VERIFY_PASSWORD_TEMPLATE
+
+ Specifies the path to the template for the change method page for the two
+ factor authentication process.
+
+ Default: ``security/two_factor_verify_password.html``.
+
+.. py:data:: SECURITY_TWO_FACTOR_SETUP_URL
+
+ Specifies the two factor setup URL.
+
+ Default: ``"/tf-setup"``.
+.. py:data:: SECURITY_TWO_FACTOR_TOKEN_VALIDATION_URL
+
+ Specifies the two factor token validation URL.
+
+ Default: ``"/tf-validate"``.
+.. py:data:: SECURITY_TWO_FACTOR_QRCODE_URL
+
+ Specifies the two factor request QrCode URL.
+
+ Default: ``/tf-qrcode``.
+.. py:data:: SECURITY_TWO_FACTOR_RESCUE_URL
+
+ Specifies the two factor rescue URL.
+
+ Default: ``"/tf-rescue"``.
+.. py:data:: SECURITY_TWO_FACTOR_CONFIRM_URL
+
+ Specifies the two factor password confirmation URL.
+
+ Default: ``"/tf-confirm"``.
+
+Unified Signin
+--------------
+
+ .. versionadded:: 3.4.0
+
+.. py:data:: SECURITY_UNIFIED_SIGNIN
+
+ To enable this feature - set this to ``True``.
+
+ Default: ``False``
+
+.. py:data:: SECURITY_US_SIGNIN_URL
+
+ Sign in a user with an identity and a passcode.
+
+ Default: ``"/us-signin"``
+
+.. py:data:: SECURITY_US_SIGNIN_SEND_CODE_URL
+
+ Endpoint that given an identity, and a previously setup authentication method, will
+ generate and return a one time code. This isn't necessary when using an authenticator app.
+
+ Default: ``"/us-signin/send-code"``
+
+.. py:data:: SECURITY_US_SETUP_URL
+
+ Endpoint for setting up and validating SMS or an authenticator app for use in
+ receiving one-time codes.
+
+ Default: ``"/us-setup"``
+
+.. py:data:: SECURITY_US_VERIFY_LINK_URL
+
+ This endpoint handles the 'magic link' that is sent when the user requests a code
+ via email. It is mostly just accessed via a ``GET`` from an email reader.
+
+ Default: ``"/us-verify-link"``
+
+.. py:data:: SECURITY_US_QRCODE_URL
+
+ Used to generate and return a QRcode that can be used to intialize an authenticator app.
+
+ Default: ``"/us-qrcode"``
+
+.. py:data:: SECURITY_US_VERIFY_URL
+
+ This endpoint handles re-authentication, the caller must be already authenticated
+ and then enter in their primary credentials (password/passcode) again. This is
+ used when an endpoint (such as ``/us-setup``) fails freshness checks.
+ This endpoint won't be registered if :py:data:`SECURITY_FRESHNESS` evaluates to < 0.
+
+ Default: ``"/us-verify"``
+
+.. py:data:: SECURITY_US_VERIFY_SEND_CODE_URL
+
+ As part of ``/us-verify``, this endpoint will send the appropriate code.
+ This endpoint won't be registered if :py:data:`SECURITY_FRESHNESS` evaluates to < 0.
+
+ Default: ``"/us-verify/send-code"``
+
+.. py:data:: SECURITY_US_POST_SETUP_VIEW
+
+ Specifies the view to redirect to after a user successfully setups an authentication method (non-json).
+ This value can be set to a URL or an endpoint name. If this value is ``None``, the user is redirected to the
+ value of :py:data:`SECURITY_POST_LOGIN_VIEW`.
+
+ Default: ``None``
+
+.. py:data:: SECURITY_US_SIGNIN_TEMPLATE
+
+ Default: ``"security/us_signin.html"``
+
+.. py:data:: SECURITY_US_SETUP_TEMPLATE
+
+ Default: ``"security/us_setup.html"``
+
+.. py:data:: SECURITY_US_VERIFY_TEMPLATE
+
+ Default: ``"security/us_verify.html"``
+
+.. py:data:: SECURITY_US_ENABLED_METHODS
+
+ Specifies the default enabled methods for unified sign in authentication.
+ Be aware that ``password`` only affects this ``SECURITY_US_SIGNIN_URL`` endpoint.
+ Removing it from here won't stop users from using the ``SECURITY_LOGIN_URL`` endpoint.
+
+ Default: ``["password", "email", "authenticator", "sms"]`` - which are the only supported options.
+
+.. py:data:: SECURITY_US_MFA_REQUIRED
+
+ A list of ``US_ENABLED_METHODS`` that will require two-factor
+ authentication. This is of course dependent on the settings of :py:data:`SECURITY_TWO_FACTOR`
+ and :py:data:`SECURITY_TWO_FACTOR_REQUIRED`. Note that even with REQUIRED, only
+ methods listed here will trigger a two-factor cycle.
+
+ Default: ``["password", "email"]``.
+
+.. py:data:: SECURITY_US_TOKEN_VALIDITY
+
+ Specifies the number of seconds access token/code is valid.
+
+ Default: ``120``
+
+.. py:data:: SECURITY_US_EMAIL_SUBJECT
+
+ Default: ``_("Verification Code")``
+
+.. py:data:: SECURITY_US_SETUP_WITHIN
+
+ Specifies the amount of time a user has before their setup
+ token expires. Always pluralize the time unit for this value.
+
+ Default: "30 minutes"
+
+.. py:data:: SECURITY_US_SIGNIN_REPLACES_LOGIN
+
+ If set, then the :py:data:`SECURITY_LOGIN_URL` will be registered to the ``us-signin`` endpoint.
+ Doing this will mean that logout will properly redirect to the us-signin endpoint.
+
+ Default: ``False``
+
+
+Additional relevant configuration variables:
+
+ * :py:data:`SECURITY_USER_IDENTITY_ATTRIBUTES` - Defines which user fields can be
+ used for identity.
+ * :py:data:`SECURITY_USER_IDENTITY_MAPPINGS` - Defines the order and methods for parsing identity.
+ * :py:data:`SECURITY_DEFAULT_REMEMBER_ME`
+ * :py:data:`SECURITY_SMS_SERVICE` - When SMS is enabled in :py:data:`SECURITY_US_ENABLED_METHODS`.
+ * :py:data:`SECURITY_SMS_SERVICE_CONFIG`
+ * :py:data:`SECURITY_TOTP_SECRETS`
+ * :py:data:`SECURITY_TOTP_ISSUER`
+ * :py:data:`SECURITY_PHONE_REGION_DEFAULT`
+ * :py:data:`SECURITY_LOGIN_ERROR_VIEW` - The user is redirected here if
+ :py:data:`SECURITY_US_VERIFY_LINK_URL` has an error and the request is json and
+ :py:data:`SECURITY_REDIRECT_BEHAVIOR` equals ``"spa"``.
+ * :py:data:`SECURITY_FRESHNESS` - Used to protect /us-setup.
+ * :py:data:`SECURITY_FRESHNESS_GRACE_PERIOD` - Used to protect /us-setup.
+
+Passwordless
+-------------
+
+.. py:data:: SECURITY_PASSWORDLESS
+
+ Specifies if Flask-Security should enable the passwordless login feature.
+ If set to ``True``, users are not required to enter a password to login but are
+ sent an email with a login link.
+ **This feature is being replaced with a more generalized passwordless feature
+ that includes using SMS or authenticator applications for generating codes.**
+
+ Default: ``False``.
+
+.. py:data:: SECURITY_SEND_LOGIN_TEMPLATE
+
+ Specifies the path to the template for the send login instructions page for
+ passwordless logins.
+
+ Default:``security/send_login.html``.
+
+.. py:data:: SECURITY_EMAIL_SUBJECT_PASSWORDLESS
+
+ Sets the subject for the passwordless feature.
+
+ Default: ``Login instructions``.
+
+.. py:data:: SECURITY_LOGIN_WITHIN
+
+ Specifies the amount of time a user has before a login link expires.
+ Always pluralize the time unit for this value.
+
+ Default: ``1 days``.
+
+.. py:data:: SECURITY_LOGIN_ERROR_VIEW
+
+ Specifies the view/URL to redirect to after a GET passwordless link or GET
+ unified sign in magic link when there is an error.
+ This is only valid if ``SECURITY_REDIRECT_BEHAVIOR`` == ``spa``.
+ Query params in the redirect will contain the error.
+
+ Default: ``None``.
+
+Trackable
+----------
+.. py:data:: SECURITY_TRACKABLE
+
+ Specifies if Flask-Security should track basic user login statistics. If set to ``True``, ensure your
+ models have the required fields/attributes and make sure to commit changes after calling
+ ``login_user``. Be sure to use `ProxyFix <http://flask.pocoo.org/docs/0.10/deploying/wsgi-standalone/#proxy-setups>`_ if you are using a proxy.
+
+ Default: ``False``
+
+Feature Flags
+-------------
+All feature flags. By default all are 'False'/not enabled.
+
+* ``SECURITY_CONFIRMABLE``
+* ``SECURITY_REGISTERABLE``
+* ``SECURITY_RECOVERABLE``
+* ``SECURITY_TRACKABLE``
+* ``SECURITY_PASSWORDLESS``
+* ``SECURITY_CHANGEABLE``
+* ``SECURITY_TWO_FACTOR``
+* :py:data:`SECURITY_UNIFIED_SIGNIN`
+
+URLs and Views
+--------------
+A list of all URLs and Views:
+
+* ``SECURITY_LOGIN_URL``
+* ``SECURITY_LOGOUT_URL``
+* ``SECURITY_REGISTER_URL``
+* ``SECURITY_RESET_URL``
+* ``SECURITY_CHANGE_URL``
+* ``SECURITY_CONFIRM_URL``
+* ``SECURITY_TWO_FACTOR_SETUP_URL``
+* ``SECURITY_TWO_FACTOR_TOKEN_VALIDATION_URL``
+* ``SECURITY_TWO_FACTOR_QRCODE_URL``
+* ``SECURITY_TWO_FACTOR_RESCUE_URL``
+* ``SECURITY_TWO_FACTOR_CONFIRM_URL``
+* ``SECURITY_POST_LOGIN_VIEW``
+* ``SECURITY_POST_LOGOUT_VIEW``
+* ``SECURITY_CONFIRM_ERROR_VIEW``
+* ``SECURITY_POST_REGISTER_VIEW``
+* ``SECURITY_POST_CONFIRM_VIEW``
+* ``SECURITY_POST_RESET_VIEW``
+* ``SECURITY_POST_CHANGE_VIEW``
+* ``SECURITY_UNAUTHORIZED_VIEW``
+* ``SECURITY_RESET_VIEW``
+* ``SECURITY_RESET_ERROR_VIEW``
+* ``SECURITY_LOGIN_ERROR_VIEW``
+* :py:data:`SECURITY_US_SIGNIN_URL`
+* :py:data:`SECURITY_US_QRCODE_URL`
+* :py:data:`SECURITY_US_SETUP_URL`
+* :py:data:`SECURITY_US_SIGNIN_SEND_CODE_URL`
+* :py:data:`SECURITY_US_VERIFY_LINK_URL`
+* :py:data:`SECURITY_US_VERIFY_URL`
+* :py:data:`SECURITY_US_VERIFY_SEND_CODE_URL`
+* :py:data:`SECURITY_US_POST_SETUP_VIEW`
+
+Template Paths
+--------------
+A list of all templates:
+
+* ``SECURITY_FORGOT_PASSWORD_TEMPLATE``
+* ``SECURITY_LOGIN_USER_TEMPLATE``
+* ``SECURITY_REGISTER_USER_TEMPLATE``
+* ``SECURITY_RESET_PASSWORD_TEMPLATE``
+* ``SECURITY_CHANGE_PASSWORD_TEMPLATE``
+* ``SECURITY_SEND_CONFIRMATION_TEMPLATE``
+* ``SECURITY_SEND_LOGIN_TEMPLATE``
+* ``SECURITY_TWO_FACTOR_VERIFY_CODE_TEMPLATE``
+* ``SECURITY_TWO_FACTOR_SETUP_TEMPLATE``
+* ``SECURITY_TWO_FACTOR_VERIFY_PASSWORD_TEMPLATE``
+* :py:data:`SECURITY_US_SIGNIN_TEMPLATE`
+* :py:data:`SECURITY_US_SETUP_TEMPLATE`
+* :py:data:`SECURITY_US_VERIFY_TEMPLATE`
+
+Messages
+-------------
+
+The following are the messages Flask-Security uses. They are tuples; the first
+element is the message and the second element is the error level.
+
+The default messages and error levels can be found in ``core.py``.
+
+* ``SECURITY_MSG_ALREADY_CONFIRMED``
+* ``SECURITY_MSG_ANONYMOUS_USER_REQUIRED``
+* ``SECURITY_MSG_CONFIRMATION_EXPIRED``
+* ``SECURITY_MSG_CONFIRMATION_REQUEST``
+* ``SECURITY_MSG_CONFIRMATION_REQUIRED``
+* ``SECURITY_MSG_CONFIRM_REGISTRATION``
+* ``SECURITY_MSG_DISABLED_ACCOUNT``
+* ``SECURITY_MSG_EMAIL_ALREADY_ASSOCIATED``
+* ``SECURITY_MSG_EMAIL_CONFIRMED``
+* ``SECURITY_MSG_EMAIL_NOT_PROVIDED``
+* ``SECURITY_MSG_FAILED_TO_SEND_CODE``
+* ``SECURITY_MSG_FORGOT_PASSWORD``
+* ``SECURITY_MSG_INVALID_CODE``
+* ``SECURITY_MSG_INVALID_CONFIRMATION_TOKEN``
+* ``SECURITY_MSG_INVALID_EMAIL_ADDRESS``
+* ``SECURITY_MSG_INVALID_LOGIN_TOKEN``
+* ``SECURITY_MSG_INVALID_PASSWORD``
+* ``SECURITY_MSG_INVALID_PASSWORD_CODE``
+* ``SECURITY_MSG_INVALID_REDIRECT``
+* ``SECURITY_MSG_INVALID_RESET_PASSWORD_TOKEN``
+* ``SECURITY_MSG_LOGIN``
+* ``SECURITY_MSG_LOGIN_EMAIL_SENT``
+* ``SECURITY_MSG_LOGIN_EXPIRED``
+* ``SECURITY_MSG_PASSWORDLESS_LOGIN_SUCCESSFUL``
+* ``SECURITY_MSG_PASSWORD_BREACHED``
+* ``SECURITY_MSG_PASSWORD_BREACHED_SITE_ERROR``
+* ``SECURITY_MSG_PASSWORD_CHANGE``
+* ``SECURITY_MSG_PASSWORD_INVALID_LENGTH``
+* ``SECURITY_MSG_PASSWORD_IS_THE_SAME``
+* ``SECURITY_MSG_PASSWORD_MISMATCH``
+* ``SECURITY_MSG_PASSWORD_NOT_PROVIDED``
+* ``SECURITY_MSG_PASSWORD_NOT_SET``
+* ``SECURITY_MSG_PASSWORD_RESET``
+* ``SECURITY_MSG_PASSWORD_RESET_EXPIRED``
+* ``SECURITY_MSG_PASSWORD_RESET_REQUEST``
+* ``SECURITY_MSG_PASSWORD_TOO_SIMPLE``
+* ``SECURITY_MSG_PHONE_INVALID``
+* ``SECURITY_MSG_REAUTHENTICATION_REQUIRED``
+* ``SECURITY_MSG_REAUTHENTICATION_SUCCESSFUL``
+* ``SECURITY_MSG_REFRESH``
+* ``SECURITY_MSG_RETYPE_PASSWORD_MISMATCH``
+* ``SECURITY_MSG_TWO_FACTOR_INVALID_TOKEN``
+* ``SECURITY_MSG_TWO_FACTOR_LOGIN_SUCCESSFUL``
+* ``SECURITY_MSG_TWO_FACTOR_CHANGE_METHOD_SUCCESSFUL``
+* ``SECURITY_MSG_TWO_FACTOR_PASSWORD_CONFIRMATION_DONE``
+* ``SECURITY_MSG_TWO_FACTOR_PASSWORD_CONFIRMATION_NEEDED``
+* ``SECURITY_MSG_TWO_FACTOR_PERMISSION_DENIED``
+* ``SECURITY_MSG_TWO_FACTOR_METHOD_NOT_AVAILABLE``
+* ``SECURITY_MSG_TWO_FACTOR_DISABLED``
+* ``SECURITY_MSG_UNAUTHORIZED``
+* ``SECURITY_MSG_UNAUTHENTICATED``
+* ``SECURITY_MSG_US_METHOD_NOT_AVAILABLE``
+* ``SECURITY_MSG_US_SETUP_EXPIRED``
+* ``SECURITY_MSG_US_SETUP_SUCCESSFUL``
+* ``SECURITY_MSG_US_SPECIFY_IDENTITY``
+* ``SECURITY_MSG_USE_CODE``
+* ``SECURITY_MSG_USER_DOES_NOT_EXIST``
diff --git a/docs/contributing.rst b/docs/contributing.rst
new file mode 100644
index 0000000..3bdd7dc
--- /dev/null
+++ b/docs/contributing.rst
@@ -0,0 +1 @@
+.. include:: ../CONTRIBUTING.rst \ No newline at end of file
diff --git a/docs/customizing.rst b/docs/customizing.rst
new file mode 100644
index 0000000..d02f2a5
--- /dev/null
+++ b/docs/customizing.rst
@@ -0,0 +1,425 @@
+Customizing
+===========
+
+Flask-Security bootstraps your application with various views for handling its
+configured features to get you up and running as quickly as possible. However,
+you'll probably want to change the way these views look to be more in line with
+your application's visual design.
+
+
+Views
+-----
+
+Flask-Security is packaged with a default template for each view it presents to
+a user. Templates are located within a subfolder named ``security``. The
+following is a list of view templates:
+
+* `security/forgot_password.html`
+* `security/login_user.html`
+* `security/register_user.html`
+* `security/reset_password.html`
+* `security/change_password.html`
+* `security/send_confirmation.html`
+* `security/send_login.html`
+* `security/verify.html`
+* `security/two_factor_verify_password.html`
+* `security/two_factor_setup.html`
+* `security/two_factor_verify_code.html`
+* `security/us_signin.html`
+* `security/us_setup.html`
+* `security/us_verify.html`
+
+Overriding these templates is simple:
+
+1. Create a folder named ``security`` within your application's templates folder
+2. Create a template with the same name for the template you wish to override
+
+You can also specify custom template file paths in the :doc:`configuration <configuration>`.
+
+Each template is passed a template context object that includes the following,
+including the objects/values that are passed to the template by the main
+Flask application context processor:
+
+* ``<template_name>_form``: A form object for the view
+* ``security``: The Flask-Security extension object
+
+To add more values to the template context, you can specify a context processor
+for all views or a specific view. For example::
+
+ security = Security(app, user_datastore)
+
+ # This processor is added to all templates
+ @security.context_processor
+ def security_context_processor():
+ return dict(hello="world")
+
+ # This processor is added to only the register view
+ @security.register_context_processor
+ def security_register_processor():
+ return dict(something="else")
+
+The following is a list of all the available context processor decorators:
+
+* ``context_processor``: All views
+* ``forgot_password_context_processor``: Forgot password view
+* ``login_context_processor``: Login view
+* ``register_context_processor``: Register view
+* ``reset_password_context_processor``: Reset password view
+* ``change_password_context_processor``: Change password view
+* ``send_confirmation_context_processor``: Send confirmation view
+* ``send_login_context_processor``: Send login view
+* ``mail_context_processor``: Whenever an email will be sent
+* ``tf_setup_context_processor``: Two factor setup view
+* ``tf_token_validation_context_processor``: Two factor token validation view
+* ``tf_verify_password_context_processor``: Two factor password re-verify view
+* ``us_signin_context_processor``: Unified sign in view
+* ``us_setup_context_processor``: Unified sign in setup view
+
+
+Forms
+-----
+
+All forms can be overridden. For each form used, you can specify a
+replacement class. This allows you to add extra fields to the
+register form or override validators::
+
+ from flask_security import RegisterForm
+ from wtforms import StringField
+ from wtforms.validators import DataRequired
+
+ class ExtendedRegisterForm(RegisterForm):
+ first_name = StringField('First Name', [DataRequired()])
+ last_name = StringField('Last Name', [DataRequired()])
+
+ security = Security(app, user_datastore,
+ register_form=ExtendedRegisterForm)
+
+For the ``register_form`` and ``confirm_register_form``, only fields that
+exist in the user model are passed (as kwargs) to :meth:`.UserDatastore.create_user`.
+Thus, in the above case, the ``first_name`` and ``last_name`` fields will only
+be passed if the model looks like::
+
+ class User(db.Model, UserMixin):
+ id = db.Column(db.Integer, primary_key=True)
+ email = db.Column(db.String(255), unique=True)
+ password = db.Column(db.String(255))
+ first_name = db.Column(db.String(255))
+ last_name = db.Column(db.String(255))
+
+The following is a list of all the available form overrides:
+
+* ``login_form``: Login form
+* ``confirm_register_form``: Confirmable register form
+* ``register_form``: Register form
+* ``forgot_password_form``: Forgot password form
+* ``reset_password_form``: Reset password form
+* ``change_password_form``: Change password form
+* ``send_confirmation_form``: Send confirmation form
+* ``passwordless_login_form``: Passwordless login form
+* ``two_factor_verify_code_form``: Two-factor verify code form
+* ``two_factor_setup_form``: Two-factor setup form
+* ``two_factor_verify_password_form``: Two-factor verify password form
+* ``two_factor_rescue_form``: Two-factor help user form
+* ``us_signin_form``: Unified sign in form
+* ``us_setup_form``: Unified sign in setup form
+* ``us_setup_validate_form``: Unified sign in setup validation form
+
+.. tip::
+ Changing/extending the form class won't directly change how it is displayed.
+ You need to ALSO provide your own template and explicitly adds the new fields you want displayed.
+
+Localization
+------------
+All messages, form labels, and form strings are localizable. Flask-Security uses
+`Flask-BabelEx <https://pythonhosted.org/Flask-BabelEx/>`_ to manage its messages.
+All translations are tagged with a domain, as specified by the configuration variable
+``SECURITY_I18N_DOMAIN`` (default: "security"). For messages and labels all this
+works seamlessly. For strings inside templates it is necessary to explicitly ask for
+the "security" domain, since your application itself might have its own domain.
+Flask-Security places the method ``_fsdomain`` in jinja2's global environment.
+In order to reference a Flask-Security translation from ANY template (such as if you copied and
+modified an existing security template) just use that method::
+
+ {{ _fsdomain("Login") }}
+
+Emails
+------
+
+Flask-Security is also packaged with a default template for each email that it
+may send. Templates are located within the subfolder named ``security/email``.
+The following is a list of email templates:
+
+* `security/email/confirmation_instructions.html`
+* `security/email/confirmation_instructions.txt`
+* `security/email/login_instructions.html`
+* `security/email/login_instructions.txt`
+* `security/email/reset_instructions.html`
+* `security/email/reset_instructions.txt`
+* `security/email/reset_notice.html`
+* `security/email/change_notice.txt`
+* `security/email/change_notice.html`
+* `security/email/reset_notice.txt`
+* `security/email/welcome.html`
+* `security/email/welcome.txt`
+* `security/email/two_factor_instructions.html`
+* `security/email/two_factor_instructions.txt`
+* `security/email/two_factor_rescue.html`
+* `security/email/two_factor_rescue.txt`
+* `security/email/us_instructions.html`
+* `security/email/us_instructions.txt`
+
+Overriding these templates is simple:
+
+1. Create a folder named ``security`` within your application's templates folder
+2. Create a folder named ``email`` within the ``security`` folder
+3. Create a template with the same name for the template you wish to override
+
+Each template is passed a template context object that includes values for any
+links that are required in the email. If you require more values in the
+templates, you can specify an email context processor with the
+``mail_context_processor`` decorator. For example::
+
+ security = Security(app, user_datastore)
+
+ # This processor is added to all emails
+ @security.mail_context_processor
+ def security_mail_processor():
+ return dict(hello="world")
+
+
+Emails with Celery
+------------------
+
+Sometimes it makes sense to send emails via a task queue, such as `Celery`_.
+To delay the sending of emails, you can use the ``@security.send_mail_task``
+decorator like so::
+
+
+ from flask_mail import Message
+
+ # Setup the task
+ @celery.task
+ def send_flask_mail(**kwargs):
+ # Use the Flask-Mail extension instance to send the incoming ``msg`` parameter
+ # which is an instance of `flask_mail.Message`
+ mail.send(Message(**kwargs))
+
+ @security.send_mail_task
+ def delay_flask_security_mail(msg):
+ send_flask_mail.delay(
+ subject=msg.subject,
+ sender=msg.sender,
+ recipients=msg.recipients,
+ body=msg.body,
+ html=msg.html,
+ )
+
+If factory method is going to be used for initialization, use ``_SecurityState``
+object returned by ``init_app`` method to initialize Celery tasks instead of using
+``security.send_mail_task`` directly like so::
+
+ from flask import Flask
+ from flask_mail import Mail, Message
+ from flask_security import Security, SQLAlchemyUserDatastore
+ from celery import Celery
+
+ mail = Mail()
+ security = Security()
+ celery = Celery()
+
+ def create_app(config):
+ """Initialize Flask instance."""
+
+ app = Flask(__name__)
+ app.config.from_object(config)
+
+ @celery.task
+ def send_flask_mail(**kwargs):
+ mail.send(Message(**kwargs))
+
+ mail.init_app(app)
+ datastore = SQLAlchemyUserDatastore(db, User, Role)
+ security_ctx = security.init_app(app, datastore)
+
+ # Flexible way for defining custom mail sending task.
+ @security_ctx.send_mail_task
+ def delay_flask_security_mail(msg):
+ send_flask_mail.delay(
+ subject=msg.subject,
+ sender=msg.sender,
+ recipients=msg.recipients,
+ body=msg.body,
+ html=msg.html,
+ )
+
+ # A shortcut.
+ security_ctx.send_mail_task(send_flask_mail.delay)
+
+ return app
+
+Note that ``flask_mail.Message`` may not be serialized as an argument passed to
+Celery. The practical way with custom serialization may look like so::
+
+ @celery.task
+ def send_flask_mail(**kwargs):
+ mail.send(Message(**kwargs))
+
+ @security_ctx.send_mail_task
+ def delay_flask_security_mail(msg):
+ send_flask_mail.delay(subject=msg.subject, sender=msg.sender,
+ recipients=msg.recipients, body=msg.body,
+ html=msg.html)
+
+.. _Celery: http://www.celeryproject.org/
+
+
+Custom send_mail method
+-----------------------
+
+It's also possible to completely override the ``security.send_mail`` method to
+implement your own logic.
+
+For example, you might want to use an alternative email library like `Flask-Emails`::
+
+ from flask import Flask
+ from flask_security import Security, SQLAlchemyUserDatastore
+ from flask_emails import Message
+
+ def create_app(config):
+ """Initialize Flask instance."""
+
+ app = Flask(__name__)
+ app.config.from_object(config)
+
+ def custom_send_mail(subject, recipient, template, **context):
+ ctx = ('security/email', template)
+ message = Message(
+ subject=subject,
+ html=_security.render_template('%s/%s.html' % ctx, **context))
+ message.send(mail_to=[recipient])
+
+ datastore = SQLAlchemyUserDatastore(db, User, Role)
+ Security(app, datastore, send_mail=custom_send_mail)
+
+ return app
+
+.. note::
+
+ The above ``security.send_mail_task`` override will be useless if you
+ override the entire ``send_mail`` method.
+
+.. _responsetopic:
+
+Responses
+---------
+Flask-Security will likely be a very small piece of your application,
+so Flask-Security makes it easy to override all aspects of API responses.
+
+JSON Response
++++++++++++++
+Applications that support a JSON based API need to be able to have a uniform
+API response. Flask-Security has a default way to render its API responses - which can
+be easily overridden by providing a callback function via :meth:`.Security.render_json`.
+As documented in :meth:`.Security.render_json`, be aware that Flask-Security registers
+its own JsonEncoder on its blueprint.
+
+401, 403, Oh My
++++++++++++++++
+For a very long read and discussion; look at `this`_. Out of the box, Flask-Security in
+tandem with Flask-Login, behaves as follows:
+
+ * If authentication fails as the result of a `@login_required`, `@auth_required`,
+ `@http_auth_required`, or `@token_auth_required` then if the request 'wants' a JSON
+ response, :meth:`.Security.render_json` is called with a 401 status code. If not
+ then flask_login.LoginManager.unauthorized() is called. By default THAT will redirect to
+ a login view.
+
+ * If authorization fails as the result of `@roles_required`, `@roles_accepted`,
+ `@permissions_required`, or `@permissions_accepted`, then if the request 'wants' a JSON
+ response, :meth:`.Security.render_json` is called with a 403 status code. If not,
+ then if *SECURITY_UNAUTHORIZED_VIEW* is defined, the response will redirected.
+ If *SECURITY_UNAUTHORIZED_VIEW* is not defined, then ``abort(403)`` is called.
+
+All this can be easily changed by registering any or all of :meth:`.Security.render_json`,
+:meth:`.Security.unauthn_handler` and :meth:`.Security.unauthz_handler`.
+
+The decision on whether to return JSON is based on:
+
+ * Was the request content-type "application/json" (e.g. request.is_json()) OR
+
+ * Is the 'best' value of the ``Accept`` HTTP header "application/json"
+
+
+.. _`this`: https://stackoverflow.com/questions/3297048/403-forbidden-vs-401-unauthorized-http-responses
+
+Authorization with OAuth2
+-------------------------
+
+Flask-Security can be set up to co-operate with `Flask-OAuthlib`_,
+by implementing a custom request loader that authorizes a user based
+either on a `Bearer` token in the HTTP `Authorization` header, or on the
+Flask-Security standard authorization logic::
+
+ from flask_oauthlib.provider import OAuth2Provider
+ from flask_security import AnonymousUser
+ from flask_security.core import (
+ _user_loader as _flask_security_user_loader,
+ _request_loader as _flask_security_request_loader)
+ from flask_security.utils import config_value as security_config_value
+
+ oauth = OAuth2Provider(app)
+
+ def _request_loader(request):
+ """
+ Load user from OAuth2 Authentication header or using
+ Flask-Security's request loader.
+ """
+ user = None
+
+ if hasattr(request, 'oauth'):
+ user = request.oauth.user
+ else:
+ # Need this try stmt in case oauthlib sometimes throws:
+ # AttributeError: dict object has no attribute startswith
+ try:
+ is_valid, oauth_request = oauth.verify_request(scopes=[])
+ if is_valid:
+ user = oauth_request.user
+ except AttributeError:
+ pass
+
+ if not user:
+ user = _flask_security_request_loader(request)
+
+ return user
+
+ def _get_login_manager(app, anonymous_user):
+ """Prepare a login manager for Flask-Security to use."""
+ login_manager = LoginManager()
+
+ login_manager.anonymous_user = anonymous_user or AnonymousUser
+ login_manager.login_view = '{0}.login'.format(
+ security_config_value('BLUEPRINT_NAME', app=app))
+ login_manager.user_loader(_flask_security_user_loader)
+ login_manager.request_loader(_request_loader)
+
+ if security_config_value('FLASH_MESSAGES', app=app):
+ (login_manager.login_message,
+ login_manager.login_message_category) = (
+ security_config_value('MSG_LOGIN', app=app))
+ (login_manager.needs_refresh_message,
+ login_manager.needs_refresh_message_category) = (
+ security_config_value('MSG_REFRESH', app=app))
+ else:
+ login_manager.login_message = None
+ login_manager.needs_refresh_message = None
+
+ login_manager.init_app(app)
+ return login_manager
+
+ security = Security(
+ app, user_datastore,
+ login_manager=_get_login_manager(app, anonymous_user=None))
+
+
+.. _Flask-OAuthlib: https://flask-oauthlib.readthedocs.io/
diff --git a/docs/features.rst b/docs/features.rst
new file mode 100644
index 0000000..a34cff1
--- /dev/null
+++ b/docs/features.rst
@@ -0,0 +1,244 @@
+Features
+========
+
+Flask-Security allows you to quickly add common security mechanisms to your
+Flask application. They include:
+
+
+Session Based Authentication
+----------------------------
+
+Session based authentication is fulfilled entirely by the `Flask-Login`_
+extension. Flask-Security handles the configuration of Flask-Login automatically
+based on a few of its own configuration values and uses Flask-Login's
+`alternative token`_ feature for remembering users when their session has
+expired. Flask-Security uses ``fs_uniquifier`` from its Token Authentication
+Feature (see below) to implement Flask-Login's `alternative token`_. `Flask-WTF`_
+integrates with the session as well to provide out of the box CSRF support.
+Flask-Security extends that to support requiring CSRF for requests that are
+authenticated via session cookies, but not for requests authenticated using tokens.
+
+
+Role/Identity Based Access
+--------------------------
+
+Flask-Security implements very basic role management out of the box. This means
+that you can associate a high level role or multiple roles to any user. For
+instance, you may assign roles such as `Admin`, `Editor`, `SuperUser`, or a
+combination of said roles to a user. Access control is based on the role name and/or
+permissions contained within the role;
+and all roles should be uniquely named. This feature is implemented using the
+`Flask-Principal`_ extension. As with basic RBAC, permissions can be assigned to roles
+to provide more granular access control. Permissions can be associated with one or
+more roles (the RoleModel contains a list of permissions). The values of
+permissions are completely up to the developer - Flask-Security simply treats them
+as strings.
+If you'd like to implement even more granular access
+control (such as per-object), you can refer to the Flask-Principal `documentation on this topic`_.
+
+
+Password Hashing
+----------------
+
+Password hashing is enabled with `passlib`_. Passwords are hashed with the
+`bcrypt`_ function by default but you can easily configure the hashing
+algorithm. You should **always use a hashing algorithm** in your production
+environment. Hash algorithms not listed in ``SECURITY_PASSWORD_SINGLE_HASH``
+will be double hashed - first an HMAC will be computed, then the selected hash
+function will be used. In this case - you must provide a ``SECURITY_PASSWORD_SALT``.
+A good way to generate this is::
+
+ secrets.SystemRandom().getrandbits(128)
+
+Bear in mind passlib does not assume which
+algorithm you will choose and may require additional libraries to be installed.
+
+Password Validation and Complexity
+-----------------------------------
+Consult :ref:`pass_validation_topic`.
+
+
+Basic HTTP Authentication
+-------------------------
+
+Basic HTTP authentication is achievable using a simple view method decorator.
+This feature expects the incoming authentication information to identify a user
+in the system. This means that the username must be equal to their email address.
+
+
+Token Authentication
+--------------------
+
+Token based authentication is enabled by retrieving the user auth token by
+performing an HTTP POST with a query param of ``include_auth_token`` with the authentication details
+as JSON data against the
+authentication endpoint. A successful call to this endpoint will return the
+user's ID and their authentication token. This token can be used in subsequent
+requests to protected resources. The auth token is supplied in the request
+through an HTTP header or query string parameter. By default the HTTP header
+name is `Authentication-Token` and the default query string parameter name is
+`auth_token`. Authentication tokens are generated using a uniquifier field in the
+user's UserModel. If that field is changed (via :meth:`.UserDatastore.set_uniquifier`)
+then any existing authentication tokens will no longer be valid. Changing
+the user's password will not affect tokens.
+
+Note that prior to release 3.3.0 or if the UserModel doesn't contain the ``fs_uniquifier``
+attribute the authentication tokens are generated using the user's password.
+Thus if the user changes his or her password their existing authentication token
+will become invalid. A new token will need to be retrieved using the user's new
+password. Verifying tokens created in this way is very slow.
+
+Two-factor Authentication (alpha)
+----------------------------------------
+Two-factor authentication is enabled by generating time-based one time passwords
+(Tokens). The tokens are generated using the users `totp secret`_, which is unique
+per user, and is generated both on first login, and when changing the two-factor
+method (doing this causes the previous totp secret to become invalid). The token
+is provided by one of 3 methods - email, sms (service is not provided), or
+an authenticator app such as Google Authenticator, LastPass Authenticator, or Authy.
+By default, tokens provided by the authenticator app are
+valid for 2 minutes, tokens sent by mail for up to 5 minute and tokens sent by
+sms for up to 2 minutes. The QR code used to supply the authenticator app with
+the secret is generated using the PyQRCode library.
+This feature is marked alpha meaning that backwards incompatible changes
+might occur during minor releases. While the feature is operational, it has these
+known limitations:
+
+ * Limited and incomplete JSON support
+ * Not enough documentation to use w/o looking at code
+
+.. _unified-sign-in:
+
+Unified Sign In
+---------------
+**This feature is in Beta - mostly due to it being brand new and little to no production soak time**
+
+Unified sign in provides a generalized login endpoint that takes an `identity`
+and a `passcode`; where (based on configuration):
+
+ * `identity` is any of :py:data:`SECURITY_USER_IDENTITY_ATTRIBUTES` (e.g. email, username, phone)
+ * `passcode` is a password or a one-time code (delivered via email, SMS, or authenticator app)
+
+Please see this `Wikipedia`_ article about multi-factor authentication.
+
+Using this feature, it is possible to not require the user to have a stored password
+at all, and just require the use of a one-time code. The mechanisms for generating
+and delivering the one-time code are similar to common two-factor mechanisms.
+
+This one-time code can be configured to be delivered via email, SMS or authenticator app -
+however be aware that NIST does not recommend email for this purpose (though many web sites do so)
+due to the fact that a) email may travel through
+many different servers as part of being delivered - and b) is available from any device.
+
+Using SMS or an authenticator app means you are providing "something you have" (the mobile device)
+and either "something you know" (passcode to unlock your device)
+or "something you are" (biometric passcode to unlock your device).
+This effectively means that using a one-time code to sign in, is in fact already two-factor (if using
+SMS or authenticator app). Many large authentication providers already offer this - here is
+`Microsoft's`_ version.
+
+Note that by configuring :py:data:`SECURITY_US_ENABLED_METHODS` an application can
+use this endpoint JUST with identity/password or in fact disallow passwords altogether.
+
+Unified sign in is integrated with two-factor authentication. Since in general
+there is no need for a second factor if the initial authentication was with SMS or
+an authenticator application, the :py:data:`SECURITY_US_MFA_REQUIRED` configuration
+determines which primary authentication mechanisms require a second factor. By default
+limited to ``email`` and ``password`` (if two-factor is enabled).
+
+Be aware that by default, the :py:data:`SECURITY_US_SETUP_URL` endpoint is protected
+with a freshness check (see :meth:`flask_security.auth_required`) which means it requires a session
+cookie to function properly. This is true even if using JSON payload or token authentication.
+If you disable the freshness check then sessions aren't required.
+
+`Current Limited Functionality`:
+
+ * Change password does not work if a user registers without a password. However
+ forgot-password will allow the user to set a new password.
+ * Registration and Confirmation only work with email - so while you can enable multiple
+ authentication methods, you still have to register with email.
+
+Email Confirmation
+------------------
+
+If desired you can require that new users confirm their email address.
+Flask-Security will send an email message to any new users with a confirmation
+link. Upon navigating to the confirmation link, the user will be automatically
+logged in. There is also view for resending a confirmation link to a given email
+if the user happens to try to use an expired token or has lost the previous
+email. Confirmation links can be configured to expire after a specified amount
+of time.
+
+
+Password Reset/Recovery
+-----------------------
+
+Password reset and recovery is available for when a user forgets his or her
+password. Flask-Security sends an email to the user with a link to a view which
+they can reset their password. Once the password is reset they are automatically
+logged in and can use the new password from then on. Password reset links can
+be configured to expire after a specified amount of time.
+
+
+User Registration
+-----------------
+
+Flask-Security comes packaged with a basic user registration view. This view is
+very simple and new users need only supply an email address and their password.
+This view can be overridden if your registration process requires more fields.
+
+
+Login Tracking
+--------------
+
+Flask-Security can, if configured, keep track of basic login events and
+statistics. They include:
+
+* Last login date
+* Current login date
+* Last login IP address
+* Current login IP address
+* Total login count
+
+
+JSON/Ajax Support
+-----------------
+
+Flask-Security supports JSON/Ajax requests where appropriate. Please
+look at :ref:`csrftopic` for details on how to work with JSON and
+Single Page Applications. More specifically
+JSON is supported for the following operations:
+
+* Login requests
+* Unified sign in requests
+* Registration requests
+* Change password requests
+* Confirmation requests
+* Forgot password requests
+* Passwordless login requests
+* Two-factor login requests
+* Change two-factor method requests
+
+In addition, Single-Page-Applications (like those built with Vue, Angular, and
+React) are supported via customizable redirect links.
+
+Command Line Interface
+----------------------
+
+Basic `Click`_ commands for managing users and roles are automatically
+registered. They can be completely disabled or their names can be changed.
+Run ``flask --help`` and look for users and roles.
+
+
+.. _Click: https://palletsprojects.com/p/click/
+.. _Flask-Login: https://flask-login.readthedocs.org/en/latest/
+.. _Flask-WTF: https://flask-wtf.readthedocs.io/en/stable/csrf.html
+.. _alternative token: https://flask-login.readthedocs.io/en/latest/#alternative-tokens
+.. _Flask-Principal: https://pypi.org/project/Flask-Principal/
+.. _documentation on this topic: http://packages.python.org/Flask-Principal/#granular-resource-protection
+.. _passlib: https://passlib.readthedocs.io/en/stable/
+.. _totp secret: https://passlib.readthedocs.io/en/stable/narr/totp-tutorial.html#overview
+.. _bcrypt: https://en.wikipedia.org/wiki/Bcrypt
+.. _PyQRCode: https://pypi.python.org/pypi/PyQRCode/
+.. _Wikipedia: https://en.wikipedia.org/wiki/Multi-factor_authentication
+.. _Microsoft's: https://docs.microsoft.com/en-us/azure/active-directory/user-help/user-help-auth-app-overview
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 0000000..b6eb3a8
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,89 @@
+.. rst-class:: hide-header
+
+
+Welcome to Flask-Security
+=========================
+
+.. image:: _static/logo-owl-full-240.png
+ :alt: Flask-Security: add a drop of security to your Flask application.
+ :align: left
+ :width: 100%
+ :target: https://github.com/Flask-Middleware/flask-security
+
+
+Flask-Security allows you to quickly add common security mechanisms to your
+Flask application. They include:
+
+1. Session based authentication
+2. Role and Permission management
+3. Password hashing
+4. Basic HTTP authentication
+5. Token based authentication
+6. Token based account activation (optional)
+7. Token based password recovery / resetting (optional)
+8. Two-factor authentication (optional/alpha)
+9. Unified sign in (optional)
+10. User registration (optional)
+11. Login tracking (optional)
+12. JSON/Ajax Support
+
+Many of these features are made possible by integrating various Flask extensions
+and libraries. They include:
+
+1. `Flask-Login <https://flask-login.readthedocs.org/en/latest/>`_
+2. `Flask-Mail <https://pypi.org/project/Flask-Mail/>`_
+3. `Flask-Principal <https://pypi.org/project/Flask-Principal/>`_
+4. `Flask-WTF <https://pypi.org/project/Flask-WTF/>`_
+5. `itsdangerous <https://pypi.org/project/itsdangerous/>`_
+6. `passlib <https://pypi.org/project/passlib/>`_
+7. `PyQRCode <https://pypi.org/project/PyQRCode/>`_
+
+Additionally, it assumes you'll be using a common library for your database
+connections and model definitions. Flask-Security supports the following Flask
+extensions out of the box for data persistence:
+
+1. `Flask-SQLAlchemy <http://pypi.python.org/pypi/flask-sqlalchemy/>`_
+2. `Flask-MongoEngine <http://pypi.python.org/pypi/flask-mongoengine/>`_
+3. `Peewee Flask utils <http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#flask-utils>`_
+4. `PonyORM <http://pypi.python.org/pypi/pony/>`_
+
+
+Getting Started
+----------------
+
+.. toctree::
+ :maxdepth: 2
+
+ features
+ configuration
+ quickstart
+ models
+
+Customizing and Usage Patterns
+-------------------------------
+
+.. toctree::
+ :maxdepth: 2
+
+ customizing
+ two_factor_configurations
+ spa
+ patterns
+
+API
+---
+
+.. toctree::
+ :maxdepth: 2
+
+ api
+
+Additional Notes
+----------------
+
+.. toctree::
+ :maxdepth: 2
+
+ contributing
+ changelog
+ authors
diff --git a/docs/models.rst b/docs/models.rst
new file mode 100644
index 0000000..91c0e3a
--- /dev/null
+++ b/docs/models.rst
@@ -0,0 +1,129 @@
+Models
+======
+
+Flask-Security assumes you'll be using libraries such as SQLAlchemy,
+MongoEngine, Peewee or PonyORM to define a data model that includes a `User`
+and `Role` model. The fields on your models must follow a particular convention
+depending on the functionality your app requires. Aside from this, you're free
+to add any additional fields to your model(s) if you want.
+
+As more features are added to Flask-Security, the list of required fields and tables grow.
+As you use these features, and therefore use these fields and tables, database migrations are required;
+which are a bit of a pain. To make things easier - Flask-Security includes mixins that
+contain ALL the fields and tables required for all features. They also contain
+various `best practice` fields - such as update and create times. These mixins can
+be easily extended to add any sort of custom fields and can be found in the
+`models` module (today there is just one for using Flask-SqlAlchemy).
+
+The provided models are versioned since they represent actual DB models, and any
+changes require a schema migration (and perhaps a data migration). Applications
+must specifically import the version they want (and handle any required migration).
+
+At the bare minimum
+your `User` and `Role` model should include the following fields:
+
+**User**
+
+* ``id`` (primary key - integer, string, or uuid)
+* ``email`` (for most features - unique, non-nullable)
+* ``password`` (non-nullable)
+* ``active`` (boolean, non-nullable)
+* ``fs_uniquifier`` (unique, non-nullable)
+
+
+**Role**
+
+* ``id`` (primary key - integer)
+* ``name`` (unique, non-nullable)
+* ``description`` (string)
+
+
+Additional Functionality
+------------------------
+
+Depending on the application's configuration, additional fields may need to be
+added to your `User` model.
+
+Confirmable
+^^^^^^^^^^^
+
+If you enable account confirmation by setting your application's
+`SECURITY_CONFIRMABLE` configuration value to `True`, your `User` model will
+require the following additional field:
+
+* ``confirmed_at`` (datetime)
+
+Trackable
+^^^^^^^^^
+
+If you enable user tracking by setting your application's `SECURITY_TRACKABLE`
+configuration value to `True`, your `User` model will require the following
+additional fields:
+
+* ``last_login_at`` (datetime)
+* ``current_login_at`` (datetime)
+* ``last_login_ip`` (string)
+* ``current_login_ip`` (string)
+* ``login_count`` (integer)
+
+Two_Factor
+^^^^^^^^^^
+
+If you enable two-factor by setting your application's `SECURITY_TWO_FACTOR`
+configuration value to `True`, your `User` model will require the following
+additional fields:
+
+* ``tf_totp_secret`` (string)
+* ``tf_primary_method`` (string)
+
+If you include 'sms' in `SECURITY_TWO_FACTOR_ENABLED_METHODS`, your `User` model
+will require the following additional field:
+
+* ``tf_phone_number`` (string)
+
+Unified Sign In
+^^^^^^^^^^^^^^^
+
+If you enable unified sign in by setting your application's :py:data:`SECURITY_UNIFIED_SIGNIN`
+configuration value to `True`, your `User` model will require the following
+additional fields:
+
+* ``us_totp_secrets`` (an arbitrarily long Text field)
+
+If you include 'sms' in :py:data:`SECURITY_US_ENABLED_METHODS`, your `User` model
+will require the following additional field:
+
+* ``us_phone_number`` (string)
+
+Permissions
+^^^^^^^^^^^
+If you want to protect endpoints with permissions, and assign permissions to roles
+that are then assigned to users the Role model requires:
+
+* ``permissions`` (UnicodeText)
+
+Custom User Payload
+^^^^^^^^^^^^^^^^^^^
+
+If you want a custom payload for JSON API responses, define
+the method `get_security_payload` in your User model. The method must return a
+serializable object:
+
+.. code-block:: python
+
+ class User(db.Model, UserMixin):
+ id = db.Column(db.Integer, primary_key=True)
+ email = TextField()
+ password = TextField()
+ active = BooleanField(default=True)
+ confirmed_at = DateTimeField(null=True)
+ name = db.Column(db.String(80))
+
+ # Custom User Payload
+ def get_security_payload(self):
+ return {
+ 'id': self.id,
+ 'name': self.name,
+ 'email': self.email
+ }
+
diff --git a/docs/patterns.rst b/docs/patterns.rst
new file mode 100644
index 0000000..ddcce61
--- /dev/null
+++ b/docs/patterns.rst
@@ -0,0 +1,312 @@
+Security Patterns
+=================
+
+Authentication and Authorization
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Flask-Security provides a set of authentication decorators:
+
+ * :func:`.auth_required`
+
+ * :func:`.http_auth_required`
+
+ * :func:`.auth_token_required`
+
+and a set of authorization decorators:
+
+ * :func:`.roles_required`
+
+ * :func:`.roles_accepted`
+
+ * :func:`.permissions_required`
+
+ * :func:`.permissions_accepted`
+
+In addition, Flask-Login provides @login_required. In order to take advantage of all the
+Flask-Security features, it is recommended to NOT use @login_required.
+
+Also, if you annotate your endpoints with JUST an authorization decorator, you will never
+get a 401 response, and (for forms) you won't be redirected to your login page. In this case
+you will always get a 403 status code (assuming you don't override the default handlers).
+
+While these annotations are quick and easy, it is likely that they won't completely satisfy
+all an application's authorization requirements. A common example might be that a user can
+only edit their own posts/documents. In cases like this - it is nice to have a uniform way
+of handling all authorization errors. A simple way to do this is to use a special exception
+class that you can raise either in response to Flask-Security authorization failures, or in your
+own code. Then use Flask's ``errorhandler`` to catch that exception and create the appropriate API response::
+
+ Class MyForbiddenException(Exception):
+ def __init__(self, msg='Not permitted with your privileges', status=http.HTTPStatus.FORBIDDEN):
+ self.info = {'status': status, 'msgs': [msg]}
+
+ _security = app.extensions["security"]
+
+ @_security.unauthz_handler
+ def my_unauthz_handler(func, params):
+ raise MyForbiddenException()
+
+ @app.errorhandler(MyForbiddenException)
+ def my_exception(ex):
+ return flask.jsonify(ex.info), ex.info['status']
+
+ @app.route('/doc/<int:doc_id>', methods=['PATCH'])
+ @auth_required('token', 'session')
+ def doc_patch(doc_id):
+ doc = fetch_doc(doc_id)
+ if not current_user.has_role('admin') and doc.owner != current_user:
+ raise MyForbiddenException(msg='You can only update docs you own')
+
+
+Freshness
+++++++++++
+A common pattern for browser-based sites is to use sessions to manage identity. This is usually
+implemented using session cookies. These cookies expire once the session (browser tab) is closed. This is very
+convenient, and keep the users from having to constantly re-authenticate. The downside is that sessions can easily be
+open for days or weeks. This adds to the security risk that some bad-actor or XSS gets control of the browser and then can
+do anything the user can. To mitigate that, operations that change fundamental identity characteristics (such as email, password, etc.)
+can be protected by requiring a 'fresh' or recent authentication. Flask-Security supports this with the following:
+
+ - :func:`.auth_required` takes parameters that define how recent the authentication must have happened. In addition a grace
+ period can be specified so that multiple step operations don't require re-authentication in the middle.
+ - A default :meth:`.Security.reauthn_handler` that is called when a request fails the recent authentication check.
+ - :py:data:`SECURITY_VERIFY_URL` and :py:data:`SECURITY_US_VERIFY_URL` endpoints that request the user to re-authenticate
+ - VerifyForm and UsVerifyForm forms that can be extended.
+
+Flask-Security itself uses this as part of securing the :ref:`unified-sign-in` setup endpoint.
+
+.. _pass_validation_topic:
+
+Password Validation and Complexity
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+There is a large body of references (and endless discussions) around how to get users to create
+good passwords. The `OWASP Authenication cheatsheet <https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html>`_
+is a useful place to start. Flask-Security has a default password validator that:
+
+ * Checks for minimum and maximum length (minimum is configurable via :py:data:`SECURITY_PASSWORD_LENGTH_MIN`).
+ The default is 8 characters as defined by `NIST <https://pages.nist.gov/800-63-3/sp800-63b.html>`_.
+ * If :py:data:`SECURITY_PASSWORD_CHECK_BREACHED` is set, will use the API for `haveibeenpwned <https://haveibeenpwned.com>`_ to
+ check if the password is on a list of breached passwords. The configuration variable :py:data:`SECURITY_PASSWORD_BREACHED_COUNT`
+ can be used to set the minimum allowable 'breaches'.
+ * If :py:data:`SECURITY_PASSWORD_COMPLEXITY_CHECKER` is set to ``zxcvbn`` and the
+ package `zxcvbn <https://pypi.org/project/zxcvbn/>`_ is installed, it will check the password for complexity.
+
+Be aware that ``zxcvbn`` is not actively being maintained, and has localization issues.
+
+The entire validator can be easily changed by supplying a :meth:`.Security.password_validator`.
+This enables application to e.g. use any piece of the UserModel (which is a parameter) as part of validation.
+A custom validator can still call the underlying methods where appropriate:
+:func:`flask_security.password_length_validator`, :func:`flask_security.password_complexity_validator`,
+and :func:`flask_security.password_breached_validator`.
+
+.. _csrftopic:
+
+CSRF
+~~~~
+By default, Flask-Security, via Flask-WTForms protects all form based POSTS
+from CSRF attacks using well vetted per-session hidden-form-field csrf-tokens.
+
+Any web application that relies on session cookies for authentication must have CSRF protection.
+For more details please read this `OWASP CSRF cheatsheet <https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.md>`_.
+A couple important take-aways - first - it isn't about forms versus JSON - it is about
+how the API is authenticated (session cookies versus authentication token). Second there is the
+concern about 'login CSRF' - is protection needed prior to authentication (yes if
+you have a really secure/popular site).
+
+Flask-Security strives to support various options for both its endpoints (e.g. ``/login``)
+and the application endpoints (protected with Flask-Security decorators such as :func:`.auth_required`).
+
+If your application just uses forms that are derived from ``Flask-WTF::Flaskform`` - you are done.
+
+
+CSRF: Single-Page-Applications and AJAX/XHR
+++++++++++++++++++++++++++++++++++++++++++++
+If you are thinking about using authentication tokens in your browser-based UI - read
+`this article <https://stormpath.com/blog/where-to-store-your-jwts-cookies-vs-html5-web-storage>`_
+on how and where to store authentication tokens. While the
+article is talking about JWT it applies to Flask-Security tokens as well.
+
+In general, it is considered more secure (and easier) to use sessions for browser
+based UI, and tokens for service to service and scripts.
+
+For SPA, and especially those that aren't served via your flask application, there are difficulties
+with actually retrieving and using a CSRF token. There are 2 normal ways to do this:
+
+ * Have the csrf-token available via a JSON GET request that can be attached as a
+ header in every mutating request.
+ * Have a cookie that can be read via javascript whose value is the csrf-token that
+ can be attached as a header in every mutating request.
+
+Flask-Security supports both solutions.
+
+Explicit fetch and send of csrf-token
+--------------------------------------
+The current session CSRF token
+is returned on every JSON GET request (to a Flask-Security endpoint) as ``response['csrf_token`]``.
+For web applications that ARE served via flask, it is even easier to get the csrf-token -
+`<https://flask-wtf.readthedocs.io/en/stable/csrf.html>`_ gives some useful tips.
+
+Armed with the csrf-token, the UI must include that in every mutating operation.
+Be careful NOT to include the csrf-token in non-mutating requests (such as GETs).
+If your application uses GET to actually modify state - please stop.
+
+An example using `axios <https://github.com/axios/axios>`_ ::
+
+
+ # This will fetch the csrf-token. Note that we do a GET on the login endpoint
+ # which will get us the csrf-token even though we aren't yet logged in.
+ # Note further the 'data: null' and explicit Content-Type header - these are
+ # critical, otherwise Flask-Security will return the login form.
+ axios.get('/login',{data: null, headers: {'Content-Type': 'application/json'}}).then(function (resp) {
+ csrf_token = resp.data['response']['csrf_token']
+ })
+
+
+ # This will add the token header to each outgoing mutating request.
+ axios.interceptors.request.use(function (config) {
+ if (["post", "delete", "patch", "put"].includes(config["method"])) {
+ if (csrf_token !== '') {
+ config.headers["X-CSRF-Token"] = csrf_token
+ }
+ }
+ return config;
+ }, function (error) {
+ // Do something with request error
+ return Promise.reject(error);
+ });
+
+
+
+Note that we use the header name ``X-CSRF-Token`` as that is one of the default
+headers configured in Flask-WTF (*WTF_CSRF_HEADERS*)
+
+To protect your application's endpoints (that presumably are not using Flask forms),
+you need to enable CSRF as described in the FlaskWTF `documentation <https://flask-wtf.readthedocs.io/en/stable/csrf.html>`_: ::
+
+ flask_wtf.CSRFProtect(app)
+
+This will turn on CSRF protection on ALL endpoints, including Flask-Security. This protection differs slightly from
+the default that is part of FlaskForm in that it will first look at the request body and see if it can find a form field that contains
+the csrf-token, and if it can't, it will check if the request has a header that is listed in *WTF_CSRF_HEADERS* and use that.
+Be aware that if you enable this it will ONLY work if you send the session cookie on each request.
+
+Using a Cookie
+--------------
+You can instruct Flask-Security to send a cookie that contains the csrf token. This can be very
+convenient since various javascript AJAX packages are pre-configured to extract the contents of a cookie
+and send it on every mutating request as an HTTP header. `axios`_ for example has a default configuration
+that it will look for a cookie named ``XSRF-TOKEN`` and will send the contents of that back
+in an HTTP header called ``X-XSRF-Token``. This means that if you use that package you don't need to make
+any changes to your UI and just need the following configuration::
+
+ # Have cookie sent
+ app.config["SECURITY_CSRF_COOKIE"] = {"key": "XSRF-TOKEN"}
+
+ # Don't have csrf tokens expire (they are invalid after logout)
+ app.config["WTF_CSRF_TIME_LIMIT"] = None
+
+ # You can't get the cookie until you are logged in.
+ app.config["SECURITY_CSRF_IGNORE_UNAUTH_ENDPOINTS"] = True
+
+ # Enable CSRF protection
+ flask_wtf.CSRFProtect(app)
+
+Angular's `httpClient`_ also supports this.
+
+For React based project you are free to choose your http client. It bundles fetch though. Retrieving the token is easy::
+
+ fetch(url, {
+ credentials: 'include',
+ mode: 'cors',
+ headers: {
+ 'Accept': 'application/json',
+ 'X-XSRF-TOKEN': getCookieValue('XSRF-TOKEN')
+ }
+ });
+
+Sending the token on every, mutating, request is something that you should implement yourself. As an example an API call to an API
+endpoint that does CSRF validation::
+
+ function addUser(details) {
+ return fetch('https://api.example.com/user', {
+ mode: 'cors',
+ method: 'POST',
+ credentials: 'include',
+ body: JSON.stringify(details),
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json',
+ 'X-XSRF-TOKEN': getCookieValue('XSRF-TOKEN')
+ }
+ }).then(response => {
+ return response.json().then(data => {
+ if (response.ok) {
+ return data;
+ } else {
+ return Promise.reject({status: response.status, data});
+ }
+ });
+ });
+ }
+
+When you have axios setup correctly, this is a lot easier::
+
+ function addUser(details) {
+ return axios.post('https://api.example.com/user', details);
+ }
+
+
+CSRF: Enable protection for session auth, but not token auth
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+As mentioned above, CSRF is critical for any mutating operation where the authentication credentials are 'invisibly' sent - such as a session cookie -
+from a browser. But if your endpoint a) can only be authenticated with an attached token or b) can be called either via session OR token;
+it is often desirable not to force token API users to deal with CSRF. To solve this, we need to keep CSRFProtect from checking the csrf-token early in the
+request and instead defer that decision to later decorators/code. Flask-Security's authentication decorators (:func:`.auth_required`,
+:func:`.auth_token_required`, and :func:`.http_auth_required` all support calling csrf protection based on configuration::
+
+ # Disable pre-request CSRF
+ app.config[WTF_CSRF_CHECK_DEFAULT] = False
+
+ # Check csrf for session and http auth (but not token)
+ app.config[SECURITY_CSRF_PROTECT_MECHANISMS] = ["session", "basic"]
+
+ # Enable CSRF protection
+ flask_wtf.CSRFProtect(app)
+
+ @app.route("/")
+ @auth_required("token", "session")
+ def home_page():
+
+With this configuration, CSRF won't be required if the caller uses an authentication token, but if it uses
+the session cookie it will.
+
+CSRF: Pro-Tips
+++++++++++++++
+ #) Be aware that for CSRF to work, callers MUST send the session cookie. So
+ for pure API (token based), and no session cookie - there is no way to support 'login CSRF'.
+ So your app must set :py:data:`SECURITY_CSRF_IGNORE_UNAUTH_ENDPOINTS`
+ (or clients must use CSRF/session cookie for logging
+ in then once they have an authentication token, no further need for cookie).
+
+ #) If you enable CSRFProtect(app) and you want to support non-form based JSON requests,
+ then you must include the CSRF token in the header (e.g. X-CSRF-Token)
+
+ #) You must enable CSRFProtect(app) if you want to accept the CSRF token in the request
+ header.
+
+ #) Annotate each of your endpoints with a @auth_required decorator (and don't rely
+ on just a @role_required or @login_required decorator) so that Flask-Security
+ gets control at the appropriate place.
+
+ #) If you can't use a decorator, Flask-Security exposes the underlying method
+ :func:`flask_security.handle_csrf`.
+
+ #) Consider starting by setting :py:data:`SECURITY_CSRF_IGNORE_UNAUTH_ENDPOINTS` to True. Your
+ application likely doesn't need 'login CSRF' protection, and it is frustrating
+ to not even be able to login via API!
+
+ #) If you have unauthenticated endpoints that you want to protect with CSRF then
+ use the :func:`flask_security.unauth_csrf` decorator.
+
+
+.. _axios: https://github.com/axios/axios
+.. _httpClient: https://angular.io/guide/http#security-xsrf-protection
diff --git a/docs/quickstart.rst b/docs/quickstart.rst
new file mode 100644
index 0000000..c7bc1dd
--- /dev/null
+++ b/docs/quickstart.rst
@@ -0,0 +1,455 @@
+Quick Start
+===========
+
+There are some complete (but simple) examples available in the *examples* directory of the
+`Flask-Security repo`_.
+
+.. danger::
+ The examples below place secrets in source files. Never do this for your application
+ especially if your source code is placed in a public repo. How you pass in secrets
+ securely will depend on your deployment model - however in most cases (e.g. docker, lambda)
+ using environment variables will be the easiest.
+
+
+* :ref:`basic-sqlalchemy-application`
+* :ref:`basic-sqlalchemy-application-with-session`
+* :ref:`basic-mongoengine-application`
+* :ref:`basic-peewee-application`
+* :ref:`mail-configuration`
+* :ref:`proxy-configuration`
+* :ref:`unit-testing`
+
+.. _basic-sqlalchemy-application:
+
+Basic SQLAlchemy Application
+----------------------------
+
+SQLAlchemy Install requirements
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+::
+
+ $ mkvirtualenv <your-app-name>
+ $ pip install flask-security-too flask-sqlalchemy
+
+
+SQLAlchemy Application
+~~~~~~~~~~~~~~~~~~~~~~
+
+The following code sample illustrates how to get started as quickly as
+possible using Flask-SQLAlchemy and the built-in model mixins:
+
+::
+
+ import os
+
+ from flask import Flask, render_template_string
+ from flask_sqlalchemy import SQLAlchemy
+ from flask_security import Security, SQLAlchemyUserDatastore, auth_required, hash_password
+ from flask_security.models import fsqla_v2 as fsqla
+
+ # Create app
+ app = Flask(__name__)
+ app.config['DEBUG'] = True
+
+ # Generate a nice key using secrets.token_urlsafe()
+ app.config['SECRET_KEY'] = os.environ.get("SECRET_KEY", 'pf9Wkove4IKEAXvy-cQkeDPhv9Cb3Ag-wyJILbq_dFw')
+ # Bcrypt is set as default SECURITY_PASSWORD_HASH, which requires a salt
+ # Generate a good salt using: secrets.SystemRandom().getrandbits(128)
+ app.config['SECURITY_PASSWORD_SALT'] = os.environ.get("SECURITY_PASSWORD_SALT", '146585145368132386173505678016728509634')
+
+ app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://'
+ # As of Flask-SQLAlchemy 2.4.0 it is easy to pass in options directly to the
+ # underlying engine. This option makes sure that DB connections from the
+ # pool are still valid. Important for entire application since
+ # many DBaaS options automatically close idle connections.
+ app.config["SQLALCHEMY_ENGINE_OPTIONS"] = {
+ "pool_pre_ping": True,
+ }
+
+ # Create database connection object
+ db = SQLAlchemy(app)
+
+ # Define models
+ fsqla.FsModels.set_db_info(db)
+
+ class Role(db.Model, fsqla.FsRoleMixin):
+ pass
+
+ class User(db.Model, fsqla.FsUserMixin):
+ pass
+
+ # Setup Flask-Security
+ user_datastore = SQLAlchemyUserDatastore(db, User, Role)
+ security = Security(app, user_datastore)
+
+ # Create a user to test with
+ @app.before_first_request
+ def create_user():
+ db.create_all()
+ user_datastore.create_user(email="test@me.com", password=hash_password("password"))
+ db.session.commit()
+
+ # Views
+ @app.route("/")
+ @auth_required()
+ def home():
+ return render_template_string("Hello {{ current_user.email }}")
+
+ if __name__ == '__main__':
+ app.run()
+
+.. _basic-sqlalchemy-application-with-session:
+
+Basic SQLAlchemy Application with session
+-----------------------------------------
+
+SQLAlchemy Install requirements
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+::
+
+ $ mkvirtualenv <your-app-name>
+ $ pip install flask-security-too sqlalchemy
+
+Also, you can use the extension `Flask-SQLAlchemy-Session documentation
+<http://flask-sqlalchemy-session.readthedocs.io/en/latest/>`_.
+
+SQLAlchemy Application
+~~~~~~~~~~~~~~~~~~~~~~
+
+The following code sample illustrates how to get started as quickly as
+possible using `SQLAlchemy in a declarative way
+<http://flask.pocoo.org/docs/1.0/patterns/sqlalchemy/#declarative>`_:
+
+We are gonna split the application at least in three files: app.py, database.py
+and models.py. You can also do the models a folder and spread your tables there.
+
+- app.py ::
+
+ import os
+
+ from flask import Flask, render_template_string
+ from flask_security import Security, current_user, auth_required, hash_password, \
+ SQLAlchemySessionUserDatastore
+ from database import db_session, init_db
+ from models import User, Role
+
+ # Create app
+ app = Flask(__name__)
+ app.config['DEBUG'] = True
+
+ # Generate a nice key using secrets.token_urlsafe()
+ app.config['SECRET_KEY'] = os.environ.get("SECRET_KEY", 'pf9Wkove4IKEAXvy-cQkeDPhv9Cb3Ag-wyJILbq_dFw')
+ # Bcrypt is set as default SECURITY_PASSWORD_HASH, which requires a salt
+ # Generate a good salt using: secrets.SystemRandom().getrandbits(128)
+ app.config['SECURITY_PASSWORD_SALT'] = os.environ.get("SECURITY_PASSWORD_SALT", '146585145368132386173505678016728509634')
+
+ # Setup Flask-Security
+ user_datastore = SQLAlchemySessionUserDatastore(db_session, User, Role)
+ security = Security(app, user_datastore)
+
+ # Create a user to test with
+ @app.before_first_request
+ def create_user():
+ init_db()
+ user_datastore.create_user(email="test@me.com", password=hash_password("password"))
+ db_session.commit()
+
+ # Views
+ @app.route("/")
+ @auth_required()
+ def home():
+ return render_template_string('Hello {{email}} !', email=current_user.email)
+
+ if __name__ == '__main__':
+ app.run()
+
+- database.py ::
+
+ from sqlalchemy import create_engine
+ from sqlalchemy.orm import scoped_session, sessionmaker
+ from sqlalchemy.ext.declarative import declarative_base
+
+ engine = create_engine('sqlite:////tmp/test.db', \
+ convert_unicode=True)
+ db_session = scoped_session(sessionmaker(autocommit=False,
+ autoflush=False,
+ bind=engine))
+ Base = declarative_base()
+ Base.query = db_session.query_property()
+
+ def init_db():
+ # import all modules here that might define models so that
+ # they will be registered properly on the metadata. Otherwise
+ # you will have to import them first before calling init_db()
+ import models
+ Base.metadata.create_all(bind=engine)
+
+- models.py ::
+
+ from database import Base
+ from flask_security import UserMixin, RoleMixin
+ from sqlalchemy import create_engine
+ from sqlalchemy.orm import relationship, backref
+ from sqlalchemy import Boolean, DateTime, Column, Integer, \
+ String, ForeignKey
+
+ class RolesUsers(Base):
+ __tablename__ = 'roles_users'
+ id = Column(Integer(), primary_key=True)
+ user_id = Column('user_id', Integer(), ForeignKey('user.id'))
+ role_id = Column('role_id', Integer(), ForeignKey('role.id'))
+
+ class Role(Base, RoleMixin):
+ __tablename__ = 'role'
+ id = Column(Integer(), primary_key=True)
+ name = Column(String(80), unique=True)
+ description = Column(String(255))
+
+ class User(Base, UserMixin):
+ __tablename__ = 'user'
+ id = Column(Integer, primary_key=True)
+ email = Column(String(255), unique=True)
+ username = Column(String(255))
+ password = Column(String(255))
+ last_login_at = Column(DateTime())
+ current_login_at = Column(DateTime())
+ last_login_ip = Column(String(100))
+ current_login_ip = Column(String(100))
+ login_count = Column(Integer)
+ active = Column(Boolean())
+ fs_uniquifier = Column(String(255))
+ confirmed_at = Column(DateTime())
+ roles = relationship('Role', secondary='roles_users',
+ backref=backref('users', lazy='dynamic'))
+
+.. _basic-mongoengine-application:
+
+Basic MongoEngine Application
+-----------------------------
+
+MongoEngine Install requirements
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+::
+
+ $ mkvirtualenv <your-app-name>
+ $ pip install flask-security-too flask-mongoengine
+
+MongoEngine Application
+~~~~~~~~~~~~~~~~~~~~~~~
+
+The following code sample illustrates how to get started as quickly as
+possible using MongoEngine:
+
+::
+
+ import os
+
+ from flask import Flask, render_template
+ from flask_mongoengine import MongoEngine
+ from flask_security import Security, MongoEngineUserDatastore, \
+ UserMixin, RoleMixin, auth_required, hash_password
+
+ # Create app
+ app = Flask(__name__)
+ app.config['DEBUG'] = True
+
+ # Generate a nice key using secrets.token_urlsafe()
+ app.config['SECRET_KEY'] = os.environ.get("SECRET_KEY", 'pf9Wkove4IKEAXvy-cQkeDPhv9Cb3Ag-wyJILbq_dFw')
+ # Bcrypt is set as default SECURITY_PASSWORD_HASH, which requires a salt
+ # Generate a good salt using: secrets.SystemRandom().getrandbits(128)
+ app.config['SECURITY_PASSWORD_SALT'] = os.environ.get("SECURITY_PASSWORD_SALT", '146585145368132386173505678016728509634')
+
+ # MongoDB Config
+ app.config['MONGODB_DB'] = 'mydatabase'
+ app.config['MONGODB_HOST'] = 'localhost'
+ app.config['MONGODB_PORT'] = 27017
+
+ # Create database connection object
+ db = MongoEngine(app)
+
+ class Role(db.Document, RoleMixin):
+ name = db.StringField(max_length=80, unique=True)
+ description = db.StringField(max_length=255)
+
+ class User(db.Document, UserMixin):
+ email = db.StringField(max_length=255)
+ password = db.StringField(max_length=255)
+ active = db.BooleanField(default=True)
+ fs_uniquifier = db.StringField(max_length=255)
+ confirmed_at = db.DateTimeField()
+ roles = db.ListField(db.ReferenceField(Role), default=[])
+
+ # Setup Flask-Security
+ user_datastore = MongoEngineUserDatastore(db, User, Role)
+ security = Security(app, user_datastore)
+
+ # Create a user to test with
+ @app.before_first_request
+ def create_user():
+ user_datastore.create_user(email="admin@me.com", password=hash_password("password"))
+
+ # Views
+ @app.route("/")
+ @auth_required()
+ def home():
+ return render_template('index.html')
+
+ if __name__ == '__main__':
+ app.run()
+
+
+.. _basic-peewee-application:
+
+Basic Peewee Application
+------------------------
+
+Peewee Install requirements
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+::
+
+ $ mkvirtualenv <your-app-name>
+ $ pip install flask-security-too peewee
+
+Peewee Application
+~~~~~~~~~~~~~~~~~~
+
+The following code sample illustrates how to get started as quickly as
+possible using Peewee:
+
+::
+
+ import os
+
+ from flask import Flask, render_template
+ from playhouse.flask_utils import FlaskDB
+ from peewee import *
+ from flask_security import Security, PeeweeUserDatastore, \
+ UserMixin, RoleMixin, auth_required, hash_password
+
+ # Create app
+ app = Flask(__name__)
+ app.config['DEBUG'] = True
+
+ # Generate a nice key using secrets.token_urlsafe()
+ app.config['SECRET_KEY'] = os.environ.get("SECRET_KEY", 'pf9Wkove4IKEAXvy-cQkeDPhv9Cb3Ag-wyJILbq_dFw')
+ # Bcrypt is set as default SECURITY_PASSWORD_HASH, which requires a salt
+ # Generate a good salt using: secrets.SystemRandom().getrandbits(128)
+ app.config['SECURITY_PASSWORD_SALT'] = os.environ.get("SECURITY_PASSWORD_SALT", '146585145368132386173505678016728509634')
+
+ app.config['DATABASE'] = {
+ 'name': 'example.db',
+ 'engine': 'peewee.SqliteDatabase',
+ }
+
+ # Create database connection object
+ db = FlaskDB(app)
+
+ class Role(db.Model, RoleMixin):
+ name = CharField(unique=True)
+ description = TextField(null=True)
+
+ class User(db.Model, UserMixin):
+ email = TextField()
+ password = TextField()
+ active = BooleanField(default=True)
+ fs_uniquifier = TextField()
+ confirmed_at = DateTimeField(null=True)
+
+ class UserRoles(db.Model):
+ # Because peewee does not come with built-in many-to-many
+ # relationships, we need this intermediary class to link
+ # user to roles.
+ user = ForeignKeyField(User, related_name='roles')
+ role = ForeignKeyField(Role, related_name='users')
+ name = property(lambda self: self.role.name)
+ description = property(lambda self: self.role.description)
+
+ # Setup Flask-Security
+ user_datastore = PeeweeUserDatastore(db, User, Role, UserRoles)
+ security = Security(app, user_datastore)
+
+ # Create a user to test with
+ @app.before_first_request
+ def create_user():
+ for Model in (Role, User, UserRoles):
+ Model.drop_table(fail_silently=True)
+ Model.create_table(fail_silently=True)
+ user_datastore.create_user(email="test@me.com", password=hash_password("password"))
+
+ # Views
+ @app.route('/')
+ @auth_required()
+ def home():
+ return render_template('index.html')
+
+ if __name__ == '__main__':
+ app.run()
+
+
+.. _mail-configuration:
+
+Mail Configuration
+------------------
+
+Flask-Security integrates with Flask-Mail to handle all email
+communications between user and site, so it's important to configure
+Flask-Mail with your email server details so Flask-Security can talk
+with Flask-Mail correctly.
+
+The following code illustrates a basic setup, which could be added to
+the basic application code in the previous section::
+
+ # At top of file
+ from flask_mail import Mail
+
+ # After 'Create app'
+ app.config['MAIL_SERVER'] = 'smtp.example.com'
+ app.config['MAIL_PORT'] = 465
+ app.config['MAIL_USE_SSL'] = True
+ app.config['MAIL_USERNAME'] = 'username'
+ app.config['MAIL_PASSWORD'] = 'password'
+ mail = Mail(app)
+
+To learn more about the various Flask-Mail settings to configure it to
+work with your particular email server configuration, please see the
+`Flask-Mail documentation <http://packages.python.org/Flask-Mail/>`_.
+
+.. _proxy-configuration:
+
+Proxy Configuration
+-------------------
+
+The user tracking features need an additional configuration
+in HTTP proxy environment. The following code illustrates a setup
+with a single HTTP proxy in front of the web application::
+
+ # At top of file
+ from werkzeug.middleware.proxy_fix import ProxyFix
+
+ # After 'Create app'
+ app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1)
+
+To learn more about the ``ProxyFix`` middleware, please see the
+`Werkzeug documentation <https://werkzeug.palletsprojects.com/en/1.0.x/middleware/proxy_fix/#module-werkzeug.middleware.proxy_fix>`_.
+
+.. _unit-testing:
+
+Unit Testing Your Application
+-----------------------------
+
+As soon as you add any of the Flask-Security decorators to your API endpoints, it can
+be frustrating to unit test your basic routing (and roles and permissions). Without getting
+into the argument of the difference between unit tests and integration tests - you can approach testing
+in 2 ways:
+
+* 'Pure' unit test - mocking out all lower level objects (such as the data store)
+* Complete app with in-memory/temporary DB (with little or no mocking).
+
+Look in the `Flask-Security repo`_ *examples* directory for actual code that implements the
+first approach.
+
+.. _Flask-Security repo: https://github.com/Flask-Middleware/flask-security
diff --git a/docs/requirements.txt b/docs/requirements.txt
new file mode 100644
index 0000000..c037791
--- /dev/null
+++ b/docs/requirements.txt
@@ -0,0 +1 @@
+-e .[docs,tests]
diff --git a/docs/spa.rst b/docs/spa.rst
new file mode 100644
index 0000000..2a67baa
--- /dev/null
+++ b/docs/spa.rst
@@ -0,0 +1,189 @@
+Working with Single Page Applications
+======================================
+`Single Page Applications (spa)`_ are a popular model for both separating
+user interface from application/backend code as well as providing a responsive
+user experience. Angular and Vue are popular Javascript frameworks for writing SPAs.
+An added benefit is that the UI can be developed completely independently (in a separate repo)
+and take advantage of the latest Javascript packing and bundling technologies that are
+evolving rapidly, and not make the Flask application have to deal with things
+like Flask-Webpack or webassets.
+
+For the purposes of this application note - this implies:
+
+ * The user interface code is delivered by some other means than the Flask application.
+ In particular this means that there are no opportunities to inject context/environment
+ via a templating language.
+
+ * The user interface interacts with the backend Flask application via JSON requests
+ and responses - not forms.
+
+ * SPAs are still browser based - so they have the same security vulnerabilities as
+ traditional html/form-based applications.
+
+ * SPAs handle all routing/redirection via code, so redirects need context.
+
+Configuration
+~~~~~~~~~~~~~
+An example configuration::
+
+ # no forms so no concept of flashing
+ SECURITY_FLASH_MESSAGES = False
+
+ # Need to be able to route backend flask API calls. Use 'accounts'
+ # to be the Flask-Security endpoints.
+ SECURITY_URL_PREFIX = '/api/accounts'
+
+ # Turn on all the great Flask-Security features
+ SECURITY_RECOVERABLE = True
+ SECURITY_TRACKABLE = True
+ SECURITY_CHANGEABLE = True
+ SECURITY_CONFIRMABLE = True
+ SECURITY_REGISTERABLE = True
+ SECURITY_UNIFIED_SIGNIN = True
+
+ # These need to be defined to handle redirects
+ # As defined in the API documentation - they will receive the relevant context
+ SECURITY_POST_CONFIRM_VIEW = "/confirmed"
+ SECURITY_CONFIRM_ERROR_VIEW = "/confirm-error"
+ SECURITY_RESET_VIEW = "/reset-password"
+ SECURITY_RESET_ERROR_VIEW = "/reset-password"
+ SECURITY_REDIRECT_BEHAVIOR = "spa"
+
+ # CSRF protection is critical for all session-based browser UIs
+
+ # enforce CSRF protection for session / browser - but allow token-based
+ # API calls to go through
+ SECURITY_CSRF_PROTECT_MECHANISMS = ["session", "basic"]
+ SECURITY_CSRF_IGNORE_UNAUTH_ENDPOINTS = True
+
+ # Send Cookie with csrf-token. This is the default for Axios and Angular.
+ SECURITY_CSRF_COOKIE = {"key": "XSRF-TOKEN"}
+ WTF_CSRF_CHECK_DEFAULT = False
+ WTF_CSRF_TIME_LIMIT = None
+
+ # In your app
+ # Enable CSRF on all api endpoints.
+ flask_wtf.CSRFProtect(app)
+
+ # Initialize Flask-Security
+ user_datastore = SQLAlchemyUserDatastore(db, User, Role)
+ security = Security(app, user_datastore)
+
+ # Optionally define and set unauthorized callbacks
+ security.unauthz_handler(<your unauth handler>)
+
+When in development mode, the Flask application will run by default on port 5000.
+The UI might want to run on port 8080. In order to test redirects you need to set::
+
+ SECURITY_REDIRECT_HOST = 'localhost:8080'
+
+Client side authentication options
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Depending on your SPA architecture and vision you can choose between cookie or token based authentication.
+
+For both there is more documentation and some examples. In both cases, you need to understand and handle :ref:`csrftopic` concerns.
+
+Security Considerations
+~~~~~~~~~~~~~~~~~~~~~~~~
+Static elements such as your UI should be served with an industrial-grade web server - such
+as `Nginx`_. This is also where various security measures should be handled such as injecting
+standard security headers such as:
+
+ * ``Strict-Transport-Security``
+ * ``X-Frame-Options``
+ * ``Content Security Policy``
+ * ``X-Content-Type-Options``
+ * ``X-XSS-Protection``
+ * ``Referrer policy``
+
+There are a lot of different ways to host a SPA as the javascript part itself is quit easily hosted from any static
+webserver. A couple of deployment options and their configurations will be describer here.
+
+Nginx
+~~~~~
+When serving a SPA from a Nginx webserver the Flask backend, with Flask-Security-Too, will probably be served via
+Nginx's reverse proxy feature. The javascript is served from Nginx itself and all calls to a certain path will be routed
+to the reversed proxy. The example below routes all http requests to *"/api/"* to the Flask backend and handles all other
+requests directly from javascript. This has a couple of benefits as all the requests happen within the same domain so you
+don't have to worry about `CORS`_ problems::
+
+ server {
+ listen 80;
+ server_name www.example.com;
+
+ #access_log /var/log/nginx/host.access.log main;
+
+ root /usr/share/nginx/html;
+ index index.html;
+
+ location / {
+ try_files $uri $uri/ /index.html;
+ }
+
+ # Location of assets folder
+ location ~ ^/(static)/ {
+ gzip_static on;
+ gzip_types text/plain text/xml text/css text/comma-separated-values
+ text/javascript application/x-javascript application/atom+xml;
+ expires max;
+ }
+
+ # redirect server error pages to the static page /50x.html
+ # 400 error's will be handled from the SPA
+ error_page 500 502 503 504 /50x.html;
+ location = /50x.html {
+ }
+
+ # route all api requests to the flask app, served by gunicorn
+ location /api/ {
+ proxy_pass http://localhost:8080/api/;
+ }
+
+ # OR served via uwsgi
+ location /api/ {
+ include ..../uwsgi_params;
+ uwsgi_pass unix:/tmp/uwsgi.sock;
+ uwsgi_pass_header AUTHENTICATION-TOKEN;
+ }
+ }
+
+.. note:: The example doesn't include SSL setup to keep it simple and still suitable for a more complex kubernetes setup
+ where Nginx is often used as a load balancer and another Nginx with SSL setup runs in front of it.
+
+Amazon lambda gateway / Serverless
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Most Flask apps can be deployed to Amazon's lambda gateway without much hassle by using `Zappa`_.
+You'll get automatic horizontal scaling, seamless upgrades, automatic SSL certificate renewal and a very cheap way of
+hosting a backend without being responsible for any infrastructure. Depending on how you design your app you could
+choose to host your backend from an api specific domain: e.g. *api.example.com*. When your SPA deployment structure is
+capable of routing the AJAX/XHR request from your javascript app to the separate backend; use it. When you want to use
+the backend from another e.g. *www.example.com* you have some deal with some `CORS`_ setup as your browser will block
+cross-domain POST requests. There is a Flask package for that: `Flask-CORS`_.
+
+The setup of CORS is simple::
+
+ CORS(
+ app,
+ supports_credentials=True, # needed for cross domain cookie support
+ resources="/*",
+ allow_headers="*",
+ origins="https://www.example.com",
+ expose_headers="Authorization,Content-Type,Authentication-Token,XSRF-TOKEN",
+ )
+
+You can then host your javascript app from an S3 bucket, with or without Cloudfront, GH-pages or from any static webserver.
+
+Some background material:
+
+ * Specific to `S3`_ but easily adaptable.
+
+ * `Flask-Talisman`_ - useful if serving everything from your Flask application - also
+ useful as a good list of things to consider.
+
+.. _Single Page Applications (spa): https://en.wikipedia.org/wiki/Single-page_application
+.. _Nginx: https://www.nginx.com/
+.. _S3: https://www.savjee.be/2018/05/Content-security-policy-and-aws-s3-cloudfront/
+.. _Flask-Talisman: https://github.com/GoogleCloudPlatform/flask-talisman
+.. _CORS: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
+.. _Flask-CORS: https://github.com/corydolphin/flask-cors
+.. _Zappa: https://github.com/Miserlou/Zappa
diff --git a/docs/two_factor_configurations.rst b/docs/two_factor_configurations.rst
new file mode 100644
index 0000000..404ee0e
--- /dev/null
+++ b/docs/two_factor_configurations.rst
@@ -0,0 +1,120 @@
+Two-factor Configurations
+=========================
+
+Two-factor authentication provides a second layer of security to any type of
+login, requiring extra information or a secondary device to log in, in addition
+to ones login credentials. The added feature includes the ability to add a
+secondary authentication method using either via email, sms message, or an
+Authenticator app such as Google, Lastpass, or Authy.
+
+The following code sample illustrates how to get started as quickly as
+possible using SQLAlchemy and two-factor feature:
+
+- `Basic SQLAlchemy Application <#basic-sqlalchemy-application>`_
+
+Basic SQLAlchemy Two-Factor Application
++++++++++++++++++++++++++++++++++++++++
+
+SQLAlchemy Install requirements
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+::
+
+ $ mkvirtualenv <your-app-name>
+ $ pip install flask-security-too flask-sqlalchemy cryptography pyqrcode
+
+
+Two-factor Application
+~~~~~~~~~~~~~~~~~~~~~~
+
+The following code sample illustrates how to get started as quickly as
+possible using SQLAlchemy:
+
+::
+
+ from flask import Flask, current_app, render_template
+ from flask_sqlalchemy import SQLAlchemy
+ from flask_security import Security, SQLAlchemyUserDatastore, \
+ UserMixin, RoleMixin, login_required
+
+
+ # At top of file
+ from flask_mail import Mail
+
+
+ # Convenient references
+ from werkzeug.datastructures import MultiDict
+ from werkzeug.local import LocalProxy
+
+
+ _security = LocalProxy(lambda: current_app.extensions['security'])
+
+ _datastore = LocalProxy(lambda: _security.datastore)
+
+ # Create app
+ app = Flask(__name__)
+ app.config['DEBUG'] = True
+ # Generate a nice key using secrets.token_urlsafe()
+ app.config['SECRET_KEY'] = os.environ.get("SECRET_KEY", 'pf9Wkove4IKEAXvy-cQkeDPhv9Cb3Ag-wyJILbq_dFw')
+ # Bcrypt is set as default SECURITY_PASSWORD_HASH, which requires a salt
+ # Generate a good salt using: secrets.SystemRandom().getrandbits(128)
+ app.config['SECURITY_PASSWORD_SALT'] = os.environ.get("SECURITY_PASSWORD_SALT", '146585145368132386173505678016728509634')
+
+ app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://'
+
+ app.config['SECURITY_TWO_FACTOR_ENABLED_METHODS'] = ['email',
+ 'authenticator'] # 'sms' also valid but requires an sms provider
+ app.config['SECURITY_TWO_FACTOR'] = True
+ app.config['SECURITY_TWO_FACTOR_RESCUE_MAIL'] = 'put_your_mail@gmail.com'
+
+ # Generate a good totp secret using: passlib.totp.generate_secret()
+ app.config['SECURITY_TOTP_SECRETS'] = {"1": "TjQ9Qa31VOrfEzuPy4VHQWPCTmRzCnFzMKLxXYiZu9B"}
+ app.config['SECURITY_TOTP_ISSUER'] = 'put_your_app_name'
+
+ # Create database connection object
+ db = SQLAlchemy(app)
+
+ # Define models
+ roles_users = db.Table('roles_users',
+ db.Column('user_id', db.Integer(), db.ForeignKey('user.id')),
+ db.Column('role_id', db.Integer(), db.ForeignKey('role.id')))
+
+ class Role(db.Model, RoleMixin):
+ id = db.Column(db.Integer(), primary_key=True)
+ name = db.Column(db.String(80), unique=True)
+ description = db.Column(db.String(255))
+
+ class User(db.Model, UserMixin):
+ id = db.Column(db.Integer, primary_key=True)
+ email = db.Column(db.String(255), unique=True)
+ password = db.Column(db.String(255))
+ active = db.Column(db.Boolean())
+ confirmed_at = db.Column(db.DateTime())
+ roles = db.relationship('Role', secondary=roles_users,
+ backref=db.backref('users', lazy='dynamic'))
+ tf_phone_number = db.Column(db.String(64))
+ tf_primary_method = db.Column(db.String(140))
+ tf_totp_secret = db.Column(db.String(255))
+
+ # Setup Flask-Security
+ user_datastore = SQLAlchemyUserDatastore(db, User, Role)
+ security = Security(app, user_datastore)
+
+ mail = Mail(app)
+
+ # Create a user to test with
+ @app.before_first_request
+ def create_user():
+ db.create_all()
+ user_datastore.create_user(email='gal@lp.com', password='password', username='gal',
+ tf_totp_secret=None, tf_primary_method=None)
+ db.session.commit()
+
+ # Views
+ @app.route('/')
+ @login_required
+ def home():
+ return render_template('index.html')
+
+ if __name__ == '__main__':
+ app.run()
diff --git a/flask_security/__init__.py b/flask_security/__init__.py
new file mode 100644
index 0000000..1a9dccd
--- /dev/null
+++ b/flask_security/__init__.py
@@ -0,0 +1,104 @@
+# -*- coding: utf-8 -*-
+"""
+ flask_security
+ ~~~~~~~~~~~~~~
+
+ Flask-Security is a Flask extension that aims to add quick and simple
+ security via Flask-Login, Flask-Principal, Flask-WTF, and passlib.
+
+ :copyright: (c) 2012-2019 by Matt Wright.
+ :copyright: (c) 2019-2020 by J. Christopher Wagner.
+ :license: MIT, see LICENSE for more details.
+"""
+
+# flake8: noqa: F401
+
+from .core import Security, RoleMixin, UserMixin, AnonymousUser, current_user
+from .datastore import (
+ UserDatastore,
+ SQLAlchemyUserDatastore,
+ MongoEngineUserDatastore,
+ PeeweeUserDatastore,
+ PonyUserDatastore,
+ SQLAlchemySessionUserDatastore,
+)
+from .decorators import (
+ auth_token_required,
+ anonymous_user_required,
+ handle_csrf,
+ http_auth_required,
+ login_required,
+ roles_accepted,
+ roles_required,
+ auth_required,
+ permissions_accepted,
+ permissions_required,
+ unauth_csrf,
+)
+from .forms import (
+ ChangePasswordForm,
+ ForgotPasswordForm,
+ LoginForm,
+ RegisterForm,
+ ResetPasswordForm,
+ PasswordlessLoginForm,
+ ConfirmRegisterForm,
+ SendConfirmationForm,
+ TwoFactorRescueForm,
+ TwoFactorSetupForm,
+ TwoFactorVerifyCodeForm,
+ TwoFactorVerifyPasswordForm,
+ VerifyForm,
+)
+from .phone_util import PhoneUtil
+from .signals import (
+ confirm_instructions_sent,
+ login_instructions_sent,
+ password_changed,
+ password_reset,
+ reset_password_instructions_sent,
+ tf_code_confirmed,
+ tf_profile_changed,
+ tf_security_token_sent,
+ tf_disabled,
+ user_authenticated,
+ user_confirmed,
+ user_registered,
+ us_security_token_sent,
+ us_profile_changed,
+)
+from .totp import Totp
+from .twofactor import tf_send_security_token
+from .unified_signin import (
+ UnifiedSigninForm,
+ UnifiedSigninSetupForm,
+ UnifiedSigninSetupValidateForm,
+ UnifiedVerifyForm,
+ us_send_security_token,
+)
+from .utils import (
+ FsJsonEncoder,
+ SmsSenderBaseClass,
+ SmsSenderFactory,
+ check_and_get_token_status,
+ get_hmac,
+ get_token_status,
+ get_url,
+ hash_password,
+ check_and_update_authn_fresh,
+ login_user,
+ logout_user,
+ password_breached_validator,
+ password_complexity_validator,
+ password_length_validator,
+ pwned,
+ send_mail,
+ transform_url,
+ uia_phone_mapper,
+ uia_email_mapper,
+ url_for_security,
+ verify_password,
+ verify_and_update_password,
+)
+
+__version__ = "3.4.2"
diff --git a/flask_security/async_compat.py b/flask_security/async_compat.py
new file mode 100644
index 0000000..3168e76
--- /dev/null
+++ b/flask_security/async_compat.py
@@ -0,0 +1,18 @@
+"""
+ Temporary workaround while we still support p2.7
+
+ :copyright: (c) 2019 by J. Christopher Wagner (jwag).
+ :license: MIT, see LICENSE for more details.
+"""
+
+from flask import current_app
+from werkzeug.local import LocalProxy
+
+_security = LocalProxy(lambda: current_app.extensions["security"])
+
+_datastore = LocalProxy(lambda: _security.datastore)
+
+
+async def _commit(response=None): # pragma: no cover
+ _datastore.commit()
+ return response
diff --git a/flask_security/babel.py b/flask_security/babel.py
new file mode 100644
index 0000000..31c6570
--- /dev/null
+++ b/flask_security/babel.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+"""
+ flask_security.babel
+ ~~~~~~~~~~~~~~~~~~~~
+
+ I18N support for Flask-Security.
+"""
+
+from flask_babelex import Domain
+from wtforms.i18n import messages_path
+
+wtforms_domain = Domain(messages_path(), domain="wtforms")
+
+
+class Translations(object):
+ """Fixes WTForms translation support and uses wtforms translations."""
+
+ def gettext(self, string):
+ return wtforms_domain.gettext(string)
+
+ def ngettext(self, singular, plural, n):
+ return wtforms_domain.ngettext(singular, plural, n)
diff --git a/flask_security/cache.py b/flask_security/cache.py
new file mode 100644
index 0000000..5ad0236
--- /dev/null
+++ b/flask_security/cache.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+"""
+ flask_security.cache
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Flask-Security token cache module
+
+ :copyright: (c) 2019.
+ :license: MIT, see LICENSE for more details.
+"""
+
+from .utils import config_value
+
+
+class VerifyHashCache:
+ """Cache handler to make it quick password check by bypassing
+ already checked passwords against exact same couple of token/password.
+ This cache handler is more efficient on small apps that
+ run on few processes as cache is only shared between threads."""
+
+ def __init__(self):
+ ttl = config_value("VERIFY_HASH_CACHE_TTL", default=(60 * 5))
+ max_size = config_value("VERIFY_HASH_CACHE_MAX_SIZE", default=500)
+
+ try:
+ from cachetools import TTLCache
+
+ self._cache = TTLCache(max_size, ttl)
+ except ImportError:
+ # this should have been checked at app init.
+ raise
+
+ def has_verify_hash_cache(self, user):
+ """Check given user id is in cache."""
+ return self._cache.get(user.id)
+
+ def set_cache(self, user):
+ """When a password is checked, then result is put in cache."""
+ self._cache[user.id] = True
+
+ def clear(self):
+ """Clear cache"""
+ self._cache.clear()
diff --git a/flask_security/changeable.py b/flask_security/changeable.py
new file mode 100644
index 0000000..101339c
--- /dev/null
+++ b/flask_security/changeable.py
@@ -0,0 +1,46 @@
+# -*- coding: utf-8 -*-
+"""
+ flask_security.changeable
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Flask-Security recoverable module
+
+ :copyright: (c) 2012 by Matt Wright.
+ :author: Eskil Heyn Olsen
+ :license: MIT, see LICENSE for more details.
+"""
+
+from flask import current_app
+from werkzeug.local import LocalProxy
+
+from .signals import password_changed
+from .utils import config_value, hash_password
+
+# Convenient references
+_security = LocalProxy(lambda: current_app.extensions["security"])
+
+_datastore = LocalProxy(lambda: _security.datastore)
+
+
+def send_password_changed_notice(user):
+ """Sends the password changed notice email for the specified user.
+
+ :param user: The user to send the notice to
+ """
+ if config_value("SEND_PASSWORD_CHANGE_EMAIL"):
+ subject = config_value("EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE")
+ _security._send_mail(subject, user.email, "change_notice", user=user)
+
+
+def change_user_password(user, password):
+ """Change the specified user's password
+
+ :param user: The user to change_password
+ :param password: The unhashed new password
+ """
+ user.password = hash_password(password)
+ if config_value("BACKWARDS_COMPAT_AUTH_TOKEN_INVALID"):
+ _datastore.set_uniquifier(user)
+ _datastore.put(user)
+ send_password_changed_notice(user)
+ password_changed.send(current_app._get_current_object(), user=user)
diff --git a/flask_security/cli.py b/flask_security/cli.py
new file mode 100644
index 0000000..b32aedd
--- /dev/null
+++ b/flask_security/cli.py
@@ -0,0 +1,187 @@
+# -*- coding: utf-8 -*-
+"""
+ flask_security.cli
+ ~~~~~~~~~~~~~~~~~~
+
+ Command Line Interface for managing accounts and roles.
+
+ :copyright: (c) 2016 by CERN.
+ :copyright: (c) 2019 by J. Christopher Wagner
+ :license: MIT, see LICENSE for more details.
+"""
+
+from __future__ import absolute_import, print_function
+
+from functools import wraps
+
+import click
+from flask import current_app
+from werkzeug.datastructures import MultiDict
+from werkzeug.local import LocalProxy
+from .quart_compat import get_quart_status
+
+from .utils import hash_password
+
+if get_quart_status(): # pragma: no cover
+ import quart.cli
+ import functools
+
+ # quart cli doesn't provide the with_appcontext function
+ def with_appcontext(f):
+ """Wraps a callback so that it's guaranteed to be executed with the
+ script's application context. If callbacks are registered directly
+ to the ``app.cli`` object then they are wrapped with this function
+ by default unless it's disabled.
+ """
+
+ @click.pass_context
+ def decorator(__ctx, *args, **kwargs):
+ with __ctx.ensure_object(quart.cli.ScriptInfo).load_app().app_context():
+ return __ctx.invoke(f, *args, **kwargs)
+
+ return functools.update_wrapper(decorator, f)
+
+
+else:
+ import flask.cli
+
+ with_appcontext = flask.cli.with_appcontext
+
+
+_security = LocalProxy(lambda: current_app.extensions["security"])
+_datastore = LocalProxy(lambda: current_app.extensions["security"].datastore)
+
+
+def commit(fn):
+ """Decorator to commit changes in datastore."""
+
+ @wraps(fn)
+ def wrapper(*args, **kwargs):
+ fn(*args, **kwargs)
+ _datastore.commit()
+
+ return wrapper
+
+
+@click.group()
+def users():
+ """User commands."""
+
+
+@click.group()
+def roles():
+ """Role commands."""
+
+
+@users.command("create")
+@click.argument("identity")
+@click.password_option()
+@click.option("-a", "--active", default=False, is_flag=True)
+@with_appcontext
+@commit
+def users_create(identity, password, active):
+ """Create a user."""
+ kwargs = {attr: identity for attr in _security.user_identity_attributes}
+ kwargs.update(**{"password": password})
+
+ form = _security.confirm_register_form(MultiDict(kwargs), meta={"csrf": False})
+
+ if form.validate():
+ kwargs["password"] = hash_password(kwargs["password"])
+ kwargs["active"] = active
+ _datastore.create_user(**kwargs)
+ click.secho("User created successfully.", fg="green")
+ kwargs["password"] = "****"
+ click.echo(kwargs)
+ else:
+ raise click.UsageError("Error creating user. %s" % form.errors)
+
+
+@roles.command("create")
+@click.argument("name")
+@click.option("-d", "--description", default=None)
+@click.option("-p", "--permissions")
+@with_appcontext
+@commit
+def roles_create(**kwargs):
+ """Create a role."""
+
+ # For some reaosn Click puts arguments in kwargs - even if they weren't specified.
+ if "permissions" in kwargs and not kwargs["permissions"]:
+ del kwargs["permissions"]
+ if "permissions" in kwargs and not hasattr(_datastore.role_model, "permissions"):
+ raise click.UsageError("Role model does not support permissions")
+ _datastore.create_role(**kwargs)
+ click.secho('Role "%(name)s" created successfully.' % kwargs, fg="green")
+
+
+@roles.command("add")
+@click.argument("user")
+@click.argument("role")
+@with_appcontext
+@commit
+def roles_add(user, role):
+ """Add user to role."""
+ user, role = _datastore._prepare_role_modify_args(user, role)
+ if user is None:
+ raise click.UsageError("Cannot find user.")
+ if role is None:
+ raise click.UsageError("Cannot find role.")
+ if _datastore.add_role_to_user(user, role):
+ click.secho(
+ 'Role "{0}" added to user "{1}" ' "successfully.".format(role, user),
+ fg="green",
+ )
+ else:
+ raise click.UsageError("Cannot add role to user.")
+
+
+@roles.command("remove")
+@click.argument("user")
+@click.argument("role")
+@with_appcontext
+@commit
+def roles_remove(user, role):
+ """Remove user from role."""
+ user, role = _datastore._prepare_role_modify_args(user, role)
+ if user is None:
+ raise click.UsageError("Cannot find user.")
+ if role is None:
+ raise click.UsageError("Cannot find role.")
+ if _datastore.remove_role_from_user(user, role):
+ click.secho(
+ 'Role "{0}" removed from user "{1}" ' "successfully.".format(role, user),
+ fg="green",
+ )
+ else:
+ raise click.UsageError("Cannot remove role from user.")
+
+
+@users.command("activate")
+@click.argument("user")
+@with_appcontext
+@commit
+def users_activate(user):
+ """Activate a user."""
+ user_obj = _datastore.get_user(user)
+ if user_obj is None:
+ raise click.UsageError("ERROR: User not found.")
+ if _datastore.activate_user(user_obj):
+ click.secho('User "{0}" has been activated.'.format(user), fg="green")
+ else:
+ click.secho('User "{0}" was already activated.'.format(user), fg="yellow")
+
+
+@users.command("deactivate")
+@click.argument("user")
+@with_appcontext
+@commit
+def users_deactivate(user):
+ """Deactivate a user."""
+ user_obj = _datastore.get_user(user)
+ if user_obj is None:
+ raise click.UsageError("ERROR: User not found.")
+ if _datastore.deactivate_user(user_obj):
+ click.secho('User "{0}" has been deactivated.'.format(user), fg="green")
+ else:
+ click.secho('User "{0}" was already deactivated.'.format(user), fg="yellow")
diff --git a/flask_security/confirmable.py b/flask_security/confirmable.py
new file mode 100644
index 0000000..e544be4
--- /dev/null
+++ b/flask_security/confirmable.py
@@ -0,0 +1,100 @@
+# -*- coding: utf-8 -*-
+"""
+ flask_security.confirmable
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Flask-Security confirmable module
+
+ :copyright: (c) 2012 by Matt Wright.
+ :copyright: (c) 2017 by CERN.
+ :license: MIT, see LICENSE for more details.
+"""
+
+from flask import current_app as app
+from werkzeug.local import LocalProxy
+
+from .signals import confirm_instructions_sent, user_confirmed
+from .utils import (
+ config_value,
+ get_token_status,
+ hash_data,
+ url_for_security,
+ verify_hash,
+)
+
+# Convenient references
+_security = LocalProxy(lambda: app.extensions["security"])
+
+_datastore = LocalProxy(lambda: _security.datastore)
+
+
+def generate_confirmation_link(user):
+ token = generate_confirmation_token(user)
+ return url_for_security("confirm_email", token=token, _external=True), token
+
+
+def send_confirmation_instructions(user):
+ """Sends the confirmation instructions email for the specified user.
+
+ :param user: The user to send the instructions to
+ """
+
+ confirmation_link, token = generate_confirmation_link(user)
+
+ _security._send_mail(
+ config_value("EMAIL_SUBJECT_CONFIRM"),
+ user.email,
+ "confirmation_instructions",
+ user=user,
+ confirmation_link=confirmation_link,
+ )
+
+ confirm_instructions_sent.send(app._get_current_object(), user=user, token=token)
+
+
+def generate_confirmation_token(user):
+ """Generates a unique confirmation token for the specified user.
+
+ :param user: The user to work with
+ """
+ data = [str(user.id), hash_data(user.email)]
+ return _security.confirm_serializer.dumps(data)
+
+
+def requires_confirmation(user):
+ """Returns `True` if the user requires confirmation."""
+ return (
+ _security.confirmable
+ and not _security.login_without_confirmation
+ and user.confirmed_at is None
+ )
+
+
+def confirm_email_token_status(token):
+ """Returns the expired status, invalid status, and user of a confirmation
+ token. For example::
+
+ expired, invalid, user = confirm_email_token_status('...')
+
+ :param token: The confirmation token
+ """
+ expired, invalid, user, token_data = get_token_status(
+ token, "confirm", "CONFIRM_EMAIL", return_data=True
+ )
+ if not invalid and user:
+ user_id, token_email_hash = token_data
+ invalid = not verify_hash(token_email_hash, user.email)
+ return expired, invalid, user
+
+
+def confirm_user(user):
+ """Confirms the specified user
+
+ :param user: The user to confirm
+ """
+ if user.confirmed_at is not None:
+ return False
+ user.confirmed_at = _security.datetime_factory()
+ _datastore.put(user)
+ user_confirmed.send(app._get_current_object(), user=user)
+ return True
diff --git a/flask_security/core.py b/flask_security/core.py
new file mode 100644
index 0000000..f502c56
--- /dev/null
+++ b/flask_security/core.py
@@ -0,0 +1,1411 @@
+# -*- coding: utf-8 -*-
+"""
+ flask_security.core
+ ~~~~~~~~~~~~~~~~~~~
+
+ Flask-Security core module
+
+ :copyright: (c) 2012 by Matt Wright.
+ :copyright: (c) 2017 by CERN.
+ :copyright: (c) 2017 by ETH Zurich, Swiss Data Science Center.
+ :copyright: (c) 2019-2020 by J. Christopher Wagner (jwag).
+ :license: MIT, see LICENSE for more details.
+"""
+
+from datetime import datetime, timedelta
+import warnings
+import sys
+
+import pkg_resources
+from flask import _request_ctx_stack, current_app, render_template
+from flask_babelex import Domain
+from flask_login import AnonymousUserMixin, LoginManager
+from flask_login import UserMixin as BaseUserMixin
+from flask_login import current_user
+from flask_principal import Identity, Principal, RoleNeed, UserNeed, identity_loaded
+from itsdangerous import URLSafeTimedSerializer
+from passlib.context import CryptContext
+from werkzeug.datastructures import ImmutableList
+from werkzeug.local import LocalProxy, Local
+
+from .decorators import (
+ default_reauthn_handler,
+ default_unauthn_handler,
+ default_unauthz_handler,
+)
+from .forms import (
+ ChangePasswordForm,
+ ConfirmRegisterForm,
+ ForgotPasswordForm,
+ LoginForm,
+ PasswordlessLoginForm,
+ RegisterForm,
+ ResetPasswordForm,
+ SendConfirmationForm,
+ TwoFactorVerifyCodeForm,
+ TwoFactorSetupForm,
+ TwoFactorVerifyPasswordForm,
+ TwoFactorRescueForm,
+ VerifyForm,
+)
+from .phone_util import PhoneUtil
+from .twofactor import tf_send_security_token
+from .unified_signin import (
+ UnifiedSigninForm,
+ UnifiedSigninSetupForm,
+ UnifiedSigninSetupValidateForm,
+ UnifiedVerifyForm,
+ us_send_security_token,
+)
+from .totp import Totp
+from .utils import _
+from .utils import config_value as cv
+from .utils import (
+ FsJsonEncoder,
+ FsPermNeed,
+ csrf_cookie_handler,
+ default_want_json,
+ default_password_validator,
+ get_config,
+ get_identity_attributes,
+ get_message,
+ hash_data,
+ localize_callback,
+ send_mail,
+ string_types,
+ uia_email_mapper,
+ uia_phone_mapper,
+ url_for_security,
+ verify_and_update_password,
+ verify_hash,
+)
+from .views import create_blueprint, default_render_json
+from .cache import VerifyHashCache
+
+# Convenient references
+_security = LocalProxy(lambda: current_app.extensions["security"])
+_datastore = LocalProxy(lambda: _security.datastore)
+local_cache = Local()
+
+# List of authentication mechanisms supported.
+AUTHN_MECHANISMS = ("basic", "session", "token")
+
+
+#: Default Flask-Security configuration
+_default_config = {
+ "BLUEPRINT_NAME": "security",
+ "CLI_ROLES_NAME": "roles",
+ "CLI_USERS_NAME": "users",
+ "URL_PREFIX": None,
+ "SUBDOMAIN": None,
+ "FLASH_MESSAGES": True,
+ "I18N_DOMAIN": "flask_security",
+ "I18N_DIRNAME": pkg_resources.resource_filename("flask_security", "translations"),
+ "PASSWORD_HASH": "bcrypt",
+ "PASSWORD_SALT": None,
+ "PASSWORD_SINGLE_HASH": {
+ "django_argon2",
+ "django_bcrypt_sha256",
+ "django_pbkdf2_sha256",
+ "django_pbkdf2_sha1",
+ "django_bcrypt",
+ "django_salted_md5",
+ "django_salted_sha1",
+ "django_des_crypt",
+ "plaintext",
+ },
+ "PASSWORD_SCHEMES": [
+ "bcrypt",
+ "argon2",
+ "des_crypt",
+ "pbkdf2_sha256",
+ "pbkdf2_sha512",
+ "sha256_crypt",
+ "sha512_crypt",
+ # And always last one...
+ "plaintext",
+ ],
+ "PASSWORD_HASH_OPTIONS": {}, # Deprecated at passlib 1.7
+ "PASSWORD_HASH_PASSLIB_OPTIONS": {
+ "argon2__rounds": 10 # 1.7.1 default is 2.
+ }, # >= 1.7.1 method to pass options.
+ "PASSWORD_LENGTH_MIN": 8,
+ "PASSWORD_COMPLEXITY_CHECKER": None,
+ "PASSWORD_CHECK_BREACHED": False,
+ "PASSWORD_BREACHED_COUNT": 1,
+ "DEPRECATED_PASSWORD_SCHEMES": ["auto"],
+ "LOGIN_URL": "/login",
+ "LOGOUT_URL": "/logout",
+ "REGISTER_URL": "/register",
+ "RESET_URL": "/reset",
+ "CHANGE_URL": "/change",
+ "CONFIRM_URL": "/confirm",
+ "VERIFY_URL": "/verify",
+ "TWO_FACTOR_SETUP_URL": "/tf-setup",
+ "TWO_FACTOR_TOKEN_VALIDATION_URL": "/tf-validate",
+ "TWO_FACTOR_QRCODE_URL": "/tf-qrcode",
+ "TWO_FACTOR_RESCUE_URL": "/tf-rescue",
+ "TWO_FACTOR_CONFIRM_URL": "/tf-confirm",
+ "LOGOUT_METHODS": ["GET", "POST"],
+ "POST_LOGIN_VIEW": "/",
+ "POST_LOGOUT_VIEW": "/",
+ "CONFIRM_ERROR_VIEW": None,
+ "POST_REGISTER_VIEW": None,
+ "POST_CONFIRM_VIEW": None,
+ "POST_RESET_VIEW": None,
+ "POST_CHANGE_VIEW": None,
+ "POST_VERIFY_VIEW": None,
+ "UNAUTHORIZED_VIEW": None,
+ "RESET_ERROR_VIEW": None,
+ "RESET_VIEW": None,
+ "LOGIN_ERROR_VIEW": None,
+ "REDIRECT_HOST": None,
+ "REDIRECT_BEHAVIOR": None,
+ "FORGOT_PASSWORD_TEMPLATE": "security/forgot_password.html",
+ "LOGIN_USER_TEMPLATE": "security/login_user.html",
+ "REGISTER_USER_TEMPLATE": "security/register_user.html",
+ "RESET_PASSWORD_TEMPLATE": "security/reset_password.html",
+ "CHANGE_PASSWORD_TEMPLATE": "security/change_password.html",
+ "SEND_CONFIRMATION_TEMPLATE": "security/send_confirmation.html",
+ "SEND_LOGIN_TEMPLATE": "security/send_login.html",
+ "VERIFY_TEMPLATE": "security/verify.html",
+ "TWO_FACTOR_VERIFY_CODE_TEMPLATE": "security/two_factor_verify_code.html",
+ "TWO_FACTOR_SETUP_TEMPLATE": "security/two_factor_setup.html",
+ "TWO_FACTOR_VERIFY_PASSWORD_TEMPLATE": "security/two_factor_verify_password.html",
+ "CONFIRMABLE": False,
+ "REGISTERABLE": False,
+ "RECOVERABLE": False,
+ "TRACKABLE": False,
+ "PASSWORDLESS": False,
+ "CHANGEABLE": False,
+ "TWO_FACTOR": False,
+ "SEND_REGISTER_EMAIL": True,
+ "SEND_PASSWORD_CHANGE_EMAIL": True,
+ "SEND_PASSWORD_RESET_EMAIL": True,
+ "SEND_PASSWORD_RESET_NOTICE_EMAIL": True,
+ "LOGIN_WITHIN": "1 days",
+ "TWO_FACTOR_AUTHENTICATOR_VALIDITY": 120,
+ "TWO_FACTOR_MAIL_VALIDITY": 300,
+ "TWO_FACTOR_SMS_VALIDITY": 120,
+ "CONFIRM_EMAIL_WITHIN": "5 days",
+ "RESET_PASSWORD_WITHIN": "5 days",
+ "LOGIN_WITHOUT_CONFIRMATION": False,
+ "AUTO_LOGIN_AFTER_CONFIRM": True,
+ "EMAIL_SENDER": LocalProxy(
+ lambda: current_app.config.get("MAIL_DEFAULT_SENDER", "no-reply@localhost")
+ ),
+ "TWO_FACTOR_RESCUE_MAIL": "no-reply@localhost",
+ "TOKEN_AUTHENTICATION_KEY": "auth_token",
+ "TOKEN_AUTHENTICATION_HEADER": "Authentication-Token",
+ "TOKEN_MAX_AGE": None,
+ "CONFIRM_SALT": "confirm-salt",
+ "RESET_SALT": "reset-salt",
+ "LOGIN_SALT": "login-salt",
+ "CHANGE_SALT": "change-salt",
+ "REMEMBER_SALT": "remember-salt",
+ "DEFAULT_REMEMBER_ME": False,
+ "DEFAULT_HTTP_AUTH_REALM": _("Login Required"),
+ "EMAIL_SUBJECT_REGISTER": _("Welcome"),
+ "EMAIL_SUBJECT_CONFIRM": _("Please confirm your email"),
+ "EMAIL_SUBJECT_PASSWORDLESS": _("Login instructions"),
+ "EMAIL_SUBJECT_PASSWORD_NOTICE": _("Your password has been reset"),
+ "EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE": _("Your password has been changed"),
+ "EMAIL_SUBJECT_PASSWORD_RESET": _("Password reset instructions"),
+ "EMAIL_PLAINTEXT": True,
+ "EMAIL_HTML": True,
+ "EMAIL_SUBJECT_TWO_FACTOR": _("Two-factor Login"),
+ "EMAIL_SUBJECT_TWO_FACTOR_RESCUE": _("Two-factor Rescue"),
+ "USER_IDENTITY_ATTRIBUTES": ["email"],
+ "USER_IDENTITY_MAPPINGS": [
+ {"email": uia_email_mapper},
+ {"us_phone_number": uia_phone_mapper},
+ ],
+ "PHONE_REGION_DEFAULT": "US",
+ "FRESHNESS": timedelta(hours=24),
+ "FRESHNESS_GRACE_PERIOD": timedelta(hours=1),
+ "HASHING_SCHEMES": ["sha256_crypt", "hex_md5"],
+ "DEPRECATED_HASHING_SCHEMES": ["hex_md5"],
+ "DATETIME_FACTORY": datetime.utcnow,
+ "USE_VERIFY_PASSWORD_CACHE": False,
+ "VERIFY_HASH_CACHE_TTL": 60 * 5,
+ "VERIFY_HASH_CACHE_MAX_SIZE": 500,
+ "TOTP_SECRETS": None,
+ "TOTP_ISSUER": None,
+ "SMS_SERVICE": "Dummy",
+ "SMS_SERVICE_CONFIG": {
+ "ACCOUNT_SID": None,
+ "AUTH_TOKEN": None,
+ "PHONE_NUMBER": None,
+ },
+ "TWO_FACTOR_REQUIRED": False,
+ "TWO_FACTOR_SECRET": None, # Deprecated - use TOTP_SECRETS
+ "TWO_FACTOR_ENABLED_METHODS": ["email", "authenticator", "sms"],
+ "TWO_FACTOR_URI_SERVICE_NAME": "service_name", # Deprecated - use TOTP_ISSUER
+ "TWO_FACTOR_SMS_SERVICE": "Dummy", # Deprecated - use SMS_SERVICE
+ "TWO_FACTOR_SMS_SERVICE_CONFIG": { # Deprecated - use SMS_SERVICE_CONFIG
+ "ACCOUNT_SID": None,
+ "AUTH_TOKEN": None,
+ "PHONE_NUMBER": None,
+ },
+ "UNIFIED_SIGNIN": False,
+ "US_SETUP_SALT": "us-setup-salt",
+ "US_SIGNIN_URL": "/us-signin",
+ "US_SIGNIN_SEND_CODE_URL": "/us-signin/send-code",
+ "US_SETUP_URL": "/us-setup",
+ "US_VERIFY_URL": "/us-verify",
+ "US_VERIFY_SEND_CODE_URL": "/us-verify/send-code",
+ "US_VERIFY_LINK_URL": "/us-verify-link",
+ "US_QRCODE_URL": "/us-qrcode",
+ "US_POST_SETUP_VIEW": None,
+ "US_SIGNIN_TEMPLATE": "security/us_signin.html",
+ "US_SETUP_TEMPLATE": "security/us_setup.html",
+ "US_VERIFY_TEMPLATE": "security/us_verify.html",
+ "US_ENABLED_METHODS": ["password", "email", "authenticator", "sms"],
+ "US_MFA_REQUIRED": ["password", "email"],
+ "US_TOKEN_VALIDITY": 120,
+ "US_EMAIL_SUBJECT": _("Verification Code"),
+ "US_SETUP_WITHIN": "30 minutes",
+ "US_SIGNIN_REPLACES_LOGIN": False,
+ "CSRF_PROTECT_MECHANISMS": AUTHN_MECHANISMS,
+ "CSRF_IGNORE_UNAUTH_ENDPOINTS": False,
+ "CSRF_COOKIE": {"key": None},
+ "CSRF_HEADER": "X-XSRF-Token",
+ "CSRF_COOKIE_REFRESH_EACH_REQUEST": False,
+ "BACKWARDS_COMPAT_UNAUTHN": False,
+ "BACKWARDS_COMPAT_AUTH_TOKEN": False,
+ "BACKWARDS_COMPAT_AUTH_TOKEN_INVALIDATE": False,
+ "JOIN_USER_ROLES": True,
+}
+
+#: Default Flask-Security messages
+_default_messages = {
+ "API_ERROR": (_("Input not appropriate for requested API"), "error"),
+ "UNAUTHORIZED": (_("You do not have permission to view this resource."), "error"),
+ "UNAUTHENTICATED": (
+ _("You are not authenticated. Please supply the correct credentials."),
+ "error",
+ ),
+ "REAUTHENTICATION_REQUIRED": (
+ _("You must re-authenticate to access this endpoint"),
+ "error",
+ ),
+ "CONFIRM_REGISTRATION": (
+ _("Thank you. Confirmation instructions have been sent to %(email)s."),
+ "success",
+ ),
+ "EMAIL_CONFIRMED": (_("Thank you. Your email has been confirmed."), "success"),
+ "ALREADY_CONFIRMED": (_("Your email has already been confirmed."), "info"),
+ "INVALID_CONFIRMATION_TOKEN": (_("Invalid confirmation token."), "error"),
+ "EMAIL_ALREADY_ASSOCIATED": (
+ _("%(email)s is already associated with an account."),
+ "error",
+ ),
+ "PASSWORD_MISMATCH": (_("Password does not match"), "error"),
+ "RETYPE_PASSWORD_MISMATCH": (_("Passwords do not match"), "error"),
+ "INVALID_REDIRECT": (_("Redirections outside the domain are forbidden"), "error"),
+ "PASSWORD_RESET_REQUEST": (
+ _("Instructions to reset your password have been sent to %(email)s."),
+ "info",
+ ),
+ "PASSWORD_RESET_EXPIRED": (
+ _(
+ "You did not reset your password within %(within)s. "
+ "New instructions have been sent to %(email)s."
+ ),
+ "error",
+ ),
+ "INVALID_RESET_PASSWORD_TOKEN": (_("Invalid reset password token."), "error"),
+ "CONFIRMATION_REQUIRED": (_("Email requires confirmation."), "error"),
+ "CONFIRMATION_REQUEST": (
+ _("Confirmation instructions have been sent to %(email)s."),
+ "info",
+ ),
+ "CONFIRMATION_EXPIRED": (
+ _(
+ "You did not confirm your email within %(within)s. "
+ "New instructions to confirm your email have been sent "
+ "to %(email)s."
+ ),
+ "error",
+ ),
+ "LOGIN_EXPIRED": (
+ _(
+ "You did not login within %(within)s. New instructions to login "
+ "have been sent to %(email)s."
+ ),
+ "error",
+ ),
+ "LOGIN_EMAIL_SENT": (
+ _("Instructions to login have been sent to %(email)s."),
+ "success",
+ ),
+ "INVALID_LOGIN_TOKEN": (_("Invalid login token."), "error"),
+ "DISABLED_ACCOUNT": (_("Account is disabled."), "error"),
+ "EMAIL_NOT_PROVIDED": (_("Email not provided"), "error"),
+ "INVALID_EMAIL_ADDRESS": (_("Invalid email address"), "error"),
+ "INVALID_CODE": (_("Invalid code"), "error"),
+ "PASSWORD_NOT_PROVIDED": (_("Password not provided"), "error"),
+ "PASSWORD_NOT_SET": (_("No password is set for this user"), "error"),
+ "PASSWORD_INVALID_LENGTH": (
+ _("Password must be at least %(length)s characters"),
+ "error",
+ ),
+ "PASSWORD_TOO_SIMPLE": (_("Password not complex enough"), "error"),
+ "PASSWORD_BREACHED": (_("Password on breached list"), "error"),
+ "PASSWORD_BREACHED_SITE_ERROR": (
+ _("Failed to contact breached passwords site"),
+ "error",
+ ),
+ "PHONE_INVALID": (_("Phone number not valid e.g. missing country code"), "error"),
+ "USER_DOES_NOT_EXIST": (_("Specified user does not exist"), "error"),
+ "INVALID_PASSWORD": (_("Invalid password"), "error"),
+ "INVALID_PASSWORD_CODE": (_("Password or code submitted is not valid"), "error"),
+ "PASSWORDLESS_LOGIN_SUCCESSFUL": (_("You have successfully logged in."), "success"),
+ "FORGOT_PASSWORD": (_("Forgot password?"), "info"),
+ "PASSWORD_RESET": (
+ _(
+ "You successfully reset your password and you have been logged in "
+ "automatically."
+ ),
+ "success",
+ ),
+ "PASSWORD_IS_THE_SAME": (
+ _("Your new password must be different than your previous password."),
+ "error",
+ ),
+ "PASSWORD_CHANGE": (_("You successfully changed your password."), "success"),
+ "LOGIN": (_("Please log in to access this page."), "info"),
+ "REFRESH": (_("Please reauthenticate to access this page."), "info"),
+ "REAUTHENTICATION_SUCCESSFUL": (_("Reauthentication successful"), "info"),
+ "ANONYMOUS_USER_REQUIRED": (
+ _("You can only access this endpoint when not logged in."),
+ "error",
+ ),
+ "FAILED_TO_SEND_CODE": (_("Failed to send code. Please try again later"), "error"),
+ "TWO_FACTOR_INVALID_TOKEN": (_("Invalid Token"), "error"),
+ "TWO_FACTOR_LOGIN_SUCCESSFUL": (_("Your token has been confirmed"), "success"),
+ "TWO_FACTOR_CHANGE_METHOD_SUCCESSFUL": (
+ _("You successfully changed your two-factor method."),
+ "success",
+ ),
+ "TWO_FACTOR_PASSWORD_CONFIRMATION_DONE": (
+ _("You successfully confirmed password"),
+ "success",
+ ),
+ "TWO_FACTOR_PASSWORD_CONFIRMATION_NEEDED": (
+ _("Password confirmation is needed in order to access page"),
+ "error",
+ ),
+ "TWO_FACTOR_PERMISSION_DENIED": (
+ _("You currently do not have permissions to access this page"),
+ "error",
+ ),
+ "TWO_FACTOR_METHOD_NOT_AVAILABLE": (_("Marked method is not valid"), "error"),
+ "TWO_FACTOR_DISABLED": (
+ _("You successfully disabled two factor authorization."),
+ "success",
+ ),
+ "US_METHOD_NOT_AVAILABLE": (_("Requested method is not valid"), "error"),
+ "US_SETUP_EXPIRED": (
+ _("Setup must be completed within %(within)s. Please start over."),
+ "error",
+ ),
+ "US_SETUP_SUCCESSFUL": (_("Unified sign in setup successful"), "info"),
+ "US_SPECIFY_IDENTITY": (_("You must specify a valid identity to sign in"), "error"),
+ "USE_CODE": (_("Use this code to sign in: %(code)s."), "info"),
+}
+
+_default_forms = {
+ "login_form": LoginForm,
+ "verify_form": VerifyForm,
+ "confirm_register_form": ConfirmRegisterForm,
+ "register_form": RegisterForm,
+ "forgot_password_form": ForgotPasswordForm,
+ "reset_password_form": ResetPasswordForm,
+ "change_password_form": ChangePasswordForm,
+ "send_confirmation_form": SendConfirmationForm,
+ "passwordless_login_form": PasswordlessLoginForm,
+ "two_factor_verify_code_form": TwoFactorVerifyCodeForm,
+ "two_factor_setup_form": TwoFactorSetupForm,
+ "two_factor_verify_password_form": TwoFactorVerifyPasswordForm,
+ "two_factor_rescue_form": TwoFactorRescueForm,
+ "us_signin_form": UnifiedSigninForm,
+ "us_setup_form": UnifiedSigninSetupForm,
+ "us_setup_validate_form": UnifiedSigninSetupValidateForm,
+ "us_verify_form": UnifiedVerifyForm,
+}
+
+
+def _user_loader(user_id):
+ """ Try to load based on fs_uniquifier (alternative_id) if available.
+
+ Note that we don't try, and fall back to the other - primarily because some DBs
+ and drivers (psycopg2) really really hate getting mismatched types during queries.
+ They hate it enough that they abort the 'transaction' and refuse to do anything
+ in the future until the transaction is rolled-back. But we don't really control
+ that and there doesn't seem to be any way to catch the actual offensive query -
+ just next time and forever, things fail.
+ This assumes that if the app has fs_uniquifier, it is non-nullable as we specify
+ so we use that and only that.
+ """
+ if hasattr(_datastore.user_model, "fs_uniquifier"):
+ selector = dict(fs_uniquifier=str(user_id))
+ else:
+ selector = dict(id=user_id)
+ user = _security.datastore.find_user(**selector)
+ if user and user.active:
+ return user
+ return None
+
+
+def _request_loader(request):
+ # Short-circuit if we have already been called and verified.
+ # This can happen since Flask-Login will call us (if no session) and our own
+ # decorator @auth_token_required can call us.
+ # N.B. we don't call current_user here since that in fact might try and LOAD
+ # a user - which would call us again.
+ if all(hasattr(_request_ctx_stack.top, k) for k in ["fs_authn_via", "user"]):
+ if _request_ctx_stack.top.fs_authn_via == "token":
+ return _request_ctx_stack.top.user
+
+ header_key = _security.token_authentication_header
+ args_key = _security.token_authentication_key
+ header_token = request.headers.get(header_key, None)
+ token = request.args.get(args_key, header_token)
+ if request.is_json:
+ data = request.get_json(silent=True) or {}
+ if isinstance(data, dict):
+ token = data.get(args_key, token)
+
+ use_cache = cv("USE_VERIFY_PASSWORD_CACHE")
+
+ try:
+ data = _security.remember_token_serializer.loads(
+ token, max_age=_security.token_max_age
+ )
+ user = _security.datastore.find_user(id=data[0])
+ if not user.active:
+ user = None
+ except Exception:
+ user = None
+
+ if not user:
+ return _security.login_manager.anonymous_user()
+ if use_cache:
+ cache = getattr(local_cache, "verify_hash_cache", None)
+ if cache is None:
+ cache = VerifyHashCache()
+ local_cache.verify_hash_cache = cache
+ if cache.has_verify_hash_cache(user):
+ _request_ctx_stack.top.fs_authn_via = "token"
+ return user
+ if user.verify_auth_token(data):
+ _request_ctx_stack.top.fs_authn_via = "token"
+ cache.set_cache(user)
+ return user
+ else:
+ if user.verify_auth_token(data):
+ _request_ctx_stack.top.fs_authn_via = "token"
+ return user
+
+ return _security.login_manager.anonymous_user()
+
+
+def _identity_loader():
+ if not isinstance(current_user._get_current_object(), AnonymousUserMixin):
+ identity = Identity(current_user.id)
+ return identity
+
+
+def _on_identity_loaded(sender, identity):
+ if hasattr(current_user, "id"):
+ identity.provides.add(UserNeed(current_user.id))
+
+ for role in getattr(current_user, "roles", []):
+ identity.provides.add(RoleNeed(role.name))
+ for fsperm in role.get_permissions():
+ identity.provides.add(FsPermNeed(fsperm))
+
+ identity.user = current_user
+
+
+def _get_login_manager(app, anonymous_user):
+ lm = LoginManager()
+ lm.anonymous_user = anonymous_user or AnonymousUser
+ lm.localize_callback = localize_callback
+ lm.login_view = "%s.login" % cv("BLUEPRINT_NAME", app=app)
+ lm.user_loader(_user_loader)
+ lm.request_loader(_request_loader)
+
+ if cv("FLASH_MESSAGES", app=app):
+ lm.login_message, lm.login_message_category = cv("MSG_LOGIN", app=app)
+ lm.needs_refresh_message, lm.needs_refresh_message_category = cv(
+ "MSG_REFRESH", app=app
+ )
+ else:
+ lm.login_message = None
+ lm.needs_refresh_message = None
+
+ lm.init_app(app)
+ return lm
+
+
+def _get_principal(app):
+ p = Principal(app, use_sessions=False)
+ p.identity_loader(_identity_loader)
+ return p
+
+
+def _get_pwd_context(app):
+ pw_hash = cv("PASSWORD_HASH", app=app)
+ schemes = cv("PASSWORD_SCHEMES", app=app)
+ deprecated = cv("DEPRECATED_PASSWORD_SCHEMES", app=app)
+ if pw_hash not in schemes:
+ allowed = ", ".join(schemes[:-1]) + " and " + schemes[-1]
+ raise ValueError(
+ "Invalid password hashing scheme %r. Allowed values are %s"
+ % (pw_hash, allowed)
+ )
+ cc = CryptContext(
+ schemes=schemes,
+ default=pw_hash,
+ deprecated=deprecated,
+ **cv("PASSWORD_HASH_PASSLIB_OPTIONS", app=app)
+ )
+ return cc
+
+
+def _get_i18n_domain(app):
+ return Domain(
+ dirname=cv("I18N_DIRNAME", app=app), domain=cv("I18N_DOMAIN", app=app)
+ )
+
+
+def _get_hashing_context(app):
+ schemes = cv("HASHING_SCHEMES", app=app)
+ deprecated = cv("DEPRECATED_HASHING_SCHEMES", app=app)
+ return CryptContext(schemes=schemes, deprecated=deprecated)
+
+
+def _get_serializer(app, name):
+ secret_key = app.config.get("SECRET_KEY")
+ salt = app.config.get("SECURITY_%s_SALT" % name.upper())
+ return URLSafeTimedSerializer(secret_key=secret_key, salt=salt)
+
+
+def _get_state(app, datastore, anonymous_user=None, **kwargs):
+ for key, value in get_config(app).items():
+ kwargs[key.lower()] = value
+
+ kwargs.update(
+ dict(
+ app=app,
+ datastore=datastore,
+ principal=_get_principal(app),
+ pwd_context=_get_pwd_context(app),
+ hashing_context=_get_hashing_context(app),
+ i18n_domain=_get_i18n_domain(app),
+ remember_token_serializer=_get_serializer(app, "remember"),
+ login_serializer=_get_serializer(app, "login"),
+ reset_serializer=_get_serializer(app, "reset"),
+ confirm_serializer=_get_serializer(app, "confirm"),
+ us_setup_serializer=_get_serializer(app, "us_setup"),
+ _context_processors={},
+ _send_mail_task=None,
+ _send_mail=kwargs.get("send_mail", send_mail),
+ _unauthorized_callback=None,
+ _render_json=default_render_json,
+ _want_json=default_want_json,
+ _unauthn_handler=default_unauthn_handler,
+ _reauthn_handler=default_reauthn_handler,
+ _unauthz_handler=default_unauthz_handler,
+ _password_validator=default_password_validator,
+ )
+ )
+
+ if "login_manager" not in kwargs:
+ kwargs["login_manager"] = _get_login_manager(app, anonymous_user)
+
+ for key, value in _default_forms.items():
+ if key not in kwargs or not kwargs[key]:
+ kwargs[key] = value
+
+ return _SecurityState(**kwargs)
+
+
+def _context_processor():
+ return dict(url_for_security=url_for_security, security=_security)
+
+
+class RoleMixin(object):
+ """Mixin for `Role` model definitions"""
+
+ def __eq__(self, other):
+ return self.name == other or self.name == getattr(other, "name", None)
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ def __hash__(self):
+ return hash(self.name)
+
+ def get_permissions(self):
+ """
+ Return set of permissions associated with role.
+
+ Either takes a comma separated string of permissions or
+ an interable of strings if permissions are in their own
+ table.
+
+ .. versionadded:: 3.3.0
+ """
+ if hasattr(self, "permissions") and self.permissions:
+ if isinstance(self.permissions, set):
+ return self.permissions
+ elif isinstance(self.permissions, list):
+ return set(self.permissions)
+ else:
+ # Assume this is a comma separated list
+ return set(self.permissions.split(","))
+ return set([])
+
+ def add_permissions(self, permissions):
+ """
+ Add one or more permissions to role.
+
+ :param permissions: a set, list, or single string.
+
+ Caller must commit to DB.
+
+ .. versionadded:: 3.3.0
+ """
+ if hasattr(self, "permissions"):
+ current_perms = self.get_permissions()
+ if isinstance(permissions, set):
+ perms = permissions
+ elif isinstance(permissions, list):
+ perms = set(permissions)
+ else:
+ perms = {permissions}
+ self.permissions = ",".join(current_perms.union(perms))
+ else:
+ raise NotImplementedError("Role model doesn't have permissions")
+
+ def remove_permissions(self, permissions):
+ """
+ Remove one or more permissions from role.
+
+ :param permissions: a set, list, or single string.
+
+ Caller must commit to DB.
+
+ .. versionadded:: 3.3.0
+ """
+ if hasattr(self, "permissions"):
+ current_perms = self.get_permissions()
+ if isinstance(permissions, set):
+ perms = permissions
+ elif isinstance(permissions, list):
+ perms = set(permissions)
+ else:
+ perms = {permissions}
+ self.permissions = ",".join(current_perms.difference(perms))
+ else:
+ raise NotImplementedError("Role model doesn't have permissions")
+
+
+class UserMixin(BaseUserMixin):
+ """Mixin for `User` model definitions"""
+
+ def get_id(self):
+ """Returns the user identification attribute.
+
+ This will be `fs_uniquifier` if that is available, else base class id
+ (which is via Flask-Login and is user.id).
+
+ .. versionadded:: 3.4.0
+ """
+ if hasattr(self, "fs_uniquifier") and self.fs_uniquifier is not None:
+ # Use fs_uniquifier as alternative_id if available and not None
+ alternative_id = str(self.fs_uniquifier)
+ if len(alternative_id) > 0:
+ # Return only if alternative_id is a valid value
+ return alternative_id
+
+ # Use upstream value if alternative_id is unavailable
+ return BaseUserMixin.get_id(self)
+
+ @property
+ def is_active(self):
+ """Returns `True` if the user is active."""
+ return self.active
+
+ def get_auth_token(self):
+ """Constructs the user's authentication token.
+
+ This data MUST be securely signed using the ``remember_token_serializer``
+ """
+ data = [str(self.id), hash_data(self.password)]
+ if hasattr(self, "fs_uniquifier"):
+ data.append(self.fs_uniquifier)
+ return _security.remember_token_serializer.dumps(data)
+
+ def verify_auth_token(self, data):
+ """
+ Perform additional verification of contents of auth token.
+ Prior to this being called the token has been validated (via signing)
+ and has not expired.
+
+ :param data: the data as formulated by :meth:`get_auth_token`
+
+ .. versionadded:: 3.3.0
+ """
+ if len(data) > 2 and hasattr(self, "fs_uniquifier"):
+ # has uniquifier - use that
+ if data[2] == self.fs_uniquifier:
+ return True
+ # Don't even try old way - if they have defined a uniquifier
+ # we want that to be able to invalidate tokens if changed.
+ return False
+ # Fall back to old and very expensive check
+ if verify_hash(data[1], self.password):
+ return True
+ return False
+
+ def has_role(self, role):
+ """Returns `True` if the user identifies with the specified role.
+
+ :param role: A role name or `Role` instance"""
+ if isinstance(role, string_types):
+ return role in (role.name for role in self.roles)
+ else:
+ return role in self.roles
+
+ def has_permission(self, permission):
+ """
+ Returns `True` if user has this permission (via a role it has).
+
+ :param permission: permission string name
+
+ .. versionadded:: 3.3.0
+
+ """
+ for role in self.roles:
+ if hasattr(role, "permissions"):
+ if permission in role.get_permissions():
+ return True
+ return False
+
+ def get_security_payload(self):
+ """Serialize user object as response payload."""
+ return {"id": str(self.id)}
+
+ def get_redirect_qparams(self, existing=None):
+ """Return user info that will be added to redirect query params.
+
+ :param existing: A dict that will be updated.
+ :return: A dict whose keys will be query params and values will be query values.
+
+ .. versionadded:: 3.2.0
+ """
+ if not existing:
+ existing = {}
+ existing.update({"email": self.email})
+ return existing
+
+ def verify_and_update_password(self, password):
+ """Returns ``True`` if the password is valid for the specified user.
+
+ Additionally, the hashed password in the database is updated if the
+ hashing algorithm happens to have changed.
+
+ N.B. you MUST call DB commit if you are using a session-based datastore
+ (such as SqlAlchemy) since the user instance might have been altered
+ (i.e. ``app.security.datastore.commit()``).
+ This is usually handled in the view.
+
+ :param password: A plaintext password to verify
+
+ .. versionadded:: 3.2.0
+ """
+ return verify_and_update_password(password, self)
+
+ def calc_username(self):
+ """ Come up with the best 'username' based on how the app
+ is configured (via :py:data:`SECURITY_USER_IDENTITY_ATTRIBUTES`).
+ Returns the first non-null match (and converts to string).
+ In theory this should NEVER be the empty string unless the user
+ record isn't actually valid.
+
+ .. versionadded:: 3.4.0
+ """
+ cusername = None
+ for attr in get_identity_attributes():
+ cusername = getattr(self, attr, None)
+ if cusername is not None and len(str(cusername)) > 0:
+ break
+ return str(cusername) if cusername is not None else ""
+
+ def us_send_security_token(self, method, **kwargs):
+ """ Generate and send the security code for unified sign in.
+
+ :param method: The method in which the code will be sent
+ :param kwargs: Opaque parameters that are subject to change at any time
+ :return: None if successful, error message if not.
+
+ This is a wrapper around :meth:`us_send_security_token`
+ that can be overridden to manage any errors.
+
+ .. versionadded:: 3.4.0
+ """
+ try:
+ us_send_security_token(self, method, **kwargs)
+ except Exception:
+ return get_message("FAILED_TO_SEND_CODE")[0]
+ return None
+
+ def tf_send_security_token(self, method, **kwargs):
+ """ Generate and send the security code for two-factor.
+
+ :param method: The method in which the code will be sent
+ :param kwargs: Opaque parameters that are subject to change at any time
+ :return: None if successful, error message if not.
+
+ This is a wrapper around :meth:`tf_send_security_token`
+ that can be overridden to manage any errors.
+
+ .. versionadded:: 3.4.0
+ """
+ try:
+ tf_send_security_token(self, method, **kwargs)
+ except Exception:
+ return get_message("FAILED_TO_SEND_CODE")[0]
+ return None
+
+
+class AnonymousUser(AnonymousUserMixin):
+ """AnonymousUser definition"""
+
+ def __init__(self):
+ self.roles = ImmutableList()
+
+ def has_role(self, *args):
+ """Returns `False`"""
+ return False
+
+
+class _SecurityState(object):
+ def __init__(self, **kwargs):
+ for key, value in kwargs.items():
+ setattr(self, key.lower(), value)
+
+ def _add_ctx_processor(self, endpoint, fn):
+ group = self._context_processors.setdefault(endpoint, [])
+ fn not in group and group.append(fn)
+
+ def _run_ctx_processor(self, endpoint):
+ rv = {}
+ for g in [None, endpoint]:
+ for fn in self._context_processors.setdefault(g, []):
+ rv.update(fn())
+ return rv
+
+ def context_processor(self, fn):
+ self._add_ctx_processor(None, fn)
+
+ def forgot_password_context_processor(self, fn):
+ self._add_ctx_processor("forgot_password", fn)
+
+ def login_context_processor(self, fn):
+ self._add_ctx_processor("login", fn)
+
+ def register_context_processor(self, fn):
+ self._add_ctx_processor("register", fn)
+
+ def reset_password_context_processor(self, fn):
+ self._add_ctx_processor("reset_password", fn)
+
+ def change_password_context_processor(self, fn):
+ self._add_ctx_processor("change_password", fn)
+
+ def send_confirmation_context_processor(self, fn):
+ self._add_ctx_processor("send_confirmation", fn)
+
+ def send_login_context_processor(self, fn):
+ self._add_ctx_processor("send_login", fn)
+
+ def verify_context_processor(self, fn):
+ self._add_ctx_processor("verify", fn)
+
+ def mail_context_processor(self, fn):
+ self._add_ctx_processor("mail", fn)
+
+ def tf_verify_password_context_processor(self, fn):
+ self._add_ctx_processor("tf_verify_password", fn)
+
+ def tf_setup_context_processor(self, fn):
+ self._add_ctx_processor("tf_setup", fn)
+
+ def tf_token_validation_context_processor(self, fn):
+ self._add_ctx_processor("tf_token_validation", fn)
+
+ def us_signin_context_processor(self, fn):
+ self._add_ctx_processor("us_signin", fn)
+
+ def us_setup_context_processor(self, fn):
+ self._add_ctx_processor("us_setup", fn)
+
+ def us_verify_context_processor(self, fn):
+ self._add_ctx_processor("us_verify", fn)
+
+ def send_mail_task(self, fn):
+ self._send_mail_task = fn
+
+ def send_mail(self, fn):
+ self._send_mail = fn
+
+ def unauthorized_handler(self, fn):
+ warnings.warn(
+ "'unauthorized_handler' has been replaced with"
+ " 'unauthz_handler' and 'unauthn_handler'",
+ DeprecationWarning,
+ )
+ self._unauthorized_callback = fn
+
+ def totp_factory(self, tf):
+ self._totp_factory = tf
+
+ def render_json(self, fn):
+ self._render_json = fn
+
+ def want_json(self, fn):
+ self._want_json = fn
+
+ def unauthz_handler(self, cb):
+ self._unauthz_handler = cb
+
+ def unauthn_handler(self, cb):
+ self._unauthn_handler = cb
+
+ def reauthn_handler(self, cb):
+ self._reauthn_handler = cb
+
+ def password_validator(self, cb):
+ self._password_validator = cb
+
+
+class Security(object):
+ """The :class:`Security` class initializes the Flask-Security extension.
+
+ :param app: The application.
+ :param datastore: An instance of a user datastore.
+ :param register_blueprint: to register the Security blueprint or not.
+ :param login_form: set form for the login view
+ :param verify_form: set form for re-authentication due to freshness check
+ :param register_form: set form for the register view when
+ *SECURITY_CONFIRMABLE* is false
+ :param confirm_register_form: set form for the register view when
+ *SECURITY_CONFIRMABLE* is true
+ :param forgot_password_form: set form for the forgot password view
+ :param reset_password_form: set form for the reset password view
+ :param change_password_form: set form for the change password view
+ :param send_confirmation_form: set form for the send confirmation view
+ :param passwordless_login_form: set form for the passwordless login view
+ :param two_factor_setup_form: set form for the 2FA setup view
+ :param two_factor_verify_code_form: set form the the 2FA verify code view
+ :param two_factor_rescue_form: set form for the 2FA rescue view
+ :param two_factor_verify_password_form: set form for the 2FA verify password view
+ :param us_signin_form: set form for the unified sign in view
+ :param us_setup_form: set form for the unified sign in setup view
+ :param us_setup_validate_form: set form for the unified sign in setup validate view
+ :param us_verify_form: set form for re-authenticating due to freshness check
+ :param anonymous_user: class to use for anonymous user
+ :param render_template: function to use to render templates. The default is Flask's
+ render_template() function.
+ :param send_mail: function to use to send email. Defaults to :func:`send_mail`
+ :param json_encoder_cls: Class to use as blueprint.json_encoder.
+ Defaults to :class:`FsJsonEncoder`
+ :param totp_cls: Class to use as TOTP factory. Defaults to :class:`Totp`
+ :param phone_util_cls: Class to use for phone number utilities.
+ Defaults to :class:`PhoneUtil`
+
+ .. versionadded:: 3.4.0
+ ``verify_form`` added as part of freshness/re-authentication
+
+ .. versionadded:: 3.4.0
+ ``us_signin_form``, ``us_setup_form``, ``us_setup_validate_form``, and
+ ``us_verify_form`` added as part of the :ref:`unified-sign-in` feature.
+
+ .. versionadded:: 3.4.0
+ ``totp_cls`` added to enable applications to implement replay protection - see
+ :py:class:`Totp`.
+
+ .. versionadded:: 3.4.0
+ ``phone_util_cls`` added to allow different phone number
+ parsing implementations - see :py:class:`PhoneUtil`
+ """
+
+ def __init__(self, app=None, datastore=None, register_blueprint=True, **kwargs):
+
+ self.app = app
+ self._datastore = datastore
+ self._register_blueprint = register_blueprint
+ self._kwargs = kwargs
+
+ self._state = None # set by init_app
+ if app is not None and datastore is not None:
+ self._state = self.init_app(
+ app, datastore, register_blueprint=register_blueprint, **kwargs
+ )
+
+ def init_app(self, app, datastore=None, register_blueprint=None, **kwargs):
+ """Initializes the Flask-Security extension for the specified
+ application and datastore implementation.
+
+ :param app: The application.
+ :param datastore: An instance of a user datastore.
+ :param register_blueprint: to register the Security blueprint or not.
+ """
+ self.app = app
+
+ if datastore is None:
+ datastore = self._datastore
+
+ if register_blueprint is None:
+ register_blueprint = self._register_blueprint
+
+ for key, value in self._kwargs.items():
+ kwargs.setdefault(key, value)
+
+ if "render_template" not in kwargs:
+ kwargs.setdefault("render_template", self.render_template)
+ if "json_encoder_cls" not in kwargs:
+ kwargs.setdefault("json_encoder_cls", FsJsonEncoder)
+ if "totp_cls" not in kwargs:
+ kwargs.setdefault("totp_cls", Totp)
+ if "phone_util_cls" not in kwargs:
+ kwargs.setdefault("phone_util_cls", PhoneUtil)
+
+ for key, value in _default_config.items():
+ app.config.setdefault("SECURITY_" + key, value)
+
+ for key, value in _default_messages.items():
+ app.config.setdefault("SECURITY_MSG_" + key, value)
+
+ identity_loaded.connect_via(app)(_on_identity_loaded)
+
+ self._state = state = _get_state(app, datastore, **kwargs)
+
+ if register_blueprint:
+ bp = create_blueprint(
+ app, state, __name__, json_encoder=kwargs["json_encoder_cls"]
+ )
+ app.register_blueprint(bp)
+ app.context_processor(_context_processor)
+
+ @app.before_first_request
+ def _register_i18n():
+ # N.B. as of jinja 2.9 '_' is always registered
+ # http://jinja.pocoo.org/docs/2.10/extensions/#i18n-extension
+ if "_" not in app.jinja_env.globals:
+ current_app.jinja_env.globals["_"] = state.i18n_domain.gettext
+ # Register so other packages can reference our translations.
+ current_app.jinja_env.globals["_fsdomain"] = state.i18n_domain.gettext
+
+ @app.before_first_request
+ def _csrf_init():
+ # various config checks - some of these are opinionated in that there
+ # could be a reason for some of these combinations - but in general
+ # they cause strange behavior.
+ # WTF_CSRF_ENABLED defaults to True if not set in Flask-WTF
+ if not current_app.config.get("WTF_CSRF_ENABLED", True):
+ return
+ csrf = current_app.extensions.get("csrf", None)
+
+ # If they don't want ALL mechanisms protected, then they must
+ # set WTF_CSRF_CHECK_DEFAULT=False so that our decorators get control.
+ if cv("CSRF_PROTECT_MECHANISMS") != AUTHN_MECHANISMS:
+ if not csrf:
+ # This isn't good.
+ raise ValueError(
+ "CSRF_PROTECT_MECHANISMS defined but"
+ " CsrfProtect not part of application"
+ )
+ if current_app.config.get("WTF_CSRF_CHECK_DEFAULT", True):
+ raise ValueError(
+ "WTF_CSRF_CHECK_DEFAULT must be set to False if"
+ " CSRF_PROTECT_MECHANISMS is set"
+ )
+ # We don't get control unless they turn off WTF_CSRF_CHECK_DEFAULT if
+ # they have enabled global CSRFProtect.
+ if (
+ cv("CSRF_IGNORE_UNAUTH_ENDPOINTS")
+ and csrf
+ and current_app.config.get("WTF_CSRF_CHECK_DEFAULT", False)
+ ):
+ raise ValueError(
+ "To ignore unauth endpoints you must set WTF_CSRF_CHECK_DEFAULT"
+ " to False"
+ )
+
+ csrf_cookie = cv("CSRF_COOKIE")
+ if csrf_cookie and csrf_cookie["key"] and not csrf:
+ # Common use case is for cookie value to be used as contents for header
+ # which is only looked at when CsrfProtect is initialized.
+ # Yes, this is opinionated - they can always get CSRF token via:
+ # 'get /login'
+ raise ValueError(
+ "CSRF_COOKIE defined however CsrfProtect not part of application"
+ )
+
+ if csrf:
+ csrf.exempt("flask_security.views.logout")
+ if csrf_cookie and csrf_cookie["key"]:
+ current_app.after_request(csrf_cookie_handler)
+ # Add configured header to WTF_CSRF_HEADERS
+ current_app.config["WTF_CSRF_HEADERS"].append(cv("CSRF_HEADER"))
+
+ @app.before_first_request
+ def _init_phone_util():
+ state._phone_util = state.phone_util_cls()
+
+ app.extensions["security"] = state
+
+ if hasattr(app, "cli"):
+ from .cli import users, roles
+
+ if state.cli_users_name:
+ app.cli.add_command(users, state.cli_users_name)
+ if state.cli_roles_name:
+ app.cli.add_command(roles, state.cli_roles_name)
+
+ # Migrate from TWO_FACTOR config to generic config.
+ for newc, oldc in [
+ ("SECURITY_SMS_SERVICE", "SECURITY_TWO_FACTOR_SMS_SERVICE"),
+ ("SECURITY_SMS_SERVICE_CONFIG", "SECURITY_TWO_FACTOR_SMS_SERVICE_CONFIG"),
+ ("SECURITY_TOTP_SECRETS", "SECURITY_TWO_FACTOR_SECRET"),
+ ("SECURITY_TOTP_ISSUER", "SECURITY_TWO_FACTOR_URI_SERVICE_NAME"),
+ ]:
+ if not app.config.get(newc, None):
+ app.config[newc] = app.config.get(oldc, None)
+
+ # Two factor configuration checks and setup
+ multi_factor = False
+ if cv("UNIFIED_SIGNIN", app=app):
+ multi_factor = True
+ if len(cv("US_ENABLED_METHODS", app=app)) < 1:
+ raise ValueError("Must configure some US_ENABLED_METHODS")
+ if cv("TWO_FACTOR", app=app):
+ multi_factor = True
+ if len(cv("TWO_FACTOR_ENABLED_METHODS", app=app)) < 1:
+ raise ValueError("Must configure some TWO_FACTOR_ENABLED_METHODS")
+
+ if multi_factor:
+ self._check_modules("pyqrcode", "TWO_FACTOR or UNIFIED_SIGNIN")
+ self._check_modules("cryptography", "TWO_FACTOR or UNIFIED_SIGNIN")
+
+ need_sms = (
+ cv("UNIFIED_SIGNIN", app=app)
+ and "sms" in cv("US_ENABLED_METHODS", app=app)
+ ) or (
+ cv("TWO_FACTOR", app=app)
+ and "sms" in cv("TWO_FACTOR_ENABLED_METHODS", app=app)
+ )
+ if need_sms:
+ sms_service = cv("SMS_SERVICE", app=app)
+ if sms_service == "Twilio": # pragma: no cover
+ self._check_modules("twilio", "SMS")
+ if state.phone_util_cls == PhoneUtil:
+ self._check_modules("phonenumbers", "SMS")
+
+ secrets = cv("TOTP_SECRETS", app=app)
+ issuer = cv("TOTP_ISSUER", app=app)
+ if not secrets or not issuer:
+ raise ValueError("Both TOTP_SECRETS and TOTP_ISSUER must be set")
+ state.totp_factory(state.totp_cls(secrets, issuer))
+
+ if cv("USE_VERIFY_PASSWORD_CACHE", app=app):
+ self._check_modules("cachetools", "USE_VERIFY_PASSWORD_CACHE")
+
+ if cv("PASSWORD_COMPLEXITY_CHECKER", app=app) == "zxcvbn":
+ self._check_modules("zxcvbn", "PASSWORD_COMPLEXITY_CHECKER")
+ return state
+
+ def _check_modules(self, module, config_name): # pragma: no cover
+ PY3 = sys.version_info[0] == 3
+ if PY3:
+ from importlib.util import find_spec
+
+ module_exists = find_spec(module)
+
+ else:
+ import imp
+
+ try:
+ imp.find_module(module)
+ module_exists = True
+ except ImportError:
+ module_exists = False
+
+ if not module_exists:
+ raise ValueError("{} is required for {}".format(module, config_name))
+
+ return module_exists
+
+ def render_template(self, *args, **kwargs):
+ return render_template(*args, **kwargs)
+
+ def send_mail(self, fn):
+ """ Function used to send emails.
+
+ :param fn: Function with signature(subject, recipient, template, context)
+
+ See :meth:`send_mail` for details.
+
+ .. versionadded:: 3.1.0
+ """
+ self._state._send_mail = fn
+
+ def render_json(self, cb):
+ """ Callback to render response payload as JSON.
+
+ :param cb: Callback function with
+ signature (payload, code, headers=None, user=None)
+
+ :payload: A dict. Please see the formal API spec for details.
+ :code: Http status code
+ :headers: Headers object
+ :user: the UserDatastore object (or None). Note that this is usually
+ the same as current_user - but not always.
+
+ The default implementation simply returns::
+
+ headers["Content-Type"] = "application/json"
+ payload = dict(meta=dict(code=code), response=payload)
+ return make_response(jsonify(payload), code, headers)
+
+ .. important::
+ Be aware the Flask's ``jsonify`` method will first look to see if a
+ ``json_encoder`` has been set on the blueprint corresponding to the current
+ request. If not then it looks for a ``json_encoder`` registered on the app;
+ and finally uses Flask's default JSONEncoder class. Flask-Security registers
+ :func:`FsJsonEncoder` as its blueprint json_encoder.
+
+
+ This can be used by applications to unify all their JSON API responses.
+ This is called in a request context and should return a Response or something
+ Flask can create a Response from.
+
+ .. versionadded:: 3.3.0
+ """
+ self._state._render_json = cb
+
+ def want_json(self, fn):
+ """ Function that returns True if response should be JSON (based on the request)
+
+ :param fn: Function with the following signature (request)
+
+ :request: Werkzueg/Flask request
+
+ The default implementation returns True if either the Content-Type is
+ "application/json" or the best Accept header value is "application/json".
+
+ .. versionadded:: 3.3.0
+ """
+ self._state._want_json = fn
+
+ def unauthz_handler(self, cb):
+ """
+ Callback for failed authorization.
+ This is called by the :func:`roles_required`, :func:`roles_accepted`,
+ :func:`permissions_required`, or :func:`permissions_accepted`
+ if a role or permission is missing.
+
+ :param cb: Callback function with signature (func, params)
+
+ :func: the decorator function (e.g. roles_required)
+ :params: list of what (if any) was passed to the decorator.
+
+ Should return a Response or something Flask can create a Response from.
+ Can raise an exception if it is handled as part of
+ flask.errorhandler(<exception>)
+
+ With the passed parameters the application could deliver a concise error
+ message.
+
+ .. versionadded:: 3.3.0
+ """
+ self._state._unauthz_handler = cb
+
+ def unauthn_handler(self, cb):
+ """
+ Callback for failed authentication.
+ This is called by :func:`auth_required`, :func:`auth_token_required`
+ or :func:`http_auth_required` if authentication fails.
+
+ :param cb: Callback function with signature (mechanisms, headers=None)
+
+ :mechanisms: List of which authentication mechanisms were tried
+ :headers: dict of headers to return
+
+ Should return a Response or something Flask can create a Response from.
+ Can raise an exception if it is handled as part of
+ ``flask.errorhandler(<exception>)``
+
+ The default implementation will return a 401 response if the request was JSON,
+ otherwise lets ``flask_login.login_manager.unauthorized()`` handle redirects.
+
+ .. versionadded:: 3.3.0
+ """
+ self._state._unauthn_handler = cb
+
+ def reauthn_handler(self, cb):
+ """
+ Callback when endpoint required a fresh authentication.
+ This is called by :func:`auth_required`.
+
+ :param cb: Callback function with signature (within, grace)
+
+ :within: timedelta that endpoint required fresh authentication within.
+ :grace: timedelta of grace period that endpoint allowed.
+
+ Should return a Response or something Flask can create a Response from.
+ Can raise an exception if it is handled as part of
+ ``flask.errorhandler(<exception>)``
+
+ The default implementation will return a 401 response if the request was JSON,
+ otherwise will redirect to :py:data:`SECURITY_US_VERIFY_URL`
+ (if :py:data:`SECURITY_UNIFIED_SIGNIN` is enabled)
+ else to :py:data:`SECURITY_VERIFY_URL`.
+ If both of those are None it sends an ``abort(401)``.
+
+ See :meth:`flask_security.auth_required` for details about freshness checking.
+
+ .. versionadded:: 3.4.0
+ """
+ self._state._reauthn_handler = cb
+
+ def password_validator(self, cb):
+ """
+ Callback for validating a user password.
+ This is called on registration as well as change and reset password.
+ For registration, ``kwargs`` will be all the form input fields that are
+ attributes of the user model.
+ For reset/change, ``kwargs`` will be user=UserModel
+
+ :param cb: Callback function with signature (password, is_register, kwargs)
+
+ :password: desired new plain text password
+ :is_register: True if called as part of initial registration
+ :kwargs: user info
+
+ Returns: None if password passes all validations. A list of (localized) messages
+ if not.
+
+ .. versionadded:: 3.4.0
+ Refer to :ref:`pass_validation_topic` for more information.
+ """
+ self._state._password_validator = cb
+
+ def __getattr__(self, name):
+ return getattr(self._state, name, None)
diff --git a/flask_security/datastore.py b/flask_security/datastore.py
new file mode 100644
index 0000000..bf37728
--- /dev/null
+++ b/flask_security/datastore.py
@@ -0,0 +1,733 @@
+# -*- coding: utf-8 -*-
+"""
+ flask_security.datastore
+ ~~~~~~~~~~~~~~~~~~~~~~~~
+
+ This module contains an user datastore classes.
+
+ :copyright: (c) 2012 by Matt Wright.
+ :copyright: (c) 2019-2020 by J. Christopher Wagner (jwag).
+ :license: MIT, see LICENSE for more details.
+"""
+import json
+import uuid
+
+from .utils import config_value, get_identity_attributes, string_types
+
+
+class Datastore(object):
+ def __init__(self, db):
+ self.db = db
+
+ def commit(self):
+ pass
+
+ def put(self, model):
+ raise NotImplementedError
+
+ def delete(self, model):
+ raise NotImplementedError
+
+
+class SQLAlchemyDatastore(Datastore):
+ def commit(self):
+ self.db.session.commit()
+
+ def put(self, model):
+ self.db.session.add(model)
+ return model
+
+ def delete(self, model):
+ self.db.session.delete(model)
+
+
+class MongoEngineDatastore(Datastore):
+ def put(self, model):
+ model.save()
+ return model
+
+ def delete(self, model):
+ model.delete()
+
+
+class PeeweeDatastore(Datastore):
+ def put(self, model):
+ model.save()
+ return model
+
+ def delete(self, model):
+ model.delete_instance(recursive=True)
+
+
+def with_pony_session(f):
+ from functools import wraps
+
+ @wraps(f)
+ def decorator(*args, **kwargs):
+ from pony.orm import db_session
+ from pony.orm.core import local
+ from flask import (
+ after_this_request,
+ current_app,
+ has_app_context,
+ has_request_context,
+ )
+ from flask.signals import appcontext_popped
+
+ register = local.db_context_counter == 0
+ if register and (has_app_context() or has_request_context()):
+ db_session.__enter__()
+
+ result = f(*args, **kwargs)
+
+ if register:
+ if has_request_context():
+
+ @after_this_request
+ def pop(request):
+ db_session.__exit__()
+ return request
+
+ elif has_app_context():
+
+ @appcontext_popped.connect_via(current_app._get_current_object())
+ def pop(sender, *args, **kwargs):
+ while local.db_context_counter:
+ db_session.__exit__()
+
+ else:
+ raise RuntimeError("Needs app or request context")
+ return result
+
+ return decorator
+
+
+class PonyDatastore(Datastore):
+ def commit(self):
+ self.db.commit()
+
+ @with_pony_session
+ def put(self, model):
+ return model
+
+ @with_pony_session
+ def delete(self, model):
+ model.delete()
+
+
+class UserDatastore(object):
+ """Abstracted user datastore.
+
+ :param user_model: A user model class definition
+ :param role_model: A role model class definition
+
+ .. important::
+ For mutating operations, the user/role will be added to the
+ datastore (by calling self.put(<object>). If the datastore is session based
+ (such as for SQLAlchemyDatastore) it is up to caller to actually
+ commit the transaction by calling datastore.commit().
+ """
+
+ def __init__(self, user_model, role_model):
+ self.user_model = user_model
+ self.role_model = role_model
+
+ def _prepare_role_modify_args(self, user, role):
+ if isinstance(user, string_types):
+ user = self.find_user(email=user)
+ if isinstance(role, string_types):
+ role = self.find_role(role)
+ return user, role
+
+ def _prepare_create_user_args(self, **kwargs):
+ kwargs.setdefault("active", True)
+ roles = kwargs.get("roles", [])
+ for i, role in enumerate(roles):
+ rn = role.name if isinstance(role, self.role_model) else role
+ # see if the role exists
+ roles[i] = self.find_role(rn)
+ kwargs["roles"] = roles
+ if hasattr(self.user_model, "fs_uniquifier"):
+ kwargs.setdefault("fs_uniquifier", uuid.uuid4().hex)
+ return kwargs
+
+ def _is_numeric(self, value):
+ try:
+ int(value)
+ except (TypeError, ValueError):
+ return False
+ return True
+
+ def _is_uuid(self, value):
+ return isinstance(value, uuid.UUID)
+
+ def get_user(self, id_or_email):
+ """Returns a user matching the specified ID or email address."""
+ raise NotImplementedError
+
+ def find_user(self, *args, **kwargs):
+ """Returns a user matching the provided parameters."""
+ raise NotImplementedError
+
+ def find_role(self, *args, **kwargs):
+ """Returns a role matching the provided name."""
+ raise NotImplementedError
+
+ def add_role_to_user(self, user, role):
+ """Adds a role to a user.
+
+ :param user: The user to manipulate. Can be an User object or email
+ :param role: The role to add to the user. Can be a Role object or
+ string role name
+ """
+ user, role = self._prepare_role_modify_args(user, role)
+ if role not in user.roles:
+ user.roles.append(role)
+ self.put(user)
+ return True
+ return False
+
+ def remove_role_from_user(self, user, role):
+ """Removes a role from a user.
+
+ :param user: The user to manipulate. Can be an User object or email
+ :param role: The role to remove from the user. Can be a Role object or
+ string role name
+ """
+ rv = False
+ user, role = self._prepare_role_modify_args(user, role)
+ if role in user.roles:
+ rv = True
+ user.roles.remove(role)
+ self.put(user)
+ return rv
+
+ def toggle_active(self, user):
+ """Toggles a user's active status. Always returns True."""
+ user.active = not user.active
+ self.put(user)
+ return True
+
+ def deactivate_user(self, user):
+ """Deactivates a specified user. Returns `True` if a change was made.
+
+ This will immediately disallow access to all endpoints that require
+ authentication either via session or tokens.
+ The user will not be able to log in again.
+
+ :param user: The user to deactivate
+ """
+ if user.active:
+ user.active = False
+ self.put(user)
+ return True
+ return False
+
+ def activate_user(self, user):
+ """Activates a specified user. Returns `True` if a change was made.
+
+ :param user: The user to activate
+ """
+ if not user.active:
+ user.active = True
+ self.put(user)
+ return True
+ return False
+
+ def set_uniquifier(self, user, uniquifier=None):
+ """ Set user's authentication token uniquifier.
+ This will immediately render outstanding auth tokens,
+ session cookies and remember cookies invalid.
+
+ :param user: User to modify
+ :param uniquifier: Unique value - if none then uuid.uuid4().hex is used
+
+ This method is a no-op if the user model doesn't contain the attribute
+ ``fs_uniquifier``
+
+ .. versionadded:: 3.3.0
+ """
+ if not hasattr(user, "fs_uniquifier"):
+ return
+ if not uniquifier:
+ uniquifier = uuid.uuid4().hex
+ user.fs_uniquifier = uniquifier
+ self.put(user)
+
+ def create_role(self, **kwargs):
+ """
+ Creates and returns a new role from the given parameters.
+ Supported params (depending on RoleModel):
+
+ :kwparam name: Role name
+ :kwparam permissions: a comma delimited list of permissions, a set or a list.
+ These are user-defined strings that correspond to strings used with
+ @permissions_required()
+
+ .. versionadded:: 3.3.0
+
+ """
+
+ # By default we just use raw DB model create - for permissions we want to
+ # be nicer and allow sending in a list or set or comma separated string.
+ if "permissions" in kwargs and hasattr(self.role_model, "permissions"):
+ perms = kwargs["permissions"]
+ if isinstance(perms, list) or isinstance(perms, set):
+ perms = ",".join(perms)
+ elif isinstance(perms, string_types):
+ # squash spaces.
+ perms = ",".join([p.strip() for p in perms.split(",")])
+ kwargs["permissions"] = perms
+
+ role = self.role_model(**kwargs)
+ return self.put(role)
+
+ def find_or_create_role(self, name, **kwargs):
+ """Returns a role matching the given name or creates it with any
+ additionally provided parameters.
+ """
+ kwargs["name"] = name
+ return self.find_role(name) or self.create_role(**kwargs)
+
+ def create_user(self, **kwargs):
+ """Creates and returns a new user from the given parameters.
+
+ :kwparam email: required.
+ :kwparam password: Hashed password.
+ :kwparam roles: list of roles to be added to user.
+ Can be Role objects or strings
+
+ .. danger::
+ Be aware that whatever `password` is passed in will
+ be stored directly in the DB. Do NOT pass in a plaintext password!
+ Best practice is to pass in ``hash_password(plaintext_password)``.
+
+ Furthermore, no validation is done on the password (e.g for minimum length).
+ Best practice is to call
+ ``app.security._password_validator(plaintext_password, True)``
+ and look for a ``None`` return meaning the password conforms to the
+ configured validations.
+
+ The new user's ``active`` property will be set to ``True``
+ unless explicitly set to ``False`` in `kwargs`.
+ """
+ kwargs = self._prepare_create_user_args(**kwargs)
+ user = self.user_model(**kwargs)
+ return self.put(user)
+
+ def delete_user(self, user):
+ """Deletes the specified user.
+
+ :param user: The user to delete
+ """
+ self.delete(user)
+
+ def reset_user_access(self, user):
+ """
+ Use this method to reset user authentication methods in the case of compromise.
+ This will:
+
+ * reset fs_uniquifier - which causes session cookie, remember cookie, auth
+ tokens to be unusable
+ * remove all unified signin TOTP secrets so those can't be used
+ * remove all two-factor secrets so those can't be used
+
+ Note that if using unified sign in and allow 'email' as a way to receive a code
+ if the email is compromised - login is still possible. To handle this - it
+ is better to deactivate the user.
+
+ Note - this method isn't used directly by Flask-Security - it is provided
+ as a helper for an applications administrative needs.
+
+ Remember to call commit on DB if needed.
+
+ .. versionadded:: 3.4.1
+ """
+ self.set_uniquifier(user)
+ if hasattr(user, "us_totp_secrets"):
+ self.us_reset(user)
+ if hasattr(user, "tf_primary_method"):
+ self.tf_reset(user)
+
+ def tf_set(self, user, primary_method, totp_secret=None, phone=None):
+ """ Set two-factor info into user record.
+ This carefully only changes things if different.
+
+ If totp_secret isn't provided - existing one won't be changed.
+ If phone isn't provided, the existing phone number won't be changed.
+
+ This could be called from an application to apiori setup a user for two factor
+ without the user having to go through the setup process.
+
+ To get a totp_secret - use ``app.security._totp_factory.generate_totp_secret()``
+
+ .. versionadded: 3.4.1
+ """
+
+ changed = False
+ if user.tf_primary_method != primary_method:
+ user.tf_primary_method = primary_method
+ changed = True
+ if totp_secret and user.tf_totp_secret != totp_secret:
+ user.tf_totp_secret = totp_secret
+ changed = True
+ if phone and user.tf_phone_number != phone:
+ user.tf_phone_number = phone
+ changed = True
+ if changed:
+ self.put(user)
+
+ def tf_reset(self, user):
+ """ Disable two-factor auth for user
+
+ .. versionadded: 3.4.1
+ """
+ user.tf_primary_method = None
+ user.tf_totp_secret = None
+ user.tf_phone_number = None
+ self.put(user)
+
+ def us_get_totp_secrets(self, user):
+ """ Return totp secrets.
+ These are json encoded in the DB.
+
+ Returns a dict with methods as keys and secrets as values.
+
+ .. versionadded:: 3.4.0
+ """
+ if not user.us_totp_secrets:
+ return {}
+ return json.loads(user.us_totp_secrets)
+
+ def us_put_totp_secrets(self, user, secrets):
+ """ Save secrets. Assume to be a dict (or None)
+ with keys as methods, and values as (encrypted) secrets.
+
+ .. versionadded:: 3.4.0
+ """
+ user.us_totp_secrets = json.dumps(secrets) if secrets else None
+ self.put(user)
+
+ def us_set(self, user, method, totp_secret=None, phone=None):
+ """ Set unified sign in info into user record.
+
+ If totp_secret isn't provided - existing one won't be changed.
+ If phone isn't provided, the existing phone number won't be changed.
+
+ This could be called from an application to apiori setup a user for unified
+ sign in without the user having to go through the setup process.
+
+ To get a totp_secret - use ``app.security._totp_factory.generate_totp_secret()``
+
+ .. versionadded: 3.4.1
+ """
+
+ if totp_secret:
+ totp_secrets = self.us_get_totp_secrets(user)
+ totp_secrets[method] = totp_secret
+ self.us_put_totp_secrets(user, totp_secrets)
+ if phone and user.us_phone_number != phone:
+ user.us_phone_number = phone
+ self.put(user)
+
+ def us_reset(self, user):
+ """ Disable unified sign in for user.
+ Be aware that if "email" is an allowed way to receive codes, they
+ will still work (as totp secrets are generated on the fly).
+ This will disable authenticator app and SMS.
+
+ .. versionadded: 3.4.1
+ """
+ user.us_totp_secrets = None
+ user.us_phone_number = None
+ self.put(user)
+
+
+class SQLAlchemyUserDatastore(SQLAlchemyDatastore, UserDatastore):
+ """A SQLAlchemy datastore implementation for Flask-Security that assumes the
+ use of the Flask-SQLAlchemy extension.
+ """
+
+ def __init__(self, db, user_model, role_model):
+ SQLAlchemyDatastore.__init__(self, db)
+ UserDatastore.__init__(self, user_model, role_model)
+
+ def get_user(self, identifier):
+ from sqlalchemy import func as alchemyFn
+ from sqlalchemy import inspect
+ from sqlalchemy.sql import sqltypes
+ from sqlalchemy.dialects.postgresql import UUID as PSQL_UUID
+
+ user_model_query = self.user_model.query
+ if config_value("JOIN_USER_ROLES") and hasattr(self.user_model, "roles"):
+ from sqlalchemy.orm import joinedload
+
+ user_model_query = user_model_query.options(joinedload("roles"))
+
+ # To support both numeric, string, and UUID primary keys, and support
+ # calling this routine with either a numeric value or a string or a UUID
+ # we need to make sure the types basically match.
+ # psycopg2 for example will complain if we attempt to 'get' a
+ # numeric primary key with a string value.
+ # TODO: other datastores don't support this - they assume the only
+ # PK is user.id. That makes things easier but for backwards compat...
+ ins = inspect(self.user_model)
+ pk_type = ins.primary_key[0].type
+ pk_isnumeric = isinstance(pk_type, sqltypes.Integer)
+ pk_isuuid = isinstance(pk_type, PSQL_UUID)
+ # Are they the same or NOT numeric nor UUID
+ if (
+ (pk_isnumeric and self._is_numeric(identifier))
+ or (pk_isuuid and self._is_uuid(identifier))
+ or (not pk_isnumeric and not pk_isuuid)
+ ):
+ rv = self.user_model.query.get(identifier)
+ if rv is not None:
+ return rv
+
+ # Not PK - iterate through other attributes and look for 'identifier'
+ for attr in get_identity_attributes():
+ column = getattr(self.user_model, attr)
+ attr_isnumeric = isinstance(column.type, sqltypes.Integer)
+
+ query = None
+ if attr_isnumeric and self._is_numeric(identifier):
+ query = column == identifier
+ elif not attr_isnumeric and not self._is_numeric(identifier):
+ # Look for exact case-insensitive match - 'ilike' honors
+ # wild cards which isn't what we want.
+ query = alchemyFn.lower(column) == alchemyFn.lower(identifier)
+ if query is not None:
+ rv = user_model_query.filter(query).first()
+ if rv is not None:
+ return rv
+
+ def find_user(self, **kwargs):
+ query = self.user_model.query
+ if config_value("JOIN_USER_ROLES") and hasattr(self.user_model, "roles"):
+ from sqlalchemy.orm import joinedload
+
+ query = query.options(joinedload("roles"))
+
+ return query.filter_by(**kwargs).first()
+
+ def find_role(self, role):
+ return self.role_model.query.filter_by(name=role).first()
+
+
+class SQLAlchemySessionUserDatastore(SQLAlchemyUserDatastore, SQLAlchemyDatastore):
+ """A SQLAlchemy datastore implementation for Flask-Security that assumes the
+ use of the flask_sqlalchemy_session extension.
+ """
+
+ def __init__(self, session, user_model, role_model):
+ class PretendFlaskSQLAlchemyDb(object):
+ """ This is a pretend db object, so we can just pass in a session.
+ """
+
+ def __init__(self, session):
+ self.session = session
+
+ SQLAlchemyUserDatastore.__init__(
+ self, PretendFlaskSQLAlchemyDb(session), user_model, role_model
+ )
+
+ def commit(self):
+ super(SQLAlchemySessionUserDatastore, self).commit()
+
+
+class MongoEngineUserDatastore(MongoEngineDatastore, UserDatastore):
+ """A MongoEngine datastore implementation for Flask-Security that assumes
+ the use of the Flask-MongoEngine extension.
+ """
+
+ def __init__(self, db, user_model, role_model):
+ MongoEngineDatastore.__init__(self, db)
+ UserDatastore.__init__(self, user_model, role_model)
+
+ def get_user(self, identifier):
+ from mongoengine import ValidationError
+
+ try:
+ return self.user_model.objects(id=identifier).first()
+ except (ValidationError, ValueError):
+ pass
+
+ is_numeric = self._is_numeric(identifier)
+
+ for attr in get_identity_attributes():
+ query_key = attr if is_numeric else "%s__iexact" % attr
+ query = {query_key: identifier}
+ try:
+ rv = self.user_model.objects(**query).first()
+ if rv is not None:
+ return rv
+ except (ValidationError, ValueError):
+ # This can happen if identifier is a string but attribute is
+ # an int.
+ pass
+
+ def find_user(self, **kwargs):
+ try:
+ from mongoengine.queryset import Q, QCombination
+ except ImportError:
+ from mongoengine.queryset.visitor import Q, QCombination
+ from mongoengine.errors import ValidationError
+
+ queries = map(lambda i: Q(**{i[0]: i[1]}), kwargs.items())
+ query = QCombination(QCombination.AND, queries)
+ try:
+ return self.user_model.objects(query).first()
+ except ValidationError: # pragma: no cover
+ return None
+
+ def find_role(self, role):
+ return self.role_model.objects(name=role).first()
+
+
+class PeeweeUserDatastore(PeeweeDatastore, UserDatastore):
+ """A PeeweeD datastore implementation for Flask-Security that assumes the
+ use of Peewee Flask utils.
+
+ :param user_model: A user model class definition
+ :param role_model: A role model class definition
+ :param role_link: A model implementing the many-to-many user-role relation
+ """
+
+ def __init__(self, db, user_model, role_model, role_link):
+ PeeweeDatastore.__init__(self, db)
+ UserDatastore.__init__(self, user_model, role_model)
+ self.UserRole = role_link
+
+ def get_user(self, identifier):
+ from peewee import fn as peeweeFn
+ from peewee import IntegerField
+
+ # For peewee we only (currently) support numeric primary keys.
+ if self._is_numeric(identifier):
+ try:
+ return self.user_model.get(self.user_model.id == identifier)
+ except (self.user_model.DoesNotExist, ValueError):
+ pass
+
+ for attr in get_identity_attributes():
+ # Read above (SQLAlchemy store) for why we are checking types.
+ column = getattr(self.user_model, attr)
+ attr_isnumeric = isinstance(column, IntegerField)
+ try:
+ if attr_isnumeric and self._is_numeric(identifier):
+ return self.user_model.get(column == identifier)
+ elif not attr_isnumeric and not self._is_numeric(identifier):
+ return self.user_model.get(
+ peeweeFn.Lower(column) == peeweeFn.Lower(identifier)
+ )
+ except (self.user_model.DoesNotExist, ValueError):
+ pass
+
+ def find_user(self, **kwargs):
+ try:
+ return self.user_model.filter(**kwargs).get()
+ except self.user_model.DoesNotExist:
+ return None
+
+ def find_role(self, role):
+ try:
+ return self.role_model.filter(name=role).get()
+ except self.role_model.DoesNotExist:
+ return None
+
+ def create_user(self, **kwargs):
+ """Creates and returns a new user from the given parameters."""
+ roles = kwargs.pop("roles", [])
+ user = self.user_model(**self._prepare_create_user_args(**kwargs))
+ user = self.put(user)
+ for role in roles:
+ self.add_role_to_user(user, role)
+ self.put(user)
+ return user
+
+ def add_role_to_user(self, user, role):
+ """Adds a role to a user.
+
+ :param user: The user to manipulate
+ :param role: The role to add to the user
+ """
+ user, role = self._prepare_role_modify_args(user, role)
+ result = self.UserRole.select().where(
+ self.UserRole.user == user.id, self.UserRole.role == role.id
+ )
+ if result.count():
+ return False
+ else:
+ self.put(self.UserRole.create(user=user.id, role=role.id))
+ return True
+
+ def remove_role_from_user(self, user, role):
+ """Removes a role from a user.
+
+ :param user: The user to manipulate
+ :param role: The role to remove from the user
+ """
+ user, role = self._prepare_role_modify_args(user, role)
+ result = self.UserRole.select().where(
+ self.UserRole.user == user, self.UserRole.role == role
+ )
+ if result.count():
+ query = self.UserRole.delete().where(
+ self.UserRole.user == user, self.UserRole.role == role
+ )
+ query.execute()
+ return True
+ else:
+ return False
+
+
+class PonyUserDatastore(PonyDatastore, UserDatastore):
+ """A Pony ORM datastore implementation for Flask-Security.
+
+ Code primarily from https://github.com/ET-CS but taken over after
+ being abandoned.
+ """
+
+ def __init__(self, db, user_model, role_model):
+ PonyDatastore.__init__(self, db)
+ UserDatastore.__init__(self, user_model, role_model)
+
+ @with_pony_session
+ def get_user(self, identifier):
+ from pony.orm.core import ObjectNotFound
+
+ try:
+ return self.user_model[identifier]
+ except (ObjectNotFound, ValueError):
+ pass
+
+ for attr in get_identity_attributes():
+ # this is a nightmare, tl;dr we need to get the thing that
+ # corresponds to email (usually)
+ try:
+ user = self.user_model.get(**{attr: identifier})
+ if user is not None:
+ return user
+ except (TypeError, ValueError):
+ pass
+
+ @with_pony_session
+ def find_user(self, **kwargs):
+ return self.user_model.get(**kwargs)
+
+ @with_pony_session
+ def find_role(self, role):
+ return self.role_model.get(name=role)
+
+ @with_pony_session
+ def add_role_to_user(self, *args, **kwargs):
+ return super(PonyUserDatastore, self).add_role_to_user(*args, **kwargs)
+
+ @with_pony_session
+ def create_user(self, **kwargs):
+ return super(PonyUserDatastore, self).create_user(**kwargs)
+
+ @with_pony_session
+ def create_role(self, **kwargs):
+ return super(PonyUserDatastore, self).create_role(**kwargs)
diff --git a/flask_security/decorators.py b/flask_security/decorators.py
new file mode 100644
index 0000000..dcb5c1a
--- /dev/null
+++ b/flask_security/decorators.py
@@ -0,0 +1,581 @@
+# -*- coding: utf-8 -*-
+"""
+ flask_security.decorators
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Flask-Security decorators module
+
+ :copyright: (c) 2012-2019 by Matt Wright.
+ :copyright: (c) 2019-2020 by J. Christopher Wagner (jwag).
+ :license: MIT, see LICENSE for more details.
+"""
+
+from collections import namedtuple
+import datetime
+from functools import wraps
+
+from flask import Response, _request_ctx_stack, abort, current_app, g, redirect, request
+from flask_login import current_user, login_required # noqa: F401
+from flask_principal import Identity, Permission, RoleNeed, identity_changed
+from flask_wtf.csrf import CSRFError
+from werkzeug.local import LocalProxy
+from werkzeug.routing import BuildError
+
+from .utils import (
+ FsPermNeed,
+ config_value,
+ do_flash,
+ get_message,
+ get_url,
+ check_and_update_authn_fresh,
+ json_error_response,
+)
+
+# Convenient references
+_security = LocalProxy(lambda: current_app.extensions["security"])
+
+_csrf = LocalProxy(lambda: current_app.extensions["csrf"])
+
+BasicAuth = namedtuple("BasicAuth", "username, password")
+
+# NOTE: this is here for backwards compatibility, it is deprecated and
+# to be removed in 4.0
+_default_unauthenticated_html = """
+ <h1>Unauthorized</h1>
+ <p>The server could not verify that you are authorized to access the URL
+ requested. You either supplied the wrong credentials (e.g. a bad password),
+ or your browser doesn't understand how to supply the credentials required.
+ </p>
+ """
+
+
+def _get_unauthenticated_response(text=None, headers=None):
+ text = text or _default_unauthenticated_html
+ headers = headers or {}
+ return Response(text, 401, headers)
+
+
+def _get_unauthorized_response(text=None, headers=None): # pragma: no cover
+ # People called this - even though it isn't public - no harm in keeping it.
+ return _get_unauthenticated_response(text, headers)
+
+
+def default_unauthn_handler(mechanisms, headers=None):
+ """ Default callback for failures to authenticate
+
+ If caller wants JSON - return 401
+ Otherwise - assume caller is html and redirect if possible to a login view.
+ We let Flask-Login handle this.
+
+ """
+ msg = get_message("UNAUTHENTICATED")[0]
+
+ if config_value("BACKWARDS_COMPAT_UNAUTHN"):
+ return _get_unauthenticated_response(headers=headers)
+ if _security._want_json(request):
+ # Ignore headers since today, the only thing in there might be WWW-Authenticate
+ # and we never want to send that in a JSON response (browsers will intercept
+ # that and pop up their own login form).
+ payload = json_error_response(errors=msg)
+ return _security._render_json(payload, 401, None, None)
+ return _security.login_manager.unauthorized()
+
+
+def default_reauthn_handler(within, grace):
+ """ Default callback for 'freshness' related authn failures.
+
+ If caller wants JSON - return 401
+ Otherwise - assume caller is html and redirect if possible to configured view.
+
+ """
+ m, c = get_message("REAUTHENTICATION_REQUIRED")
+
+ if _security._want_json(request):
+ is_us = config_value("UNIFIED_SIGNIN")
+ payload = json_error_response(errors=m)
+ payload["reauth_required"] = True
+ payload["unified_signin_enabled"] = is_us
+ return _security._render_json(payload, 401, None, None)
+
+ if config_value("UNIFIED_SIGNIN"):
+ view = _security.us_verify_url
+ else:
+ view = _security.verify_url
+ if view:
+ do_flash(m, c)
+ redirect_url = get_url(view, qparams={"next": request.url})
+ return redirect(redirect_url)
+ abort(401)
+
+
+def default_unauthz_handler(func, params):
+ unauthz_message, unauthz_message_type = get_message("UNAUTHORIZED")
+ if _security._want_json(request):
+ payload = json_error_response(errors=unauthz_message)
+ return _security._render_json(payload, 403, None, None)
+ view = config_value("UNAUTHORIZED_VIEW")
+ if view:
+ if callable(view):
+ view = view()
+ else:
+ try:
+ view = get_url(view)
+ except BuildError:
+ view = None
+ do_flash(unauthz_message, unauthz_message_type)
+ redirect_to = "/"
+ if request.referrer and not request.referrer.split("?")[0].endswith(
+ request.path
+ ):
+ redirect_to = request.referrer
+
+ return redirect(view or redirect_to)
+ abort(403)
+
+
+def _check_token():
+ # N.B. this isn't great Flask-Login 0.5.0 made this protected
+ # Issue https://github.com/maxcountryman/flask-login/issues/471
+ # was filed to restore public access. We want to call this via
+ # login_manager in case someone has overridden the login_manager which we
+ # allow.
+ if hasattr(_security.login_manager, "request_callback"):
+ # Pre 0.5.0
+ user = _security.login_manager.request_callback(request)
+ else:
+ user = _security.login_manager._request_callback(request)
+
+ if user and user.is_authenticated:
+ app = current_app._get_current_object()
+ _request_ctx_stack.top.user = user
+ identity_changed.send(app, identity=Identity(user.id))
+ return True
+
+ return False
+
+
+def _check_http_auth():
+ auth = request.authorization or BasicAuth(username=None, password=None)
+ if not auth.username:
+ return False
+ user = _security.datastore.get_user(auth.username)
+
+ if user and user.verify_and_update_password(auth.password):
+ _security.datastore.commit()
+ app = current_app._get_current_object()
+ _request_ctx_stack.top.user = user
+ identity_changed.send(app, identity=Identity(user.id))
+ return True
+
+ return False
+
+
+def handle_csrf(method):
+ """ Invoke CSRF protection based on authentication method.
+
+ Usually this is called as part of a decorator, but if that isn't
+ appropriate, endpoint code can call this directly.
+
+ If CSRF protection is appropriate, this will call flask_wtf::protect() which
+ will raise a ValidationError on CSRF failure.
+
+ This routine does nothing if any of these are true:
+
+ #) *WTF_CSRF_ENABLED* is set to False
+
+ #) the Flask-WTF CSRF module hasn't been initialized
+
+ #) csrfProtect already checked and accepted the token
+
+ If the passed in method is not in *SECURITY_CSRF_PROTECT_MECHANISMS* then not only
+ will no CSRF code be run, but a flag in the current context ``fs_ignore_csrf``
+ will be set so that downstream code knows to ignore any CSRF checks.
+
+ .. versionadded:: 3.3.0
+ """
+ if (
+ not current_app.config.get("WTF_CSRF_ENABLED", False)
+ or not current_app.extensions.get("csrf", None)
+ or g.get("csrf_valid", False)
+ ):
+ return
+
+ if config_value("CSRF_PROTECT_MECHANISMS"):
+ if method in config_value("CSRF_PROTECT_MECHANISMS"):
+ _csrf.protect()
+ else:
+ _request_ctx_stack.top.fs_ignore_csrf = True
+
+
+def http_auth_required(realm):
+ """Decorator that protects endpoints using Basic HTTP authentication.
+
+ :param realm: optional realm name
+
+ Once authenticated, if so configured, CSRF protection will be tested.
+ """
+
+ def decorator(fn):
+ @wraps(fn)
+ def wrapper(*args, **kwargs):
+ if _check_http_auth():
+ handle_csrf("basic")
+ return fn(*args, **kwargs)
+ if _security._unauthorized_callback:
+ return _security._unauthorized_callback()
+ else:
+ r = _security.default_http_auth_realm if callable(realm) else realm
+ h = {"WWW-Authenticate": 'Basic realm="%s"' % r}
+ return _security._unauthn_handler(["basic"], headers=h)
+
+ return wrapper
+
+ if callable(realm):
+ return decorator(realm)
+ return decorator
+
+
+def auth_token_required(fn):
+ """Decorator that protects endpoints using token authentication. The token
+ should be added to the request by the client by using a query string
+ variable with a name equal to the configuration value of
+ *SECURITY_TOKEN_AUTHENTICATION_KEY* or in a request header named that of
+ the configuration value of *SECURITY_TOKEN_AUTHENTICATION_HEADER*
+
+ Once authenticated, if so configured, CSRF protection will be tested.
+ """
+
+ @wraps(fn)
+ def decorated(*args, **kwargs):
+ if _check_token():
+ handle_csrf("token")
+ return fn(*args, **kwargs)
+ if _security._unauthorized_callback:
+ return _security._unauthorized_callback()
+ else:
+ return _security._unauthn_handler(["token"])
+
+ return decorated
+
+
+def auth_required(*auth_methods, **kwargs):
+ """
+ Decorator that protects endpoints through multiple mechanisms
+ Example::
+
+ @app.route('/dashboard')
+ @auth_required('token', 'session')
+ def dashboard():
+ return 'Dashboard'
+
+ :param auth_methods: Specified mechanisms (token, basic, session). If not specified
+ then all current available mechanisms will be tried.
+ :kwparam within: Add 'freshness' check to authentication. Is either an int
+ specifying # of minutes, or a callable that returns a timedelta. For timedeltas,
+ timedelta.total_seconds() is used for the calculations:
+
+ - If > 0, then the caller must have authenticated within the time specified
+ (as measured using the session cookie).
+ - If 0 and not within the grace period (see below) the caller will
+ always be redirected to re-authenticate.
+ - If < 0 (the default) no freshness check is performed.
+
+ Note that Basic Auth, by definition, is always 'fresh' and will never result in
+ a redirect/error.
+ :kwparam grace: Add a grace period for freshness checks. As above, either an int
+ or a callable returning a timedelta. If not specified then
+ :py:data:`SECURITY_FRESHNESS_GRACE_PERIOD` is used. The grace period allows
+ callers to complete the required operations w/o being prompted again.
+ See :meth:`flask_security.check_and_update_authn_fresh` for details.
+
+ Note that regardless of order specified - they will be tried in the following
+ order: token, session, basic.
+
+ The first mechanism that succeeds is used, following that, depending on
+ configuration, CSRF protection will be tested.
+
+ On authentication failure `.Security.unauthorized_callback` (deprecated)
+ or :meth:`.Security.unauthn_handler` will be called.
+
+ .. versionchanged:: 3.3.0
+ If ``auth_methods`` isn't specified, then all will be tried. Authentication
+ mechanisms will always be tried in order of ``token``, ``session``, ``basic``
+ regardless of how they are specified in the ``auth_methods`` parameter.
+
+ .. versionchanged:: 3.4.0
+ Added ``within`` and ``grace`` parameters to enforce a freshness check.
+
+ """
+
+ login_mechanisms = {
+ "token": lambda: _check_token(),
+ "session": lambda: current_user.is_authenticated,
+ "basic": lambda: _check_http_auth(),
+ }
+ mechanisms_order = ["token", "session", "basic"]
+ if not auth_methods:
+ auth_methods = {"basic", "session", "token"}
+ else:
+ auth_methods = [am for am in auth_methods]
+
+ def wrapper(fn):
+ @wraps(fn)
+ def decorated_view(*args, **dkwargs):
+ # 2.7 doesn't support keyword args after *args....
+ within = kwargs.get("within", -1)
+ if callable(within):
+ within = within()
+ else:
+ within = datetime.timedelta(minutes=within)
+ grace = kwargs.get("grace", None)
+ if grace is None:
+ grace = config_value("FRESHNESS_GRACE_PERIOD")
+ elif callable(grace):
+ grace = grace()
+ else:
+ grace = datetime.timedelta(minutes=grace)
+
+ h = {}
+ if "basic" in auth_methods:
+ r = _security.default_http_auth_realm
+ h["WWW-Authenticate"] = 'Basic realm="%s"' % r
+ mechanisms = [
+ (method, login_mechanisms.get(method))
+ for method in mechanisms_order
+ if method in auth_methods
+ ]
+ for method, mechanism in mechanisms:
+ if mechanism and mechanism():
+ # successfully authenticated. Basic auth is by definition 'fresh'.
+ # Note that using token auth is ok - but caller still has to pass
+ # in a session cookie...
+ if method != "basic" and not check_and_update_authn_fresh(
+ within, grace
+ ):
+ return _security._reauthn_handler(within, grace)
+ handle_csrf(method)
+ return fn(*args, **dkwargs)
+ if _security._unauthorized_callback:
+ return _security._unauthorized_callback()
+ else:
+ return _security._unauthn_handler(auth_methods, headers=h)
+
+ return decorated_view
+
+ return wrapper
+
+
+def unauth_csrf(fall_through=False):
+ """Decorator for endpoints that don't need authentication
+ but do want CSRF checks (available via Header rather than just form).
+ This is required when setting *WTF_CSRF_CHECK_DEFAULT* = **False** since in that
+ case, without this decorator, the form validation will attempt to do the CSRF
+ check, and that will fail since the csrf-token is in the header (for pure JSON
+ requests).
+
+ This decorator does nothing unless Flask-WTF::CSRFProtect has been initialized.
+
+ This decorator does nothing if *WTF_CSRF_ENABLED* == **False**.
+
+ This decorator will always require CSRF if the caller is authenticated.
+
+ This decorator will suppress CSRF if caller isn't authenticated and has set the
+ *SECURITY_CSRF_IGNORE_UNAUTH_ENDPOINTS* config variable.
+
+ :param fall_through: if set to True, then if CSRF fails here - simply keep going.
+ This is appropriate if underlying view is form based and once the form is
+ instantiated, the csrf_token will be available.
+ Note that this can mask some errors such as 'The CSRF session token is missing.'
+ meaning that the caller didn't send a session cookie and instead the caller
+ might get a 'The CSRF token is missing.' error.
+
+ .. versionadded:: 3.3.0
+ """
+
+ def wrapper(fn):
+ @wraps(fn)
+ def decorated(*args, **kwargs):
+ if not current_app.config.get(
+ "WTF_CSRF_ENABLED", False
+ ) or not current_app.extensions.get("csrf", None):
+ return fn(*args, **kwargs)
+
+ if (
+ config_value("CSRF_IGNORE_UNAUTH_ENDPOINTS")
+ and not current_user.is_authenticated
+ ):
+ _request_ctx_stack.top.fs_ignore_csrf = True
+ else:
+ try:
+ _csrf.protect()
+ except CSRFError:
+ if not fall_through:
+ raise
+
+ return fn(*args, **kwargs)
+
+ return decorated
+
+ return wrapper
+
+
+def roles_required(*roles):
+ """Decorator which specifies that a user must have all the specified roles.
+ Example::
+
+ @app.route('/dashboard')
+ @roles_required('admin', 'editor')
+ def dashboard():
+ return 'Dashboard'
+
+ The current user must have both the `admin` role and `editor` role in order
+ to view the page.
+
+ :param roles: The required roles.
+ """
+
+ def wrapper(fn):
+ @wraps(fn)
+ def decorated_view(*args, **kwargs):
+ perms = [Permission(RoleNeed(role)) for role in roles]
+ for perm in perms:
+ if not perm.can():
+ if _security._unauthorized_callback:
+ # Backwards compat - deprecated
+ return _security._unauthorized_callback()
+ return _security._unauthz_handler(roles_required, list(roles))
+ return fn(*args, **kwargs)
+
+ return decorated_view
+
+ return wrapper
+
+
+def roles_accepted(*roles):
+ """Decorator which specifies that a user must have at least one of the
+ specified roles. Example::
+
+ @app.route('/create_post')
+ @roles_accepted('editor', 'author')
+ def create_post():
+ return 'Create Post'
+
+ The current user must have either the `editor` role or `author` role in
+ order to view the page.
+
+ :param roles: The possible roles.
+ """
+
+ def wrapper(fn):
+ @wraps(fn)
+ def decorated_view(*args, **kwargs):
+ perm = Permission(*[RoleNeed(role) for role in roles])
+ if perm.can():
+ return fn(*args, **kwargs)
+ if _security._unauthorized_callback:
+ # Backwards compat - deprecated
+ return _security._unauthorized_callback()
+ return _security._unauthz_handler(roles_accepted, list(roles))
+
+ return decorated_view
+
+ return wrapper
+
+
+def permissions_required(*fsperms):
+ """Decorator which specifies that a user must have all the specified permissions.
+ Example::
+
+ @app.route('/dashboard')
+ @permissions_required('admin-write', 'editor-write')
+ def dashboard():
+ return 'Dashboard'
+
+ The current user must have BOTH permissions (via the roles it has)
+ to view the page.
+
+ N.B. Don't confuse these permissions with flask-principle Permission()!
+
+ :param fsperms: The required permissions.
+
+ .. versionadded:: 3.3.0
+ """
+
+ def wrapper(fn):
+ @wraps(fn)
+ def decorated_view(*args, **kwargs):
+ perms = [Permission(FsPermNeed(fsperm)) for fsperm in fsperms]
+ for perm in perms:
+ if not perm.can():
+ if _security._unauthorized_callback:
+ # Backwards compat - deprecated
+ return _security._unauthorized_callback()
+ return _security._unauthz_handler(
+ permissions_required, list(fsperms)
+ )
+
+ return fn(*args, **kwargs)
+
+ return decorated_view
+
+ return wrapper
+
+
+def permissions_accepted(*fsperms):
+ """Decorator which specifies that a user must have at least one of the
+ specified permissions. Example::
+
+ @app.route('/create_post')
+ @permissions_accepted('editor-write', 'author-wrote')
+ def create_post():
+ return 'Create Post'
+
+ The current user must have one of the permissions (via the roles it has)
+ to view the page.
+
+ N.B. Don't confuse these permissions with flask-principle Permission()!
+
+ :param fsperms: The possible permissions.
+
+ .. versionadded:: 3.3.0
+ """
+
+ def wrapper(fn):
+ @wraps(fn)
+ def decorated_view(*args, **kwargs):
+ perm = Permission(*[FsPermNeed(fsperm) for fsperm in fsperms])
+ if perm.can():
+ return fn(*args, **kwargs)
+ if _security._unauthorized_callback:
+ # Backwards compat - deprecated
+ return _security._unauthorized_callback()
+ return _security._unauthz_handler(permissions_accepted, list(fsperms))
+
+ return decorated_view
+
+ return wrapper
+
+
+def anonymous_user_required(f):
+ """Decorator which requires that caller NOT be logged in.
+ If a logged in user accesses an endpoint protected with this decorator
+ they will be redirected to the *SECURITY_POST_LOGIN_VIEW*.
+ If the caller requests a JSON response, a 400 will be returned.
+
+ .. versionchanged:: 3.3.0
+ Support for JSON response was added.
+ """
+
+ @wraps(f)
+ def wrapper(*args, **kwargs):
+ if current_user.is_authenticated:
+ if _security._want_json(request):
+ payload = json_error_response(
+ errors=get_message("ANONYMOUS_USER_REQUIRED")[0]
+ )
+ return _security._render_json(payload, 400, None, None)
+ else:
+ return redirect(get_url(_security.post_login_view))
+ return f(*args, **kwargs)
+
+ return wrapper
diff --git a/flask_security/forms.py b/flask_security/forms.py
new file mode 100644
index 0000000..f18fb3a
--- /dev/null
+++ b/flask_security/forms.py
@@ -0,0 +1,611 @@
+# -*- coding: utf-8 -*-
+"""
+ flask_security.forms
+ ~~~~~~~~~~~~~~~~~~~~
+
+ Flask-Security forms module
+
+ :copyright: (c) 2012 by Matt Wright.
+ :copyright: (c) 2017 by CERN.
+ :copyright: (c) 2019-2020 by J. Christopher Wagner (jwag).
+ :license: MIT, see LICENSE for more details.
+"""
+
+import inspect
+
+from flask import Markup, current_app, request
+from flask_login import current_user
+from flask_wtf import FlaskForm as BaseForm
+from speaklater import is_lazy_string, make_lazy_string
+from werkzeug.local import LocalProxy
+from wtforms import (
+ BooleanField,
+ Field,
+ HiddenField,
+ PasswordField,
+ RadioField,
+ StringField,
+ SubmitField,
+ ValidationError,
+ validators,
+)
+
+from .confirmable import requires_confirmation
+from .utils import (
+ _,
+ _datastore,
+ config_value,
+ do_flash,
+ get_message,
+ hash_password,
+ localize_callback,
+ url_for_security,
+ validate_redirect_url,
+)
+
+# Convenient references
+_security = LocalProxy(lambda: current_app.extensions["security"])
+
+_default_field_labels = {
+ "email": _("Email Address"),
+ "password": _("Password"),
+ "remember_me": _("Remember Me"),
+ "login": _("Login"),
+ "signin": _("Sign In"),
+ "register": _("Register"),
+ "send_confirmation": _("Resend Confirmation Instructions"),
+ "recover_password": _("Recover Password"),
+ "reset_password": _("Reset Password"),
+ "retype_password": _("Retype Password"),
+ "new_password": _("New Password"),
+ "change_password": _("Change Password"),
+ "send_login_link": _("Send Login Link"),
+ "verify_password": _("Verify Password"),
+ "change_method": _("Change Method"),
+ "phone": _("Phone Number"),
+ "code": _("Authentication Code"),
+ "submit": _("Submit"),
+ "submitcode": _("Submit Code"),
+ "error": _("Error(s)"),
+ "identity": _("Identity"),
+ "sendcode": _("Send Code"),
+ "passcode": _("Passcode"),
+}
+
+
+class ValidatorMixin(object):
+ """
+ This is called at import time - so there is no app context.
+ Validators have state - namely self.message - but we need that
+ xlated on a per-request basis. So we want a lazy_string - but we can't create
+ that until we are in an app context.
+ """
+
+ def __init__(self, *args, **kwargs):
+ # If the message is available from config[MSG_xx] then it can be xlated.
+ # Otherwise it will be used as is.
+ if "message" in kwargs:
+ self._original_message = kwargs["message"]
+ del kwargs["message"]
+ else:
+ self._original_message = None
+ super(ValidatorMixin, self).__init__(*args, **kwargs)
+
+ def __call__(self, form, field):
+ if self._original_message and (
+ not is_lazy_string(self.message) and not self.message
+ ):
+ # Creat on first usage within app context.
+ cv = config_value("MSG_" + self._original_message)
+ if cv:
+ self.message = make_lazy_string(_local_xlate, cv[0])
+ else:
+ self.message = self._original_message
+ return super(ValidatorMixin, self).__call__(form, field)
+
+
+class EqualTo(ValidatorMixin, validators.EqualTo):
+ pass
+
+
+class Required(ValidatorMixin, validators.DataRequired):
+ pass
+
+
+class Email(ValidatorMixin, validators.Email):
+ pass
+
+
+class Length(ValidatorMixin, validators.Length):
+ pass
+
+
+email_required = Required(message="EMAIL_NOT_PROVIDED")
+email_validator = Email(message="INVALID_EMAIL_ADDRESS")
+password_required = Required(message="PASSWORD_NOT_PROVIDED")
+
+
+def _local_xlate(text):
+ """ LazyStrings need to be evaluated in the context of a request
+ where _security.i18_domain is available.
+ """
+ return localize_callback(text)
+
+
+def get_form_field_label(key):
+ """ This is called during import since form fields are declared as part of
+ class. Thus can't call 'localize_callback' until we need to actually
+ translate/render form.
+ """
+ return make_lazy_string(_local_xlate, _default_field_labels.get(key, ""))
+
+
+def unique_user_email(form, field):
+ if _datastore.get_user(field.data) is not None:
+ msg = get_message("EMAIL_ALREADY_ASSOCIATED", email=field.data)[0]
+ raise ValidationError(msg)
+
+
+def valid_user_email(form, field):
+ form.user = _datastore.get_user(field.data)
+ if form.user is None:
+ raise ValidationError(get_message("USER_DOES_NOT_EXIST")[0])
+
+
+class Form(BaseForm):
+ def __init__(self, *args, **kwargs):
+ if current_app.testing:
+ self.TIME_LIMIT = None
+ super(Form, self).__init__(*args, **kwargs)
+
+
+class EmailFormMixin:
+ email = StringField(
+ get_form_field_label("email"), validators=[email_required, email_validator]
+ )
+
+
+class UserEmailFormMixin:
+ user = None
+ email = StringField(
+ get_form_field_label("email"),
+ validators=[email_required, email_validator, valid_user_email],
+ )
+
+
+class UniqueEmailFormMixin:
+ email = StringField(
+ get_form_field_label("email"),
+ validators=[email_required, email_validator, unique_user_email],
+ )
+
+
+class PasswordFormMixin:
+ password = PasswordField(
+ get_form_field_label("password"), validators=[password_required]
+ )
+
+
+class NewPasswordFormMixin:
+ password = PasswordField(
+ get_form_field_label("password"), validators=[password_required]
+ )
+
+
+class PasswordConfirmFormMixin:
+ password_confirm = PasswordField(
+ get_form_field_label("retype_password"),
+ validators=[
+ EqualTo("password", message="RETYPE_PASSWORD_MISMATCH"),
+ password_required,
+ ],
+ )
+
+
+class NextFormMixin:
+ next = HiddenField()
+
+ def validate_next(self, field):
+ if field.data and not validate_redirect_url(field.data):
+ field.data = ""
+ do_flash(*get_message("INVALID_REDIRECT"))
+ raise ValidationError(get_message("INVALID_REDIRECT")[0])
+
+
+class RegisterFormMixin:
+ submit = SubmitField(get_form_field_label("register"))
+
+ def to_dict(self, only_user):
+ """
+ Return form data as dictionary
+ :param only_user: bool, if True then only fields that have
+ corresponding members in UserModel are returned
+ :return: dict
+ """
+
+ def is_field_and_user_attr(member):
+ if not isinstance(member, Field):
+ return False
+
+ # If only fields recorded on UserModel should be returned,
+ # perform check on user model, else return True
+ if only_user is True:
+ return hasattr(_datastore.user_model, member.name)
+ else:
+ return True
+
+ fields = inspect.getmembers(self, is_field_and_user_attr)
+ return dict((key, value.data) for key, value in fields)
+
+
+class SendConfirmationForm(Form, UserEmailFormMixin):
+ """The default send confirmation form"""
+
+ submit = SubmitField(get_form_field_label("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(get_message("ALREADY_CONFIRMED")[0])
+ return False
+ return True
+
+
+class ForgotPasswordForm(Form, UserEmailFormMixin):
+ """The default forgot password form"""
+
+ submit = SubmitField(get_form_field_label("recover_password"))
+
+ def validate(self):
+ if not super(ForgotPasswordForm, self).validate():
+ return False
+ if not self.user.is_active:
+ self.email.errors.append(get_message("DISABLED_ACCOUNT")[0])
+ return False
+ if requires_confirmation(self.user):
+ self.email.errors.append(get_message("CONFIRMATION_REQUIRED")[0])
+ return False
+ return True
+
+
+class PasswordlessLoginForm(Form, UserEmailFormMixin):
+ """The passwordless login form"""
+
+ submit = SubmitField(get_form_field_label("send_login_link"))
+
+ def __init__(self, *args, **kwargs):
+ super(PasswordlessLoginForm, self).__init__(*args, **kwargs)
+
+ def validate(self):
+ if not super(PasswordlessLoginForm, self).validate():
+ return False
+ if not self.user.is_active:
+ self.email.errors.append(get_message("DISABLED_ACCOUNT")[0])
+ return False
+ return True
+
+
+class LoginForm(Form, NextFormMixin):
+ """The default login form"""
+
+ email = StringField(get_form_field_label("email"), validators=[email_required])
+ password = PasswordField(
+ get_form_field_label("password"), validators=[password_required]
+ )
+ remember = BooleanField(get_form_field_label("remember_me"))
+ submit = SubmitField(get_form_field_label("login"))
+
+ def __init__(self, *args, **kwargs):
+ super(LoginForm, self).__init__(*args, **kwargs)
+ if not self.next.data:
+ self.next.data = request.args.get("next", "")
+ self.remember.default = config_value("DEFAULT_REMEMBER_ME")
+ if (
+ current_app.extensions["security"].recoverable
+ and not self.password.description
+ ):
+ html = Markup(
+ u'<a href="{url}">{message}</a>'.format(
+ url=url_for_security("forgot_password"),
+ message=get_message("FORGOT_PASSWORD")[0],
+ )
+ )
+ self.password.description = html
+
+ def validate(self):
+ if not super(LoginForm, self).validate():
+ return False
+
+ self.user = _datastore.get_user(self.email.data)
+
+ if self.user is None:
+ self.email.errors.append(get_message("USER_DOES_NOT_EXIST")[0])
+ # Reduce timing variation between existing and non-existing users
+ hash_password(self.password.data)
+ return False
+ if not self.user.password:
+ self.password.errors.append(get_message("PASSWORD_NOT_SET")[0])
+ # Reduce timing variation between existing and non-existing users
+ hash_password(self.password.data)
+ return False
+ if not self.user.verify_and_update_password(self.password.data):
+ 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
+ return True
+
+
+class VerifyForm(Form, PasswordFormMixin):
+ """The verify authentication form"""
+
+ user = None
+ submit = SubmitField(get_form_field_label("verify_password"))
+
+ def validate(self):
+ if not super(VerifyForm, self).validate():
+ return False
+
+ self.user = current_user
+ if not self.user.verify_and_update_password(self.password.data):
+ self.password.errors.append(get_message("INVALID_PASSWORD")[0])
+ return False
+ return True
+
+
+class ConfirmRegisterForm(Form, RegisterFormMixin, UniqueEmailFormMixin):
+ """ This form is used for registering when 'confirmable' is set.
+ The only difference between this and the other RegisterForm is that
+ this one doesn't require re-typing in the password...
+ """
+
+ # Password optional when Unified Signin enabled.
+ password = PasswordField(
+ get_form_field_label("password"), validators=[validators.Optional()]
+ )
+
+ def validate(self):
+ if not super(ConfirmRegisterForm, self).validate():
+ return False
+
+ # To support unified sign in - we permit registering with no password.
+ if not config_value("UNIFIED_SIGNIN"):
+ # password required
+ if not self.password.data or not self.password.data.strip():
+ self.password.errors.append(get_message("PASSWORD_NOT_PROVIDED")[0])
+ return False
+
+ if self.password.data:
+ # We do explicit validation here for passwords
+ # (rather than write a validator class) for 2 reasons:
+ # 1) We want to control which fields are passed -
+ # sometimes that's current_user
+ # other times it's the registration fields.
+ # 2) We want to be able to return multiple error messages.
+ rfields = {}
+ for k, v in self.data.items():
+ if hasattr(_datastore.user_model, k):
+ rfields[k] = v
+ del rfields["password"]
+ pbad = _security._password_validator(self.password.data, True, **rfields)
+ if pbad:
+ self.password.errors.extend(pbad)
+ return False
+ return True
+
+
+class RegisterForm(ConfirmRegisterForm, NextFormMixin):
+
+ # Password optional when Unified Signin enabled.
+ password_confirm = PasswordField(
+ get_form_field_label("retype_password"),
+ validators=[
+ EqualTo("password", message="RETYPE_PASSWORD_MISMATCH"),
+ validators.Optional(),
+ ],
+ )
+
+ def validate(self):
+ if not super(RegisterForm, self).validate():
+ return False
+ if not config_value("UNIFIED_SIGNIN"):
+ # password_confirm required
+ if not self.password_confirm.data or not self.password_confirm.data.strip():
+ self.password_confirm.errors.append(
+ get_message("PASSWORD_NOT_PROVIDED")[0]
+ )
+ return False
+ return True
+
+ def __init__(self, *args, **kwargs):
+ super(RegisterForm, self).__init__(*args, **kwargs)
+ if not self.next.data:
+ self.next.data = request.args.get("next", "")
+
+
+class ResetPasswordForm(Form, NewPasswordFormMixin, PasswordConfirmFormMixin):
+ """The default reset password form"""
+
+ submit = SubmitField(get_form_field_label("reset_password"))
+
+ def validate(self):
+ if not super(ResetPasswordForm, self).validate():
+ return False
+
+ pbad = _security._password_validator(
+ self.password.data, False, user=current_user
+ )
+ if pbad:
+ self.password.errors.extend(pbad)
+ return False
+ return True
+
+
+class ChangePasswordForm(Form, PasswordFormMixin):
+ """The default change password form"""
+
+ new_password = PasswordField(
+ get_form_field_label("new_password"), validators=[password_required]
+ )
+
+ new_password_confirm = PasswordField(
+ get_form_field_label("retype_password"),
+ validators=[
+ EqualTo("new_password", message="RETYPE_PASSWORD_MISMATCH"),
+ password_required,
+ ],
+ )
+
+ submit = SubmitField(get_form_field_label("change_password"))
+
+ def validate(self):
+ if not super(ChangePasswordForm, self).validate():
+ return False
+
+ if not current_user.verify_and_update_password(self.password.data):
+ self.password.errors.append(get_message("INVALID_PASSWORD")[0])
+ return False
+ if self.password.data == self.new_password.data:
+ self.password.errors.append(get_message("PASSWORD_IS_THE_SAME")[0])
+ return False
+ pbad = _security._password_validator(
+ self.new_password.data, False, user=current_user
+ )
+ if pbad:
+ self.new_password.errors.extend(pbad)
+ return False
+ return True
+
+
+class TwoFactorSetupForm(Form, UserEmailFormMixin):
+ """The Two-factor token validation form"""
+
+ setup = RadioField(
+ "Available Methods",
+ choices=[
+ ("email", "Set up using email"),
+ (
+ "authenticator",
+ "Set up using an authenticator app (e.g. google, lastpass, authy)",
+ ),
+ ("sms", "Set up using SMS"),
+ ("disable", "Disable two factor authentication"),
+ ],
+ )
+ phone = StringField(get_form_field_label("phone"))
+ submit = SubmitField(get_form_field_label("submit"))
+
+ def __init__(self, *args, **kwargs):
+ super(TwoFactorSetupForm, self).__init__(*args, **kwargs)
+
+ def validate(self):
+ # TODO: the super class validate is never called - thus we have to
+ # initialize errors to lists below. It also means that 'email' is never
+ # validated - though it isn't required so the mixin might not be correct.
+ choices = config_value("TWO_FACTOR_ENABLED_METHODS")
+ if "email" in choices:
+ # backwards compat
+ choices.append("mail")
+ if not config_value("TWO_FACTOR_REQUIRED"):
+ choices.append("disable")
+ if "setup" not in self.data or self.data["setup"] not in choices:
+ self.setup.errors = list()
+ self.setup.errors.append(get_message("TWO_FACTOR_METHOD_NOT_AVAILABLE")[0])
+ return False
+ if self.setup.data == "sms" and len(self.phone.data) > 0:
+ # Somewhat bizarre - but this isn't required the first time around
+ # when they select "sms". Then they get a field to fill out with
+ # phone number, then Submit again.
+ msg = _security._phone_util.validate_phone_number(self.phone.data)
+ if msg:
+ self.phone.errors = list()
+ self.phone.errors.append(msg)
+ return False
+
+ return True
+
+
+class TwoFactorVerifyCodeForm(Form, UserEmailFormMixin):
+ """The Two-factor token validation form"""
+
+ code = StringField(get_form_field_label("code"))
+ submit = SubmitField(get_form_field_label("submitcode"))
+
+ def __init__(self, *args, **kwargs):
+ super(TwoFactorVerifyCodeForm, self).__init__(*args, **kwargs)
+
+ def validate(self):
+ # codes sent by sms or mail will be valid for another window cycle
+ if (
+ self.primary_method == "google_authenticator"
+ or self.primary_method == "authenticator"
+ ):
+ self.window = config_value("TWO_FACTOR_AUTHENTICATOR_VALIDITY")
+ elif self.primary_method == "email" or self.primary_method == "mail":
+ self.window = config_value("TWO_FACTOR_MAIL_VALIDITY")
+ elif self.primary_method == "sms":
+ self.window = config_value("TWO_FACTOR_SMS_VALIDITY")
+ else:
+ return False
+
+ # verify entered token with user's totp secret
+ if not _security._totp_factory.verify_totp(
+ token=self.code.data,
+ totp_secret=self.tf_totp_secret,
+ user=self.user,
+ window=self.window,
+ ):
+ self.code.errors = list()
+ self.code.errors.append(get_message("TWO_FACTOR_INVALID_TOKEN")[0])
+
+ return False
+
+ return True
+
+
+class TwoFactorVerifyPasswordForm(Form, PasswordFormMixin):
+ """The verify password form"""
+
+ submit = SubmitField(get_form_field_label("verify_password"))
+
+ def validate(self):
+ if not super(TwoFactorVerifyPasswordForm, self).validate():
+ return False
+
+ self.user = current_user
+ if not self.user.verify_and_update_password(self.password.data):
+ self.password.errors.append(get_message("INVALID_PASSWORD")[0])
+ return False
+
+ return True
+
+
+class TwoFactorRescueForm(Form):
+ """The Two-factor Rescue validation form """
+
+ help_setup = RadioField(
+ "Trouble Accessing Your Account?",
+ choices=[
+ ("lost_device", "Can not access mobile device?"),
+ ("no_mail_access", "Can not access mail account?"),
+ ],
+ )
+ submit = SubmitField(get_form_field_label("submit"))
+
+ def __init__(self, *args, **kwargs):
+ super(TwoFactorRescueForm, self).__init__(*args, **kwargs)
+
+ def validate(self):
+ if not super(TwoFactorRescueForm, self).validate():
+ return False
+ return True
diff --git a/flask_security/models/__init__.py b/flask_security/models/__init__.py
new file mode 100644
index 0000000..6951442
--- /dev/null
+++ b/flask_security/models/__init__.py
@@ -0,0 +1,12 @@
+""""
+Copyright 2019 by J. Christopher Wagner (jwag). All rights reserved.
+:license: MIT, see LICENSE for more details.
+
+This packages contains OPTIONAL models for various ORMs/databases that can be used
+to quickly get the required DB models setup.
+
+These models have the fields for ALL features. This makes it easy for applications
+to add features w/o a DB migration (and modern DBs are pretty efficient at storing
+empty values!).
+
+"""
diff --git a/flask_security/models/fsqla.py b/flask_security/models/fsqla.py
new file mode 100644
index 0000000..b1bbf79
--- /dev/null
+++ b/flask_security/models/fsqla.py
@@ -0,0 +1,164 @@
+"""
+Copyright 2019 by J. Christopher Wagner (jwag). All rights reserved.
+:license: MIT, see LICENSE for more details.
+
+
+Complete models for all features when using Flask-SqlAlchemy
+
+BE AWARE: Once any version of this is shipped no changes can be made - instead
+a new version needs to be created.
+"""
+
+import datetime
+from sqlalchemy import (
+ Boolean,
+ DateTime,
+ Column,
+ Integer,
+ String,
+ UnicodeText,
+ ForeignKey,
+)
+from sqlalchemy.ext.declarative import declared_attr
+from sqlalchemy.orm import relationship
+from sqlalchemy.sql import func
+
+from flask_security import RoleMixin, UserMixin
+
+
+class FsModels(object):
+ """
+ Helper class for model mixins.
+ This records the ``db`` (which is a Flask-SqlAlchemy object) for use in
+ mixins.
+ """
+
+ roles_users = None
+ db = None
+ fs_model_version = 1
+
+ @classmethod
+ def set_db_info(cls, appdb):
+ """ Initialize Model.
+ This needs to be called after the DB object has been created
+ (e.g. db = Sqlalchemy())
+ """
+ cls.db = appdb
+ cls.roles_users = appdb.Table(
+ "roles_users",
+ Column("user_id", Integer(), ForeignKey("user.id")),
+ Column("role_id", Integer(), ForeignKey("role.id")),
+ )
+
+
+class FsRoleMixin(RoleMixin):
+ id = Column(Integer(), primary_key=True)
+ name = Column(String(80), unique=True, nullable=False)
+ description = Column(String(255))
+ # A comma separated list of strings
+ permissions = Column(UnicodeText, nullable=True)
+ update_datetime = Column(
+ DateTime,
+ nullable=False,
+ server_default=func.now(),
+ onupdate=datetime.datetime.utcnow,
+ )
+
+
+class FsUserMixin(UserMixin):
+ """ User information
+ """
+
+ # flask_security basic fields
+ id = Column(Integer, primary_key=True)
+ email = Column(String(255), unique=True, nullable=False)
+ # Username is important since shouldn't expose email to other users in most cases.
+ username = Column(String(255))
+ password = Column(String(255), nullable=False)
+ active = Column(Boolean(), nullable=False)
+
+ # Faster token checking
+ fs_uniquifier = Column(String(64), unique=True, nullable=False)
+
+ # confirmable
+ confirmed_at = Column(DateTime())
+
+ # trackable
+ last_login_at = Column(DateTime())
+ current_login_at = Column(DateTime())
+ last_login_ip = Column(String(64))
+ current_login_ip = Column(String(64))
+ login_count = Column(Integer)
+
+ # 2FA
+ tf_primary_method = Column(String(64), nullable=True)
+ tf_totp_secret = Column(String(255), nullable=True)
+ tf_phone_number = Column(String(128), nullable=True)
+
+ @declared_attr
+ def roles(cls):
+ return FsModels.db.relationship(
+ "Role",
+ secondary=FsModels.roles_users,
+ backref=FsModels.db.backref("users", lazy="dynamic"),
+ )
+
+ create_datetime = Column(DateTime, nullable=False, server_default=func.now())
+ update_datetime = Column(
+ DateTime,
+ nullable=False,
+ server_default=func.now(),
+ onupdate=datetime.datetime.utcnow,
+ )
+
+
+"""
+These are placeholders - not current used
+"""
+
+
+class FsOauth2ClientMixin(object):
+ """ Oauth2 client """
+
+ id = Column(String(64), primary_key=True)
+
+ @declared_attr
+ def user_id(cls):
+ return Column(
+ Integer, ForeignKey("user.id", ondelete="CASCADE"), nullable=False
+ )
+
+ @declared_attr
+ def user(cls):
+ return relationship("User")
+
+ grant_type = Column(String(32), nullable=False)
+ scopes = Column(UnicodeText(), default="")
+ response_type = Column(UnicodeText, nullable=False, default="")
+ redirect_uris = Column(UnicodeText())
+
+
+class FsTokenMixin(object):
+ """ (Bearer) Tokens that have been given out """
+
+ id = Column(Integer, primary_key=True)
+
+ @declared_attr
+ def client_id(cls):
+ return Column(
+ Integer, ForeignKey("oauth2_client.id", ondelete="CASCADE"), nullable=False
+ )
+
+ # client = relationship("fs_oauth2_client")
+ @declared_attr
+ def user_id(cls):
+ return Column(
+ Integer, ForeignKey("user.id", ondelete="CASCADE"), nullable=False
+ )
+
+ scopes = Column(UnicodeText(), default="")
+ revoked = Column(Boolean(), nullable=False, default=False)
+ access_token = Column(String(100), unique=True, nullable=False)
+ refresh_token = Column(String(100), unique=True)
+ issued_at = Column(DateTime, nullable=False, server_default=func.now())
+ expires_at = Column(DateTime())
diff --git a/flask_security/models/fsqla_v2.py b/flask_security/models/fsqla_v2.py
new file mode 100644
index 0000000..fe7c398
--- /dev/null
+++ b/flask_security/models/fsqla_v2.py
@@ -0,0 +1,53 @@
+"""
+Copyright 2020 by J. Christopher Wagner (jwag). All rights reserved.
+:license: MIT, see LICENSE for more details.
+
+
+Complete models for all features when using Flask-SqlAlchemy
+
+BE AWARE: Once any version of this is shipped no changes can be made - instead
+a new version needs to be created.
+
+This is Version 2:
+ - Add support for unified sign in.
+ - Make username unique (but not required).
+"""
+
+from sqlalchemy import Column, String, Text
+from sqlalchemy.ext.declarative import declared_attr
+
+
+from .fsqla import FsModels as FsModelsV1
+from .fsqla import FsUserMixin as FsUserMixinV1
+from .fsqla import FsRoleMixin as FsRoleMixinV1
+
+
+class FsModels(FsModelsV1):
+ fs_model_version = 2
+ pass
+
+
+class FsRoleMixin(FsRoleMixinV1):
+ pass
+
+
+class FsUserMixin(FsUserMixinV1):
+ """ User information
+ """
+
+ # Make username unique but not required.
+ username = Column(String(255), unique=True, nullable=True)
+
+ # unified sign in
+ us_totp_secrets = Column(Text, nullable=True)
+ us_phone_number = Column(String(128), nullable=True)
+
+ # This is repeated since I couldn't figure out how to have it reference the
+ # new version of FsModels.
+ @declared_attr
+ def roles(cls):
+ return FsModels.db.relationship(
+ "Role",
+ secondary=FsModels.roles_users,
+ backref=FsModels.db.backref("users", lazy="dynamic"),
+ )
diff --git a/flask_security/passwordless.py b/flask_security/passwordless.py
new file mode 100644
index 0000000..6f91553
--- /dev/null
+++ b/flask_security/passwordless.py
@@ -0,0 +1,62 @@
+# -*- coding: utf-8 -*-
+"""
+ flask_security.passwordless
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Flask-Security passwordless module
+
+ :copyright: (c) 2012 by Matt Wright.
+ :license: MIT, see LICENSE for more details.
+"""
+
+from flask import current_app as app
+from werkzeug.local import LocalProxy
+
+from .signals import login_instructions_sent
+from .utils import config_value, get_token_status, url_for_security
+
+# Convenient references
+_security = LocalProxy(lambda: app.extensions["security"])
+
+_datastore = LocalProxy(lambda: _security.datastore)
+
+
+def send_login_instructions(user):
+ """Sends the login instructions email for the specified user.
+
+ :param user: The user to send the instructions to
+ :param token: The login token
+ """
+ token = generate_login_token(user)
+ login_link = url_for_security("token_login", token=token, _external=True)
+
+ _security._send_mail(
+ config_value("EMAIL_SUBJECT_PASSWORDLESS"),
+ user.email,
+ "login_instructions",
+ user=user,
+ login_link=login_link,
+ )
+
+ login_instructions_sent.send(
+ app._get_current_object(), user=user, login_token=token
+ )
+
+
+def generate_login_token(user):
+ """Generates a unique login token for the specified user.
+
+ :param user: The user the token belongs to
+ """
+ return _security.login_serializer.dumps([str(user.id)])
+
+
+def login_token_status(token):
+ """Returns the expired status, invalid status, and user of a login token.
+ For example::
+
+ expired, invalid, user = login_token_status('...')
+
+ :param token: The login token
+ """
+ return get_token_status(token, "login", "LOGIN")
diff --git a/flask_security/phone_util.py b/flask_security/phone_util.py
new file mode 100644
index 0000000..9bd0195
--- /dev/null
+++ b/flask_security/phone_util.py
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+"""
+ flask_security.phone_util
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Utility class for managing phone numbers
+
+ :copyright: (c) 2020 by J. Christopher Wagner (jwag).
+ :license: MIT, see LICENSE for more details.
+
+ Avoid making 'phonenumbers' a required package unless needed.
+"""
+
+from .utils import config_value, get_message
+
+
+class PhoneUtil(object):
+ """
+ Provide parsing and validation for user inputted phone numbers.
+ Subclass this to use a different underlying phone number parsing library.
+
+ To provide your own implementation, pass in the class as ``phone_util_cls``
+ at init time. Your class will be instantiated once prior to the first
+ request being handled.
+
+ .. versionadded:: 3.4.0
+ """
+
+ def validate_phone_number(self, input_data):
+ """ Return ``None`` if a valid phone number else an error message. """
+ import phonenumbers
+
+ try:
+ z = phonenumbers.parse(
+ input_data, region=config_value("PHONE_REGION_DEFAULT")
+ )
+ if phonenumbers.is_valid_number(z):
+ return None
+ except phonenumbers.phonenumberutil.NumberParseException:
+ pass
+ return get_message("PHONE_INVALID")[0]
+
+ def get_canonical_form(self, input_data):
+ """ Validate and return a canonical form to be stored in DB
+ and compared against.
+ Returns ``None`` if input isn't a valid phone number.
+ """
+ import phonenumbers
+
+ try:
+ z = phonenumbers.parse(
+ input_data, region=config_value("PHONE_REGION_DEFAULT")
+ )
+ if phonenumbers.is_valid_number(z):
+ return phonenumbers.format_number(
+ z, phonenumbers.PhoneNumberFormat.E164
+ )
+ return None
+ except phonenumbers.phonenumberutil.NumberParseException:
+ return None
diff --git a/flask_security/quart_compat.py b/flask_security/quart_compat.py
new file mode 100644
index 0000000..677b452
--- /dev/null
+++ b/flask_security/quart_compat.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+"""
+ flask_security.quart_compat
+ ~~~~~~~~~~~~~~~~~~~~
+
+ Flask-Security quart compatibility modiles
+
+ :copyright: (c) 2019 by Shinon.
+ :license: MIT, see LICENSE for more details.
+
+ This modules tests whether we are using quart or not
+ we can test if the name of the imported flask is: quart.flask_patch
+"""
+import flask
+
+if "quart." in flask.__name__ or hasattr(flask, "_quart_patched"): # pragma: no cover
+ is_quart = True
+else:
+ is_quart = False
+
+
+@property
+def best(self): # pragma: no cover
+ options = sorted(
+ self.options,
+ key=lambda option: (option.value != "*", option.quality, option.value),
+ reverse=True,
+ )
+ return options[0].value
+
+
+def get_quart_status():
+ """
+ Tests if we are using Quart Patched Flask or Vanilla Flask.
+ :return: boolean value determining if it is quart patched flask or not
+ """
+ return is_quart
diff --git a/flask_security/recoverable.py b/flask_security/recoverable.py
new file mode 100644
index 0000000..e70189a
--- /dev/null
+++ b/flask_security/recoverable.py
@@ -0,0 +1,107 @@
+# -*- coding: utf-8 -*-
+"""
+ flask_security.recoverable
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Flask-Security recoverable module
+
+ :copyright: (c) 2012 by Matt Wright.
+ :license: MIT, see LICENSE for more details.
+"""
+
+from flask import current_app as app
+from werkzeug.local import LocalProxy
+
+from .signals import password_reset, reset_password_instructions_sent
+from .utils import (
+ config_value,
+ get_token_status,
+ hash_data,
+ hash_password,
+ url_for_security,
+ verify_hash,
+)
+
+# Convenient references
+_security = LocalProxy(lambda: app.extensions["security"])
+
+_datastore = LocalProxy(lambda: _security.datastore)
+
+
+def send_reset_password_instructions(user):
+ """Sends the reset password instructions email for the specified user.
+
+ :param user: The user to send the instructions to
+ """
+ token = generate_reset_password_token(user)
+ reset_link = url_for_security("reset_password", token=token, _external=True)
+
+ if config_value("SEND_PASSWORD_RESET_EMAIL"):
+ _security._send_mail(
+ config_value("EMAIL_SUBJECT_PASSWORD_RESET"),
+ user.email,
+ "reset_instructions",
+ user=user,
+ reset_link=reset_link,
+ )
+
+ reset_password_instructions_sent.send(
+ app._get_current_object(), user=user, token=token
+ )
+
+
+def send_password_reset_notice(user):
+ """Sends the password reset notice email for the specified user.
+
+ :param user: The user to send the notice to
+ """
+ if config_value("SEND_PASSWORD_RESET_NOTICE_EMAIL"):
+ _security._send_mail(
+ config_value("EMAIL_SUBJECT_PASSWORD_NOTICE"),
+ user.email,
+ "reset_notice",
+ user=user,
+ )
+
+
+def generate_reset_password_token(user):
+ """Generates a unique reset password token for the specified user.
+
+ :param user: The user to work with
+ """
+ password_hash = hash_data(user.password) if user.password else None
+ data = [str(user.id), password_hash]
+ return _security.reset_serializer.dumps(data)
+
+
+def reset_password_token_status(token):
+ """Returns the expired status, invalid status, and user of a password reset
+ token. For example::
+
+ expired, invalid, user, data = reset_password_token_status('...')
+
+ :param token: The password reset token
+ """
+ expired, invalid, user, data = get_token_status(
+ token, "reset", "RESET_PASSWORD", return_data=True
+ )
+ if not invalid and user:
+ if user.password:
+ if not verify_hash(data[1], user.password):
+ invalid = True
+
+ return expired, invalid, user
+
+
+def update_password(user, password):
+ """Update the specified user's password
+
+ :param user: The user to update_password
+ :param password: The unhashed new password
+ """
+ user.password = hash_password(password)
+ if config_value("BACKWARDS_COMPAT_AUTH_TOKEN_INVALID"):
+ _datastore.set_uniquifier(user)
+ _datastore.put(user)
+ send_password_reset_notice(user)
+ password_reset.send(app._get_current_object(), user=user)
diff --git a/flask_security/registerable.py b/flask_security/registerable.py
new file mode 100644
index 0000000..bee7ba7
--- /dev/null
+++ b/flask_security/registerable.py
@@ -0,0 +1,72 @@
+# -*- coding: utf-8 -*-
+"""
+ flask_security.registerable
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Flask-Security registerable module
+
+ :copyright: (c) 2012 by Matt Wright.
+ :copyright: (c) 2019-2020 by J. Christopher Wagner (jwag).
+ :license: MIT, see LICENSE for more details.
+"""
+
+import uuid
+
+from flask import current_app as app
+from werkzeug.local import LocalProxy
+
+from .confirmable import generate_confirmation_link
+from .signals import user_registered
+from .utils import config_value, do_flash, get_message, hash_password
+
+# Convenient references
+_security = LocalProxy(lambda: app.extensions["security"])
+
+_datastore = LocalProxy(lambda: _security.datastore)
+
+
+def register_user(registration_form):
+ """
+ Calls datastore to create user, triggers post-registration logic
+ (e.g. sending confirmation link, sending registration mail)
+ :param registration_form: form with user registration data
+ :return: user instance
+ """
+
+ user_model_kwargs = registration_form.to_dict(only_user=True)
+
+ if not user_model_kwargs["password"]:
+ # For no password - set an unguessable password.
+ # Since we still allow 'plaintext' as a password scheme - can't use a simple
+ # sentinel.
+ user_model_kwargs["password"] = "NoPassword-" + uuid.uuid4().hex
+
+ user_model_kwargs["password"] = hash_password(user_model_kwargs["password"])
+ user = _datastore.create_user(**user_model_kwargs)
+ # This has always been here - but should probably be removed since in all other
+ # cases we use a 'after_this_request(commit)'. Seems like this would break quart
+ # compat as well?
+ _datastore.commit()
+
+ confirmation_link, token = None, None
+ if _security.confirmable:
+ confirmation_link, token = generate_confirmation_link(user)
+ do_flash(*get_message("CONFIRM_REGISTRATION", email=user.email))
+
+ user_registered.send(
+ app._get_current_object(),
+ user=user,
+ confirm_token=token,
+ form_data=registration_form.to_dict(only_user=False),
+ )
+
+ if config_value("SEND_REGISTER_EMAIL"):
+ _security._send_mail(
+ config_value("EMAIL_SUBJECT_REGISTER"),
+ user.email,
+ "welcome",
+ user=user,
+ confirmation_link=confirmation_link,
+ )
+
+ return user
diff --git a/flask_security/signals.py b/flask_security/signals.py
new file mode 100644
index 0000000..cd94b95
--- /dev/null
+++ b/flask_security/signals.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+"""
+ flask_security.signals
+ ~~~~~~~~~~~~~~~~~~~~~~
+
+ Flask-Security signals module
+
+ :copyright: (c) 2012 by Matt Wright.
+ :copyright: (c) 2019-2020 by J. Christopher Wagner (jwag).
+ :license: MIT, see LICENSE for more details.
+"""
+
+import blinker
+
+signals = blinker.Namespace()
+
+user_authenticated = signals.signal("user-authenticated")
+
+user_registered = signals.signal("user-registered")
+
+user_confirmed = signals.signal("user-confirmed")
+
+confirm_instructions_sent = signals.signal("confirm-instructions-sent")
+
+login_instructions_sent = signals.signal("login-instructions-sent")
+
+password_reset = signals.signal("password-reset")
+
+password_changed = signals.signal("password-changed")
+
+reset_password_instructions_sent = signals.signal("password-reset-instructions-sent")
+
+tf_code_confirmed = signals.signal("tf-code-confirmed")
+
+tf_profile_changed = signals.signal("tf-profile-changed")
+
+tf_security_token_sent = signals.signal("tf-security-token-sent")
+
+tf_disabled = signals.signal("tf-disabled")
+
+us_security_token_sent = signals.signal("us-security-token-sent")
+
+us_profile_changed = signals.signal("us-profile-changed")
diff --git a/flask_security/templates/security/_macros.html b/flask_security/templates/security/_macros.html
new file mode 100644
index 0000000..f3111f1
--- /dev/null
+++ b/flask_security/templates/security/_macros.html
@@ -0,0 +1,28 @@
+{% macro render_field_with_errors(field) %}
+ <p>
+ {{ field.label }} {{ field(**kwargs)|safe }}
+ {% if field.errors %}
+ <ul>
+ {% for error in field.errors %}
+ <li>{{ error }}</li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+ </p>
+{% endmacro %}
+
+{% macro render_field(field) %}
+ <p>{{ field(**kwargs)|safe }}</p>
+{% endmacro %}
+
+{% macro render_field_errors(field) %}
+ <p>
+ {% if field and field.errors %}
+ <ul>
+ {% for error in field.errors %}
+ <li>{{ error }}</li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+ </p>
+{% endmacro %}
diff --git a/flask_security/templates/security/_menu.html b/flask_security/templates/security/_menu.html
new file mode 100644
index 0000000..c032ade
--- /dev/null
+++ b/flask_security/templates/security/_menu.html
@@ -0,0 +1,20 @@
+{% if security.registerable or security.recoverable or security.confirmable or security.unified_signin %}
+<h2>{{ _('Menu') }}</h2>
+<ul>
+ {% if not skip_login_menu %}
+ <li><a href="{{ url_for_security('login') }}{% if 'next' in request.args %}?next={{ request.args.next|urlencode }}{% endif %}">{{ _('Login') }}</a></li>
+ {% endif %}
+ {% if security.unified_signin and not skip_login_menu %}
+ <li><a href="{{ url_for_security('us_signin') }}{% if 'next' in request.args %}?next={{ request.args.next|urlencode }}{% endif %}">{{ _("Unified Sign In") }}</a><br/></li>
+ {% endif %}
+ {% if security.registerable %}
+ <li><a href="{{ url_for_security('register') }}{% if 'next' in request.args %}?next={{ request.args.next|urlencode }}{% endif %}">{{ _('Register') }}</a><br/></li>
+ {% endif %}
+ {% if security.recoverable %}
+ <li><a href="{{ url_for_security('forgot_password') }}">{{ _('Forgot password') }}</a><br/></li>
+ {% endif %}
+ {% if security.confirmable %}
+ <li><a href="{{ url_for_security('send_confirmation') }}">{{ _('Confirm account') }}</a></li>
+ {% endif %}
+</ul>
+{% endif %}
diff --git a/flask_security/templates/security/_messages.html b/flask_security/templates/security/_messages.html
new file mode 100644
index 0000000..dabc7b5
--- /dev/null
+++ b/flask_security/templates/security/_messages.html
@@ -0,0 +1,9 @@
+{%- with messages = get_flashed_messages(with_categories=true) -%}
+ {% if messages %}
+ <ul class="flashes">
+ {% for category, message in messages %}
+ <li class="{{ category }}">{{ message }}</li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+{%- endwith %}
diff --git a/flask_security/templates/security/base.html b/flask_security/templates/security/base.html
new file mode 100644
index 0000000..da792e7
--- /dev/null
+++ b/flask_security/templates/security/base.html
@@ -0,0 +1,30 @@
+{% block doc -%}
+<!DOCTYPE html>
+<html{% block html_attribs %}{% endblock html_attribs %}>
+ {%- block html %}
+ <head>
+ {%- block head %}
+ <title>{% block title %}{{ title|default }}{% endblock title %}</title>
+
+ {%- block metas %}
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ {%- endblock metas %}
+
+ {%- block styles %}
+ {%- endblock styles %}
+ {%- endblock head %}
+ </head>
+ <body{% block body_attribs %}{% endblock body_attribs %}>
+ {% block body -%}
+ {% block navbar %}
+ {%- endblock navbar %}
+ {% block content -%}
+ {%- endblock content %}
+
+ {% block scripts %}
+ {%- endblock scripts %}
+ {%- endblock body %}
+ </body>
+ {%- endblock html %}
+</html>
+{% endblock doc -%}
diff --git a/flask_security/templates/security/change_password.html b/flask_security/templates/security/change_password.html
new file mode 100644
index 0000000..0c08d16
--- /dev/null
+++ b/flask_security/templates/security/change_password.html
@@ -0,0 +1,14 @@
+{% extends "security/base.html" %}
+{% from "security/_macros.html" import render_field_with_errors, render_field %}
+
+{% block content %}
+{% include "security/_messages.html" %}
+<h1>{{ _('Change password') }}</h1>
+<form action="{{ url_for_security('change_password') }}" method="POST" name="change_password_form">
+ {{ change_password_form.hidden_tag() }}
+ {{ render_field_with_errors(change_password_form.password) }}
+ {{ render_field_with_errors(change_password_form.new_password) }}
+ {{ render_field_with_errors(change_password_form.new_password_confirm) }}
+ {{ render_field(change_password_form.submit) }}
+</form>
+{% endblock %}
diff --git a/flask_security/templates/security/email/change_notice.html b/flask_security/templates/security/email/change_notice.html
new file mode 100644
index 0000000..9699641
--- /dev/null
+++ b/flask_security/templates/security/email/change_notice.html
@@ -0,0 +1,4 @@
+<p>{{ _('Your password has been changed.') }}</p>
+{% if security.recoverable %}
+<p>{{ _('If you did not change your password,') }} <a href="{{ url_for_security('forgot_password', _external=True) }}">{{ _('click here to reset it') }}</a>.</p>
+{% endif %}
diff --git a/flask_security/templates/security/email/change_notice.txt b/flask_security/templates/security/email/change_notice.txt
new file mode 100644
index 0000000..63c72b3
--- /dev/null
+++ b/flask_security/templates/security/email/change_notice.txt
@@ -0,0 +1,5 @@
+{{ _('Your password has been changed') }}
+{% if security.recoverable %}
+{{ _('If you did not change your password, click the link below to reset it.') }}
+{{ url_for_security('forgot_password', _external=True) }}
+{% endif %}
diff --git a/flask_security/templates/security/email/confirmation_instructions.html b/flask_security/templates/security/email/confirmation_instructions.html
new file mode 100644
index 0000000..449812d
--- /dev/null
+++ b/flask_security/templates/security/email/confirmation_instructions.html
@@ -0,0 +1,3 @@
+<p>{{ _('Please confirm your email through the link below:') }}</p>
+
+<p><a href="{{ confirmation_link }}">{{ _('Confirm my account') }}</a></p>
diff --git a/flask_security/templates/security/email/confirmation_instructions.txt b/flask_security/templates/security/email/confirmation_instructions.txt
new file mode 100644
index 0000000..acf0994
--- /dev/null
+++ b/flask_security/templates/security/email/confirmation_instructions.txt
@@ -0,0 +1,3 @@
+{{ _('Please confirm your email through the link below:') }}
+
+{{ confirmation_link }}
diff --git a/flask_security/templates/security/email/login_instructions.html b/flask_security/templates/security/email/login_instructions.html
new file mode 100644
index 0000000..fad7d11
--- /dev/null
+++ b/flask_security/templates/security/email/login_instructions.html
@@ -0,0 +1,5 @@
+<p>{{ _('Welcome %(email)s!', email=user.email) }}</p>
+
+<p>{{ _('You can log into your account through the link below:') }}</p>
+
+<p><a href="{{ login_link }}">{{ _('Login now') }}</a></p>
diff --git a/flask_security/templates/security/email/login_instructions.txt b/flask_security/templates/security/email/login_instructions.txt
new file mode 100644
index 0000000..043bd95
--- /dev/null
+++ b/flask_security/templates/security/email/login_instructions.txt
@@ -0,0 +1,5 @@
+{{ _('Welcome %(email)s!', email=user.email) }}
+
+{{ _('You can log into your account through the link below:') }}
+
+{{ login_link }}
diff --git a/flask_security/templates/security/email/reset_instructions.html b/flask_security/templates/security/email/reset_instructions.html
new file mode 100644
index 0000000..879f732
--- /dev/null
+++ b/flask_security/templates/security/email/reset_instructions.html
@@ -0,0 +1 @@
+<p><a href="{{ reset_link }}">{{ _('Click here to reset your password') }}</a></p>
diff --git a/flask_security/templates/security/email/reset_instructions.txt b/flask_security/templates/security/email/reset_instructions.txt
new file mode 100644
index 0000000..7f25544
--- /dev/null
+++ b/flask_security/templates/security/email/reset_instructions.txt
@@ -0,0 +1,3 @@
+{{ _('Click the link below to reset your password:') }}
+
+{{ reset_link }}
diff --git a/flask_security/templates/security/email/reset_notice.html b/flask_security/templates/security/email/reset_notice.html
new file mode 100644
index 0000000..6911932
--- /dev/null
+++ b/flask_security/templates/security/email/reset_notice.html
@@ -0,0 +1 @@
+<p>{{ _('Your password has been reset') }}</p>
diff --git a/flask_security/templates/security/email/reset_notice.txt b/flask_security/templates/security/email/reset_notice.txt
new file mode 100644
index 0000000..da93147
--- /dev/null
+++ b/flask_security/templates/security/email/reset_notice.txt
@@ -0,0 +1 @@
+{{ _('Your password has been reset') }}
diff --git a/flask_security/templates/security/email/two_factor_instructions.html b/flask_security/templates/security/email/two_factor_instructions.html
new file mode 100644
index 0000000..b1b2192
--- /dev/null
+++ b/flask_security/templates/security/email/two_factor_instructions.html
@@ -0,0 +1,3 @@
+<p>{{ _("Welcome") }} {{ username }}!</p>
+
+<p>{{ _("You can log into your account using the following code:") }} {{ token }}</p>
diff --git a/flask_security/templates/security/email/two_factor_instructions.txt b/flask_security/templates/security/email/two_factor_instructions.txt
new file mode 100644
index 0000000..6bbc865
--- /dev/null
+++ b/flask_security/templates/security/email/two_factor_instructions.txt
@@ -0,0 +1,3 @@
+{{ _("Welcome") }} {{ username }}!
+
+{{ _("You can log into your account using the following code:") }} {{ token }}
diff --git a/flask_security/templates/security/email/two_factor_rescue.html b/flask_security/templates/security/email/two_factor_rescue.html
new file mode 100644
index 0000000..ba8c433
--- /dev/null
+++ b/flask_security/templates/security/email/two_factor_rescue.html
@@ -0,0 +1 @@
+<p> {{ user.email }} {{ _("can not access mail account") }}</p>
diff --git a/flask_security/templates/security/email/two_factor_rescue.txt b/flask_security/templates/security/email/two_factor_rescue.txt
new file mode 100644
index 0000000..4508e33
--- /dev/null
+++ b/flask_security/templates/security/email/two_factor_rescue.txt
@@ -0,0 +1 @@
+{{ user.email }} {{ _("can not access mail account") }}
diff --git a/flask_security/templates/security/email/us_instructions.html b/flask_security/templates/security/email/us_instructions.html
new file mode 100644
index 0000000..02cd3ed
--- /dev/null
+++ b/flask_security/templates/security/email/us_instructions.html
@@ -0,0 +1,9 @@
+<p>{{ _("Welcome") }} {{ username }}!</p>
+
+<p>{{ _("You can sign into your account using the following code:") }} {{ token }}</p>
+
+{% if login_link %}
+ <p>{{ _("Or use the the link below:") }}</p>
+
+ <p><a href="{{ login_link }}">{{ _("Sign In") }}</a></p>
+{% endif %}
diff --git a/flask_security/templates/security/email/us_instructions.txt b/flask_security/templates/security/email/us_instructions.txt
new file mode 100644
index 0000000..90e9975
--- /dev/null
+++ b/flask_security/templates/security/email/us_instructions.txt
@@ -0,0 +1,10 @@
+{{ _("Welcome") }} {{ username }}!
+
+{{ _("You can sign into your account using the following code:") }} {{ token }}
+
+{% if login_link %}
+
+ {{ _("Or use the link below:") }}
+
+ {{ login_link }}
+{% endif %}
diff --git a/flask_security/templates/security/email/welcome.html b/flask_security/templates/security/email/welcome.html
new file mode 100644
index 0000000..1b33bd6
--- /dev/null
+++ b/flask_security/templates/security/email/welcome.html
@@ -0,0 +1,7 @@
+<p>{{ _('Welcome %(email)s!', email=user.email) }}</p>
+
+{% if security.confirmable %}
+<p>{{ _('You can confirm your email through the link below:') }}</p>
+
+<p><a href="{{ confirmation_link }}">{{ _('Confirm my account') }}</a></p>
+{% endif %}
diff --git a/flask_security/templates/security/email/welcome.txt b/flask_security/templates/security/email/welcome.txt
new file mode 100644
index 0000000..b5c454c
--- /dev/null
+++ b/flask_security/templates/security/email/welcome.txt
@@ -0,0 +1,7 @@
+{{ _('Welcome %(email)s!', email=user.email) }}
+
+{% if security.confirmable %}
+{{ _('You can confirm your email through the link below:') }}
+
+{{ confirmation_link }}
+{% endif %}
diff --git a/flask_security/templates/security/forgot_password.html b/flask_security/templates/security/forgot_password.html
new file mode 100644
index 0000000..22391db
--- /dev/null
+++ b/flask_security/templates/security/forgot_password.html
@@ -0,0 +1,13 @@
+{% extends "security/base.html" %}
+{% from "security/_macros.html" import render_field_with_errors, render_field %}
+
+{% block content %}
+{% include "security/_messages.html" %}
+<h1>{{ _('Send password reset instructions') }}</h1>
+<form action="{{ url_for_security('forgot_password') }}" method="POST" name="forgot_password_form">
+ {{ forgot_password_form.hidden_tag() }}
+ {{ render_field_with_errors(forgot_password_form.email) }}
+ {{ render_field(forgot_password_form.submit) }}
+</form>
+{% include "security/_menu.html" %}
+{% endblock %}
diff --git a/flask_security/templates/security/login_user.html b/flask_security/templates/security/login_user.html
new file mode 100644
index 0000000..13791d2
--- /dev/null
+++ b/flask_security/templates/security/login_user.html
@@ -0,0 +1,16 @@
+{% extends "security/base.html" %}
+{% from "security/_macros.html" import render_field_with_errors, render_field, render_field_errors %}
+
+{% block content %}
+{% include "security/_messages.html" %}
+<h1>{{ _('Login') }}</h1>
+<form action="{{ url_for_security('login') }}" method="POST" name="login_user_form">
+ {{ login_user_form.hidden_tag() }}
+ {{ render_field_with_errors(login_user_form.email) }}
+ {{ render_field_with_errors(login_user_form.password) }}
+ {{ render_field_with_errors(login_user_form.remember) }}
+ {{ render_field_errors(login_user_form.csrf_token) }}
+ {{ render_field(login_user_form.submit) }}
+</form>
+{% include "security/_menu.html" %}
+{% endblock %}
diff --git a/flask_security/templates/security/register_user.html b/flask_security/templates/security/register_user.html
new file mode 100644
index 0000000..abad512
--- /dev/null
+++ b/flask_security/templates/security/register_user.html
@@ -0,0 +1,17 @@
+{% extends "security/base.html" %}
+{% from "security/_macros.html" import render_field_with_errors, render_field %}
+
+{% block content %}
+{% include "security/_messages.html" %}
+<h1>{{ _('Register') }}</h1>
+<form action="{{ url_for_security('register') }}" method="POST" name="register_user_form">
+ {{ register_user_form.hidden_tag() }}
+ {{ render_field_with_errors(register_user_form.email) }}
+ {{ render_field_with_errors(register_user_form.password) }}
+ {% if register_user_form.password_confirm %}
+ {{ render_field_with_errors(register_user_form.password_confirm) }}
+ {% endif %}
+ {{ render_field(register_user_form.submit) }}
+</form>
+{% include "security/_menu.html" %}
+{% endblock %}
diff --git a/flask_security/templates/security/reset_password.html b/flask_security/templates/security/reset_password.html
new file mode 100644
index 0000000..18cf6f9
--- /dev/null
+++ b/flask_security/templates/security/reset_password.html
@@ -0,0 +1,14 @@
+{% extends "security/base.html" %}
+{% from "security/_macros.html" import render_field_with_errors, render_field %}
+
+{% block content %}
+{% include "security/_messages.html" %}
+<h1>{{ _('Reset password') }}</h1>
+<form action="{{ url_for_security('reset_password', token=reset_password_token) }}" method="POST" name="reset_password_form">
+ {{ reset_password_form.hidden_tag() }}
+ {{ render_field_with_errors(reset_password_form.password) }}
+ {{ render_field_with_errors(reset_password_form.password_confirm) }}
+ {{ render_field(reset_password_form.submit) }}
+</form>
+{% include "security/_menu.html" %}
+{% endblock %}
diff --git a/flask_security/templates/security/send_confirmation.html b/flask_security/templates/security/send_confirmation.html
new file mode 100644
index 0000000..730c8b8
--- /dev/null
+++ b/flask_security/templates/security/send_confirmation.html
@@ -0,0 +1,13 @@
+{% extends "security/base.html" %}
+{% from "security/_macros.html" import render_field_with_errors, render_field %}
+
+{% block content %}
+{% include "security/_messages.html" %}
+<h1>{{ _('Resend confirmation instructions') }}</h1>
+<form action="{{ url_for_security('send_confirmation') }}" method="POST" name="send_confirmation_form">
+ {{ send_confirmation_form.hidden_tag() }}
+ {{ render_field_with_errors(send_confirmation_form.email) }}
+ {{ render_field(send_confirmation_form.submit) }}
+</form>
+{% include "security/_menu.html" %}
+{% endblock %}
diff --git a/flask_security/templates/security/send_login.html b/flask_security/templates/security/send_login.html
new file mode 100644
index 0000000..cb628fc
--- /dev/null
+++ b/flask_security/templates/security/send_login.html
@@ -0,0 +1,13 @@
+{% extends "security/base.html" %}
+{% from "security/_macros.html" import render_field_with_errors, render_field %}
+
+{% block content %}
+{% include "security/_messages.html" %}
+<h1>{{ _('Login') }}</h1>
+<form action="{{ url_for_security('login') }}" method="POST" name="send_login_form">
+ {{ send_login_form.hidden_tag() }}
+ {{ render_field_with_errors(send_login_form.email) }}
+ {{ render_field(send_login_form.submit) }}
+</form>
+{% include "security/_menu.html" %}
+{% endblock %}
diff --git a/flask_security/templates/security/two_factor_setup.html b/flask_security/templates/security/two_factor_setup.html
new file mode 100644
index 0000000..fdb080d
--- /dev/null
+++ b/flask_security/templates/security/two_factor_setup.html
@@ -0,0 +1,38 @@
+{% extends "security/base.html" %}
+{% from "security/_macros.html" import render_field_with_errors, render_field, render_field_no_label, render_field_errors %}
+
+{% block content %}
+ {% include "security/_messages.html" %}
+ <h1>{{ _("Two-factor authentication adds an extra layer of security to your account") }}</h1>
+ <h2>{{ _("In addition to your username and password, you'll need to use a code that we will send you") }}</h2>
+ <form action="{{ url_for_security("two_factor_setup") }}" method="POST" name="two_factor_setup_form">
+ {{ two_factor_setup_form.hidden_tag() }}
+ {% for subfield in two_factor_setup_form.setup %}
+ {% if subfield.data in choices %}
+ {{ render_field_with_errors(subfield) }}
+ {% endif %}
+ {% endfor %}
+ {{ render_field_errors(two_factor_setup_form.setup) }}
+ {{ render_field(two_factor_setup_form.submit) }}
+ {% if chosen_method=="email" and chosen_method in choices %}
+ <p>{{ _("To complete logging in, please enter the code sent to your mail") }}</p>
+ {% endif %}
+ {% if chosen_method=="authenticator" and chosen_method in choices %}
+ <p>{{ _("Open your authenticator app on your device and scan the following qrcode to start receiving codes:") }}</p>
+ <p><img alt="{{ _("Two factor authentication code") }}" id="qrcode" src="{{ url_for_security("two_factor_qrcode") }}"></p>
+ {% endif %}
+ {% if chosen_method=="sms" and chosen_method in choices %}
+ <p>{{ _("To Which Phone Number Should We Send Code To?") }}</p>
+ {{ two_factor_setup_form.hidden_tag() }}
+ {{ render_field_with_errors(two_factor_setup_form.phone, placeholder="enter phone number") }}
+ {{ render_field(two_factor_setup_form.submit) }}
+ {% endif %}
+ </form>
+ <form action="{{ url_for_security("two_factor_token_validation") }}" method="POST"
+ name="two_factor_verify_code_form">
+ {{ two_factor_verify_code_form.hidden_tag() }}
+ {{ render_field_with_errors(two_factor_verify_code_form.code) }}
+ {{ render_field(two_factor_verify_code_form.submit) }}
+ </form>
+ {% include "security/_menu.html" %}
+{% endblock %}
diff --git a/flask_security/templates/security/two_factor_verify_code.html b/flask_security/templates/security/two_factor_verify_code.html
new file mode 100644
index 0000000..c58d1ba
--- /dev/null
+++ b/flask_security/templates/security/two_factor_verify_code.html
@@ -0,0 +1,26 @@
+{% extends "security/base.html" %}
+{% from "security/_macros.html" import render_field_with_errors, render_field %}
+
+{% block content %}
+ {% include "security/_messages.html" %}
+ <h1>{{ _("Two-factor Authentication") }}</h1>
+ <h2>{{ _("Please enter your authentication code") }}</h2>
+ <form action="{{ url_for_security("two_factor_token_validation") }}" method="POST"
+ name="two_factor_verify_code_form">
+ {{ two_factor_verify_code_form.hidden_tag() }}
+ {{ render_field_with_errors(two_factor_verify_code_form.code, placeholder="enter code") }}
+ {{ render_field(two_factor_verify_code_form.submit) }}
+ </form>
+ <form action="{{ url_for_security("two_factor_rescue") }}" method="POST" name="two_factor_rescue_form">
+ {{ two_factor_rescue_form.hidden_tag() }}
+ {{ render_field_with_errors(two_factor_rescue_form.help_setup) }}
+ {% if problem=="lost_device" %}
+ <p>{{ _("The code for authentication was sent to your email address") }}</p>
+ {% endif %}
+ {% if problem=="no_mail_access" %}
+ <p>{{ _("A mail was sent to us in order to reset your application account") }}</p>
+ {% endif %}
+ {{ render_field(two_factor_rescue_form.submit) }}
+ </form>
+ {% include "security/_menu.html" %}
+{% endblock %}
diff --git a/flask_security/templates/security/two_factor_verify_password.html b/flask_security/templates/security/two_factor_verify_password.html
new file mode 100644
index 0000000..a6d6cce
--- /dev/null
+++ b/flask_security/templates/security/two_factor_verify_password.html
@@ -0,0 +1,13 @@
+{% extends "security/base.html" %}
+{% from "security/_macros.html" import render_field_with_errors, render_field %}
+
+{% block content %}
+ {% include "security/_messages.html" %}
+ <h1>{{ _("Please Enter Your Password") }}</h1>
+ <form action="{{ url_for_security("two_factor_verify_password") }}" method="POST"
+ name="two_factor_verify_password_form">
+ {{ two_factor_verify_password_form.hidden_tag() }}
+ {{ render_field_with_errors(two_factor_verify_password_form.password, placeholder="enter password") }}
+ {{ render_field(two_factor_verify_password_form.submit) }}
+ </form>
+{% endblock %}
diff --git a/flask_security/templates/security/us_setup.html b/flask_security/templates/security/us_setup.html
new file mode 100644
index 0000000..e1d023c
--- /dev/null
+++ b/flask_security/templates/security/us_setup.html
@@ -0,0 +1,47 @@
+{% extends "security/base.html" %}
+{% from "security/_macros.html" import render_field_with_errors, render_field, render_field_errors %}
+
+{% block content %}
+ {% include "security/_messages.html" %}
+ <h1>{{ _("Setup Unified Sign In options") }}</h1>
+ <form action="{{ url_for_security("us_setup") }}" method="POST"
+ name="us_setup_form">
+ {{ us_setup_form.hidden_tag() }}
+ {% if setup_methods %}
+ <p>Currently Active options:
+ {% if active_methods %}
+ {{ ", ".join(active_methods) }}
+ {% else %}
+ None.
+ {% endif %}
+ </p>
+ {% for subfield in us_setup_form.chosen_method %}
+ {% if subfield.data in available_methods %}
+ {{ render_field_with_errors(subfield) }}
+ {% endif %}
+ {% endfor %}
+ {{ render_field_errors(us_setup_form.chosen_method) }}
+ {% if "sms" in available_methods %}
+ {{ render_field_with_errors(us_setup_form.phone) }}
+ {% endif %}
+ {% if chosen_method == "authenticator" %}
+ <p>{{ _("Open your authenticator app on your device and scan the following qrcode to start receiving codes:") }}</p>
+ <p><img alt="{{ _("Passwordless QRCode") }}" id="qrcode" src="{{ url_for_security("us_qrcode", token=state) }}"></p>
+ {% endif %}
+ {% if code_sent %}
+ <p>{{ _("Code has been sent") }}
+ {% endif %}
+ {{ render_field(us_setup_form.submit) }}
+ {% else %}
+ <h3>{{ _("No methods have been enabled - nothing to setup") }}</h3>
+ {% endif %}
+ </form>
+ {% if state %}
+ <form action="{{ url_for_security("us_setup_validate", token=state) }}" method="POST"
+ name="us_setup_validate_form">
+ {{ us_setup_validate_form.hidden_tag() }}
+ {{ render_field_with_errors(us_setup_validate_form.passcode) }}
+ {{ render_field(us_setup_validate_form.submit) }}
+ </form>
+ {% endif %}
+{% endblock %}
diff --git a/flask_security/templates/security/us_signin.html b/flask_security/templates/security/us_signin.html
new file mode 100644
index 0000000..343211d
--- /dev/null
+++ b/flask_security/templates/security/us_signin.html
@@ -0,0 +1,29 @@
+{% extends "security/base.html" %}
+{% from "security/_macros.html" import render_field_with_errors, render_field, render_field_errors %}
+
+{% block content %}
+ {% include "security/_messages.html" %}
+ <h1>{{ _("Sign In") }}</h1>
+ <form action="{{ url_for_security("us_signin") }}" method="POST"
+ name="us_signin_form">
+ {{ us_signin_form.hidden_tag() }}
+ {{ render_field_with_errors(us_signin_form.identity) }}
+ {{ render_field_with_errors(us_signin_form.passcode) }}
+ {{ render_field_with_errors(us_signin_form.remember) }}
+ {{ render_field(us_signin_form.submit) }}
+ {% if code_methods %}
+ <h4>{{ _("Request one-time code be sent") }}</h4>
+ {% for subfield in us_signin_form.chosen_method %}
+ {% if subfield.data in code_methods %}
+ {{ render_field_with_errors(subfield) }}
+ {% endif %}
+ {% endfor %}
+ {{ render_field_errors(us_signin_form.chosen_method) }}
+ {% if code_sent %}
+ <p>{{ _("Code has been sent") }}
+ {% endif %}
+ {{ render_field(us_signin_form.submit_send_code, formaction=url_for_security('us_signin_send_code')) }}
+ {% endif %}
+ </form>
+ {% include "security/_menu.html" %}
+{% endblock %}
diff --git a/flask_security/templates/security/us_verify.html b/flask_security/templates/security/us_verify.html
new file mode 100644
index 0000000..a26416d
--- /dev/null
+++ b/flask_security/templates/security/us_verify.html
@@ -0,0 +1,27 @@
+{% extends "security/base.html" %}
+{% from "security/_macros.html" import render_field_with_errors, render_field, render_field_errors %}
+
+{% block content %}
+ {% include "security/_messages.html" %}
+ <h1>{{ _("Please re-authenticate") }}</h1>
+ <form action="{{ url_for_security("us_verify") }}{% if 'next' in request.args %}?next={{ request.args.next|urlencode }}{% endif %}" method="POST"
+ name="us_verify_form">
+ {{ us_verify_form.hidden_tag() }}
+ {{ render_field_with_errors(us_verify_form.passcode) }}
+ {{ render_field(us_verify_form.submit) }}
+ {% if code_methods %}
+ <h4>{{ _("Request one-time code be sent") }}</h4>
+ {% for subfield in us_verify_form.chosen_method %}
+ {% if subfield.data in code_methods %}
+ {{ render_field_with_errors(subfield) }}
+ {% endif %}
+ {% endfor %}
+ {{ render_field_errors(us_verify_form.chosen_method) }}
+ {% if code_sent %}
+ <p>{{ _("Code has been sent") }}
+ {% endif %}
+ {{ render_field(us_verify_form.submit_send_code, formaction=send_code_to) }}
+ {% endif %}
+ </form>
+ {% include "security/_menu.html" %}
+{% endblock %}
diff --git a/flask_security/templates/security/verify.html b/flask_security/templates/security/verify.html
new file mode 100644
index 0000000..76a5bf8
--- /dev/null
+++ b/flask_security/templates/security/verify.html
@@ -0,0 +1,13 @@
+{% extends "security/base.html" %}
+{% from "security/_macros.html" import render_field_with_errors, render_field %}
+
+{% block content %}
+ {% include "security/_messages.html" %}
+ <h1>{{ _("Please Enter Your Password") }}</h1>
+ <form action="{{ url_for_security("verify") }}{% if 'next' in request.args %}?next={{ request.args.next|urlencode }}{% endif %}" method="POST"
+ name="verify_form">
+ {{ verify_form.hidden_tag() }}
+ {{ render_field_with_errors(verify_form.password) }}
+ {{ render_field(verify_form.submit) }}
+ </form>
+{% endblock %}
diff --git a/flask_security/totp.py b/flask_security/totp.py
new file mode 100644
index 0000000..57b25b8
--- /dev/null
+++ b/flask_security/totp.py
@@ -0,0 +1,106 @@
+# -*- coding: utf-8 -*-
+"""
+ flask_security.totp
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Flask-Security TOTP (Timed-One-Time-Passwords) module
+
+ :copyright: (c) 2019 by J. Christopher Wagner (jwag).
+ :license: MIT, see LICENSE for more details.
+"""
+
+from passlib.totp import TOTP, TokenError
+
+
+class Totp(object):
+ """ Encapsulate usage of Passlib TOTP functionality.
+
+ Flask-Security doesn't implement any replay-attack protection out of the box
+ as suggested by:
+ https://passlib.readthedocs.io/en/stable/narr/totp-tutorial.html#match-verify
+
+ Subclass this and implement the get/set last_counter methods. Your subclass can
+ be registered at Flask-Security creation/initialization time.
+
+ .. versionadded:: 3.4.0
+
+ """
+
+ def __init__(self, secrets, issuer):
+ """ Initialize a totp factory.
+ secrets are used to encrypt the per-user totp_secret on disk.
+ """
+ # This should be a dict with at least one entry
+ if not isinstance(secrets, dict) or len(secrets) < 1:
+ raise ValueError("secrets needs to be a dict with at least one entry")
+ self._totp = TOTP.using(issuer=issuer, secrets=secrets)
+
+ def generate_totp_password(self, totp_secret):
+ """Get time-based one-time password on the basis of given secret and time
+ :param totp_secret: the unique shared secret of the user
+ """
+ return self._totp.from_source(totp_secret).generate().token
+
+ def generate_totp_secret(self):
+ """ Create new user-unique totp_secret.
+
+ We return an encrypted json string so that when sent in a cookie or
+ sent to DB - it is encrypted.
+
+ """
+ return self._totp.new().to_json(encrypt=True)
+
+ def verify_totp(self, token, totp_secret, user, window=0):
+ """ Verifies token for specific user.
+
+ :param token: token to be check against user's secret
+ :param totp_secret: the unique shared secret of the user
+ :param user: User model
+ :param window: optional. How far backward and forward in time to search
+ for a match. Measured in seconds.
+ :return: True if match
+ """
+
+ # TODO - in old implementation using onetimepass window was described
+ # as 'compensate for clock skew) and 'interval_length' would say how long
+ # the token is good for.
+ # In passlib - 'window' means how far back and forward to look and 'clock_skew'
+ # is specifically for well, clock slew.
+ try:
+ tmatch = self._totp.verify(
+ token,
+ totp_secret,
+ window=window,
+ last_counter=self.get_last_counter(user),
+ )
+ self.set_last_counter(user, tmatch)
+ return True
+
+ except TokenError:
+ return False
+
+ def get_totp_uri(self, username, totp_secret):
+ """ Generate provisioning url for use with the qrcode
+ scanner built into the app
+
+ :param username: username/email of the current user
+ :param totp_secret: a unique shared secret of the user
+ """
+ tp = self._totp.from_source(totp_secret)
+ return tp.to_uri(username)
+
+ def get_last_counter(self, user):
+ """ Implement this to fetch stored last_counter from cache.
+
+ :param user: User model
+ :return: last_counter as stored in set_last_counter()
+ """
+ return None
+
+ def set_last_counter(self, user, tmatch):
+ """ Implement this to cache last_counter.
+
+ :param user: User model
+ :param tmatch: a TotpMatch as returned from totp.verify()
+ """
+ pass
diff --git a/flask_security/translations/ca_ES/LC_MESSAGES/flask_security.mo b/flask_security/translations/ca_ES/LC_MESSAGES/flask_security.mo
new file mode 100644
index 0000000..c1e1c16
--- /dev/null
+++ b/flask_security/translations/ca_ES/LC_MESSAGES/flask_security.mo
Binary files differ
diff --git a/flask_security/translations/ca_ES/LC_MESSAGES/flask_security.po b/flask_security/translations/ca_ES/LC_MESSAGES/flask_security.po
new file mode 100644
index 0000000..9313535
--- /dev/null
+++ b/flask_security/translations/ca_ES/LC_MESSAGES/flask_security.po
@@ -0,0 +1,628 @@
+# Catalan (Spain) translations for Flask-Security.
+# Copyright (C) 2017 DINSIC
+# This file is distributed under the same license as the Flask-Security
+# project.
+# Orestes Sanchez <miceno.atreides@gmail.com>, 2018.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Flask-Security 3.1.0\n"
+"Report-Msgid-Bugs-To: info@inveniosoftware.org\n"
+"POT-Creation-Date: 2020-04-19 13:18-0700\n"
+"PO-Revision-Date: 2019-06-16 00:12+0200\n"
+"Last-Translator: Orestes Sanchez <miceno.atreides@gmail.com>\n"
+"Language: ca_ES\n"
+"Language-Team: \n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 2.8.0\n"
+
+#: flask_security/core.py:207
+msgid "Login Required"
+msgstr "Per poder veure la pàgina sol·licitada és necessari iniciar la sessió"
+
+#: flask_security/core.py:208
+#: flask_security/templates/security/email/two_factor_instructions.html:1
+#: flask_security/templates/security/email/us_instructions.html:1
+msgid "Welcome"
+msgstr "Benvingut"
+
+#: flask_security/core.py:209
+msgid "Please confirm your email"
+msgstr "Si us plau, confirmeu el vostre correu electrònic"
+
+#: flask_security/core.py:210
+msgid "Login instructions"
+msgstr "Instruccions d'inici de la sessió"
+
+#: flask_security/core.py:211
+#: flask_security/templates/security/email/reset_notice.html:1
+msgid "Your password has been reset"
+msgstr "S'ha restablit la teva contrasenya"
+
+#: flask_security/core.py:212
+msgid "Your password has been changed"
+msgstr "S'ha canviat la teva contrasenya"
+
+#: flask_security/core.py:213
+msgid "Password reset instructions"
+msgstr "Instruccions de recuperació de la contrasenya"
+
+#: flask_security/core.py:216
+msgid "Two-factor Login"
+msgstr ""
+
+#: flask_security/core.py:217
+msgid "Two-factor Rescue"
+msgstr ""
+
+#: flask_security/core.py:266
+msgid "Verification Code"
+msgstr ""
+
+#: flask_security/core.py:282
+msgid "Input not appropriate for requested API"
+msgstr ""
+
+#: flask_security/core.py:283
+msgid "You do not have permission to view this resource."
+msgstr "No tens permís d'accés per a consultar aquest recurs."
+
+#: flask_security/core.py:285
+msgid "You are not authenticated. Please supply the correct credentials."
+msgstr ""
+
+#: flask_security/core.py:289
+#, fuzzy
+msgid "You must re-authenticate to access this endpoint"
+msgstr "Has d'iniciar una nova sessió per tal d'accedir a aquesta pàgina."
+
+#: flask_security/core.py:293
+#, python-format
+msgid "Thank you. Confirmation instructions have been sent to %(email)s."
+msgstr ""
+"Moltes gràcies. S'ha enviat un correu electrònic a %(email)s amb "
+"instruccions per confirmar el teu compte."
+
+#: flask_security/core.py:296
+msgid "Thank you. Your email has been confirmed."
+msgstr "Moltes gràcies. S'ha confirmat el teu correu electrònic."
+
+#: flask_security/core.py:297
+msgid "Your email has already been confirmed."
+msgstr "El teu correu electrònic ja s'havia confirmat."
+
+#: flask_security/core.py:298
+msgid "Invalid confirmation token."
+msgstr "Token de confirmació no vàlid."
+
+#: flask_security/core.py:300
+#, python-format
+msgid "%(email)s is already associated with an account."
+msgstr "%(email)s ja es associat amb un compte."
+
+#: flask_security/core.py:303
+msgid "Password does not match"
+msgstr "La contrasenya no coincideix"
+
+#: flask_security/core.py:304
+msgid "Passwords do not match"
+msgstr "Les contrasenyes no coincideixen"
+
+#: flask_security/core.py:305
+msgid "Redirections outside the domain are forbidden"
+msgstr "Les redireccions a llocs web externes s'han prohibit"
+
+#: flask_security/core.py:307
+#, python-format
+msgid "Instructions to reset your password have been sent to %(email)s."
+msgstr ""
+"Les instruccions per restablir la teva contrasenya s'han enviat a "
+"%(email)s."
+
+#: flask_security/core.py:311
+#, python-format
+msgid ""
+"You did not reset your password within %(within)s. New instructions have "
+"been sent to %(email)s."
+msgstr ""
+"No vas restablir la teva contrasenya abans de %(within)s. S'han enviat "
+"noves instruccions a %(email)s."
+
+#: flask_security/core.py:317
+msgid "Invalid reset password token."
+msgstr "El token per restablir la contrasenya no és vàlid."
+
+#: flask_security/core.py:318
+msgid "Email requires confirmation."
+msgstr "El correu electrònic requereix d'una confirmació."
+
+#: flask_security/core.py:320
+#, python-format
+msgid "Confirmation instructions have been sent to %(email)s."
+msgstr "Les instruccions de confirmació s'han enviat a %(email)s."
+
+#: flask_security/core.py:324
+#, python-format
+msgid ""
+"You did not confirm your email within %(within)s. New instructions to "
+"confirm your email have been sent to %(email)s."
+msgstr ""
+"No vas confirmar el teu correu electrònic abans de %(within)s. S'han "
+"enviat noves instruccions a %(email)s."
+
+#: flask_security/core.py:332
+#, python-format
+msgid ""
+"You did not login within %(within)s. New instructions to login have been "
+"sent to %(email)s."
+msgstr ""
+"No vas iniciar la sessió abans de %(within)s. S'han enviat noves "
+"instruccions a %(email)s."
+
+#: flask_security/core.py:339
+#, python-format
+msgid "Instructions to login have been sent to %(email)s."
+msgstr "S'han enviat instruccions per l'inici de sessió a %(email)s."
+
+#: flask_security/core.py:342
+msgid "Invalid login token."
+msgstr "Token de d'inici de sessió no vàlid."
+
+#: flask_security/core.py:343
+msgid "Account is disabled."
+msgstr "el compte està desactivat."
+
+#: flask_security/core.py:344
+msgid "Email not provided"
+msgstr "No s'ha inclòs el correu electrònic"
+
+#: flask_security/core.py:345
+msgid "Invalid email address"
+msgstr "Adreça de correu electrònic no vàlida"
+
+#: flask_security/core.py:346
+#, fuzzy
+msgid "Invalid code"
+msgstr "Contrasenya no vàlida"
+
+#: flask_security/core.py:347
+msgid "Password not provided"
+msgstr "No s'ha inclòs la contrasenya"
+
+#: flask_security/core.py:348
+msgid "No password is set for this user"
+msgstr "No hi ha cap contrasenya per a l'usuari"
+
+#: flask_security/core.py:350
+#, fuzzy, python-format
+msgid "Password must be at least %(length)s characters"
+msgstr "La contrasenya ha de tenir al menys 6 caràcters"
+
+#: flask_security/core.py:353
+msgid "Password not complex enough"
+msgstr ""
+
+#: flask_security/core.py:354
+msgid "Password on breached list"
+msgstr ""
+
+#: flask_security/core.py:356
+msgid "Failed to contact breached passwords site"
+msgstr ""
+
+#: flask_security/core.py:359
+msgid "Phone number not valid e.g. missing country code"
+msgstr ""
+
+#: flask_security/core.py:360
+msgid "Specified user does not exist"
+msgstr "L'usuari no existeix"
+
+#: flask_security/core.py:361
+msgid "Invalid password"
+msgstr "Contrasenya no vàlida"
+
+#: flask_security/core.py:362
+msgid "Password or code submitted is not valid"
+msgstr ""
+
+#: flask_security/core.py:363
+msgid "You have successfully logged in."
+msgstr "La sessió s'ha iniciat amb èxit."
+
+#: flask_security/core.py:364
+msgid "Forgot password?"
+msgstr "Has oblidat la teva contrasenya?"
+
+#: flask_security/core.py:366
+msgid ""
+"You successfully reset your password and you have been logged in "
+"automatically."
+msgstr ""
+"Has restablert la teva contrasenya amb èxit i s'ha iniciat la sessió "
+"automàticament."
+
+#: flask_security/core.py:373
+msgid "Your new password must be different than your previous password."
+msgstr "La nova contrasenya ha de ser diferent de l'anterior."
+
+#: flask_security/core.py:376
+msgid "You successfully changed your password."
+msgstr "La teva contrasenya s'ha modificat amb èxit."
+
+#: flask_security/core.py:377
+msgid "Please log in to access this page."
+msgstr "Has d'iniciar sessió per tal d'accedir a aquesta pàgina."
+
+#: flask_security/core.py:378
+msgid "Please reauthenticate to access this page."
+msgstr "Has d'iniciar una nova sessió per tal d'accedir a aquesta pàgina."
+
+#: flask_security/core.py:379
+msgid "Reauthentication successful"
+msgstr ""
+
+#: flask_security/core.py:381
+msgid "You can only access this endpoint when not logged in."
+msgstr ""
+
+#: flask_security/core.py:384
+msgid "Failed to send code. Please try again later"
+msgstr ""
+
+#: flask_security/core.py:385
+msgid "Invalid Token"
+msgstr ""
+
+#: flask_security/core.py:386
+msgid "Your token has been confirmed"
+msgstr ""
+
+#: flask_security/core.py:388
+msgid "You successfully changed your two-factor method."
+msgstr ""
+
+#: flask_security/core.py:392
+msgid "You successfully confirmed password"
+msgstr ""
+
+#: flask_security/core.py:396
+msgid "Password confirmation is needed in order to access page"
+msgstr ""
+
+#: flask_security/core.py:400
+msgid "You currently do not have permissions to access this page"
+msgstr ""
+
+#: flask_security/core.py:403
+msgid "Marked method is not valid"
+msgstr ""
+
+#: flask_security/core.py:405
+msgid "You successfully disabled two factor authorization."
+msgstr ""
+
+#: flask_security/core.py:408
+msgid "Requested method is not valid"
+msgstr ""
+
+#: flask_security/core.py:410
+#, python-format
+msgid "Setup must be completed within %(within)s. Please start over."
+msgstr ""
+
+#: flask_security/core.py:413
+msgid "Unified sign in setup successful"
+msgstr ""
+
+#: flask_security/core.py:414
+msgid "You must specify a valid identity to sign in"
+msgstr ""
+
+#: flask_security/core.py:415
+#, python-format
+msgid "Use this code to sign in: %(code)s."
+msgstr ""
+
+#: flask_security/forms.py:50
+msgid "Email Address"
+msgstr "Correu electrònic"
+
+#: flask_security/forms.py:51
+msgid "Password"
+msgstr "Contrasenya"
+
+#: flask_security/forms.py:52
+msgid "Remember Me"
+msgstr "Recorda'm"
+
+#: flask_security/forms.py:53 flask_security/templates/security/_menu.html:5
+#: flask_security/templates/security/login_user.html:6
+#: flask_security/templates/security/send_login.html:6
+msgid "Login"
+msgstr "Iniciar sessió"
+
+#: flask_security/forms.py:54
+#: flask_security/templates/security/email/us_instructions.html:8
+#: flask_security/templates/security/us_signin.html:6
+msgid "Sign In"
+msgstr ""
+
+#: flask_security/forms.py:55 flask_security/templates/security/_menu.html:11
+#: flask_security/templates/security/register_user.html:6
+msgid "Register"
+msgstr "Registrar-se"
+
+#: flask_security/forms.py:56
+msgid "Resend Confirmation Instructions"
+msgstr "Reenviar les instruccions de confirmació"
+
+#: flask_security/forms.py:57
+msgid "Recover Password"
+msgstr "Restablir la contrasenya"
+
+#: flask_security/forms.py:58
+msgid "Reset Password"
+msgstr "Restablir la contrasenya"
+
+#: flask_security/forms.py:59
+msgid "Retype Password"
+msgstr "Escriu la contrasenya una altra vegada"
+
+#: flask_security/forms.py:60
+msgid "New Password"
+msgstr "Nova contrasenya"
+
+#: flask_security/forms.py:61
+msgid "Change Password"
+msgstr "Canvi de contrasenya"
+
+#: flask_security/forms.py:62
+msgid "Send Login Link"
+msgstr "Enviar l'enllaç d'inici de sessió"
+
+#: flask_security/forms.py:63
+msgid "Verify Password"
+msgstr ""
+
+#: flask_security/forms.py:64
+msgid "Change Method"
+msgstr ""
+
+#: flask_security/forms.py:65
+msgid "Phone Number"
+msgstr ""
+
+#: flask_security/forms.py:66
+msgid "Authentication Code"
+msgstr ""
+
+#: flask_security/forms.py:67
+msgid "Submit"
+msgstr ""
+
+#: flask_security/forms.py:68
+msgid "Submit Code"
+msgstr ""
+
+#: flask_security/forms.py:69
+msgid "Error(s)"
+msgstr ""
+
+#: flask_security/forms.py:70
+msgid "Identity"
+msgstr ""
+
+#: flask_security/forms.py:71
+msgid "Send Code"
+msgstr ""
+
+#: flask_security/forms.py:72
+#, fuzzy
+msgid "Passcode"
+msgstr "Contrasenya"
+
+#: flask_security/unified_signin.py:145
+#, fuzzy
+msgid "Code or Password"
+msgstr "Restablir la contrasenya"
+
+#: flask_security/unified_signin.py:150 flask_security/unified_signin.py:270
+msgid "Available Methods"
+msgstr ""
+
+#: flask_security/unified_signin.py:151
+msgid "Via email"
+msgstr ""
+
+#: flask_security/unified_signin.py:151
+msgid "Via SMS"
+msgstr ""
+
+#: flask_security/unified_signin.py:272
+msgid "Set up using email"
+msgstr ""
+
+#: flask_security/unified_signin.py:275
+msgid "Set up using an authenticator app (e.g. google, lastpass, authy)"
+msgstr ""
+
+#: flask_security/unified_signin.py:277
+msgid "Set up using SMS"
+msgstr ""
+
+#: flask_security/templates/security/_menu.html:2
+msgid "Menu"
+msgstr "Menú"
+
+#: flask_security/templates/security/_menu.html:8
+msgid "Unified Sign In"
+msgstr ""
+
+#: flask_security/templates/security/_menu.html:14
+msgid "Forgot password"
+msgstr "Contrasenya oblidada"
+
+#: flask_security/templates/security/_menu.html:17
+msgid "Confirm account"
+msgstr "Confirmació de compte"
+
+#: flask_security/templates/security/change_password.html:6
+msgid "Change password"
+msgstr "Canviar la contrasenya"
+
+#: flask_security/templates/security/forgot_password.html:6
+msgid "Send password reset instructions"
+msgstr "Enviar instruccions per restablir la contrasenya"
+
+#: flask_security/templates/security/reset_password.html:6
+msgid "Reset password"
+msgstr "Restablir la contrasenya"
+
+#: flask_security/templates/security/send_confirmation.html:6
+msgid "Resend confirmation instructions"
+msgstr "Reenviar instruccions de confirmació"
+
+#: flask_security/templates/security/two_factor_setup.html:6
+msgid "Two-factor authentication adds an extra layer of security to your account"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_setup.html:7
+msgid ""
+"In addition to your username and password, you'll need to use a code that"
+" we will send you"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_setup.html:18
+msgid "To complete logging in, please enter the code sent to your mail"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_setup.html:21
+#: flask_security/templates/security/us_setup.html:21
+msgid ""
+"Open your authenticator app on your device and scan the following qrcode "
+"to start receiving codes:"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_setup.html:22
+msgid "Two factor authentication code"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_setup.html:25
+msgid "To Which Phone Number Should We Send Code To?"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_verify_code.html:6
+msgid "Two-factor Authentication"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_verify_code.html:7
+msgid "Please enter your authentication code"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_verify_code.html:18
+msgid "The code for authentication was sent to your email address"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_verify_code.html:21
+msgid "A mail was sent to us in order to reset your application account"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_verify_password.html:6
+#: flask_security/templates/security/verify.html:6
+msgid "Please Enter Your Password"
+msgstr ""
+
+#: flask_security/templates/security/us_setup.html:6
+msgid "Setup Unified Sign In options"
+msgstr ""
+
+#: flask_security/templates/security/us_setup.html:22
+msgid "Passwordless QRCode"
+msgstr ""
+
+#: flask_security/templates/security/us_setup.html:25
+#: flask_security/templates/security/us_signin.html:23
+#: flask_security/templates/security/us_verify.html:21
+#, fuzzy
+msgid "Code has been sent"
+msgstr "S'ha restablit la teva contrasenya"
+
+#: flask_security/templates/security/us_setup.html:29
+msgid "No methods have been enabled - nothing to setup"
+msgstr ""
+
+#: flask_security/templates/security/us_signin.html:15
+#: flask_security/templates/security/us_verify.html:13
+msgid "Request one-time code be sent"
+msgstr ""
+
+#: flask_security/templates/security/us_verify.html:6
+#, fuzzy
+msgid "Please re-authenticate"
+msgstr "Has d'iniciar una nova sessió per tal d'accedir a aquesta pàgina."
+
+#: flask_security/templates/security/email/change_notice.html:1
+msgid "Your password has been changed."
+msgstr "S'ha canviat la teva contrasenya."
+
+#: flask_security/templates/security/email/change_notice.html:3
+msgid "If you did not change your password,"
+msgstr "Si no has canviat la teva contrasenya,"
+
+#: flask_security/templates/security/email/change_notice.html:3
+msgid "click here to reset it"
+msgstr "fes clic aquí per a restablir-la"
+
+#: flask_security/templates/security/email/confirmation_instructions.html:1
+msgid "Please confirm your email through the link below:"
+msgstr "Confirma el teu correu electrònic fent clic aquí:"
+
+#: flask_security/templates/security/email/confirmation_instructions.html:3
+#: flask_security/templates/security/email/welcome.html:6
+msgid "Confirm my account"
+msgstr "Confirmeu el compte"
+
+#: flask_security/templates/security/email/login_instructions.html:1
+#: flask_security/templates/security/email/welcome.html:1
+#, python-format
+msgid "Welcome %(email)s!"
+msgstr "Benvingut %(email)s!"
+
+#: flask_security/templates/security/email/login_instructions.html:3
+msgid "You can log into your account through the link below:"
+msgstr "Inicia la sessió fent clic aquí:"
+
+#: flask_security/templates/security/email/login_instructions.html:5
+msgid "Login now"
+msgstr "Iniciar sessió ara"
+
+#: flask_security/templates/security/email/reset_instructions.html:1
+msgid "Click here to reset your password"
+msgstr "Feu clic aquí per restablir la contrasenya"
+
+#: flask_security/templates/security/email/two_factor_instructions.html:3
+msgid "You can log into your account using the following code:"
+msgstr ""
+
+#: flask_security/templates/security/email/two_factor_rescue.html:1
+msgid "can not access mail account"
+msgstr ""
+
+#: flask_security/templates/security/email/us_instructions.html:3
+#, fuzzy
+msgid "You can sign into your account using the following code:"
+msgstr "Inicia la sessió fent clic aquí:"
+
+#: flask_security/templates/security/email/us_instructions.html:6
+#, fuzzy
+msgid "Or use the the link below:"
+msgstr "Confirmeu el vostre correu electrònic fent clic a continuació:"
+
+#: flask_security/templates/security/email/welcome.html:4
+msgid "You can confirm your email through the link below:"
+msgstr "Confirmeu el vostre correu electrònic fent clic a continuació:"
+
diff --git a/flask_security/translations/da_DK/LC_MESSAGES/flask_security.mo b/flask_security/translations/da_DK/LC_MESSAGES/flask_security.mo
new file mode 100644
index 0000000..d80dc41
--- /dev/null
+++ b/flask_security/translations/da_DK/LC_MESSAGES/flask_security.mo
Binary files differ
diff --git a/flask_security/translations/da_DK/LC_MESSAGES/flask_security.po b/flask_security/translations/da_DK/LC_MESSAGES/flask_security.po
new file mode 100644
index 0000000..ba28500
--- /dev/null
+++ b/flask_security/translations/da_DK/LC_MESSAGES/flask_security.po
@@ -0,0 +1,626 @@
+# Danish (Denmark) translations for Flask-Security.
+# Copyright (C) 2017 ORGANIZATION
+# This file is distributed under the same license as the Flask-Security
+# project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2017.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Flask-Security 2.1.0\n"
+"Report-Msgid-Bugs-To: info@inveniosoftware.org\n"
+"POT-Creation-Date: 2020-04-19 13:18-0700\n"
+"PO-Revision-Date: 2017-03-23 14:04+0100\n"
+"Last-Translator: Leonhard Printz <leonhardprintz@protonmail.ch>\n"
+"Language: da_DK\n"
+"Language-Team: da_DK <LL@li.org>\n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 2.8.0\n"
+
+#: flask_security/core.py:207
+msgid "Login Required"
+msgstr "Login påkræveet"
+
+#: flask_security/core.py:208
+#: flask_security/templates/security/email/two_factor_instructions.html:1
+#: flask_security/templates/security/email/us_instructions.html:1
+msgid "Welcome"
+msgstr "Velkommen"
+
+#: flask_security/core.py:209
+msgid "Please confirm your email"
+msgstr "Bekræft venligst din email"
+
+#: flask_security/core.py:210
+msgid "Login instructions"
+msgstr "Logininstruktioner"
+
+#: flask_security/core.py:211
+#: flask_security/templates/security/email/reset_notice.html:1
+msgid "Your password has been reset"
+msgstr "Din adgangskode er blevet nulstillet"
+
+#: flask_security/core.py:212
+msgid "Your password has been changed"
+msgstr "Din adgangskode er blevet ændret"
+
+#: flask_security/core.py:213
+msgid "Password reset instructions"
+msgstr "Instruktioner til nulstilling af adganskode"
+
+#: flask_security/core.py:216
+msgid "Two-factor Login"
+msgstr ""
+
+#: flask_security/core.py:217
+msgid "Two-factor Rescue"
+msgstr ""
+
+#: flask_security/core.py:266
+msgid "Verification Code"
+msgstr ""
+
+#: flask_security/core.py:282
+msgid "Input not appropriate for requested API"
+msgstr ""
+
+#: flask_security/core.py:283
+msgid "You do not have permission to view this resource."
+msgstr "Du har ikke adgang til denne resource."
+
+#: flask_security/core.py:285
+msgid "You are not authenticated. Please supply the correct credentials."
+msgstr ""
+
+#: flask_security/core.py:289
+#, fuzzy
+msgid "You must re-authenticate to access this endpoint"
+msgstr "Bekræft identitet for at få adgang til denne side."
+
+#: flask_security/core.py:293
+#, python-format
+msgid "Thank you. Confirmation instructions have been sent to %(email)s."
+msgstr "Mange tak. Bekræftelsesinstruktioner er blevet sendt til %(email)s."
+
+#: flask_security/core.py:296
+msgid "Thank you. Your email has been confirmed."
+msgstr "Mange Tak. Din email er blevet bekræftet."
+
+#: flask_security/core.py:297
+msgid "Your email has already been confirmed."
+msgstr "Din email er allerede blevet bekræftet."
+
+#: flask_security/core.py:298
+msgid "Invalid confirmation token."
+msgstr "Ugyldig bekræftigelsestoken."
+
+#: flask_security/core.py:300
+#, python-format
+msgid "%(email)s is already associated with an account."
+msgstr "%(email)s er allerede brugt af en anden konto."
+
+#: flask_security/core.py:303
+msgid "Password does not match"
+msgstr "Adgangskode passer ikke"
+
+#: flask_security/core.py:304
+msgid "Passwords do not match"
+msgstr "Adgangskoderne passer ikke"
+
+#: flask_security/core.py:305
+msgid "Redirections outside the domain are forbidden"
+msgstr "Omdirigering udenfor domænet er forbudt"
+
+#: flask_security/core.py:307
+#, python-format
+msgid "Instructions to reset your password have been sent to %(email)s."
+msgstr ""
+"Instruktioner til nulstilling af din adgangskode er blevet sendt til "
+"%(email)s."
+
+#: flask_security/core.py:311
+#, python-format
+msgid ""
+"You did not reset your password within %(within)s. New instructions have "
+"been sent to %(email)s."
+msgstr ""
+"Du har ikke nulstillet din adgangskode indenfor %(within)s. Nye "
+"instruktioner er sendt til %(email)s."
+
+#: flask_security/core.py:317
+msgid "Invalid reset password token."
+msgstr "Ugyldig nulstillingstoken."
+
+#: flask_security/core.py:318
+msgid "Email requires confirmation."
+msgstr "Email kræver bekræftigelse."
+
+#: flask_security/core.py:320
+#, python-format
+msgid "Confirmation instructions have been sent to %(email)s."
+msgstr "Bekræftigelsesinstruktioner er blevet sendt til %(email)s."
+
+#: flask_security/core.py:324
+#, python-format
+msgid ""
+"You did not confirm your email within %(within)s. New instructions to "
+"confirm your email have been sent to %(email)s."
+msgstr ""
+"Du har ikke bekræftet din email indenfor %(within)s. Nye instruktioner er"
+" blevet sendt til %(email)s."
+
+#: flask_security/core.py:332
+#, python-format
+msgid ""
+"You did not login within %(within)s. New instructions to login have been "
+"sent to %(email)s."
+msgstr ""
+"Du har ikke logget in indenfor %(within)s. Nye logininstruktioner er "
+"blevet sendt til %(email)s."
+
+#: flask_security/core.py:339
+#, python-format
+msgid "Instructions to login have been sent to %(email)s."
+msgstr "Logininstruktioner er blevet sendt til %(email)s."
+
+#: flask_security/core.py:342
+msgid "Invalid login token."
+msgstr "Ugyldig logintoken."
+
+#: flask_security/core.py:343
+msgid "Account is disabled."
+msgstr "Kontoen er deaktiveret."
+
+#: flask_security/core.py:344
+msgid "Email not provided"
+msgstr "Email ikke angivet"
+
+#: flask_security/core.py:345
+msgid "Invalid email address"
+msgstr "Ugyldig email adresse"
+
+#: flask_security/core.py:346
+#, fuzzy
+msgid "Invalid code"
+msgstr "Ugyldig adgangskode"
+
+#: flask_security/core.py:347
+msgid "Password not provided"
+msgstr "Adgangskode ikke angivet"
+
+#: flask_security/core.py:348
+msgid "No password is set for this user"
+msgstr "Denne bruger har ingen adganskode"
+
+#: flask_security/core.py:350
+#, fuzzy, python-format
+msgid "Password must be at least %(length)s characters"
+msgstr "Adgangskoden skal indeholde mindst 6 tegn"
+
+#: flask_security/core.py:353
+msgid "Password not complex enough"
+msgstr ""
+
+#: flask_security/core.py:354
+msgid "Password on breached list"
+msgstr ""
+
+#: flask_security/core.py:356
+msgid "Failed to contact breached passwords site"
+msgstr ""
+
+#: flask_security/core.py:359
+msgid "Phone number not valid e.g. missing country code"
+msgstr ""
+
+#: flask_security/core.py:360
+msgid "Specified user does not exist"
+msgstr "Denne bruger findes ikke"
+
+#: flask_security/core.py:361
+msgid "Invalid password"
+msgstr "Ugyldig adgangskode"
+
+#: flask_security/core.py:362
+msgid "Password or code submitted is not valid"
+msgstr ""
+
+#: flask_security/core.py:363
+msgid "You have successfully logged in."
+msgstr "Du er hermed blevet logget ind."
+
+#: flask_security/core.py:364
+msgid "Forgot password?"
+msgstr "Glemt adgangskode?"
+
+#: flask_security/core.py:366
+msgid ""
+"You successfully reset your password and you have been logged in "
+"automatically."
+msgstr ""
+"Du har hermed nulstillet din adgangskode og er blevet automatisk logget "
+"ind."
+
+#: flask_security/core.py:373
+msgid "Your new password must be different than your previous password."
+msgstr "Din nye adgangskode skal være anderledes end din tidligere adgangskode."
+
+#: flask_security/core.py:376
+msgid "You successfully changed your password."
+msgstr "Du har hermed ændret din adgangskode."
+
+#: flask_security/core.py:377
+msgid "Please log in to access this page."
+msgstr "Log in for at få adgang til denne side."
+
+#: flask_security/core.py:378
+msgid "Please reauthenticate to access this page."
+msgstr "Bekræft identitet for at få adgang til denne side."
+
+#: flask_security/core.py:379
+msgid "Reauthentication successful"
+msgstr ""
+
+#: flask_security/core.py:381
+msgid "You can only access this endpoint when not logged in."
+msgstr ""
+
+#: flask_security/core.py:384
+msgid "Failed to send code. Please try again later"
+msgstr ""
+
+#: flask_security/core.py:385
+msgid "Invalid Token"
+msgstr ""
+
+#: flask_security/core.py:386
+msgid "Your token has been confirmed"
+msgstr ""
+
+#: flask_security/core.py:388
+msgid "You successfully changed your two-factor method."
+msgstr ""
+
+#: flask_security/core.py:392
+msgid "You successfully confirmed password"
+msgstr ""
+
+#: flask_security/core.py:396
+msgid "Password confirmation is needed in order to access page"
+msgstr ""
+
+#: flask_security/core.py:400
+msgid "You currently do not have permissions to access this page"
+msgstr ""
+
+#: flask_security/core.py:403
+msgid "Marked method is not valid"
+msgstr ""
+
+#: flask_security/core.py:405
+msgid "You successfully disabled two factor authorization."
+msgstr ""
+
+#: flask_security/core.py:408
+msgid "Requested method is not valid"
+msgstr ""
+
+#: flask_security/core.py:410
+#, python-format
+msgid "Setup must be completed within %(within)s. Please start over."
+msgstr ""
+
+#: flask_security/core.py:413
+msgid "Unified sign in setup successful"
+msgstr ""
+
+#: flask_security/core.py:414
+msgid "You must specify a valid identity to sign in"
+msgstr ""
+
+#: flask_security/core.py:415
+#, python-format
+msgid "Use this code to sign in: %(code)s."
+msgstr ""
+
+#: flask_security/forms.py:50
+msgid "Email Address"
+msgstr "Email adresse"
+
+#: flask_security/forms.py:51
+msgid "Password"
+msgstr "Adgangskode"
+
+#: flask_security/forms.py:52
+msgid "Remember Me"
+msgstr "Husk"
+
+#: flask_security/forms.py:53 flask_security/templates/security/_menu.html:5
+#: flask_security/templates/security/login_user.html:6
+#: flask_security/templates/security/send_login.html:6
+msgid "Login"
+msgstr "Login"
+
+#: flask_security/forms.py:54
+#: flask_security/templates/security/email/us_instructions.html:8
+#: flask_security/templates/security/us_signin.html:6
+msgid "Sign In"
+msgstr ""
+
+#: flask_security/forms.py:55 flask_security/templates/security/_menu.html:11
+#: flask_security/templates/security/register_user.html:6
+msgid "Register"
+msgstr "Registrer"
+
+#: flask_security/forms.py:56
+msgid "Resend Confirmation Instructions"
+msgstr "Gensend bekræftelsesinstruktioner"
+
+#: flask_security/forms.py:57
+msgid "Recover Password"
+msgstr "Genopret adgangskode"
+
+#: flask_security/forms.py:58
+msgid "Reset Password"
+msgstr "Nulstil adgangskode"
+
+#: flask_security/forms.py:59
+msgid "Retype Password"
+msgstr "Gentast adgangskode"
+
+#: flask_security/forms.py:60
+msgid "New Password"
+msgstr "Ny adgangskode"
+
+#: flask_security/forms.py:61
+msgid "Change Password"
+msgstr "Ændre adgangskode"
+
+#: flask_security/forms.py:62
+msgid "Send Login Link"
+msgstr "Send login link"
+
+#: flask_security/forms.py:63
+msgid "Verify Password"
+msgstr ""
+
+#: flask_security/forms.py:64
+msgid "Change Method"
+msgstr ""
+
+#: flask_security/forms.py:65
+msgid "Phone Number"
+msgstr ""
+
+#: flask_security/forms.py:66
+msgid "Authentication Code"
+msgstr ""
+
+#: flask_security/forms.py:67
+msgid "Submit"
+msgstr ""
+
+#: flask_security/forms.py:68
+msgid "Submit Code"
+msgstr ""
+
+#: flask_security/forms.py:69
+msgid "Error(s)"
+msgstr ""
+
+#: flask_security/forms.py:70
+msgid "Identity"
+msgstr ""
+
+#: flask_security/forms.py:71
+msgid "Send Code"
+msgstr ""
+
+#: flask_security/forms.py:72
+#, fuzzy
+msgid "Passcode"
+msgstr "Adgangskode"
+
+#: flask_security/unified_signin.py:145
+#, fuzzy
+msgid "Code or Password"
+msgstr "Genopret adgangskode"
+
+#: flask_security/unified_signin.py:150 flask_security/unified_signin.py:270
+msgid "Available Methods"
+msgstr ""
+
+#: flask_security/unified_signin.py:151
+msgid "Via email"
+msgstr ""
+
+#: flask_security/unified_signin.py:151
+msgid "Via SMS"
+msgstr ""
+
+#: flask_security/unified_signin.py:272
+msgid "Set up using email"
+msgstr ""
+
+#: flask_security/unified_signin.py:275
+msgid "Set up using an authenticator app (e.g. google, lastpass, authy)"
+msgstr ""
+
+#: flask_security/unified_signin.py:277
+msgid "Set up using SMS"
+msgstr ""
+
+#: flask_security/templates/security/_menu.html:2
+msgid "Menu"
+msgstr "Menu"
+
+#: flask_security/templates/security/_menu.html:8
+msgid "Unified Sign In"
+msgstr ""
+
+#: flask_security/templates/security/_menu.html:14
+msgid "Forgot password"
+msgstr "Glemt din adgangskode"
+
+#: flask_security/templates/security/_menu.html:17
+msgid "Confirm account"
+msgstr "Bekræft konto"
+
+#: flask_security/templates/security/change_password.html:6
+msgid "Change password"
+msgstr "Ændre adgangskode"
+
+#: flask_security/templates/security/forgot_password.html:6
+msgid "Send password reset instructions"
+msgstr "Send adgangskode nulstillingsinstruktioner"
+
+#: flask_security/templates/security/reset_password.html:6
+msgid "Reset password"
+msgstr "Nulstil adgangskode"
+
+#: flask_security/templates/security/send_confirmation.html:6
+msgid "Resend confirmation instructions"
+msgstr "Gensend bekræftelsesinstruktioner"
+
+#: flask_security/templates/security/two_factor_setup.html:6
+msgid "Two-factor authentication adds an extra layer of security to your account"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_setup.html:7
+msgid ""
+"In addition to your username and password, you'll need to use a code that"
+" we will send you"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_setup.html:18
+msgid "To complete logging in, please enter the code sent to your mail"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_setup.html:21
+#: flask_security/templates/security/us_setup.html:21
+msgid ""
+"Open your authenticator app on your device and scan the following qrcode "
+"to start receiving codes:"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_setup.html:22
+msgid "Two factor authentication code"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_setup.html:25
+msgid "To Which Phone Number Should We Send Code To?"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_verify_code.html:6
+msgid "Two-factor Authentication"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_verify_code.html:7
+msgid "Please enter your authentication code"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_verify_code.html:18
+msgid "The code for authentication was sent to your email address"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_verify_code.html:21
+msgid "A mail was sent to us in order to reset your application account"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_verify_password.html:6
+#: flask_security/templates/security/verify.html:6
+msgid "Please Enter Your Password"
+msgstr ""
+
+#: flask_security/templates/security/us_setup.html:6
+msgid "Setup Unified Sign In options"
+msgstr ""
+
+#: flask_security/templates/security/us_setup.html:22
+msgid "Passwordless QRCode"
+msgstr ""
+
+#: flask_security/templates/security/us_setup.html:25
+#: flask_security/templates/security/us_signin.html:23
+#: flask_security/templates/security/us_verify.html:21
+#, fuzzy
+msgid "Code has been sent"
+msgstr "Din adgangskode er blevet nulstillet"
+
+#: flask_security/templates/security/us_setup.html:29
+msgid "No methods have been enabled - nothing to setup"
+msgstr ""
+
+#: flask_security/templates/security/us_signin.html:15
+#: flask_security/templates/security/us_verify.html:13
+msgid "Request one-time code be sent"
+msgstr ""
+
+#: flask_security/templates/security/us_verify.html:6
+#, fuzzy
+msgid "Please re-authenticate"
+msgstr "Bekræft identitet for at få adgang til denne side."
+
+#: flask_security/templates/security/email/change_notice.html:1
+msgid "Your password has been changed."
+msgstr "Din adgangskode er blevet ændret."
+
+#: flask_security/templates/security/email/change_notice.html:3
+msgid "If you did not change your password,"
+msgstr "Hvis du ikke har ændret din adgangskode,"
+
+#: flask_security/templates/security/email/change_notice.html:3
+msgid "click here to reset it"
+msgstr "klik her for at ændre den"
+
+#: flask_security/templates/security/email/confirmation_instructions.html:1
+msgid "Please confirm your email through the link below:"
+msgstr "Bekræft venligst din email gennem nedenstående link:"
+
+#: flask_security/templates/security/email/confirmation_instructions.html:3
+#: flask_security/templates/security/email/welcome.html:6
+msgid "Confirm my account"
+msgstr "Bekræft ny konto"
+
+#: flask_security/templates/security/email/login_instructions.html:1
+#: flask_security/templates/security/email/welcome.html:1
+#, python-format
+msgid "Welcome %(email)s!"
+msgstr "Velkommen %(email)s!"
+
+#: flask_security/templates/security/email/login_instructions.html:3
+msgid "You can log into your account through the link below:"
+msgstr "Du kan logge ind gennem nedenstående link:"
+
+#: flask_security/templates/security/email/login_instructions.html:5
+msgid "Login now"
+msgstr "Login"
+
+#: flask_security/templates/security/email/reset_instructions.html:1
+msgid "Click here to reset your password"
+msgstr "Klik her for at nulstille din adgangskode"
+
+#: flask_security/templates/security/email/two_factor_instructions.html:3
+msgid "You can log into your account using the following code:"
+msgstr ""
+
+#: flask_security/templates/security/email/two_factor_rescue.html:1
+msgid "can not access mail account"
+msgstr ""
+
+#: flask_security/templates/security/email/us_instructions.html:3
+#, fuzzy
+msgid "You can sign into your account using the following code:"
+msgstr "Du kan logge ind gennem nedenstående link:"
+
+#: flask_security/templates/security/email/us_instructions.html:6
+#, fuzzy
+msgid "Or use the the link below:"
+msgstr "Bekræft venligst din email gennem nedenstående link:"
+
+#: flask_security/templates/security/email/welcome.html:4
+msgid "You can confirm your email through the link below:"
+msgstr "Bekræft venligst din email gennem nedenstående link:"
+
diff --git a/flask_security/translations/de_DE/LC_MESSAGES/flask_security.mo b/flask_security/translations/de_DE/LC_MESSAGES/flask_security.mo
new file mode 100644
index 0000000..29d4297
--- /dev/null
+++ b/flask_security/translations/de_DE/LC_MESSAGES/flask_security.mo
Binary files differ
diff --git a/flask_security/translations/de_DE/LC_MESSAGES/flask_security.po b/flask_security/translations/de_DE/LC_MESSAGES/flask_security.po
new file mode 100644
index 0000000..8bba92d
--- /dev/null
+++ b/flask_security/translations/de_DE/LC_MESSAGES/flask_security.po
@@ -0,0 +1,628 @@
+# German translation for Flask-Security (Du/Sie distinction has been
+# avoided)
+# Copyright (C) 2017 ORGANIZATION
+# This file is distributed under the same license as the Flask-Security
+# project.
+# Ingo Kleiber <ingo@kleiber.me>, 2017,
+# Erich Seifert <dev@erichseifert.de>, 2017.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Flask-Security 2.0.1\n"
+"Report-Msgid-Bugs-To: info@inveniosoftware.org\n"
+"POT-Creation-Date: 2020-04-19 13:18-0700\n"
+"PO-Revision-Date: 2017-09-25 09:14+0200\n"
+"Last-Translator: Erich Seifert <dev@erichseifert.de>\n"
+"Language: de_DE\n"
+"Language-Team: de_DE <LL@li.org>\n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 2.8.0\n"
+
+#: flask_security/core.py:207
+msgid "Login Required"
+msgstr "Anmeldung erforderlich"
+
+#: flask_security/core.py:208
+#: flask_security/templates/security/email/two_factor_instructions.html:1
+#: flask_security/templates/security/email/us_instructions.html:1
+msgid "Welcome"
+msgstr "Willkommen"
+
+#: flask_security/core.py:209
+msgid "Please confirm your email"
+msgstr "Bitte E-Mail-Adresse bestätigen"
+
+#: flask_security/core.py:210
+msgid "Login instructions"
+msgstr "Anmeldeanleitung"
+
+#: flask_security/core.py:211
+#: flask_security/templates/security/email/reset_notice.html:1
+msgid "Your password has been reset"
+msgstr "Das Passwort wurde zurückgesetzt"
+
+#: flask_security/core.py:212
+msgid "Your password has been changed"
+msgstr "Das Passwort wurde geändert"
+
+#: flask_security/core.py:213
+msgid "Password reset instructions"
+msgstr "Anleitung zur Passwortwiederherstellung"
+
+#: flask_security/core.py:216
+msgid "Two-factor Login"
+msgstr ""
+
+#: flask_security/core.py:217
+msgid "Two-factor Rescue"
+msgstr ""
+
+#: flask_security/core.py:266
+msgid "Verification Code"
+msgstr ""
+
+#: flask_security/core.py:282
+msgid "Input not appropriate for requested API"
+msgstr ""
+
+#: flask_security/core.py:283
+msgid "You do not have permission to view this resource."
+msgstr "Keine Berechtigung um diese Ressource zu sehen."
+
+#: flask_security/core.py:285
+msgid "You are not authenticated. Please supply the correct credentials."
+msgstr ""
+
+#: flask_security/core.py:289
+#, fuzzy
+msgid "You must re-authenticate to access this endpoint"
+msgstr "Bitte neu authentifizieren, um auf diese Seite zuzugreifen."
+
+#: flask_security/core.py:293
+#, python-format
+msgid "Thank you. Confirmation instructions have been sent to %(email)s."
+msgstr "Vielen Dank. Bestätigungsanleitung wurde an %(email)s gesendet."
+
+#: flask_security/core.py:296
+msgid "Thank you. Your email has been confirmed."
+msgstr "Vielen Dank. Die E-Mail-Adresse wurde bestätigt."
+
+#: flask_security/core.py:297
+msgid "Your email has already been confirmed."
+msgstr "Die E-Mail-Adresse wurde bereits bestätigt."
+
+#: flask_security/core.py:298
+msgid "Invalid confirmation token."
+msgstr "Ungültiger Bestätigungscode."
+
+#: flask_security/core.py:300
+#, python-format
+msgid "%(email)s is already associated with an account."
+msgstr "%(email)s ist bereits mit einem Konto verknüpft."
+
+#: flask_security/core.py:303
+msgid "Password does not match"
+msgstr "Das Passwort stimmt nicht überein"
+
+#: flask_security/core.py:304
+msgid "Passwords do not match"
+msgstr "Die Passwörter stimmen nicht überein"
+
+#: flask_security/core.py:305
+msgid "Redirections outside the domain are forbidden"
+msgstr "Weiterleitungen außerhalb der Domain sind verboten"
+
+#: flask_security/core.py:307
+#, python-format
+msgid "Instructions to reset your password have been sent to %(email)s."
+msgstr ""
+"Eine Anleitung, um das Passwort wiederherzustellen wurde an %(email)s "
+"gesendet."
+
+#: flask_security/core.py:311
+#, python-format
+msgid ""
+"You did not reset your password within %(within)s. New instructions have "
+"been sent to %(email)s."
+msgstr ""
+"Das Passwort wurde nicht innerhalb von %(within)s zurückgesetzt. Eine "
+"neue Anleitung wurde an %(email)s gesendet."
+
+#: flask_security/core.py:317
+msgid "Invalid reset password token."
+msgstr "Ungültiger Passwortwiederherstellungscode."
+
+#: flask_security/core.py:318
+msgid "Email requires confirmation."
+msgstr "Die E-Mail-Adresse muss bestätigt werden."
+
+#: flask_security/core.py:320
+#, python-format
+msgid "Confirmation instructions have been sent to %(email)s."
+msgstr "Bestätigungsanleitung wurde an %(email)s gesendet."
+
+#: flask_security/core.py:324
+#, python-format
+msgid ""
+"You did not confirm your email within %(within)s. New instructions to "
+"confirm your email have been sent to %(email)s."
+msgstr ""
+"Die E-Mail-Adresse wurden nicht innerhalb von %(within)s bestätigt. Neue "
+"Instruktionen wurden an %(email)s gesendet."
+
+#: flask_security/core.py:332
+#, python-format
+msgid ""
+"You did not login within %(within)s. New instructions to login have been "
+"sent to %(email)s."
+msgstr ""
+"Die Anmeldung erfolgte nicht in %(within)s. Eine neue Anleitung wurde an "
+"%(email)s gesendet."
+
+#: flask_security/core.py:339
+#, python-format
+msgid "Instructions to login have been sent to %(email)s."
+msgstr "Eine Anleitung zur Anmeldung wurde an %(email)s gesendet."
+
+#: flask_security/core.py:342
+msgid "Invalid login token."
+msgstr "Ungültiger Anmeldecode."
+
+#: flask_security/core.py:343
+msgid "Account is disabled."
+msgstr "Konto ist deaktiviert."
+
+#: flask_security/core.py:344
+msgid "Email not provided"
+msgstr "Keine E-Mail-Adresse angegeben"
+
+#: flask_security/core.py:345
+msgid "Invalid email address"
+msgstr "Ungültige E-Mail-Adresse"
+
+#: flask_security/core.py:346
+#, fuzzy
+msgid "Invalid code"
+msgstr "Ungültiges Passwort"
+
+#: flask_security/core.py:347
+msgid "Password not provided"
+msgstr "Kein Passwort angegeben"
+
+#: flask_security/core.py:348
+msgid "No password is set for this user"
+msgstr "Für diesen Benutzer ist kein Passwort gesetzt"
+
+#: flask_security/core.py:350
+#, fuzzy, python-format
+msgid "Password must be at least %(length)s characters"
+msgstr "Das Passwort muss mindestens 6 Zeichen lang sein"
+
+#: flask_security/core.py:353
+msgid "Password not complex enough"
+msgstr ""
+
+#: flask_security/core.py:354
+msgid "Password on breached list"
+msgstr ""
+
+#: flask_security/core.py:356
+msgid "Failed to contact breached passwords site"
+msgstr ""
+
+#: flask_security/core.py:359
+msgid "Phone number not valid e.g. missing country code"
+msgstr ""
+
+#: flask_security/core.py:360
+msgid "Specified user does not exist"
+msgstr "Angegebener Benutzer existiert nicht"
+
+#: flask_security/core.py:361
+msgid "Invalid password"
+msgstr "Ungültiges Passwort"
+
+#: flask_security/core.py:362
+msgid "Password or code submitted is not valid"
+msgstr ""
+
+#: flask_security/core.py:363
+msgid "You have successfully logged in."
+msgstr "Die Anmeldung war erfolgreich."
+
+#: flask_security/core.py:364
+msgid "Forgot password?"
+msgstr "Passwort vergessen?"
+
+#: flask_security/core.py:366
+msgid ""
+"You successfully reset your password and you have been logged in "
+"automatically."
+msgstr ""
+"Das Passwort wurde erfolgreich wiederhergestellt und die Anmeldung "
+"erfolgte automatisch."
+
+#: flask_security/core.py:373
+msgid "Your new password must be different than your previous password."
+msgstr "Das neue Passwort muss sich vom vorherigen unterscheiden."
+
+#: flask_security/core.py:376
+msgid "You successfully changed your password."
+msgstr "Das Passwort wurde erfolgreich geändert."
+
+#: flask_security/core.py:377
+msgid "Please log in to access this page."
+msgstr "Bitte anmelden, um diese Seite zu sehen."
+
+#: flask_security/core.py:378
+msgid "Please reauthenticate to access this page."
+msgstr "Bitte neu authentifizieren, um auf diese Seite zuzugreifen."
+
+#: flask_security/core.py:379
+msgid "Reauthentication successful"
+msgstr ""
+
+#: flask_security/core.py:381
+msgid "You can only access this endpoint when not logged in."
+msgstr ""
+
+#: flask_security/core.py:384
+msgid "Failed to send code. Please try again later"
+msgstr ""
+
+#: flask_security/core.py:385
+msgid "Invalid Token"
+msgstr ""
+
+#: flask_security/core.py:386
+msgid "Your token has been confirmed"
+msgstr ""
+
+#: flask_security/core.py:388
+msgid "You successfully changed your two-factor method."
+msgstr ""
+
+#: flask_security/core.py:392
+msgid "You successfully confirmed password"
+msgstr ""
+
+#: flask_security/core.py:396
+msgid "Password confirmation is needed in order to access page"
+msgstr ""
+
+#: flask_security/core.py:400
+msgid "You currently do not have permissions to access this page"
+msgstr ""
+
+#: flask_security/core.py:403
+msgid "Marked method is not valid"
+msgstr ""
+
+#: flask_security/core.py:405
+msgid "You successfully disabled two factor authorization."
+msgstr ""
+
+#: flask_security/core.py:408
+msgid "Requested method is not valid"
+msgstr ""
+
+#: flask_security/core.py:410
+#, python-format
+msgid "Setup must be completed within %(within)s. Please start over."
+msgstr ""
+
+#: flask_security/core.py:413
+msgid "Unified sign in setup successful"
+msgstr ""
+
+#: flask_security/core.py:414
+msgid "You must specify a valid identity to sign in"
+msgstr ""
+
+#: flask_security/core.py:415
+#, python-format
+msgid "Use this code to sign in: %(code)s."
+msgstr ""
+
+#: flask_security/forms.py:50
+msgid "Email Address"
+msgstr "E-Mail-Adresse"
+
+#: flask_security/forms.py:51
+msgid "Password"
+msgstr "Passwort"
+
+#: flask_security/forms.py:52
+msgid "Remember Me"
+msgstr "Erinnern"
+
+#: flask_security/forms.py:53 flask_security/templates/security/_menu.html:5
+#: flask_security/templates/security/login_user.html:6
+#: flask_security/templates/security/send_login.html:6
+msgid "Login"
+msgstr "Anmelden"
+
+#: flask_security/forms.py:54
+#: flask_security/templates/security/email/us_instructions.html:8
+#: flask_security/templates/security/us_signin.html:6
+msgid "Sign In"
+msgstr ""
+
+#: flask_security/forms.py:55 flask_security/templates/security/_menu.html:11
+#: flask_security/templates/security/register_user.html:6
+msgid "Register"
+msgstr "Registrieren"
+
+#: flask_security/forms.py:56
+msgid "Resend Confirmation Instructions"
+msgstr "Bestätigungsanleitung neu senden"
+
+#: flask_security/forms.py:57
+msgid "Recover Password"
+msgstr "Passwort wiederherstellen"
+
+#: flask_security/forms.py:58
+msgid "Reset Password"
+msgstr "Passwort zurücksetzen"
+
+#: flask_security/forms.py:59
+msgid "Retype Password"
+msgstr "Passwort neu eingeben"
+
+#: flask_security/forms.py:60
+msgid "New Password"
+msgstr "Neues Passwort"
+
+#: flask_security/forms.py:61
+msgid "Change Password"
+msgstr "Passwort ändern"
+
+#: flask_security/forms.py:62
+msgid "Send Login Link"
+msgstr "Anmelde-Link versenden"
+
+#: flask_security/forms.py:63
+msgid "Verify Password"
+msgstr ""
+
+#: flask_security/forms.py:64
+msgid "Change Method"
+msgstr ""
+
+#: flask_security/forms.py:65
+msgid "Phone Number"
+msgstr ""
+
+#: flask_security/forms.py:66
+msgid "Authentication Code"
+msgstr ""
+
+#: flask_security/forms.py:67
+msgid "Submit"
+msgstr ""
+
+#: flask_security/forms.py:68
+msgid "Submit Code"
+msgstr ""
+
+#: flask_security/forms.py:69
+msgid "Error(s)"
+msgstr ""
+
+#: flask_security/forms.py:70
+msgid "Identity"
+msgstr ""
+
+#: flask_security/forms.py:71
+msgid "Send Code"
+msgstr ""
+
+#: flask_security/forms.py:72
+#, fuzzy
+msgid "Passcode"
+msgstr "Passwort"
+
+#: flask_security/unified_signin.py:145
+#, fuzzy
+msgid "Code or Password"
+msgstr "Passwort wiederherstellen"
+
+#: flask_security/unified_signin.py:150 flask_security/unified_signin.py:270
+msgid "Available Methods"
+msgstr ""
+
+#: flask_security/unified_signin.py:151
+msgid "Via email"
+msgstr ""
+
+#: flask_security/unified_signin.py:151
+msgid "Via SMS"
+msgstr ""
+
+#: flask_security/unified_signin.py:272
+msgid "Set up using email"
+msgstr ""
+
+#: flask_security/unified_signin.py:275
+msgid "Set up using an authenticator app (e.g. google, lastpass, authy)"
+msgstr ""
+
+#: flask_security/unified_signin.py:277
+msgid "Set up using SMS"
+msgstr ""
+
+#: flask_security/templates/security/_menu.html:2
+msgid "Menu"
+msgstr "Menü"
+
+#: flask_security/templates/security/_menu.html:8
+msgid "Unified Sign In"
+msgstr ""
+
+#: flask_security/templates/security/_menu.html:14
+msgid "Forgot password"
+msgstr "Passwort vergessen"
+
+#: flask_security/templates/security/_menu.html:17
+msgid "Confirm account"
+msgstr "Konto bestätigen"
+
+#: flask_security/templates/security/change_password.html:6
+msgid "Change password"
+msgstr "Passwort ändern"
+
+#: flask_security/templates/security/forgot_password.html:6
+msgid "Send password reset instructions"
+msgstr "Anleitung zur Passwortzurücksetzung versenden"
+
+#: flask_security/templates/security/reset_password.html:6
+msgid "Reset password"
+msgstr "Passwort zurücksetzen"
+
+#: flask_security/templates/security/send_confirmation.html:6
+msgid "Resend confirmation instructions"
+msgstr "Bestätigungsanleitung erneut versenden"
+
+#: flask_security/templates/security/two_factor_setup.html:6
+msgid "Two-factor authentication adds an extra layer of security to your account"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_setup.html:7
+msgid ""
+"In addition to your username and password, you'll need to use a code that"
+" we will send you"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_setup.html:18
+msgid "To complete logging in, please enter the code sent to your mail"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_setup.html:21
+#: flask_security/templates/security/us_setup.html:21
+msgid ""
+"Open your authenticator app on your device and scan the following qrcode "
+"to start receiving codes:"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_setup.html:22
+msgid "Two factor authentication code"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_setup.html:25
+msgid "To Which Phone Number Should We Send Code To?"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_verify_code.html:6
+msgid "Two-factor Authentication"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_verify_code.html:7
+msgid "Please enter your authentication code"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_verify_code.html:18
+msgid "The code for authentication was sent to your email address"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_verify_code.html:21
+msgid "A mail was sent to us in order to reset your application account"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_verify_password.html:6
+#: flask_security/templates/security/verify.html:6
+msgid "Please Enter Your Password"
+msgstr ""
+
+#: flask_security/templates/security/us_setup.html:6
+msgid "Setup Unified Sign In options"
+msgstr ""
+
+#: flask_security/templates/security/us_setup.html:22
+msgid "Passwordless QRCode"
+msgstr ""
+
+#: flask_security/templates/security/us_setup.html:25
+#: flask_security/templates/security/us_signin.html:23
+#: flask_security/templates/security/us_verify.html:21
+#, fuzzy
+msgid "Code has been sent"
+msgstr "Das Passwort wurde zurückgesetzt"
+
+#: flask_security/templates/security/us_setup.html:29
+msgid "No methods have been enabled - nothing to setup"
+msgstr ""
+
+#: flask_security/templates/security/us_signin.html:15
+#: flask_security/templates/security/us_verify.html:13
+msgid "Request one-time code be sent"
+msgstr ""
+
+#: flask_security/templates/security/us_verify.html:6
+#, fuzzy
+msgid "Please re-authenticate"
+msgstr "Bitte neu authentifizieren, um auf diese Seite zuzugreifen."
+
+#: flask_security/templates/security/email/change_notice.html:1
+msgid "Your password has been changed."
+msgstr "Das Passwort wurde geändert."
+
+#: flask_security/templates/security/email/change_notice.html:3
+msgid "If you did not change your password,"
+msgstr "Falls das Passwort nicht geändert wurde"
+
+#: flask_security/templates/security/email/change_notice.html:3
+msgid "click here to reset it"
+msgstr "hier klicken, um es zurückzusetzen"
+
+#: flask_security/templates/security/email/confirmation_instructions.html:1
+msgid "Please confirm your email through the link below:"
+msgstr "Bitte die E-Mail-Adresse durch den Link unten bestätigen:"
+
+#: flask_security/templates/security/email/confirmation_instructions.html:3
+#: flask_security/templates/security/email/welcome.html:6
+msgid "Confirm my account"
+msgstr "Das Konto bestätigen"
+
+#: flask_security/templates/security/email/login_instructions.html:1
+#: flask_security/templates/security/email/welcome.html:1
+#, python-format
+msgid "Welcome %(email)s!"
+msgstr "Willkommen %(email)s!"
+
+#: flask_security/templates/security/email/login_instructions.html:3
+msgid "You can log into your account through the link below:"
+msgstr "Die Anmeldung kann über den Link unten erfolgen:"
+
+#: flask_security/templates/security/email/login_instructions.html:5
+msgid "Login now"
+msgstr "Jetzt anmelden"
+
+#: flask_security/templates/security/email/reset_instructions.html:1
+msgid "Click here to reset your password"
+msgstr "Hier klicken, um das Passwort zurückzusetzen"
+
+#: flask_security/templates/security/email/two_factor_instructions.html:3
+msgid "You can log into your account using the following code:"
+msgstr ""
+
+#: flask_security/templates/security/email/two_factor_rescue.html:1
+msgid "can not access mail account"
+msgstr ""
+
+#: flask_security/templates/security/email/us_instructions.html:3
+#, fuzzy
+msgid "You can sign into your account using the following code:"
+msgstr "Die Anmeldung kann über den Link unten erfolgen:"
+
+#: flask_security/templates/security/email/us_instructions.html:6
+#, fuzzy
+msgid "Or use the the link below:"
+msgstr "Die E-Mail-Adresse kann über den Link unten bestätigt werden"
+
+#: flask_security/templates/security/email/welcome.html:4
+msgid "You can confirm your email through the link below:"
+msgstr "Die E-Mail-Adresse kann über den Link unten bestätigt werden"
+
diff --git a/flask_security/translations/es_ES/LC_MESSAGES/flask_security.mo b/flask_security/translations/es_ES/LC_MESSAGES/flask_security.mo
new file mode 100644
index 0000000..6334186
--- /dev/null
+++ b/flask_security/translations/es_ES/LC_MESSAGES/flask_security.mo
Binary files differ
diff --git a/flask_security/translations/es_ES/LC_MESSAGES/flask_security.po b/flask_security/translations/es_ES/LC_MESSAGES/flask_security.po
new file mode 100644
index 0000000..bd079bc
--- /dev/null
+++ b/flask_security/translations/es_ES/LC_MESSAGES/flask_security.po
@@ -0,0 +1,629 @@
+# Spanish (Spain) translations for Flask-Security.
+# Copyright (C) 2017 DINSIC
+# This file is distributed under the same license as the Flask-Security
+# project.
+# Mauko Quiroga <mauko.quiroga@data.gouv.fr>, 2017.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Flask-Security 2.0.1\n"
+"Report-Msgid-Bugs-To: info@inveniosoftware.org\n"
+"POT-Creation-Date: 2020-04-19 13:18-0700\n"
+"PO-Revision-Date: 2017-08-25 17:21+0200\n"
+"Last-Translator: Mauko Quiroga <mauko.quiroga@data.gouv.fr>\n"
+"Language: es_ES\n"
+"Language-Team: \n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 2.8.0\n"
+
+#: flask_security/core.py:207
+msgid "Login Required"
+msgstr "Inicio de sesión necesario"
+
+#: flask_security/core.py:208
+#: flask_security/templates/security/email/two_factor_instructions.html:1
+#: flask_security/templates/security/email/us_instructions.html:1
+msgid "Welcome"
+msgstr "Bienvenido"
+
+#: flask_security/core.py:209
+msgid "Please confirm your email"
+msgstr "Por favor, confirma tu correo electrónico"
+
+#: flask_security/core.py:210
+msgid "Login instructions"
+msgstr "Instrucciones para iniciar sesión"
+
+#: flask_security/core.py:211
+#: flask_security/templates/security/email/reset_notice.html:1
+msgid "Your password has been reset"
+msgstr "Tu contraseña ha sido restablecida"
+
+#: flask_security/core.py:212
+msgid "Your password has been changed"
+msgstr "Tu contraseña ha sido cambiada"
+
+#: flask_security/core.py:213
+msgid "Password reset instructions"
+msgstr "Instrucciones de recuperación de contraseña"
+
+#: flask_security/core.py:216
+msgid "Two-factor Login"
+msgstr ""
+
+#: flask_security/core.py:217
+msgid "Two-factor Rescue"
+msgstr ""
+
+#: flask_security/core.py:266
+msgid "Verification Code"
+msgstr ""
+
+#: flask_security/core.py:282
+msgid "Input not appropriate for requested API"
+msgstr ""
+
+#: flask_security/core.py:283
+msgid "You do not have permission to view this resource."
+msgstr "No tienes permiso para consultar este recurso."
+
+#: flask_security/core.py:285
+msgid "You are not authenticated. Please supply the correct credentials."
+msgstr ""
+
+#: flask_security/core.py:289
+#, fuzzy
+msgid "You must re-authenticate to access this endpoint"
+msgstr "Deber iniciar sesión nuevamente para poder acceder a esta página."
+
+#: flask_security/core.py:293
+#, python-format
+msgid "Thank you. Confirmation instructions have been sent to %(email)s."
+msgstr ""
+"Gracias. Un correo con instrucciones sobre cómo confirmar tu cuenta ha "
+"sido enviado a %(email)s."
+
+#: flask_security/core.py:296
+msgid "Thank you. Your email has been confirmed."
+msgstr "Gracias. Tu correo electrónico ha sido confirmado."
+
+#: flask_security/core.py:297
+msgid "Your email has already been confirmed."
+msgstr "Tu correo electrónico ya ha sido confirmado."
+
+#: flask_security/core.py:298
+msgid "Invalid confirmation token."
+msgstr "Autentificador de confirmación inválido."
+
+#: flask_security/core.py:300
+#, python-format
+msgid "%(email)s is already associated with an account."
+msgstr "%(email)s ya está asociado a una cuenta."
+
+#: flask_security/core.py:303
+msgid "Password does not match"
+msgstr "La contraseña no coincide"
+
+#: flask_security/core.py:304
+msgid "Passwords do not match"
+msgstr "Las contraseñas no coinciden"
+
+#: flask_security/core.py:305
+msgid "Redirections outside the domain are forbidden"
+msgstr "Las redirecciones a sitios web externos están prohibidas"
+
+#: flask_security/core.py:307
+#, python-format
+msgid "Instructions to reset your password have been sent to %(email)s."
+msgstr ""
+"Las instrucciones para restablecer tu contraseña han sido enviadas a "
+"%(email)s."
+
+#: flask_security/core.py:311
+#, python-format
+msgid ""
+"You did not reset your password within %(within)s. New instructions have "
+"been sent to %(email)s."
+msgstr ""
+"No restableciste tu contraseña antes de %(within)s. Nuevas instrucciones "
+"han sido enviadas a %(email)s."
+
+#: flask_security/core.py:317
+msgid "Invalid reset password token."
+msgstr "Autentificador de restablecimiento de contraseña inválido."
+
+#: flask_security/core.py:318
+msgid "Email requires confirmation."
+msgstr "El correo electrónico requiere confirmación."
+
+#: flask_security/core.py:320
+#, python-format
+msgid "Confirmation instructions have been sent to %(email)s."
+msgstr "Las instrucciones de confirmación se han enviado a %(email)s."
+
+#: flask_security/core.py:324
+#, python-format
+msgid ""
+"You did not confirm your email within %(within)s. New instructions to "
+"confirm your email have been sent to %(email)s."
+msgstr ""
+"No confirmaste tu correo electrónico antes de %(within)s. Nuevas "
+"instrucciones para confirmar tu correo electrónico han sido enviadas a "
+"%(email)s."
+
+#: flask_security/core.py:332
+#, python-format
+msgid ""
+"You did not login within %(within)s. New instructions to login have been "
+"sent to %(email)s."
+msgstr ""
+"No iniciaste sesión antes de %(within)s. Nuevas instrucciones para "
+"iniciar sesión han sido enviadas a %(email)s."
+
+#: flask_security/core.py:339
+#, python-format
+msgid "Instructions to login have been sent to %(email)s."
+msgstr "Instrucciones para iniciar sesión han sido enviadas a %(email)s."
+
+#: flask_security/core.py:342
+msgid "Invalid login token."
+msgstr "Autenticador de inicio de sesión inválido."
+
+#: flask_security/core.py:343
+msgid "Account is disabled."
+msgstr "Cuenta deshabilitada."
+
+#: flask_security/core.py:344
+msgid "Email not provided"
+msgstr "Correo electrónico no indicado"
+
+#: flask_security/core.py:345
+msgid "Invalid email address"
+msgstr "Dirección de correo electrónico inválida"
+
+#: flask_security/core.py:346
+#, fuzzy
+msgid "Invalid code"
+msgstr "Contraseña inválida"
+
+#: flask_security/core.py:347
+msgid "Password not provided"
+msgstr "Contraseña no indicada"
+
+#: flask_security/core.py:348
+msgid "No password is set for this user"
+msgstr "Ninguna contraseña ha sido definida para este·a usuario·a"
+
+#: flask_security/core.py:350
+#, fuzzy, python-format
+msgid "Password must be at least %(length)s characters"
+msgstr "La contraseña debe contar al menos con 6 caracteres"
+
+#: flask_security/core.py:353
+msgid "Password not complex enough"
+msgstr ""
+
+#: flask_security/core.py:354
+msgid "Password on breached list"
+msgstr ""
+
+#: flask_security/core.py:356
+msgid "Failed to contact breached passwords site"
+msgstr ""
+
+#: flask_security/core.py:359
+msgid "Phone number not valid e.g. missing country code"
+msgstr ""
+
+#: flask_security/core.py:360
+msgid "Specified user does not exist"
+msgstr "Usuario·a especificado·a no existe"
+
+#: flask_security/core.py:361
+msgid "Invalid password"
+msgstr "Contraseña inválida"
+
+#: flask_security/core.py:362
+msgid "Password or code submitted is not valid"
+msgstr ""
+
+#: flask_security/core.py:363
+msgid "You have successfully logged in."
+msgstr "Has iniciado sesión con éxito."
+
+#: flask_security/core.py:364
+msgid "Forgot password?"
+msgstr "¿Has olvidado tu contraseña?"
+
+#: flask_security/core.py:366
+msgid ""
+"You successfully reset your password and you have been logged in "
+"automatically."
+msgstr ""
+"Has restablecido tu contraseña con éxito y has iniciado sesión "
+"automáticamente."
+
+#: flask_security/core.py:373
+msgid "Your new password must be different than your previous password."
+msgstr "Tu nueva contraseña debe ser diferente de la antigua."
+
+#: flask_security/core.py:376
+msgid "You successfully changed your password."
+msgstr "Has cambiado tu contraseña con éxito."
+
+#: flask_security/core.py:377
+msgid "Please log in to access this page."
+msgstr "Debes iniciar sesión para poder acceder a esta página."
+
+#: flask_security/core.py:378
+msgid "Please reauthenticate to access this page."
+msgstr "Deber iniciar sesión nuevamente para poder acceder a esta página."
+
+#: flask_security/core.py:379
+msgid "Reauthentication successful"
+msgstr ""
+
+#: flask_security/core.py:381
+msgid "You can only access this endpoint when not logged in."
+msgstr ""
+
+#: flask_security/core.py:384
+msgid "Failed to send code. Please try again later"
+msgstr ""
+
+#: flask_security/core.py:385
+msgid "Invalid Token"
+msgstr ""
+
+#: flask_security/core.py:386
+msgid "Your token has been confirmed"
+msgstr ""
+
+#: flask_security/core.py:388
+msgid "You successfully changed your two-factor method."
+msgstr ""
+
+#: flask_security/core.py:392
+msgid "You successfully confirmed password"
+msgstr ""
+
+#: flask_security/core.py:396
+msgid "Password confirmation is needed in order to access page"
+msgstr ""
+
+#: flask_security/core.py:400
+msgid "You currently do not have permissions to access this page"
+msgstr ""
+
+#: flask_security/core.py:403
+msgid "Marked method is not valid"
+msgstr ""
+
+#: flask_security/core.py:405
+msgid "You successfully disabled two factor authorization."
+msgstr ""
+
+#: flask_security/core.py:408
+msgid "Requested method is not valid"
+msgstr ""
+
+#: flask_security/core.py:410
+#, python-format
+msgid "Setup must be completed within %(within)s. Please start over."
+msgstr ""
+
+#: flask_security/core.py:413
+msgid "Unified sign in setup successful"
+msgstr ""
+
+#: flask_security/core.py:414
+msgid "You must specify a valid identity to sign in"
+msgstr ""
+
+#: flask_security/core.py:415
+#, python-format
+msgid "Use this code to sign in: %(code)s."
+msgstr ""
+
+#: flask_security/forms.py:50
+msgid "Email Address"
+msgstr "Correo electrónico"
+
+#: flask_security/forms.py:51
+msgid "Password"
+msgstr "Contraseña"
+
+#: flask_security/forms.py:52
+msgid "Remember Me"
+msgstr "Recordarme"
+
+#: flask_security/forms.py:53 flask_security/templates/security/_menu.html:5
+#: flask_security/templates/security/login_user.html:6
+#: flask_security/templates/security/send_login.html:6
+msgid "Login"
+msgstr "Iniciar sesión"
+
+#: flask_security/forms.py:54
+#: flask_security/templates/security/email/us_instructions.html:8
+#: flask_security/templates/security/us_signin.html:6
+msgid "Sign In"
+msgstr ""
+
+#: flask_security/forms.py:55 flask_security/templates/security/_menu.html:11
+#: flask_security/templates/security/register_user.html:6
+msgid "Register"
+msgstr "Registrarse"
+
+#: flask_security/forms.py:56
+msgid "Resend Confirmation Instructions"
+msgstr "Reenviar instrucciones de confirmación"
+
+#: flask_security/forms.py:57
+msgid "Recover Password"
+msgstr "Recuperar contraseña"
+
+#: flask_security/forms.py:58
+msgid "Reset Password"
+msgstr "Restablecer contraseña"
+
+#: flask_security/forms.py:59
+msgid "Retype Password"
+msgstr "Escribir contraseña nuevamente"
+
+#: flask_security/forms.py:60
+msgid "New Password"
+msgstr "Nueva contraseña"
+
+#: flask_security/forms.py:61
+msgid "Change Password"
+msgstr "Cambiar la contraseña"
+
+#: flask_security/forms.py:62
+msgid "Send Login Link"
+msgstr "Enviar enlace para iniciar sesión"
+
+#: flask_security/forms.py:63
+msgid "Verify Password"
+msgstr ""
+
+#: flask_security/forms.py:64
+msgid "Change Method"
+msgstr ""
+
+#: flask_security/forms.py:65
+msgid "Phone Number"
+msgstr ""
+
+#: flask_security/forms.py:66
+msgid "Authentication Code"
+msgstr ""
+
+#: flask_security/forms.py:67
+msgid "Submit"
+msgstr ""
+
+#: flask_security/forms.py:68
+msgid "Submit Code"
+msgstr ""
+
+#: flask_security/forms.py:69
+msgid "Error(s)"
+msgstr ""
+
+#: flask_security/forms.py:70
+msgid "Identity"
+msgstr ""
+
+#: flask_security/forms.py:71
+msgid "Send Code"
+msgstr ""
+
+#: flask_security/forms.py:72
+#, fuzzy
+msgid "Passcode"
+msgstr "Contraseña"
+
+#: flask_security/unified_signin.py:145
+#, fuzzy
+msgid "Code or Password"
+msgstr "Recuperar contraseña"
+
+#: flask_security/unified_signin.py:150 flask_security/unified_signin.py:270
+msgid "Available Methods"
+msgstr ""
+
+#: flask_security/unified_signin.py:151
+msgid "Via email"
+msgstr ""
+
+#: flask_security/unified_signin.py:151
+msgid "Via SMS"
+msgstr ""
+
+#: flask_security/unified_signin.py:272
+msgid "Set up using email"
+msgstr ""
+
+#: flask_security/unified_signin.py:275
+msgid "Set up using an authenticator app (e.g. google, lastpass, authy)"
+msgstr ""
+
+#: flask_security/unified_signin.py:277
+msgid "Set up using SMS"
+msgstr ""
+
+#: flask_security/templates/security/_menu.html:2
+msgid "Menu"
+msgstr "Menú"
+
+#: flask_security/templates/security/_menu.html:8
+msgid "Unified Sign In"
+msgstr ""
+
+#: flask_security/templates/security/_menu.html:14
+msgid "Forgot password"
+msgstr "Olvidé mi contraseña"
+
+#: flask_security/templates/security/_menu.html:17
+msgid "Confirm account"
+msgstr "Confirmar cuenta"
+
+#: flask_security/templates/security/change_password.html:6
+msgid "Change password"
+msgstr "Cambiar la contraseña"
+
+#: flask_security/templates/security/forgot_password.html:6
+msgid "Send password reset instructions"
+msgstr "Enviar instrucciones para restablecer la contraseña"
+
+#: flask_security/templates/security/reset_password.html:6
+msgid "Reset password"
+msgstr "Restablecer contraseña"
+
+#: flask_security/templates/security/send_confirmation.html:6
+msgid "Resend confirmation instructions"
+msgstr "Reenviar instrucciones de confirmación"
+
+#: flask_security/templates/security/two_factor_setup.html:6
+msgid "Two-factor authentication adds an extra layer of security to your account"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_setup.html:7
+msgid ""
+"In addition to your username and password, you'll need to use a code that"
+" we will send you"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_setup.html:18
+msgid "To complete logging in, please enter the code sent to your mail"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_setup.html:21
+#: flask_security/templates/security/us_setup.html:21
+msgid ""
+"Open your authenticator app on your device and scan the following qrcode "
+"to start receiving codes:"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_setup.html:22
+msgid "Two factor authentication code"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_setup.html:25
+msgid "To Which Phone Number Should We Send Code To?"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_verify_code.html:6
+msgid "Two-factor Authentication"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_verify_code.html:7
+msgid "Please enter your authentication code"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_verify_code.html:18
+msgid "The code for authentication was sent to your email address"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_verify_code.html:21
+msgid "A mail was sent to us in order to reset your application account"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_verify_password.html:6
+#: flask_security/templates/security/verify.html:6
+msgid "Please Enter Your Password"
+msgstr ""
+
+#: flask_security/templates/security/us_setup.html:6
+msgid "Setup Unified Sign In options"
+msgstr ""
+
+#: flask_security/templates/security/us_setup.html:22
+msgid "Passwordless QRCode"
+msgstr ""
+
+#: flask_security/templates/security/us_setup.html:25
+#: flask_security/templates/security/us_signin.html:23
+#: flask_security/templates/security/us_verify.html:21
+#, fuzzy
+msgid "Code has been sent"
+msgstr "Tu contraseña ha sido restablecida"
+
+#: flask_security/templates/security/us_setup.html:29
+msgid "No methods have been enabled - nothing to setup"
+msgstr ""
+
+#: flask_security/templates/security/us_signin.html:15
+#: flask_security/templates/security/us_verify.html:13
+msgid "Request one-time code be sent"
+msgstr ""
+
+#: flask_security/templates/security/us_verify.html:6
+#, fuzzy
+msgid "Please re-authenticate"
+msgstr "Deber iniciar sesión nuevamente para poder acceder a esta página."
+
+#: flask_security/templates/security/email/change_notice.html:1
+msgid "Your password has been changed."
+msgstr "Tu contraseña ha sido cambiada."
+
+#: flask_security/templates/security/email/change_notice.html:3
+msgid "If you did not change your password,"
+msgstr "Si no has cambiado tu contraseña,"
+
+#: flask_security/templates/security/email/change_notice.html:3
+msgid "click here to reset it"
+msgstr "haz clic aquí para restablecerla"
+
+#: flask_security/templates/security/email/confirmation_instructions.html:1
+msgid "Please confirm your email through the link below:"
+msgstr "Confirma tu correo electrónico haciendo clic aquí:"
+
+#: flask_security/templates/security/email/confirmation_instructions.html:3
+#: flask_security/templates/security/email/welcome.html:6
+msgid "Confirm my account"
+msgstr "Confirmar mi cuenta"
+
+#: flask_security/templates/security/email/login_instructions.html:1
+#: flask_security/templates/security/email/welcome.html:1
+#, python-format
+msgid "Welcome %(email)s!"
+msgstr "¡Bienvenido %(email)s!"
+
+#: flask_security/templates/security/email/login_instructions.html:3
+msgid "You can log into your account through the link below:"
+msgstr "Inicia sesión haciendo clic aquí:"
+
+#: flask_security/templates/security/email/login_instructions.html:5
+msgid "Login now"
+msgstr "Iniciar sesión ahora"
+
+#: flask_security/templates/security/email/reset_instructions.html:1
+msgid "Click here to reset your password"
+msgstr "Haz clic aquí para restablecer la contraseña"
+
+#: flask_security/templates/security/email/two_factor_instructions.html:3
+msgid "You can log into your account using the following code:"
+msgstr ""
+
+#: flask_security/templates/security/email/two_factor_rescue.html:1
+msgid "can not access mail account"
+msgstr ""
+
+#: flask_security/templates/security/email/us_instructions.html:3
+#, fuzzy
+msgid "You can sign into your account using the following code:"
+msgstr "Inicia sesión haciendo clic aquí:"
+
+#: flask_security/templates/security/email/us_instructions.html:6
+#, fuzzy
+msgid "Or use the the link below:"
+msgstr "Confirma tu correo electrónico haciendo clic aquí:"
+
+#: flask_security/templates/security/email/welcome.html:4
+msgid "You can confirm your email through the link below:"
+msgstr "Confirma tu correo electrónico haciendo clic aquí:"
+
diff --git a/flask_security/translations/flask_security.pot b/flask_security/translations/flask_security.pot
new file mode 100644
index 0000000..a86fdd5
--- /dev/null
+++ b/flask_security/translations/flask_security.pot
@@ -0,0 +1,607 @@
+# Translations template for Flask-Security.
+# Copyright (C) 2020 ORGANIZATION
+# This file is distributed under the same license as the Flask-Security
+# project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2020.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Flask-Security 3.4.0\n"
+"Report-Msgid-Bugs-To: jwag956@github.com\n"
+"POT-Creation-Date: 2020-04-19 13:18-0700\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 2.8.0\n"
+
+#: flask_security/core.py:207
+msgid "Login Required"
+msgstr ""
+
+#: flask_security/core.py:208
+#: flask_security/templates/security/email/two_factor_instructions.html:1
+#: flask_security/templates/security/email/us_instructions.html:1
+msgid "Welcome"
+msgstr ""
+
+#: flask_security/core.py:209
+msgid "Please confirm your email"
+msgstr ""
+
+#: flask_security/core.py:210
+msgid "Login instructions"
+msgstr ""
+
+#: flask_security/core.py:211
+#: flask_security/templates/security/email/reset_notice.html:1
+msgid "Your password has been reset"
+msgstr ""
+
+#: flask_security/core.py:212
+msgid "Your password has been changed"
+msgstr ""
+
+#: flask_security/core.py:213
+msgid "Password reset instructions"
+msgstr ""
+
+#: flask_security/core.py:216
+msgid "Two-factor Login"
+msgstr ""
+
+#: flask_security/core.py:217
+msgid "Two-factor Rescue"
+msgstr ""
+
+#: flask_security/core.py:266
+msgid "Verification Code"
+msgstr ""
+
+#: flask_security/core.py:282
+msgid "Input not appropriate for requested API"
+msgstr ""
+
+#: flask_security/core.py:283
+msgid "You do not have permission to view this resource."
+msgstr ""
+
+#: flask_security/core.py:285
+msgid "You are not authenticated. Please supply the correct credentials."
+msgstr ""
+
+#: flask_security/core.py:289
+msgid "You must re-authenticate to access this endpoint"
+msgstr ""
+
+#: flask_security/core.py:293
+#, python-format
+msgid "Thank you. Confirmation instructions have been sent to %(email)s."
+msgstr ""
+
+#: flask_security/core.py:296
+msgid "Thank you. Your email has been confirmed."
+msgstr ""
+
+#: flask_security/core.py:297
+msgid "Your email has already been confirmed."
+msgstr ""
+
+#: flask_security/core.py:298
+msgid "Invalid confirmation token."
+msgstr ""
+
+#: flask_security/core.py:300
+#, python-format
+msgid "%(email)s is already associated with an account."
+msgstr ""
+
+#: flask_security/core.py:303
+msgid "Password does not match"
+msgstr ""
+
+#: flask_security/core.py:304
+msgid "Passwords do not match"
+msgstr ""
+
+#: flask_security/core.py:305
+msgid "Redirections outside the domain are forbidden"
+msgstr ""
+
+#: flask_security/core.py:307
+#, python-format
+msgid "Instructions to reset your password have been sent to %(email)s."
+msgstr ""
+
+#: flask_security/core.py:311
+#, python-format
+msgid ""
+"You did not reset your password within %(within)s. New instructions have "
+"been sent to %(email)s."
+msgstr ""
+
+#: flask_security/core.py:317
+msgid "Invalid reset password token."
+msgstr ""
+
+#: flask_security/core.py:318
+msgid "Email requires confirmation."
+msgstr ""
+
+#: flask_security/core.py:320
+#, python-format
+msgid "Confirmation instructions have been sent to %(email)s."
+msgstr ""
+
+#: flask_security/core.py:324
+#, python-format
+msgid ""
+"You did not confirm your email within %(within)s. New instructions to "
+"confirm your email have been sent to %(email)s."
+msgstr ""
+
+#: flask_security/core.py:332
+#, python-format
+msgid ""
+"You did not login within %(within)s. New instructions to login have been "
+"sent to %(email)s."
+msgstr ""
+
+#: flask_security/core.py:339
+#, python-format
+msgid "Instructions to login have been sent to %(email)s."
+msgstr ""
+
+#: flask_security/core.py:342
+msgid "Invalid login token."
+msgstr ""
+
+#: flask_security/core.py:343
+msgid "Account is disabled."
+msgstr ""
+
+#: flask_security/core.py:344
+msgid "Email not provided"
+msgstr ""
+
+#: flask_security/core.py:345
+msgid "Invalid email address"
+msgstr ""
+
+#: flask_security/core.py:346
+msgid "Invalid code"
+msgstr ""
+
+#: flask_security/core.py:347
+msgid "Password not provided"
+msgstr ""
+
+#: flask_security/core.py:348
+msgid "No password is set for this user"
+msgstr ""
+
+#: flask_security/core.py:350
+#, python-format
+msgid "Password must be at least %(length)s characters"
+msgstr ""
+
+#: flask_security/core.py:353
+msgid "Password not complex enough"
+msgstr ""
+
+#: flask_security/core.py:354
+msgid "Password on breached list"
+msgstr ""
+
+#: flask_security/core.py:356
+msgid "Failed to contact breached passwords site"
+msgstr ""
+
+#: flask_security/core.py:359
+msgid "Phone number not valid e.g. missing country code"
+msgstr ""
+
+#: flask_security/core.py:360
+msgid "Specified user does not exist"
+msgstr ""
+
+#: flask_security/core.py:361
+msgid "Invalid password"
+msgstr ""
+
+#: flask_security/core.py:362
+msgid "Password or code submitted is not valid"
+msgstr ""
+
+#: flask_security/core.py:363
+msgid "You have successfully logged in."
+msgstr ""
+
+#: flask_security/core.py:364
+msgid "Forgot password?"
+msgstr ""
+
+#: flask_security/core.py:366
+msgid ""
+"You successfully reset your password and you have been logged in "
+"automatically."
+msgstr ""
+
+#: flask_security/core.py:373
+msgid "Your new password must be different than your previous password."
+msgstr ""
+
+#: flask_security/core.py:376
+msgid "You successfully changed your password."
+msgstr ""
+
+#: flask_security/core.py:377
+msgid "Please log in to access this page."
+msgstr ""
+
+#: flask_security/core.py:378
+msgid "Please reauthenticate to access this page."
+msgstr ""
+
+#: flask_security/core.py:379
+msgid "Reauthentication successful"
+msgstr ""
+
+#: flask_security/core.py:381
+msgid "You can only access this endpoint when not logged in."
+msgstr ""
+
+#: flask_security/core.py:384
+msgid "Failed to send code. Please try again later"
+msgstr ""
+
+#: flask_security/core.py:385
+msgid "Invalid Token"
+msgstr ""
+
+#: flask_security/core.py:386
+msgid "Your token has been confirmed"
+msgstr ""
+
+#: flask_security/core.py:388
+msgid "You successfully changed your two-factor method."
+msgstr ""
+
+#: flask_security/core.py:392
+msgid "You successfully confirmed password"
+msgstr ""
+
+#: flask_security/core.py:396
+msgid "Password confirmation is needed in order to access page"
+msgstr ""
+
+#: flask_security/core.py:400
+msgid "You currently do not have permissions to access this page"
+msgstr ""
+
+#: flask_security/core.py:403
+msgid "Marked method is not valid"
+msgstr ""
+
+#: flask_security/core.py:405
+msgid "You successfully disabled two factor authorization."
+msgstr ""
+
+#: flask_security/core.py:408
+msgid "Requested method is not valid"
+msgstr ""
+
+#: flask_security/core.py:410
+#, python-format
+msgid "Setup must be completed within %(within)s. Please start over."
+msgstr ""
+
+#: flask_security/core.py:413
+msgid "Unified sign in setup successful"
+msgstr ""
+
+#: flask_security/core.py:414
+msgid "You must specify a valid identity to sign in"
+msgstr ""
+
+#: flask_security/core.py:415
+#, python-format
+msgid "Use this code to sign in: %(code)s."
+msgstr ""
+
+#: flask_security/forms.py:50
+msgid "Email Address"
+msgstr ""
+
+#: flask_security/forms.py:51
+msgid "Password"
+msgstr ""
+
+#: flask_security/forms.py:52
+msgid "Remember Me"
+msgstr ""
+
+#: flask_security/forms.py:53 flask_security/templates/security/_menu.html:5
+#: flask_security/templates/security/login_user.html:6
+#: flask_security/templates/security/send_login.html:6
+msgid "Login"
+msgstr ""
+
+#: flask_security/forms.py:54
+#: flask_security/templates/security/email/us_instructions.html:8
+#: flask_security/templates/security/us_signin.html:6
+msgid "Sign In"
+msgstr ""
+
+#: flask_security/forms.py:55 flask_security/templates/security/_menu.html:11
+#: flask_security/templates/security/register_user.html:6
+msgid "Register"
+msgstr ""
+
+#: flask_security/forms.py:56
+msgid "Resend Confirmation Instructions"
+msgstr ""
+
+#: flask_security/forms.py:57
+msgid "Recover Password"
+msgstr ""
+
+#: flask_security/forms.py:58
+msgid "Reset Password"
+msgstr ""
+
+#: flask_security/forms.py:59
+msgid "Retype Password"
+msgstr ""
+
+#: flask_security/forms.py:60
+msgid "New Password"
+msgstr ""
+
+#: flask_security/forms.py:61
+msgid "Change Password"
+msgstr ""
+
+#: flask_security/forms.py:62
+msgid "Send Login Link"
+msgstr ""
+
+#: flask_security/forms.py:63
+msgid "Verify Password"
+msgstr ""
+
+#: flask_security/forms.py:64
+msgid "Change Method"
+msgstr ""
+
+#: flask_security/forms.py:65
+msgid "Phone Number"
+msgstr ""
+
+#: flask_security/forms.py:66
+msgid "Authentication Code"
+msgstr ""
+
+#: flask_security/forms.py:67
+msgid "Submit"
+msgstr ""
+
+#: flask_security/forms.py:68
+msgid "Submit Code"
+msgstr ""
+
+#: flask_security/forms.py:69
+msgid "Error(s)"
+msgstr ""
+
+#: flask_security/forms.py:70
+msgid "Identity"
+msgstr ""
+
+#: flask_security/forms.py:71
+msgid "Send Code"
+msgstr ""
+
+#: flask_security/forms.py:72
+msgid "Passcode"
+msgstr ""
+
+#: flask_security/unified_signin.py:145
+msgid "Code or Password"
+msgstr ""
+
+#: flask_security/unified_signin.py:150 flask_security/unified_signin.py:270
+msgid "Available Methods"
+msgstr ""
+
+#: flask_security/unified_signin.py:151
+msgid "Via email"
+msgstr ""
+
+#: flask_security/unified_signin.py:151
+msgid "Via SMS"
+msgstr ""
+
+#: flask_security/unified_signin.py:272
+msgid "Set up using email"
+msgstr ""
+
+#: flask_security/unified_signin.py:275
+msgid "Set up using an authenticator app (e.g. google, lastpass, authy)"
+msgstr ""
+
+#: flask_security/unified_signin.py:277
+msgid "Set up using SMS"
+msgstr ""
+
+#: flask_security/templates/security/_menu.html:2
+msgid "Menu"
+msgstr ""
+
+#: flask_security/templates/security/_menu.html:8
+msgid "Unified Sign In"
+msgstr ""
+
+#: flask_security/templates/security/_menu.html:14
+msgid "Forgot password"
+msgstr ""
+
+#: flask_security/templates/security/_menu.html:17
+msgid "Confirm account"
+msgstr ""
+
+#: flask_security/templates/security/change_password.html:6
+msgid "Change password"
+msgstr ""
+
+#: flask_security/templates/security/forgot_password.html:6
+msgid "Send password reset instructions"
+msgstr ""
+
+#: flask_security/templates/security/reset_password.html:6
+msgid "Reset password"
+msgstr ""
+
+#: flask_security/templates/security/send_confirmation.html:6
+msgid "Resend confirmation instructions"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_setup.html:6
+msgid "Two-factor authentication adds an extra layer of security to your account"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_setup.html:7
+msgid ""
+"In addition to your username and password, you'll need to use a code that"
+" we will send you"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_setup.html:18
+msgid "To complete logging in, please enter the code sent to your mail"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_setup.html:21
+#: flask_security/templates/security/us_setup.html:21
+msgid ""
+"Open your authenticator app on your device and scan the following qrcode "
+"to start receiving codes:"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_setup.html:22
+msgid "Two factor authentication code"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_setup.html:25
+msgid "To Which Phone Number Should We Send Code To?"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_verify_code.html:6
+msgid "Two-factor Authentication"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_verify_code.html:7
+msgid "Please enter your authentication code"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_verify_code.html:18
+msgid "The code for authentication was sent to your email address"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_verify_code.html:21
+msgid "A mail was sent to us in order to reset your application account"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_verify_password.html:6
+#: flask_security/templates/security/verify.html:6
+msgid "Please Enter Your Password"
+msgstr ""
+
+#: flask_security/templates/security/us_setup.html:6
+msgid "Setup Unified Sign In options"
+msgstr ""
+
+#: flask_security/templates/security/us_setup.html:22
+msgid "Passwordless QRCode"
+msgstr ""
+
+#: flask_security/templates/security/us_setup.html:25
+#: flask_security/templates/security/us_signin.html:23
+#: flask_security/templates/security/us_verify.html:21
+msgid "Code has been sent"
+msgstr ""
+
+#: flask_security/templates/security/us_setup.html:29
+msgid "No methods have been enabled - nothing to setup"
+msgstr ""
+
+#: flask_security/templates/security/us_signin.html:15
+#: flask_security/templates/security/us_verify.html:13
+msgid "Request one-time code be sent"
+msgstr ""
+
+#: flask_security/templates/security/us_verify.html:6
+msgid "Please re-authenticate"
+msgstr ""
+
+#: flask_security/templates/security/email/change_notice.html:1
+msgid "Your password has been changed."
+msgstr ""
+
+#: flask_security/templates/security/email/change_notice.html:3
+msgid "If you did not change your password,"
+msgstr ""
+
+#: flask_security/templates/security/email/change_notice.html:3
+msgid "click here to reset it"
+msgstr ""
+
+#: flask_security/templates/security/email/confirmation_instructions.html:1
+msgid "Please confirm your email through the link below:"
+msgstr ""
+
+#: flask_security/templates/security/email/confirmation_instructions.html:3
+#: flask_security/templates/security/email/welcome.html:6
+msgid "Confirm my account"
+msgstr ""
+
+#: flask_security/templates/security/email/login_instructions.html:1
+#: flask_security/templates/security/email/welcome.html:1
+#, python-format
+msgid "Welcome %(email)s!"
+msgstr ""
+
+#: flask_security/templates/security/email/login_instructions.html:3
+msgid "You can log into your account through the link below:"
+msgstr ""
+
+#: flask_security/templates/security/email/login_instructions.html:5
+msgid "Login now"
+msgstr ""
+
+#: flask_security/templates/security/email/reset_instructions.html:1
+msgid "Click here to reset your password"
+msgstr ""
+
+#: flask_security/templates/security/email/two_factor_instructions.html:3
+msgid "You can log into your account using the following code:"
+msgstr ""
+
+#: flask_security/templates/security/email/two_factor_rescue.html:1
+msgid "can not access mail account"
+msgstr ""
+
+#: flask_security/templates/security/email/us_instructions.html:3
+msgid "You can sign into your account using the following code:"
+msgstr ""
+
+#: flask_security/templates/security/email/us_instructions.html:6
+msgid "Or use the the link below:"
+msgstr ""
+
+#: flask_security/templates/security/email/welcome.html:4
+msgid "You can confirm your email through the link below:"
+msgstr ""
+
diff --git a/flask_security/translations/fr_FR/LC_MESSAGES/flask_security.mo b/flask_security/translations/fr_FR/LC_MESSAGES/flask_security.mo
new file mode 100644
index 0000000..87f96af
--- /dev/null
+++ b/flask_security/translations/fr_FR/LC_MESSAGES/flask_security.mo
Binary files differ
diff --git a/flask_security/translations/fr_FR/LC_MESSAGES/flask_security.po b/flask_security/translations/fr_FR/LC_MESSAGES/flask_security.po
new file mode 100644
index 0000000..5b8b98a
--- /dev/null
+++ b/flask_security/translations/fr_FR/LC_MESSAGES/flask_security.po
@@ -0,0 +1,630 @@
+# French (France) translations for Flask-Security.
+# Copyright (C) 2017 CERN
+# This file is distributed under the same license as the Flask-Security
+# project.
+# Alexandre Bulté <alexandre@bulte.net>, 2017.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Flask-Security 2.0.1\n"
+"Report-Msgid-Bugs-To: info@inveniosoftware.org\n"
+"POT-Creation-Date: 2020-04-19 13:18-0700\n"
+"PO-Revision-Date: 2017-06-08 10:13+0200\n"
+"Last-Translator: Alexandre Bulté <alexandre@bulte.net>\n"
+"Language: fr_FR\n"
+"Language-Team: fr_FR <LL@li.org>\n"
+"Plural-Forms: nplurals=2; plural=(n > 1)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 2.8.0\n"
+
+#: flask_security/core.py:207
+msgid "Login Required"
+msgstr "Connexion requise"
+
+#: flask_security/core.py:208
+#: flask_security/templates/security/email/two_factor_instructions.html:1
+#: flask_security/templates/security/email/us_instructions.html:1
+msgid "Welcome"
+msgstr "Bienvenue"
+
+#: flask_security/core.py:209
+msgid "Please confirm your email"
+msgstr "Merci de confirmer votre adresse email"
+
+#: flask_security/core.py:210
+msgid "Login instructions"
+msgstr "Instructions de connexion"
+
+#: flask_security/core.py:211
+#: flask_security/templates/security/email/reset_notice.html:1
+msgid "Your password has been reset"
+msgstr "Votre mot de passe a été réinitialisé"
+
+#: flask_security/core.py:212
+msgid "Your password has been changed"
+msgstr "Votre mot de passe a été changé"
+
+#: flask_security/core.py:213
+msgid "Password reset instructions"
+msgstr "Instructions de réinitialisation de votre mot de passe"
+
+#: flask_security/core.py:216
+msgid "Two-factor Login"
+msgstr ""
+
+#: flask_security/core.py:217
+msgid "Two-factor Rescue"
+msgstr ""
+
+#: flask_security/core.py:266
+msgid "Verification Code"
+msgstr ""
+
+#: flask_security/core.py:282
+msgid "Input not appropriate for requested API"
+msgstr ""
+
+#: flask_security/core.py:283
+msgid "You do not have permission to view this resource."
+msgstr "Vous n'avez pas l'autorisation d'accéder à cette ressource."
+
+#: flask_security/core.py:285
+msgid "You are not authenticated. Please supply the correct credentials."
+msgstr ""
+
+#: flask_security/core.py:289
+#, fuzzy
+msgid "You must re-authenticate to access this endpoint"
+msgstr "Merci de vous reconnecter pour accéder à cette page."
+
+#: flask_security/core.py:293
+#, python-format
+msgid "Thank you. Confirmation instructions have been sent to %(email)s."
+msgstr "Merci. Les instructions de confirmation ont été envoyées à %(email)s."
+
+#: flask_security/core.py:296
+msgid "Thank you. Your email has been confirmed."
+msgstr "Merci. Votre adresse email a été confirmée."
+
+#: flask_security/core.py:297
+msgid "Your email has already been confirmed."
+msgstr "Votre adresse email a déjà été confirmée."
+
+#: flask_security/core.py:298
+msgid "Invalid confirmation token."
+msgstr "Token de confirmation non valide."
+
+#: flask_security/core.py:300
+#, python-format
+msgid "%(email)s is already associated with an account."
+msgstr "L'adresse %(email)s est déjà utilisée."
+
+#: flask_security/core.py:303
+msgid "Password does not match"
+msgstr "Le mot de passe ne correspond pas"
+
+#: flask_security/core.py:304
+msgid "Passwords do not match"
+msgstr "Les mots de passe ne correspondent pas"
+
+#: flask_security/core.py:305
+msgid "Redirections outside the domain are forbidden"
+msgstr "Les redirections en dehors du domaine sont interdites"
+
+#: flask_security/core.py:307
+#, python-format
+msgid "Instructions to reset your password have been sent to %(email)s."
+msgstr ""
+"Les instructions de réinitialisation de votre mot de passe ont été "
+"envoyées à %(email)s."
+
+#: flask_security/core.py:311
+#, python-format
+msgid ""
+"You did not reset your password within %(within)s. New instructions have "
+"been sent to %(email)s."
+msgstr ""
+"Vous n'avez pas réinitialisé votre mot de passe dans l'intervalle requis "
+"(%(within)s)De nouvelles instructions ont été envoyées à %(email)s."
+
+#: flask_security/core.py:317
+msgid "Invalid reset password token."
+msgstr "Token de réinitialisation non valide."
+
+#: flask_security/core.py:318
+msgid "Email requires confirmation."
+msgstr "Une confirmation de l'adresse email est requise."
+
+#: flask_security/core.py:320
+#, python-format
+msgid "Confirmation instructions have been sent to %(email)s."
+msgstr "Les instructions de confirmation ont été envoyées à %(email)s."
+
+#: flask_security/core.py:324
+#, python-format
+msgid ""
+"You did not confirm your email within %(within)s. New instructions to "
+"confirm your email have been sent to %(email)s."
+msgstr ""
+"Vous n'avez pas confirmé votre adresse email dans l'intervalle requis "
+"(%(within)s)De nouvelles instructions ont été envoyées à %(email)s."
+
+#: flask_security/core.py:332
+#, python-format
+msgid ""
+"You did not login within %(within)s. New instructions to login have been "
+"sent to %(email)s."
+msgstr ""
+"Vous ne vous êtes pas connecté dans l'intervalle requis (%(within)s)De "
+"nouvelles instructions ont été envoyées à %(email)s."
+
+#: flask_security/core.py:339
+#, python-format
+msgid "Instructions to login have been sent to %(email)s."
+msgstr "Les instructions de connexion ont été envoyées à %(email)s."
+
+#: flask_security/core.py:342
+msgid "Invalid login token."
+msgstr "Token de connexion non valide."
+
+#: flask_security/core.py:343
+msgid "Account is disabled."
+msgstr "Le compte est désactivé."
+
+#: flask_security/core.py:344
+msgid "Email not provided"
+msgstr "Merci d'indiquer une adresse email"
+
+#: flask_security/core.py:345
+msgid "Invalid email address"
+msgstr "Adresse email non valide"
+
+#: flask_security/core.py:346
+#, fuzzy
+msgid "Invalid code"
+msgstr "Mot de passe non valide"
+
+#: flask_security/core.py:347
+msgid "Password not provided"
+msgstr "Merci d'indiquer un mot de passe"
+
+#: flask_security/core.py:348
+msgid "No password is set for this user"
+msgstr "Cet utilisateur n'a pas de mot de passe"
+
+#: flask_security/core.py:350
+#, fuzzy, python-format
+msgid "Password must be at least %(length)s characters"
+msgstr "Le mot de passe doit comporter au moins 6 caractères"
+
+#: flask_security/core.py:353
+msgid "Password not complex enough"
+msgstr ""
+
+#: flask_security/core.py:354
+msgid "Password on breached list"
+msgstr ""
+
+#: flask_security/core.py:356
+msgid "Failed to contact breached passwords site"
+msgstr ""
+
+#: flask_security/core.py:359
+msgid "Phone number not valid e.g. missing country code"
+msgstr ""
+
+#: flask_security/core.py:360
+msgid "Specified user does not exist"
+msgstr "Cet utilisateur n'existe pas"
+
+#: flask_security/core.py:361
+msgid "Invalid password"
+msgstr "Mot de passe non valide"
+
+#: flask_security/core.py:362
+msgid "Password or code submitted is not valid"
+msgstr ""
+
+#: flask_security/core.py:363
+msgid "You have successfully logged in."
+msgstr "Vous êtes bien connecté."
+
+#: flask_security/core.py:364
+msgid "Forgot password?"
+msgstr "Mot de passe oublié&thinsp;?"
+
+#: flask_security/core.py:366
+msgid ""
+"You successfully reset your password and you have been logged in "
+"automatically."
+msgstr ""
+"Vous avez bien réinitialisé votre mot de passe et avez été "
+"automatiquement connecté."
+
+#: flask_security/core.py:373
+msgid "Your new password must be different than your previous password."
+msgstr "Votre nouveau mot de passe doit être différent du précédent."
+
+#: flask_security/core.py:376
+msgid "You successfully changed your password."
+msgstr "Vous avez bien changé votre mot de passe."
+
+#: flask_security/core.py:377
+msgid "Please log in to access this page."
+msgstr "Merci de vous connecter pour accéder à cette page."
+
+#: flask_security/core.py:378
+msgid "Please reauthenticate to access this page."
+msgstr "Merci de vous reconnecter pour accéder à cette page."
+
+#: flask_security/core.py:379
+msgid "Reauthentication successful"
+msgstr ""
+
+#: flask_security/core.py:381
+msgid "You can only access this endpoint when not logged in."
+msgstr ""
+
+#: flask_security/core.py:384
+msgid "Failed to send code. Please try again later"
+msgstr ""
+
+#: flask_security/core.py:385
+msgid "Invalid Token"
+msgstr ""
+
+#: flask_security/core.py:386
+msgid "Your token has been confirmed"
+msgstr ""
+
+#: flask_security/core.py:388
+msgid "You successfully changed your two-factor method."
+msgstr ""
+
+#: flask_security/core.py:392
+msgid "You successfully confirmed password"
+msgstr ""
+
+#: flask_security/core.py:396
+msgid "Password confirmation is needed in order to access page"
+msgstr ""
+
+#: flask_security/core.py:400
+msgid "You currently do not have permissions to access this page"
+msgstr ""
+
+#: flask_security/core.py:403
+msgid "Marked method is not valid"
+msgstr ""
+
+#: flask_security/core.py:405
+msgid "You successfully disabled two factor authorization."
+msgstr ""
+
+#: flask_security/core.py:408
+msgid "Requested method is not valid"
+msgstr ""
+
+#: flask_security/core.py:410
+#, python-format
+msgid "Setup must be completed within %(within)s. Please start over."
+msgstr ""
+
+#: flask_security/core.py:413
+msgid "Unified sign in setup successful"
+msgstr ""
+
+#: flask_security/core.py:414
+msgid "You must specify a valid identity to sign in"
+msgstr ""
+
+#: flask_security/core.py:415
+#, python-format
+msgid "Use this code to sign in: %(code)s."
+msgstr ""
+
+#: flask_security/forms.py:50
+msgid "Email Address"
+msgstr "Adresse email"
+
+#: flask_security/forms.py:51
+msgid "Password"
+msgstr "Mot de passe"
+
+#: flask_security/forms.py:52
+msgid "Remember Me"
+msgstr "Se souvenir de moi"
+
+#: flask_security/forms.py:53 flask_security/templates/security/_menu.html:5
+#: flask_security/templates/security/login_user.html:6
+#: flask_security/templates/security/send_login.html:6
+msgid "Login"
+msgstr "Connexion"
+
+#: flask_security/forms.py:54
+#: flask_security/templates/security/email/us_instructions.html:8
+#: flask_security/templates/security/us_signin.html:6
+msgid "Sign In"
+msgstr ""
+
+#: flask_security/forms.py:55 flask_security/templates/security/_menu.html:11
+#: flask_security/templates/security/register_user.html:6
+msgid "Register"
+msgstr "Inscription"
+
+#: flask_security/forms.py:56
+msgid "Resend Confirmation Instructions"
+msgstr "Renvoyer les instructions de confirmation"
+
+#: flask_security/forms.py:57
+msgid "Recover Password"
+msgstr "Récupérer le mot de passe"
+
+#: flask_security/forms.py:58
+msgid "Reset Password"
+msgstr "Réinitialiser le mot de passe"
+
+#: flask_security/forms.py:59
+msgid "Retype Password"
+msgstr "Confirmer le mot de passe"
+
+#: flask_security/forms.py:60
+msgid "New Password"
+msgstr "Nouveau mot de passe"
+
+#: flask_security/forms.py:61
+msgid "Change Password"
+msgstr "Changer le mot de passe"
+
+#: flask_security/forms.py:62
+msgid "Send Login Link"
+msgstr "Envoyer le lien de connexion"
+
+#: flask_security/forms.py:63
+msgid "Verify Password"
+msgstr ""
+
+#: flask_security/forms.py:64
+msgid "Change Method"
+msgstr ""
+
+#: flask_security/forms.py:65
+msgid "Phone Number"
+msgstr ""
+
+#: flask_security/forms.py:66
+msgid "Authentication Code"
+msgstr ""
+
+#: flask_security/forms.py:67
+msgid "Submit"
+msgstr ""
+
+#: flask_security/forms.py:68
+msgid "Submit Code"
+msgstr ""
+
+#: flask_security/forms.py:69
+msgid "Error(s)"
+msgstr ""
+
+#: flask_security/forms.py:70
+msgid "Identity"
+msgstr ""
+
+#: flask_security/forms.py:71
+msgid "Send Code"
+msgstr ""
+
+#: flask_security/forms.py:72
+#, fuzzy
+msgid "Passcode"
+msgstr "Mot de passe"
+
+#: flask_security/unified_signin.py:145
+#, fuzzy
+msgid "Code or Password"
+msgstr "Récupérer le mot de passe"
+
+#: flask_security/unified_signin.py:150 flask_security/unified_signin.py:270
+msgid "Available Methods"
+msgstr ""
+
+#: flask_security/unified_signin.py:151
+msgid "Via email"
+msgstr ""
+
+#: flask_security/unified_signin.py:151
+msgid "Via SMS"
+msgstr ""
+
+#: flask_security/unified_signin.py:272
+msgid "Set up using email"
+msgstr ""
+
+#: flask_security/unified_signin.py:275
+msgid "Set up using an authenticator app (e.g. google, lastpass, authy)"
+msgstr ""
+
+#: flask_security/unified_signin.py:277
+msgid "Set up using SMS"
+msgstr ""
+
+#: flask_security/templates/security/_menu.html:2
+msgid "Menu"
+msgstr "Menu"
+
+#: flask_security/templates/security/_menu.html:8
+msgid "Unified Sign In"
+msgstr ""
+
+#: flask_security/templates/security/_menu.html:14
+msgid "Forgot password"
+msgstr "Mot de passe oublié"
+
+#: flask_security/templates/security/_menu.html:17
+msgid "Confirm account"
+msgstr "Confirmer le compte"
+
+#: flask_security/templates/security/change_password.html:6
+msgid "Change password"
+msgstr "Changer de mot de passe"
+
+#: flask_security/templates/security/forgot_password.html:6
+msgid "Send password reset instructions"
+msgstr "Envoyer les instructions de réinitialisation de mot de passe"
+
+#: flask_security/templates/security/reset_password.html:6
+msgid "Reset password"
+msgstr "Réinitialiser le mot de passe"
+
+#: flask_security/templates/security/send_confirmation.html:6
+msgid "Resend confirmation instructions"
+msgstr "Renvoyer les instructions de confirmation"
+
+#: flask_security/templates/security/two_factor_setup.html:6
+msgid "Two-factor authentication adds an extra layer of security to your account"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_setup.html:7
+msgid ""
+"In addition to your username and password, you'll need to use a code that"
+" we will send you"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_setup.html:18
+msgid "To complete logging in, please enter the code sent to your mail"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_setup.html:21
+#: flask_security/templates/security/us_setup.html:21
+msgid ""
+"Open your authenticator app on your device and scan the following qrcode "
+"to start receiving codes:"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_setup.html:22
+msgid "Two factor authentication code"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_setup.html:25
+msgid "To Which Phone Number Should We Send Code To?"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_verify_code.html:6
+msgid "Two-factor Authentication"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_verify_code.html:7
+msgid "Please enter your authentication code"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_verify_code.html:18
+msgid "The code for authentication was sent to your email address"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_verify_code.html:21
+msgid "A mail was sent to us in order to reset your application account"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_verify_password.html:6
+#: flask_security/templates/security/verify.html:6
+msgid "Please Enter Your Password"
+msgstr ""
+
+#: flask_security/templates/security/us_setup.html:6
+msgid "Setup Unified Sign In options"
+msgstr ""
+
+#: flask_security/templates/security/us_setup.html:22
+msgid "Passwordless QRCode"
+msgstr ""
+
+#: flask_security/templates/security/us_setup.html:25
+#: flask_security/templates/security/us_signin.html:23
+#: flask_security/templates/security/us_verify.html:21
+#, fuzzy
+msgid "Code has been sent"
+msgstr "Votre mot de passe a été réinitialisé"
+
+#: flask_security/templates/security/us_setup.html:29
+msgid "No methods have been enabled - nothing to setup"
+msgstr ""
+
+#: flask_security/templates/security/us_signin.html:15
+#: flask_security/templates/security/us_verify.html:13
+msgid "Request one-time code be sent"
+msgstr ""
+
+#: flask_security/templates/security/us_verify.html:6
+#, fuzzy
+msgid "Please re-authenticate"
+msgstr "Merci de vous reconnecter pour accéder à cette page."
+
+#: flask_security/templates/security/email/change_notice.html:1
+msgid "Your password has been changed."
+msgstr "Votre mot de passe a été changé."
+
+#: flask_security/templates/security/email/change_notice.html:3
+msgid "If you did not change your password,"
+msgstr "Si vous n'avez pas changé votre mot de passe,"
+
+#: flask_security/templates/security/email/change_notice.html:3
+msgid "click here to reset it"
+msgstr "cliquez ici pour le réinitialiser"
+
+#: flask_security/templates/security/email/confirmation_instructions.html:1
+msgid "Please confirm your email through the link below:"
+msgstr "Merci de confirmer votre adresse email via le lien ci-dessous&thinsp;:"
+
+#: flask_security/templates/security/email/confirmation_instructions.html:3
+#: flask_security/templates/security/email/welcome.html:6
+msgid "Confirm my account"
+msgstr "Confirmer mon compte"
+
+#: flask_security/templates/security/email/login_instructions.html:1
+#: flask_security/templates/security/email/welcome.html:1
+#, python-format
+msgid "Welcome %(email)s!"
+msgstr "Bienvenue %(email)s&thinsp;!"
+
+#: flask_security/templates/security/email/login_instructions.html:3
+msgid "You can log into your account through the link below:"
+msgstr "Vous pouvez vous connecter via le lien ci-dessous&thinsp;:"
+
+#: flask_security/templates/security/email/login_instructions.html:5
+msgid "Login now"
+msgstr "Se connecter maintenant"
+
+#: flask_security/templates/security/email/reset_instructions.html:1
+msgid "Click here to reset your password"
+msgstr "Cliquez pour réinitialiser votre mot de passe"
+
+#: flask_security/templates/security/email/two_factor_instructions.html:3
+msgid "You can log into your account using the following code:"
+msgstr ""
+
+#: flask_security/templates/security/email/two_factor_rescue.html:1
+msgid "can not access mail account"
+msgstr ""
+
+#: flask_security/templates/security/email/us_instructions.html:3
+#, fuzzy
+msgid "You can sign into your account using the following code:"
+msgstr "Vous pouvez vous connecter via le lien ci-dessous&thinsp;:"
+
+#: flask_security/templates/security/email/us_instructions.html:6
+#, fuzzy
+msgid "Or use the the link below:"
+msgstr ""
+"Vous pouvez confirmer votre votre adresse email via le lien ci-"
+"dessous&thinsp;:"
+
+#: flask_security/templates/security/email/welcome.html:4
+msgid "You can confirm your email through the link below:"
+msgstr ""
+"Vous pouvez confirmer votre votre adresse email via le lien ci-"
+"dessous&thinsp;:"
+
diff --git a/flask_security/translations/ja_JP/LC_MESSAGES/flask_security.mo b/flask_security/translations/ja_JP/LC_MESSAGES/flask_security.mo
new file mode 100644
index 0000000..950204e
--- /dev/null
+++ b/flask_security/translations/ja_JP/LC_MESSAGES/flask_security.mo
Binary files differ
diff --git a/flask_security/translations/ja_JP/LC_MESSAGES/flask_security.po b/flask_security/translations/ja_JP/LC_MESSAGES/flask_security.po
new file mode 100644
index 0000000..860e4f2
--- /dev/null
+++ b/flask_security/translations/ja_JP/LC_MESSAGES/flask_security.po
@@ -0,0 +1,616 @@
+# Japanese translations for Flask-Security.
+# Copyright (C) 2017 CERN
+# This file is distributed under the same license as the Flask-Security
+# project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2017.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Flask-Security 2.0.1\n"
+"Report-Msgid-Bugs-To: info@inveniosoftware.org\n"
+"POT-Creation-Date: 2020-04-19 13:18-0700\n"
+"PO-Revision-Date: 2018-01-25 14:12+0900\n"
+"Last-Translator: \n"
+"Language: ja\n"
+"Language-Team: \n"
+"Plural-Forms: nplurals=1; plural=0\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 2.8.0\n"
+
+#: flask_security/core.py:207
+msgid "Login Required"
+msgstr "ログインが必要です"
+
+#: flask_security/core.py:208
+#: flask_security/templates/security/email/two_factor_instructions.html:1
+#: flask_security/templates/security/email/us_instructions.html:1
+msgid "Welcome"
+msgstr "ようこそ"
+
+#: flask_security/core.py:209
+msgid "Please confirm your email"
+msgstr "メール アドレスの検証"
+
+#: flask_security/core.py:210
+msgid "Login instructions"
+msgstr "ログイン手順"
+
+#: flask_security/core.py:211
+#: flask_security/templates/security/email/reset_notice.html:1
+msgid "Your password has been reset"
+msgstr "パスワード変更"
+
+#: flask_security/core.py:212
+msgid "Your password has been changed"
+msgstr "パスワードが変更されました。"
+
+#: flask_security/core.py:213
+msgid "Password reset instructions"
+msgstr "パスワード再設定手順"
+
+#: flask_security/core.py:216
+msgid "Two-factor Login"
+msgstr ""
+
+#: flask_security/core.py:217
+msgid "Two-factor Rescue"
+msgstr ""
+
+#: flask_security/core.py:266
+msgid "Verification Code"
+msgstr ""
+
+#: flask_security/core.py:282
+msgid "Input not appropriate for requested API"
+msgstr ""
+
+#: flask_security/core.py:283
+msgid "You do not have permission to view this resource."
+msgstr "アクセス権がありません"
+
+#: flask_security/core.py:285
+msgid "You are not authenticated. Please supply the correct credentials."
+msgstr ""
+
+#: flask_security/core.py:289
+#, fuzzy
+msgid "You must re-authenticate to access this endpoint"
+msgstr "再度ログインしてください"
+
+#: flask_security/core.py:293
+#, python-format
+msgid "Thank you. Confirmation instructions have been sent to %(email)s."
+msgstr "ご登録ありがとうございます。%(email)sにメール アドレス検証手順が送信されました。"
+
+#: flask_security/core.py:296
+msgid "Thank you. Your email has been confirmed."
+msgstr "ありがとうございます。メール アドレスが検証されました。"
+
+#: flask_security/core.py:297
+msgid "Your email has already been confirmed."
+msgstr "メール アドレスは検証済みです"
+
+#: flask_security/core.py:298
+msgid "Invalid confirmation token."
+msgstr "リンクが無効です"
+
+#: flask_security/core.py:300
+#, python-format
+msgid "%(email)s is already associated with an account."
+msgstr "%(email)s のアカウントは既に作成されています"
+
+#: flask_security/core.py:303
+msgid "Password does not match"
+msgstr "パスワードが一致しません"
+
+#: flask_security/core.py:304
+msgid "Passwords do not match"
+msgstr "入力したパスワードが一致していません"
+
+#: flask_security/core.py:305
+msgid "Redirections outside the domain are forbidden"
+msgstr "ドメイン外へのリダイレクトは禁止されています"
+
+#: flask_security/core.py:307
+#, python-format
+msgid "Instructions to reset your password have been sent to %(email)s."
+msgstr "パスワードの再設定手順が %(email)s に送信されました"
+
+#: flask_security/core.py:311
+#, python-format
+msgid ""
+"You did not reset your password within %(within)s. New instructions have "
+"been sent to %(email)s."
+msgstr "%(within)s以内にパスワードを設定しませんでした。パスワード再設定手順を %(email)s に再度送信しました。"
+
+#: flask_security/core.py:317
+msgid "Invalid reset password token."
+msgstr "リンクが無効です"
+
+#: flask_security/core.py:318
+msgid "Email requires confirmation."
+msgstr "メール アドレスの検証が必要です"
+
+#: flask_security/core.py:320
+#, python-format
+msgid "Confirmation instructions have been sent to %(email)s."
+msgstr "%(email)sにメール アドレス検証手順が再送信されました"
+
+#: flask_security/core.py:324
+#, python-format
+msgid ""
+"You did not confirm your email within %(within)s. New instructions to "
+"confirm your email have been sent to %(email)s."
+msgstr "%(within)s以内にメール アドレスが検証されませんでした。新しい検証手順を %(email)s に送信しました。"
+
+#: flask_security/core.py:332
+#, python-format
+msgid ""
+"You did not login within %(within)s. New instructions to login have been "
+"sent to %(email)s."
+msgstr "%(within)s以内にログインしませんでした。ログイン手順を %(email)s に再度送信しました。"
+
+#: flask_security/core.py:339
+#, python-format
+msgid "Instructions to login have been sent to %(email)s."
+msgstr "%(email)sにログイン手順が送信されました"
+
+#: flask_security/core.py:342
+msgid "Invalid login token."
+msgstr "リンクが無効です"
+
+#: flask_security/core.py:343
+msgid "Account is disabled."
+msgstr "アカウントが無効になっています"
+
+#: flask_security/core.py:344
+msgid "Email not provided"
+msgstr "メール アドレスを入力してください"
+
+#: flask_security/core.py:345
+msgid "Invalid email address"
+msgstr "正しいメール アドレスを入力してください"
+
+#: flask_security/core.py:346
+#, fuzzy
+msgid "Invalid code"
+msgstr "入力を確認してください"
+
+#: flask_security/core.py:347
+msgid "Password not provided"
+msgstr "パスワードを入力してください"
+
+#: flask_security/core.py:348
+msgid "No password is set for this user"
+msgstr "パスワードが設定されていません"
+
+#: flask_security/core.py:350
+#, fuzzy, python-format
+msgid "Password must be at least %(length)s characters"
+msgstr "パスワードは6文字以上でなければなりません"
+
+#: flask_security/core.py:353
+msgid "Password not complex enough"
+msgstr ""
+
+#: flask_security/core.py:354
+msgid "Password on breached list"
+msgstr ""
+
+#: flask_security/core.py:356
+msgid "Failed to contact breached passwords site"
+msgstr ""
+
+#: flask_security/core.py:359
+msgid "Phone number not valid e.g. missing country code"
+msgstr ""
+
+#: flask_security/core.py:360
+msgid "Specified user does not exist"
+msgstr "入力を確認してください"
+
+#: flask_security/core.py:361
+msgid "Invalid password"
+msgstr "入力を確認してください"
+
+#: flask_security/core.py:362
+msgid "Password or code submitted is not valid"
+msgstr ""
+
+#: flask_security/core.py:363
+msgid "You have successfully logged in."
+msgstr "ログインしました"
+
+#: flask_security/core.py:364
+msgid "Forgot password?"
+msgstr "パスワードを忘れた場合"
+
+#: flask_security/core.py:366
+msgid ""
+"You successfully reset your password and you have been logged in "
+"automatically."
+msgstr "パスワードの再設定が完了しました。"
+
+#: flask_security/core.py:373
+msgid "Your new password must be different than your previous password."
+msgstr "新旧パスワードが同じです"
+
+#: flask_security/core.py:376
+msgid "You successfully changed your password."
+msgstr "パスワードが変更されました"
+
+#: flask_security/core.py:377
+msgid "Please log in to access this page."
+msgstr "ログインしてください"
+
+#: flask_security/core.py:378
+msgid "Please reauthenticate to access this page."
+msgstr "再度ログインしてください"
+
+#: flask_security/core.py:379
+msgid "Reauthentication successful"
+msgstr ""
+
+#: flask_security/core.py:381
+msgid "You can only access this endpoint when not logged in."
+msgstr ""
+
+#: flask_security/core.py:384
+msgid "Failed to send code. Please try again later"
+msgstr ""
+
+#: flask_security/core.py:385
+msgid "Invalid Token"
+msgstr ""
+
+#: flask_security/core.py:386
+msgid "Your token has been confirmed"
+msgstr ""
+
+#: flask_security/core.py:388
+msgid "You successfully changed your two-factor method."
+msgstr ""
+
+#: flask_security/core.py:392
+msgid "You successfully confirmed password"
+msgstr ""
+
+#: flask_security/core.py:396
+msgid "Password confirmation is needed in order to access page"
+msgstr ""
+
+#: flask_security/core.py:400
+msgid "You currently do not have permissions to access this page"
+msgstr ""
+
+#: flask_security/core.py:403
+msgid "Marked method is not valid"
+msgstr ""
+
+#: flask_security/core.py:405
+msgid "You successfully disabled two factor authorization."
+msgstr ""
+
+#: flask_security/core.py:408
+msgid "Requested method is not valid"
+msgstr ""
+
+#: flask_security/core.py:410
+#, python-format
+msgid "Setup must be completed within %(within)s. Please start over."
+msgstr ""
+
+#: flask_security/core.py:413
+msgid "Unified sign in setup successful"
+msgstr ""
+
+#: flask_security/core.py:414
+msgid "You must specify a valid identity to sign in"
+msgstr ""
+
+#: flask_security/core.py:415
+#, python-format
+msgid "Use this code to sign in: %(code)s."
+msgstr ""
+
+#: flask_security/forms.py:50
+msgid "Email Address"
+msgstr "メール アドレス"
+
+#: flask_security/forms.py:51
+msgid "Password"
+msgstr "パスワード"
+
+#: flask_security/forms.py:52
+msgid "Remember Me"
+msgstr "次回以降ログインを省略する"
+
+#: flask_security/forms.py:53 flask_security/templates/security/_menu.html:5
+#: flask_security/templates/security/login_user.html:6
+#: flask_security/templates/security/send_login.html:6
+msgid "Login"
+msgstr "ログイン"
+
+#: flask_security/forms.py:54
+#: flask_security/templates/security/email/us_instructions.html:8
+#: flask_security/templates/security/us_signin.html:6
+msgid "Sign In"
+msgstr ""
+
+#: flask_security/forms.py:55 flask_security/templates/security/_menu.html:11
+#: flask_security/templates/security/register_user.html:6
+msgid "Register"
+msgstr "ユーザ登録"
+
+#: flask_security/forms.py:56
+msgid "Resend Confirmation Instructions"
+msgstr "検証手順の再送信"
+
+#: flask_security/forms.py:57
+msgid "Recover Password"
+msgstr "再設定手順を送信"
+
+#: flask_security/forms.py:58
+msgid "Reset Password"
+msgstr "パスワード変更"
+
+#: flask_security/forms.py:59
+msgid "Retype Password"
+msgstr "パスワード再入力"
+
+#: flask_security/forms.py:60
+msgid "New Password"
+msgstr "新しいパスワード"
+
+#: flask_security/forms.py:61
+msgid "Change Password"
+msgstr "変更"
+
+#: flask_security/forms.py:62
+msgid "Send Login Link"
+msgstr "ログイン手順を送信"
+
+#: flask_security/forms.py:63
+msgid "Verify Password"
+msgstr ""
+
+#: flask_security/forms.py:64
+msgid "Change Method"
+msgstr ""
+
+#: flask_security/forms.py:65
+msgid "Phone Number"
+msgstr ""
+
+#: flask_security/forms.py:66
+msgid "Authentication Code"
+msgstr ""
+
+#: flask_security/forms.py:67
+msgid "Submit"
+msgstr ""
+
+#: flask_security/forms.py:68
+msgid "Submit Code"
+msgstr ""
+
+#: flask_security/forms.py:69
+msgid "Error(s)"
+msgstr ""
+
+#: flask_security/forms.py:70
+msgid "Identity"
+msgstr ""
+
+#: flask_security/forms.py:71
+msgid "Send Code"
+msgstr ""
+
+#: flask_security/forms.py:72
+#, fuzzy
+msgid "Passcode"
+msgstr "パスワード"
+
+#: flask_security/unified_signin.py:145
+#, fuzzy
+msgid "Code or Password"
+msgstr "再設定手順を送信"
+
+#: flask_security/unified_signin.py:150 flask_security/unified_signin.py:270
+msgid "Available Methods"
+msgstr ""
+
+#: flask_security/unified_signin.py:151
+msgid "Via email"
+msgstr ""
+
+#: flask_security/unified_signin.py:151
+msgid "Via SMS"
+msgstr ""
+
+#: flask_security/unified_signin.py:272
+msgid "Set up using email"
+msgstr ""
+
+#: flask_security/unified_signin.py:275
+msgid "Set up using an authenticator app (e.g. google, lastpass, authy)"
+msgstr ""
+
+#: flask_security/unified_signin.py:277
+msgid "Set up using SMS"
+msgstr ""
+
+#: flask_security/templates/security/_menu.html:2
+msgid "Menu"
+msgstr "メニュー"
+
+#: flask_security/templates/security/_menu.html:8
+msgid "Unified Sign In"
+msgstr ""
+
+#: flask_security/templates/security/_menu.html:14
+msgid "Forgot password"
+msgstr "パスワードを忘れた場合"
+
+#: flask_security/templates/security/_menu.html:17
+msgid "Confirm account"
+msgstr "メール アドレスの検証"
+
+#: flask_security/templates/security/change_password.html:6
+msgid "Change password"
+msgstr "パスワードの変更"
+
+#: flask_security/templates/security/forgot_password.html:6
+msgid "Send password reset instructions"
+msgstr "パスワード再設定手順の送信"
+
+#: flask_security/templates/security/reset_password.html:6
+msgid "Reset password"
+msgstr "パスワード再設定"
+
+#: flask_security/templates/security/send_confirmation.html:6
+msgid "Resend confirmation instructions"
+msgstr "検証手順の再送信"
+
+#: flask_security/templates/security/two_factor_setup.html:6
+msgid "Two-factor authentication adds an extra layer of security to your account"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_setup.html:7
+msgid ""
+"In addition to your username and password, you'll need to use a code that"
+" we will send you"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_setup.html:18
+msgid "To complete logging in, please enter the code sent to your mail"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_setup.html:21
+#: flask_security/templates/security/us_setup.html:21
+msgid ""
+"Open your authenticator app on your device and scan the following qrcode "
+"to start receiving codes:"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_setup.html:22
+msgid "Two factor authentication code"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_setup.html:25
+msgid "To Which Phone Number Should We Send Code To?"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_verify_code.html:6
+msgid "Two-factor Authentication"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_verify_code.html:7
+msgid "Please enter your authentication code"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_verify_code.html:18
+msgid "The code for authentication was sent to your email address"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_verify_code.html:21
+msgid "A mail was sent to us in order to reset your application account"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_verify_password.html:6
+#: flask_security/templates/security/verify.html:6
+msgid "Please Enter Your Password"
+msgstr ""
+
+#: flask_security/templates/security/us_setup.html:6
+msgid "Setup Unified Sign In options"
+msgstr ""
+
+#: flask_security/templates/security/us_setup.html:22
+msgid "Passwordless QRCode"
+msgstr ""
+
+#: flask_security/templates/security/us_setup.html:25
+#: flask_security/templates/security/us_signin.html:23
+#: flask_security/templates/security/us_verify.html:21
+#, fuzzy
+msgid "Code has been sent"
+msgstr "パスワード変更"
+
+#: flask_security/templates/security/us_setup.html:29
+msgid "No methods have been enabled - nothing to setup"
+msgstr ""
+
+#: flask_security/templates/security/us_signin.html:15
+#: flask_security/templates/security/us_verify.html:13
+msgid "Request one-time code be sent"
+msgstr ""
+
+#: flask_security/templates/security/us_verify.html:6
+#, fuzzy
+msgid "Please re-authenticate"
+msgstr "再度ログインしてください"
+
+#: flask_security/templates/security/email/change_notice.html:1
+msgid "Your password has been changed."
+msgstr "パスワードが変更されました。"
+
+#: flask_security/templates/security/email/change_notice.html:3
+msgid "If you did not change your password,"
+msgstr "パスワードを変更した覚えがない場合には、"
+
+#: flask_security/templates/security/email/change_notice.html:3
+msgid "click here to reset it"
+msgstr "このリンクを開いてください。"
+
+#: flask_security/templates/security/email/confirmation_instructions.html:1
+msgid "Please confirm your email through the link below:"
+msgstr "以下のリンクからメール アドレスを検証してください:"
+
+#: flask_security/templates/security/email/confirmation_instructions.html:3
+#: flask_security/templates/security/email/welcome.html:6
+msgid "Confirm my account"
+msgstr "メール アドレスの検証"
+
+#: flask_security/templates/security/email/login_instructions.html:1
+#: flask_security/templates/security/email/welcome.html:1
+#, python-format
+msgid "Welcome %(email)s!"
+msgstr "ようこそ %(email)s !"
+
+#: flask_security/templates/security/email/login_instructions.html:3
+msgid "You can log into your account through the link below:"
+msgstr "以下のリンクによりログインできます。"
+
+#: flask_security/templates/security/email/login_instructions.html:5
+msgid "Login now"
+msgstr "ログイン"
+
+#: flask_security/templates/security/email/reset_instructions.html:1
+msgid "Click here to reset your password"
+msgstr "パスワードを再設定するためにこのリンクを開いてください。"
+
+#: flask_security/templates/security/email/two_factor_instructions.html:3
+msgid "You can log into your account using the following code:"
+msgstr ""
+
+#: flask_security/templates/security/email/two_factor_rescue.html:1
+msgid "can not access mail account"
+msgstr ""
+
+#: flask_security/templates/security/email/us_instructions.html:3
+#, fuzzy
+msgid "You can sign into your account using the following code:"
+msgstr "以下のリンクによりログインできます。"
+
+#: flask_security/templates/security/email/us_instructions.html:6
+#, fuzzy
+msgid "Or use the the link below:"
+msgstr "以下のリンクによりメール アドレスを検証できます。"
+
+#: flask_security/templates/security/email/welcome.html:4
+msgid "You can confirm your email through the link below:"
+msgstr "以下のリンクによりメール アドレスを検証できます。"
+
diff --git a/flask_security/translations/nl_NL/LC_MESSAGES/flask_security.mo b/flask_security/translations/nl_NL/LC_MESSAGES/flask_security.mo
new file mode 100644
index 0000000..e186a7b
--- /dev/null
+++ b/flask_security/translations/nl_NL/LC_MESSAGES/flask_security.mo
Binary files differ
diff --git a/flask_security/translations/nl_NL/LC_MESSAGES/flask_security.po b/flask_security/translations/nl_NL/LC_MESSAGES/flask_security.po
new file mode 100644
index 0000000..063524f
--- /dev/null
+++ b/flask_security/translations/nl_NL/LC_MESSAGES/flask_security.po
@@ -0,0 +1,638 @@
+# Dutch (Netherlands) translations for Flask-Security.
+# Copyright (C) 2017 CERN
+# This file is distributed under the same license as the Flask-Security
+# project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2017.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Flask-Security 2.0.1\n"
+"Report-Msgid-Bugs-To: info@inveniosoftware.org\n"
+"POT-Creation-Date: 2020-04-19 13:18-0700\n"
+"PO-Revision-Date: 2017-05-01 17:52+0200\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language: nl_NL\n"
+"Language-Team: nl_NL <LL@li.org>\n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 2.8.0\n"
+
+#: flask_security/core.py:207
+msgid "Login Required"
+msgstr "Inloggen Verplicht"
+
+#: flask_security/core.py:208
+#: flask_security/templates/security/email/two_factor_instructions.html:1
+#: flask_security/templates/security/email/us_instructions.html:1
+msgid "Welcome"
+msgstr "Welkom"
+
+#: flask_security/core.py:209
+msgid "Please confirm your email"
+msgstr "Gelieve uw e-mailadres te bevestigen"
+
+#: flask_security/core.py:210
+msgid "Login instructions"
+msgstr "Aanmeld instructies"
+
+#: flask_security/core.py:211
+#: flask_security/templates/security/email/reset_notice.html:1
+msgid "Your password has been reset"
+msgstr "Uw wachtwoord werd gereset"
+
+#: flask_security/core.py:212
+msgid "Your password has been changed"
+msgstr "Uw wachtwoord werd gewijzigd"
+
+#: flask_security/core.py:213
+msgid "Password reset instructions"
+msgstr "Wachtwoord reset instructies"
+
+#: flask_security/core.py:216
+msgid "Two-factor Login"
+msgstr "Dubbele Authenticatie Aanmelding"
+
+#: flask_security/core.py:217
+msgid "Two-factor Rescue"
+msgstr "Dubbele Authenticatie Herstellen"
+
+#: flask_security/core.py:266
+#, fuzzy
+msgid "Verification Code"
+msgstr "Authenticatie Code"
+
+#: flask_security/core.py:282
+msgid "Input not appropriate for requested API"
+msgstr ""
+
+#: flask_security/core.py:283
+msgid "You do not have permission to view this resource."
+msgstr "U heeft niet de nodige rechten om deze pagina te zien."
+
+#: flask_security/core.py:285
+msgid "You are not authenticated. Please supply the correct credentials."
+msgstr "U bent niet aangemeld. Voer alstublieft de juiste gegevens in."
+
+#: flask_security/core.py:289
+#, fuzzy
+msgid "You must re-authenticate to access this endpoint"
+msgstr "Gelieve opnieuw in te loggen om deze pagina te zien."
+
+#: flask_security/core.py:293
+#, python-format
+msgid "Thank you. Confirmation instructions have been sent to %(email)s."
+msgstr "Bedankt. Instructies voor bevestiging zijn verzonden naar %(email)s."
+
+#: flask_security/core.py:296
+msgid "Thank you. Your email has been confirmed."
+msgstr "Bedankt. Uw e-mailadres werd bevestigd."
+
+#: flask_security/core.py:297
+msgid "Your email has already been confirmed."
+msgstr "Uw e-mailadres werd reeds bevestigd."
+
+#: flask_security/core.py:298
+msgid "Invalid confirmation token."
+msgstr "Ongeldige bevestiging token."
+
+#: flask_security/core.py:300
+#, python-format
+msgid "%(email)s is already associated with an account."
+msgstr "%(email)s is al gelinkt aan een ander account."
+
+#: flask_security/core.py:303
+msgid "Password does not match"
+msgstr "Wachtwoord komt niet overeen"
+
+#: flask_security/core.py:304
+msgid "Passwords do not match"
+msgstr "Wachtwoorden komen niet overeen"
+
+#: flask_security/core.py:305
+msgid "Redirections outside the domain are forbidden"
+msgstr "Omleidingen buiten het domein zijn niet toegelaten"
+
+#: flask_security/core.py:307
+#, python-format
+msgid "Instructions to reset your password have been sent to %(email)s."
+msgstr "Instructies om uw wachtwoord te resetten werden verzonden naar %(email)s."
+
+#: flask_security/core.py:311
+#, python-format
+msgid ""
+"You did not reset your password within %(within)s. New instructions have "
+"been sent to %(email)s."
+msgstr ""
+"U heeft uw wachtwoord niet gereset gedurende %(within)s. Nieuwe "
+"instructies werden verzonden naar %(email)s."
+
+#: flask_security/core.py:317
+msgid "Invalid reset password token."
+msgstr "Ongeldig wachtwoord reset token."
+
+#: flask_security/core.py:318
+msgid "Email requires confirmation."
+msgstr "E-mailadres moet bevestigd worden."
+
+#: flask_security/core.py:320
+#, python-format
+msgid "Confirmation instructions have been sent to %(email)s."
+msgstr ""
+"Instructies ter bevestiging van uw e-mailadres werden verzonden naar "
+"%(email)s."
+
+#: flask_security/core.py:324
+#, python-format
+msgid ""
+"You did not confirm your email within %(within)s. New instructions to "
+"confirm your email have been sent to %(email)s."
+msgstr ""
+"U heeft uw e-mailadres niet bevestigd in de voorziene %(within)s. Nieuwe "
+"instructies ter bevestiging van uw e-mailadres werden verzonden naar "
+"%(email)s."
+
+#: flask_security/core.py:332
+#, python-format
+msgid ""
+"You did not login within %(within)s. New instructions to login have been "
+"sent to %(email)s."
+msgstr ""
+"Je bent niet ingelogd geweest gedurende %(within)s. Nieuwe instructies om"
+" in te loggen werden verzonden naar%(email)s."
+
+#: flask_security/core.py:339
+#, python-format
+msgid "Instructions to login have been sent to %(email)s."
+msgstr "Instructies om in te loggen werden verzonden naar %(email)s."
+
+#: flask_security/core.py:342
+msgid "Invalid login token."
+msgstr "Ongeldige aanmelding."
+
+#: flask_security/core.py:343
+msgid "Account is disabled."
+msgstr "Account is geblokkeerd."
+
+#: flask_security/core.py:344
+msgid "Email not provided"
+msgstr "Email niet ingevuld"
+
+#: flask_security/core.py:345
+msgid "Invalid email address"
+msgstr "Ongeldig e-mailadres"
+
+#: flask_security/core.py:346
+#, fuzzy
+msgid "Invalid code"
+msgstr "Niet valide token"
+
+#: flask_security/core.py:347
+msgid "Password not provided"
+msgstr "Wachtwoord niet ingevuld"
+
+#: flask_security/core.py:348
+msgid "No password is set for this user"
+msgstr "Er is geen wachtwoord gezet voor deze gebruiker"
+
+#: flask_security/core.py:350
+#, fuzzy, python-format
+msgid "Password must be at least %(length)s characters"
+msgstr "Uw wachtwoord moet minstens 6 karakters bevatten"
+
+#: flask_security/core.py:353
+msgid "Password not complex enough"
+msgstr ""
+
+#: flask_security/core.py:354
+msgid "Password on breached list"
+msgstr ""
+
+#: flask_security/core.py:356
+msgid "Failed to contact breached passwords site"
+msgstr ""
+
+#: flask_security/core.py:359
+msgid "Phone number not valid e.g. missing country code"
+msgstr ""
+
+#: flask_security/core.py:360
+msgid "Specified user does not exist"
+msgstr "Deze gebruiker bestaat niet"
+
+#: flask_security/core.py:361
+msgid "Invalid password"
+msgstr "Ongeldig wachtwoord"
+
+#: flask_security/core.py:362
+#, fuzzy
+msgid "Password or code submitted is not valid"
+msgstr "De gemarkeerde methode is niet valide"
+
+#: flask_security/core.py:363
+msgid "You have successfully logged in."
+msgstr "U bent succesvol ingelogd."
+
+#: flask_security/core.py:364
+msgid "Forgot password?"
+msgstr "Wachtwoord vergeten?"
+
+#: flask_security/core.py:366
+msgid ""
+"You successfully reset your password and you have been logged in "
+"automatically."
+msgstr "U heeft uw wachtwoord succesvol gereset en bent nu automatisch ingelogd."
+
+#: flask_security/core.py:373
+msgid "Your new password must be different than your previous password."
+msgstr "Uw nieuw wachtwoord moet verschillend zijn van het voorgaande wachtwoord."
+
+#: flask_security/core.py:376
+msgid "You successfully changed your password."
+msgstr "Uw wachtwoord werd met succes gewijzigd."
+
+#: flask_security/core.py:377
+msgid "Please log in to access this page."
+msgstr "Gelieve in te loggen om deze pagina te zien."
+
+#: flask_security/core.py:378
+msgid "Please reauthenticate to access this page."
+msgstr "Gelieve opnieuw in te loggen om deze pagina te zien."
+
+#: flask_security/core.py:379
+#, fuzzy
+msgid "Reauthentication successful"
+msgstr "Authenticatie Code"
+
+#: flask_security/core.py:381
+msgid "You can only access this endpoint when not logged in."
+msgstr ""
+
+#: flask_security/core.py:384
+msgid "Failed to send code. Please try again later"
+msgstr ""
+
+#: flask_security/core.py:385
+msgid "Invalid Token"
+msgstr "Niet valide token"
+
+#: flask_security/core.py:386
+msgid "Your token has been confirmed"
+msgstr "Uw token is bevestigd"
+
+#: flask_security/core.py:388
+msgid "You successfully changed your two-factor method."
+msgstr "U heeft succesvol uw Dubbele Authenticatie methode veranderd."
+
+#: flask_security/core.py:392
+msgid "You successfully confirmed password"
+msgstr "U heeft succesvol uw wachtwoord aangepast"
+
+#: flask_security/core.py:396
+msgid "Password confirmation is needed in order to access page"
+msgstr "Wachtwoord bevestiging is nodig voor we deze pagina kunnen laten zien"
+
+#: flask_security/core.py:400
+msgid "You currently do not have permissions to access this page"
+msgstr "U heeft niet de juiste permissies om deze pagina te laden"
+
+#: flask_security/core.py:403
+msgid "Marked method is not valid"
+msgstr "De gemarkeerde methode is niet valide"
+
+#: flask_security/core.py:405
+msgid "You successfully disabled two factor authorization."
+msgstr "U heeft succesvol Dubbele Authenticatie uitgeschakeld."
+
+#: flask_security/core.py:408
+#, fuzzy
+msgid "Requested method is not valid"
+msgstr "De gemarkeerde methode is niet valide"
+
+#: flask_security/core.py:410
+#, python-format
+msgid "Setup must be completed within %(within)s. Please start over."
+msgstr ""
+
+#: flask_security/core.py:413
+msgid "Unified sign in setup successful"
+msgstr ""
+
+#: flask_security/core.py:414
+msgid "You must specify a valid identity to sign in"
+msgstr ""
+
+#: flask_security/core.py:415
+#, python-format
+msgid "Use this code to sign in: %(code)s."
+msgstr ""
+
+#: flask_security/forms.py:50
+msgid "Email Address"
+msgstr "E-mailadres"
+
+#: flask_security/forms.py:51
+msgid "Password"
+msgstr "wachtwoord"
+
+#: flask_security/forms.py:52
+msgid "Remember Me"
+msgstr "Ingelogd blijven"
+
+#: flask_security/forms.py:53 flask_security/templates/security/_menu.html:5
+#: flask_security/templates/security/login_user.html:6
+#: flask_security/templates/security/send_login.html:6
+msgid "Login"
+msgstr "Aanmelden"
+
+#: flask_security/forms.py:54
+#: flask_security/templates/security/email/us_instructions.html:8
+#: flask_security/templates/security/us_signin.html:6
+msgid "Sign In"
+msgstr ""
+
+#: flask_security/forms.py:55 flask_security/templates/security/_menu.html:11
+#: flask_security/templates/security/register_user.html:6
+msgid "Register"
+msgstr "Registreer"
+
+#: flask_security/forms.py:56
+msgid "Resend Confirmation Instructions"
+msgstr "Verzend instructies om te bevestigen opnieuw"
+
+#: flask_security/forms.py:57
+msgid "Recover Password"
+msgstr "Herstel wachtwoord"
+
+#: flask_security/forms.py:58
+msgid "Reset Password"
+msgstr "reset wachtwoord"
+
+#: flask_security/forms.py:59
+msgid "Retype Password"
+msgstr "Type wachtwoord opnieuw"
+
+#: flask_security/forms.py:60
+msgid "New Password"
+msgstr "Nieuw wachtwoord"
+
+#: flask_security/forms.py:61
+msgid "Change Password"
+msgstr "Verander wachtwoord"
+
+#: flask_security/forms.py:62
+msgid "Send Login Link"
+msgstr "Verzend aanmeld link"
+
+#: flask_security/forms.py:63
+msgid "Verify Password"
+msgstr "Wachtwoord Verificatie"
+
+#: flask_security/forms.py:64
+msgid "Change Method"
+msgstr "Verander Methode"
+
+#: flask_security/forms.py:65
+msgid "Phone Number"
+msgstr "Telefoonnummer"
+
+#: flask_security/forms.py:66
+msgid "Authentication Code"
+msgstr "Authenticatie Code"
+
+#: flask_security/forms.py:67
+msgid "Submit"
+msgstr ""
+
+#: flask_security/forms.py:68
+msgid "Submit Code"
+msgstr ""
+
+#: flask_security/forms.py:69
+msgid "Error(s)"
+msgstr ""
+
+#: flask_security/forms.py:70
+msgid "Identity"
+msgstr ""
+
+#: flask_security/forms.py:71
+msgid "Send Code"
+msgstr ""
+
+#: flask_security/forms.py:72
+#, fuzzy
+msgid "Passcode"
+msgstr "wachtwoord"
+
+#: flask_security/unified_signin.py:145
+#, fuzzy
+msgid "Code or Password"
+msgstr "Herstel wachtwoord"
+
+#: flask_security/unified_signin.py:150 flask_security/unified_signin.py:270
+msgid "Available Methods"
+msgstr ""
+
+#: flask_security/unified_signin.py:151
+msgid "Via email"
+msgstr ""
+
+#: flask_security/unified_signin.py:151
+msgid "Via SMS"
+msgstr ""
+
+#: flask_security/unified_signin.py:272
+msgid "Set up using email"
+msgstr ""
+
+#: flask_security/unified_signin.py:275
+msgid "Set up using an authenticator app (e.g. google, lastpass, authy)"
+msgstr ""
+
+#: flask_security/unified_signin.py:277
+msgid "Set up using SMS"
+msgstr ""
+
+#: flask_security/templates/security/_menu.html:2
+msgid "Menu"
+msgstr "Menu"
+
+#: flask_security/templates/security/_menu.html:8
+msgid "Unified Sign In"
+msgstr ""
+
+#: flask_security/templates/security/_menu.html:14
+msgid "Forgot password"
+msgstr "Wachtwoord vergeten"
+
+#: flask_security/templates/security/_menu.html:17
+msgid "Confirm account"
+msgstr "Bevestig account"
+
+#: flask_security/templates/security/change_password.html:6
+msgid "Change password"
+msgstr "Verander wachtwoord"
+
+#: flask_security/templates/security/forgot_password.html:6
+msgid "Send password reset instructions"
+msgstr "Verzend wachtwoord reset instructies"
+
+#: flask_security/templates/security/reset_password.html:6
+msgid "Reset password"
+msgstr "Reset wachtwoord"
+
+#: flask_security/templates/security/send_confirmation.html:6
+msgid "Resend confirmation instructions"
+msgstr "Verzend bevestiging instructies opnieuw"
+
+#: flask_security/templates/security/two_factor_setup.html:6
+msgid "Two-factor authentication adds an extra layer of security to your account"
+msgstr ""
+"Dubbele Authenticatie voegt een extra laag van beveiliging toe aan uw "
+"account"
+
+#: flask_security/templates/security/two_factor_setup.html:7
+msgid ""
+"In addition to your username and password, you'll need to use a code that"
+" we will send you"
+msgstr ""
+"Naast uw gebruikersnaam en wachtwoord, heeft u ook een code nodig dat we "
+"u zullen toezenden"
+
+#: flask_security/templates/security/two_factor_setup.html:18
+msgid "To complete logging in, please enter the code sent to your mail"
+msgstr ""
+"Om verder in te loggen moet U de code die we naar uw e-mail hebben "
+"gezonden invoeren"
+
+#: flask_security/templates/security/two_factor_setup.html:21
+#: flask_security/templates/security/us_setup.html:21
+#, fuzzy
+msgid ""
+"Open your authenticator app on your device and scan the following qrcode "
+"to start receiving codes:"
+msgstr ""
+"Open Google Authenticator op uw toestel en scan de volgende qrcode om "
+"codes te kunnen ontvangen:"
+
+#: flask_security/templates/security/two_factor_setup.html:22
+msgid "Two factor authentication code"
+msgstr "Dubbele Authenticatie code"
+
+#: flask_security/templates/security/two_factor_setup.html:25
+msgid "To Which Phone Number Should We Send Code To?"
+msgstr "Naar welk telefoonnummer kunnen we code verzenden?"
+
+#: flask_security/templates/security/two_factor_verify_code.html:6
+msgid "Two-factor Authentication"
+msgstr "Dubbele Authenticatie"
+
+#: flask_security/templates/security/two_factor_verify_code.html:7
+msgid "Please enter your authentication code"
+msgstr "Voer uw authenticatie code in"
+
+#: flask_security/templates/security/two_factor_verify_code.html:18
+msgid "The code for authentication was sent to your email address"
+msgstr "The code voor authenticatie is naar uw e-mail adres verzonden"
+
+#: flask_security/templates/security/two_factor_verify_code.html:21
+msgid "A mail was sent to us in order to reset your application account"
+msgstr "Een bericht is naar uw e-mail adres verzonden om uw account te herstellen"
+
+#: flask_security/templates/security/two_factor_verify_password.html:6
+#: flask_security/templates/security/verify.html:6
+msgid "Please Enter Your Password"
+msgstr "Voer uw wachtwoord in"
+
+#: flask_security/templates/security/us_setup.html:6
+msgid "Setup Unified Sign In options"
+msgstr ""
+
+#: flask_security/templates/security/us_setup.html:22
+msgid "Passwordless QRCode"
+msgstr ""
+
+#: flask_security/templates/security/us_setup.html:25
+#: flask_security/templates/security/us_signin.html:23
+#: flask_security/templates/security/us_verify.html:21
+#, fuzzy
+msgid "Code has been sent"
+msgstr "Uw wachtwoord werd gereset"
+
+#: flask_security/templates/security/us_setup.html:29
+msgid "No methods have been enabled - nothing to setup"
+msgstr ""
+
+#: flask_security/templates/security/us_signin.html:15
+#: flask_security/templates/security/us_verify.html:13
+msgid "Request one-time code be sent"
+msgstr ""
+
+#: flask_security/templates/security/us_verify.html:6
+#, fuzzy
+msgid "Please re-authenticate"
+msgstr "Voer uw authenticatie code in"
+
+#: flask_security/templates/security/email/change_notice.html:1
+msgid "Your password has been changed."
+msgstr "Uw wachtwoord werd gewijzigd."
+
+#: flask_security/templates/security/email/change_notice.html:3
+msgid "If you did not change your password,"
+msgstr "Als u uw wachtwoord niet hebt aangepast,"
+
+#: flask_security/templates/security/email/change_notice.html:3
+msgid "click here to reset it"
+msgstr "Klik hier om het te resetten"
+
+#: flask_security/templates/security/email/confirmation_instructions.html:1
+msgid "Please confirm your email through the link below:"
+msgstr "Gelieve uw e-mailadres te bevestigen via onderstaande link:"
+
+#: flask_security/templates/security/email/confirmation_instructions.html:3
+#: flask_security/templates/security/email/welcome.html:6
+msgid "Confirm my account"
+msgstr "Bevestig mijn account"
+
+#: flask_security/templates/security/email/login_instructions.html:1
+#: flask_security/templates/security/email/welcome.html:1
+#, python-format
+msgid "Welcome %(email)s!"
+msgstr "Welkom, %(email)s!"
+
+#: flask_security/templates/security/email/login_instructions.html:3
+msgid "You can log into your account through the link below:"
+msgstr "U kunt inloggen door onderstaande link te gebruiken:"
+
+#: flask_security/templates/security/email/login_instructions.html:5
+msgid "Login now"
+msgstr "Nu inloggen"
+
+#: flask_security/templates/security/email/reset_instructions.html:1
+msgid "Click here to reset your password"
+msgstr "Klik hier om uw wachtwoord te resetten"
+
+#: flask_security/templates/security/email/two_factor_instructions.html:3
+msgid "You can log into your account using the following code:"
+msgstr "U kunt inloggen door de volgende code te gebruiken:"
+
+#: flask_security/templates/security/email/two_factor_rescue.html:1
+msgid "can not access mail account"
+msgstr "kan niet in het e-mail account"
+
+#: flask_security/templates/security/email/us_instructions.html:3
+#, fuzzy
+msgid "You can sign into your account using the following code:"
+msgstr "U kunt inloggen door de volgende code te gebruiken:"
+
+#: flask_security/templates/security/email/us_instructions.html:6
+#, fuzzy
+msgid "Or use the the link below:"
+msgstr "U kan uw e-mailadres bevestigen via de onderstaande link:"
+
+#: flask_security/templates/security/email/welcome.html:4
+msgid "You can confirm your email through the link below:"
+msgstr "U kan uw e-mailadres bevestigen via de onderstaande link:"
+
diff --git a/flask_security/translations/pt_BR/LC_MESSAGES/flask_security.mo b/flask_security/translations/pt_BR/LC_MESSAGES/flask_security.mo
new file mode 100644
index 0000000..cf95485
--- /dev/null
+++ b/flask_security/translations/pt_BR/LC_MESSAGES/flask_security.mo
Binary files differ
diff --git a/flask_security/translations/pt_BR/LC_MESSAGES/flask_security.po b/flask_security/translations/pt_BR/LC_MESSAGES/flask_security.po
new file mode 100644
index 0000000..21a39f9
--- /dev/null
+++ b/flask_security/translations/pt_BR/LC_MESSAGES/flask_security.po
@@ -0,0 +1,622 @@
+# Portuguese (Brazil) translations for Flask-Security.
+# Copyright (C) 2017 CERN
+# This file is distributed under the same license as the Flask-Security
+# project.
+# José Neto <josenetodino@gmail.com>, 2017.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Flask-Security 2.0.1\n"
+"Report-Msgid-Bugs-To: info@inveniosoftware.org\n"
+"POT-Creation-Date: 2020-04-19 13:18-0700\n"
+"PO-Revision-Date: 2017-09-27 23:39-0300\n"
+"Last-Translator: José Neto <josenetodino@gmail.com>\n"
+"Language: pt_BR\n"
+"Language-Team: \n"
+"Plural-Forms: nplurals=2; plural=(n > 1)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 2.8.0\n"
+
+#: flask_security/core.py:207
+msgid "Login Required"
+msgstr "Login obrigatório"
+
+#: flask_security/core.py:208
+#: flask_security/templates/security/email/two_factor_instructions.html:1
+#: flask_security/templates/security/email/us_instructions.html:1
+msgid "Welcome"
+msgstr "Bem-vindo"
+
+#: flask_security/core.py:209
+msgid "Please confirm your email"
+msgstr "Por favor, confirme seu email"
+
+#: flask_security/core.py:210
+msgid "Login instructions"
+msgstr "Instruções de login"
+
+#: flask_security/core.py:211
+#: flask_security/templates/security/email/reset_notice.html:1
+msgid "Your password has been reset"
+msgstr "Sua senha foi redefinida"
+
+#: flask_security/core.py:212
+msgid "Your password has been changed"
+msgstr "Sua senha foi alterada"
+
+#: flask_security/core.py:213
+msgid "Password reset instructions"
+msgstr "Instruções para redfinir a senha"
+
+#: flask_security/core.py:216
+msgid "Two-factor Login"
+msgstr ""
+
+#: flask_security/core.py:217
+msgid "Two-factor Rescue"
+msgstr ""
+
+#: flask_security/core.py:266
+msgid "Verification Code"
+msgstr ""
+
+#: flask_security/core.py:282
+msgid "Input not appropriate for requested API"
+msgstr ""
+
+#: flask_security/core.py:283
+msgid "You do not have permission to view this resource."
+msgstr "Você não tem permissão para ver este recurso"
+
+#: flask_security/core.py:285
+msgid "You are not authenticated. Please supply the correct credentials."
+msgstr ""
+
+#: flask_security/core.py:289
+#, fuzzy
+msgid "You must re-authenticate to access this endpoint"
+msgstr "Por favor, reautentique-se para acessar esta página."
+
+#: flask_security/core.py:293
+#, python-format
+msgid "Thank you. Confirmation instructions have been sent to %(email)s."
+msgstr "Obrigado. As instruções para a confirmação foram enviadas para %(email)s."
+
+#: flask_security/core.py:296
+msgid "Thank you. Your email has been confirmed."
+msgstr "Obrigado. Seu email foi confirmado."
+
+#: flask_security/core.py:297
+msgid "Your email has already been confirmed."
+msgstr "Seu email já foi confirmado."
+
+#: flask_security/core.py:298
+msgid "Invalid confirmation token."
+msgstr "Token de confirmação inválido."
+
+#: flask_security/core.py:300
+#, python-format
+msgid "%(email)s is already associated with an account."
+msgstr "%(email)s já está associado a uma conta."
+
+#: flask_security/core.py:303
+msgid "Password does not match"
+msgstr "Senha não confere"
+
+#: flask_security/core.py:304
+msgid "Passwords do not match"
+msgstr "Senhas não conferem"
+
+#: flask_security/core.py:305
+msgid "Redirections outside the domain are forbidden"
+msgstr "Redirecionamentos para fora do domínio são proibidos"
+
+#: flask_security/core.py:307
+#, python-format
+msgid "Instructions to reset your password have been sent to %(email)s."
+msgstr "As instruções para redefinir sua senha foram enviadas para %(email)s."
+
+#: flask_security/core.py:311
+#, python-format
+msgid ""
+"You did not reset your password within %(within)s. New instructions have "
+"been sent to %(email)s."
+msgstr ""
+"Você não redefiniu sua senha dentro de %(within)s. Novas instruções foram"
+" enviadas para %(email)s."
+
+#: flask_security/core.py:317
+msgid "Invalid reset password token."
+msgstr "Token de redefinição de senha inválido."
+
+#: flask_security/core.py:318
+msgid "Email requires confirmation."
+msgstr "O email requer confirmação."
+
+#: flask_security/core.py:320
+#, python-format
+msgid "Confirmation instructions have been sent to %(email)s."
+msgstr "As instruções de confirmaç foram enviadas para %(email)s."
+
+#: flask_security/core.py:324
+#, python-format
+msgid ""
+"You did not confirm your email within %(within)s. New instructions to "
+"confirm your email have been sent to %(email)s."
+msgstr ""
+"Você não confirmou seu email dentro de %(within)s. Novas instruções foram"
+" enviadas para %(email)s."
+
+#: flask_security/core.py:332
+#, python-format
+msgid ""
+"You did not login within %(within)s. New instructions to login have been "
+"sent to %(email)s."
+msgstr ""
+"Você não logou dentro de %(within)s. Novas instruções para logar foram "
+"enviadas para %(email)s."
+
+#: flask_security/core.py:339
+#, python-format
+msgid "Instructions to login have been sent to %(email)s."
+msgstr "Instruções para logar foram enviadas para %(email)s."
+
+#: flask_security/core.py:342
+msgid "Invalid login token."
+msgstr "Token de login inválido."
+
+#: flask_security/core.py:343
+msgid "Account is disabled."
+msgstr "Conta desabilitada."
+
+#: flask_security/core.py:344
+msgid "Email not provided"
+msgstr "Email não informado"
+
+#: flask_security/core.py:345
+msgid "Invalid email address"
+msgstr "Endereço de email inválido"
+
+#: flask_security/core.py:346
+#, fuzzy
+msgid "Invalid code"
+msgstr "Senha inválida"
+
+#: flask_security/core.py:347
+msgid "Password not provided"
+msgstr "Senha não informada"
+
+#: flask_security/core.py:348
+msgid "No password is set for this user"
+msgstr "Nenhuma senha definida para este usuário"
+
+#: flask_security/core.py:350
+#, fuzzy, python-format
+msgid "Password must be at least %(length)s characters"
+msgstr "A senha deve ter pelo menos 6 caracteres"
+
+#: flask_security/core.py:353
+msgid "Password not complex enough"
+msgstr ""
+
+#: flask_security/core.py:354
+msgid "Password on breached list"
+msgstr ""
+
+#: flask_security/core.py:356
+msgid "Failed to contact breached passwords site"
+msgstr ""
+
+#: flask_security/core.py:359
+msgid "Phone number not valid e.g. missing country code"
+msgstr ""
+
+#: flask_security/core.py:360
+msgid "Specified user does not exist"
+msgstr "Usuário não existe"
+
+#: flask_security/core.py:361
+msgid "Invalid password"
+msgstr "Senha inválida"
+
+#: flask_security/core.py:362
+msgid "Password or code submitted is not valid"
+msgstr ""
+
+#: flask_security/core.py:363
+msgid "You have successfully logged in."
+msgstr "Você logou com sucesso."
+
+#: flask_security/core.py:364
+msgid "Forgot password?"
+msgstr "Esqueceu a senha?"
+
+#: flask_security/core.py:366
+msgid ""
+"You successfully reset your password and you have been logged in "
+"automatically."
+msgstr "Você redefiniu sua senha com sucesso e foi logado automaticamente."
+
+#: flask_security/core.py:373
+msgid "Your new password must be different than your previous password."
+msgstr "Sua nova senha deve ser diferente da sua senha anterior."
+
+#: flask_security/core.py:376
+msgid "You successfully changed your password."
+msgstr "Você alterou sua senha com sucesso."
+
+#: flask_security/core.py:377
+msgid "Please log in to access this page."
+msgstr "Por favor, logue para acessar esta página."
+
+#: flask_security/core.py:378
+msgid "Please reauthenticate to access this page."
+msgstr "Por favor, reautentique-se para acessar esta página."
+
+#: flask_security/core.py:379
+msgid "Reauthentication successful"
+msgstr ""
+
+#: flask_security/core.py:381
+msgid "You can only access this endpoint when not logged in."
+msgstr ""
+
+#: flask_security/core.py:384
+msgid "Failed to send code. Please try again later"
+msgstr ""
+
+#: flask_security/core.py:385
+msgid "Invalid Token"
+msgstr ""
+
+#: flask_security/core.py:386
+msgid "Your token has been confirmed"
+msgstr ""
+
+#: flask_security/core.py:388
+msgid "You successfully changed your two-factor method."
+msgstr ""
+
+#: flask_security/core.py:392
+msgid "You successfully confirmed password"
+msgstr ""
+
+#: flask_security/core.py:396
+msgid "Password confirmation is needed in order to access page"
+msgstr ""
+
+#: flask_security/core.py:400
+msgid "You currently do not have permissions to access this page"
+msgstr ""
+
+#: flask_security/core.py:403
+msgid "Marked method is not valid"
+msgstr ""
+
+#: flask_security/core.py:405
+msgid "You successfully disabled two factor authorization."
+msgstr ""
+
+#: flask_security/core.py:408
+msgid "Requested method is not valid"
+msgstr ""
+
+#: flask_security/core.py:410
+#, python-format
+msgid "Setup must be completed within %(within)s. Please start over."
+msgstr ""
+
+#: flask_security/core.py:413
+msgid "Unified sign in setup successful"
+msgstr ""
+
+#: flask_security/core.py:414
+msgid "You must specify a valid identity to sign in"
+msgstr ""
+
+#: flask_security/core.py:415
+#, python-format
+msgid "Use this code to sign in: %(code)s."
+msgstr ""
+
+#: flask_security/forms.py:50
+msgid "Email Address"
+msgstr "Endereço de email"
+
+#: flask_security/forms.py:51
+msgid "Password"
+msgstr "Senha"
+
+#: flask_security/forms.py:52
+msgid "Remember Me"
+msgstr "Lembre de mim"
+
+#: flask_security/forms.py:53 flask_security/templates/security/_menu.html:5
+#: flask_security/templates/security/login_user.html:6
+#: flask_security/templates/security/send_login.html:6
+msgid "Login"
+msgstr "Login"
+
+#: flask_security/forms.py:54
+#: flask_security/templates/security/email/us_instructions.html:8
+#: flask_security/templates/security/us_signin.html:6
+msgid "Sign In"
+msgstr ""
+
+#: flask_security/forms.py:55 flask_security/templates/security/_menu.html:11
+#: flask_security/templates/security/register_user.html:6
+msgid "Register"
+msgstr "Registro"
+
+#: flask_security/forms.py:56
+msgid "Resend Confirmation Instructions"
+msgstr "Reenviar instruções de confirmação"
+
+#: flask_security/forms.py:57
+msgid "Recover Password"
+msgstr "Recuperar senha"
+
+#: flask_security/forms.py:58
+msgid "Reset Password"
+msgstr "Redefinir senha"
+
+#: flask_security/forms.py:59
+msgid "Retype Password"
+msgstr "Reescreva a senha"
+
+#: flask_security/forms.py:60
+msgid "New Password"
+msgstr "Nova senha"
+
+#: flask_security/forms.py:61
+msgid "Change Password"
+msgstr "Alterar senha"
+
+#: flask_security/forms.py:62
+msgid "Send Login Link"
+msgstr "Enviar link de login"
+
+#: flask_security/forms.py:63
+msgid "Verify Password"
+msgstr ""
+
+#: flask_security/forms.py:64
+msgid "Change Method"
+msgstr ""
+
+#: flask_security/forms.py:65
+msgid "Phone Number"
+msgstr ""
+
+#: flask_security/forms.py:66
+msgid "Authentication Code"
+msgstr ""
+
+#: flask_security/forms.py:67
+msgid "Submit"
+msgstr ""
+
+#: flask_security/forms.py:68
+msgid "Submit Code"
+msgstr ""
+
+#: flask_security/forms.py:69
+msgid "Error(s)"
+msgstr ""
+
+#: flask_security/forms.py:70
+msgid "Identity"
+msgstr ""
+
+#: flask_security/forms.py:71
+msgid "Send Code"
+msgstr ""
+
+#: flask_security/forms.py:72
+#, fuzzy
+msgid "Passcode"
+msgstr "Senha"
+
+#: flask_security/unified_signin.py:145
+#, fuzzy
+msgid "Code or Password"
+msgstr "Recuperar senha"
+
+#: flask_security/unified_signin.py:150 flask_security/unified_signin.py:270
+msgid "Available Methods"
+msgstr ""
+
+#: flask_security/unified_signin.py:151
+msgid "Via email"
+msgstr ""
+
+#: flask_security/unified_signin.py:151
+msgid "Via SMS"
+msgstr ""
+
+#: flask_security/unified_signin.py:272
+msgid "Set up using email"
+msgstr ""
+
+#: flask_security/unified_signin.py:275
+msgid "Set up using an authenticator app (e.g. google, lastpass, authy)"
+msgstr ""
+
+#: flask_security/unified_signin.py:277
+msgid "Set up using SMS"
+msgstr ""
+
+#: flask_security/templates/security/_menu.html:2
+msgid "Menu"
+msgstr "Menu"
+
+#: flask_security/templates/security/_menu.html:8
+msgid "Unified Sign In"
+msgstr ""
+
+#: flask_security/templates/security/_menu.html:14
+msgid "Forgot password"
+msgstr "Esqueceu a senha"
+
+#: flask_security/templates/security/_menu.html:17
+msgid "Confirm account"
+msgstr "Confirmar conta"
+
+#: flask_security/templates/security/change_password.html:6
+msgid "Change password"
+msgstr "Alterar senha"
+
+#: flask_security/templates/security/forgot_password.html:6
+msgid "Send password reset instructions"
+msgstr "Enviar instruções para redefinir a senha"
+
+#: flask_security/templates/security/reset_password.html:6
+msgid "Reset password"
+msgstr "Redefinir senha"
+
+#: flask_security/templates/security/send_confirmation.html:6
+msgid "Resend confirmation instructions"
+msgstr "Reenviar instruções de confirmação"
+
+#: flask_security/templates/security/two_factor_setup.html:6
+msgid "Two-factor authentication adds an extra layer of security to your account"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_setup.html:7
+msgid ""
+"In addition to your username and password, you'll need to use a code that"
+" we will send you"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_setup.html:18
+msgid "To complete logging in, please enter the code sent to your mail"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_setup.html:21
+#: flask_security/templates/security/us_setup.html:21
+msgid ""
+"Open your authenticator app on your device and scan the following qrcode "
+"to start receiving codes:"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_setup.html:22
+msgid "Two factor authentication code"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_setup.html:25
+msgid "To Which Phone Number Should We Send Code To?"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_verify_code.html:6
+msgid "Two-factor Authentication"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_verify_code.html:7
+msgid "Please enter your authentication code"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_verify_code.html:18
+msgid "The code for authentication was sent to your email address"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_verify_code.html:21
+msgid "A mail was sent to us in order to reset your application account"
+msgstr ""
+
+#: flask_security/templates/security/two_factor_verify_password.html:6
+#: flask_security/templates/security/verify.html:6
+msgid "Please Enter Your Password"
+msgstr ""
+
+#: flask_security/templates/security/us_setup.html:6
+msgid "Setup Unified Sign In options"
+msgstr ""
+
+#: flask_security/templates/security/us_setup.html:22
+msgid "Passwordless QRCode"
+msgstr ""
+
+#: flask_security/templates/security/us_setup.html:25
+#: flask_security/templates/security/us_signin.html:23
+#: flask_security/templates/security/us_verify.html:21
+#, fuzzy
+msgid "Code has been sent"
+msgstr "Sua senha foi redefinida"
+
+#: flask_security/templates/security/us_setup.html:29
+msgid "No methods have been enabled - nothing to setup"
+msgstr ""
+
+#: flask_security/templates/security/us_signin.html:15
+#: flask_security/templates/security/us_verify.html:13
+msgid "Request one-time code be sent"
+msgstr ""
+
+#: flask_security/templates/security/us_verify.html:6
+#, fuzzy
+msgid "Please re-authenticate"
+msgstr "Por favor, reautentique-se para acessar esta página."
+
+#: flask_security/templates/security/email/change_notice.html:1
+msgid "Your password has been changed."
+msgstr "Sua senha foi alterada."
+
+#: flask_security/templates/security/email/change_notice.html:3
+msgid "If you did not change your password,"
+msgstr "Se você não alterou sua senha,"
+
+#: flask_security/templates/security/email/change_notice.html:3
+msgid "click here to reset it"
+msgstr "clique aqui para resetar"
+
+#: flask_security/templates/security/email/confirmation_instructions.html:1
+msgid "Please confirm your email through the link below:"
+msgstr "Por favor, confirme seu email através do link abaixo:"
+
+#: flask_security/templates/security/email/confirmation_instructions.html:3
+#: flask_security/templates/security/email/welcome.html:6
+msgid "Confirm my account"
+msgstr "Confirmar minha conta"
+
+#: flask_security/templates/security/email/login_instructions.html:1
+#: flask_security/templates/security/email/welcome.html:1
+#, python-format
+msgid "Welcome %(email)s!"
+msgstr "Bem-vindo %(email)s!"
+
+#: flask_security/templates/security/email/login_instructions.html:3
+msgid "You can log into your account through the link below:"
+msgstr "Você pode logar na sua conta através do link abaixo:"
+
+#: flask_security/templates/security/email/login_instructions.html:5
+msgid "Login now"
+msgstr "Logar agora"
+
+#: flask_security/templates/security/email/reset_instructions.html:1
+msgid "Click here to reset your password"
+msgstr "Clique aqui para redefinir sua senha"
+
+#: flask_security/templates/security/email/two_factor_instructions.html:3
+msgid "You can log into your account using the following code:"
+msgstr ""
+
+#: flask_security/templates/security/email/two_factor_rescue.html:1
+msgid "can not access mail account"
+msgstr ""
+
+#: flask_security/templates/security/email/us_instructions.html:3
+#, fuzzy
+msgid "You can sign into your account using the following code:"
+msgstr "Você pode logar na sua conta através do link abaixo:"
+
+#: flask_security/templates/security/email/us_instructions.html:6
+#, fuzzy
+msgid "Or use the the link below:"
+msgstr "Você pode confirmar seu email através do link abaixo:"
+
+#: flask_security/templates/security/email/welcome.html:4
+msgid "You can confirm your email through the link below:"
+msgstr "Você pode confirmar seu email através do link abaixo:"
+
diff --git a/flask_security/translations/pt_PT/LC_MESSAGES/flask_security.mo b/flask_security/translations/pt_PT/LC_MESSAGES/flask_security.mo
new file mode 100644
index 0000000..4c90010
--- /dev/null
+++ b/flask_security/translations/pt_PT/LC_MESSAGES/flask_security.mo
Binary files differ
diff --git a/flask_security/translations/pt_PT/LC_MESSAGES/flask_security.po b/flask_security/translations/pt_PT/LC_MESSAGES/flask_security.po
new file mode 100644
index 0000000..396e7be
--- /dev/null
+++ b/flask_security/translations/pt_PT/LC_MESSAGES/flask_security.po
@@ -0,0 +1,626 @@
+# Portuguese (Portugal) translations for Flask-Security.
+# Copyright (C) 2017 CERN
+# This file is distributed under the same license as the Flask-Security
+# project.
+# Micael Grilo <micael.grilo@outlook.com>, 2018.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Flask-Security 2.0.1\n"
+"Report-Msgid-Bugs-To: info@inveniosoftware.org\n"
+"POT-Creation-Date: 2020-04-19 13:18-0700\n"
+"PO-Revision-Date: 2018-04-27 14:00+0100\n"
+"Last-Translator: Micael Grilo <micael.grilo@outlook.com>\n"
+"Language: pt_PT\n"
+"Language-Team: \n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 2.8.0\n"
+
+#: flask_security/core.py:207
+msgid "Login Required"
+msgstr "Login obrigatório"
+
+#: flask_security/core.py:208
+#: flask_security/templates/security/email/two_factor_instructions.html:1
+#: flask_security/templates/security/email/us_instructions.html:1
+msgid "Welcome"
+msgstr "Bem-vindo"
+
+#: flask_security/core.py:209
+msgid "Please confirm your email"
+msgstr "Por favor, confirme o seu email"
+
+#: flask_security/core.py:210
+msgid "Login instructions"
+msgstr "Instruções de login"
+
+#: flask_security/core.py:211
+#: flask_security/templates/security/email/reset_notice.html:1
+msgid "Your password has been reset"
+msgstr "A sua palavra-passe foi redefinida"
+
+#: flask_security/core.py:212
+msgid "Your password has been changed"
+msgstr "A sua palavra-passe foi alterada"
+
+#: flask_security/core.py:213
+msgid "Password reset instructions"
+msgstr "Instruções para redefinir a palavra-passe"
+
+#: flask_security/core.py:216
+msgid "Two-factor Login"
+msgstr ""
+
+#: flask_security/core.py:217
+msgid "Two-factor Rescue"
+msgstr ""
+
+#: flask_security/core.py:266
+msgid "Verification Code"
+msgstr ""
+
+#: flask_security/core.py:282
+msgid "Input not appropriate for requested API"
+msgstr ""
+
+#: flask_security/core.py:283
+msgid "You do not have permission to view this resource."
+msgstr "Não tem permissões para ver este recurso"
+
+#: flask_security/core.py:285
+msgid "You are not authenticated. Please supply the correct credentials."
+msgstr ""
+
+#: flask_security/core.py:289
+#, fuzzy
+msgid "You must re-authenticate to access this endpoint"
+msgstr "Por favor, reautentique-se para aceder esta página."
+
+#: flask_security/core.py:293
+#, python-format
+msgid "Thank you. Confirmation instructions have been sent to %(email)s."
+msgstr "Obrigado. As instruções para a confirmação foram enviadas para %(email)s."
+
+#: flask_security/core.py:296
+msgid "Thank you. Your email has been confirmed."
+msgstr "Obrigado. O seu email foi confirmado."
+
+#: flask_security/core.py:297
+msgid "Your email has already been confirmed."
+msgstr "O seu email já foi confirmado."
+
+#: flask_security/core.py:298
+msgid "Invalid confirmation token."
+msgstr "Token de confirmação inválido."
+
+#: flask_security/core.py:300
+#, python-format
+msgid "%(email)s is already associated with an account."
+msgstr "%(email)s já está associado a uma conta."
+
+#: flask_security/core.py:303
+msgid "Password does not match"
+msgstr "Palavra-passe não coincide"
+
+#: flask_security/core.py:304
+msgid "Passwords do not match"
+msgstr "Palavras-passe não coincidem"
+
+#: flask_security/core.py:305
+msgid "Redirections outside the domain are forbidden"
+msgstr "Redirecionamentos para fora do domínio são proibidos"
+
+#: flask_security/core.py:307
+#, python-format
+msgid "Instructions to reset your password have been sent to %(email)s."
+msgstr ""
+"As instruções para redefinir a sua palavra-passe foram enviadas para "
+"%(email)s."
+
+#: flask_security/core.py:311
+#, python-format
+msgid ""
+"You did not reset your password within %(within)s. New instructions have "
+"been sent to %(email)s."
+msgstr ""
+"Não redefiniu a sua palavra-passe dentro de %(within)s. Novas instruções "
+"foram enviadas para %(email)s."
+
+#: flask_security/core.py:317
+msgid "Invalid reset password token."
+msgstr "Token de redefinição de senha inválido."
+
+#: flask_security/core.py:318
+msgid "Email requires confirmation."
+msgstr "O email requer confirmação."
+
+#: flask_security/core.py:320
+#, python-format
+msgid "Confirmation instructions have been sent to %(email)s."
+msgstr "As instruções de confirmação foram enviadas para %(email)s."
+
+#: flask_security/core.py:324
+#, python-format
+msgid ""
+"You did not confirm your email within %(within)s. New instructions to "
+"confirm your email have been sent to %(email)s."
+msgstr ""
+"Não confirmou o seu email dentro de %(within)s. Novas instruções foram "
+"enviadas para %(email)s."
+
+#: flask_security/core.py:332
+#, python-format
+msgid ""
+"You did not login within %(within)s. New instructions to login have been "
+"sent to %(email)s."
+msgstr ""
+"Não iniciou sessão dentro de %(within)s. Novas instruções de inicio de "
+"sessão foram enviadas para %(email)s."
+
+#: flask_security/core.py:339
+#, python-format
+msgid "Instructions to login have been sent to %(email)s."
+msgstr "Instruções para o inicio de sessão foram enviadas para %(email)s."
+
+#: flask_security/core.py:342
+msgid "Invalid login token."
+msgstr "Token de login inválido."
+
+#: flask_security/core.py:343
+msgid "Account is disabled."
+msgstr "Conta desactivada."
+
+#: flask_security/core.py:344
+msgid "Email not provided"
+msgstr "Email em falta"
+
+#: flask_security/core.py:345
+msgid "Invalid email address"
+msgstr "Endereço de email inválido"
+
+#: flask_security/core.py:346
+#, fuzzy
+msgid "Invalid code"
+msgstr "Palavra-passe inválida"
+
+#: flask_security/core.py:347
+msgid "Password not provided"
+msgstr "Palavra-passe em falta"
+
+#: flask_security/core.py:348
+msgid "No password is set for this user"
+msgstr "Nenhuma palavra-passe foi definida para este utilizador"
+
+#: flask_security/core.py:350
+#, fuzzy, python-format
+msgid "Password must be at least %(length)s characters"
+msgstr "A palavra-passe deve ter pelo menos 6 caracteres"
+
+#: flask_security/core.py:353
+msgid "Password not complex enough"
+msgstr ""
+
+#: flask_security/core.py:354
+msgid "Password on breached list"
+msgstr ""
+
+#: flask_security/core.py:356
+msgid "Failed to contact breached passwords site"
+msgstr ""
+
+#: flask_security/core.py:359
+msgid "Phone number not valid e.g. missing country code"
+msgstr ""
+
+#: flask_security/core.py:360
+msgid "Specified user does not exist"
+msgstr "Utilizador não existe"
+
+#: flask_security/core.py:361
+msgid "Invalid password"
+msgstr "Palavra-passe inválida"
+
+#: flask_security/core.py:362
+msgid "Password or code submitted is not valid"
+msgstr ""
+
+#: flask_security/core.py:363
+msgid "You have successfully logged in."
+msgstr "Sessão iniciada com sucesso."
+
+#: flask_security/core.py:364
+msgid "Forgot password?"
+msgstr "Esqueceu a palavra-passe?"
+
+#: flask_security/core.py:366
+msgid ""
+"You successfully reset your password and you have been logged in "
+"automatically."
+msgstr ""
+"Redefiniu a sua palavra-passe com sucesso e iniciou sessão "
+"automaticamente."
+
+#: flask_security/core.py:373
+msgid "Your new password must be different than your previous password."
+msgstr "A sua nova palavra-passe deve ser diferente da anterior."
+
+#: flask_security/core.py:376
+msgid "You successfully changed your password."
+msgstr "Alterou a sua palavra-passe com sucesso."
+
+#: flask_security/core.py:377
+msgid "Please log in to access this page."
+msgstr "Por favor, inicie sessão para aceder a esta página."
+
+#: flask_security/core.py:378
+msgid "Please reauthenticate to access this page."
+msgstr "Por favor, reautentique-se para aceder esta página."
+
+#: flask_security/core.py:379
+msgid "Reauthentication successful"
+msgstr ""
+
+#: flask_security/core.py:381
+msgid "You can only access this endpoint when not logged in."
+msgstr ""
+
+#: flask_security/core.py:384
+msgid "Failed to send code. Please try again later"
+msgstr ""
+
+#: flask_security/core.py:385
+msgid "Invalid Token"
+msgstr ""
+
+#: flask_security/core.py:386
+msgid "Your token has been confirmed"
+msgstr ""
+
+#: flask_security/core.py:388
+msgid "You successfully changed your two-factor method."
+msgstr ""
+
+#: flask_security/core.py:392
+msgid "You successfully confirmed password"
+msgstr ""
+
+#: flask_security/core.py:396
+msgid "Password confirmation is needed in order to access page"
+msgstr ""
+
+#: flask_security/core.py:400
+msgid "You currently do not have permissions to access this page"
+msgstr ""
+
+#: flask_security/core.py:403
+msgid "Marked method is not valid"
+msgstr ""
+
+#: flask_security/core.py:405
+msgid "You successfully disabled two factor authorization."
+msgstr ""
+
+#: flask_security/core.py:408
+msgid "Requested method is not valid"
+msgstr ""
+
+#: flask_security/core.py:410
+#, python-format
+msgid "Setup must be completed within %(within)s. Please start over."
+msgstr ""
+
+#: flask_security/core.py:413<