diff -Nru flask-security-3.4.2/babel.ini flask-security-4.0.0/babel.ini --- flask-security-3.4.2/babel.ini 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/babel.ini 2021-01-26 02:39:51.000000000 +0000 @@ -8,3 +8,6 @@ [jinja2: **/templates/**.html] encoding = utf-8 extensions = jinja2.ext.autoescape, jinja2.ext.with_ + +[jinja2: **/templates/**.txt] +extensions = jinja2.ext.with_ diff -Nru flask-security-3.4.2/CHANGES.rst flask-security-4.0.0/CHANGES.rst --- flask-security-3.4.2/CHANGES.rst 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/CHANGES.rst 2021-01-26 02:39:51.000000000 +0000 @@ -6,18 +6,234 @@ Version 4.0.0 ------------- -Release Target 2020 +Released January 26, 2021 +**PLEASE READ CHANGE NOTES CAREFULLY - THERE ARE LIKELY REQUIRED CHANGES YOU WILL HAVE TO MAKE TO EVEN START YOUR APPLICATION WITH 4.0** + +Start Here ++++++++++++ +- Your UserModel must contain ``fs_uniquifier`` +- Either uninstall Flask-BabelEx (if you don't need translations) or add either Flask-Babel (>=2.0) or Flask-BabelEx to your + dependencies AND be sure to initialize it in your app. +- Add Flask-Mail to your dependencies. +- If you have unicode emails or passwords read change notes below. + +Version 4.0.0rc2 +---------------- + +Released January 18, 2021 + +Features & Cleanup ++++++++++++++++++++ - 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`_ +- Removal of token caching feature (a relatively new feature that had some systemic issues) +- (:pr:`328`) Remove dependence on Flask-Mail and refactor. +- (:pr:`335`) Remove two-factor `/tf-confirm` endpoint and use generic `freshness` mechanism. +- (:pr:`336`) Remove ``SECURITY_BACKWARDS_COMPAT_AUTH_TOKEN_INVALID(ATE)``. In addition to + not making sense - the documentation has never been correct. +- (:pr:`339`) Require ``fs_uniquifier`` in the UserModel and stop using/referencing the UserModel + primary key. +- (:pr:`349`) Change ``SECURITY_USER_IDENTITY_ATTRIBUTES`` configuration variable semantics. +- Remove (all?) requirements around having an 'email' column in the UserModel. API change - + JSON SPA redirects used to always include a query param 'email=xx'. While that is still sent + (if and only if) the UserModel contains an 'email' columns, a new query param 'identity' is returned + which returns the value of :meth:`.UserMixin.calc_username()`. +- (:pr:`382`) Improvements and documentation for two-factor authentication. +- (:pr:`394`) Add support for email validation and normalization (see :class:`.MailUtil`). +- (:issue:`231`) Normalize unicode passwords (see :class:`.PasswordUtil`). +- (:issue:`391`) Option to redirect to `/confirm` if user hits an endpoint that requires + confirmation. New option :py:data:`SECURITY_REQUIRES_CONFIRMATION_ERROR_VIEW` which if set and the user + hits the `/login`, `/reset`, or `/us-signin` endpoint, and they require confirmation the response will be a redirect. (SnaKyEyeS) +- (:issue:`366`) Allow redirects on sub-domains. Please see :py:data:`SECURITY_REDIRECT_ALLOW_SUBDOMAINS`. (willcroft) +- (:pr:`376`) Have POST redirects default to Flask's ``APPLICATION_ROOT``. Previously the default configuration was ``/``. + Now it first looks at Flask's `APPLICATION_ROOT` configuration and uses that (which also by default is ``/``. (tysonholub) +- (:pr:`401`) Add 2FA Validity Window so an application can configure how often the second factor has to be entered. (baurt) +- (:pr:`403`) Add HTML5 Email input types to email fields. This has some backwards compatibility concerns outlined below. (drola) +- (:pr:`413`) Add hy_AM translations. (rudolfamirjanyan) +- (:pr:`410`) Add Basque and fix Spanish translations. (mmozos) +- (:pr:`408`) Polish translations. (kamil559) +- (:pr:`390`) Update ru_RU translations. (TitaniumHocker) + +Fixed ++++++ +- (:issue:`389`) Fixes for translations. First - email subjects were never being translated. Second, converted + all templates to use _fsdomain(xx) rather than _(xx) so that they get translated regardless of the app's domain. +- (:issue:`381`) Support Flask-Babel 2.0 which has backported Domain support. Flask-Security now supports + Flask-Babel (>=2.00), Flask-BabelEx, as well as no translation support. Please see backwards compatibility notes below. +- (:pr:`352`) Fix issue with adding/deleting permissions - all mutating methods must be at the datastore layer so that + db.put() can be called. Added :meth:`.UserDatastore.add_permissions_to_role` and :meth:`.UserDatastore.remove_permissions_from_role`. + The methods :meth:`.RoleMixin.add_permissions` and :meth:`.RoleMixin.remove_permissions` have been deprecated. +- (:issue:`395`) Provide ability to change table names for User and Role tables in the fsqla model. +- (:issue:`338`) All sessions are invalidated when a user changes or resets their password. This is accomplished by + changing the user's `fs_uniquifier`. The user is automatically re-logged in (and a new session + created) after a successful change operation. +- (:issue:`418`) Two-factor (and to a lesser extent unified sign in) QRcode fetching wasn't protected via CSRF. The + fix makes things secure and simpler (always good); however read below for compatibility concerns. In addition, the elements that make up the QRcode (key, username, issuer) area also made available to the form + and returned as part of the JSON return value - this allows for manual or other ways to initialize the authenticator + app. +- (:issue:`421`) GET on `/login` and `/change` could return the callers authentication_token. This is a security + concern since GETs don't have CSRF protection. This bug was introduced in 3.3.0. + +Backwards Compatibility Concerns ++++++++++++++++++++++++++++++++++ +- (:pr:`328`) Remove dependence on Flask-Mail and refactor. The ``send_mail_task`` and + ``send_mail`` methods as part of Flask-Security initialization + have been removed and replaced with a new :class:`.MailUtil` class. + The utility method :func:`.send_mail` can still be used. + If your application didn't use either of the deprecated methods, then the only change required + is to add Flask-Mail to your package requirements (since Flask-Security no longer lists it). + Please see the :ref:`emails_topic` for updated examples. + +- (:pr:`335`) Convert two-factor setup flow to use the freshness feature rather than + its own verify password endpoint. This COMPLETELY removes the ``/tf-confirm`` endpoint + and associated form: ``two_factor_verify_password_form``. Now, when /tf-setup is invoked, + the :meth:`flask_security.check_and_update_authn_fresh` is invoked, and if the current session isn't 'fresh' + the caller will be redirected to a verify endpoint (either :py:data:`SECURITY_VERIFY_URL` or + :py:data:`SECURITY_US_VERIFY_URL`). The simplest change would be to call ``/verify`` everywhere + the application used to call ``/tf-confirm``. + +- (:pr:`339`) Require ``fs_uniquifier``. In 3.3 the ``fs_uniquifier`` was added in the UserModel to fix + the slow authentication token issue. In 3.4 the ``fs_uniquifier`` was used to implement Flask-Login's + `Alternative Token` feature - thus decoupling the primary key (id) from any security context. + All along, there have been a few issues with applications not wanting to use the name 'id' in their + model, or wanting a different type for their primary key. With this change, Flask-Security no longer + interprets or uses the UserModel primary key - just the ``fs_uniquifier`` field. See the changes section for 3.3 + for information on how to do the schema and data upgrades required to add this field. There is also an API change - + the JSON response (via UserModel.get_security_payload()) returned the ``user.id`` field. With this change + the default is an empty directory - override :meth:`.UserMixin.get_security_payload()` to return any portion of the UserModel you need. + +- (:pr:`349`) :py:data:`SECURITY_USER_IDENTITY_ATTRIBUTES` has changed syntax and semantics. It now contains + the combined information from the old ``SECURITY_USER_IDENTITY_ATTRIBUTES`` and the newly introduced in 3.4 :py:data:`SECURITY_USER_IDENTITY_MAPPINGS`. + This enabled changing the underlying way we validate credentials in the login form and unified sign in form. + In prior releases we simply tried to look up the form value as the PK of the UserModel - this often failed and then + looped through the other ``SECURITY_USER_IDENTITY_ATTRIBUTES``. This had a history of issues, including many applications not + wanting to have a standard PK for the user model. Now, using the mapping configuration, the UserModel attribute/column the input + corresponds to is determined, then the UserModel is queried specifically for that *attribute:value* pair. If you application + didn't change the variable, no modifications are required. + +- (:pr:`354`) The :class:`flask_security.PhoneUtil` is now initialized as part of Flask-Security initialization rather than + ``@app.before_first_request`` (since that broke the CLI). Since it isn't called in an application context, the *app* being initialized is + passed as an argument to *__init__*. + +- (:issue:`381`) When using Flask-Babel (>= 2.0) it is required that the application initialize Flask-Babel (e.g. Babel(app)). + Flask-BabelEx would self-initialize so it didn't matter. Flask-Security will throw a run time error upon first request if Flask-Babel + OR FLask-BabelEx + is installed, but not initialized. Also, Flask-Security no longer has a dependency on either Flask-Babel or Flask-BabelEx - if neither + are installed, it falls back to a dummy translation. *If your application expects translation services, it must specify the appropriate* + *dependency AND initialize it.* + +- (:pr:`394`) Email input is now normalized prior to being stored in the DB. Previously, it was validated, but the raw input + was stored. Normalization and validation rely on the `email_validator `_ package. + The :class:`.MailUtil` class provides the interface for normalization and validation - allowing all this to be customized. + If you have unicode local or domain parts - existing users may have difficulties logging in. Administratively you need to + read each user record, normalize the email (see :class:`.MailUtil`), and write it back. + +- (:issue:`381`) Passwords are now, by default, normalized using Python's unicodedata.normalize() method. + The :py:data:`SECURITY_PASSWORD_NORMALIZE_FORM` defaults to "NKFD". This brings Flask-Security + in line with the NIST recommendations outlined in `Memorized Secret Verifiers `_ + If your users have unicode passwords + they may have difficulty authenticating. You can turn off this normalization or have your users reset their passwords. + Password normalization and validation has been encapsulated in a new :class:`.PasswordUtil` class. This replaces + the method ``password_validator`` introduced in 3.4.0. + +- (:pr:`403`) By default all forms that have an email as input now use the wtforms html5 ``EmailField``. For most applications this will + make the user experience slightly nicer - especially for mobile devices. Some applications use the email form field for other + identity attributes (such as username). If your application does this you will probably need to subclass ``LoginForm`` and change + the email type back to StringField. + +- (:issue:`338`) By default, both passwords and authentication tokens use the same attribute ``fs_uniquifier`` to + uniquely identify the user. This means that if the user changes or resets their password, all authentication tokens + also become invalid. This could be viewed as a feature or a bug. If this behavior isn't desired, add another + uniquifier: ``fs_token_uniquifier`` to your UserModel and that will be used to generate authentication tokens. + +- (:issue:`418`) Fix CSRF vulnerability w.r.t. getting QRcodes. Both two-factor and unified-signup had a separate + GET endpoint to fetch the QRcode when setting up an authenticator app. GETS don't have any CSRF protection. Both + of those endpoints have been completely removed, and the QRcode is embedded in a successful POST of the setup form. + The changes to the templates are minimal and of course if you didn't override the template - there is no + compatibility concern. + +- (:issue:`421`) Fix CSRF vulnerability on `/login` and `/change` that could return the callers authentication token. + Now, callers can only get the authentication token on successful POST calls. + +Version 3.4.5 +-------------- + +Released January 8, 2021 + +Security Vulnerability Fix. + +Two CSRF vulnerabilities were reported: `qrcode`_ and `login`_. This release +fixes the more severe of the 2 - the `/login` vulnerability. The QRcode issue +has a much smaller risk profile since a) it is only for two-factor authentication +using an authenticator app b) the qrcode is only available during the time +the user is first setting up their authentication app. +The QRcode issue has been fixed in 4.0. + +.. _qrcode: https://github.com/Flask-Middleware/flask-security/issues/418 +.. _login: https://github.com/Flask-Middleware/flask-security/issues/421 + +Fixed ++++++ + +- (:issue:`421`) GET on `/login` and `/change` could return the callers authentication_token. This is a security + concern since GETs don't have CSRF protection. This bug was introduced in 3.3.0. + +Backwards Compatibility Concerns +++++++++++++++++++++++++++++++++ + +- (:issue:`421`) Fix CSRF vulnerability on `/login` and `/change` that could return the callers authentication token. + Now, callers can only get the authentication token on successful POST calls. + +Version 3.4.4 +-------------- + +Released July 27, 2020 + +Bug/regression fixes. + +Fixed ++++++ + +- (:issue:`359`) Basic Auth broken. When the unauthenticated handler was changed to provide a more + uniform/consistent response - it broke using Basic Auth from a browser, since it always redirected rather than + returning 401. Now, if the response headers contain ``WWW-Authenticate`` + (which is set if ``basic`` @auth_required method is used), a 401 is returned. See below + for backwards compatibility concerns. + +- (:pr:`362`) As part of figuring out issue 359 - a redirect loop was found. In release 3.3.0 code was put + in to redirect to :py:data:`SECURITY_POST_LOGIN_VIEW` when GET or POST was called and the caller was already authenticated. The + method used would honor the request ``next`` query parameter. This could cause redirect loops. The pre-3.3.0 behavior + of redirecting to :py:data:`SECURITY_POST_LOGIN_VIEW` and ignoring the ``next`` parameter has been restored. + +- (:issue:`347`) Fix peewee. Turns out - due to lack of unit tests - peewee hasn't worked since + 'permissions' were added in 3.3. Furthermore, changes in 3.4 around get_id and alternative tokens also + didn't work since peewee defines its own `get_id` method. + +Compatibility Concerns +++++++++++++++++++++++ + +In 3.3.0, :meth:`flask_security.auth_required` was changed to add a default argument if none was given. The default +include all current methods - ``session``, ``token``, and ``basic``. However ``basic`` really isn't like the others +and requires that we send back a ``WWW-Authenticate`` header if authentication fails (and return a 401 and not redirect). +``basic`` has been removed from the default set and must once again be explicitly requested. + +Version 3.4.3 +------------- + +Released June 12, 2020 + +Minor fixes for a regression and a couple other minor changes + +Fixed ++++++ -.. _here: https://github.com/Flask-Middleware/flask-security/issues/85 +- (:issue:`340`) Fix regression where tf_phone_number was required, even if SMS wasn't configured. +- (:pr:`342`) Pick up some small documentation fixes from 4.0.0. Version 3.4.2 ------------- -Released May x, 2020 +Released May 2, 2020 Only change is to move repo to the Flask-Middleware github organization. @@ -95,6 +311,8 @@ Other changes with possible backwards compatibility issues: - ``/tf-setup`` never did any phone number validation. Now it does. +- ``two_factor_setup.html`` template - the chosen_method check was changed to ``email``. + If you have your own custom template - be sure make that change. Version 3.3.3 ------------- @@ -165,7 +383,7 @@ - (: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 +- (:issue:`121`) Unauthorized 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 @@ -280,7 +498,7 @@ Released June 26th 2019 -- (opr #839) Support caching of authentication token (eregnier). +- (:pr:`80`) Support caching of authentication token (eregnier `opr #839 `_). 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 @@ -301,23 +519,23 @@ 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) +- (:pr:`53`) Use Security.render_template in mails too (noirbizarre `opr #487 `_) +- (:pr:`56`) Optimize DB accesses by using an SQL JOIN when retrieving a user. (nfvs `opr #679 `_) +- (:pr:`57`) Add base template to security templates (grihabor `opr #697 `_) +- (:pr:`73`) datastore: get user by numeric identity attribute (jirikuncar `opr #633 `_) +- (:pr:`58`) bugfix: support application factory pattern (briancappello `opr #703 `_) +- (:pr:`60`) Make SECURITY_PASSWORD_SINGLE_HASH a list of scheme ignoring double hash (noirbizarre `opr #714 `_) +- (:pr:`61`) Allow custom login_manager to be passed in to Flask-Security (jaza `opr #717 `_) +- (:pr:`62`) Docs for OAauth2-based custom login manager (jaza `opr #727 `_) +- (:pr:`63`) core: make the User model check the password (mklassen `opr #779 `_) +- (:pr:`64`) Customizable send_mail (abulte `opr #730 `_) +- (:pr:`68`) core: fix default for UNAUTHORIZED_VIEW (jirijunkar `opr #726 `_) These should all be backwards compatible. Possible compatibility issues: -- #487 - prior to this, render_template() was overiddable for views, but not +- #487 - prior to this, render_template() was overridable 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). @@ -326,7 +544,7 @@ Got exception during processing: - 'User.roles' does not support object population - eager loading cannot be applied. - This is likely solveable by removing ``lazy='dynamic'`` from your Role definition. + This is likely solvable by removing ``lazy='dynamic'`` from your Role definition. Performance improvements: @@ -399,7 +617,7 @@ 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. + for sending respectively 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. @@ -425,7 +643,7 @@ - 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. +- Changed obsolete 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`. @@ -462,7 +680,7 @@ 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 in form validation that did not enforce case insensitivity - Fixed a bug with validating redirects @@ -504,7 +722,7 @@ - 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 +- Added `SECURITY_SEND_PASSWORD_RESET_NOTICE_EMAIL` configuration 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 @@ -660,7 +878,7 @@ 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 + accommodate API changes. See documentation for a list of new features and for help on how to upgrade. Version 1.2.3 diff -Nru flask-security-3.4.2/CONTRIBUTING.rst flask-security-4.0.0/CONTRIBUTING.rst --- flask-security-3.4.2/CONTRIBUTING.rst 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/CONTRIBUTING.rst 2021-01-26 02:39:51.000000000 +0000 @@ -44,10 +44,10 @@ #. (Optional, but recommended) Create a Python 3.6 (or greater) virtualenv to work in, and activate it. - #. Fork the repo `Flask-Security `_ + #. Fork the repo `Flask-Security `_ (look for the "Fork" button). - #. Clone your fork locally:: + #. Clone your fork locally:: $ git clone https://github.com//flask-security @@ -55,13 +55,17 @@ $ git checkout -b name-of-your-bugfix-or-feature - #. Change directory to flask_security:: + #. Change directory to flask_security:: $ cd flask_security - #. Install the requirements:: + #. Install the requirements:: - $ pip install -e .[tests] + $ pip install -r requirements/tests.txt + + #. Install pre-commit hooks:: + + $ pre-commit install #. Develop the Feature/Bug Fix and edit @@ -71,9 +75,16 @@ #. When done, verify unit tests, syntax etc. all pass:: - $ python setup.py test + $ pytest tests + $ pre-commit run --all-files $ python setup.py build_sphinx compile_catalog + #. Use tox:: + + $ tox # run everything CI does + $ tox -e py38-low # make sure works with older dependencies + $ tox -e style # run pre-commit/style checks + #. When the tests are successful, commit your changes and push your branch to GitHub:: @@ -137,9 +148,9 @@ of course install the DB locally then:: # For postgres - python setup.py test --realdburl postgres://@localhost/ + pytest --realdburl postgres://@localhost/ # For mysql - python setup.py test --realdburl "mysql+pymysql://root:@localhost/" + pytest --realdburl "mysql+pymysql://root:@localhost/" Views +++++ diff -Nru flask-security-3.4.2/debian/changelog flask-security-4.0.0/debian/changelog --- flask-security-3.4.2/debian/changelog 2020-07-06 18:05:29.000000000 +0000 +++ flask-security-4.0.0/debian/changelog 2021-02-01 14:42:21.000000000 +0000 @@ -1,3 +1,23 @@ +flask-security (4.0.0-1) unstable; urgency=medium + + * Team upload. + + [ Debian Janitor ] + * Set upstream metadata fields: Bug-Database, Bug-Submit, Repository, + Repository-Browse. + + [ Ondřej Nový ] + * d/control: Update Maintainer field with new Debian Python Team + contact address. + * d/control: Update Vcs-* fields with new Debian Python Team Salsa + layout. + + [ Christoph Berg ] + * New upstream version 4.0.0. + + Fixes /login and /change vulnerability. (Closes: 980189, CVE-2021-21241) + + -- Christoph Berg Mon, 01 Feb 2021 15:42:21 +0100 + flask-security (3.4.2-2) unstable; urgency=medium * Team upload. diff -Nru flask-security-3.4.2/debian/control flask-security-4.0.0/debian/control --- flask-security-3.4.2/debian/control 2020-07-06 18:05:29.000000000 +0000 +++ flask-security-4.0.0/debian/control 2021-02-01 14:41:21.000000000 +0000 @@ -1,5 +1,5 @@ Source: flask-security -Maintainer: Debian Python Modules Team +Maintainer: Debian Python Team Uploaders: Adrian Vondendriesch Section: python Priority: optional @@ -30,8 +30,8 @@ twine, Standards-Version: 4.5.0 Homepage: https://github.com/mattupstate/flask-security -Vcs-Git: https://salsa.debian.org/python-team/modules/flask-security.git -Vcs-Browser: https://salsa.debian.org/python-team/modules/flask-security +Vcs-Git: https://salsa.debian.org/python-team/packages/flask-security.git +Vcs-Browser: https://salsa.debian.org/python-team/packages/flask-security Package: python3-flask-security Architecture: all diff -Nru flask-security-3.4.2/debian/patches/flask-version flask-security-4.0.0/debian/patches/flask-version --- flask-security-3.4.2/debian/patches/flask-version 2020-07-06 18:05:29.000000000 +0000 +++ flask-security-4.0.0/debian/patches/flask-version 1970-01-01 00:00:00.000000000 +0000 @@ -1,13 +0,0 @@ -Remove Flask version annotation so we can be installed on stretch and xenial/bionic - ---- a/setup.py -+++ b/setup.py -@@ -57,7 +57,7 @@ for reqs in extras_require.values(): - setup_requires = ["Babel>=1.3", "pytest-runner>=2.6.2", "twine", "wheel"] - - install_requires = [ -- "Flask>=1.0.2", -+ "Flask", - "Flask-Login>=0.4.1", - "Flask-Mail>=0.9.1", - "Flask-Principal>=0.4.0", diff -Nru flask-security-3.4.2/debian/patches/series flask-security-4.0.0/debian/patches/series --- flask-security-3.4.2/debian/patches/series 2020-07-06 18:05:29.000000000 +0000 +++ flask-security-4.0.0/debian/patches/series 1970-01-01 00:00:00.000000000 +0000 @@ -1,2 +0,0 @@ -flask-version -#xenial-pytest-runner diff -Nru flask-security-3.4.2/debian/patches/xenial-pytest-runner flask-security-4.0.0/debian/patches/xenial-pytest-runner --- flask-security-3.4.2/debian/patches/xenial-pytest-runner 2020-07-06 18:05:29.000000000 +0000 +++ flask-security-4.0.0/debian/patches/xenial-pytest-runner 1970-01-01 00:00:00.000000000 +0000 @@ -1,11 +0,0 @@ ---- a/setup.py -+++ b/setup.py -@@ -54,7 +54,7 @@ extras_require["all"] = [] - for reqs in extras_require.values(): - extras_require["all"].extend(reqs) - --setup_requires = ["Babel>=1.3", "pytest-runner>=2.6.2", "twine", "wheel"] -+setup_requires = ["Babel>=1.3", "twine", "wheel"] - - install_requires = [ - "Flask", diff -Nru flask-security-3.4.2/debian/upstream/metadata flask-security-4.0.0/debian/upstream/metadata --- flask-security-3.4.2/debian/upstream/metadata 1970-01-01 00:00:00.000000000 +0000 +++ flask-security-4.0.0/debian/upstream/metadata 2021-02-01 14:41:21.000000000 +0000 @@ -0,0 +1,5 @@ +--- +Bug-Database: https://github.com/Flask-Middleware/flask-security/issues +Bug-Submit: https://github.com/Flask-Middleware/flask-security/issues/new +Repository: https://github.com/Flask-Middleware/flask-security.git +Repository-Browse: https://github.com/Flask-Middleware/flask-security diff -Nru flask-security-3.4.2/docs/api.rst flask-security-4.0.0/docs/api.rst --- flask-security-3.4.2/docs/api.rst 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/docs/api.rst 2021-01-26 02:39:51.000000000 +0000 @@ -96,6 +96,8 @@ .. autofunction:: flask_security.get_hmac +.. autofunction:: flask_security.get_request_attr + .. autofunction:: flask_security.verify_password .. autofunction:: flask_security.verify_and_update_password @@ -126,6 +128,8 @@ .. autofunction:: flask_security.transform_url +.. autofunction:: flask_security.unique_identity_attribute + .. autofunction:: flask_security.us_send_security_token .. autofunction:: flask_security.tf_send_security_token @@ -133,10 +137,19 @@ .. autoclass:: flask_security.FsJsonEncoder .. autoclass:: flask_security.Totp - :members: get_last_counter, set_last_counter + :members: get_last_counter, set_last_counter, generate_qrcode .. autoclass:: flask_security.PhoneUtil :members: + :special-members: __init__ + +.. autoclass:: flask_security.MailUtil + :members: + :special-members: __init__ + +.. autoclass:: flask_security.PasswordUtil + :members: + :special-members: __init__ .. autoclass:: flask_security.SmsSenderBaseClass :members: send_sms @@ -144,6 +157,8 @@ .. autoclass:: flask_security.SmsSenderFactory :members: createSender +.. _signals_topic: + Signals ------- See the `Flask documentation on signals`_ for information on how to use these diff -Nru flask-security-3.4.2/docs/configuration.rst flask-security-4.0.0/docs/configuration.rst --- flask-security-3.4.2/docs/configuration.rst 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/docs/configuration.rst 2021-01-26 02:39:51.000000000 +0000 @@ -27,7 +27,8 @@ .. py:data:: SECURITY_SUBDOMAIN - Specifies the subdomain for the Flask-Security blueprint. + Specifies the subdomain for the Flask-Security blueprint. If your authenticated + content is on a different subdomain, also enable :py:data:`SECURITY_REDIRECT_ALLOW_SUBDOMAINS`. Default: ``None``. .. py:data:: SECURITY_FLASH_MESSAGES @@ -142,6 +143,18 @@ .. versionadded:: 3.4.0 +.. py:data:: SECURITY_PASSWORD_NORMALIZE_FORM + + Passwords are normalized prior to changing or comparing. This satisfies + the NIST requirement: `5.1.1.2 Memorized Secret Verifiers`_. + Normalization is performed using the Python unicodedata.normalize() method. + + Default: "NFKD" + + .. versionadded:: 4.0.0 + +.. _5.1.1.2 Memorized Secret Verifiers: https://pages.nist.gov/800-63-3/sp800-63b.html#sec5 + .. py:data:: SECURITY_TOKEN_AUTHENTICATION_KEY Specifies the query string parameter to read when using token authentication. @@ -160,35 +173,22 @@ 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 +.. py:data:: SECURITY_EMAIL_VALIDATOR_ARGS - 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.** + Email address are validated using the `email_validator`_ package. Its methods + have some configurable options - these can be set here and will be passed in. - Default: ``None``. - -.. py:data:: SECURITY_VERIFY_HASH_CACHE_MAX_SIZE + Default: ``None``, meaning use the defaults from email_validator package. - Limitation for token validation cache size. Rules are the ones of TTLCache of - cachetools package. + .. versionadded:: 4.0.0 - Default: ``500`` +.. _email_validator: https://pypi.org/project/email-validator/ -.. py:data:: SECURITY_VERIFY_HASH_CACHE_TTL +.. py:data:: SECURITY_DEFAULT_HTTP_AUTH_REALM - Time to live for password check cache entries. + Specifies the default authentication realm when using basic HTTP auth. - Default: ``300`` (5 minutes) + Default: ``Login Required`` .. py:data:: SECURITY_REDIRECT_BEHAVIOR @@ -213,6 +213,17 @@ .. versionadded:: 3.3.0 +.. py:data:: SECURITY_REDIRECT_ALLOW_SUBDOMAINS + + If ``True`` then subdomains (and the root domain) of the top-level host set + by Flask's ``SERVER_NAME`` configuration will be allowed as post-login redirect targets. + This is beneficial if you wish to place your authentiation on one subdomain and + authenticated content on another, for example ``auth.domain.tld`` and ``app.domain.tld``. + + Default: ``False``. + + .. versionadded:: 4.0.0 + .. py:data:: SECURITY_CSRF_PROTECT_MECHANISMS Authentication mechanisms that require CSRF protection. @@ -261,46 +272,78 @@ .. py:data:: SECURITY_USER_IDENTITY_ATTRIBUTES - Specifies which attributes of the user object can be used for login. + Specifies which attributes of the user object can be used for credential validation. + + Defines the order and matching that will be applied when validating login + credentials (either via standard login form or the unified sign in form). + The identity field in the form will be matched in order using this configuration + - the FIRST match will then be used to look up the user in the DB. - Default: ``['email']``. + 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 + using the column name specified in the key. + + 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. .. danger:: Make sure that any attributes listed here are marked Unique in your UserDataStore model. -.. py:data:: SECURITY_USER_IDENTITY_MAPPINGS + .. danger:: + Make sure your mapper methods guard against malicious user input. For example, + if you allow ``username`` as an identity method you could use `bleach`_:: - 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. + def uia_username_mapper(identity): + # we allow pretty much anything - but we bleach it. + return bleach.clean(identity, strip=True) Default:: [ - {"email": uia_email_mapper}, - {"us_phone_number": uia_phone_mapper}, - ], + {"email": {"mapper": uia_email_mapper, "case_insensitive": True}}, + ] - Be aware that ONLY those attributes listed in :py:data:`SECURITY_USER_IDENTITY_ATTRIBUTES` - will be considered - regardless of the setting of this variable. + If you enable :py:data:`SECURITY_UNIFIED_SIGNIN` and set ``sms`` as a :py:data:`SECURITY_US_ENABLED_METHODS` + the following would be necessary:: - 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. + [ + {"email": {"mapper": uia_email_mapper, "case_insensitive": True}}, + {"us_phone_number": {"mapper": uia_phone_number}}, + ] - 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. + .. versionchanged:: 4.0.0 + Changed from list to list of dict. + +.. _bleach: https://pypi.org/project/bleach/ + +.. py:data:: SECURITY_USER_IDENTITY_MAPPINGS .. versionadded:: 3.4.0 + .. deprecated:: 4.0.0 + Superseded by :py:data:`SECURITY_USER_IDENTITY_ATTRIBUTES` + +.. py:data:: SECURITY_API_ENABLED_METHODS + + Various endpoints of Flask-Security require the caller to be authenticated. + This variable controls which of the methods - ``token``, ``session``, ``basic`` + will be allowed. The default does NOT include ``basic`` since if ``basic`` + is in the list, and if the user is NOT authenticated, then the standard/required + response of 401 with the ``WWW-Authenticate`` header is returned. This is + rarely what the client wants. + + Default: ``["session", "token"]``. + + .. versionadded:: 4.0.0 .. py:data:: SECURITY_DEFAULT_REMEMBER_ME @@ -326,12 +369,6 @@ 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. @@ -388,12 +425,13 @@ .. 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. + This is used to protect the endpoint: :py:data:`SECURITY_US_SETUP_URL`, and + :py:data:`SECURITY_TWO_FACTOR_SETUP_URL`. 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. + Please see :meth:`flask_security.check_and_update_authn_fresh` for details. Default: timedelta(hours=24) @@ -403,8 +441,8 @@ 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. + This is used to protect the endpoint: :py:data:`SECURITY_US_SETUP_URL`, and + :py:data:`SECURITY_TWO_FACTOR_SETUP_URL`. 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. @@ -447,6 +485,11 @@ Remember tokens are used instead of user ID's as it is more secure. Default: ``"remember-salt"``. +.. py:data:: SECURITY_TWO_FACTOR_VALIDITY_SALT + + Specifies the salt value when generating two factor validity tokens. + + Default: ``"tf-validity-salt"``. .. py:data:: SECURITY_US_SETUP_SALT Default: ``"us-setup-salt"`` @@ -489,7 +532,6 @@ .. _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/ @@ -519,16 +561,16 @@ .. 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. + or an endpoint name. Defaults to the Flask config ``APPLICATION_ROOT`` value which itself defaults to ``"/"``. - Default: ``"/"``. + Default: ``APPLICATION_ROOT``. .. 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. + Specifies the default view to redirect to after a user logs out. This value can be set to a URL + or an endpoint name. Defaults to the Flask config ``APPLICATION_ROOT`` value which itself defaults to ``"/"``. - Default: ``"/"``. + Default: ``APPLICATION_ROOT``. .. py:data:: SECURITY_UNAUTHORIZED_VIEW @@ -543,7 +585,7 @@ Specifies the path to the template for the user login page. - Default:``security/login_user.html``. + Default: ``"security/login_user.html"``. .. py:data:: SECURITY_VERIFY_URL @@ -552,6 +594,13 @@ Default: ``"/verify"`` + +.. py:data:: SECURITY_VERIFY_TEMPLATE + + Specifies the path to the template for the verify password page. + + Default: ``"security/verify.html"``. + .. py:data:: SECURITY_POST_VERIFY_URL Specifies the default view to redirect to after a user successfully re-authenticates either via @@ -657,7 +706,15 @@ Specifies if a user may login before confirming their email when the value of ``SECURITY_CONFIRMABLE`` is set to ``True``. - Default:``False``. + Default: ``False``. +.. py:data:: SECURITY_REQUIRES_CONFIRMATION_ERROR_VIEW + + Specifies a redirect page if the users tries to login, reset password or us-signin with an unconfirmed account. + If an URL endpoint is specified, flashes an error messages and passes user email as an argument. + For us-signin, no argument is specified: it simply flashes the error message and redirects. + Default behavior is to reload the form with an error message without redirecting to an other page. + + Default: ``None``. Changeable ---------- @@ -866,12 +923,6 @@ 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 @@ -883,21 +934,35 @@ 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. +.. py:data:: SECURITY_TWO_FACTOR_ALWAYS_VALIDATE + + Specifies whether the application should require a two factor code upon every login. + If set to ``False`` then the 2 values below are used to determine when + a code is required. Note that this is cookie based - so a new browser + session will always require a fresh two-factor code. + + Default: ``True``. +.. py:data:: SECURITY_TWO_FACTOR_LOGIN_VALIDITY + + Specifies the expiration of the two factor validity cookie and verification of the token. + + Default: ``30 Days``. + + +.. py:data:: SECURITY_TWO_FACTOR_VALIDITY_COOKIE + + A dictionary containing the parameters of the two factor validity cookie. + The complete set of parameters is described in Flask's `set_cookie`_ documentation. + + Default: ``{'httponly': True, 'secure': False, 'samesite': None}``. - Default: ``"/tf-confirm"``. Unified Signin -------------- @@ -937,12 +1002,6 @@ 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 @@ -985,6 +1044,11 @@ 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. + If you select ``sms`` then make sure you add this to :py:data:`SECURITY_USER_IDENTITY_ATTRIBUTES`:: + + {"us_phone_number": {"mapper": uia_phone_number}}, + + Default: ``["password", "email", "authenticator", "sms"]`` - which are the only supported options. .. py:data:: SECURITY_US_MFA_REQUIRED @@ -1004,6 +1068,8 @@ .. py:data:: SECURITY_US_EMAIL_SUBJECT + Sets the email subject when sending the verification code via email. + Default: ``_("Verification Code")`` .. py:data:: SECURITY_US_SETUP_WITHIN @@ -1023,9 +1089,7 @@ 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_USER_IDENTITY_ATTRIBUTES` - Defines the order and methods for parsing and validating 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` @@ -1109,15 +1173,14 @@ * ``SECURITY_LOGIN_URL`` * ``SECURITY_LOGOUT_URL`` +* :py:data:`SECURITY_VERIFY_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`` @@ -1130,7 +1193,6 @@ * ``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` @@ -1144,6 +1206,7 @@ * ``SECURITY_FORGOT_PASSWORD_TEMPLATE`` * ``SECURITY_LOGIN_USER_TEMPLATE`` +* :py:data:`SECURITY_VERIFY_TEMPLATE` * ``SECURITY_REGISTER_USER_TEMPLATE`` * ``SECURITY_RESET_PASSWORD_TEMPLATE`` * ``SECURITY_CHANGE_PASSWORD_TEMPLATE`` @@ -1151,7 +1214,6 @@ * ``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` @@ -1165,6 +1227,7 @@ The default messages and error levels can be found in ``core.py``. * ``SECURITY_MSG_ALREADY_CONFIRMED`` +* ``SECURITY_MSG_API_ERROR`` * ``SECURITY_MSG_ANONYMOUS_USER_REQUIRED`` * ``SECURITY_MSG_CONFIRMATION_EXPIRED`` * ``SECURITY_MSG_CONFIRMATION_REQUEST`` @@ -1176,6 +1239,7 @@ * ``SECURITY_MSG_EMAIL_NOT_PROVIDED`` * ``SECURITY_MSG_FAILED_TO_SEND_CODE`` * ``SECURITY_MSG_FORGOT_PASSWORD`` +* ``SECURITY_MSG_IDENTITY_ALREADY_ASSOCIATED`` * ``SECURITY_MSG_INVALID_CODE`` * ``SECURITY_MSG_INVALID_CONFIRMATION_TOKEN`` * ``SECURITY_MSG_INVALID_EMAIL_ADDRESS`` @@ -1208,8 +1272,6 @@ * ``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`` diff -Nru flask-security-3.4.2/docs/conf.py flask-security-4.0.0/docs/conf.py --- flask-security-3.4.2/docs/conf.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/docs/conf.py 2021-01-26 02:39:51.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Flask-Security documentation build configuration file, created by # sphinx-quickstart on Mon Mar 12 15:35:21 2012. @@ -49,8 +48,8 @@ master_doc = "index" # General information about the project. -project = u"Flask-Security" -copyright = u"2012-2020" +project = "Flask-Security" +copyright = "2012-2020" author = "Matt Wright & Chris Wagner" # The version info for the project you're documenting, acts as replacement for @@ -58,7 +57,7 @@ # built documents. # # The short X.Y version. -version = "3.4.2" +version = "4.0.0" # The full version, including alpha/beta/rc tags. release = version @@ -168,7 +167,7 @@ # (source start file, target name, title, author, documentclass # [howto/manual]). latex_documents = [ - ("index", "Flask-Security.tex", u"Flask-Security Documentation", author, "manual") + ("index", "Flask-Security.tex", "Flask-Security Documentation", author, "manual") ] # The name of an image file (relative to this directory) to place at the top of @@ -201,8 +200,8 @@ ( "index", "Flask-Security", - u"Flask-Security Documentation", - u"Matt Wright", + "Flask-Security Documentation", + "Matt Wright", "Flask-Security", "One line description of project.", "Miscellaneous", @@ -222,10 +221,10 @@ # -- 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" +epub_title = "Flask-Security" +epub_author = "Matt Wright" +epub_publisher = "J. Christopher Wagner" +epub_copyright = "2012-2020" # The language of the text. It defaults to the language option # or en if the language is not set. diff -Nru flask-security-3.4.2/docs/contributing.rst flask-security-4.0.0/docs/contributing.rst --- flask-security-3.4.2/docs/contributing.rst 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/docs/contributing.rst 2021-01-26 02:39:51.000000000 +0000 @@ -1 +1 @@ -.. include:: ../CONTRIBUTING.rst \ No newline at end of file +.. include:: ../CONTRIBUTING.rst diff -Nru flask-security-3.4.2/docs/customizing.rst flask-security-4.0.0/docs/customizing.rst --- flask-security-3.4.2/docs/customizing.rst 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/docs/customizing.rst 2021-01-26 02:39:51.000000000 +0000 @@ -22,7 +22,6 @@ * `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` @@ -71,7 +70,6 @@ * ``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 @@ -118,7 +116,6 @@ * ``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 @@ -131,17 +128,26 @@ Localization ------------ All messages, form labels, and form strings are localizable. Flask-Security uses +`Flask-Babel `_ or `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. +Flask-Security places the method ``_fsdomain`` in jinja2's global environment and +uses that in all templates. 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") }} +Be aware that Flask-Security will validate and normalize email input using the +`email_validator `_ package. +The normalized form is stored in the DB. + + +.. _emails_topic: + Emails ------ @@ -156,9 +162,9 @@ * `security/email/reset_instructions.html` * `security/email/reset_instructions.txt` * `security/email/reset_notice.html` +* `security/email/reset_notice.txt` * `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` @@ -187,103 +193,89 @@ return dict(hello="world") -Emails with Celery ------------------- +There are many configuration variables associated with emails, and each template +will receive a slightly different context. The ``Gate Config`` column are configuration variables that if set +to ``False`` will bypass sending of the email (they all default to ``True``). +In most cases, in addition to an email being sent, a :ref:`Signal ` is sent. +The table below summarizes all this: -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:: +============================= ================================ ====================================== ====================== =============================== +**Template Name** **Gate Config** **Subject Config** **Context Vars** **Signal Sent** +----------------------------- -------------------------------- -------------------------------------- ---------------------- ------------------------------- +confirmation_instructions N/A EMAIL_SUBJECT_CONFIRM - user confirm_instructions_sent + - confirmation_link +login_instructions N/A EMAIL_SUBJECT_PASSWORDLESS - user login_instructions_sent + - login_link - 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:: +reset_instructions SEND_PASSWORD_RESET_EMAIL EMAIL_SUBJECT_PASSWORD_RESET - user reset_password_instructions_sent + - reset_link - 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() +reset_notice SEND_PASSWORD_RESET_NOTICE_EMAIL EMAIL_SUBJECT_PASSWORD_NOTICE - user password_reset - def create_app(config): - """Initialize Flask instance.""" +change_notice SEND_PASSWORD_CHANGE_EMAIL EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE - user password_changed - app = Flask(__name__) - app.config.from_object(config) +welcome - @celery.task - def send_flask_mail(**kwargs): - mail.send(Message(**kwargs)) +two_factor_instructions N/A EMAIL_SUBJECT_TWO_FACTOR - user tf_security_token_sent + - token + - username - mail.init_app(app) - datastore = SQLAlchemyUserDatastore(db, User, Role) - security_ctx = security.init_app(app, datastore) +two_factor_rescue N/A EMAIL_SUBJECT_TWO_FACTOR_RESCUE - user N/A - # 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, - ) +us_instructions N/A US_EMAIL_SUBJECT - user us_security_token_sent + - token + - login_link + - username +============================= ================================ ====================================== ====================== =============================== - # A shortcut. - security_ctx.send_mail_task(send_flask_mail.delay) +When sending an email, Flask-Security goes through the following steps: - return app + #. Calls the email context processor as described above -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:: + #. Calls ``render_template`` (as configured at Flask-Security initialization time) with the + context and template to produce a text and/or html version of the message - @celery.task - def send_flask_mail(**kwargs): - mail.send(Message(**kwargs)) + #. Calls :meth:`.MailUtil.send_mail` with all the required parameters. - @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) +The default implementation of ``MailUtil.send_mail`` uses Flask-Mail to create and send the message. +By providing your own implementation, you can use any available python email handling package. -.. _Celery: http://www.celeryproject.org/ +Emails with Celery +++++++++++++++++++ +Sometimes it makes sense to send emails via a task queue, such as `Celery`_. +This is supported by providing your own implementation of the :class:`.MailUtil` class:: -Custom send_mail method ------------------------ + from flask_security import MailUtil + class MyMailUtil(MailUtil): -It's also possible to completely override the ``security.send_mail`` method to -implement your own logic. + def send_mail(self, template, subject, recipient, sender, body, html, user, **kwargs): + send_flask_mail.delay( + subject=subject, + sender=sender, + recipients=recipients, + body=body, + html=html, + ) -For example, you might want to use an alternative email library like `Flask-Emails`:: +Then register your class as part of Flask-Security initialization:: from flask import Flask + from flask_mail import Mail, Message from flask_security import Security, SQLAlchemyUserDatastore - from flask_emails import Message + from celery import Celery + + mail = Mail() + security = Security() + celery = Celery() + + + @celery.task + def send_flask_mail(**kwargs): + mail.send(Message(**kwargs)) def create_app(config): """Initialize Flask instance.""" @@ -291,22 +283,14 @@ 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]) - + mail.init_app(app) datastore = SQLAlchemyUserDatastore(db, User, Role) - Security(app, datastore, send_mail=custom_send_mail) + security.init_app(app, datastore, mail_util_cls=MyMailUtil) return app -.. note:: +.. _Celery: http://www.celeryproject.org/ - The above ``security.send_mail_task`` override will be useless if you - override the entire ``send_mail`` method. .. _responsetopic: @@ -320,25 +304,29 @@ 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 +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: +tandem with Flask-Login, behave 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 + * If authentication fails as the result of a `@login_required`, `@auth_required("session", "token")`, + 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 authentication fails as the result of a `@http_auth_required` or `@auth_required("basic")` + then a 401 is returned along with the http header ``WWW-Authenticate`` set to + ``Basic realm="xxxx"``. The realm name is defined by :py:data:`SECURITY_DEFAULT_HTTP_AUTH_REALM`. + * 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. + then if :py:data:`SECURITY_UNAUTHORIZED_VIEW` is defined, the response will redirected. + If :py:data:`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`. diff -Nru flask-security-3.4.2/docs/features.rst flask-security-4.0.0/docs/features.rst --- flask-security-3.4.2/docs/features.rst 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/docs/features.rst 2021-01-26 02:39:51.000000000 +0000 @@ -11,11 +11,11 @@ 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`_ +`alternative token`_ feature to associate the value of ``fs_uniquifier`` with the user. +(This enables easily invalidating all existing sessions for a given user without +having to change their user id). `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 +Flask-Security extends that to support configurations that would require CSRF for requests that are authenticated via session cookies, but not for requests authenticated using tokens. @@ -69,27 +69,33 @@ 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 +Token based authentication can be used by retrieving the user auth token from an +authentication endpoint (e.g. ``/login``, ``/us-signin``). +Perform an HTTP POST with a query param of ``include_auth_token`` and the authentication details +as JSON data. +A successful call will return the authentication token. This token can be used in subsequent +requests to protected resources. The auth token should be 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. +`auth_token`. -Two-factor Authentication (alpha) +Authentication tokens are generated using a uniquifier field in the +user's UserModel. By default that field is ``fs_uniquifier``. This means that +if that field is changed (via :meth:`.UserDatastore.set_uniquifier`) +then any existing authentication tokens will no longer be valid. This value is changed +whenever a user changes their password. If this is not the desired behavior then you can add an additional +attribute to the UserModel: ``fs_token_uniquifier`` and that will be used instead, thus +isolating password changes from authentication tokens. That attribute can be changed via +:meth:`.UserDatastore.set_token_uniquifier`. This attribute should have ``unique=True``. +Unlike ``fs_uniquifier``, it can be set to ``nullable`` - it will automatically be generated +at first use if null. + +.. _two-factor: + +Two-factor Authentication ---------------------------------------- +**This feature is in Beta - mostly due to it being brand new and little to no production soak time** + 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 @@ -100,12 +106,7 @@ 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 +Please read :ref:`2fa_theory_of_operation` for more details. .. _unified-sign-in: @@ -132,7 +133,7 @@ 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). +or "something you are" (biometric quality 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. @@ -173,12 +174,15 @@ Password Reset/Recovery ----------------------- -Password reset and recovery is available for when a user forgets his or her +Password reset and recovery is available for when a user forgets their 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 +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. +As with password change - this will update the the user's ``fs_uniquifier`` attribute +which will invalidate all existing sessions AND (by default) all authentication tokens. + User Registration ----------------- @@ -186,6 +190,22 @@ 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. +User email is validated and normalized using the +`email_validator `_ package. + +Password Change +--------------- +Flask-Security comes packaged with a basic change user password view. Unlike password +recovery, this endpoint is used when the user is already authenticated. The result +of a successful password change is not only a new password, but a new value for ``fs_uniquifier``. +This has the effect is immediately invalidating all existing sessions. The change request +itself effectively re-logs in the user so a new session is created. Note that since the user +is effectively re-logged in, the same signals are sent as when the user normally authenticates. + +*NOTE* The ``fs_uniquifier`` by default, controls both sessions and authenticated tokens. +Thus changing the password also invalidates all authentication tokens. This may not be desirable +behavior, so if the UserModel contains an attribute ``fs_token_uniquifier``, then that will be used +when generating authentication tokens and so won't be affected by password changes. Login Tracking diff -Nru flask-security-3.4.2/docs/.gitignore flask-security-4.0.0/docs/.gitignore --- flask-security-3.4.2/docs/.gitignore 1970-01-01 00:00:00.000000000 +0000 +++ flask-security-4.0.0/docs/.gitignore 2021-01-26 02:39:51.000000000 +0000 @@ -0,0 +1 @@ +_build diff -Nru flask-security-3.4.2/docs/index.rst flask-security-4.0.0/docs/index.rst --- flask-security-3.4.2/docs/index.rst 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/docs/index.rst 2021-01-26 02:39:51.000000000 +0000 @@ -30,13 +30,13 @@ Many of these features are made possible by integrating various Flask extensions and libraries. They include: -1. `Flask-Login `_ -2. `Flask-Mail `_ -3. `Flask-Principal `_ -4. `Flask-WTF `_ -5. `itsdangerous `_ -6. `passlib `_ -7. `PyQRCode `_ +* `Flask-Login `_ +* `Flask-Mail `_ +* `Flask-Principal `_ +* `Flask-WTF `_ +* `itsdangerous `_ +* `passlib `_ +* `PyQRCode `_ Additionally, it assumes you'll be using a common library for your database connections and model definitions. Flask-Security supports the following Flask diff -Nru flask-security-3.4.2/docs/models.rst flask-security-4.0.0/docs/models.rst --- flask-security-3.4.2/docs/models.rst 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/docs/models.rst 2021-01-26 02:39:51.000000000 +0000 @@ -19,12 +19,15 @@ 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: +Your `User` model needs a Primary Key - Flask-Security doesn't actually reference +this - so it can be any name or type your application needs. It should be used in the +foreign relationship between `User` and `Role`. + +At the bare minimum your `User` and `Role` model should include the following fields: **User** -* ``id`` (primary key - integer, string, or uuid) +* primary key * ``email`` (for most features - unique, non-nullable) * ``password`` (non-nullable) * ``active`` (boolean, non-nullable) @@ -33,7 +36,7 @@ **Role** -* ``id`` (primary key - integer) +* primary key * ``name`` (unique, non-nullable) * ``description`` (string) @@ -95,6 +98,13 @@ * ``us_phone_number`` (string) +Separate Identity Domains +~~~~~~~~~~~~~~~~~~~~~~~~~ +If you want authentication tokens to not be invalidated when the user changes their +password add the following to your `User` model: + +* ``fs_token_uniquifier`` (unique, non-nullable) + Permissions ^^^^^^^^^^^ If you want to protect endpoints with permissions, and assign permissions to roles @@ -121,9 +131,8 @@ # Custom User Payload def get_security_payload(self): - return { - 'id': self.id, - 'name': self.name, - 'email': self.email - } - + rv = super().get_security_payload() + # :meth:`User.calc_username` + rv["username"] = self.calc_username() + rv["confirmation_needed"] = self.confirmed_at is None + return rv diff -Nru flask-security-3.4.2/docs/openapi.yaml flask-security-4.0.0/docs/openapi.yaml --- flask-security-3.4.2/docs/openapi.yaml 1970-01-01 00:00:00.000000000 +0000 +++ flask-security-4.0.0/docs/openapi.yaml 2021-01-26 02:39:51.000000000 +0000 @@ -0,0 +1,1532 @@ +openapi: 3.0.0 +info: + description: | + Default API for Flask-Security. + + __N.B. This is preliminary.__ + + Since Flask-Security is middleware, with many possible configurations this is a + guide to how the APIs will behave using standard defaults. + + By default, all POST requests require a CSRF token. This is handled automatically + if you render the form from your Flask application. If you send JSON, then you must include a request header (configured via __SECURITY_CSRF_HEADER__). + Please read the online documentation to find out details on how CSRF can be configured. + + You can download the latest spec from: https://github.com/Flask-Middleware/flask-security/blob/master/docs/openapi.yaml + version: 1.0.0 + title: "Flask-Security External API" + contact: + name: Flask-Security-Too + url: https://github.com/Flask-Middleware/flask-security + license: + name: MIT + url: https://github.com/Flask-Middleware/flask-security/blob/master/LICENSE +paths: + /login: + get: + summary: Retrieve login form and/or user information + responses: + 200: + description: > + Login form or user information. The JSON response will always + carry the csrf_token information. If the caller is logged in, then + additional information is returned. This can be very useful for single-page applications where during a force refresh, all state is lost. + By performing this GET, the session cookie will authenticate the user and the response will contain user information. + content: + text/html: + schema: + example: render_template(SECURITY_LOGIN_USER_TEMPLATE) + application/json: + schema: + $ref: "#/components/schemas/DefaultJsonResponse" + 302: + description: Response when already logged in (non-JSON request) + headers: + Location: + description: Redirect to ``SECURITY_POST_LOGIN_VIEW`` + schema: + type: string + format: uri + post: + summary: Login to application + description: Supports both json and form request types. If the caller is already logged in, then in the form case, they are redirected to SECURITY_POST_LOGIN_VIEW, for a json request, a 400 is returned. + parameters: + - name: next + in: query + description: > + URL to redirect to on successful login. Ignored for json request. + schema: + type: string + - $ref: "#/components/parameters/include_auth_token" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Login" + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/Login" + responses: + 200: + description: Login response + content: + application/json: + schema: + $ref: "#/components/schemas/LoginJsonResponse" + text/html: + schema: + description: Unsuccessful login + type: string + example: render_template(SECURITY_LOGIN_USER_TEMPLATE) with error values + 302: + description: > + If the caller already authenticated, the form contents is ignored and a + redirect is done: redirect(next) or redirect(SECURITY_POST_LOGIN_VIEW). + + If the caller is NOT already authenticated, and the form contents are + validated the caller will be redirected to: + redirect(next) or redirect(SECURITY_POST_LOGIN_VIEW) + headers: + Location: + description: Redirect to ``SECURITY_POST_LOGIN_VIEW`` + schema: + type: string + 400: + description: Errors while validating login, or caller already authenticated/logged in. + content: + application/json: + schema: + $ref: "#/components/schemas/DefaultJsonErrorResponse" + /login(passwordless): + get: + summary: Return passwordless login form + responses: + 200: + description: Passwordless login form + content: + text/html: + schema: + type: string + example: render_template(SECURITY_SEND_LOGIN_TEMPLATE) + post: + summary: Send passwordless login instructions email + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/EmailLink" + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/EmailLink" + responses: + 200: + description: > + Passwordless login response. For forms both success and validation errors. + content: + text/html: + schema: + description: Passwordless login form - with errors. + type: string + example: render_template(SECURITY_SEND_LOGIN_TEMPLATE) + application/json: + schema: + $ref: "#/components/schemas/DefaultJsonResponse" + 400: + description: Errors while validating form + content: + application/json: + schema: + $ref: "#/components/schemas/DefaultJsonErrorResponse" + /login(passwordless)/{token}: + parameters: + - name: token + in: path + required: true + schema: + type: string + get: + summary: Login via token + description: > + This is the result of getting a passwordless login token and is usually + the result of clicking the link from a passwordless email. + This ALWAYS results in a 302 redirect. + responses: + 302: + description: > + Redirects depending on success/error and whether + __SECURITY_REDIRECT_BEHAVIOR__ == 'spa'. + headers: + Location: + description: | + On spa-success: SECURITY_POST_LOGIN_VIEW?identity={identity}&email={email} + + On spa-error-expired: SECURITY_LOGIN_ERROR_VIEW?error={msg}&identity={identity}&email={email} + + On spa-error-invalid-token: SECURITY_LOGIN_ERROR_VIEW?error={msg} + + On form-success: SECURITY_POST_LOGIN_VIEW + + On form-error-expired: SECURITY_LOGIN_VIEW + + On form-error-invalid-token: SECURITY_LOGIN_VIEW + schema: + type: string + /logout: + get: + summary: Log out current user + responses: + 302: + description: Successful logout + headers: + Location: + description: Redirect to ``SECURITY_POST_LOGOUT_VIEW`` + schema: + type: string + post: + summary: Log out current user + responses: + 200: + description: Successful logout + content: + application/json: + schema: + type: object + required: [meta] + properties: + meta: + type: object + required: [code] + properties: + code: + type: integer + example: 200 + description: Http status code + /register: + get: + summary: Return register form + responses: + 200: + description: Register form + content: + text/html: + schema: + type: string + example: render_template(SECURITY_REGISTER_USER_TEMPLATE) + 302: + description: Response when already logged in + headers: + Location: + description: Redirect to ``SECURITY_POST_LOGIN_VIEW`` + schema: + type: string + post: + summary: Register new user with application + parameters: + - name: next + in: query + description: > + URL to redirect to on successful registration. Ignored for json request. + schema: + type: string + + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Register" + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/RegisterForm" + responses: + 200: + description: Register response + content: + application/json: + schema: + $ref: "#/components/schemas/DefaultJsonResponse" + text/html: + schema: + description: Unsuccessful registration + type: string + example: render_template(SECURITY_REGISTER_USER_TEMPLATE) with error values + 302: + description: > + Successful registration with form data body. + headers: + Location: + description: redirect to ``next`` or ``SECURITY_POST_REGISTER_VIEW`` + schema: + type: string + 400: + description: Errors while validating registration form + content: + application/json: + schema: + $ref: "#/components/schemas/DefaultJsonErrorResponse" + /change: + get: + summary: Return change password form + responses: + 200: + description: change password form + content: + text/html: + schema: + example: render_template(SECURITY_CHANGE_PASSWORD_TEMPLATE) + post: + summary: Change password + parameters: + - name: X-XSRF-Token + in: header + schema: + $ref: "#/components/headers/X-CSRF-Token" + - $ref: "#/components/parameters/include_auth_token" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ChangePassword" + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/ChangePassword" + responses: + 200: + description: Change password response. + content: + text/html: + schema: + description: Change form validation error. + type: string + example: render_template(SECURITY_CHANGE_PASSWORD_TEMPLATE) with error values + application/json: + schema: + $ref: "#/components/schemas/JsonResponseWithToken" + 302: + description: Password has been changed (non-json) + headers: + Location: + description: | + On success: Redirect to ``SECURITY_POST_CHANGE_VIEW`` or + ``SECURITY_POST_LOGIN_VIEW`` + schema: + type: string + 400: + description: Errors while validating form + content: + application/json: + schema: + $ref: "#/components/schemas/DefaultJsonErrorResponse" + /reset: + get: + summary: Return reset password form + responses: + 200: + description: Reset password form + content: + text/html: + schema: + type: string + example: render_template(SECURITY_FORGOT_PASSWORD_TEMPLATE) + 302: + description: Response when already logged in + headers: + Location: + description: Redirect to ``SECURITY_POST_LOGIN_VIEW`` + schema: + type: string + post: + summary: Send reset password instructions email + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/EmailLink" + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/EmailLink" + responses: + 200: + description: > + Reset password response. For forms both success and validation errors. + content: + text/html: + schema: + description: Forgot password form - with errors. + type: string + example: render_template(SECURITY_FORGOT_PASSWORD_TEMPLATE) + application/json: + schema: + $ref: "#/components/schemas/DefaultJsonResponseNoUser" + 400: + description: Errors while validating form + content: + application/json: + schema: + $ref: "#/components/schemas/DefaultJsonErrorResponse" + /reset/{token}: + parameters: + - name: token + in: path + required: true + schema: + type: string + get: + summary: Request to reset password + description: > + This is the result of getting a reset-password token and is usually + the result of clicking the link from a reset-password email. + If __SECURITY_REDIRECT_BEHAVIOR__ == 'spa' then a 302 is always returned. + responses: + 200: + description: Reset password form + content: + text/html: + schema: + type: string + example: render_template(SECURITY_RESET_PASSWORD_TEMPLATE) + 302: + description: > + Redirects depending on success/error and whether + __SECURITY_REDIRECT_BEHAVIOR__ == 'spa'. + headers: + Location: + description: | + On spa-success: SECURITY_RESET_VIEW?token={token}&identity={identity}&email={email} + + On spa-error-expired: SECURITY_RESET_ERROR_VIEW?error={msg}&identity={identity}&email={email} + + On spa-error-invalid-token: SECURITY_RESET_ERROR_VIEW?error={msg} + + On default-error: redirect(SECURITY_FORGOT_PASSWORD) + schema: + type: string + post: + summary: Reset password + parameters: + - $ref: "#/components/parameters/include_auth_token" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ResetPassword" + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/ResetPassword" + responses: + 200: + description: Reset response + content: + text/html: + schema: + description: Reset form validation error. + type: string + example: render_template(SECURITY_RESET_PASSWORD_TEMPLATE) with error values + application/json: + schema: + $ref: "#/components/schemas/JsonResponseWithToken" + 302: + description: Password has been reset or validation error (non-json) + headers: + Location: + description: | + On success: redirect(SECURITY_POST_RESET_VIEW) or + redirect(SECURITY_POST_LOGIN_VIEW) + + On invalid/expired token: redirect(SECURITY_FORGOT_PASSWORD) + schema: + type: string + 400: + description: Errors while validating form + content: + application/json: + schema: + $ref: "#/components/schemas/DefaultJsonErrorResponse" + /confirm: + get: + summary: Return send confirmation form + responses: + 200: + description: Confirmation form + content: + text/html: + schema: + example: render_template(SECURITY_SEND_CONFIRMATION_TEMPLATE) + post: + summary: Send confirmation email + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/EmailLink" + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/EmailLink" + responses: + 200: + description: > + Confirmation response. For forms both success and validation errors. + content: + text/html: + schema: + description: Confirmation form - with errors. + type: string + example: render_template(SECURITY_SEND_CONFIRMATION_TEMPLATE) + application/json: + schema: + $ref: "#/components/schemas/DefaultJsonResponse" + 400: + description: Errors while validating form + content: + application/json: + schema: + $ref: "#/components/schemas/DefaultJsonErrorResponse" + /confirm/{token}: + parameters: + - name: token + in: path + required: true + schema: + type: string + get: + summary: Request to confirm account + description: > + This is the result of getting a confirmation token and is usually + the result of clicking the link from a confirmation email. + This ALWAYS results in a 302 redirect. + By default (unless __SECURITY_AUTO_LOGIN_AFTER_CONFIRM__ == False), the user + denoted by the token is logged in as a side-effect. + responses: + 302: + description: > + Redirects depending on success/error and whether + __SECURITY_REDIRECT_BEHAVIOR__ == 'spa'. + headers: + Location: + description: | + On spa-success: SECURITY_POST_CONFIRM_VIEW?identity={identity}&email={email}&{level}={msg} + + On spa-error-expired: SECURITY_CONFIRM_ERROR_VIEW?error={msg}&identity={identity}&email={email} + + On spa-error-invalid-token: SECURITY_CONFIRM_ERROR_VIEW?error={msg} + + On form-success: SECURITY_POST_CONFIRM_VIEW or + SECURITY_POST_LOGIN_VIEW + + On form-success (no auto-login): SECURITY_POST_CONFIRM_VIEW or + SECURITY_LOGIN_URL + + On form-error-expired: SECURITY_CONFIRM_ERROR_VIEW or + SECURITY_CONFIRM_URL + + On form-error-invalid-token: SECURITY_CONFIRM_ERROR_VIEW or + SECURITY_CONFIRM_URL + schema: + type: string + /us-signin: + get: + summary: Unified Sign In. + responses: + 200: + description: Sign in form + content: + text/html: + schema: + example: render_template(SECURITY_US_SIGNIN_TEMPLATE) + application/json: + schema: + type: object + properties: + available_methods: + type: string + description: Config setting SECURITY_US_ENABLED_METHODS + code_methods: + type: string + description: All SECURITY_US_ENABLED_METHODS that require a code to be generated and sent. + identity_attributes: + type: string + description: Configuration setting SECURITY_USER_IDENTITY_ATTRIBUTES + post: + summary: Unified Sign In. + parameters: + - $ref: "#/components/parameters/include_auth_token" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/UsSignin" + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/UsSignin" + responses: + 200: + description: Unified Sign In response + content: + application/json: + schema: + $ref: "#/components/schemas/UsSigninJsonResponse" + text/html: + schema: + description: Unsuccessful sign in + type: string + example: render_template(SECURITY_US_SIGNIN_TEMPLATE) with error values + 302: + description: > + If the caller already authenticated, the form contents is ignored and a + redirect is done: redirect(next) or redirect(SECURITY_POST_LOGIN_VIEW). + + If the caller is NOT already authenticated, and the form contents are + validated the caller will be redirected to: + redirect(next) or redirect(SECURITY_POST_LOGIN_VIEW) + headers: + Location: + description: Redirect to ``SECURITY_POST_LOGIN_VIEW`` + schema: + type: string + 400: + description: Errors while validating attributes, or caller already authenticated/logged in. + content: + application/json: + schema: + $ref: "#/components/schemas/DefaultJsonErrorResponse" + /us-signin/send-code: + get: + summary: Unified Sign In send authentication code + responses: + 200: + description: Send Code form + content: + text/html: + schema: + example: render_template(SECURITY_US_SIGNIN_TEMPLATE) + application/json: + schema: + type: object + properties: + methods: + type: string + description: Config setting SECURITY_US_ENABLED_METHODS + code_methods: + type: string + description: All SECURITY_US_ENABLED_METHODS that require a code to be generated and sent. + identity_attributes: + type: string + description: Configuration setting SECURITY_USER_IDENTITY_ATTRIBUTES + post: + summary: Send Code for unified sign in. + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/UsSigninSendCode" + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/UsSigninSendCode" + responses: + 200: + description: Send code response + content: + application/json: + schema: + description: Code successfully sent + text/html: + schema: + description: Validation error, code send error, or code successfully sent + type: string + example: render_template(SECURITY_US_SIGNIN_TEMPLATE) with error values + 400: + description: Errors while validating attributes. + content: + application/json: + schema: + $ref: "#/components/schemas/DefaultJsonErrorResponse" + 500: + description: Error when trying to send code. + content: + application/json: + schema: + $ref: "#/components/schemas/DefaultJsonErrorResponse" + + /us-verify: + get: + summary: Unified sign in re-authentication. + description: > + If an endpoint is protected with @auth_required() with a freshness declaration + this endpoint will be called to request an already signed in user to re-authenticate. + responses: + 200: + description: Verify/re-authenticate form + content: + text/html: + schema: + example: render_template(SECURITY_US_VERIFY_TEMPLATE) + application/json: + schema: + type: object + properties: + available_methods: + type: string + description: Config setting SECURITY_US_ENABLED_METHODS + code_methods: + type: string + description: All SECURITY_US_ENABLED_METHODS that require a code to be generated and sent. + post: + summary: Unified sign in re-authentication + parameters: + - $ref: "#/components/parameters/include_auth_token" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/UsSigninVerify" + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/UsSigninVerify" + responses: + 200: + description: Verify/re-authenticate response. + content: + application/json: + schema: + allOf: + - description: > + The user successfully re-authenticated. + - $ref: "#/components/schemas/JsonResponseWithToken" + text/html: + schema: + description: Unsuccessful re-authentication. + type: string + example: render_template(SECURITY_US_VERIFY_TEMPLATE) with error values + 302: + description: User successfully re-authenticated when using form based request. + headers: + Location: + description: Redirect to ``next`` or ``SECURITY_POST_VERIFY_VIEW`` + schema: + type: string + 400: + description: Errors while validating attributes. + content: + application/json: + schema: + $ref: "#/components/schemas/DefaultJsonErrorResponse" + /us-verify/send-code: + get: + summary: Unified sign in verify/re-authenticate send authentication code + responses: + 200: + description: Send Code form + content: + text/html: + schema: + example: render_template(SECURITY_US_VERIFY_TEMPLATE) + application/json: + schema: + type: object + properties: + methods: + type: string + description: Config setting SECURITY_US_ENABLED_METHODS + code_methods: + type: string + description: All SECURITY_US_ENABLED_METHODS that require a code to be generated and sent. + post: + summary: Send Code for unified sign in verify. + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/UsSigninVerifySendCode" + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/UsSigninVerifySendCode" + responses: + 200: + description: Send code response + content: + application/json: + schema: + description: Code successfully sent + text/html: + schema: + description: Validation error, code send error, or code successfully sent + type: string + example: render_template(SECURITY_US_VERIFY_TEMPLATE) with error values + 400: + description: Errors while validating attributes. + content: + application/json: + schema: + $ref: "#/components/schemas/DefaultJsonErrorResponse" + 500: + description: Error when trying to send code. + content: + application/json: + schema: + $ref: "#/components/schemas/DefaultJsonErrorResponse" + /us-setup: + get: + summary: Unified sign in setup passcode options. + responses: + 200: + description: Setup form + content: + text/html: + schema: + example: render_template(SECURITY_US_SETUP_TEMPLATE) + application/json: + schema: + type: object + properties: + available_methods: + type: string + description: Config setting SECURITY_US_ENABLED_METHODS + active_methods: + type: string + description: Methods that have already been setup. + setup_methods: + type: string + description: All SECURITY_US_ENABLED_METHODS that require setup. + identity_attributes: + type: string + description: Configuration setting SECURITY_USER_IDENTITY_ATTRIBUTES + phone: + type: string + description: existing configured phone number + post: + summary: Unified sign in setup. + description: > + An authenticated user can call this endpoint to update or add additional methods for authenticating (e.g. sms, authenticator app). This is controlled by application configuration settings SECURITY_US_ENABLED_METHODS. This endpoint is protected by a 'freshness' check - meaning the caller will be required to have authenticated recently. In addition, to ensure correctness, the newly setup method must be verified by sending and entering a code prior to it being permanently stored. This verification process is also time-limited. + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/UsSetup" + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/UsSetup" + responses: + 200: + description: Unified sign in setup response. + content: + application/json: + schema: + $ref: "#/components/schemas/UsSetupJsonResponse" + text/html: + schema: + description: Invalid form values or verification code sent successfully and should be entered into the form. + type: string + example: render_template(SECURITY_US_SETUP_TEMPLATE) with error values + 400: + description: Errors while validating attributes. + content: + application/json: + schema: + $ref: "#/components/schemas/DefaultJsonErrorResponse" + 500: + description: Error when trying to send code. + content: + application/json: + schema: + $ref: "#/components/schemas/DefaultJsonErrorResponse" + /us-setup/{token}: + parameters: + - name: token + in: path + required: true + schema: + type: string + get: + summary: Validate unified sign in setup request. + description: > + This does nothing but redirect back to the setup form. + responses: + 200: + description: Get form. + content: + text/html: + schema: + example: render_template(SECURITY_US_SETUP_TEMPLATE) + + post: + summary: Validate passcode sent and store setup method. + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/UsSetupValidateRequest" + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/UsSetupValidateRequest" + responses: + 200: + description: Successfully validated and persisted sign in method. + content: + application/json: + schema: + $ref: "#/components/schemas/UsSetupValidateJsonResponse" + 302: + description: Successfuly validated and persisted sign in method. + headers: + Location: + description: | + On form-success: SECURITY_POST_SETUP_VIEW or + SECURITY_POST_LOGIN_VIEW + schema: + type: string + 400: + description: Validation failed. + content: + application/json: + schema: + $ref: "#/components/schemas/DefaultJsonErrorResponse" + /us-verify-link: + parameters: + - name: email + in: query + required: true + schema: + type: string + - name: code + in: query + required: true + schema: + type: string + get: + summary: A magic link to authenticate (instead of manually entering a code). + description: > + This is the result of getting a passcode link and is usually + the result of clicking the link from an email. + This ALWAYS results in a 302 redirect. + N.B. Magic link with 2FA enabled does not work and the SPA will get a redirect to the login error page with tf_required. Must use code option instead. + responses: + 302: + description: > + Redirects depending on success/error and whether + __SECURITY_REDIRECT_BEHAVIOR__ == 'spa'. Also, if Two-Factor authentication has been enabled, further authentication/redirects might be required. + headers: + Location: + description: | + On spa-success: SECURITY_POST_LOGIN_VIEW?identity={identity}&email={email} + + On spa-error-expired: SECURITY_LOGIN_ERROR_VIEW?error={msg} + + On spa-error-invalid-token: SECURITY_LOGIN_ERROR_VIEW?error={msg} + + On spa-two-factor-required: SECURITY_LOGIN_ERROR_VIEW?tf_required=1 + + On form-success: SECURITY_POST_LOGIN_VIEW + + On form-error-expired: SECURITY_US_SIGNIN_URL + + On form-error-invalid-token: SECURITY_US_SIGNIN_URL + + On form-success and two-factor: SECURITY_TWO_FACTOR_TOKEN_VALIDATION_URL or SECURITY_TWO_FACTOR_SETUP_URL + schema: + type: string + + /tf-setup: + get: + summary: Two-factor authentication setup. + responses: + 200: + description: Setup form + content: + text/html: + schema: + example: render_template(SECURITY_TWO_FACTOR_SETUP_TEMPLATE) + application/json: + schema: + type: object + properties: + tf_required: + type: string + description: Config setting SECURITY_TWO_FACTOR_REQUIRED. + tf_primary_method: + type: string + description: Current (if any) setup method. + tf_available_methods: + type: string + description: Config setting SECURITY_TWO_FACTOR_ENABLED_METHODS. If SECURITY_TWO_FACTOR_REQUIRED is false then 'disable' will be part of the set. + tf_phone_number: + type: string + description: Currently configured (if any) phone number. + post: + summary: Two factor setup. + description: > + Two-factor setup can be used in three cases: + + 1) Initial login and application requires 2FA + + 2) An authenticated user wishing to change their 2FA configuration + + 3) An authenticated user wishes to enable or disable 2FA (assuming SECURITY_TWO_FACTOR_REQUIRED is False). + + + Allowed 2FA methods are controlled via the configuration SECURITY_TWO_FACTOR_ENABLED_METHODS. + + + This endpoint is protected by a 'freshness' check - meaning the caller will be required to have authenticated recently. In addition, to ensure correctness, the newly setup method must be verified by sending and entering a code prior to it being permanently stored. + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/TfSetup" + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/TfSetup" + responses: + 200: + description: > + Two factor setup response. Please note that the newly setup method must be validated PRIOR to it being stored permanently. + content: + application/json: + schema: + $ref: "#/components/schemas/TfSetupJsonResponse" + text/html: + schema: + description: Invalid form values or verification code sent successfully and should be entered into the form. + type: string + example: render_template(SECURITY_TWO_FACTOR_SETUP_TEMPLATE) with error values + 400: + description: Errors while validating attributes. + content: + application/json: + schema: + $ref: "#/components/schemas/DefaultJsonErrorResponse" + 500: + description: Error when trying to send code. + content: + application/json: + schema: + $ref: "#/components/schemas/DefaultJsonErrorResponse" + /tf-validate: + get: + summary: Retrieve form based on current two-factor state. + responses: + 200: + description: Code validation + content: + text/html: + schema: + description: > + If this is a normal, already setup method, then render_template(SECURITY_TWO_FACTOR_VERIFY_CODE_TEMPLATE) is returned; + if this is validating a new method then render_template(SECURITY_TWO_FACTOR_SETUP_TEMPLATE) is returned. + type: string + post: + summary: Send two-factor code. + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + code: + description: The code sent via the configured method (e.g. SMS, email, authenticator). + type: string + example: 12345 + application/x-www-form-urlencoded: + schema: + type: object + properties: + code: + description: The code sent via the configured method (e.g. SMS, email, authenticator). + type: string + example: 12345 + responses: + 200: + description: Two factor code validation response. + content: + application/json: + schema: + allOf: + - description: > + The code was correct, the caller is now signed in. + - $ref: "#/components/schemas/TfValidateJsonResponse" + text/html: + schema: + description: + Unsuccessfully processed code. As above, which form is + rendered depends on the state of the user's two factor configuration. + type: string + 302: + description: User successfully sent code when using form based request. The caller is not logged in. + headers: + Location: + description: Redirect to either ``next`` or ``SECURITY_POST_LOGIN_VIEW`` + schema: + type: string + 400: + description: Errors while validating attributes. + content: + application/json: + schema: + $ref: "#/components/schemas/DefaultJsonErrorResponse" + + /tf-rescue: + get: + summary: Help user that has lost authenticator or SMS device. + responses: + 200: + description: Return form. + content: + text/html: + schema: + description: > + render_template(SECURITY_TWO_FACTOR_VERIFY_CODE_TEMPLATE). + type: string + post: + summary: Request help. + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + help-setup: + description: Either 'lost_device' or 'no_mail_access'. + type: string + example: "lost_device" + application/x-www-form-urlencoded: + schema: + type: object + properties: + help_setup: + description: Either 'lost_device' or 'no_mail_access'. + type: string + example: "lost_device" + responses: + 200: + description: > + If 'lost_device' was specified, then an authentication code was sent to the email + on record for the user. If 'no_mail_access' then an email was sent to administrator address + specified by SECURITY_TWO_FACTOR_RESCUE_MAIL. + content: + application/json: + schema: + $ref: "#/components/schemas/DefaultJsonResponseNoUser" + text/html: + schema: + description: Invalid form values or verification code sent successfully and should be entered into the form. + type: string + example: render_template(SECURITY_TWO_FACTOR_VERIFY_CODE_TEMPLATE) with error values + 400: + description: Failed to send code + content: + application/json: + schema: + $ref: "#/components/schemas/DefaultJsonErrorResponse" + +components: + schemas: + Login: + type: object + required: [email, password] + properties: + email: + type: string + description: | + user identifier. This is by default an email address, but can be any (unique) + field that is part of the User model and is defined in the __SECURITY_USER_IDENTITY_ATTRIBUTES__ configuration variable. It will also match against numeric User model fields. + password: + type: string + description: Password + remember_me: + type: boolean + description: > + If true, will remember userid as part of cookie. There is a configuration variable DEFAULT_REMEMBER_ME that can be set. This field will override that. + tf_validity_token: + type: string + description: Code verifying the user has successfully verfied 2FA in the past. If verified, the user is able to skip validation of the second factor. Only used when SECURITY_TWO_FACTOR_ALWAYS_VALIDATE is False. + LoginJsonResponse: + type: object + description: > + The user successfully signed in. Note that depending on SECURITY_TWO_FACTOR configuration variables, a second form of authentication might be required. + Note that if 2FA is not configured, none of the ``tf_`` properties will be returned. + required: [meta, response] + properties: + meta: + type: object + required: [code] + properties: + code: + type: integer + example: 200 + description: Http status code + response: + type: object + properties: + authentication_token: + type: string + description: > + Token to be used in future token-based API calls. Only returned if "include_auth_token" parameter is set. + tf_required: + type: boolean + description: If two-factor authentication is required for caller. + tf_state: + type: string + description: if "setup_from_login" then the caller must go through two-factor setup endpoint. If "ready" then a code has been sent and should be supplied to SECURITY_TWO_FACTOR_TOKEN_VALIDATION_URL. + tf_primary_method: + type: string + description: Which method was used to send code. + DefaultJsonResponse: + type: object + properties: + user: + type: object + description: > + By default an empty dictionary is returned. However by overriding _User::get_security_payload()_ any attributes of the User model can be returned. + csrf_token: + type: string + description: Session CSRF token + DefaultJsonResponseNoUser: + type: object + properties: + csrf_token: + type: string + description: Session CSRF token + JsonResponseWithToken: + type: object + properties: + user: + type: object + description: > + By default an empty dictionary is returned. However by overriding _User::get_security_payload()_ any attributes of the User model can be returned. + properties: + authentication_token: + type: string + description: > + Token to be used in future token-based API calls. + Note this only returned from those APIs that accept a + 'include_auth_token' query param. + csrf_token: + type: string + description: Session CSRF token + DefaultJsonErrorResponse: + type: object + required: [meta, response] + properties: + meta: + type: object + required: [code] + properties: + code: + type: integer + example: 400 + description: Http status code + response: + type: object + description: > + For form validation errors, the 'errors' key will be set with a list of errors per + invalid form input field. For non-form related errors, the 'error' key will be set + with a single (localized) error string. + properties: + errors: + type: object + description: > + Errors per input/form field ('email' below is just an example) + properties: + email: + type: array + items: + type: string + example: Email issues. + description: Error message (localized) + error: + type: string + example: "Unauthenticated" + description: Error message (localized) + Register: + type: object + required: [email, password] + properties: + email: + type: string + description: > + user identifier. This is by default an email address, but can be any (unique) + field that is part of the User model and is defined in the __SECURITY_USER_IDENTITY_ATTRIBUTES__ configuration variable. It will also match against numeric User model fields. + password: + type: string + description: Password + RegisterForm: + type: object + required: [email, password] + properties: + email: + type: string + description: > + user identifier. This is by default an email address, but can be any (unique) + field that is part of the User model and is defined in the __SECURITY_USER_IDENTITY_ATTRIBUTES__ configuration variable. It will also match against numeric User model fields. + password: + type: string + description: Password + password_confirm: + type: string + description: > + If present, must re-type in password. This will not be present if the __SECURITY_CONFIRM__ configuration is true. + next: + type: string + description: > + Redirect URL. Overrides __SECURITY_POST_REGISTER_VIEW__. + ResetPassword: + type: object + required: [password, password_confirm] + properties: + password: + type: string + description: Password + password_confirm: + type: string + description: Password - again + ChangePassword: + type: object + required: [password, new_password, new_password_confirm] + properties: + password: + type: string + description: Password + new_password: + type: string + description: New password + new_password_confirm: + type: string + description: New password - again + EmailLink: + type: object + required: [email] + properties: + email: + type: string + description: > + Email address to send link email to. + UsSignin: + type: object + required: [identity, passcode] + properties: + identity: + type: string + description: Configured by SECURITY_USER_IDENTITY_ATTRIBUTES + example: me@you.com, +16505551212 + passcode: + type: string + description: password or code + remember_me: + type: boolean + tf_validity_token: + type: string + description: Code verifying the user has successfully verfied 2FA in the past. If verified, the user is able to skip validation of the second factor. Only used when SECURITY_TWO_FACTOR_ALWAYS_VALIDATE is False. + UsSigninJsonResponse: + type: object + description: > + The user successfully signed in. Note that depending on SECURITY_TWO_FACTOR and SECURITY_US_MFA_REQUIRED configuration variables, a second form of authentication might be required. + required: [meta, response] + properties: + meta: + type: object + required: [code] + properties: + code: + type: integer + example: 200 + description: Http status code + response: + type: object + properties: + authentication_token: + type: string + description: > + Token to be used in future token-based API calls. Only returned if "include_auth_token" parameter is set. + tf_required: + type: boolean + description: If two-factor authentication is required for caller. + tf_state: + type: string + description: if "setup_from_login" then the caller must go through two-factor setup endpoint. If "ready" then a code has been sent and should be supplied to SECURITY_TWO_FACTOR_TOKEN_VALIDATION_URL. + tf_primary_method: + type: string + description: Which method was used to send code. + UsSigninSendCode: + type: object + required: [identity, chosen_method] + properties: + identity: + type: string + description: Configured by SECURITY_USER_IDENTITY_ATTRIBUTES + example: me@you.com, +16505551212 + chosen_method: + type: string + description: which method should be used to send the code, as configured with SECURITY_US_ENABLED_METHODS + UsSigninVerify: + type: object + required: [passcode] + properties: + passcode: + type: string + description: password or code + UsSigninVerifySendCode: + type: object + required: [chosen_method] + properties: + chosen_method: + type: string + description: which method should be used to send the code, as configured with SECURITY_US_ENABLED_METHODS + UsSetup: + type: object + required: [chosen_method] + properties: + chosen_method: + type: string + description: which method should be used to send the code, as configured with SECURITY_US_ENABLED_METHODS + phone: + type: string + description: phone number (this will be normalized). Required if chosen_method == "sms". + UsSetupJsonResponse: + type: object + required: [meta, response] + properties: + meta: + type: object + required: [code] + properties: + code: + type: integer + example: 200 + description: Http status code + response: + type: object + properties: + chosen_method: + type: string + description: The chosen_method as passed into API. + phone: + type: string + description: The canonicalized phone number if setting up SMS + authr_key: + type: string + description: TOTP key for setting up authenticator (if chosen_method == 'authenticator') + authr_issuer: + type: string + description: Issuer as configured with TOTP_ISSUER (same as used in QRcode) (if chosen_method == 'authenticator') + authr_username: + type: string + description: Username (same as used in QRcode) (if chosen_method == 'authenticator') + state: + type: string + description: Opaque blob that must be pass to /us-setup/. This is a signed, timed token. + UsSetupValidateRequest: + type: object + required: [passcode] + properties: + passcode: + type: string + description: Code/Passcode as received from method being setup. + UsSetupValidateJsonResponse: + type: object + required: [meta, response] + properties: + meta: + type: object + required: [code] + properties: + code: + type: integer + example: 200 + description: Http status code + response: + type: object + properties: + chosen_method: + type: string + description: The chosen_method as passed into API. + phone: + type: string + description: Phone number if set. + TfSetup: + type: object + required: [setup] + properties: + setup: + type: string + description: > + Which method should be used to send the code, as configured with SECURITY_TWO_FACTOR_ENABLED_METHODS. + If SECURITY_TWO_FACTOR_REQUIRED is False, the additional method 'disable' is available. + example: sms + phone: + type: string + description: phone number (this will be validated for format). Required if setup == "sms". + example: 650-555-1212 + TfSetupJsonResponse: + type: object + required: [meta, response] + properties: + meta: + type: object + required: [code] + properties: + code: + type: integer + example: 200 + description: Http status code + response: + type: object + properties: + tf_state: + type: string + description: > + Current state of Two Factor configuration. Not present when disabling 2FA. This will be set to 'validating_profile' + indicating the caller needs to call '/tf-validate' with the correct code. + example: validating_profile + tf_primary_method: + type: string + description: Current method being congfigured. + example: sms + tf_authr_key: + type: string + description: TOTP key for setting up authenticator (if tf_primary_method == 'authenticator') + tf_authr_issuer: + type: string + description: Issuer as configured with TOTP_ISSUER (same as used in QRcode) (if tf_primary_method == 'authenticator') + tf_authr_username: + type: string + description: Username (same as used in QRcode) (if tf_primary_method == 'authenticator') + TfValidateJsonResponse: + type: object + properties: + user: + type: object + description: > + By default an empty dictionary is returned. However by overriding _User::get_security_payload()_ any attributes of the User model can be returned. + csrf_token: + type: string + description: Session CSRF token + tf_validity_token: + type: string + description: A timed token that verifies that the user has successfully completed 2FA. Only sent if SECURITY_TWO_FACTOR_ALWAYS_VALIDATE is False and remember_me (from /login POST) is True + + headers: + X-CSRF-Token: + description: CSRF token + schema: + type: string + parameters: + include_auth_token: + name: include_auth_token + description: If set/sent, will return an Authentication Token for user + in: query + schema: + type: string diff -Nru flask-security-3.4.2/docs/patterns.rst flask-security-4.0.0/docs/patterns.rst --- flask-security-3.4.2/docs/patterns.rst 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/docs/patterns.rst 2021-01-26 02:39:51.000000000 +0000 @@ -57,11 +57,21 @@ raise MyForbiddenException(msg='You can only update docs you own') +A note about Basic Auth ++++++++++++++++++++++++ +Basic Auth is supported in Flask-Security, using the @http_auth_required() decorator. If a request for an endpoint +protected with @http_auth_required is received, and the request doesn't contain the appropriate HTTP Headers, a 401 is returned +along with the required WWW-Authenticate header. In this case there won't be a usable session cookie returned so all future requests +will also require credentials to be sent. Effectively the caller is temporarily 'logged in' at the beginning of each request and 'logged out' again +at the end of the request. Most (all?) browsers intercept this response and pop up a login dialog box and remember, for the site, the entered credentials. +This effectively bypasses any of the normal Flask-Security login forms. By default, the Flask-Security endpoints that require the caller be +authenticated do NOT support ``basic`` - however the :py:data:`SECURITY_API_ENABLED_METHODS` can be used to override this. + 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 +convenient, and keeps 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: @@ -69,10 +79,10 @@ - :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. + - :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. +Flask-Security itself uses this as part of securing the :ref:`unified-sign-in` and :ref:`two-factor` setup endpoints. .. _pass_validation_topic: @@ -92,12 +102,19 @@ 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. +In addition to validation, unicode passwords should be normalized as specified +by NIST requirement: `5.1.1.2 Memorized Secret Verifiers`_. Normalization can +be disabled by setting the :py:data:`SECURITY_PASSWORD_NORMALIZE_FORM` to ``None``. +Validation and normalization is encapsulated in :class:`.PasswordUtil`. +This can be overridden by passing your class at app initialization time. +The :meth:`.PasswordUtil.validate` is passed additional kwargs to allow custom +validators more flexibility. 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`. +.. _5.1.1.2 Memorized Secret Verifiers: https://pages.nist.gov/800-63-3/sp800-63b.html#sec5 + .. _csrftopic: CSRF diff -Nru flask-security-3.4.2/docs/quickstart.rst flask-security-4.0.0/docs/quickstart.rst --- flask-security-3.4.2/docs/quickstart.rst 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/docs/quickstart.rst 2021-01-26 02:39:51.000000000 +0000 @@ -4,6 +4,12 @@ There are some complete (but simple) examples available in the *examples* directory of the `Flask-Security repo`_. +.. note:: + The below quickstarts are just that - they don't enable most of the features (such as registration, reset, etc.). + They basically create a single user, and you can login as that user... that's it. + As you add more features, additional packages (e.g. Flask-Mail, Flask-Babel, pyqrcode) might be required + and will need to be added to your requirements.txt (or equivalent) file. + .. 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 @@ -87,7 +93,8 @@ @app.before_first_request def create_user(): db.create_all() - user_datastore.create_user(email="test@me.com", password=hash_password("password")) + if not user_datastore.find_user(email="test@me.com"): + user_datastore.create_user(email="test@me.com", password=hash_password("password")) db.session.commit() # Views @@ -120,7 +127,7 @@ The following code sample illustrates how to get started as quickly as possible using `SQLAlchemy in a declarative way -`_: +`_: 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. @@ -153,7 +160,8 @@ @app.before_first_request def create_user(): init_db() - user_datastore.create_user(email="test@me.com", password=hash_password("password")) + if not user_datastore.find_user(email="test@me.com"): + user_datastore.create_user(email="test@me.com", password=hash_password("password")) db_session.commit() # Views @@ -171,8 +179,7 @@ from sqlalchemy.orm import scoped_session, sessionmaker from sqlalchemy.ext.declarative import declarative_base - engine = create_engine('sqlite:////tmp/test.db', \ - convert_unicode=True) + engine = create_engine('sqlite:////tmp/test.db') db_session = scoped_session(sessionmaker(autocommit=False, autoflush=False, bind=engine)) @@ -211,15 +218,15 @@ __tablename__ = 'user' id = Column(Integer, primary_key=True) email = Column(String(255), unique=True) - username = Column(String(255)) - password = Column(String(255)) + username = Column(String(255), unique=True, nullable=True) + password = Column(String(255), nullable=False) 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)) + fs_uniquifier = Column(String(255), unique=True, nullable=False) confirmed_at = Column(DateTime()) roles = relationship('Role', secondary='roles_users', backref=backref('users', lazy='dynamic')) @@ -273,12 +280,13 @@ class Role(db.Document, RoleMixin): name = db.StringField(max_length=80, unique=True) description = db.StringField(max_length=255) + permissions = 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) + fs_uniquifier = db.StringField(max_length=64, unique=True) confirmed_at = db.DateTimeField() roles = db.ListField(db.ReferenceField(Role), default=[]) @@ -289,7 +297,8 @@ # 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")) + if not user_datastore.find_user(email="test@me.com"): + user_datastore.create_user(email="test@me.com", password=hash_password("password")) # Views @app.route("/") @@ -348,15 +357,18 @@ # Create database connection object db = FlaskDB(app) - class Role(db.Model, RoleMixin): + class Role(RoleMixin, db.Model): name = CharField(unique=True) description = TextField(null=True) + permissions = TextField(null=True) - class User(db.Model, UserMixin): + # N.B. order is important since db.Model also contains a get_id() - + # we need the one from UserMixin. + class User(UserMixin, db.Model): email = TextField() password = TextField() active = BooleanField(default=True) - fs_uniquifier = TextField() + fs_uniquifier = TextField(null=False) confirmed_at = DateTimeField(null=True) class UserRoles(db.Model): @@ -368,6 +380,9 @@ name = property(lambda self: self.role.name) description = property(lambda self: self.role.description) + def get_permissions(self): + return self.role.get_permissions() + # Setup Flask-Security user_datastore = PeeweeUserDatastore(db, User, Role, UserRoles) security = Security(app, user_datastore) @@ -378,7 +393,8 @@ 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")) + if not user_datastore.find_user(email="test@me.com"): + user_datastore.create_user(email="test@me.com", password=hash_password("password")) # Views @app.route('/') @@ -395,10 +411,10 @@ 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. +Flask-Security integrates with an outgoing mail service via the ``mail_util_cls`` which +is part of initial configuration. The default class :class:`flask_security.MailUtil` utilizes the +`Flask-Mail `_ package. Be sure to add flask_mail to +your requirements.txt. The following code illustrates a basic setup, which could be added to the basic application code in the previous section:: @@ -409,7 +425,8 @@ # After 'Create app' app.config['MAIL_SERVER'] = 'smtp.example.com' app.config['MAIL_PORT'] = 465 - app.config['MAIL_USE_SSL'] = True + app.config['MAIL_USE_SSL'] = False + app.config['MAIL_USE_TLS'] = True app.config['MAIL_USERNAME'] = 'username' app.config['MAIL_PASSWORD'] = 'password' mail = Mail(app) diff -Nru flask-security-3.4.2/docs/_static/openapi_view.html flask-security-4.0.0/docs/_static/openapi_view.html --- flask-security-3.4.2/docs/_static/openapi_view.html 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/docs/_static/openapi_view.html 2021-01-26 02:39:51.000000000 +0000 @@ -10,6 +10,8 @@ allow-try="false" allow-spec-url-load="false" allow-spec-file-load="false" + show-components="true" + schema-description-expanded="true" heading-text="Flask Security External API"> `_ - Basic SQLAlchemy Two-Factor Application +++++++++++++++++++++++++++++++++++++++ @@ -67,6 +65,9 @@ app.config['SECURITY_TWO_FACTOR'] = True app.config['SECURITY_TWO_FACTOR_RESCUE_MAIL'] = 'put_your_mail@gmail.com' + app.config['SECURITY_TWO_FACTOR_ALWAYS_VALIDATE']=False + app.config['SECURITY_TWO_FACTOR_LOGIN_VALIDITY']='1 week' + # 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' @@ -118,3 +119,58 @@ if __name__ == '__main__': app.run() + +.. _2fa_theory_of_operation: + +Theory of Operation ++++++++++++++++++++++ + +.. note:: + The Two-factor feature requires that session cookies be received and sent as part of the API. + This is true regardless of if the application uses forms or JSON. + +The Two-factor (2FA) API has four paths: + + - Normal login once everything set up + - Changing 2FA setup + - Initial login/registration when 2FA is required + - Rescue + +When using forms, the flow from one state to the next is handled by the forms themselves. When using JSON +the application must of course explicitly access the appropriate endpoints. The descriptions below describe the JSON access pattern. + +Normal Login +~~~~~~~~~~~~ +In the normal case, when the user has already setup their preferred 2FA method (e.g. email, SMS, authenticator app), +then the flow starts with the authentication process using the ``/login`` or ``/us-signin`` endpoints, providing +their identity and password. If 2FA is required, the response will indicate that. Then, the application must POST to the ``/tf-validate`` +with the correct code. + +Changing 2FA Setup +~~~~~~~~~~~~~~~~~~~ +An authenticated user can change their 2FA configuration (primary_method, phone number, etc.). In order to prevent a user from being +locked out, the new configuration must be validated before it is stored permanently. The user starts with a GET on ``/tf-setup``. This will return +a list of configured 2FA methods the user can choose from, and the existing configuration. This must be followed with a POST on ``/tf-setup`` with the new primary +method (and phone number if SMS). In the case of SMS, a code will be sent to the phone/device and again use ``/tf-validate`` to confirm code. +In the case of setting up an authenticator app, the response to the POST will contain the QRcode image as well +as the required information for manual entry. +Once the code has been successfully +entered, the new configuration will be permanently stored. + +Initial login/registration +~~~~~~~~~~~~~~~~~~~~~~~~~~~ +This is basically a combination of the above two - initial POST to ``/login`` will return indicating that 2FA is required. The user must then POST to ``/tf_setup`` to setup +the desired 2FA method, and finally have the user enter the code and POST to ``/tf-validate``. + +Rescue +~~~~~~ +Life happens - if the user doesn't have their mobile devices (SMS) or authenticator app, then they can request using ``/tf-rescue`` endpoint to have the code sent to their email. +If they have lost access to their email, they can request an email be sent to the application administrators. + +Validity +~~~~~~~~ +Sometimes it can be preferrable to enter the 2FA code once a day/week/month, especially if a user logs in and out of a website multiple times. This allows the +security of a two factor authentication but with a slightly better user experience. This can be achevied by setting ``SECURITY_TWO_FACTOR_ALWAYS_VALIDATE`` to ``False``, +and clicking the 'Remember' button on the login form. Once the two factor code is validated, a cookie is set to allow skipping the validation step. The cookie is named +``tf_validity`` and contains the signed token containing the user's ``fs_uniquifier``. The cookie and token are both set to expire after the time delta given in +``SECURITY_TWO_FACTOR_LOGIN_VALIDITY``. Note that setting ``SECURITY_TWO_FACTOR_LOGIN_VALIDITY`` to 0 is equivalent to ``SECURITY_TWO_FACTOR_ALWAYS_VALIDATE`` being ``True``. diff -Nru flask-security-3.4.2/flask_security/async_compat.py flask-security-4.0.0/flask_security/async_compat.py --- flask-security-3.4.2/flask_security/async_compat.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/async_compat.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,18 +0,0 @@ -""" - 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 -Nru flask-security-3.4.2/flask_security/babel.py flask-security-4.0.0/flask_security/babel.py --- flask-security-3.4.2/flask_security/babel.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/babel.py 2021-01-26 02:39:51.000000000 +0000 @@ -1,22 +1,100 @@ -# -*- coding: utf-8 -*- """ flask_security.babel ~~~~~~~~~~~~~~~~~~~~ I18N support for Flask-Security. + + As of Flask-Babel 2.0.0 - it supports the Flask-BabelEx Domain extension - and it + is maintained. (Flask-BabelEx is no longer maintained). So we start with that, + then fall back to Flask-BabelEx, then fall back to a Null Domain + (just as Flask-Admin). """ -from flask_babelex import Domain +# flake8: noqa: F811 + from wtforms.i18n import messages_path -wtforms_domain = Domain(messages_path(), domain="wtforms") +from .utils import config_value as cv + +_domain_cls = None +try: + from flask_babel import Domain + + _domain_cls = Domain + _dir_keyword = "translation_directories" +except ImportError: # pragma: no cover + try: + from flask_babelex import Domain + + _domain_cls = Domain + _dir_keyword = "dirname" + except ImportError: + # Fake up just enough + class Domain: + @staticmethod + def gettext(string, **variables): + return string % variables + + @staticmethod + def ngettext(singular, plural, num, **variables): + variables.setdefault("num", num) + return (singular if num == 1 else plural) % variables + + @staticmethod + def lazy_gettext(string, **variables): + return Domain.gettext(string, **variables) + + class Translations: + """ dummy Translations class for WTForms, no translation support """ + + def gettext(self, string): + return string + + def ngettext(self, singular, plural, n): + return singular if n == 1 else plural + + def get_i18n_domain(app): + return Domain() + + def have_babel(): + return False + + def is_lazy_string(obj): + return False + + def make_lazy_string(__func, msg): + return msg + + +if _domain_cls: + # Have either Flask-Babel or Flask-BabelEx + from babel.support import LazyProxy + + wtforms_domain = _domain_cls(messages_path(), domain="wtforms") + + def get_i18n_domain(app): + kwargs = { + _dir_keyword: cv("I18N_DIRNAME", app=app), + "domain": cv("I18N_DOMAIN", app=app), + } + return _domain_cls(**kwargs) + + def have_babel(): + return True + + def is_lazy_string(obj): + """Checks if the given object is a lazy string.""" + return isinstance(obj, LazyProxy) + def make_lazy_string(__func, msg): + """Creates a lazy string by invoking func with args.""" + return LazyProxy(__func, msg, enable_cache=False) -class Translations(object): - """Fixes WTForms translation support and uses wtforms translations.""" + class Translations: + """Fixes WTForms translation support and uses wtforms translations.""" - def gettext(self, string): - return wtforms_domain.gettext(string) + def gettext(self, string): + return wtforms_domain.gettext(string) - def ngettext(self, singular, plural, n): - return wtforms_domain.ngettext(singular, plural, n) + def ngettext(self, singular, plural, n): + return wtforms_domain.ngettext(singular, plural, n) diff -Nru flask-security-3.4.2/flask_security/cache.py flask-security-4.0.0/flask_security/cache.py --- flask-security-3.4.2/flask_security/cache.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/cache.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,43 +0,0 @@ -# -*- 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 -Nru flask-security-3.4.2/flask_security/changeable.py flask-security-4.0.0/flask_security/changeable.py --- flask-security-3.4.2/flask_security/changeable.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/changeable.py 2021-01-26 02:39:51.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ flask_security.changeable ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -6,15 +5,17 @@ Flask-Security recoverable module :copyright: (c) 2012 by Matt Wright. + :copyright: (c) 2019-2020 by J. Christopher Wagner (jwag). :author: Eskil Heyn Olsen :license: MIT, see LICENSE for more details. """ -from flask import current_app +from flask import current_app, request, session +from flask_login import COOKIE_NAME as REMEMBER_COOKIE_NAME from werkzeug.local import LocalProxy from .signals import password_changed -from .utils import config_value, hash_password +from .utils import config_value, hash_password, login_user, send_mail # Convenient references _security = LocalProxy(lambda: current_app.extensions["security"]) @@ -29,7 +30,7 @@ """ 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) + send_mail(subject, user.email, "change_notice", user=user) def change_user_password(user, password): @@ -39,8 +40,17 @@ :param password: The unhashed new password """ user.password = hash_password(password) - if config_value("BACKWARDS_COMPAT_AUTH_TOKEN_INVALID"): - _datastore.set_uniquifier(user) + # Change uniquifier - this will cause ALL sessions to be invalidated. + _datastore.set_uniquifier(user) _datastore.put(user) + + # re-login user - this will update session, optional remember etc. + remember_cookie_name = current_app.config.get( + "REMEMBER_COOKIE_NAME", REMEMBER_COOKIE_NAME + ) + has_remember_cookie = ( + remember_cookie_name in request.cookies and session.get("remember") != "clear" + ) + login_user(user, remember=has_remember_cookie, authn_via=["change"]) send_password_changed_notice(user) password_changed.send(current_app._get_current_object(), user=user) diff -Nru flask-security-3.4.2/flask_security/cli.py flask-security-4.0.0/flask_security/cli.py --- flask-security-3.4.2/flask_security/cli.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/cli.py 2021-01-26 02:39:51.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ flask_security.cli ~~~~~~~~~~~~~~~~~~ @@ -6,11 +5,10 @@ Command Line Interface for managing accounts and roles. :copyright: (c) 2016 by CERN. - :copyright: (c) 2019 by J. Christopher Wagner + :copyright: (c) 2019-2020 by J. Christopher Wagner :license: MIT, see LICENSE for more details. """ -from __future__ import absolute_import, print_function from functools import wraps @@ -20,7 +18,12 @@ from werkzeug.local import LocalProxy from .quart_compat import get_quart_status -from .utils import hash_password +from .utils import ( + find_user, + get_identity_attributes, + get_identity_attribute, + hash_password, +) if get_quart_status(): # pragma: no cover import quart.cli @@ -65,7 +68,10 @@ @click.group() def users(): - """User commands.""" + """User commands. + + For commands that require a USER - pass in any identity attribute. + """ @click.group() @@ -73,22 +79,56 @@ """Role commands.""" -@users.command("create") -@click.argument("identity") +@users.command( + "create", + help="Create a new user with one or more attributes using the syntax:" + " attr:value. If attr isn't set 'email' is presumed." + " Identity attribute values will be validated using the configured" + " confirm_register_form;" + " however, any ADDITIONAL attribute:value pairs will be sent to" + " datastore.create_user", +) +@click.argument( + "attributes", + nargs=-1, +) @click.password_option() @click.option("-a", "--active", default=False, is_flag=True) @with_appcontext @commit -def users_create(identity, password, active): +def users_create(attributes, password, active): """Create a user.""" - kwargs = {attr: identity for attr in _security.user_identity_attributes} + kwargs = {} + + identity_attributes = get_identity_attributes() + for attrarg in attributes: + # If given identity is an identity_attribute - do a bit of pre-validating + # to provide nicer errors. + attr = "email" + if ":" in attrarg: + attr, attrarg = attrarg.split(":") + if attr in identity_attributes: + details = get_identity_attribute(attr) + idata = details["mapper"](attrarg) + if not idata: + raise click.UsageError( + f"Attr {attr} with value {attrarg} wasn't accepted by mapper" + ) + + kwargs[attr] = attrarg kwargs.update(**{"password": password}) form = _security.confirm_register_form(MultiDict(kwargs), meta={"csrf": False}) if form.validate(): - kwargs["password"] = hash_password(kwargs["password"]) + # We don't use the form directly to provide values so that this CLI can actually + # set any usermodel attribute. We do grab email and password from the form + # so that we get any normalization results. + kwargs["password"] = hash_password(form.password.data) kwargs["active"] = active + # echo normalized email... + if "email" in kwargs: + kwargs["email"] = form.email.data _datastore.create_user(**kwargs) click.secho("User created successfully.", fg="green") kwargs["password"] = "****" @@ -100,13 +140,13 @@ @roles.command("create") @click.argument("name") @click.option("-d", "--description", default=None) -@click.option("-p", "--permissions") +@click.option("-p", "--permissions", help="A comma separated list") @with_appcontext @commit def roles_create(**kwargs): """Create a role.""" - # For some reaosn Click puts arguments in kwargs - even if they weren't specified. + # For some reason 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"): @@ -122,14 +162,16 @@ @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.") + user_obj = find_user(user) + if user_obj is None: + raise click.UsageError("User not found.") + + role = _datastore._prepare_role_modify_args(role) if role is None: raise click.UsageError("Cannot find role.") - if _datastore.add_role_to_user(user, role): + if _datastore.add_role_to_user(user_obj, role): click.secho( - 'Role "{0}" added to user "{1}" ' "successfully.".format(role, user), + f'Role "{role.name}" added to user "{user}" successfully.', fg="green", ) else: @@ -143,33 +185,82 @@ @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.") + user_obj = find_user(user) + if user_obj is None: + raise click.UsageError("User not found.") + + role = _datastore._prepare_role_modify_args(role) if role is None: raise click.UsageError("Cannot find role.") - if _datastore.remove_role_from_user(user, role): + if _datastore.remove_role_from_user(user_obj, role): click.secho( - 'Role "{0}" removed from user "{1}" ' "successfully.".format(role, user), + f'Role "{role.name}" removed from user "{user}" successfully.', fg="green", ) else: raise click.UsageError("Cannot remove role from user.") +@roles.command("add_permissions") +@click.argument("role") +@click.argument("permissions") +@with_appcontext +@commit +def roles_add_permissions(role, permissions): + """Add permissions to role. + + Role is an existing role name. + Permissions are a comma separated list. + """ + role = _datastore._prepare_role_modify_args(role) + if role is None: + raise click.UsageError("Cannot find role.") + if _datastore.add_permissions_to_role(role, permissions): + click.secho( + f'Permission(s) "{permissions}" added to role "{role.name}" successfully.', + fg="green", + ) + else: # pragma: no cover + raise click.UsageError("Cannot add permission(s) to role.") + + +@roles.command("remove_permissions") +@click.argument("role") +@click.argument("permissions") +@with_appcontext +@commit +def roles_remove_permissions(role, permissions): + """Remove permissions from role. + + Role is an existing role name. + Permissions are a comma separated list. + """ + role = _datastore._prepare_role_modify_args(role) + if role is None: + raise click.UsageError("Cannot find role.") + if _datastore.remove_permissions_from_role(role, permissions): + click.secho( + f'Permission(s) "{permissions}" removed from role' + f' "{role.name}" successfully.', + fg="green", + ) + else: # pragma: no cover + raise click.UsageError("Cannot remove permission(s) from role.") + + @users.command("activate") @click.argument("user") @with_appcontext @commit def users_activate(user): """Activate a user.""" - user_obj = _datastore.get_user(user) + user_obj = find_user(user) if user_obj is None: - raise click.UsageError("ERROR: User not found.") + raise click.UsageError("User not found.") if _datastore.activate_user(user_obj): - click.secho('User "{0}" has been activated.'.format(user), fg="green") + click.secho(f'User "{user}" has been activated.', fg="green") else: - click.secho('User "{0}" was already activated.'.format(user), fg="yellow") + click.secho(f'User "{user}" was already activated.', fg="yellow") @users.command("deactivate") @@ -178,10 +269,30 @@ @commit def users_deactivate(user): """Deactivate a user.""" - user_obj = _datastore.get_user(user) + user_obj = find_user(user) if user_obj is None: - raise click.UsageError("ERROR: User not found.") + raise click.UsageError("User not found.") if _datastore.deactivate_user(user_obj): - click.secho('User "{0}" has been deactivated.'.format(user), fg="green") + click.secho(f'User "{user}" has been deactivated.', fg="green") else: - click.secho('User "{0}" was already deactivated.'.format(user), fg="yellow") + click.secho(f'User "{user}" was already deactivated.', fg="yellow") + + +@users.command( + "reset_access", + help="Reset all authentication credentials for user." + " This includes sessions, authentication tokens, two-factor" + " and unified sign in secrets. ", +) +@click.argument("user") +@with_appcontext +@commit +def users_reset_access(user): + """ Reset all authentication tokens etc.""" + user_obj = find_user(user) + if user_obj is None: + raise click.UsageError("User not found.") + _datastore.reset_user_access(user_obj) + click.secho( + f'User "{user}" authentication credentials have been reset.', fg="green" + ) diff -Nru flask-security-3.4.2/flask_security/confirmable.py flask-security-4.0.0/flask_security/confirmable.py --- flask-security-3.4.2/flask_security/confirmable.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/confirmable.py 2021-01-26 02:39:51.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ flask_security.confirmable ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -18,6 +17,7 @@ config_value, get_token_status, hash_data, + send_mail, url_for_security, verify_hash, ) @@ -41,7 +41,7 @@ confirmation_link, token = generate_confirmation_link(user) - _security._send_mail( + send_mail( config_value("EMAIL_SUBJECT_CONFIRM"), user.email, "confirmation_instructions", @@ -57,7 +57,7 @@ :param user: The user to work with """ - data = [str(user.id), hash_data(user.email)] + data = [str(user.fs_uniquifier), hash_data(user.email)] return _security.confirm_serializer.dumps(data) diff -Nru flask-security-3.4.2/flask_security/core.py flask-security-4.0.0/flask_security/core.py --- flask-security-3.4.2/flask_security/core.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/core.py 2021-01-26 02:39:51.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ flask_security.core ~~~~~~~~~~~~~~~~~~~ @@ -14,11 +13,9 @@ 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 @@ -26,8 +23,9 @@ from itsdangerous import URLSafeTimedSerializer from passlib.context import CryptContext from werkzeug.datastructures import ImmutableList -from werkzeug.local import LocalProxy, Local +from werkzeug.local import LocalProxy +from .babel import get_i18n_domain, have_babel from .decorators import ( default_reauthn_handler, default_unauthn_handler, @@ -44,10 +42,11 @@ SendConfirmationForm, TwoFactorVerifyCodeForm, TwoFactorSetupForm, - TwoFactorVerifyPasswordForm, TwoFactorRescueForm, VerifyForm, ) +from .mail_util import MailUtil +from .password_util import PasswordUtil from .phone_util import PhoneUtil from .twofactor import tf_send_security_token from .unified_signin import ( @@ -65,27 +64,21 @@ FsPermNeed, csrf_cookie_handler, default_want_json, - default_password_validator, get_config, + get_identity_attribute, get_identity_attributes, get_message, - hash_data, localize_callback, - send_mail, - string_types, + set_request_attr, 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") @@ -101,6 +94,7 @@ "FLASH_MESSAGES": True, "I18N_DOMAIN": "flask_security", "I18N_DIRNAME": pkg_resources.resource_filename("flask_security", "translations"), + "EMAIL_VALIDATOR_ARGS": None, "PASSWORD_HASH": "bcrypt", "PASSWORD_SALT": None, "PASSWORD_SINGLE_HASH": { @@ -133,6 +127,7 @@ "PASSWORD_COMPLEXITY_CHECKER": None, "PASSWORD_CHECK_BREACHED": False, "PASSWORD_BREACHED_COUNT": 1, + "PASSWORD_NORMALIZE_FORM": "NFKD", "DEPRECATED_PASSWORD_SCHEMES": ["auto"], "LOGIN_URL": "/login", "LOGOUT_URL": "/logout", @@ -143,9 +138,7 @@ "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": "/", @@ -159,8 +152,10 @@ "RESET_ERROR_VIEW": None, "RESET_VIEW": None, "LOGIN_ERROR_VIEW": None, + "REQUIRES_CONFIRMATION_ERROR_VIEW": None, "REDIRECT_HOST": None, "REDIRECT_BEHAVIOR": None, + "REDIRECT_ALLOW_SUBDOMAINS": False, "FORGOT_PASSWORD_TEMPLATE": "security/forgot_password.html", "LOGIN_USER_TEMPLATE": "security/login_user.html", "REGISTER_USER_TEMPLATE": "security/register_user.html", @@ -171,7 +166,6 @@ "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, @@ -187,6 +181,14 @@ "TWO_FACTOR_AUTHENTICATOR_VALIDITY": 120, "TWO_FACTOR_MAIL_VALIDITY": 300, "TWO_FACTOR_SMS_VALIDITY": 120, + "TWO_FACTOR_ALWAYS_VALIDATE": True, + "TWO_FACTOR_LOGIN_VALIDITY": "30 days", + "TWO_FACTOR_VALIDITY_SALT": "tf-validity-salt", + "TWO_FACTOR_VALIDITY_COOKIE": { + "httponly": True, + "secure": False, + "samesite": None, + }, "CONFIRM_EMAIL_WITHIN": "5 days", "RESET_PASSWORD_WITHIN": "5 days", "LOGIN_WITHOUT_CONFIRMATION": False, @@ -215,20 +217,16 @@ "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}, + "USER_IDENTITY_ATTRIBUTES": [ + {"email": {"mapper": uia_email_mapper, "case_insensitive": True}} ], "PHONE_REGION_DEFAULT": "US", "FRESHNESS": timedelta(hours=24), "FRESHNESS_GRACE_PERIOD": timedelta(hours=1), + "API_ENABLED_METHODS": ["session", "token"], "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", @@ -255,7 +253,6 @@ "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", @@ -273,7 +270,6 @@ "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, } @@ -300,6 +296,13 @@ _("%(email)s is already associated with an account."), "error", ), + "IDENTITY_ALREADY_ASSOCIATED": ( + _( + "Identity attribute '%(attr)s' with value '%(value)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"), @@ -388,14 +391,6 @@ _("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", @@ -427,7 +422,6 @@ "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, @@ -437,23 +431,10 @@ 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) + """Load based on fs_uniquifier (alternative_id).""" + user = _security.datastore.find_user(fs_uniquifier=str(user_id)) if user and user.active: + set_request_attr("fs_authn_via", "session") return user return None @@ -477,49 +458,35 @@ 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 hasattr(_security.datastore.user_model, "fs_token_uniquifier"): + user = _security.datastore.find_user(fs_token_uniquifier=data[0]) + else: + user = _security.datastore.find_user(fs_uniquifier=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 + if user and user.verify_auth_token(data): + set_request_attr("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) + identity = Identity(current_user.fs_uniquifier) return identity def _on_identity_loaded(sender, identity): - if hasattr(current_user, "id"): - identity.provides.add(UserNeed(current_user.id)) + if hasattr(current_user, "fs_uniquifier"): + identity.provides.add(UserNeed(current_user.fs_uniquifier)) for role in getattr(current_user, "roles", []): identity.provides.add(RoleNeed(role.name)) @@ -570,17 +537,11 @@ schemes=schemes, default=pw_hash, deprecated=deprecated, - **cv("PASSWORD_HASH_PASSLIB_OPTIONS", app=app) + **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) @@ -604,22 +565,20 @@ principal=_get_principal(app), pwd_context=_get_pwd_context(app), hashing_context=_get_hashing_context(app), - i18n_domain=_get_i18n_domain(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"), + tf_validity_serializer=_get_serializer(app, "two_factor_validity"), _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, ) ) @@ -637,7 +596,7 @@ return dict(url_for_security=url_for_security, security=_security) -class RoleMixin(object): +class RoleMixin: """Mixin for `Role` model definitions""" def __eq__(self, other): @@ -653,9 +612,8 @@ """ 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. + Supports permissions being a comma separated string, an iterable, or a set + based on how the underlying DB model was built. .. versionadded:: 3.3.0 """ @@ -667,7 +625,7 @@ else: # Assume this is a comma separated list return set(self.permissions.split(",")) - return set([]) + return set() def add_permissions(self, permissions): """ @@ -675,9 +633,10 @@ :param permissions: a set, list, or single string. - Caller must commit to DB. - .. versionadded:: 3.3.0 + + .. deprecated:: 3.4.4 + Use :meth:`.UserDatastore.add_permissions_to_role` """ if hasattr(self, "permissions"): current_perms = self.get_permissions() @@ -688,7 +647,7 @@ else: perms = {permissions} self.permissions = ",".join(current_perms.union(perms)) - else: + else: # pragma: no cover raise NotImplementedError("Role model doesn't have permissions") def remove_permissions(self, permissions): @@ -697,9 +656,10 @@ :param permissions: a set, list, or single string. - Caller must commit to DB. - .. versionadded:: 3.3.0 + + .. deprecated:: 3.4.4 + Use :meth:`.UserDatastore.remove_permissions_from_role` """ if hasattr(self, "permissions"): current_perms = self.get_permissions() @@ -710,7 +670,7 @@ else: perms = {permissions} self.permissions = ",".join(current_perms.difference(perms)) - else: + else: # pragma: no cover raise NotImplementedError("Role model doesn't have permissions") @@ -718,22 +678,12 @@ """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). + """Returns the user identification attribute. 'Alternative-token' for + Flask-Login. This is always ``fs_uniquifier``. .. 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) + return str(self.fs_uniquifier) @property def is_active(self): @@ -743,11 +693,24 @@ def get_auth_token(self): """Constructs the user's authentication token. + :raises ValueError: If ``fs_token_uniquifier`` is part of model but not set. + + Optionally use a separate uniquifier so that changing password doesn't + invalidate auth tokens. + This data MUST be securely signed using the ``remember_token_serializer`` + + .. versionchanged:: 4.0.0 + If user model has ``fs_token_uniquifier`` - use that (raise ValueError + if not set). Otherwise fallback to using ``fs_uniqifier``. """ - data = [str(self.id), hash_data(self.password)] - if hasattr(self, "fs_uniquifier"): - data.append(self.fs_uniquifier) + + if hasattr(self, "fs_token_uniquifier"): + if not self.fs_token_uniquifier: + raise ValueError() + data = [str(self.fs_token_uniquifier)] + else: + data = [str(self.fs_uniquifier)] return _security.remember_token_serializer.dumps(data) def verify_auth_token(self, data): @@ -759,24 +722,22 @@ :param data: the data as formulated by :meth:`get_auth_token` .. versionadded:: 3.3.0 + + .. versionchanged:: 4.0.0 + If user model has ``fs_token_uniquifier`` - use that otherwise + use ``fs_uniquifier``. """ - 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 + + if hasattr(self, "fs_token_uniquifier"): + return data[0] == self.fs_token_uniquifier + + return data[0] == self.fs_uniquifier 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): + if isinstance(role, str): return role in (role.name for role in self.roles) else: return role in self.roles @@ -791,14 +752,16 @@ """ for role in self.roles: - if hasattr(role, "permissions"): - if permission in role.get_permissions(): - return True + 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)} + """Serialize user object as response payload. + Override this to return any/all of the user object in JSON responses. + Return a dict. + """ + return {} def get_redirect_qparams(self, existing=None): """Return user info that will be added to redirect query params. @@ -807,10 +770,15 @@ :return: A dict whose keys will be query params and values will be query values. .. versionadded:: 3.2.0 + + .. versionchanged:: 4.0.0 + Add 'identity' using UserMixin.calc_username() - email is optional. """ if not existing: existing = {} - existing.update({"email": self.email}) + if hasattr(self, "email"): + existing.update({"email": self.email}) + existing.update({"identity": self.calc_username()}) return existing def verify_and_update_password(self, password): @@ -831,7 +799,7 @@ return verify_and_update_password(password, self) def calc_username(self): - """ Come up with the best 'username' based on how the app + """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 @@ -847,7 +815,7 @@ 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. + """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 @@ -865,7 +833,7 @@ return None def tf_send_security_token(self, method, **kwargs): - """ Generate and send the security code for two-factor. + """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 @@ -894,7 +862,7 @@ return False -class _SecurityState(object): +class _SecurityState: def __init__(self, **kwargs): for key, value in kwargs.items(): setattr(self, key.lower(), value) @@ -940,9 +908,6 @@ 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) @@ -958,12 +923,6 @@ 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" @@ -990,11 +949,8 @@ def reauthn_handler(self, cb): self._reauthn_handler = cb - def password_validator(self, cb): - self._password_validator = cb - -class Security(object): +class Security: """The :class:`Security` class initializes the Flask-Security extension. :param app: The application. @@ -1014,7 +970,6 @@ :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 @@ -1022,12 +977,14 @@ :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` + :param mail_util_cls: Class to use for sending emails. Defaults to :class:`MailUtil` + :param password_util_cls: Class to use for password normalization/validation. + Defaults to :class:`PasswordUtil` .. versionadded:: 3.4.0 ``verify_form`` added as part of freshness/re-authentication @@ -1043,6 +1000,16 @@ .. versionadded:: 3.4.0 ``phone_util_cls`` added to allow different phone number parsing implementations - see :py:class:`PhoneUtil` + + .. versionadded:: 4.0.0 + ``mail_util_cls`` added to isolate mailing handling. + ``password_util_cls`` added to encapsulate password validation/normalization. + + .. deprecated:: 4.0.0 + ``send_mail`` and ``send_mail_task``. Replaced with ``mail_util_cls``. + ``two_factor_verify_password_form`` removed. + ``password_validator`` removed in favor of the new ``password_util_cls``. + """ def __init__(self, app=None, datastore=None, register_blueprint=True, **kwargs): @@ -1085,6 +1052,18 @@ kwargs.setdefault("totp_cls", Totp) if "phone_util_cls" not in kwargs: kwargs.setdefault("phone_util_cls", PhoneUtil) + if "mail_util_cls" not in kwargs: + kwargs.setdefault("mail_util_cls", MailUtil) + if "password_util_cls" not in kwargs: + kwargs.setdefault("password_util_cls", PasswordUtil) + + # default post redirects to APPLICATION_ROOT, which itself defaults to "/" + app.config.setdefault( + "SECURITY_POST_LOGIN_VIEW", app.config.get("APPLICATION_ROOT", "/") + ) + app.config.setdefault( + "SECURITY_POST_LOGOUT_VIEW", app.config.get("APPLICATION_ROOT", "/") + ) for key, value in _default_config.items(): app.config.setdefault("SECURITY_" + key, value) @@ -1095,6 +1074,10 @@ identity_loaded.connect_via(app)(_on_identity_loaded) self._state = state = _get_state(app, datastore, **kwargs) + if hasattr(datastore, "user_model") and not hasattr( + datastore.user_model, "fs_uniquifier" + ): # pragma: no cover + raise ValueError("User model must contain fs_uniquifier as of 4.0.0") if register_blueprint: bp = create_blueprint( @@ -1105,8 +1088,7 @@ @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 + # This is only not registered if Flask-Babel isn't installed... if "_" not in app.jinja_env.globals: current_app.jinja_env.globals["_"] = state.i18n_domain.gettext # Register so other packages can reference our translations. @@ -1166,8 +1148,17 @@ 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() + def check_babel(): + # Verify that if Flask-Babel or Flask-BabelEx is installed + # it has been initialized + if have_babel() and "babel" not in app.extensions: + raise ValueError( + "Flask-Babel or Flask-BabelEx is installed but not initialized" + ) + + state._phone_util = state.phone_util_cls(app) + state._mail_util = state.mail_util_cls(app) + state._password_util = state.password_util_cls(app) app.extensions["security"] = state @@ -1189,12 +1180,33 @@ if not app.config.get(newc, None): app.config[newc] = app.config.get(oldc, None) + # Check for pre-4.0 SECURITY_USER_IDENTITY_ATTRIBUTES format + for uia in cv("USER_IDENTITY_ATTRIBUTES", app=app): # pragma: no cover + if not isinstance(uia, dict): + raise ValueError( + "SECURITY_USER_IDENTITY_ATTRIBUTES changed semantics" + " in 4.0 - please see release notes." + ) + if len(list(uia.keys())) != 1: + raise ValueError( + "Each element in SECURITY_USER_IDENTITY_ATTRIBUTES" + " must have one and only one key." + ) + # 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 "sms" in cv( + "US_ENABLED_METHODS", app=app + ) and not get_identity_attribute("us_phone_number", app=app): + warnings.warn( + "'sms' was enabled in SECURITY_US_ENABLED_METHODS;" + " however 'us_phone_number' not configured in" + " SECURITY_USER_IDENTITY_ATTRIBUTES" + ) if cv("TWO_FACTOR", app=app): multi_factor = True if len(cv("TWO_FACTOR_ENABLED_METHODS", app=app)) < 1: @@ -1224,50 +1236,24 @@ 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 + from importlib.util import find_spec + module_exists = find_spec(module) if not module_exists: - raise ValueError("{} is required for {}".format(module, config_name)) + raise ValueError(f"{module} is required for {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. + """Callback to render response payload as JSON. :param cb: Callback function with signature (payload, code, headers=None, user=None) @@ -1301,7 +1287,7 @@ self._state._render_json = cb def want_json(self, fn): - """ Function that returns True if response should be JSON (based on the request) + """Function that returns True if response should be JSON (based on the request) :param fn: Function with the following signature (request) @@ -1385,27 +1371,5 @@ """ 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 -Nru flask-security-3.4.2/flask_security/datastore.py flask-security-4.0.0/flask_security/datastore.py --- flask-security-3.4.2/flask_security/datastore.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/datastore.py 2021-01-26 02:39:51.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ flask_security.datastore ~~~~~~~~~~~~~~~~~~~~~~~~ @@ -12,10 +11,10 @@ import json import uuid -from .utils import config_value, get_identity_attributes, string_types +from .utils import config_value -class Datastore(object): +class Datastore: def __init__(self, db): self.db = db @@ -115,7 +114,7 @@ model.delete() -class UserDatastore(object): +class UserDatastore: """Abstracted user datastore. :param user_model: A user model class definition @@ -132,12 +131,10 @@ 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): + def _prepare_role_modify_args(self, role): + if isinstance(role, str): role = self.find_role(role) - return user, role + return role def _prepare_create_user_args(self, **kwargs): kwargs.setdefault("active", True) @@ -147,23 +144,11 @@ # 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) + kwargs.setdefault("fs_uniquifier", uuid.uuid4().hex) + if hasattr(self.user_model, "fs_token_uniquifier"): + kwargs.setdefault("fs_token_uniquifier", uuid.uuid4().hex) - def get_user(self, id_or_email): - """Returns a user matching the specified ID or email address.""" - raise NotImplementedError + return kwargs def find_user(self, *args, **kwargs): """Returns a user matching the provided parameters.""" @@ -179,8 +164,9 @@ :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 + :return: True is role was added, False if role already existed. """ - user, role = self._prepare_role_modify_args(user, role) + role = self._prepare_role_modify_args(role) if role not in user.roles: user.roles.append(role) self.put(user) @@ -193,15 +179,59 @@ :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 + :return: True if role was removed, False if role doesn't exist or user didn't + have role. """ rv = False - user, role = self._prepare_role_modify_args(user, role) + role = self._prepare_role_modify_args(role) if role in user.roles: rv = True user.roles.remove(role) self.put(user) return rv + def add_permissions_to_role(self, role, permissions): + """Add one or more permissions to role. + + :param role: The role to modify. Can be a Role object or + string role name + :param permissions: a set, list, or single string. + :return: True if permissions added, False if role doesn't exist. + + Caller must commit to DB. + + .. versionadded:: 4.0.0 + """ + + rv = False + role = self._prepare_role_modify_args(role) + if role: + rv = True + role.add_permissions(permissions) + self.put(role) + return rv + + def remove_permissions_from_role(self, role, permissions): + """Remove one or more permissions from a role. + + :param role: The role to modify. Can be a Role object or + string role name + :param permissions: a set, list, or single string. + :return: True if permissions removed, False if role doesn't exist. + + Caller must commit to DB. + + .. versionadded:: 4.0.0 + """ + + rv = False + role = self._prepare_role_modify_args(role) + if role: + rv = True + role.remove_permissions(permissions) + self.put(role) + return rv + def toggle_active(self, user): """Toggles a user's active status. Always returns True.""" user.active = not user.active @@ -235,25 +265,38 @@ return False def set_uniquifier(self, user, uniquifier=None): - """ Set user's authentication token uniquifier. + """Set user's Flask-Security identity key. 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 set_token_uniquifier(self, user, uniquifier=None): + """Set user's auth token identity key. + This will immediately render outstanding auth tokens 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_token_uniquifier`` + + .. versionadded:: 4.0.0 + """ + if not uniquifier: + uniquifier = uuid.uuid4().hex + if hasattr(user, "fs_token_uniquifier"): + user.fs_token_uniquifier = uniquifier + self.put(user) + def create_role(self, **kwargs): """ Creates and returns a new role from the given parameters. @@ -274,7 +317,7 @@ perms = kwargs["permissions"] if isinstance(perms, list) or isinstance(perms, set): perms = ",".join(perms) - elif isinstance(perms, string_types): + elif isinstance(perms, str): # squash spaces. perms = ",".join([p.strip() for p in perms.split(",")]) kwargs["permissions"] = perms @@ -297,16 +340,27 @@ :kwparam roles: list of roles to be added to user. Can be Role objects or strings + .. note:: + No normalization is done on email - it is assumed the caller has already + done that. + + .. note:: + The roles kwparam is modified as part of the call - it will, if necessary + be converted from names to role instances. + .. 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. + Furthermore, no validation nor normalization is done on the password + (e.g for minimum length). + + Best practice is:: + pbad, pnorm = app.security._password_util.validate(password, True) + + Look for `pbad` being None. Pass the normalized password `pnorm` to this + method. The new user's ``active`` property will be set to ``True`` unless explicitly set to ``False`` in `kwargs`. @@ -329,28 +383,30 @@ * reset fs_uniquifier - which causes session cookie, remember cookie, auth tokens to be unusable + * reset fs_token_uniquifier (if present) - cause 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 + 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. + as a helper for an application's administrative needs. Remember to call commit on DB if needed. .. versionadded:: 3.4.1 """ self.set_uniquifier(user) + self.set_token_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. + """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. @@ -378,7 +434,7 @@ self.put(user) def tf_reset(self, user): - """ Disable two-factor auth for user + """Disable two-factor auth for user .. versionadded: 3.4.1 """ @@ -388,7 +444,7 @@ self.put(user) def us_get_totp_secrets(self, user): - """ Return totp secrets. + """Return totp secrets. These are json encoded in the DB. Returns a dict with methods as keys and secrets as values. @@ -400,7 +456,7 @@ return json.loads(user.us_totp_secrets) def us_put_totp_secrets(self, user, secrets): - """ Save secrets. Assume to be a dict (or None) + """Save secrets. Assume to be a dict (or None) with keys as methods, and values as (encrypted) secrets. .. versionadded:: 3.4.0 @@ -409,7 +465,7 @@ self.put(user) def us_set(self, user, method, totp_secret=None, phone=None): - """ Set unified sign in info into user record. + """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. @@ -431,7 +487,7 @@ self.put(user) def us_reset(self, user): - """ Disable unified sign in for 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. @@ -452,64 +508,28 @@ SQLAlchemyDatastore.__init__(self, db) UserDatastore.__init__(self, user_model, role_model) - def get_user(self, identifier): + def find_user(self, case_insensitive=False, **kwargs): 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() + if case_insensitive: + # While it is of course possible to pass in multiple keys to filter on + # that isn't the normal use case. If caller asks for case_insensitive + # AND gives multiple keys - throw an error. + if len(kwargs) > 1: + raise ValueError("Case insensitive option only supports single key") + attr, identifier = kwargs.popitem() + subquery = alchemyFn.lower( + getattr(self.user_model, attr) + ) == alchemyFn.lower(identifier) + return query.filter(subquery).first() + else: + return query.filter_by(**kwargs).first() def find_role(self, role): return self.role_model.query.filter_by(name=role).first() @@ -521,9 +541,8 @@ """ 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. - """ + class PretendFlaskSQLAlchemyDb: + """This is a pretend db object, so we can just pass in a session.""" def __init__(self, session): self.session = session @@ -533,7 +552,7 @@ ) def commit(self): - super(SQLAlchemySessionUserDatastore, self).commit() + super().commit() class MongoEngineUserDatastore(MongoEngineDatastore, UserDatastore): @@ -545,39 +564,24 @@ 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 + def find_user(self, case_insensitive=False, **kwargs): + 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() + if case_insensitive: + # While it is of course possible to pass in multiple keys to filter on + # that isn't the normal use case. If caller asks for case_insensitive + # AND gives multiple keys - throw an error. + if len(kwargs) > 1: + raise ValueError("Case insensitive option only supports single key") + attr, identifier = kwargs.popitem() + query = {f"{attr}__iexact": identifier} + return self.user_model.objects(**query).first() + else: + queries = map(lambda i: Q(**{i[0]: i[1]}), kwargs.items()) + query = QCombination(QCombination.AND, queries) + return self.user_model.objects(query).first() except ValidationError: # pragma: no cover return None @@ -599,34 +603,23 @@ UserDatastore.__init__(self, user_model, role_model) self.UserRole = role_link - def get_user(self, identifier): + def find_user(self, case_insensitive=False, **kwargs): 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() + if case_insensitive: + # While it is of course possible to pass in multiple keys to filter on + # that isn't the normal use case. If caller asks for case_insensitive + # AND gives multiple keys - throw an error. + if len(kwargs) > 1: + raise ValueError("Case insensitive option only supports single key") + attr, identifier = kwargs.popitem() + return self.user_model.get( + peeweeFn.lower(getattr(self.user_model, attr)) + == peeweeFn.lower(identifier) + ) + else: + return self.user_model.filter(**kwargs).get() except self.user_model.DoesNotExist: return None @@ -652,7 +645,7 @@ :param user: The user to manipulate :param role: The role to add to the user """ - user, role = self._prepare_role_modify_args(user, role) + role = self._prepare_role_modify_args(role) result = self.UserRole.select().where( self.UserRole.user == user.id, self.UserRole.role == role.id ) @@ -668,7 +661,7 @@ :param user: The user to manipulate :param role: The role to remove from the user """ - user, role = self._prepare_role_modify_args(user, role) + role = self._prepare_role_modify_args(role) result = self.UserRole.select().where( self.UserRole.user == user, self.UserRole.role == role ) @@ -694,26 +687,15 @@ UserDatastore.__init__(self, user_model, role_model) @with_pony_session - def get_user(self, identifier): - from pony.orm.core import ObjectNotFound + def find_user(self, case_insensitive=False, **kwargs): + if case_insensitive: + # While it is of course possible to pass in multiple keys to filter on + # that isn't the normal use case. If caller asks for case_insensitive + # AND gives multiple keys - throw an error. + if len(kwargs) > 1: + raise ValueError("Case insensitive option only supports single key") + # TODO - implement case insensitive look ups. - 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 @@ -722,12 +704,12 @@ @with_pony_session def add_role_to_user(self, *args, **kwargs): - return super(PonyUserDatastore, self).add_role_to_user(*args, **kwargs) + return super().add_role_to_user(*args, **kwargs) @with_pony_session def create_user(self, **kwargs): - return super(PonyUserDatastore, self).create_user(**kwargs) + return super().create_user(**kwargs) @with_pony_session def create_role(self, **kwargs): - return super(PonyUserDatastore, self).create_role(**kwargs) + return super().create_role(**kwargs) diff -Nru flask-security-3.4.2/flask_security/decorators.py flask-security-4.0.0/flask_security/decorators.py --- flask-security-3.4.2/flask_security/decorators.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/decorators.py 2021-01-26 02:39:51.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ flask_security.decorators ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -25,10 +24,12 @@ FsPermNeed, config_value, do_flash, + find_user, get_message, get_url, check_and_update_authn_fresh, json_error_response, + set_request_attr, ) # Convenient references @@ -61,28 +62,33 @@ def default_unauthn_handler(mechanisms, headers=None): - """ Default callback for failures to authenticate + """Default callback for failures to authenticate - If caller wants JSON - return 401 + If caller wants JSON - return 401. + If caller wants BasicAuth - return 401 (the WWW-Authenticate header is set). Otherwise - assume caller is html and redirect if possible to a login view. We let Flask-Login handle this. """ + headers = headers or {} 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._render_json(payload, 401, headers, None) + + # Basic-Auth is often used to provide a browser based login form and then the + # browser will always add the BasicAuth credentials. For that to work we need to + # return 401 and not redirect to our login view. + if "WWW-Authenticate" in headers: + return Response(msg, 401, headers) return _security.login_manager.unauthorized() def default_reauthn_handler(within, grace): - """ Default callback for 'freshness' related authn failures. + """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. @@ -148,7 +154,7 @@ 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)) + identity_changed.send(app, identity=Identity(user.fs_uniquifier)) return True return False @@ -158,20 +164,22 @@ auth = request.authorization or BasicAuth(username=None, password=None) if not auth.username: return False - user = _security.datastore.get_user(auth.username) + user = find_user(auth.username) + if user and not user.active: + return False 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)) + identity_changed.send(app, identity=Identity(user.fs_uniquifier)) return True return False def handle_csrf(method): - """ Invoke CSRF protection based on authentication 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. @@ -212,6 +220,9 @@ :param realm: optional realm name + If authentication fails, then a 401 with the 'WWW-Authenticate' header set will be + returned. + Once authenticated, if so configured, CSRF protection will be tested. """ @@ -220,6 +231,7 @@ def wrapper(*args, **kwargs): if _check_http_auth(): handle_csrf("basic") + set_request_attr("fs_authn_via", "basic") return fn(*args, **kwargs) if _security._unauthorized_callback: return _security._unauthorized_callback() @@ -249,6 +261,7 @@ def decorated(*args, **kwargs): if _check_token(): handle_csrf("token") + set_request_attr("fs_authn_via", "token") return fn(*args, **kwargs) if _security._unauthorized_callback: return _security._unauthorized_callback() @@ -269,7 +282,9 @@ return 'Dashboard' :param auth_methods: Specified mechanisms (token, basic, session). If not specified - then all current available mechanisms will be tried. + then all current available mechanisms (except "basic") will be tried. A callable + can also be passed (useful if you need app/request context). The callable + must return a list. :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: @@ -297,6 +312,14 @@ On authentication failure `.Security.unauthorized_callback` (deprecated) or :meth:`.Security.unauthn_handler` will be called. + As a side effect, upon successful authentication, the request global + ``fs_authn_via`` will be set to the method ("basic", "token", "session") + + .. note:: + If "basic" is specified in addition to other methods, then if authentication + fails, a 401 with the "WWW-Authenticate" header will be returned - rather than + being redirected to the login view. + .. 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`` @@ -305,6 +328,12 @@ .. versionchanged:: 3.4.0 Added ``within`` and ``grace`` parameters to enforce a freshness check. + .. versionchanged:: 3.4.4 + If ``auth_methods`` isn't specified try all mechanisms EXCEPT ``basic``. + + .. versionchanged:: 4.0.0 + auth_methods can be passed as a callable. + """ login_mechanisms = { @@ -313,10 +342,7 @@ "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] + auth_methods_arg = auth_methods def wrapper(fn): @wraps(fn) @@ -335,6 +361,16 @@ else: grace = datetime.timedelta(minutes=grace) + if not auth_methods_arg: + auth_methods = {"session", "token"} + else: + auth_methods = [] + for am in auth_methods_arg: + if callable(am): + auth_methods.extend(am()) + else: + auth_methods.append(am) + h = {} if "basic" in auth_methods: r = _security.default_http_auth_realm @@ -349,11 +385,10 @@ # 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 - ): + if not check_and_update_authn_fresh(within, grace, method): return _security._reauthn_handler(within, grace) handle_csrf(method) + set_request_attr("fs_authn_via", method) return fn(*args, **dkwargs) if _security._unauthorized_callback: return _security._unauthorized_callback() diff -Nru flask-security-3.4.2/flask_security/forms.py flask-security-4.0.0/flask_security/forms.py --- flask-security-3.4.2/flask_security/forms.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/forms.py 2021-01-26 02:39:51.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ flask_security.forms ~~~~~~~~~~~~~~~~~~~~ @@ -16,7 +15,6 @@ 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, @@ -29,13 +27,18 @@ ValidationError, validators, ) +from wtforms.fields.html5 import EmailField +from wtforms.validators import StopValidation +from .babel import is_lazy_string, make_lazy_string from .confirmable import requires_confirmation from .utils import ( _, _datastore, config_value, do_flash, + find_user, + get_identity_attribute, get_message, hash_password, localize_callback, @@ -73,7 +76,7 @@ } -class ValidatorMixin(object): +class ValidatorMixin: """ This is called at import time - so there is no app context. Validators have state - namely self.message - but we need that @@ -89,7 +92,7 @@ del kwargs["message"] else: self._original_message = None - super(ValidatorMixin, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def __call__(self, form, field): if self._original_message and ( @@ -101,7 +104,7 @@ self.message = make_lazy_string(_local_xlate, cv[0]) else: self.message = self._original_message - return super(ValidatorMixin, self).__call__(form, field) + return super().__call__(form, field) class EqualTo(ValidatorMixin, validators.EqualTo): @@ -112,28 +115,42 @@ pass -class Email(ValidatorMixin, validators.Email): +class Length(ValidatorMixin, validators.Length): pass -class Length(ValidatorMixin, validators.Length): - pass +class EmailValidation: + """Simple interface to email_validator.""" + + def __call__(self, form, field): + if field.data is None: # pragma: no cover + raise ValidationError(get_message("EMAIL_NOT_PROVIDED")[0]) + + try: + field.data = _security._mail_util.validate(field.data) + except ValueError: + msg = get_message("INVALID_EMAIL_ADDRESS")[0] + # we stop further validators if email isn't valid. + # TODO: email_validator provides some really nice error messages - however + # they aren't localized. And there isn't an easy way to add multiple + # errors at once. + raise StopValidation(msg) email_required = Required(message="EMAIL_NOT_PROVIDED") -email_validator = Email(message="INVALID_EMAIL_ADDRESS") +email_validator = EmailValidation() password_required = Required(message="PASSWORD_NOT_PROVIDED") def _local_xlate(text): - """ LazyStrings need to be evaluated in the context of a request + """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 + """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. """ @@ -141,13 +158,52 @@ 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] + uia_email = get_identity_attribute("email") + norm_email = _security._mail_util.normalize(field.data) + if ( + _datastore.find_user( + case_insensitive=uia_email.get("case_insensitive", False), email=norm_email + ) + is not None + ): + msg = get_message("EMAIL_ALREADY_ASSOCIATED", email=norm_email)[0] raise ValidationError(msg) +def unique_identity_attribute(form, field): + """A validator that checks the field data against all configured + SECURITY_USER_IDENTITY_ATTRIBUTES. + This can be used as part of registration. + + Be aware that the "mapper" function likely also nornalizes the input in addition + to validating it. + + :param form: + :param field: + :return: Nothing; if field data corresponds to an existing User, ValidationError + is raised. + """ + for mapping in config_value("USER_IDENTITY_ATTRIBUTES"): + attr = list(mapping.keys())[0] + details = mapping[attr] + idata = details["mapper"](field.data) + if idata: + if _datastore.find_user( + case_insensitive=details.get("case_insensitive", False), **{attr: idata} + ): + msg = get_message( + "IDENTITY_ALREADY_ASSOCIATED", attr=attr, value=idata + )[0] + raise ValidationError(msg) + + def valid_user_email(form, field): - form.user = _datastore.get_user(field.data) + # Verify email exists in DB - be sure to normalize first. + uia_email = get_identity_attribute("email") + norm_email = _security._mail_util.normalize(field.data) + form.user = _datastore.find_user( + case_insensitive=uia_email.get("case_insensitive", False), email=norm_email + ) if form.user is None: raise ValidationError(get_message("USER_DOES_NOT_EXIST")[0]) @@ -156,25 +212,25 @@ def __init__(self, *args, **kwargs): if current_app.testing: self.TIME_LIMIT = None - super(Form, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) class EmailFormMixin: - email = StringField( + email = EmailField( get_form_field_label("email"), validators=[email_required, email_validator] ) class UserEmailFormMixin: user = None - email = StringField( + email = EmailField( get_form_field_label("email"), validators=[email_required, email_validator, valid_user_email], ) class UniqueEmailFormMixin: - email = StringField( + email = EmailField( get_form_field_label("email"), validators=[email_required, email_validator, unique_user_email], ) @@ -235,7 +291,7 @@ return True fields = inspect.getmembers(self, is_field_and_user_attr) - return dict((key, value.data) for key, value in fields) + return {key: value.data for key, value in fields} class SendConfirmationForm(Form, UserEmailFormMixin): @@ -244,12 +300,12 @@ submit = SubmitField(get_form_field_label("send_confirmation")) def __init__(self, *args, **kwargs): - super(SendConfirmationForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) if request.method == "GET": self.email.data = request.args.get("email", None) def validate(self): - if not super(SendConfirmationForm, self).validate(): + if not super().validate(): return False if self.user.confirmed_at is not None: self.email.errors.append(get_message("ALREADY_CONFIRMED")[0]) @@ -262,13 +318,18 @@ submit = SubmitField(get_form_field_label("recover_password")) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.requires_confirmation = False + def validate(self): - if not super(ForgotPasswordForm, self).validate(): + if not super().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.requires_confirmation = requires_confirmation(self.user) + if self.requires_confirmation: self.email.errors.append(get_message("CONFIRMATION_REQUIRED")[0]) return False return True @@ -280,10 +341,10 @@ submit = SubmitField(get_form_field_label("send_login_link")) def __init__(self, *args, **kwargs): - super(PasswordlessLoginForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def validate(self): - if not super(PasswordlessLoginForm, self).validate(): + if not super().validate(): return False if not self.user.is_active: self.email.errors.append(get_message("DISABLED_ACCOUNT")[0]) @@ -294,7 +355,7 @@ class LoginForm(Form, NextFormMixin): """The default login form""" - email = StringField(get_form_field_label("email"), validators=[email_required]) + email = EmailField(get_form_field_label("email"), validators=[email_required]) password = PasswordField( get_form_field_label("password"), validators=[password_required] ) @@ -302,7 +363,7 @@ submit = SubmitField(get_form_field_label("login")) def __init__(self, *args, **kwargs): - super(LoginForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) if not self.next.data: self.next.data = request.args.get("next", "") self.remember.default = config_value("DEFAULT_REMEMBER_ME") @@ -311,18 +372,22 @@ and not self.password.description ): html = Markup( - u'{message}'.format( + '{message}'.format( url=url_for_security("forgot_password"), message=get_message("FORGOT_PASSWORD")[0], ) ) self.password.description = html + self.requires_confirmation = False def validate(self): - if not super(LoginForm, self).validate(): + if not super().validate(): return False - self.user = _datastore.get_user(self.email.data) + # Historically, this used get_user() which would look at all + # USER_IDENTITY_ATTRIBUTES - even though the field name is 'email' + # We keep that behavior (for now) as we transition to find_user. + self.user = find_user(self.email.data) if self.user is None: self.email.errors.append(get_message("USER_DOES_NOT_EXIST")[0]) @@ -334,10 +399,12 @@ # Reduce timing variation between existing and non-existing users hash_password(self.password.data) return False + self.password.data = _security._password_util.normalize(self.password.data) 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.requires_confirmation = requires_confirmation(self.user) + if self.requires_confirmation: self.email.errors.append(get_message("CONFIRMATION_REQUIRED")[0]) return False if not self.user.is_active: @@ -353,10 +420,11 @@ submit = SubmitField(get_form_field_label("verify_password")) def validate(self): - if not super(VerifyForm, self).validate(): + if not super().validate(): return False self.user = current_user + self.password.data = _security._password_util.normalize(self.password.data) if not self.user.verify_and_update_password(self.password.data): self.password.errors.append(get_message("INVALID_PASSWORD")[0]) return False @@ -364,7 +432,7 @@ class ConfirmRegisterForm(Form, RegisterFormMixin, UniqueEmailFormMixin): - """ This form is used for registering when 'confirmable' is set. + """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... """ @@ -375,7 +443,7 @@ ) def validate(self): - if not super(ConfirmRegisterForm, self).validate(): + if not super().validate(): return False # To support unified sign in - we permit registering with no password. @@ -397,7 +465,9 @@ if hasattr(_datastore.user_model, k): rfields[k] = v del rfields["password"] - pbad = _security._password_validator(self.password.data, True, **rfields) + pbad, self.password.data = _security._password_util.validate( + self.password.data, True, **rfields + ) if pbad: self.password.errors.extend(pbad) return False @@ -416,7 +486,7 @@ ) def validate(self): - if not super(RegisterForm, self).validate(): + if not super().validate(): return False if not config_value("UNIFIED_SIGNIN"): # password_confirm required @@ -428,7 +498,7 @@ return True def __init__(self, *args, **kwargs): - super(RegisterForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) if not self.next.data: self.next.data = request.args.get("next", "") @@ -439,10 +509,10 @@ submit = SubmitField(get_form_field_label("reset_password")) def validate(self): - if not super(ResetPasswordForm, self).validate(): + if not super().validate(): return False - pbad = _security._password_validator( + pbad, self.password.data = _security._password_util.validate( self.password.data, False, user=current_user ) if pbad: @@ -469,16 +539,17 @@ submit = SubmitField(get_form_field_label("change_password")) def validate(self): - if not super(ChangePasswordForm, self).validate(): + if not super().validate(): return False + self.password.data = _security._password_util.normalize(self.password.data) 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( + pbad, self.new_password.data = _security._password_util.validate( self.new_password.data, False, user=current_user ) if pbad: @@ -506,7 +577,7 @@ submit = SubmitField(get_form_field_label("submit")) def __init__(self, *args, **kwargs): - super(TwoFactorSetupForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def validate(self): # TODO: the super class validate is never called - thus we have to @@ -542,7 +613,7 @@ submit = SubmitField(get_form_field_label("submitcode")) def __init__(self, *args, **kwargs): - super(TwoFactorVerifyCodeForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def validate(self): # codes sent by sms or mail will be valid for another window cycle @@ -573,23 +644,6 @@ 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 """ @@ -603,9 +657,9 @@ submit = SubmitField(get_form_field_label("submit")) def __init__(self, *args, **kwargs): - super(TwoFactorRescueForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def validate(self): - if not super(TwoFactorRescueForm, self).validate(): + if not super().validate(): return False return True diff -Nru flask-security-3.4.2/flask_security/__init__.py flask-security-4.0.0/flask_security/__init__.py --- flask-security-3.4.2/flask_security/__init__.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/__init__.py 2021-01-26 02:39:51.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ flask_security ~~~~~~~~~~~~~~ @@ -47,9 +46,11 @@ TwoFactorRescueForm, TwoFactorSetupForm, TwoFactorVerifyCodeForm, - TwoFactorVerifyPasswordForm, VerifyForm, + unique_identity_attribute, ) +from .mail_util import MailUtil +from .password_util import PasswordUtil from .phone_util import PhoneUtil from .signals import ( confirm_instructions_sent, @@ -82,6 +83,7 @@ SmsSenderFactory, check_and_get_token_status, get_hmac, + get_request_attr, get_token_status, get_url, hash_password, @@ -101,4 +103,4 @@ verify_and_update_password, ) -__version__ = "3.4.2" +__version__ = "4.0.0" diff -Nru flask-security-3.4.2/flask_security/mail_util.py flask-security-4.0.0/flask_security/mail_util.py --- flask-security-3.4.2/flask_security/mail_util.py 1970-01-01 00:00:00.000000000 +0000 +++ flask-security-4.0.0/flask_security/mail_util.py 2021-01-26 02:39:51.000000000 +0000 @@ -0,0 +1,87 @@ +""" + flask_security.mail_util + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Utility class providing methods for validating, normalizing and sending emails. + + :copyright: (c) 2020 by J. Christopher Wagner (jwag). + :license: MIT, see LICENSE for more details. + + While this default implementation uses FlaskMail - we want to make sure that + FlaskMail isn't REQUIRED (if this implementation isn't used). +""" + +import email_validator +from flask import current_app + +from .utils import config_value + + +class MailUtil: + """ + Utility class providing methods for validating, normalizing and sending emails. + + This default class uses the email_validator package to handle validation and + normalization, and the flask_mail package to send emails. + + To provide your own implementation, pass in the class as ``mail_util_cls`` + at init time. Your class will be instantiated once as part of app initialization. + + .. versionadded:: 4.0.0 + """ + + def __init__(self, app): + """Instantiate class. + + :param app: The Flask application being initialized. + """ + pass + + def send_mail( + self, template, subject, recipient, sender, body, html, user, **kwargs + ): + """Send an email via the Flask-Mail extension. + + :param template: the Template name. The message has already been rendered + however this might be useful to differentiate why the email is being sent. + :param subject: Email subject + :param recipient: Email recipient + :param sender: who to send email as (see :py:data:`SECURITY_EMAIL_SENDER`) + :param body: the rendered body (text) + :param html: the rendered body (html) + :param user: the user model + """ + + from flask_mail import Message + + msg = Message(subject, sender=sender, recipients=[recipient]) + msg.body = body + msg.html = html + + mail = current_app.extensions.get("mail") + mail.send(msg) + + def normalize(self, email): + """ + Given an input email - return a normalized version. + Must be called in app context and uses :py:data:`SECURITY_EMAIL_VALIDATOR_ARGS` + config variable to pass any relevant arguments to + email_validator.validate_email() method. + + Will throw email_validator.EmailNotValidError if email isn't even valid. + """ + validator_args = config_value("EMAIL_VALIDATOR_ARGS") or {} + valid = email_validator.validate_email(email, **validator_args) + return valid.email + + def validate(self, email): + """ + Validate the given email. + If valid, the normalized version is returned. + + ValueError is thrown if not valid. + """ + + validator_args = config_value("EMAIL_VALIDATOR_ARGS") or {} + valid = email_validator.validate_email(email, **validator_args) + return valid.email diff -Nru flask-security-3.4.2/flask_security/models/fsqla.py flask-security-4.0.0/flask_security/models/fsqla.py --- flask-security-3.4.2/flask_security/models/fsqla.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/models/fsqla.py 2021-01-26 02:39:51.000000000 +0000 @@ -1,9 +1,11 @@ """ -Copyright 2019 by J. Christopher Wagner (jwag). All rights reserved. +Copyright 2019-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 +Complete models for all features when using Flask-SqlAlchemy. + +You can change the table names by passing them in to the set_db_info() method. BE AWARE: Once any version of this is shipped no changes can be made - instead a new version needs to be created. @@ -26,7 +28,7 @@ from flask_security import RoleMixin, UserMixin -class FsModels(object): +class FsModels: """ Helper class for model mixins. This records the ``db`` (which is a Flask-SqlAlchemy object) for use in @@ -36,18 +38,22 @@ roles_users = None db = None fs_model_version = 1 + user_table_name = "user" + role_table_name = "role" @classmethod - def set_db_info(cls, appdb): - """ Initialize Model. + def set_db_info(cls, appdb, user_table_name="user", role_table_name="role"): + """Initialize Model. This needs to be called after the DB object has been created (e.g. db = Sqlalchemy()) """ cls.db = appdb + cls.user_table_name = user_table_name + cls.role_table_name = role_table_name cls.roles_users = appdb.Table( "roles_users", - Column("user_id", Integer(), ForeignKey("user.id")), - Column("role_id", Integer(), ForeignKey("role.id")), + Column("user_id", Integer(), ForeignKey(f"{cls.user_table_name}.id")), + Column("role_id", Integer(), ForeignKey(f"{cls.role_table_name}.id")), ) @@ -66,8 +72,7 @@ class FsUserMixin(UserMixin): - """ User information - """ + """User information""" # flask_security basic fields id = Column(Integer, primary_key=True) @@ -77,7 +82,7 @@ password = Column(String(255), nullable=False) active = Column(Boolean(), nullable=False) - # Faster token checking + # Flask-Security user identifier fs_uniquifier = Column(String(64), unique=True, nullable=False) # confirmable @@ -97,6 +102,7 @@ @declared_attr def roles(cls): + # The first arg is a class name, the backref is a column name return FsModels.db.relationship( "Role", secondary=FsModels.roles_users, @@ -117,7 +123,7 @@ """ -class FsOauth2ClientMixin(object): +class FsOauth2ClientMixin: """ Oauth2 client """ id = Column(String(64), primary_key=True) @@ -138,7 +144,7 @@ redirect_uris = Column(UnicodeText()) -class FsTokenMixin(object): +class FsTokenMixin: """ (Bearer) Tokens that have been given out """ id = Column(Integer, primary_key=True) diff -Nru flask-security-3.4.2/flask_security/models/fsqla_v2.py flask-security-4.0.0/flask_security/models/fsqla_v2.py --- flask-security-3.4.2/flask_security/models/fsqla_v2.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/models/fsqla_v2.py 2021-01-26 02:39:51.000000000 +0000 @@ -32,8 +32,7 @@ class FsUserMixin(FsUserMixinV1): - """ User information - """ + """User information""" # Make username unique but not required. username = Column(String(255), unique=True, nullable=True) diff -Nru flask-security-3.4.2/flask_security/passwordless.py flask-security-4.0.0/flask_security/passwordless.py --- flask-security-3.4.2/flask_security/passwordless.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/passwordless.py 2021-01-26 02:39:51.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ flask_security.passwordless ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -13,7 +12,7 @@ from werkzeug.local import LocalProxy from .signals import login_instructions_sent -from .utils import config_value, get_token_status, url_for_security +from .utils import config_value, get_token_status, send_mail, url_for_security # Convenient references _security = LocalProxy(lambda: app.extensions["security"]) @@ -25,12 +24,11 @@ """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( + send_mail( config_value("EMAIL_SUBJECT_PASSWORDLESS"), user.email, "login_instructions", @@ -48,7 +46,7 @@ :param user: The user the token belongs to """ - return _security.login_serializer.dumps([str(user.id)]) + return _security.login_serializer.dumps([str(user.fs_uniquifier)]) def login_token_status(token): diff -Nru flask-security-3.4.2/flask_security/password_util.py flask-security-4.0.0/flask_security/password_util.py --- flask-security-3.4.2/flask_security/password_util.py 1970-01-01 00:00:00.000000000 +0000 +++ flask-security-4.0.0/flask_security/password_util.py 2021-01-26 02:39:51.000000000 +0000 @@ -0,0 +1,77 @@ +""" + flask_security.password_util + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Utility class providing methods for validating and normalizing passwords. + + :copyright: (c) 2020 by J. Christopher Wagner (jwag). + :license: MIT, see LICENSE for more details. + +""" +import unicodedata + +from .utils import ( + config_value, + password_length_validator, + password_breached_validator, + password_complexity_validator, +) + + +class PasswordUtil: + """ + Utility class providing methods for validating and normalizing passwords. + + To provide your own implementation, pass in the class as ``password_util_cls`` + at init time. Your class will be instantiated once as part of app initialization. + + .. versionadded:: 4.0.0 + """ + + def __init__(self, app): + """Instantiate class. + + :param app: The Flask application being initialized. + """ + pass + + def normalize(self, password): + """ + Given an input password - return a normalized version (using Python's + unicodedata.normalize()). + Must be called in app context and uses + :py:data:`SECURITY_PASSWORD_NORMALIZE_FORM` config variable. + """ + cf = config_value("PASSWORD_NORMALIZE_FORM") + if cf: + return unicodedata.normalize(cf, password) + return password + + def validate(self, password, is_register, **kwargs): + """ + Password validation. + Called in app/request context. + + If is_register is True then kwargs will be the contents of the register form. + If is_register is False, then there is a single kwarg "user" which has the + current user data model. + + The password is first normalized then validated. + Return value is a tuple ([msgs], normalized_password) + """ + + cf = config_value("PASSWORD_NORMALIZE_FORM") + if cf: + pnorm = unicodedata.normalize(cf, password) + else: + pnorm = password + + notok = password_length_validator(pnorm) + if notok: + return notok, pnorm + + notok = password_breached_validator(pnorm) + if notok: + return notok, pnorm + + return password_complexity_validator(pnorm, is_register, **kwargs), pnorm diff -Nru flask-security-3.4.2/flask_security/phone_util.py flask-security-4.0.0/flask_security/phone_util.py --- flask-security-3.4.2/flask_security/phone_util.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/phone_util.py 2021-01-26 02:39:51.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ flask_security.phone_util ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -14,20 +13,32 @@ from .utils import config_value, get_message -class PhoneUtil(object): +class PhoneUtil: """ 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. + at init time. Your class will be instantiated once as part of + Flask-Security initialization. .. versionadded:: 3.4.0 + + .. versionchanged:: 4.0.0 + __init__ takes app argument, and is instantiated at Flask-Security + initialization time rather than at first request. """ + def __init__(self, app): + """Instantiate class. + + :param app: The Flask application being initialized. + """ + pass + def validate_phone_number(self, input_data): - """ Return ``None`` if a valid phone number else an error message. """ + """Return ``None`` if a valid phone number else + the ``PHONE_INVALID`` error message.""" import phonenumbers try: @@ -41,7 +52,7 @@ return get_message("PHONE_INVALID")[0] def get_canonical_form(self, input_data): - """ Validate and return a canonical form to be stored in DB + """Validate and return a canonical form to be stored in DB and compared against. Returns ``None`` if input isn't a valid phone number. """ diff -Nru flask-security-3.4.2/flask_security/quart_compat.py flask-security-4.0.0/flask_security/quart_compat.py --- flask-security-3.4.2/flask_security/quart_compat.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/quart_compat.py 2021-01-26 02:39:51.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ flask_security.quart_compat ~~~~~~~~~~~~~~~~~~~~ diff -Nru flask-security-3.4.2/flask_security/recoverable.py flask-security-4.0.0/flask_security/recoverable.py --- flask-security-3.4.2/flask_security/recoverable.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/recoverable.py 2021-01-26 02:39:51.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ flask_security.recoverable ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -6,6 +5,7 @@ Flask-Security recoverable module :copyright: (c) 2012 by Matt Wright. + :copyright: (c) 2019-2020 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ @@ -18,6 +18,7 @@ get_token_status, hash_data, hash_password, + send_mail, url_for_security, verify_hash, ) @@ -37,7 +38,7 @@ reset_link = url_for_security("reset_password", token=token, _external=True) if config_value("SEND_PASSWORD_RESET_EMAIL"): - _security._send_mail( + send_mail( config_value("EMAIL_SUBJECT_PASSWORD_RESET"), user.email, "reset_instructions", @@ -56,7 +57,7 @@ :param user: The user to send the notice to """ if config_value("SEND_PASSWORD_RESET_NOTICE_EMAIL"): - _security._send_mail( + send_mail( config_value("EMAIL_SUBJECT_PASSWORD_NOTICE"), user.email, "reset_notice", @@ -70,7 +71,7 @@ :param user: The user to work with """ password_hash = hash_data(user.password) if user.password else None - data = [str(user.id), password_hash] + data = [str(user.fs_uniquifier), password_hash] return _security.reset_serializer.dumps(data) @@ -85,6 +86,11 @@ expired, invalid, user, data = get_token_status( token, "reset", "RESET_PASSWORD", return_data=True ) + # This check looks to see if the password has been changed since the reset token + # was created. As of #338 - we reset the fs_uniquifier on each password change + # so the token would have been marked invalid above. + # This made sure that the token couldn't be used twice. + # TODO - look at removing this entire check. if not invalid and user: if user.password: if not verify_hash(data[1], user.password): @@ -100,8 +106,8 @@ :param password: The unhashed new password """ user.password = hash_password(password) - if config_value("BACKWARDS_COMPAT_AUTH_TOKEN_INVALID"): - _datastore.set_uniquifier(user) + # Change uniquifier - this will cause ALL sessions to be invalidated. + _datastore.set_uniquifier(user) _datastore.put(user) send_password_reset_notice(user) password_reset.send(app._get_current_object(), user=user) diff -Nru flask-security-3.4.2/flask_security/registerable.py flask-security-4.0.0/flask_security/registerable.py --- flask-security-3.4.2/flask_security/registerable.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/registerable.py 2021-01-26 02:39:51.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ flask_security.registerable ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -17,7 +16,7 @@ from .confirmable import generate_confirmation_link from .signals import user_registered -from .utils import config_value, do_flash, get_message, hash_password +from .utils import config_value, do_flash, get_message, hash_password, send_mail # Convenient references _security = LocalProxy(lambda: app.extensions["security"]) @@ -61,7 +60,7 @@ ) if config_value("SEND_REGISTER_EMAIL"): - _security._send_mail( + send_mail( config_value("EMAIL_SUBJECT_REGISTER"), user.email, "welcome", diff -Nru flask-security-3.4.2/flask_security/signals.py flask-security-4.0.0/flask_security/signals.py --- flask-security-3.4.2/flask_security/signals.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/signals.py 2021-01-26 02:39:51.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ flask_security.signals ~~~~~~~~~~~~~~~~~~~~~~ diff -Nru flask-security-3.4.2/flask_security/templates/security/change_password.html flask-security-4.0.0/flask_security/templates/security/change_password.html --- flask-security-3.4.2/flask_security/templates/security/change_password.html 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/templates/security/change_password.html 2021-01-26 02:39:51.000000000 +0000 @@ -3,7 +3,7 @@ {% block content %} {% include "security/_messages.html" %} -

{{ _('Change password') }}

+

{{ _fsdomain('Change password') }}

{{ change_password_form.hidden_tag() }} {{ render_field_with_errors(change_password_form.password) }} diff -Nru flask-security-3.4.2/flask_security/templates/security/email/change_notice.html flask-security-4.0.0/flask_security/templates/security/email/change_notice.html --- flask-security-3.4.2/flask_security/templates/security/email/change_notice.html 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/templates/security/email/change_notice.html 2021-01-26 02:39:51.000000000 +0000 @@ -1,4 +1,4 @@ -

{{ _('Your password has been changed.') }}

+

{{ _fsdomain('Your password has been changed.') }}

{% if security.recoverable %} -

{{ _('If you did not change your password,') }} {{ _('click here to reset it') }}.

+

{{ _fsdomain('If you did not change your password,') }} {{ _fsdomain('click here to reset it') }}.

{% endif %} diff -Nru flask-security-3.4.2/flask_security/templates/security/email/change_notice.txt flask-security-4.0.0/flask_security/templates/security/email/change_notice.txt --- flask-security-3.4.2/flask_security/templates/security/email/change_notice.txt 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/templates/security/email/change_notice.txt 2021-01-26 02:39:51.000000000 +0000 @@ -1,5 +1,5 @@ -{{ _('Your password has been changed') }} +{{ _fsdomain('Your password has been changed') }} {% if security.recoverable %} -{{ _('If you did not change your password, click the link below to reset it.') }} +{{ _fsdomain('If you did not change your password, click the link below to reset it.') }} {{ url_for_security('forgot_password', _external=True) }} {% endif %} diff -Nru flask-security-3.4.2/flask_security/templates/security/email/confirmation_instructions.html flask-security-4.0.0/flask_security/templates/security/email/confirmation_instructions.html --- flask-security-3.4.2/flask_security/templates/security/email/confirmation_instructions.html 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/templates/security/email/confirmation_instructions.html 2021-01-26 02:39:51.000000000 +0000 @@ -1,3 +1,3 @@ -

{{ _('Please confirm your email through the link below:') }}

+

{{ _fsdomain('Please confirm your email through the link below:') }}

-

{{ _('Confirm my account') }}

+

{{ _fsdomain('Confirm my account') }}

diff -Nru flask-security-3.4.2/flask_security/templates/security/email/confirmation_instructions.txt flask-security-4.0.0/flask_security/templates/security/email/confirmation_instructions.txt --- flask-security-3.4.2/flask_security/templates/security/email/confirmation_instructions.txt 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/templates/security/email/confirmation_instructions.txt 2021-01-26 02:39:51.000000000 +0000 @@ -1,3 +1,3 @@ -{{ _('Please confirm your email through the link below:') }} +{{ _fsdomain('Please confirm your email through the link below:') }} {{ confirmation_link }} diff -Nru flask-security-3.4.2/flask_security/templates/security/email/login_instructions.html flask-security-4.0.0/flask_security/templates/security/email/login_instructions.html --- flask-security-3.4.2/flask_security/templates/security/email/login_instructions.html 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/templates/security/email/login_instructions.html 2021-01-26 02:39:51.000000000 +0000 @@ -1,5 +1,5 @@ -

{{ _('Welcome %(email)s!', email=user.email) }}

+

{{ _fsdomain('Welcome %(email)s!', email=user.email) }}

-

{{ _('You can log into your account through the link below:') }}

+

{{ _fsdomain('You can log into your account through the link below:') }}

-

{{ _('Login now') }}

+

{{ _fsdomain('Login now') }}

diff -Nru flask-security-3.4.2/flask_security/templates/security/email/login_instructions.txt flask-security-4.0.0/flask_security/templates/security/email/login_instructions.txt --- flask-security-3.4.2/flask_security/templates/security/email/login_instructions.txt 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/templates/security/email/login_instructions.txt 2021-01-26 02:39:51.000000000 +0000 @@ -1,5 +1,5 @@ -{{ _('Welcome %(email)s!', email=user.email) }} +{{ _fsdomain('Welcome %(email)s!', email=user.email) }} -{{ _('You can log into your account through the link below:') }} +{{ _fsdomain('You can log into your account through the link below:') }} {{ login_link }} diff -Nru flask-security-3.4.2/flask_security/templates/security/email/reset_instructions.html flask-security-4.0.0/flask_security/templates/security/email/reset_instructions.html --- flask-security-3.4.2/flask_security/templates/security/email/reset_instructions.html 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/templates/security/email/reset_instructions.html 2021-01-26 02:39:51.000000000 +0000 @@ -1 +1 @@ -

{{ _('Click here to reset your password') }}

+

{{ _fsdomain('Click here to reset your password') }}

diff -Nru flask-security-3.4.2/flask_security/templates/security/email/reset_instructions.txt flask-security-4.0.0/flask_security/templates/security/email/reset_instructions.txt --- flask-security-3.4.2/flask_security/templates/security/email/reset_instructions.txt 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/templates/security/email/reset_instructions.txt 2021-01-26 02:39:51.000000000 +0000 @@ -1,3 +1,3 @@ -{{ _('Click the link below to reset your password:') }} +{{ _fsdomain('Click the link below to reset your password:') }} {{ reset_link }} diff -Nru flask-security-3.4.2/flask_security/templates/security/email/reset_notice.html flask-security-4.0.0/flask_security/templates/security/email/reset_notice.html --- flask-security-3.4.2/flask_security/templates/security/email/reset_notice.html 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/templates/security/email/reset_notice.html 2021-01-26 02:39:51.000000000 +0000 @@ -1 +1 @@ -

{{ _('Your password has been reset') }}

+

{{ _fsdomain('Your password has been reset') }}

diff -Nru flask-security-3.4.2/flask_security/templates/security/email/reset_notice.txt flask-security-4.0.0/flask_security/templates/security/email/reset_notice.txt --- flask-security-3.4.2/flask_security/templates/security/email/reset_notice.txt 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/templates/security/email/reset_notice.txt 2021-01-26 02:39:51.000000000 +0000 @@ -1 +1 @@ -{{ _('Your password has been reset') }} +{{ _fsdomain('Your password has been reset') }} diff -Nru flask-security-3.4.2/flask_security/templates/security/email/two_factor_instructions.html flask-security-4.0.0/flask_security/templates/security/email/two_factor_instructions.html --- flask-security-3.4.2/flask_security/templates/security/email/two_factor_instructions.html 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/templates/security/email/two_factor_instructions.html 2021-01-26 02:39:51.000000000 +0000 @@ -1,3 +1,3 @@ -

{{ _("Welcome") }} {{ username }}!

+

{{ _fsdomain("Welcome") }} {{ username }}!

-

{{ _("You can log into your account using the following code:") }} {{ token }}

+

{{ _fsdomain("You can log into your account using the following code:") }} {{ token }}

diff -Nru flask-security-3.4.2/flask_security/templates/security/email/two_factor_instructions.txt flask-security-4.0.0/flask_security/templates/security/email/two_factor_instructions.txt --- flask-security-3.4.2/flask_security/templates/security/email/two_factor_instructions.txt 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/templates/security/email/two_factor_instructions.txt 2021-01-26 02:39:51.000000000 +0000 @@ -1,3 +1,3 @@ -{{ _("Welcome") }} {{ username }}! +{{ _fsdomain("Welcome") }} {{ username }}! -{{ _("You can log into your account using the following code:") }} {{ token }} +{{ _fsdomain("You can log into your account using the following code:") }} {{ token }} diff -Nru flask-security-3.4.2/flask_security/templates/security/email/two_factor_rescue.html flask-security-4.0.0/flask_security/templates/security/email/two_factor_rescue.html --- flask-security-3.4.2/flask_security/templates/security/email/two_factor_rescue.html 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/templates/security/email/two_factor_rescue.html 2021-01-26 02:39:51.000000000 +0000 @@ -1 +1 @@ -

{{ user.email }} {{ _("can not access mail account") }}

+

{{ user.email }} {{ _fsdomain("can not access mail account") }}

diff -Nru flask-security-3.4.2/flask_security/templates/security/email/two_factor_rescue.txt flask-security-4.0.0/flask_security/templates/security/email/two_factor_rescue.txt --- flask-security-3.4.2/flask_security/templates/security/email/two_factor_rescue.txt 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/templates/security/email/two_factor_rescue.txt 2021-01-26 02:39:51.000000000 +0000 @@ -1 +1 @@ -{{ user.email }} {{ _("can not access mail account") }} +{{ user.email }} {{ _fsdomain("can not access mail account") }} diff -Nru flask-security-3.4.2/flask_security/templates/security/email/us_instructions.html flask-security-4.0.0/flask_security/templates/security/email/us_instructions.html --- flask-security-3.4.2/flask_security/templates/security/email/us_instructions.html 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/templates/security/email/us_instructions.html 2021-01-26 02:39:51.000000000 +0000 @@ -1,9 +1,9 @@ -

{{ _("Welcome") }} {{ username }}!

+

{{ _fsdomain("Welcome") }} {{ username }}!

-

{{ _("You can sign into your account using the following code:") }} {{ token }}

+

{{ _fsdomain("You can sign into your account using the following code:") }} {{ token }}

{% if login_link %} -

{{ _("Or use the the link below:") }}

+

{{ _fsdomain("Or use the the link below:") }}

-

{{ _("Sign In") }}

+

{{ _fsdomain("Sign In") }}

{% endif %} diff -Nru flask-security-3.4.2/flask_security/templates/security/email/us_instructions.txt flask-security-4.0.0/flask_security/templates/security/email/us_instructions.txt --- flask-security-3.4.2/flask_security/templates/security/email/us_instructions.txt 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/templates/security/email/us_instructions.txt 2021-01-26 02:39:51.000000000 +0000 @@ -1,10 +1,10 @@ -{{ _("Welcome") }} {{ username }}! +{{ _fsdomain("Welcome") }} {{ username }}! -{{ _("You can sign into your account using the following code:") }} {{ token }} +{{ _fsdomain("You can sign into your account using the following code:") }} {{ token }} {% if login_link %} - {{ _("Or use the link below:") }} + {{ _fsdomain("Or use the link below:") }} {{ login_link }} {% endif %} diff -Nru flask-security-3.4.2/flask_security/templates/security/email/welcome.html flask-security-4.0.0/flask_security/templates/security/email/welcome.html --- flask-security-3.4.2/flask_security/templates/security/email/welcome.html 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/templates/security/email/welcome.html 2021-01-26 02:39:51.000000000 +0000 @@ -1,7 +1,7 @@ -

{{ _('Welcome %(email)s!', email=user.email) }}

+

{{ _fsdomain('Welcome %(email)s!', email=user.email) }}

{% if security.confirmable %} -

{{ _('You can confirm your email through the link below:') }}

+

{{ _fsdomain('You can confirm your email through the link below:') }}

-

{{ _('Confirm my account') }}

+

{{ _fsdomain('Confirm my account') }}

{% endif %} diff -Nru flask-security-3.4.2/flask_security/templates/security/email/welcome.txt flask-security-4.0.0/flask_security/templates/security/email/welcome.txt --- flask-security-3.4.2/flask_security/templates/security/email/welcome.txt 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/templates/security/email/welcome.txt 2021-01-26 02:39:51.000000000 +0000 @@ -1,7 +1,7 @@ -{{ _('Welcome %(email)s!', email=user.email) }} +{{ _fsdomain('Welcome %(email)s!', email=user.email) }} {% if security.confirmable %} -{{ _('You can confirm your email through the link below:') }} +{{ _fsdomain('You can confirm your email through the link below:') }} {{ confirmation_link }} {% endif %} diff -Nru flask-security-3.4.2/flask_security/templates/security/forgot_password.html flask-security-4.0.0/flask_security/templates/security/forgot_password.html --- flask-security-3.4.2/flask_security/templates/security/forgot_password.html 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/templates/security/forgot_password.html 2021-01-26 02:39:51.000000000 +0000 @@ -3,10 +3,10 @@ {% block content %} {% include "security/_messages.html" %} -

{{ _('Send password reset instructions') }}

+

{{ _fsdomain('Send password reset instructions') }}

{{ forgot_password_form.hidden_tag() }} - {{ render_field_with_errors(forgot_password_form.email) }} + {{ render_field_with_errors(forgot_password_form.email) }} {{ render_field(forgot_password_form.submit) }}
{% include "security/_menu.html" %} diff -Nru flask-security-3.4.2/flask_security/templates/security/login_user.html flask-security-4.0.0/flask_security/templates/security/login_user.html --- flask-security-3.4.2/flask_security/templates/security/login_user.html 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/templates/security/login_user.html 2021-01-26 02:39:51.000000000 +0000 @@ -3,7 +3,7 @@ {% block content %} {% include "security/_messages.html" %} -

{{ _('Login') }}

+

{{ _fsdomain('Login') }}

{{ login_user_form.hidden_tag() }} {{ render_field_with_errors(login_user_form.email) }} diff -Nru flask-security-3.4.2/flask_security/templates/security/_menu.html flask-security-4.0.0/flask_security/templates/security/_menu.html --- flask-security-3.4.2/flask_security/templates/security/_menu.html 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/templates/security/_menu.html 2021-01-26 02:39:51.000000000 +0000 @@ -1,20 +1,20 @@ {% if security.registerable or security.recoverable or security.confirmable or security.unified_signin %} -

{{ _('Menu') }}

+

{{ _fsdomain('Menu') }}

{% endif %} diff -Nru flask-security-3.4.2/flask_security/templates/security/register_user.html flask-security-4.0.0/flask_security/templates/security/register_user.html --- flask-security-3.4.2/flask_security/templates/security/register_user.html 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/templates/security/register_user.html 2021-01-26 02:39:51.000000000 +0000 @@ -3,7 +3,7 @@ {% block content %} {% include "security/_messages.html" %} -

{{ _('Register') }}

+

{{ _fsdomain('Register') }}

{{ register_user_form.hidden_tag() }} {{ render_field_with_errors(register_user_form.email) }} diff -Nru flask-security-3.4.2/flask_security/templates/security/reset_password.html flask-security-4.0.0/flask_security/templates/security/reset_password.html --- flask-security-3.4.2/flask_security/templates/security/reset_password.html 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/templates/security/reset_password.html 2021-01-26 02:39:51.000000000 +0000 @@ -3,7 +3,7 @@ {% block content %} {% include "security/_messages.html" %} -

{{ _('Reset password') }}

+

{{ _fsdomain('Reset password') }}

{{ reset_password_form.hidden_tag() }} {{ render_field_with_errors(reset_password_form.password) }} diff -Nru flask-security-3.4.2/flask_security/templates/security/send_confirmation.html flask-security-4.0.0/flask_security/templates/security/send_confirmation.html --- flask-security-3.4.2/flask_security/templates/security/send_confirmation.html 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/templates/security/send_confirmation.html 2021-01-26 02:39:51.000000000 +0000 @@ -3,7 +3,7 @@ {% block content %} {% include "security/_messages.html" %} -

{{ _('Resend confirmation instructions') }}

+

{{ _fsdomain('Resend confirmation instructions') }}

{{ send_confirmation_form.hidden_tag() }} {{ render_field_with_errors(send_confirmation_form.email) }} diff -Nru flask-security-3.4.2/flask_security/templates/security/send_login.html flask-security-4.0.0/flask_security/templates/security/send_login.html --- flask-security-3.4.2/flask_security/templates/security/send_login.html 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/templates/security/send_login.html 2021-01-26 02:39:51.000000000 +0000 @@ -3,7 +3,7 @@ {% block content %} {% include "security/_messages.html" %} -

{{ _('Login') }}

+

{{ _fsdomain('Login') }}

{{ send_login_form.hidden_tag() }} {{ render_field_with_errors(send_login_form.email) }} diff -Nru flask-security-3.4.2/flask_security/templates/security/two_factor_setup.html flask-security-4.0.0/flask_security/templates/security/two_factor_setup.html --- flask-security-3.4.2/flask_security/templates/security/two_factor_setup.html 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/templates/security/two_factor_setup.html 2021-01-26 02:39:51.000000000 +0000 @@ -1,10 +1,35 @@ +{# + This template receives different input based on state of tf-setup. In addition + to form values the following are available: + On GET: + choices: Value of SECURITY_TWO_FACTOR_ENABLED_METHODS (with possible addition of 'delete' + two_factor_required: Value of SECURITY_TWO_FACTOR_REQUIRED + On successful POST: + chosen_method: which 2FA method was chosen (e.g. sms, authenticator) + choices: Value of SECURITY_TWO_FACTOR_ENABLED_METHODS + + If chosen_method == 'authenticator': + authr_qrcode: the image source for the qrcode + authr_key: same key as in qrcode - for possible manual entry + authr_username: same username as in qrcode + authr_issuer: same issuer as in qrcode +#} + {% extends "security/base.html" %} {% from "security/_macros.html" import render_field_with_errors, render_field, render_field_no_label, render_field_errors %} +{% block styles %} + {{ super() }} + +{% endblock %} + {% block content %} {% include "security/_messages.html" %} -

{{ _("Two-factor authentication adds an extra layer of security to your account") }}

-

{{ _("In addition to your username and password, you'll need to use a code that we will send you") }}

+

{{ _fsdomain("Two-factor authentication adds an extra layer of security to your account") }}

+

{{ _fsdomain("In addition to your username and password, you'll need to use a code that we will send you") }}

{{ two_factor_setup_form.hidden_tag() }} {% for subfield in two_factor_setup_form.setup %} @@ -15,19 +40,30 @@ {{ render_field_errors(two_factor_setup_form.setup) }} {{ render_field(two_factor_setup_form.submit) }} {% if chosen_method=="email" and chosen_method in choices %} -

{{ _("To complete logging in, please enter the code sent to your mail") }}

+

{{ _fsdomain("To complete logging in, please enter the code sent to your mail") }}

{% endif %} {% if chosen_method=="authenticator" and chosen_method in choices %} -

{{ _("Open your authenticator app on your device and scan the following qrcode to start receiving codes:") }}

-

{{ _(

+
+
+
+ {{ _fsdomain("Open an authenticator app on your device and scan the following QRcode (or enter the code below manually) to start receiving codes:") }} +
+
+ {{ _fsdomain( +
+
+ {{ authr_key }} +
+
{% endif %} {% if chosen_method=="sms" and chosen_method in choices %} -

{{ _("To Which Phone Number Should We Send Code To?") }}

+

{{ _fsdomain("To Which Phone Number Should We Send Code To?") }}

{{ 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 %}
+
{{ two_factor_verify_code_form.hidden_tag() }} diff -Nru flask-security-3.4.2/flask_security/templates/security/two_factor_verify_code.html flask-security-4.0.0/flask_security/templates/security/two_factor_verify_code.html --- flask-security-3.4.2/flask_security/templates/security/two_factor_verify_code.html 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/templates/security/two_factor_verify_code.html 2021-01-26 02:39:51.000000000 +0000 @@ -3,8 +3,8 @@ {% block content %} {% include "security/_messages.html" %} -

{{ _("Two-factor Authentication") }}

-

{{ _("Please enter your authentication code") }}

+

{{ _fsdomain("Two-factor Authentication") }}

+

{{ _fsdomain("Please enter your authentication code") }}

{{ two_factor_verify_code_form.hidden_tag() }} @@ -15,10 +15,10 @@ {{ two_factor_rescue_form.hidden_tag() }} {{ render_field_with_errors(two_factor_rescue_form.help_setup) }} {% if problem=="lost_device" %} -

{{ _("The code for authentication was sent to your email address") }}

+

{{ _fsdomain("The code for authentication was sent to your email address") }}

{% endif %} {% if problem=="no_mail_access" %} -

{{ _("A mail was sent to us in order to reset your application account") }}

+

{{ _fsdomain("A mail was sent to us in order to reset your application account") }}

{% endif %} {{ render_field(two_factor_rescue_form.submit) }}
diff -Nru flask-security-3.4.2/flask_security/templates/security/two_factor_verify_password.html flask-security-4.0.0/flask_security/templates/security/two_factor_verify_password.html --- flask-security-3.4.2/flask_security/templates/security/two_factor_verify_password.html 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/templates/security/two_factor_verify_password.html 1970-01-01 00:00:00.000000000 +0000 @@ -1,13 +0,0 @@ -{% extends "security/base.html" %} -{% from "security/_macros.html" import render_field_with_errors, render_field %} - -{% block content %} - {% include "security/_messages.html" %} -

{{ _("Please Enter Your Password") }}

-
- {{ 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) }} -
-{% endblock %} diff -Nru flask-security-3.4.2/flask_security/templates/security/us_setup.html flask-security-4.0.0/flask_security/templates/security/us_setup.html --- flask-security-3.4.2/flask_security/templates/security/us_setup.html 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/templates/security/us_setup.html 2021-01-26 02:39:51.000000000 +0000 @@ -1,9 +1,39 @@ +{# + This template receives the following pieces of context in addition to the form: + On GET: + available_methods: Value of SECURITY_US_ENABLED_METHODS + active_methods: Which methods user has already set up + setup_methods: Which methods require a setup (e.g. password doesn't require any setup) + + On successful POST: + available_methods: Value of SECURITY_US_ENABLED_METHODS + active_methods: Which methods user has already set up + setup_methods: Which methods require a setup (e.g. password doesn't require any setup) + chosen_method: which identity method was chosen (e.g. sms, authenticator) + code_sent: Was a code sent? + state: a signed state token used to validate the code. + + If chosen method is 'authenticator' then additionally: + authr_qrcode: the image source for the qrcode + authr_key: same key as in qrcode - for possible manual entry + authr_username: same username as in qrcode + authr_issuer: same issuer as in qrcode +#} + {% extends "security/base.html" %} {% from "security/_macros.html" import render_field_with_errors, render_field, render_field_errors %} +{% block styles %} + {{ super() }} + +{% endblock %} + {% block content %} {% include "security/_messages.html" %} -

{{ _("Setup Unified Sign In options") }}

+

{{ _fsdomain("Setup Unified Sign In options") }}

{{ us_setup_form.hidden_tag() }} @@ -24,19 +54,31 @@ {% if "sms" in available_methods %} {{ render_field_with_errors(us_setup_form.phone) }} {% endif %} - {% if chosen_method == "authenticator" %} -

{{ _("Open your authenticator app on your device and scan the following qrcode to start receiving codes:") }}

-

{{ _(

- {% endif %} {% if code_sent %} -

{{ _("Code has been sent") }} +

{{ _fsdomain("Code has been sent") }}
{% endif %} {{ render_field(us_setup_form.submit) }} + + {% if chosen_method == "authenticator" %} +
+
+
+ {{ _fsdomain("Open an authenticator app on your device and scan the following QRcode (or enter the code below manually) to start receiving passcodes:") }} +
+
+ {{ _fsdomain( +
+
+ {{ authr_key }} +
+
+ {% endif %} {% else %} -

{{ _("No methods have been enabled - nothing to setup") }}

+

{{ _fsdomain("No methods have been enabled - nothing to setup") }}

{% endif %}
- {% if state %} + {% if state %} +
{{ us_setup_validate_form.hidden_tag() }} diff -Nru flask-security-3.4.2/flask_security/templates/security/us_signin.html flask-security-4.0.0/flask_security/templates/security/us_signin.html --- flask-security-3.4.2/flask_security/templates/security/us_signin.html 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/templates/security/us_signin.html 2021-01-26 02:39:51.000000000 +0000 @@ -3,7 +3,7 @@ {% block content %} {% include "security/_messages.html" %} -

{{ _("Sign In") }}

+

{{ _fsdomain("Sign In") }}

{{ us_signin_form.hidden_tag() }} @@ -12,7 +12,7 @@ {{ render_field_with_errors(us_signin_form.remember) }} {{ render_field(us_signin_form.submit) }} {% if code_methods %} -

{{ _("Request one-time code be sent") }}

+

{{ _fsdomain("Request one-time code be sent") }}

{% for subfield in us_signin_form.chosen_method %} {% if subfield.data in code_methods %} {{ render_field_with_errors(subfield) }} @@ -20,7 +20,7 @@ {% endfor %} {{ render_field_errors(us_signin_form.chosen_method) }} {% if code_sent %} -

{{ _("Code has been sent") }} +

{{ _fsdomain("Code has been sent") }} {% endif %} {{ render_field(us_signin_form.submit_send_code, formaction=url_for_security('us_signin_send_code')) }} {% endif %} diff -Nru flask-security-3.4.2/flask_security/templates/security/us_verify.html flask-security-4.0.0/flask_security/templates/security/us_verify.html --- flask-security-3.4.2/flask_security/templates/security/us_verify.html 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/templates/security/us_verify.html 2021-01-26 02:39:51.000000000 +0000 @@ -3,14 +3,14 @@ {% block content %} {% include "security/_messages.html" %} -

{{ _("Please re-authenticate") }}

+

{{ _fsdomain("Please re-authenticate") }}

{{ us_verify_form.hidden_tag() }} {{ render_field_with_errors(us_verify_form.passcode) }} {{ render_field(us_verify_form.submit) }} {% if code_methods %} -

{{ _("Request one-time code be sent") }}

+

{{ _fsdomain("Request one-time code be sent") }}

{% for subfield in us_verify_form.chosen_method %} {% if subfield.data in code_methods %} {{ render_field_with_errors(subfield) }} @@ -18,7 +18,7 @@ {% endfor %} {{ render_field_errors(us_verify_form.chosen_method) }} {% if code_sent %} -

{{ _("Code has been sent") }} +

{{ _fsdomain("Code has been sent") }} {% endif %} {{ render_field(us_verify_form.submit_send_code, formaction=send_code_to) }} {% endif %} diff -Nru flask-security-3.4.2/flask_security/templates/security/verify.html flask-security-4.0.0/flask_security/templates/security/verify.html --- flask-security-3.4.2/flask_security/templates/security/verify.html 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/templates/security/verify.html 2021-01-26 02:39:51.000000000 +0000 @@ -3,7 +3,7 @@ {% block content %} {% include "security/_messages.html" %} -

{{ _("Please Enter Your Password") }}

+

{{ _fsdomain("Please Enter Your Password") }}

{{ verify_form.hidden_tag() }} diff -Nru flask-security-3.4.2/flask_security/totp.py flask-security-4.0.0/flask_security/totp.py --- flask-security-3.4.2/flask_security/totp.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/totp.py 2021-01-26 02:39:51.000000000 +0000 @@ -1,19 +1,20 @@ -# -*- coding: utf-8 -*- """ flask_security.totp ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Flask-Security TOTP (Timed-One-Time-Passwords) module - :copyright: (c) 2019 by J. Christopher Wagner (jwag). + :copyright: (c) 2019-2021 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ +import base64 +import io from passlib.totp import TOTP, TokenError -class Totp(object): - """ Encapsulate usage of Passlib TOTP functionality. +class Totp: + """Encapsulate usage of Passlib TOTP functionality. Flask-Security doesn't implement any replay-attack protection out of the box as suggested by: @@ -27,7 +28,7 @@ """ def __init__(self, secrets, issuer): - """ Initialize a totp factory. + """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 @@ -42,7 +43,7 @@ return self._totp.from_source(totp_secret).generate().token def generate_totp_secret(self): - """ Create new user-unique totp_secret. + """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. @@ -51,7 +52,7 @@ return self._totp.new().to_json(encrypt=True) def verify_totp(self, token, totp_secret, user, window=0): - """ Verifies token for specific user. + """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 @@ -80,7 +81,7 @@ return False def get_totp_uri(self, username, totp_secret): - """ Generate provisioning url for use with the qrcode + """Generate provisioning url for use with the qrcode scanner built into the app :param username: username/email of the current user @@ -89,8 +90,63 @@ tp = self._totp.from_source(totp_secret) return tp.to_uri(username) + def get_totp_pretty_key(self, totp_secret): + """Generate pretty key for manual input + + :param totp_secret: a unique shared secret of the user + + .. versionadded:: 4.0.0 + """ + tp = self._totp.from_source(totp_secret) + return tp.pretty_key() + + def fetch_setup_values(self, totp, user): + """Generate various values user needs to setup authenticator app. + Returns dict with keys: + 'key': totp key + 'image': image as string (useful for ) + 'username: qrcode best practice + 'issuer': qrcode best practice + + .. versionadded:: 4.0.0 + """ + + r = dict() + + # By convention, the URI should have the username that the user + # logs in with. + username = user.calc_username() or "Unknown" + r["username"] = username + r["key"] = self.get_totp_pretty_key(totp) + r["issuer"] = self._totp.issuer + r["image"] = self.generate_qrcode(username, totp) + return r + + def generate_qrcode(self, username, totp): + """Generate QRcode + Using username, totp, generate the actual QRcode image. + This method can be overridden to fine-tune how the image is created - + such as size, color etc. + + It must return a string suitable for use in an tag. + + .. versionadded:: 4.0.0 + """ + try: + import pyqrcode + + code = pyqrcode.create(self.get_totp_uri(username, totp)) + with io.BytesIO() as virtual_file: + code.svg(file=virtual_file, scale=3) + image_as_str = base64.b64encode(virtual_file.getvalue()).decode("ascii") + + return f"data:image/svg+xml;base64,{image_as_str}" + except ImportError: # pragma: no cover + # This should have been checked at app init. + raise + def get_last_counter(self, user): - """ Implement this to fetch stored last_counter from cache. + """Implement this to fetch stored last_counter from cache. :param user: User model :return: last_counter as stored in set_last_counter() @@ -98,7 +154,7 @@ return None def set_last_counter(self, user, tmatch): - """ Implement this to cache last_counter. + """Implement this to cache last_counter. :param user: User model :param tmatch: a TotpMatch as returned from totp.verify() Binary files /tmp/tmpl_LU2e/ak23Qk5OdH/flask-security-3.4.2/flask_security/translations/ca_ES/LC_MESSAGES/flask_security.mo and /tmp/tmpl_LU2e/U10RHAXkq7/flask-security-4.0.0/flask_security/translations/ca_ES/LC_MESSAGES/flask_security.mo differ diff -Nru flask-security-3.4.2/flask_security/translations/ca_ES/LC_MESSAGES/flask_security.po flask-security-4.0.0/flask_security/translations/ca_ES/LC_MESSAGES/flask_security.po --- flask-security-3.4.2/flask_security/translations/ca_ES/LC_MESSAGES/flask_security.po 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/translations/ca_ES/LC_MESSAGES/flask_security.po 2021-01-26 02:39:51.000000000 +0000 @@ -8,7 +8,7 @@ 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" +"POT-Creation-Date: 2021-01-22 14:48-0800\n" "PO-Revision-Date: 2019-06-16 00:12+0200\n" "Last-Translator: Orestes Sanchez \n" "Language: ca_ES\n" @@ -17,112 +17,123 @@ "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" +"Generated-By: Babel 2.9.0\n" -#: flask_security/core.py:207 +#: flask_security/core.py:209 msgid "Login Required" msgstr "Per poder veure la pàgina sol·licitada és necessari iniciar la sessió" -#: flask_security/core.py:208 +#: flask_security/core.py:210 #: flask_security/templates/security/email/two_factor_instructions.html:1 +#: flask_security/templates/security/email/two_factor_instructions.txt:1 #: flask_security/templates/security/email/us_instructions.html:1 +#: flask_security/templates/security/email/us_instructions.txt:1 +#: tests/templates/_nav.html:2 msgid "Welcome" msgstr "Benvingut" -#: flask_security/core.py:209 +#: flask_security/core.py:211 msgid "Please confirm your email" msgstr "Si us plau, confirmeu el vostre correu electrònic" -#: flask_security/core.py:210 +#: flask_security/core.py:212 msgid "Login instructions" msgstr "Instruccions d'inici de la sessió" -#: flask_security/core.py:211 +#: flask_security/core.py:213 #: flask_security/templates/security/email/reset_notice.html:1 +#: flask_security/templates/security/email/reset_notice.txt:1 msgid "Your password has been reset" msgstr "S'ha restablit la teva contrasenya" -#: flask_security/core.py:212 +#: flask_security/core.py:214 +#: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed" msgstr "S'ha canviat la teva contrasenya" -#: flask_security/core.py:213 +#: flask_security/core.py:215 msgid "Password reset instructions" msgstr "Instruccions de recuperació de la contrasenya" -#: flask_security/core.py:216 +#: flask_security/core.py:218 msgid "Two-factor Login" msgstr "" -#: flask_security/core.py:217 +#: flask_security/core.py:219 msgid "Two-factor Rescue" msgstr "" -#: flask_security/core.py:266 +#: flask_security/core.py:263 msgid "Verification Code" msgstr "" -#: flask_security/core.py:282 +#: flask_security/core.py:278 msgid "Input not appropriate for requested API" msgstr "" -#: flask_security/core.py:283 +#: flask_security/core.py:279 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 +#: flask_security/core.py:281 msgid "You are not authenticated. Please supply the correct credentials." msgstr "" -#: flask_security/core.py:289 -#, fuzzy +#: flask_security/core.py:285 msgid "You must re-authenticate to access this endpoint" -msgstr "Has d'iniciar una nova sessió per tal d'accedir a aquesta pàgina." +msgstr "" -#: flask_security/core.py:293 +#: flask_security/core.py:289 #, 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 +#: flask_security/core.py:292 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 +#: flask_security/core.py:293 msgid "Your email has already been confirmed." msgstr "El teu correu electrònic ja s'havia confirmat." -#: flask_security/core.py:298 +#: flask_security/core.py:294 msgid "Invalid confirmation token." msgstr "Token de confirmació no vàlid." -#: flask_security/core.py:300 +#: flask_security/core.py:296 #, 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 +#: flask_security/core.py:300 +#, python-format +msgid "" +"Identity attribute '%(attr)s' with value '%(value)s' is already " +"associated with an account." +msgstr "" + +#: flask_security/core.py:306 msgid "Password does not match" msgstr "La contrasenya no coincideix" -#: flask_security/core.py:304 +#: flask_security/core.py:307 msgid "Passwords do not match" msgstr "Les contrasenyes no coincideixen" -#: flask_security/core.py:305 +#: flask_security/core.py:308 msgid "Redirections outside the domain are forbidden" msgstr "Les redireccions a llocs web externes s'han prohibit" -#: flask_security/core.py:307 +#: flask_security/core.py:310 #, 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 +#: flask_security/core.py:314 #, python-format msgid "" "You did not reset your password within %(within)s. New instructions have " @@ -131,20 +142,20 @@ "No vas restablir la teva contrasenya abans de %(within)s. S'han enviat " "noves instruccions a %(email)s." -#: flask_security/core.py:317 +#: flask_security/core.py:320 msgid "Invalid reset password token." msgstr "El token per restablir la contrasenya no és vàlid." -#: flask_security/core.py:318 +#: flask_security/core.py:321 msgid "Email requires confirmation." msgstr "El correu electrònic requereix d'una confirmació." -#: flask_security/core.py:320 +#: flask_security/core.py:323 #, 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 +#: flask_security/core.py:327 #, python-format msgid "" "You did not confirm your email within %(within)s. New instructions to " @@ -153,7 +164,7 @@ "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 +#: flask_security/core.py:335 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " @@ -162,82 +173,81 @@ "No vas iniciar la sessió abans de %(within)s. S'han enviat noves " "instruccions a %(email)s." -#: flask_security/core.py:339 +#: flask_security/core.py:342 #, 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 +#: flask_security/core.py:345 msgid "Invalid login token." msgstr "Token de d'inici de sessió no vàlid." -#: flask_security/core.py:343 +#: flask_security/core.py:346 msgid "Account is disabled." msgstr "el compte està desactivat." -#: flask_security/core.py:344 +#: flask_security/core.py:347 msgid "Email not provided" msgstr "No s'ha inclòs el correu electrònic" -#: flask_security/core.py:345 +#: flask_security/core.py:348 msgid "Invalid email address" msgstr "Adreça de correu electrònic no vàlida" -#: flask_security/core.py:346 -#, fuzzy +#: flask_security/core.py:349 msgid "Invalid code" -msgstr "Contrasenya no vàlida" +msgstr "" -#: flask_security/core.py:347 +#: flask_security/core.py:350 msgid "Password not provided" msgstr "No s'ha inclòs la contrasenya" -#: flask_security/core.py:348 +#: flask_security/core.py:351 msgid "No password is set for this user" msgstr "No hi ha cap contrasenya per a l'usuari" -#: flask_security/core.py:350 +#: flask_security/core.py:353 #, fuzzy, python-format msgid "Password must be at least %(length)s characters" -msgstr "La contrasenya ha de tenir al menys 6 caràcters" +msgstr "La contrasenya ha de tenir al menys %(length)s caràcters" -#: flask_security/core.py:353 +#: flask_security/core.py:356 msgid "Password not complex enough" msgstr "" -#: flask_security/core.py:354 +#: flask_security/core.py:357 msgid "Password on breached list" msgstr "" -#: flask_security/core.py:356 +#: flask_security/core.py:359 msgid "Failed to contact breached passwords site" msgstr "" -#: flask_security/core.py:359 +#: flask_security/core.py:362 msgid "Phone number not valid e.g. missing country code" msgstr "" -#: flask_security/core.py:360 +#: flask_security/core.py:363 msgid "Specified user does not exist" msgstr "L'usuari no existeix" -#: flask_security/core.py:361 +#: flask_security/core.py:364 msgid "Invalid password" msgstr "Contrasenya no vàlida" -#: flask_security/core.py:362 +#: flask_security/core.py:365 msgid "Password or code submitted is not valid" msgstr "" -#: flask_security/core.py:363 +#: flask_security/core.py:366 msgid "You have successfully logged in." msgstr "La sessió s'ha iniciat amb èxit." -#: flask_security/core.py:364 +#: flask_security/core.py:367 msgid "Forgot password?" msgstr "Has oblidat la teva contrasenya?" -#: flask_security/core.py:366 +#: flask_security/core.py:369 msgid "" "You successfully reset your password and you have been logged in " "automatically." @@ -245,212 +255,202 @@ "Has restablert la teva contrasenya amb èxit i s'ha iniciat la sessió " "automàticament." -#: flask_security/core.py:373 +#: flask_security/core.py:376 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 +#: flask_security/core.py:379 msgid "You successfully changed your password." msgstr "La teva contrasenya s'ha modificat amb èxit." -#: flask_security/core.py:377 +#: flask_security/core.py:380 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 +#: flask_security/core.py:381 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 +#: flask_security/core.py:382 msgid "Reauthentication successful" msgstr "" -#: flask_security/core.py:381 +#: flask_security/core.py:384 msgid "You can only access this endpoint when not logged in." msgstr "" -#: flask_security/core.py:384 +#: flask_security/core.py:387 msgid "Failed to send code. Please try again later" msgstr "" -#: flask_security/core.py:385 +#: flask_security/core.py:388 msgid "Invalid Token" msgstr "" -#: flask_security/core.py:386 +#: flask_security/core.py:389 msgid "Your token has been confirmed" msgstr "" -#: flask_security/core.py:388 +#: flask_security/core.py:391 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 +#: flask_security/core.py:395 msgid "You currently do not have permissions to access this page" msgstr "" -#: flask_security/core.py:403 +#: flask_security/core.py:398 msgid "Marked method is not valid" msgstr "" -#: flask_security/core.py:405 +#: flask_security/core.py:400 msgid "You successfully disabled two factor authorization." msgstr "" -#: flask_security/core.py:408 +#: flask_security/core.py:403 msgid "Requested method is not valid" msgstr "" -#: flask_security/core.py:410 +#: flask_security/core.py:405 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "" -#: flask_security/core.py:413 +#: flask_security/core.py:408 msgid "Unified sign in setup successful" msgstr "" -#: flask_security/core.py:414 +#: flask_security/core.py:409 msgid "You must specify a valid identity to sign in" msgstr "" -#: flask_security/core.py:415 +#: flask_security/core.py:410 #, python-format msgid "Use this code to sign in: %(code)s." msgstr "" -#: flask_security/forms.py:50 +#: flask_security/forms.py:53 msgid "Email Address" msgstr "Correu electrònic" -#: flask_security/forms.py:51 +#: flask_security/forms.py:54 msgid "Password" msgstr "Contrasenya" -#: flask_security/forms.py:52 +#: flask_security/forms.py:55 msgid "Remember Me" msgstr "Recorda'm" -#: flask_security/forms.py:53 flask_security/templates/security/_menu.html:5 +#: flask_security/forms.py:56 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/forms.py:57 #: 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/forms.py:58 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 +#: flask_security/forms.py:59 msgid "Resend Confirmation Instructions" msgstr "Reenviar les instruccions de confirmació" -#: flask_security/forms.py:57 +#: flask_security/forms.py:60 msgid "Recover Password" msgstr "Restablir la contrasenya" -#: flask_security/forms.py:58 +#: flask_security/forms.py:61 msgid "Reset Password" msgstr "Restablir la contrasenya" -#: flask_security/forms.py:59 +#: flask_security/forms.py:62 msgid "Retype Password" msgstr "Escriu la contrasenya una altra vegada" -#: flask_security/forms.py:60 +#: flask_security/forms.py:63 msgid "New Password" msgstr "Nova contrasenya" -#: flask_security/forms.py:61 +#: flask_security/forms.py:64 msgid "Change Password" msgstr "Canvi de contrasenya" -#: flask_security/forms.py:62 +#: flask_security/forms.py:65 msgid "Send Login Link" msgstr "Enviar l'enllaç d'inici de sessió" -#: flask_security/forms.py:63 +#: flask_security/forms.py:66 msgid "Verify Password" msgstr "" -#: flask_security/forms.py:64 +#: flask_security/forms.py:67 msgid "Change Method" msgstr "" -#: flask_security/forms.py:65 +#: flask_security/forms.py:68 msgid "Phone Number" msgstr "" -#: flask_security/forms.py:66 +#: flask_security/forms.py:69 msgid "Authentication Code" msgstr "" -#: flask_security/forms.py:67 +#: flask_security/forms.py:70 msgid "Submit" msgstr "" -#: flask_security/forms.py:68 +#: flask_security/forms.py:71 msgid "Submit Code" msgstr "" -#: flask_security/forms.py:69 +#: flask_security/forms.py:72 msgid "Error(s)" msgstr "" -#: flask_security/forms.py:70 +#: flask_security/forms.py:73 msgid "Identity" msgstr "" -#: flask_security/forms.py:71 +#: flask_security/forms.py:74 msgid "Send Code" msgstr "" -#: flask_security/forms.py:72 -#, fuzzy +#: flask_security/forms.py:75 msgid "Passcode" -msgstr "Contrasenya" +msgstr "" -#: flask_security/unified_signin.py:145 -#, fuzzy +#: flask_security/unified_signin.py:131 msgid "Code or Password" -msgstr "Restablir la contrasenya" +msgstr "" -#: flask_security/unified_signin.py:150 flask_security/unified_signin.py:270 +#: flask_security/unified_signin.py:136 flask_security/unified_signin.py:262 msgid "Available Methods" msgstr "" -#: flask_security/unified_signin.py:151 +#: flask_security/unified_signin.py:137 msgid "Via email" msgstr "" -#: flask_security/unified_signin.py:151 +#: flask_security/unified_signin.py:137 msgid "Via SMS" msgstr "" -#: flask_security/unified_signin.py:272 +#: flask_security/unified_signin.py:264 msgid "Set up using email" msgstr "" -#: flask_security/unified_signin.py:275 +#: flask_security/unified_signin.py:267 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" -#: flask_security/unified_signin.py:277 +#: flask_security/unified_signin.py:269 msgid "Set up using SMS" msgstr "" @@ -486,32 +486,31 @@ msgid "Resend confirmation instructions" msgstr "Reenviar instruccions de confirmació" -#: flask_security/templates/security/two_factor_setup.html:6 +#: flask_security/templates/security/two_factor_setup.html:31 msgid "Two-factor authentication adds an extra layer of security to your account" msgstr "" -#: flask_security/templates/security/two_factor_setup.html:7 +#: flask_security/templates/security/two_factor_setup.html:32 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 +#: flask_security/templates/security/two_factor_setup.html:43 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 +#: flask_security/templates/security/two_factor_setup.html:49 msgid "" -"Open your authenticator app on your device and scan the following qrcode " -"to start receiving codes:" +"Open an authenticator app on your device and scan the following QRcode " +"(or enter the code below manually) to start receiving codes:" msgstr "" -#: flask_security/templates/security/two_factor_setup.html:22 +#: flask_security/templates/security/two_factor_setup.html:52 msgid "Two factor authentication code" msgstr "" -#: flask_security/templates/security/two_factor_setup.html:25 +#: flask_security/templates/security/two_factor_setup.html:60 msgid "To Which Phone Number Should We Send Code To?" msgstr "" @@ -531,27 +530,27 @@ 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 +#: flask_security/templates/security/us_setup.html:36 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_setup.html:58 #: 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" +msgstr "" + +#: flask_security/templates/security/us_setup.html:66 +msgid "" +"Open an authenticator app on your device and scan the following QRcode " +"(or enter the code below manually) to start receiving passcodes:" +msgstr "" + +#: flask_security/templates/security/us_setup.html:69 +msgid "Passwordless QRCode" +msgstr "" -#: flask_security/templates/security/us_setup.html:29 +#: flask_security/templates/security/us_setup.html:77 msgid "No methods have been enabled - nothing to setup" msgstr "" @@ -561,9 +560,12 @@ 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." +msgstr "" + +#: flask_security/templates/security/verify.html:6 +msgid "Please Enter Your Password" +msgstr "" #: flask_security/templates/security/email/change_notice.html:1 msgid "Your password has been changed." @@ -577,7 +579,12 @@ msgid "click here to reset it" msgstr "fes clic aquí per a restablir-la" +#: flask_security/templates/security/email/change_notice.txt:3 +msgid "If you did not change your password, click the link below to reset it." +msgstr "" + #: flask_security/templates/security/email/confirmation_instructions.html:1 +#: flask_security/templates/security/email/confirmation_instructions.txt:1 msgid "Please confirm your email through the link below:" msgstr "Confirma el teu correu electrònic fent clic aquí:" @@ -587,12 +594,15 @@ msgstr "Confirmeu el compte" #: flask_security/templates/security/email/login_instructions.html:1 +#: flask_security/templates/security/email/login_instructions.txt:1 #: flask_security/templates/security/email/welcome.html:1 +#: flask_security/templates/security/email/welcome.txt:1 #, python-format msgid "Welcome %(email)s!" msgstr "Benvingut %(email)s!" #: flask_security/templates/security/email/login_instructions.html:3 +#: flask_security/templates/security/email/login_instructions.txt:3 msgid "You can log into your account through the link below:" msgstr "Inicia la sessió fent clic aquí:" @@ -604,25 +614,46 @@ msgid "Click here to reset your password" msgstr "Feu clic aquí per restablir la contrasenya" +#: flask_security/templates/security/email/reset_instructions.txt:1 +msgid "Click the link below to reset your password:" +msgstr "" + #: flask_security/templates/security/email/two_factor_instructions.html:3 +#: flask_security/templates/security/email/two_factor_instructions.txt:3 msgid "You can log into your account using the following code:" msgstr "" #: flask_security/templates/security/email/two_factor_rescue.html:1 +#: flask_security/templates/security/email/two_factor_rescue.txt:1 msgid "can not access mail account" msgstr "" #: flask_security/templates/security/email/us_instructions.html:3 -#, fuzzy +#: flask_security/templates/security/email/us_instructions.txt:3 msgid "You can sign into your account using the following code:" -msgstr "Inicia la sessió fent clic aquí:" +msgstr "" #: 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ó:" +msgstr "" + +#: flask_security/templates/security/email/us_instructions.txt:7 +msgid "Or use the link below:" +msgstr "" #: flask_security/templates/security/email/welcome.html:4 +#: flask_security/templates/security/email/welcome.txt:4 msgid "You can confirm your email through the link below:" msgstr "Confirmeu el vostre correu electrònic fent clic a continuació:" +#~ msgid "You successfully confirmed password" +#~ msgstr "" + +#~ msgid "Password confirmation is needed in order to access page" +#~ msgstr "" + +#~ msgid "" +#~ "Open your authenticator app on your " +#~ "device and scan the following qrcode " +#~ "to start receiving codes:" +#~ msgstr "" Binary files /tmp/tmpl_LU2e/ak23Qk5OdH/flask-security-3.4.2/flask_security/translations/da_DK/LC_MESSAGES/flask_security.mo and /tmp/tmpl_LU2e/U10RHAXkq7/flask-security-4.0.0/flask_security/translations/da_DK/LC_MESSAGES/flask_security.mo differ diff -Nru flask-security-3.4.2/flask_security/translations/da_DK/LC_MESSAGES/flask_security.po flask-security-4.0.0/flask_security/translations/da_DK/LC_MESSAGES/flask_security.po --- flask-security-3.4.2/flask_security/translations/da_DK/LC_MESSAGES/flask_security.po 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/translations/da_DK/LC_MESSAGES/flask_security.po 2021-01-26 02:39:51.000000000 +0000 @@ -8,7 +8,7 @@ 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" +"POT-Creation-Date: 2021-01-22 14:48-0800\n" "PO-Revision-Date: 2017-03-23 14:04+0100\n" "Last-Translator: Leonhard Printz \n" "Language: da_DK\n" @@ -17,110 +17,121 @@ "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" +"Generated-By: Babel 2.9.0\n" -#: flask_security/core.py:207 +#: flask_security/core.py:209 msgid "Login Required" msgstr "Login påkræveet" -#: flask_security/core.py:208 +#: flask_security/core.py:210 #: flask_security/templates/security/email/two_factor_instructions.html:1 +#: flask_security/templates/security/email/two_factor_instructions.txt:1 #: flask_security/templates/security/email/us_instructions.html:1 +#: flask_security/templates/security/email/us_instructions.txt:1 +#: tests/templates/_nav.html:2 msgid "Welcome" msgstr "Velkommen" -#: flask_security/core.py:209 +#: flask_security/core.py:211 msgid "Please confirm your email" msgstr "Bekræft venligst din email" -#: flask_security/core.py:210 +#: flask_security/core.py:212 msgid "Login instructions" msgstr "Logininstruktioner" -#: flask_security/core.py:211 +#: flask_security/core.py:213 #: flask_security/templates/security/email/reset_notice.html:1 +#: flask_security/templates/security/email/reset_notice.txt:1 msgid "Your password has been reset" msgstr "Din adgangskode er blevet nulstillet" -#: flask_security/core.py:212 +#: flask_security/core.py:214 +#: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed" msgstr "Din adgangskode er blevet ændret" -#: flask_security/core.py:213 +#: flask_security/core.py:215 msgid "Password reset instructions" msgstr "Instruktioner til nulstilling af adganskode" -#: flask_security/core.py:216 +#: flask_security/core.py:218 msgid "Two-factor Login" msgstr "" -#: flask_security/core.py:217 +#: flask_security/core.py:219 msgid "Two-factor Rescue" msgstr "" -#: flask_security/core.py:266 +#: flask_security/core.py:263 msgid "Verification Code" msgstr "" -#: flask_security/core.py:282 +#: flask_security/core.py:278 msgid "Input not appropriate for requested API" msgstr "" -#: flask_security/core.py:283 +#: flask_security/core.py:279 msgid "You do not have permission to view this resource." msgstr "Du har ikke adgang til denne resource." -#: flask_security/core.py:285 +#: flask_security/core.py:281 msgid "You are not authenticated. Please supply the correct credentials." msgstr "" -#: flask_security/core.py:289 -#, fuzzy +#: flask_security/core.py:285 msgid "You must re-authenticate to access this endpoint" -msgstr "Bekræft identitet for at få adgang til denne side." +msgstr "" -#: flask_security/core.py:293 +#: flask_security/core.py:289 #, 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 +#: flask_security/core.py:292 msgid "Thank you. Your email has been confirmed." msgstr "Mange Tak. Din email er blevet bekræftet." -#: flask_security/core.py:297 +#: flask_security/core.py:293 msgid "Your email has already been confirmed." msgstr "Din email er allerede blevet bekræftet." -#: flask_security/core.py:298 +#: flask_security/core.py:294 msgid "Invalid confirmation token." msgstr "Ugyldig bekræftigelsestoken." -#: flask_security/core.py:300 +#: flask_security/core.py:296 #, 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 +#: flask_security/core.py:300 +#, python-format +msgid "" +"Identity attribute '%(attr)s' with value '%(value)s' is already " +"associated with an account." +msgstr "" + +#: flask_security/core.py:306 msgid "Password does not match" msgstr "Adgangskode passer ikke" -#: flask_security/core.py:304 +#: flask_security/core.py:307 msgid "Passwords do not match" msgstr "Adgangskoderne passer ikke" -#: flask_security/core.py:305 +#: flask_security/core.py:308 msgid "Redirections outside the domain are forbidden" msgstr "Omdirigering udenfor domænet er forbudt" -#: flask_security/core.py:307 +#: flask_security/core.py:310 #, 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 +#: flask_security/core.py:314 #, python-format msgid "" "You did not reset your password within %(within)s. New instructions have " @@ -129,20 +140,20 @@ "Du har ikke nulstillet din adgangskode indenfor %(within)s. Nye " "instruktioner er sendt til %(email)s." -#: flask_security/core.py:317 +#: flask_security/core.py:320 msgid "Invalid reset password token." msgstr "Ugyldig nulstillingstoken." -#: flask_security/core.py:318 +#: flask_security/core.py:321 msgid "Email requires confirmation." msgstr "Email kræver bekræftigelse." -#: flask_security/core.py:320 +#: flask_security/core.py:323 #, 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 +#: flask_security/core.py:327 #, python-format msgid "" "You did not confirm your email within %(within)s. New instructions to " @@ -151,7 +162,7 @@ "Du har ikke bekræftet din email indenfor %(within)s. Nye instruktioner er" " blevet sendt til %(email)s." -#: flask_security/core.py:332 +#: flask_security/core.py:335 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " @@ -160,82 +171,81 @@ "Du har ikke logget in indenfor %(within)s. Nye logininstruktioner er " "blevet sendt til %(email)s." -#: flask_security/core.py:339 +#: flask_security/core.py:342 #, 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 +#: flask_security/core.py:345 msgid "Invalid login token." msgstr "Ugyldig logintoken." -#: flask_security/core.py:343 +#: flask_security/core.py:346 msgid "Account is disabled." msgstr "Kontoen er deaktiveret." -#: flask_security/core.py:344 +#: flask_security/core.py:347 msgid "Email not provided" msgstr "Email ikke angivet" -#: flask_security/core.py:345 +#: flask_security/core.py:348 msgid "Invalid email address" msgstr "Ugyldig email adresse" -#: flask_security/core.py:346 -#, fuzzy +#: flask_security/core.py:349 msgid "Invalid code" -msgstr "Ugyldig adgangskode" +msgstr "" -#: flask_security/core.py:347 +#: flask_security/core.py:350 msgid "Password not provided" msgstr "Adgangskode ikke angivet" -#: flask_security/core.py:348 +#: flask_security/core.py:351 msgid "No password is set for this user" msgstr "Denne bruger har ingen adganskode" -#: flask_security/core.py:350 +#: flask_security/core.py:353 #, fuzzy, python-format msgid "Password must be at least %(length)s characters" -msgstr "Adgangskoden skal indeholde mindst 6 tegn" +msgstr "Adgangskoden skal indeholde mindst %(length)s tegn" -#: flask_security/core.py:353 +#: flask_security/core.py:356 msgid "Password not complex enough" msgstr "" -#: flask_security/core.py:354 +#: flask_security/core.py:357 msgid "Password on breached list" msgstr "" -#: flask_security/core.py:356 +#: flask_security/core.py:359 msgid "Failed to contact breached passwords site" msgstr "" -#: flask_security/core.py:359 +#: flask_security/core.py:362 msgid "Phone number not valid e.g. missing country code" msgstr "" -#: flask_security/core.py:360 +#: flask_security/core.py:363 msgid "Specified user does not exist" msgstr "Denne bruger findes ikke" -#: flask_security/core.py:361 +#: flask_security/core.py:364 msgid "Invalid password" msgstr "Ugyldig adgangskode" -#: flask_security/core.py:362 +#: flask_security/core.py:365 msgid "Password or code submitted is not valid" msgstr "" -#: flask_security/core.py:363 +#: flask_security/core.py:366 msgid "You have successfully logged in." msgstr "Du er hermed blevet logget ind." -#: flask_security/core.py:364 +#: flask_security/core.py:367 msgid "Forgot password?" msgstr "Glemt adgangskode?" -#: flask_security/core.py:366 +#: flask_security/core.py:369 msgid "" "You successfully reset your password and you have been logged in " "automatically." @@ -243,212 +253,202 @@ "Du har hermed nulstillet din adgangskode og er blevet automatisk logget " "ind." -#: flask_security/core.py:373 +#: flask_security/core.py:376 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 +#: flask_security/core.py:379 msgid "You successfully changed your password." msgstr "Du har hermed ændret din adgangskode." -#: flask_security/core.py:377 +#: flask_security/core.py:380 msgid "Please log in to access this page." msgstr "Log in for at få adgang til denne side." -#: flask_security/core.py:378 +#: flask_security/core.py:381 msgid "Please reauthenticate to access this page." msgstr "Bekræft identitet for at få adgang til denne side." -#: flask_security/core.py:379 +#: flask_security/core.py:382 msgid "Reauthentication successful" msgstr "" -#: flask_security/core.py:381 +#: flask_security/core.py:384 msgid "You can only access this endpoint when not logged in." msgstr "" -#: flask_security/core.py:384 +#: flask_security/core.py:387 msgid "Failed to send code. Please try again later" msgstr "" -#: flask_security/core.py:385 +#: flask_security/core.py:388 msgid "Invalid Token" msgstr "" -#: flask_security/core.py:386 +#: flask_security/core.py:389 msgid "Your token has been confirmed" msgstr "" -#: flask_security/core.py:388 +#: flask_security/core.py:391 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 +#: flask_security/core.py:395 msgid "You currently do not have permissions to access this page" msgstr "" -#: flask_security/core.py:403 +#: flask_security/core.py:398 msgid "Marked method is not valid" msgstr "" -#: flask_security/core.py:405 +#: flask_security/core.py:400 msgid "You successfully disabled two factor authorization." msgstr "" -#: flask_security/core.py:408 +#: flask_security/core.py:403 msgid "Requested method is not valid" msgstr "" -#: flask_security/core.py:410 +#: flask_security/core.py:405 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "" -#: flask_security/core.py:413 +#: flask_security/core.py:408 msgid "Unified sign in setup successful" msgstr "" -#: flask_security/core.py:414 +#: flask_security/core.py:409 msgid "You must specify a valid identity to sign in" msgstr "" -#: flask_security/core.py:415 +#: flask_security/core.py:410 #, python-format msgid "Use this code to sign in: %(code)s." msgstr "" -#: flask_security/forms.py:50 +#: flask_security/forms.py:53 msgid "Email Address" msgstr "Email adresse" -#: flask_security/forms.py:51 +#: flask_security/forms.py:54 msgid "Password" msgstr "Adgangskode" -#: flask_security/forms.py:52 +#: flask_security/forms.py:55 msgid "Remember Me" msgstr "Husk" -#: flask_security/forms.py:53 flask_security/templates/security/_menu.html:5 +#: flask_security/forms.py:56 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/forms.py:57 #: 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/forms.py:58 flask_security/templates/security/_menu.html:11 #: flask_security/templates/security/register_user.html:6 msgid "Register" msgstr "Registrer" -#: flask_security/forms.py:56 +#: flask_security/forms.py:59 msgid "Resend Confirmation Instructions" msgstr "Gensend bekræftelsesinstruktioner" -#: flask_security/forms.py:57 +#: flask_security/forms.py:60 msgid "Recover Password" msgstr "Genopret adgangskode" -#: flask_security/forms.py:58 +#: flask_security/forms.py:61 msgid "Reset Password" msgstr "Nulstil adgangskode" -#: flask_security/forms.py:59 +#: flask_security/forms.py:62 msgid "Retype Password" msgstr "Gentast adgangskode" -#: flask_security/forms.py:60 +#: flask_security/forms.py:63 msgid "New Password" msgstr "Ny adgangskode" -#: flask_security/forms.py:61 +#: flask_security/forms.py:64 msgid "Change Password" msgstr "Ændre adgangskode" -#: flask_security/forms.py:62 +#: flask_security/forms.py:65 msgid "Send Login Link" msgstr "Send login link" -#: flask_security/forms.py:63 +#: flask_security/forms.py:66 msgid "Verify Password" msgstr "" -#: flask_security/forms.py:64 +#: flask_security/forms.py:67 msgid "Change Method" msgstr "" -#: flask_security/forms.py:65 +#: flask_security/forms.py:68 msgid "Phone Number" msgstr "" -#: flask_security/forms.py:66 +#: flask_security/forms.py:69 msgid "Authentication Code" msgstr "" -#: flask_security/forms.py:67 +#: flask_security/forms.py:70 msgid "Submit" msgstr "" -#: flask_security/forms.py:68 +#: flask_security/forms.py:71 msgid "Submit Code" msgstr "" -#: flask_security/forms.py:69 +#: flask_security/forms.py:72 msgid "Error(s)" msgstr "" -#: flask_security/forms.py:70 +#: flask_security/forms.py:73 msgid "Identity" msgstr "" -#: flask_security/forms.py:71 +#: flask_security/forms.py:74 msgid "Send Code" msgstr "" -#: flask_security/forms.py:72 -#, fuzzy +#: flask_security/forms.py:75 msgid "Passcode" -msgstr "Adgangskode" +msgstr "" -#: flask_security/unified_signin.py:145 -#, fuzzy +#: flask_security/unified_signin.py:131 msgid "Code or Password" -msgstr "Genopret adgangskode" +msgstr "" -#: flask_security/unified_signin.py:150 flask_security/unified_signin.py:270 +#: flask_security/unified_signin.py:136 flask_security/unified_signin.py:262 msgid "Available Methods" msgstr "" -#: flask_security/unified_signin.py:151 +#: flask_security/unified_signin.py:137 msgid "Via email" msgstr "" -#: flask_security/unified_signin.py:151 +#: flask_security/unified_signin.py:137 msgid "Via SMS" msgstr "" -#: flask_security/unified_signin.py:272 +#: flask_security/unified_signin.py:264 msgid "Set up using email" msgstr "" -#: flask_security/unified_signin.py:275 +#: flask_security/unified_signin.py:267 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" -#: flask_security/unified_signin.py:277 +#: flask_security/unified_signin.py:269 msgid "Set up using SMS" msgstr "" @@ -484,32 +484,31 @@ msgid "Resend confirmation instructions" msgstr "Gensend bekræftelsesinstruktioner" -#: flask_security/templates/security/two_factor_setup.html:6 +#: flask_security/templates/security/two_factor_setup.html:31 msgid "Two-factor authentication adds an extra layer of security to your account" msgstr "" -#: flask_security/templates/security/two_factor_setup.html:7 +#: flask_security/templates/security/two_factor_setup.html:32 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 +#: flask_security/templates/security/two_factor_setup.html:43 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 +#: flask_security/templates/security/two_factor_setup.html:49 msgid "" -"Open your authenticator app on your device and scan the following qrcode " -"to start receiving codes:" +"Open an authenticator app on your device and scan the following QRcode " +"(or enter the code below manually) to start receiving codes:" msgstr "" -#: flask_security/templates/security/two_factor_setup.html:22 +#: flask_security/templates/security/two_factor_setup.html:52 msgid "Two factor authentication code" msgstr "" -#: flask_security/templates/security/two_factor_setup.html:25 +#: flask_security/templates/security/two_factor_setup.html:60 msgid "To Which Phone Number Should We Send Code To?" msgstr "" @@ -529,27 +528,27 @@ 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 +#: flask_security/templates/security/us_setup.html:36 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_setup.html:58 #: 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" +msgstr "" + +#: flask_security/templates/security/us_setup.html:66 +msgid "" +"Open an authenticator app on your device and scan the following QRcode " +"(or enter the code below manually) to start receiving passcodes:" +msgstr "" + +#: flask_security/templates/security/us_setup.html:69 +msgid "Passwordless QRCode" +msgstr "" -#: flask_security/templates/security/us_setup.html:29 +#: flask_security/templates/security/us_setup.html:77 msgid "No methods have been enabled - nothing to setup" msgstr "" @@ -559,9 +558,12 @@ 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." +msgstr "" + +#: flask_security/templates/security/verify.html:6 +msgid "Please Enter Your Password" +msgstr "" #: flask_security/templates/security/email/change_notice.html:1 msgid "Your password has been changed." @@ -575,7 +577,12 @@ msgid "click here to reset it" msgstr "klik her for at ændre den" +#: flask_security/templates/security/email/change_notice.txt:3 +msgid "If you did not change your password, click the link below to reset it." +msgstr "" + #: flask_security/templates/security/email/confirmation_instructions.html:1 +#: flask_security/templates/security/email/confirmation_instructions.txt:1 msgid "Please confirm your email through the link below:" msgstr "Bekræft venligst din email gennem nedenstående link:" @@ -585,12 +592,15 @@ msgstr "Bekræft ny konto" #: flask_security/templates/security/email/login_instructions.html:1 +#: flask_security/templates/security/email/login_instructions.txt:1 #: flask_security/templates/security/email/welcome.html:1 +#: flask_security/templates/security/email/welcome.txt:1 #, python-format msgid "Welcome %(email)s!" msgstr "Velkommen %(email)s!" #: flask_security/templates/security/email/login_instructions.html:3 +#: flask_security/templates/security/email/login_instructions.txt:3 msgid "You can log into your account through the link below:" msgstr "Du kan logge ind gennem nedenstående link:" @@ -602,25 +612,46 @@ msgid "Click here to reset your password" msgstr "Klik her for at nulstille din adgangskode" +#: flask_security/templates/security/email/reset_instructions.txt:1 +msgid "Click the link below to reset your password:" +msgstr "" + #: flask_security/templates/security/email/two_factor_instructions.html:3 +#: flask_security/templates/security/email/two_factor_instructions.txt:3 msgid "You can log into your account using the following code:" msgstr "" #: flask_security/templates/security/email/two_factor_rescue.html:1 +#: flask_security/templates/security/email/two_factor_rescue.txt:1 msgid "can not access mail account" msgstr "" #: flask_security/templates/security/email/us_instructions.html:3 -#, fuzzy +#: flask_security/templates/security/email/us_instructions.txt:3 msgid "You can sign into your account using the following code:" -msgstr "Du kan logge ind gennem nedenstående link:" +msgstr "" #: 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:" +msgstr "" + +#: flask_security/templates/security/email/us_instructions.txt:7 +msgid "Or use the link below:" +msgstr "" #: flask_security/templates/security/email/welcome.html:4 +#: flask_security/templates/security/email/welcome.txt:4 msgid "You can confirm your email through the link below:" msgstr "Bekræft venligst din email gennem nedenstående link:" +#~ msgid "You successfully confirmed password" +#~ msgstr "" + +#~ msgid "Password confirmation is needed in order to access page" +#~ msgstr "" + +#~ msgid "" +#~ "Open your authenticator app on your " +#~ "device and scan the following qrcode " +#~ "to start receiving codes:" +#~ msgstr "" Binary files /tmp/tmpl_LU2e/ak23Qk5OdH/flask-security-3.4.2/flask_security/translations/de_DE/LC_MESSAGES/flask_security.mo and /tmp/tmpl_LU2e/U10RHAXkq7/flask-security-4.0.0/flask_security/translations/de_DE/LC_MESSAGES/flask_security.mo differ diff -Nru flask-security-3.4.2/flask_security/translations/de_DE/LC_MESSAGES/flask_security.po flask-security-4.0.0/flask_security/translations/de_DE/LC_MESSAGES/flask_security.po --- flask-security-3.4.2/flask_security/translations/de_DE/LC_MESSAGES/flask_security.po 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/translations/de_DE/LC_MESSAGES/flask_security.po 2021-01-26 02:39:51.000000000 +0000 @@ -10,7 +10,7 @@ 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" +"POT-Creation-Date: 2021-01-22 14:48-0800\n" "PO-Revision-Date: 2017-09-25 09:14+0200\n" "Last-Translator: Erich Seifert \n" "Language: de_DE\n" @@ -19,110 +19,122 @@ "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" +"Generated-By: Babel 2.9.0\n" -#: flask_security/core.py:207 +#: flask_security/core.py:209 msgid "Login Required" msgstr "Anmeldung erforderlich" -#: flask_security/core.py:208 +#: flask_security/core.py:210 #: flask_security/templates/security/email/two_factor_instructions.html:1 +#: flask_security/templates/security/email/two_factor_instructions.txt:1 #: flask_security/templates/security/email/us_instructions.html:1 +#: flask_security/templates/security/email/us_instructions.txt:1 +#: tests/templates/_nav.html:2 msgid "Welcome" msgstr "Willkommen" -#: flask_security/core.py:209 +#: flask_security/core.py:211 msgid "Please confirm your email" msgstr "Bitte E-Mail-Adresse bestätigen" -#: flask_security/core.py:210 +#: flask_security/core.py:212 msgid "Login instructions" msgstr "Anmeldeanleitung" -#: flask_security/core.py:211 +#: flask_security/core.py:213 #: flask_security/templates/security/email/reset_notice.html:1 +#: flask_security/templates/security/email/reset_notice.txt:1 msgid "Your password has been reset" msgstr "Das Passwort wurde zurückgesetzt" -#: flask_security/core.py:212 +#: flask_security/core.py:214 +#: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed" msgstr "Das Passwort wurde geändert" -#: flask_security/core.py:213 +#: flask_security/core.py:215 msgid "Password reset instructions" msgstr "Anleitung zur Passwortwiederherstellung" -#: flask_security/core.py:216 +#: flask_security/core.py:218 msgid "Two-factor Login" msgstr "" -#: flask_security/core.py:217 +#: flask_security/core.py:219 msgid "Two-factor Rescue" msgstr "" -#: flask_security/core.py:266 +#: flask_security/core.py:263 msgid "Verification Code" msgstr "" -#: flask_security/core.py:282 +#: flask_security/core.py:278 msgid "Input not appropriate for requested API" msgstr "" -#: flask_security/core.py:283 +#: flask_security/core.py:279 msgid "You do not have permission to view this resource." msgstr "Keine Berechtigung um diese Ressource zu sehen." -#: flask_security/core.py:285 +#: flask_security/core.py:281 msgid "You are not authenticated. Please supply the correct credentials." msgstr "" -#: flask_security/core.py:289 +#: flask_security/core.py:285 #, fuzzy msgid "You must re-authenticate to access this endpoint" msgstr "Bitte neu authentifizieren, um auf diese Seite zuzugreifen." -#: flask_security/core.py:293 +#: flask_security/core.py:289 #, 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 +#: flask_security/core.py:292 msgid "Thank you. Your email has been confirmed." msgstr "Vielen Dank. Die E-Mail-Adresse wurde bestätigt." -#: flask_security/core.py:297 +#: flask_security/core.py:293 msgid "Your email has already been confirmed." msgstr "Die E-Mail-Adresse wurde bereits bestätigt." -#: flask_security/core.py:298 +#: flask_security/core.py:294 msgid "Invalid confirmation token." msgstr "Ungültiger Bestätigungscode." -#: flask_security/core.py:300 +#: flask_security/core.py:296 #, 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 +#: flask_security/core.py:300 +#, python-format +msgid "" +"Identity attribute '%(attr)s' with value '%(value)s' is already " +"associated with an account." +msgstr "" + +#: flask_security/core.py:306 msgid "Password does not match" msgstr "Das Passwort stimmt nicht überein" -#: flask_security/core.py:304 +#: flask_security/core.py:307 msgid "Passwords do not match" msgstr "Die Passwörter stimmen nicht überein" -#: flask_security/core.py:305 +#: flask_security/core.py:308 msgid "Redirections outside the domain are forbidden" msgstr "Weiterleitungen außerhalb der Domain sind verboten" -#: flask_security/core.py:307 +#: flask_security/core.py:310 #, 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 +#: flask_security/core.py:314 #, python-format msgid "" "You did not reset your password within %(within)s. New instructions have " @@ -131,20 +143,20 @@ "Das Passwort wurde nicht innerhalb von %(within)s zurückgesetzt. Eine " "neue Anleitung wurde an %(email)s gesendet." -#: flask_security/core.py:317 +#: flask_security/core.py:320 msgid "Invalid reset password token." msgstr "Ungültiger Passwortwiederherstellungscode." -#: flask_security/core.py:318 +#: flask_security/core.py:321 msgid "Email requires confirmation." msgstr "Die E-Mail-Adresse muss bestätigt werden." -#: flask_security/core.py:320 +#: flask_security/core.py:323 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "Bestätigungsanleitung wurde an %(email)s gesendet." -#: flask_security/core.py:324 +#: flask_security/core.py:327 #, python-format msgid "" "You did not confirm your email within %(within)s. New instructions to " @@ -153,7 +165,7 @@ "Die E-Mail-Adresse wurden nicht innerhalb von %(within)s bestätigt. Neue " "Instruktionen wurden an %(email)s gesendet." -#: flask_security/core.py:332 +#: flask_security/core.py:335 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " @@ -162,82 +174,81 @@ "Die Anmeldung erfolgte nicht in %(within)s. Eine neue Anleitung wurde an " "%(email)s gesendet." -#: flask_security/core.py:339 +#: flask_security/core.py:342 #, 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 +#: flask_security/core.py:345 msgid "Invalid login token." msgstr "Ungültiger Anmeldecode." -#: flask_security/core.py:343 +#: flask_security/core.py:346 msgid "Account is disabled." msgstr "Konto ist deaktiviert." -#: flask_security/core.py:344 +#: flask_security/core.py:347 msgid "Email not provided" msgstr "Keine E-Mail-Adresse angegeben" -#: flask_security/core.py:345 +#: flask_security/core.py:348 msgid "Invalid email address" msgstr "Ungültige E-Mail-Adresse" -#: flask_security/core.py:346 -#, fuzzy +#: flask_security/core.py:349 msgid "Invalid code" -msgstr "Ungültiges Passwort" +msgstr "" -#: flask_security/core.py:347 +#: flask_security/core.py:350 msgid "Password not provided" msgstr "Kein Passwort angegeben" -#: flask_security/core.py:348 +#: flask_security/core.py:351 msgid "No password is set for this user" msgstr "Für diesen Benutzer ist kein Passwort gesetzt" -#: flask_security/core.py:350 +#: flask_security/core.py:353 #, fuzzy, python-format msgid "Password must be at least %(length)s characters" -msgstr "Das Passwort muss mindestens 6 Zeichen lang sein" +msgstr "Das Passwort muss mindestens %(length)s Zeichen lang sein" -#: flask_security/core.py:353 +#: flask_security/core.py:356 msgid "Password not complex enough" msgstr "" -#: flask_security/core.py:354 +#: flask_security/core.py:357 msgid "Password on breached list" msgstr "" -#: flask_security/core.py:356 +#: flask_security/core.py:359 msgid "Failed to contact breached passwords site" msgstr "" -#: flask_security/core.py:359 +#: flask_security/core.py:362 msgid "Phone number not valid e.g. missing country code" msgstr "" -#: flask_security/core.py:360 +#: flask_security/core.py:363 msgid "Specified user does not exist" msgstr "Angegebener Benutzer existiert nicht" -#: flask_security/core.py:361 +#: flask_security/core.py:364 msgid "Invalid password" msgstr "Ungültiges Passwort" -#: flask_security/core.py:362 +#: flask_security/core.py:365 msgid "Password or code submitted is not valid" msgstr "" -#: flask_security/core.py:363 +#: flask_security/core.py:366 msgid "You have successfully logged in." msgstr "Die Anmeldung war erfolgreich." -#: flask_security/core.py:364 +#: flask_security/core.py:367 msgid "Forgot password?" msgstr "Passwort vergessen?" -#: flask_security/core.py:366 +#: flask_security/core.py:369 msgid "" "You successfully reset your password and you have been logged in " "automatically." @@ -245,212 +256,202 @@ "Das Passwort wurde erfolgreich wiederhergestellt und die Anmeldung " "erfolgte automatisch." -#: flask_security/core.py:373 +#: flask_security/core.py:376 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 +#: flask_security/core.py:379 msgid "You successfully changed your password." msgstr "Das Passwort wurde erfolgreich geändert." -#: flask_security/core.py:377 +#: flask_security/core.py:380 msgid "Please log in to access this page." msgstr "Bitte anmelden, um diese Seite zu sehen." -#: flask_security/core.py:378 +#: flask_security/core.py:381 msgid "Please reauthenticate to access this page." msgstr "Bitte neu authentifizieren, um auf diese Seite zuzugreifen." -#: flask_security/core.py:379 +#: flask_security/core.py:382 msgid "Reauthentication successful" msgstr "" -#: flask_security/core.py:381 +#: flask_security/core.py:384 msgid "You can only access this endpoint when not logged in." msgstr "" -#: flask_security/core.py:384 +#: flask_security/core.py:387 msgid "Failed to send code. Please try again later" msgstr "" -#: flask_security/core.py:385 +#: flask_security/core.py:388 msgid "Invalid Token" msgstr "" -#: flask_security/core.py:386 +#: flask_security/core.py:389 msgid "Your token has been confirmed" msgstr "" -#: flask_security/core.py:388 +#: flask_security/core.py:391 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 +#: flask_security/core.py:395 msgid "You currently do not have permissions to access this page" msgstr "" -#: flask_security/core.py:403 +#: flask_security/core.py:398 msgid "Marked method is not valid" msgstr "" -#: flask_security/core.py:405 +#: flask_security/core.py:400 msgid "You successfully disabled two factor authorization." msgstr "" -#: flask_security/core.py:408 +#: flask_security/core.py:403 msgid "Requested method is not valid" msgstr "" -#: flask_security/core.py:410 +#: flask_security/core.py:405 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "" -#: flask_security/core.py:413 +#: flask_security/core.py:408 msgid "Unified sign in setup successful" msgstr "" -#: flask_security/core.py:414 +#: flask_security/core.py:409 msgid "You must specify a valid identity to sign in" msgstr "" -#: flask_security/core.py:415 +#: flask_security/core.py:410 #, python-format msgid "Use this code to sign in: %(code)s." msgstr "" -#: flask_security/forms.py:50 +#: flask_security/forms.py:53 msgid "Email Address" msgstr "E-Mail-Adresse" -#: flask_security/forms.py:51 +#: flask_security/forms.py:54 msgid "Password" msgstr "Passwort" -#: flask_security/forms.py:52 +#: flask_security/forms.py:55 msgid "Remember Me" msgstr "Erinnern" -#: flask_security/forms.py:53 flask_security/templates/security/_menu.html:5 +#: flask_security/forms.py:56 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/forms.py:57 #: 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/forms.py:58 flask_security/templates/security/_menu.html:11 #: flask_security/templates/security/register_user.html:6 msgid "Register" msgstr "Registrieren" -#: flask_security/forms.py:56 +#: flask_security/forms.py:59 msgid "Resend Confirmation Instructions" msgstr "Bestätigungsanleitung neu senden" -#: flask_security/forms.py:57 +#: flask_security/forms.py:60 msgid "Recover Password" msgstr "Passwort wiederherstellen" -#: flask_security/forms.py:58 +#: flask_security/forms.py:61 msgid "Reset Password" msgstr "Passwort zurücksetzen" -#: flask_security/forms.py:59 +#: flask_security/forms.py:62 msgid "Retype Password" msgstr "Passwort neu eingeben" -#: flask_security/forms.py:60 +#: flask_security/forms.py:63 msgid "New Password" msgstr "Neues Passwort" -#: flask_security/forms.py:61 +#: flask_security/forms.py:64 msgid "Change Password" msgstr "Passwort ändern" -#: flask_security/forms.py:62 +#: flask_security/forms.py:65 msgid "Send Login Link" msgstr "Anmelde-Link versenden" -#: flask_security/forms.py:63 +#: flask_security/forms.py:66 msgid "Verify Password" msgstr "" -#: flask_security/forms.py:64 +#: flask_security/forms.py:67 msgid "Change Method" msgstr "" -#: flask_security/forms.py:65 +#: flask_security/forms.py:68 msgid "Phone Number" msgstr "" -#: flask_security/forms.py:66 +#: flask_security/forms.py:69 msgid "Authentication Code" msgstr "" -#: flask_security/forms.py:67 +#: flask_security/forms.py:70 msgid "Submit" msgstr "" -#: flask_security/forms.py:68 +#: flask_security/forms.py:71 msgid "Submit Code" msgstr "" -#: flask_security/forms.py:69 +#: flask_security/forms.py:72 msgid "Error(s)" msgstr "" -#: flask_security/forms.py:70 +#: flask_security/forms.py:73 msgid "Identity" msgstr "" -#: flask_security/forms.py:71 +#: flask_security/forms.py:74 msgid "Send Code" msgstr "" -#: flask_security/forms.py:72 -#, fuzzy +#: flask_security/forms.py:75 msgid "Passcode" -msgstr "Passwort" +msgstr "" -#: flask_security/unified_signin.py:145 -#, fuzzy +#: flask_security/unified_signin.py:131 msgid "Code or Password" -msgstr "Passwort wiederherstellen" +msgstr "" -#: flask_security/unified_signin.py:150 flask_security/unified_signin.py:270 +#: flask_security/unified_signin.py:136 flask_security/unified_signin.py:262 msgid "Available Methods" msgstr "" -#: flask_security/unified_signin.py:151 +#: flask_security/unified_signin.py:137 msgid "Via email" msgstr "" -#: flask_security/unified_signin.py:151 +#: flask_security/unified_signin.py:137 msgid "Via SMS" msgstr "" -#: flask_security/unified_signin.py:272 +#: flask_security/unified_signin.py:264 msgid "Set up using email" msgstr "" -#: flask_security/unified_signin.py:275 +#: flask_security/unified_signin.py:267 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" -#: flask_security/unified_signin.py:277 +#: flask_security/unified_signin.py:269 msgid "Set up using SMS" msgstr "" @@ -486,32 +487,31 @@ msgid "Resend confirmation instructions" msgstr "Bestätigungsanleitung erneut versenden" -#: flask_security/templates/security/two_factor_setup.html:6 +#: flask_security/templates/security/two_factor_setup.html:31 msgid "Two-factor authentication adds an extra layer of security to your account" msgstr "" -#: flask_security/templates/security/two_factor_setup.html:7 +#: flask_security/templates/security/two_factor_setup.html:32 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 +#: flask_security/templates/security/two_factor_setup.html:43 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 +#: flask_security/templates/security/two_factor_setup.html:49 msgid "" -"Open your authenticator app on your device and scan the following qrcode " -"to start receiving codes:" +"Open an authenticator app on your device and scan the following QRcode " +"(or enter the code below manually) to start receiving codes:" msgstr "" -#: flask_security/templates/security/two_factor_setup.html:22 +#: flask_security/templates/security/two_factor_setup.html:52 msgid "Two factor authentication code" msgstr "" -#: flask_security/templates/security/two_factor_setup.html:25 +#: flask_security/templates/security/two_factor_setup.html:60 msgid "To Which Phone Number Should We Send Code To?" msgstr "" @@ -531,27 +531,27 @@ 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 +#: flask_security/templates/security/us_setup.html:36 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_setup.html:58 #: 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" +msgstr "" + +#: flask_security/templates/security/us_setup.html:66 +msgid "" +"Open an authenticator app on your device and scan the following QRcode " +"(or enter the code below manually) to start receiving passcodes:" +msgstr "" + +#: flask_security/templates/security/us_setup.html:69 +msgid "Passwordless QRCode" +msgstr "" -#: flask_security/templates/security/us_setup.html:29 +#: flask_security/templates/security/us_setup.html:77 msgid "No methods have been enabled - nothing to setup" msgstr "" @@ -565,6 +565,10 @@ msgid "Please re-authenticate" msgstr "Bitte neu authentifizieren, um auf diese Seite zuzugreifen." +#: flask_security/templates/security/verify.html:6 +msgid "Please Enter Your Password" +msgstr "" + #: flask_security/templates/security/email/change_notice.html:1 msgid "Your password has been changed." msgstr "Das Passwort wurde geändert." @@ -577,7 +581,12 @@ msgid "click here to reset it" msgstr "hier klicken, um es zurückzusetzen" +#: flask_security/templates/security/email/change_notice.txt:3 +msgid "If you did not change your password, click the link below to reset it." +msgstr "" + #: flask_security/templates/security/email/confirmation_instructions.html:1 +#: flask_security/templates/security/email/confirmation_instructions.txt:1 msgid "Please confirm your email through the link below:" msgstr "Bitte die E-Mail-Adresse durch den Link unten bestätigen:" @@ -587,12 +596,15 @@ msgstr "Das Konto bestätigen" #: flask_security/templates/security/email/login_instructions.html:1 +#: flask_security/templates/security/email/login_instructions.txt:1 #: flask_security/templates/security/email/welcome.html:1 +#: flask_security/templates/security/email/welcome.txt:1 #, python-format msgid "Welcome %(email)s!" msgstr "Willkommen %(email)s!" #: flask_security/templates/security/email/login_instructions.html:3 +#: flask_security/templates/security/email/login_instructions.txt:3 msgid "You can log into your account through the link below:" msgstr "Die Anmeldung kann über den Link unten erfolgen:" @@ -604,25 +616,46 @@ msgid "Click here to reset your password" msgstr "Hier klicken, um das Passwort zurückzusetzen" +#: flask_security/templates/security/email/reset_instructions.txt:1 +msgid "Click the link below to reset your password:" +msgstr "" + #: flask_security/templates/security/email/two_factor_instructions.html:3 +#: flask_security/templates/security/email/two_factor_instructions.txt:3 msgid "You can log into your account using the following code:" msgstr "" #: flask_security/templates/security/email/two_factor_rescue.html:1 +#: flask_security/templates/security/email/two_factor_rescue.txt:1 msgid "can not access mail account" msgstr "" #: flask_security/templates/security/email/us_instructions.html:3 -#, fuzzy +#: flask_security/templates/security/email/us_instructions.txt:3 msgid "You can sign into your account using the following code:" -msgstr "Die Anmeldung kann über den Link unten erfolgen:" +msgstr "" #: 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" +msgstr "" + +#: flask_security/templates/security/email/us_instructions.txt:7 +msgid "Or use the link below:" +msgstr "" #: flask_security/templates/security/email/welcome.html:4 +#: flask_security/templates/security/email/welcome.txt:4 msgid "You can confirm your email through the link below:" msgstr "Die E-Mail-Adresse kann über den Link unten bestätigt werden" +#~ msgid "You successfully confirmed password" +#~ msgstr "" + +#~ msgid "Password confirmation is needed in order to access page" +#~ msgstr "" + +#~ msgid "" +#~ "Open your authenticator app on your " +#~ "device and scan the following qrcode " +#~ "to start receiving codes:" +#~ msgstr "" Binary files /tmp/tmpl_LU2e/ak23Qk5OdH/flask-security-3.4.2/flask_security/translations/es_ES/LC_MESSAGES/flask_security.mo and /tmp/tmpl_LU2e/U10RHAXkq7/flask-security-4.0.0/flask_security/translations/es_ES/LC_MESSAGES/flask_security.mo differ diff -Nru flask-security-3.4.2/flask_security/translations/es_ES/LC_MESSAGES/flask_security.po flask-security-4.0.0/flask_security/translations/es_ES/LC_MESSAGES/flask_security.po --- flask-security-3.4.2/flask_security/translations/es_ES/LC_MESSAGES/flask_security.po 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/translations/es_ES/LC_MESSAGES/flask_security.po 2021-01-26 02:39:51.000000000 +0000 @@ -8,121 +8,134 @@ 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" +"POT-Creation-Date: 2021-01-22 14:48-0800\n" "PO-Revision-Date: 2017-08-25 17:21+0200\n" -"Last-Translator: Mauko Quiroga \n" +"Last-Translator: Martin Mozos \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" +"Generated-By: Babel 2.9.0\n" -#: flask_security/core.py:207 +#: flask_security/core.py:209 msgid "Login Required" msgstr "Inicio de sesión necesario" -#: flask_security/core.py:208 +#: flask_security/core.py:210 #: flask_security/templates/security/email/two_factor_instructions.html:1 +#: flask_security/templates/security/email/two_factor_instructions.txt:1 #: flask_security/templates/security/email/us_instructions.html:1 +#: flask_security/templates/security/email/us_instructions.txt:1 +#: tests/templates/_nav.html:2 msgid "Welcome" msgstr "Bienvenido" -#: flask_security/core.py:209 +#: flask_security/core.py:211 msgid "Please confirm your email" msgstr "Por favor, confirma tu correo electrónico" -#: flask_security/core.py:210 +#: flask_security/core.py:212 msgid "Login instructions" msgstr "Instrucciones para iniciar sesión" -#: flask_security/core.py:211 +#: flask_security/core.py:213 #: flask_security/templates/security/email/reset_notice.html:1 +#: flask_security/templates/security/email/reset_notice.txt:1 msgid "Your password has been reset" msgstr "Tu contraseña ha sido restablecida" -#: flask_security/core.py:212 +#: flask_security/core.py:214 +#: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed" msgstr "Tu contraseña ha sido cambiada" -#: flask_security/core.py:213 +#: flask_security/core.py:215 msgid "Password reset instructions" msgstr "Instrucciones de recuperación de contraseña" -#: flask_security/core.py:216 +#: flask_security/core.py:218 msgid "Two-factor Login" -msgstr "" +msgstr "Inicio de sesión de dos factores" -#: flask_security/core.py:217 +#: flask_security/core.py:219 msgid "Two-factor Rescue" -msgstr "" +msgstr "Recuperación de sesión de dos factores" -#: flask_security/core.py:266 +#: flask_security/core.py:263 msgid "Verification Code" -msgstr "" +msgstr "Código de verificación" -#: flask_security/core.py:282 +#: flask_security/core.py:278 msgid "Input not appropriate for requested API" -msgstr "" +msgstr "Entrada no apropiada para la API solicitada" -#: flask_security/core.py:283 +#: flask_security/core.py:279 msgid "You do not have permission to view this resource." msgstr "No tienes permiso para consultar este recurso." -#: flask_security/core.py:285 +#: flask_security/core.py:281 msgid "You are not authenticated. Please supply the correct credentials." -msgstr "" +msgstr "No estás autenticado. Por favor, proporciona las credenciales correctas." -#: flask_security/core.py:289 -#, fuzzy +#: flask_security/core.py:285 msgid "You must re-authenticate to access this endpoint" -msgstr "Deber iniciar sesión nuevamente para poder acceder a esta página." +msgstr "Debes volver a autenticarte para acceder a este recurso" -#: flask_security/core.py:293 +#: flask_security/core.py:289 #, 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 +#: flask_security/core.py:292 msgid "Thank you. Your email has been confirmed." msgstr "Gracias. Tu correo electrónico ha sido confirmado." -#: flask_security/core.py:297 +#: flask_security/core.py:293 msgid "Your email has already been confirmed." msgstr "Tu correo electrónico ya ha sido confirmado." -#: flask_security/core.py:298 +#: flask_security/core.py:294 msgid "Invalid confirmation token." msgstr "Autentificador de confirmación inválido." -#: flask_security/core.py:300 +#: flask_security/core.py:296 #, 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 +#: flask_security/core.py:300 +#, python-format +msgid "" +"Identity attribute '%(attr)s' with value '%(value)s' is already " +"associated with an account." +msgstr "" +"El atributo de identidad '%(attr)s' con el valor '%(value)s' ya está " +"asociado con una cuenta." + +#: flask_security/core.py:306 msgid "Password does not match" msgstr "La contraseña no coincide" -#: flask_security/core.py:304 +#: flask_security/core.py:307 msgid "Passwords do not match" msgstr "Las contraseñas no coinciden" -#: flask_security/core.py:305 +#: flask_security/core.py:308 msgid "Redirections outside the domain are forbidden" msgstr "Las redirecciones a sitios web externos están prohibidas" -#: flask_security/core.py:307 +#: flask_security/core.py:310 #, 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 +#: flask_security/core.py:314 #, python-format msgid "" "You did not reset your password within %(within)s. New instructions have " @@ -131,20 +144,20 @@ "No restableciste tu contraseña antes de %(within)s. Nuevas instrucciones " "han sido enviadas a %(email)s." -#: flask_security/core.py:317 +#: flask_security/core.py:320 msgid "Invalid reset password token." msgstr "Autentificador de restablecimiento de contraseña inválido." -#: flask_security/core.py:318 +#: flask_security/core.py:321 msgid "Email requires confirmation." msgstr "El correo electrónico requiere confirmación." -#: flask_security/core.py:320 +#: flask_security/core.py:323 #, 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 +#: flask_security/core.py:327 #, python-format msgid "" "You did not confirm your email within %(within)s. New instructions to " @@ -154,7 +167,7 @@ "instrucciones para confirmar tu correo electrónico han sido enviadas a " "%(email)s." -#: flask_security/core.py:332 +#: flask_security/core.py:335 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " @@ -163,82 +176,81 @@ "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 +#: flask_security/core.py:342 #, 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 +#: flask_security/core.py:345 msgid "Invalid login token." msgstr "Autenticador de inicio de sesión inválido." -#: flask_security/core.py:343 +#: flask_security/core.py:346 msgid "Account is disabled." msgstr "Cuenta deshabilitada." -#: flask_security/core.py:344 +#: flask_security/core.py:347 msgid "Email not provided" msgstr "Correo electrónico no indicado" -#: flask_security/core.py:345 +#: flask_security/core.py:348 msgid "Invalid email address" msgstr "Dirección de correo electrónico inválida" -#: flask_security/core.py:346 -#, fuzzy +#: flask_security/core.py:349 msgid "Invalid code" -msgstr "Contraseña inválida" +msgstr "Código no válido" -#: flask_security/core.py:347 +#: flask_security/core.py:350 msgid "Password not provided" msgstr "Contraseña no indicada" -#: flask_security/core.py:348 +#: flask_security/core.py:351 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 +#: flask_security/core.py:353 #, fuzzy, python-format msgid "Password must be at least %(length)s characters" -msgstr "La contraseña debe contar al menos con 6 caracteres" +msgstr "La contraseña debe contar al menos con %(length)s caracteres" -#: flask_security/core.py:353 +#: flask_security/core.py:356 msgid "Password not complex enough" -msgstr "" +msgstr "La contraseña no es lo suficientemente compleja" -#: flask_security/core.py:354 +#: flask_security/core.py:357 msgid "Password on breached list" -msgstr "" +msgstr "Contraseña en lista infringida" -#: flask_security/core.py:356 +#: flask_security/core.py:359 msgid "Failed to contact breached passwords site" -msgstr "" +msgstr "No se pudo contactar con el sitio de contraseñas infringidas" -#: flask_security/core.py:359 +#: flask_security/core.py:362 msgid "Phone number not valid e.g. missing country code" -msgstr "" +msgstr "El número de teléfono no es válido, p. ej. falta el código de país" -#: flask_security/core.py:360 +#: flask_security/core.py:363 msgid "Specified user does not exist" msgstr "Usuario·a especificado·a no existe" -#: flask_security/core.py:361 +#: flask_security/core.py:364 msgid "Invalid password" msgstr "Contraseña inválida" -#: flask_security/core.py:362 +#: flask_security/core.py:365 msgid "Password or code submitted is not valid" -msgstr "" +msgstr "La contraseña o el código facilitado no es válido" -#: flask_security/core.py:363 +#: flask_security/core.py:366 msgid "You have successfully logged in." msgstr "Has iniciado sesión con éxito." -#: flask_security/core.py:364 +#: flask_security/core.py:367 msgid "Forgot password?" msgstr "¿Has olvidado tu contraseña?" -#: flask_security/core.py:366 +#: flask_security/core.py:369 msgid "" "You successfully reset your password and you have been logged in " "automatically." @@ -246,214 +258,206 @@ "Has restablecido tu contraseña con éxito y has iniciado sesión " "automáticamente." -#: flask_security/core.py:373 +#: flask_security/core.py:376 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 +#: flask_security/core.py:379 msgid "You successfully changed your password." msgstr "Has cambiado tu contraseña con éxito." -#: flask_security/core.py:377 +#: flask_security/core.py:380 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 +#: flask_security/core.py:381 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 +#: flask_security/core.py:382 msgid "Reauthentication successful" -msgstr "" +msgstr "Reautenticación exitosa" -#: flask_security/core.py:381 +#: flask_security/core.py:384 msgid "You can only access this endpoint when not logged in." -msgstr "" +msgstr "Solo puedes acceder a este recurso si no estás conectado." -#: flask_security/core.py:384 +#: flask_security/core.py:387 msgid "Failed to send code. Please try again later" -msgstr "" +msgstr "No se pudo enviar el código. Por favor, inténtelo de nuevo más tarde" -#: flask_security/core.py:385 +#: flask_security/core.py:388 msgid "Invalid Token" -msgstr "" +msgstr "Token no válido" -#: flask_security/core.py:386 +#: flask_security/core.py:389 msgid "Your token has been confirmed" -msgstr "" +msgstr "Tu token ha sido confirmado" -#: flask_security/core.py:388 +#: flask_security/core.py:391 msgid "You successfully changed your two-factor method." -msgstr "" - -#: flask_security/core.py:392 -msgid "You successfully confirmed password" -msgstr "" +msgstr "Cambiaste con éxito tu método de dos factores." -#: flask_security/core.py:396 -msgid "Password confirmation is needed in order to access page" -msgstr "" - -#: flask_security/core.py:400 +#: flask_security/core.py:395 msgid "You currently do not have permissions to access this page" -msgstr "" +msgstr "Actualmente no tienes permisos para acceder a esta página" -#: flask_security/core.py:403 +#: flask_security/core.py:398 msgid "Marked method is not valid" -msgstr "" +msgstr "El método marcado no es válido" -#: flask_security/core.py:405 +#: flask_security/core.py:400 msgid "You successfully disabled two factor authorization." -msgstr "" +msgstr "Has deshabilitado con éxito la autorización de dos factores." -#: flask_security/core.py:408 +#: flask_security/core.py:403 msgid "Requested method is not valid" -msgstr "" +msgstr "El método solicitado no es válido" -#: flask_security/core.py:410 +#: flask_security/core.py:405 #, python-format msgid "Setup must be completed within %(within)s. Please start over." -msgstr "" +msgstr "La configuración debe completarse en %(within)s. Empiece de nuevo." -#: flask_security/core.py:413 +#: flask_security/core.py:408 msgid "Unified sign in setup successful" -msgstr "" +msgstr "El inicio de sesión unificado se configuró correctamente" -#: flask_security/core.py:414 +#: flask_security/core.py:409 msgid "You must specify a valid identity to sign in" -msgstr "" +msgstr "Debes especificar una identidad válida para iniciar sesión" -#: flask_security/core.py:415 +#: flask_security/core.py:410 #, python-format msgid "Use this code to sign in: %(code)s." -msgstr "" +msgstr "Utiliza este código para iniciar sesión: %(code)s." -#: flask_security/forms.py:50 +#: flask_security/forms.py:53 msgid "Email Address" msgstr "Correo electrónico" -#: flask_security/forms.py:51 +#: flask_security/forms.py:54 msgid "Password" msgstr "Contraseña" -#: flask_security/forms.py:52 +#: flask_security/forms.py:55 msgid "Remember Me" msgstr "Recordarme" -#: flask_security/forms.py:53 flask_security/templates/security/_menu.html:5 +#: flask_security/forms.py:56 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/forms.py:57 #: flask_security/templates/security/email/us_instructions.html:8 #: flask_security/templates/security/us_signin.html:6 msgid "Sign In" -msgstr "" +msgstr "Iniciar sesión" -#: flask_security/forms.py:55 flask_security/templates/security/_menu.html:11 +#: flask_security/forms.py:58 flask_security/templates/security/_menu.html:11 #: flask_security/templates/security/register_user.html:6 msgid "Register" msgstr "Registrarse" -#: flask_security/forms.py:56 +#: flask_security/forms.py:59 msgid "Resend Confirmation Instructions" msgstr "Reenviar instrucciones de confirmación" -#: flask_security/forms.py:57 +#: flask_security/forms.py:60 msgid "Recover Password" msgstr "Recuperar contraseña" -#: flask_security/forms.py:58 +#: flask_security/forms.py:61 msgid "Reset Password" msgstr "Restablecer contraseña" -#: flask_security/forms.py:59 +#: flask_security/forms.py:62 msgid "Retype Password" msgstr "Escribir contraseña nuevamente" -#: flask_security/forms.py:60 +#: flask_security/forms.py:63 msgid "New Password" msgstr "Nueva contraseña" -#: flask_security/forms.py:61 +#: flask_security/forms.py:64 msgid "Change Password" msgstr "Cambiar la contraseña" -#: flask_security/forms.py:62 +#: flask_security/forms.py:65 msgid "Send Login Link" msgstr "Enviar enlace para iniciar sesión" -#: flask_security/forms.py:63 +#: flask_security/forms.py:66 msgid "Verify Password" -msgstr "" +msgstr "Verificar contraseña" -#: flask_security/forms.py:64 +#: flask_security/forms.py:67 msgid "Change Method" -msgstr "" +msgstr "Método de cambio" -#: flask_security/forms.py:65 +#: flask_security/forms.py:68 msgid "Phone Number" -msgstr "" +msgstr "Número de teléfono" -#: flask_security/forms.py:66 +#: flask_security/forms.py:69 msgid "Authentication Code" -msgstr "" +msgstr "Código de autenticación" -#: flask_security/forms.py:67 +#: flask_security/forms.py:70 msgid "Submit" -msgstr "" +msgstr "Enviar" -#: flask_security/forms.py:68 +#: flask_security/forms.py:71 msgid "Submit Code" -msgstr "" +msgstr "Enviar código" -#: flask_security/forms.py:69 +#: flask_security/forms.py:72 msgid "Error(s)" -msgstr "" +msgstr "Error(es)" -#: flask_security/forms.py:70 +#: flask_security/forms.py:73 msgid "Identity" -msgstr "" +msgstr "Identidad" -#: flask_security/forms.py:71 +#: flask_security/forms.py:74 msgid "Send Code" -msgstr "" +msgstr "Enviar código" -#: flask_security/forms.py:72 -#, fuzzy +#: flask_security/forms.py:75 msgid "Passcode" -msgstr "Contraseña" +msgstr "Código de acceso" -#: flask_security/unified_signin.py:145 -#, fuzzy +#: flask_security/unified_signin.py:131 msgid "Code or Password" -msgstr "Recuperar contraseña" +msgstr "Código o contraseña" -#: flask_security/unified_signin.py:150 flask_security/unified_signin.py:270 +#: flask_security/unified_signin.py:136 flask_security/unified_signin.py:262 msgid "Available Methods" -msgstr "" +msgstr "Métodos disponibles" -#: flask_security/unified_signin.py:151 +#: flask_security/unified_signin.py:137 msgid "Via email" -msgstr "" +msgstr "Vía correo electrónico" -#: flask_security/unified_signin.py:151 +#: flask_security/unified_signin.py:137 msgid "Via SMS" -msgstr "" +msgstr "Vía SMS" -#: flask_security/unified_signin.py:272 +#: flask_security/unified_signin.py:264 msgid "Set up using email" -msgstr "" +msgstr "Configuración mediante correo electrónico" -#: flask_security/unified_signin.py:275 +#: flask_security/unified_signin.py:267 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" +"Configurar usando una aplicación de autenticación (por ejemplo, google, " +"lastpass, authy)" -#: flask_security/unified_signin.py:277 +#: flask_security/unified_signin.py:269 msgid "Set up using SMS" -msgstr "" +msgstr "Configurar usando SMS" #: flask_security/templates/security/_menu.html:2 msgid "Menu" @@ -461,7 +465,7 @@ #: flask_security/templates/security/_menu.html:8 msgid "Unified Sign In" -msgstr "" +msgstr "Inicio de sesión unificado" #: flask_security/templates/security/_menu.html:14 msgid "Forgot password" @@ -487,84 +491,92 @@ msgid "Resend confirmation instructions" msgstr "Reenviar instrucciones de confirmación" -#: flask_security/templates/security/two_factor_setup.html:6 +#: flask_security/templates/security/two_factor_setup.html:31 msgid "Two-factor authentication adds an extra layer of security to your account" msgstr "" +"La autenticación de dos factores agrega una capa adicional de seguridad a" +" tu cuenta" -#: flask_security/templates/security/two_factor_setup.html:7 +#: flask_security/templates/security/two_factor_setup.html:32 msgid "" "In addition to your username and password, you'll need to use a code that" " we will send you" msgstr "" +"Además de tu nombre de usuario y contraseña, deberás utilizar un código " +"que te enviaremos" -#: flask_security/templates/security/two_factor_setup.html:18 +#: flask_security/templates/security/two_factor_setup.html:43 msgid "To complete logging in, please enter the code sent to your mail" -msgstr "" +msgstr "Para completar el inicio de sesión, ingresa el código enviado a tu correo" -#: flask_security/templates/security/two_factor_setup.html:21 -#: flask_security/templates/security/us_setup.html:21 +#: flask_security/templates/security/two_factor_setup.html:49 msgid "" -"Open your authenticator app on your device and scan the following qrcode " -"to start receiving codes:" +"Open an authenticator app on your device and scan the following QRcode " +"(or enter the code below manually) to start receiving codes:" msgstr "" -#: flask_security/templates/security/two_factor_setup.html:22 +#: flask_security/templates/security/two_factor_setup.html:52 msgid "Two factor authentication code" -msgstr "" +msgstr "Código de autenticación de dos factores" -#: flask_security/templates/security/two_factor_setup.html:25 +#: flask_security/templates/security/two_factor_setup.html:60 msgid "To Which Phone Number Should We Send Code To?" -msgstr "" +msgstr "¿A qué número de teléfono debemos enviar el código?" #: flask_security/templates/security/two_factor_verify_code.html:6 msgid "Two-factor Authentication" -msgstr "" +msgstr "Autenticación de dos factores" #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Please enter your authentication code" -msgstr "" +msgstr "Ingrese su código de autenticación" #: flask_security/templates/security/two_factor_verify_code.html:18 msgid "The code for authentication was sent to your email address" -msgstr "" +msgstr "El código de autenticación se envió a tu dirección de correo electrónico." #: 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 "" +"Se nos envió un correo electrónico para restablecer tu cuenta de " +"aplicación" -#: 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 +#: flask_security/templates/security/us_setup.html:36 msgid "Setup Unified Sign In options" -msgstr "" +msgstr "Configurar opciones de inicio de sesión unificado" -#: 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_setup.html:58 #: 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" +msgstr "Se envió el código" -#: flask_security/templates/security/us_setup.html:29 -msgid "No methods have been enabled - nothing to setup" +#: flask_security/templates/security/us_setup.html:66 +msgid "" +"Open an authenticator app on your device and scan the following QRcode " +"(or enter the code below manually) to start receiving passcodes:" msgstr "" +#: flask_security/templates/security/us_setup.html:69 +msgid "Passwordless QRCode" +msgstr "Código QR sin contraseña" + +#: flask_security/templates/security/us_setup.html:77 +msgid "No methods have been enabled - nothing to setup" +msgstr "No se han habilitado métodos, no hay nada que configurar" + #: flask_security/templates/security/us_signin.html:15 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" -msgstr "" +msgstr "Solicitar que se envíe un código de un solo uso" #: 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." +msgstr "Por favor, vuelva a autenticarse" + +#: flask_security/templates/security/verify.html:6 +msgid "Please Enter Your Password" +msgstr "Por favor, introduzca su contraseña" #: flask_security/templates/security/email/change_notice.html:1 msgid "Your password has been changed." @@ -578,7 +590,14 @@ msgid "click here to reset it" msgstr "haz clic aquí para restablecerla" +#: flask_security/templates/security/email/change_notice.txt:3 +msgid "If you did not change your password, click the link below to reset it." +msgstr "" +"Si no cambiaste la contraseña, haz clic en el enlace de abajo para " +"restablecerla." + #: flask_security/templates/security/email/confirmation_instructions.html:1 +#: flask_security/templates/security/email/confirmation_instructions.txt:1 msgid "Please confirm your email through the link below:" msgstr "Confirma tu correo electrónico haciendo clic aquí:" @@ -588,12 +607,15 @@ msgstr "Confirmar mi cuenta" #: flask_security/templates/security/email/login_instructions.html:1 +#: flask_security/templates/security/email/login_instructions.txt:1 #: flask_security/templates/security/email/welcome.html:1 +#: flask_security/templates/security/email/welcome.txt:1 #, python-format msgid "Welcome %(email)s!" msgstr "¡Bienvenido %(email)s!" #: flask_security/templates/security/email/login_instructions.html:3 +#: flask_security/templates/security/email/login_instructions.txt:3 msgid "You can log into your account through the link below:" msgstr "Inicia sesión haciendo clic aquí:" @@ -605,25 +627,44 @@ msgid "Click here to reset your password" msgstr "Haz clic aquí para restablecer la contraseña" +#: flask_security/templates/security/email/reset_instructions.txt:1 +msgid "Click the link below to reset your password:" +msgstr "Haz clic en el enlace de abajo para restablecer la contraseña:" + #: flask_security/templates/security/email/two_factor_instructions.html:3 +#: flask_security/templates/security/email/two_factor_instructions.txt:3 msgid "You can log into your account using the following code:" -msgstr "" +msgstr "Puedes iniciar sesión en tu cuenta utilizando el siguiente código:" #: flask_security/templates/security/email/two_factor_rescue.html:1 +#: flask_security/templates/security/email/two_factor_rescue.txt:1 msgid "can not access mail account" -msgstr "" +msgstr "no puede acceder a la cuenta de correo" #: flask_security/templates/security/email/us_instructions.html:3 -#, fuzzy +#: flask_security/templates/security/email/us_instructions.txt:3 msgid "You can sign into your account using the following code:" -msgstr "Inicia sesión haciendo clic aquí:" +msgstr "Puedes iniciar sesión en tu cuenta utilizando el siguiente código:" #: 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í:" +msgstr "O utilizar el siguiente enlace:" + +#: flask_security/templates/security/email/us_instructions.txt:7 +msgid "Or use the link below:" +msgstr "O utilizar el siguiente enlace:" #: flask_security/templates/security/email/welcome.html:4 +#: flask_security/templates/security/email/welcome.txt:4 msgid "You can confirm your email through the link below:" msgstr "Confirma tu correo electrónico haciendo clic aquí:" +#~ msgid "" +#~ "Open your authenticator app on your " +#~ "device and scan the following qrcode " +#~ "to start receiving codes:" +#~ msgstr "" +#~ "Abre tu aplicación de autenticación en" +#~ " tu dispositivo y escanea el " +#~ "siguiente código qr para comenzar a " +#~ "recibir códigos:" Binary files /tmp/tmpl_LU2e/ak23Qk5OdH/flask-security-3.4.2/flask_security/translations/eu_ES/LC_MESSAGES/flask_security.mo and /tmp/tmpl_LU2e/U10RHAXkq7/flask-security-4.0.0/flask_security/translations/eu_ES/LC_MESSAGES/flask_security.mo differ diff -Nru flask-security-3.4.2/flask_security/translations/eu_ES/LC_MESSAGES/flask_security.po flask-security-4.0.0/flask_security/translations/eu_ES/LC_MESSAGES/flask_security.po --- flask-security-3.4.2/flask_security/translations/eu_ES/LC_MESSAGES/flask_security.po 1970-01-01 00:00:00.000000000 +0000 +++ flask-security-4.0.0/flask_security/translations/eu_ES/LC_MESSAGES/flask_security.po 2021-01-26 02:39:51.000000000 +0000 @@ -0,0 +1,658 @@ +# Basque (Spain) translations for Flask-Security. +# Copyright (C) 2020 +# This file is distributed under the same license as the Flask-Security +# project. +# Martin Mozos , 2020. +# +msgid "" +msgstr "" +"Project-Id-Version: Flask-Security 4.0.0\n" +"Report-Msgid-Bugs-To: jwag956@github.com\n" +"POT-Creation-Date: 2021-01-22 14:48-0800\n" +"PO-Revision-Date: 2020-11-28 13:41+0100\n" +"Last-Translator: Martin Mozos \n" +"Language: eu_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.9.0\n" + +#: flask_security/core.py:209 +msgid "Login Required" +msgstr "Saioa hasi behar da" + +#: flask_security/core.py:210 +#: flask_security/templates/security/email/two_factor_instructions.html:1 +#: flask_security/templates/security/email/two_factor_instructions.txt:1 +#: flask_security/templates/security/email/us_instructions.html:1 +#: flask_security/templates/security/email/us_instructions.txt:1 +#: tests/templates/_nav.html:2 +msgid "Welcome" +msgstr "Ongi etorri" + +#: flask_security/core.py:211 +msgid "Please confirm your email" +msgstr "Mesedez berretsi zure posta elektronikoa" + +#: flask_security/core.py:212 +msgid "Login instructions" +msgstr "Saioa hasteko argibideak" + +#: flask_security/core.py:213 +#: flask_security/templates/security/email/reset_notice.html:1 +#: flask_security/templates/security/email/reset_notice.txt:1 +msgid "Your password has been reset" +msgstr "Zure pasahitza berrezarri da" + +#: flask_security/core.py:214 +#: flask_security/templates/security/email/change_notice.txt:1 +msgid "Your password has been changed" +msgstr "Zure pasahitza aldatu da" + +#: flask_security/core.py:215 +msgid "Password reset instructions" +msgstr "Pasahitza berreskuratzeko argibideak" + +#: flask_security/core.py:218 +msgid "Two-factor Login" +msgstr "Bi faktoreko saioa hastea" + +#: flask_security/core.py:219 +msgid "Two-factor Rescue" +msgstr "Bi faktoreko saioa berreskuratzea" + +#: flask_security/core.py:263 +msgid "Verification Code" +msgstr "Egiaztapen kodea" + +#: flask_security/core.py:278 +msgid "Input not appropriate for requested API" +msgstr "Sarrera ez da egokia eskatutako APIarentzat" + +#: flask_security/core.py:279 +msgid "You do not have permission to view this resource." +msgstr "Ez duzu baliabide hau kontsultatzeko baimenik." + +#: flask_security/core.py:281 +msgid "You are not authenticated. Please supply the correct credentials." +msgstr "Ez zaude autentifikatuta. Mesedez, eman egiaztagiri zuzenak." + +#: flask_security/core.py:285 +msgid "You must re-authenticate to access this endpoint" +msgstr "Baliabide honetara sartzeko berriro autentifikatu behar duzu" + +#: flask_security/core.py:289 +#, python-format +msgid "Thank you. Confirmation instructions have been sent to %(email)s." +msgstr "Eskerrik asko. Baieztapen argibideak %(email)s helbidera bidali dira." + +#: flask_security/core.py:292 +msgid "Thank you. Your email has been confirmed." +msgstr "Eskerrik asko. Zure posta elektronikoa berretsi da." + +#: flask_security/core.py:293 +msgid "Your email has already been confirmed." +msgstr "Zure posta elektronikoa dagoeneko baieztatuta dago." + +#: flask_security/core.py:294 +msgid "Invalid confirmation token." +msgstr "Berrespen token baliogabea." + +#: flask_security/core.py:296 +#, python-format +msgid "%(email)s is already associated with an account." +msgstr "%(email)s dagoeneko kontu batekin lotuta dago." + +#: flask_security/core.py:300 +#, python-format +msgid "" +"Identity attribute '%(attr)s' with value '%(value)s' is already " +"associated with an account." +msgstr "" +"'%(attr)s' identitate atributua '%(value)s' balioarekin dagoeneko kontu " +"batekin lotuta dago" + +#: flask_security/core.py:306 +msgid "Password does not match" +msgstr "Pasahitza ez dator bat" + +#: flask_security/core.py:307 +msgid "Passwords do not match" +msgstr "Pasahitzak ez datoz bat" + +#: flask_security/core.py:308 +msgid "Redirections outside the domain are forbidden" +msgstr "Domeinutik kanpoko birbideratzeak debekatuta daude" + +#: flask_security/core.py:310 +#, python-format +msgid "Instructions to reset your password have been sent to %(email)s." +msgstr "Pasahitza berrezartzeko argibideak %(email)s helbidera bidali dira." + +#: flask_security/core.py:314 +#, python-format +msgid "" +"You did not reset your password within %(within)s. New instructions have " +"been sent to %(email)s." +msgstr "" +"Ez duzu zure pasahitza berrezarri %(within)s barruan. Argibide berriak " +"bidali dira %(email)s-era." + +#: flask_security/core.py:320 +msgid "Invalid reset password token." +msgstr "Pasahitza berrezartzeko token baliogabea." + +#: flask_security/core.py:321 +msgid "Email requires confirmation." +msgstr "Mezu elektronikoak berrespena behar du." + +#: flask_security/core.py:323 +#, python-format +msgid "Confirmation instructions have been sent to %(email)s." +msgstr "Baieztapen argibideak %(email)s helbidera bidali dira." + +#: flask_security/core.py:327 +#, 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 "" +"Ez duzu zure posta elektronikoa berretsi %(within)s barruan. Zure posta " +"elektronikoa berresteko argibide berriak %(email)s helbidera bidali dira." + +#: flask_security/core.py:335 +#, python-format +msgid "" +"You did not login within %(within)s. New instructions to login have been " +"sent to %(email)s." +msgstr "" +"Ez duzu saioa hasi %(within)s barruan. Saioa hasteko argibide berriak " +"%(email)s helbidera bidali dira." + +#: flask_security/core.py:342 +#, python-format +msgid "Instructions to login have been sent to %(email)s." +msgstr "Saioa hasteko argibideak %(email)s helbidera bidali dira." + +#: flask_security/core.py:345 +msgid "Invalid login token." +msgstr "Saioa hasteko token baliogabea." + +#: flask_security/core.py:346 +msgid "Account is disabled." +msgstr "Kontua desgaituta dago." + +#: flask_security/core.py:347 +msgid "Email not provided" +msgstr "Helbide elektronikoa beharrezkoa da" + +#: flask_security/core.py:348 +msgid "Invalid email address" +msgstr "Helbide elektroniko baliogabea" + +#: flask_security/core.py:349 +msgid "Invalid code" +msgstr "Kode baliogabea" + +#: flask_security/core.py:350 +msgid "Password not provided" +msgstr "Pasahitza beharrezkoa da" + +#: flask_security/core.py:351 +msgid "No password is set for this user" +msgstr "Ez da pasahitzik ezarri erabiltzaile honentzat" + +#: flask_security/core.py:353 +#, python-format +msgid "Password must be at least %(length)s characters" +msgstr "Pasahitzak gutxienez %(length)s karaktere izan behar ditu" + +#: flask_security/core.py:356 +msgid "Password not complex enough" +msgstr "Pasahitza ez da nahikoa konplexua" + +#: flask_security/core.py:357 +msgid "Password on breached list" +msgstr "Pasahitza urratutako zerrendan" + +#: flask_security/core.py:359 +msgid "Failed to contact breached passwords site" +msgstr "Ezin izan da urratutako pasahitzen iturburuarekin harremanetan jarri" + +#: flask_security/core.py:362 +msgid "Phone number not valid e.g. missing country code" +msgstr "Telefono zenbakiak ez du balio, baliteke herrialde kodea faltatzea" + +#: flask_security/core.py:363 +msgid "Specified user does not exist" +msgstr "Zehaztutako erabiltzea ez da existitzen" + +#: flask_security/core.py:364 +msgid "Invalid password" +msgstr "Pasahitz okerra" + +#: flask_security/core.py:365 +msgid "Password or code submitted is not valid" +msgstr "Zehaztutako pasahitzak edo kodeak ez du balio" + +#: flask_security/core.py:366 +msgid "You have successfully logged in." +msgstr "Behar bezala hasi duzu saioa." + +#: flask_security/core.py:367 +msgid "Forgot password?" +msgstr "Pasahitza ahaztua?" + +#: flask_security/core.py:369 +msgid "" +"You successfully reset your password and you have been logged in " +"automatically." +msgstr "Pasahitza berrezarri duzunez automatikoki hasi duzu saioa." + +#: flask_security/core.py:376 +msgid "Your new password must be different than your previous password." +msgstr "Zure pasahitz berriak zure aurreko pasahitzaren ezberdin behar du izan." + +#: flask_security/core.py:379 +msgid "You successfully changed your password." +msgstr "Pasahitza behar bezala aldatu duzu." + +#: flask_security/core.py:380 +msgid "Please log in to access this page." +msgstr "Hasi saioa orri honetara sartzeko." + +#: flask_security/core.py:381 +msgid "Please reauthenticate to access this page." +msgstr "Mesedez, berriro autentifikatu orri honetara sartzeko." + +#: flask_security/core.py:382 +msgid "Reauthentication successful" +msgstr "Berautentifikatzea osatu da" + +#: flask_security/core.py:384 +msgid "You can only access this endpoint when not logged in." +msgstr "Saioa amaitzen ez duzunean soilik sar zaitezke baliabide honetara" + +#: flask_security/core.py:387 +msgid "Failed to send code. Please try again later" +msgstr "Ezin izan da kodea bidali. Saiatu berriro geroago" + +#: flask_security/core.py:388 +msgid "Invalid Token" +msgstr "Token baliogabea" + +#: flask_security/core.py:389 +msgid "Your token has been confirmed" +msgstr "Zure token baieztatu da" + +#: flask_security/core.py:391 +msgid "You successfully changed your two-factor method." +msgstr "Bi faktoretako metodoa ondo aldatu duzu." + +#: flask_security/core.py:395 +msgid "You currently do not have permissions to access this page" +msgstr "Une honetan ez duzu baimenik orrialde honetara sartzeko" + +#: flask_security/core.py:398 +msgid "Marked method is not valid" +msgstr "Markatutako metodoak ez du balio" + +#: flask_security/core.py:400 +msgid "You successfully disabled two factor authorization." +msgstr "Bi faktoreren baimena behar bezala desgaitu duzu." + +#: flask_security/core.py:403 +msgid "Requested method is not valid" +msgstr "Eskatutako metodoak ez du balio" + +#: flask_security/core.py:405 +#, python-format +msgid "Setup must be completed within %(within)s. Please start over." +msgstr "Konfigurazioa %(within)s barruan osatu behar da. Mesedez, berriro hasi." + +#: flask_security/core.py:408 +msgid "Unified sign in setup successful" +msgstr "Bateratutako saio hasierarako konfigurazioa ongi egin da" + +#: flask_security/core.py:409 +msgid "You must specify a valid identity to sign in" +msgstr "Saioa hasteko identitate baliagarria zehaztu behar duzu" + +#: flask_security/core.py:410 +#, python-format +msgid "Use this code to sign in: %(code)s." +msgstr "Erabili kode hau saioa hasteko: %(code)s." + +#: flask_security/forms.py:53 +msgid "Email Address" +msgstr "Posta elektronikoa" + +#: flask_security/forms.py:54 +msgid "Password" +msgstr "Pasahitza" + +#: flask_security/forms.py:55 +msgid "Remember Me" +msgstr "Gogorarazi" + +#: flask_security/forms.py:56 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 "Hasi saioa" + +#: flask_security/forms.py:57 +#: flask_security/templates/security/email/us_instructions.html:8 +#: flask_security/templates/security/us_signin.html:6 +msgid "Sign In" +msgstr "Hasi saioa" + +#: flask_security/forms.py:58 flask_security/templates/security/_menu.html:11 +#: flask_security/templates/security/register_user.html:6 +msgid "Register" +msgstr "Izena eman" + +#: flask_security/forms.py:59 +msgid "Resend Confirmation Instructions" +msgstr "Bidali berrespen argibideak" + +#: flask_security/forms.py:60 +msgid "Recover Password" +msgstr "Pasahitza berreskuratu" + +#: flask_security/forms.py:61 +msgid "Reset Password" +msgstr "Pasahitza berrezarri" + +#: flask_security/forms.py:62 +msgid "Retype Password" +msgstr "Idatzi berriro pasahitza" + +#: flask_security/forms.py:63 +msgid "New Password" +msgstr "Pasahitz berria" + +#: flask_security/forms.py:64 +msgid "Change Password" +msgstr "Aldatu pasahitza" + +#: flask_security/forms.py:65 +msgid "Send Login Link" +msgstr "Bidali saioa hasteko esteka" + +#: flask_security/forms.py:66 +msgid "Verify Password" +msgstr "Pasahitza ziurtatu" + +#: flask_security/forms.py:67 +msgid "Change Method" +msgstr "Aldatzeko metodoa" + +#: flask_security/forms.py:68 +msgid "Phone Number" +msgstr "Telefono zenbakia" + +#: flask_security/forms.py:69 +msgid "Authentication Code" +msgstr "Autentifikazio kodea" + +#: flask_security/forms.py:70 +msgid "Submit" +msgstr "Bidali" + +#: flask_security/forms.py:71 +msgid "Submit Code" +msgstr "Bidali kodea" + +#: flask_security/forms.py:72 +msgid "Error(s)" +msgstr "Errorea(k)" + +#: flask_security/forms.py:73 +msgid "Identity" +msgstr "Identitatea" + +#: flask_security/forms.py:74 +msgid "Send Code" +msgstr "Bidali kodea" + +#: flask_security/forms.py:75 +msgid "Passcode" +msgstr "Pasakodea" + +#: flask_security/unified_signin.py:131 +msgid "Code or Password" +msgstr "Kodea edo pasahitza" + +#: flask_security/unified_signin.py:136 flask_security/unified_signin.py:262 +msgid "Available Methods" +msgstr "Eskuragarri dauden metodoak" + +#: flask_security/unified_signin.py:137 +msgid "Via email" +msgstr "Posta elektronikoaren bidez" + +#: flask_security/unified_signin.py:137 +msgid "Via SMS" +msgstr "SMS bidez" + +#: flask_security/unified_signin.py:264 +msgid "Set up using email" +msgstr "Konfiguratu posta elektronikoa erabiliz" + +#: flask_security/unified_signin.py:267 +msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" +msgstr "" +"Konfiguratu autentifikatzaile aplikazio bat erabiliz (google, lastpass " +"edo authy adibidez)" + +#: flask_security/unified_signin.py:269 +msgid "Set up using SMS" +msgstr "Konfiguratu SMS bidez" + +#: flask_security/templates/security/_menu.html:2 +msgid "Menu" +msgstr "Menua" + +#: flask_security/templates/security/_menu.html:8 +msgid "Unified Sign In" +msgstr "Saio hasiera bateratua" + +#: flask_security/templates/security/_menu.html:14 +msgid "Forgot password" +msgstr "Pasahitza ahaztua" + +#: flask_security/templates/security/_menu.html:17 +msgid "Confirm account" +msgstr "Berretsi kontua" + +#: flask_security/templates/security/change_password.html:6 +msgid "Change password" +msgstr "Aldatu pasahitza" + +#: flask_security/templates/security/forgot_password.html:6 +msgid "Send password reset instructions" +msgstr "Bidali pasahitza berrezartzeko argibideak" + +#: flask_security/templates/security/reset_password.html:6 +msgid "Reset password" +msgstr "Pasahitza berrezarri" + +#: flask_security/templates/security/send_confirmation.html:6 +msgid "Resend confirmation instructions" +msgstr "Bidali berrespen argibideak" + +#: flask_security/templates/security/two_factor_setup.html:31 +msgid "Two-factor authentication adds an extra layer of security to your account" +msgstr "" +"Bi faktoreko autentifikazioak segurtasun geruza gehigarri bat esleitzen " +"dio zure kontuari" + +#: flask_security/templates/security/two_factor_setup.html:32 +msgid "" +"In addition to your username and password, you'll need to use a code that" +" we will send you" +msgstr "" +"Zure erabiltzaile izenaz eta pasahitzaz gain, bidaliko dizugun kodea " +"erabili beharko duzu" + +#: flask_security/templates/security/two_factor_setup.html:43 +msgid "To complete logging in, please enter the code sent to your mail" +msgstr "Saioa hasten bukatzeko, idatzi zure posta elektronikora bidalitako kodea" + +#: flask_security/templates/security/two_factor_setup.html:49 +msgid "" +"Open an authenticator app on your device and scan the following QRcode " +"(or enter the code below manually) to start receiving codes:" +msgstr "" + +#: flask_security/templates/security/two_factor_setup.html:52 +msgid "Two factor authentication code" +msgstr "Bi faktoretako autentifikazio kodea" + +#: flask_security/templates/security/two_factor_setup.html:60 +msgid "To Which Phone Number Should We Send Code To?" +msgstr "Zein telefono zenbakiri bidali beharko genioke kodea?" + +#: flask_security/templates/security/two_factor_verify_code.html:6 +msgid "Two-factor Authentication" +msgstr "Bi faktoretako autentifikazioa" + +#: flask_security/templates/security/two_factor_verify_code.html:7 +msgid "Please enter your authentication code" +msgstr "Mesedez, sartu autentifikazio kodea" + +#: flask_security/templates/security/two_factor_verify_code.html:18 +msgid "The code for authentication was sent to your email address" +msgstr "Autentifikaziorako kodea zure helbide elektronikora bidali da" + +#: 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 "Zure eskaera kontua berrezartzeko mezu elektroniko bat bidali ziguten" + +#: flask_security/templates/security/us_setup.html:36 +msgid "Setup Unified Sign In options" +msgstr "Konfiguratu saioa hasteko aukera bateratuak" + +#: flask_security/templates/security/us_setup.html:58 +#: flask_security/templates/security/us_signin.html:23 +#: flask_security/templates/security/us_verify.html:21 +msgid "Code has been sent" +msgstr "Kodea bidali da" + +#: flask_security/templates/security/us_setup.html:66 +msgid "" +"Open an authenticator app on your device and scan the following QRcode " +"(or enter the code below manually) to start receiving passcodes:" +msgstr "" + +#: flask_security/templates/security/us_setup.html:69 +msgid "Passwordless QRCode" +msgstr "Pasahitzik gabeko QRCode" + +#: flask_security/templates/security/us_setup.html:77 +msgid "No methods have been enabled - nothing to setup" +msgstr "Ez da metodorik gaitu, ez dago ezer konfiguratzeko" + +#: flask_security/templates/security/us_signin.html:15 +#: flask_security/templates/security/us_verify.html:13 +msgid "Request one-time code be sent" +msgstr "Eskatu erabilera-bakarreko kodea bidaltzeko" + +#: flask_security/templates/security/us_verify.html:6 +msgid "Please re-authenticate" +msgstr "Mesedez, berriro autentifikatu" + +#: flask_security/templates/security/verify.html:6 +msgid "Please Enter Your Password" +msgstr "Mesedez, sartu zure pasahitza" + +#: flask_security/templates/security/email/change_notice.html:1 +msgid "Your password has been changed." +msgstr "Zure pasahitza aldatu da." + +#: flask_security/templates/security/email/change_notice.html:3 +msgid "If you did not change your password," +msgstr "Pasahitza aldatu ez baduzu," + +#: flask_security/templates/security/email/change_notice.html:3 +msgid "click here to reset it" +msgstr "egin klik hemen berrezartzeko" + +#: flask_security/templates/security/email/change_notice.txt:3 +msgid "If you did not change your password, click the link below to reset it." +msgstr "Pasahitza aldatu ez baduzu, egin klik beheko estekan berrezartzeko." + +#: flask_security/templates/security/email/confirmation_instructions.html:1 +#: flask_security/templates/security/email/confirmation_instructions.txt:1 +msgid "Please confirm your email through the link below:" +msgstr "Mesedez, berretsi zure posta elektronikoa beheko estekaren bidez:" + +#: flask_security/templates/security/email/confirmation_instructions.html:3 +#: flask_security/templates/security/email/welcome.html:6 +msgid "Confirm my account" +msgstr "Berretsi nire kontua" + +#: flask_security/templates/security/email/login_instructions.html:1 +#: flask_security/templates/security/email/login_instructions.txt:1 +#: flask_security/templates/security/email/welcome.html:1 +#: flask_security/templates/security/email/welcome.txt:1 +#, python-format +msgid "Welcome %(email)s!" +msgstr "Ongi etorri %(email)s!" + +#: flask_security/templates/security/email/login_instructions.html:3 +#: flask_security/templates/security/email/login_instructions.txt:3 +msgid "You can log into your account through the link below:" +msgstr "Beheko estekaren bidez saioa has zenezake:" + +#: flask_security/templates/security/email/login_instructions.html:5 +msgid "Login now" +msgstr "Hasi saioa orain" + +#: flask_security/templates/security/email/reset_instructions.html:1 +msgid "Click here to reset your password" +msgstr "Egin klik hemen zure pasahitza berrezartzeko" + +#: flask_security/templates/security/email/reset_instructions.txt:1 +msgid "Click the link below to reset your password:" +msgstr "Egin klik beheko estekan zure pasahitza berrezartzeko:" + +#: flask_security/templates/security/email/two_factor_instructions.html:3 +#: flask_security/templates/security/email/two_factor_instructions.txt:3 +msgid "You can log into your account using the following code:" +msgstr "Zure kontuan saioa has dezakezu kode hau erabiliz:" + +#: flask_security/templates/security/email/two_factor_rescue.html:1 +#: flask_security/templates/security/email/two_factor_rescue.txt:1 +msgid "can not access mail account" +msgstr "ezin da posta kontura sartu" + +#: flask_security/templates/security/email/us_instructions.html:3 +#: flask_security/templates/security/email/us_instructions.txt:3 +msgid "You can sign into your account using the following code:" +msgstr "Zure kontuan saioa has dezakezu kode hau erabiliz:" + +#: flask_security/templates/security/email/us_instructions.html:6 +msgid "Or use the the link below:" +msgstr "Edo erabili beheko esteka:" + +#: flask_security/templates/security/email/us_instructions.txt:7 +msgid "Or use the link below:" +msgstr "Edo erabili beheko esteka:" + +#: flask_security/templates/security/email/welcome.html:4 +#: flask_security/templates/security/email/welcome.txt:4 +msgid "You can confirm your email through the link below:" +msgstr "Zure posta elektronikoa beheko estekaren bidez baiezta dezakezu:" + +#~ msgid "" +#~ "Open your authenticator app on your " +#~ "device and scan the following qrcode " +#~ "to start receiving codes:" +#~ msgstr "" +#~ "Kodeak jasotzen hasteko ireki autentifikazio" +#~ " aplikazioa zure gailuan eta eskaneatu " +#~ "qrcode hau" diff -Nru flask-security-3.4.2/flask_security/translations/flask_security.pot flask-security-4.0.0/flask_security/translations/flask_security.pot --- flask-security-3.4.2/flask_security/translations/flask_security.pot 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/translations/flask_security.pot 2021-01-26 02:39:51.000000000 +0000 @@ -1,439 +1,443 @@ # Translations template for Flask-Security. -# Copyright (C) 2020 ORGANIZATION +# Copyright (C) 2021 ORGANIZATION # This file is distributed under the same license as the Flask-Security # project. -# FIRST AUTHOR , 2020. +# FIRST AUTHOR , 2021. # #, fuzzy msgid "" msgstr "" -"Project-Id-Version: Flask-Security 3.4.0\n" +"Project-Id-Version: Flask-Security 4.0.0\n" "Report-Msgid-Bugs-To: jwag956@github.com\n" -"POT-Creation-Date: 2020-04-19 13:18-0700\n" +"POT-Creation-Date: 2021-01-22 14:48-0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \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" +"Generated-By: Babel 2.9.0\n" -#: flask_security/core.py:207 +#: flask_security/core.py:209 msgid "Login Required" msgstr "" -#: flask_security/core.py:208 +#: flask_security/core.py:210 #: flask_security/templates/security/email/two_factor_instructions.html:1 +#: flask_security/templates/security/email/two_factor_instructions.txt:1 #: flask_security/templates/security/email/us_instructions.html:1 +#: flask_security/templates/security/email/us_instructions.txt:1 +#: tests/templates/_nav.html:2 msgid "Welcome" msgstr "" -#: flask_security/core.py:209 +#: flask_security/core.py:211 msgid "Please confirm your email" msgstr "" -#: flask_security/core.py:210 +#: flask_security/core.py:212 msgid "Login instructions" msgstr "" -#: flask_security/core.py:211 +#: flask_security/core.py:213 #: flask_security/templates/security/email/reset_notice.html:1 +#: flask_security/templates/security/email/reset_notice.txt:1 msgid "Your password has been reset" msgstr "" -#: flask_security/core.py:212 +#: flask_security/core.py:214 +#: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed" msgstr "" -#: flask_security/core.py:213 +#: flask_security/core.py:215 msgid "Password reset instructions" msgstr "" -#: flask_security/core.py:216 +#: flask_security/core.py:218 msgid "Two-factor Login" msgstr "" -#: flask_security/core.py:217 +#: flask_security/core.py:219 msgid "Two-factor Rescue" msgstr "" -#: flask_security/core.py:266 +#: flask_security/core.py:263 msgid "Verification Code" msgstr "" -#: flask_security/core.py:282 +#: flask_security/core.py:278 msgid "Input not appropriate for requested API" msgstr "" -#: flask_security/core.py:283 +#: flask_security/core.py:279 msgid "You do not have permission to view this resource." msgstr "" -#: flask_security/core.py:285 +#: flask_security/core.py:281 msgid "You are not authenticated. Please supply the correct credentials." msgstr "" -#: flask_security/core.py:289 +#: flask_security/core.py:285 msgid "You must re-authenticate to access this endpoint" msgstr "" -#: flask_security/core.py:293 +#: flask_security/core.py:289 #, python-format msgid "Thank you. Confirmation instructions have been sent to %(email)s." msgstr "" -#: flask_security/core.py:296 +#: flask_security/core.py:292 msgid "Thank you. Your email has been confirmed." msgstr "" -#: flask_security/core.py:297 +#: flask_security/core.py:293 msgid "Your email has already been confirmed." msgstr "" -#: flask_security/core.py:298 +#: flask_security/core.py:294 msgid "Invalid confirmation token." msgstr "" -#: flask_security/core.py:300 +#: flask_security/core.py:296 #, python-format msgid "%(email)s is already associated with an account." msgstr "" -#: flask_security/core.py:303 +#: flask_security/core.py:300 +#, python-format +msgid "" +"Identity attribute '%(attr)s' with value '%(value)s' is already " +"associated with an account." +msgstr "" + +#: flask_security/core.py:306 msgid "Password does not match" msgstr "" -#: flask_security/core.py:304 +#: flask_security/core.py:307 msgid "Passwords do not match" msgstr "" -#: flask_security/core.py:305 +#: flask_security/core.py:308 msgid "Redirections outside the domain are forbidden" msgstr "" -#: flask_security/core.py:307 +#: flask_security/core.py:310 #, python-format msgid "Instructions to reset your password have been sent to %(email)s." msgstr "" -#: flask_security/core.py:311 +#: flask_security/core.py:314 #, 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 +#: flask_security/core.py:320 msgid "Invalid reset password token." msgstr "" -#: flask_security/core.py:318 +#: flask_security/core.py:321 msgid "Email requires confirmation." msgstr "" -#: flask_security/core.py:320 +#: flask_security/core.py:323 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "" -#: flask_security/core.py:324 +#: flask_security/core.py:327 #, 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 +#: flask_security/core.py:335 #, 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 +#: flask_security/core.py:342 #, python-format msgid "Instructions to login have been sent to %(email)s." msgstr "" -#: flask_security/core.py:342 +#: flask_security/core.py:345 msgid "Invalid login token." msgstr "" -#: flask_security/core.py:343 +#: flask_security/core.py:346 msgid "Account is disabled." msgstr "" -#: flask_security/core.py:344 +#: flask_security/core.py:347 msgid "Email not provided" msgstr "" -#: flask_security/core.py:345 +#: flask_security/core.py:348 msgid "Invalid email address" msgstr "" -#: flask_security/core.py:346 +#: flask_security/core.py:349 msgid "Invalid code" msgstr "" -#: flask_security/core.py:347 +#: flask_security/core.py:350 msgid "Password not provided" msgstr "" -#: flask_security/core.py:348 +#: flask_security/core.py:351 msgid "No password is set for this user" msgstr "" -#: flask_security/core.py:350 +#: flask_security/core.py:353 #, python-format msgid "Password must be at least %(length)s characters" msgstr "" -#: flask_security/core.py:353 +#: flask_security/core.py:356 msgid "Password not complex enough" msgstr "" -#: flask_security/core.py:354 +#: flask_security/core.py:357 msgid "Password on breached list" msgstr "" -#: flask_security/core.py:356 +#: flask_security/core.py:359 msgid "Failed to contact breached passwords site" msgstr "" -#: flask_security/core.py:359 +#: flask_security/core.py:362 msgid "Phone number not valid e.g. missing country code" msgstr "" -#: flask_security/core.py:360 +#: flask_security/core.py:363 msgid "Specified user does not exist" msgstr "" -#: flask_security/core.py:361 +#: flask_security/core.py:364 msgid "Invalid password" msgstr "" -#: flask_security/core.py:362 +#: flask_security/core.py:365 msgid "Password or code submitted is not valid" msgstr "" -#: flask_security/core.py:363 +#: flask_security/core.py:366 msgid "You have successfully logged in." msgstr "" -#: flask_security/core.py:364 +#: flask_security/core.py:367 msgid "Forgot password?" msgstr "" -#: flask_security/core.py:366 +#: flask_security/core.py:369 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "" -#: flask_security/core.py:373 +#: flask_security/core.py:376 msgid "Your new password must be different than your previous password." msgstr "" -#: flask_security/core.py:376 +#: flask_security/core.py:379 msgid "You successfully changed your password." msgstr "" -#: flask_security/core.py:377 +#: flask_security/core.py:380 msgid "Please log in to access this page." msgstr "" -#: flask_security/core.py:378 +#: flask_security/core.py:381 msgid "Please reauthenticate to access this page." msgstr "" -#: flask_security/core.py:379 +#: flask_security/core.py:382 msgid "Reauthentication successful" msgstr "" -#: flask_security/core.py:381 +#: flask_security/core.py:384 msgid "You can only access this endpoint when not logged in." msgstr "" -#: flask_security/core.py:384 +#: flask_security/core.py:387 msgid "Failed to send code. Please try again later" msgstr "" -#: flask_security/core.py:385 +#: flask_security/core.py:388 msgid "Invalid Token" msgstr "" -#: flask_security/core.py:386 +#: flask_security/core.py:389 msgid "Your token has been confirmed" msgstr "" -#: flask_security/core.py:388 +#: flask_security/core.py:391 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 +#: flask_security/core.py:395 msgid "You currently do not have permissions to access this page" msgstr "" -#: flask_security/core.py:403 +#: flask_security/core.py:398 msgid "Marked method is not valid" msgstr "" -#: flask_security/core.py:405 +#: flask_security/core.py:400 msgid "You successfully disabled two factor authorization." msgstr "" -#: flask_security/core.py:408 +#: flask_security/core.py:403 msgid "Requested method is not valid" msgstr "" -#: flask_security/core.py:410 +#: flask_security/core.py:405 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "" -#: flask_security/core.py:413 +#: flask_security/core.py:408 msgid "Unified sign in setup successful" msgstr "" -#: flask_security/core.py:414 +#: flask_security/core.py:409 msgid "You must specify a valid identity to sign in" msgstr "" -#: flask_security/core.py:415 +#: flask_security/core.py:410 #, python-format msgid "Use this code to sign in: %(code)s." msgstr "" -#: flask_security/forms.py:50 +#: flask_security/forms.py:53 msgid "Email Address" msgstr "" -#: flask_security/forms.py:51 +#: flask_security/forms.py:54 msgid "Password" msgstr "" -#: flask_security/forms.py:52 +#: flask_security/forms.py:55 msgid "Remember Me" msgstr "" -#: flask_security/forms.py:53 flask_security/templates/security/_menu.html:5 +#: flask_security/forms.py:56 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/forms.py:57 #: 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/forms.py:58 flask_security/templates/security/_menu.html:11 #: flask_security/templates/security/register_user.html:6 msgid "Register" msgstr "" -#: flask_security/forms.py:56 +#: flask_security/forms.py:59 msgid "Resend Confirmation Instructions" msgstr "" -#: flask_security/forms.py:57 +#: flask_security/forms.py:60 msgid "Recover Password" msgstr "" -#: flask_security/forms.py:58 +#: flask_security/forms.py:61 msgid "Reset Password" msgstr "" -#: flask_security/forms.py:59 +#: flask_security/forms.py:62 msgid "Retype Password" msgstr "" -#: flask_security/forms.py:60 +#: flask_security/forms.py:63 msgid "New Password" msgstr "" -#: flask_security/forms.py:61 +#: flask_security/forms.py:64 msgid "Change Password" msgstr "" -#: flask_security/forms.py:62 +#: flask_security/forms.py:65 msgid "Send Login Link" msgstr "" -#: flask_security/forms.py:63 +#: flask_security/forms.py:66 msgid "Verify Password" msgstr "" -#: flask_security/forms.py:64 +#: flask_security/forms.py:67 msgid "Change Method" msgstr "" -#: flask_security/forms.py:65 +#: flask_security/forms.py:68 msgid "Phone Number" msgstr "" -#: flask_security/forms.py:66 +#: flask_security/forms.py:69 msgid "Authentication Code" msgstr "" -#: flask_security/forms.py:67 +#: flask_security/forms.py:70 msgid "Submit" msgstr "" -#: flask_security/forms.py:68 +#: flask_security/forms.py:71 msgid "Submit Code" msgstr "" -#: flask_security/forms.py:69 +#: flask_security/forms.py:72 msgid "Error(s)" msgstr "" -#: flask_security/forms.py:70 +#: flask_security/forms.py:73 msgid "Identity" msgstr "" -#: flask_security/forms.py:71 +#: flask_security/forms.py:74 msgid "Send Code" msgstr "" -#: flask_security/forms.py:72 +#: flask_security/forms.py:75 msgid "Passcode" msgstr "" -#: flask_security/unified_signin.py:145 +#: flask_security/unified_signin.py:131 msgid "Code or Password" msgstr "" -#: flask_security/unified_signin.py:150 flask_security/unified_signin.py:270 +#: flask_security/unified_signin.py:136 flask_security/unified_signin.py:262 msgid "Available Methods" msgstr "" -#: flask_security/unified_signin.py:151 +#: flask_security/unified_signin.py:137 msgid "Via email" msgstr "" -#: flask_security/unified_signin.py:151 +#: flask_security/unified_signin.py:137 msgid "Via SMS" msgstr "" -#: flask_security/unified_signin.py:272 +#: flask_security/unified_signin.py:264 msgid "Set up using email" msgstr "" -#: flask_security/unified_signin.py:275 +#: flask_security/unified_signin.py:267 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" -#: flask_security/unified_signin.py:277 +#: flask_security/unified_signin.py:269 msgid "Set up using SMS" msgstr "" @@ -469,32 +473,31 @@ msgid "Resend confirmation instructions" msgstr "" -#: flask_security/templates/security/two_factor_setup.html:6 +#: flask_security/templates/security/two_factor_setup.html:31 msgid "Two-factor authentication adds an extra layer of security to your account" msgstr "" -#: flask_security/templates/security/two_factor_setup.html:7 +#: flask_security/templates/security/two_factor_setup.html:32 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 +#: flask_security/templates/security/two_factor_setup.html:43 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 +#: flask_security/templates/security/two_factor_setup.html:49 msgid "" -"Open your authenticator app on your device and scan the following qrcode " -"to start receiving codes:" +"Open an authenticator app on your device and scan the following QRcode " +"(or enter the code below manually) to start receiving codes:" msgstr "" -#: flask_security/templates/security/two_factor_setup.html:22 +#: flask_security/templates/security/two_factor_setup.html:52 msgid "Two factor authentication code" msgstr "" -#: flask_security/templates/security/two_factor_setup.html:25 +#: flask_security/templates/security/two_factor_setup.html:60 msgid "To Which Phone Number Should We Send Code To?" msgstr "" @@ -514,26 +517,27 @@ 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 +#: flask_security/templates/security/us_setup.html:36 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_setup.html:58 #: 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 +#: flask_security/templates/security/us_setup.html:66 +msgid "" +"Open an authenticator app on your device and scan the following QRcode " +"(or enter the code below manually) to start receiving passcodes:" +msgstr "" + +#: flask_security/templates/security/us_setup.html:69 +msgid "Passwordless QRCode" +msgstr "" + +#: flask_security/templates/security/us_setup.html:77 msgid "No methods have been enabled - nothing to setup" msgstr "" @@ -546,6 +550,10 @@ msgid "Please re-authenticate" msgstr "" +#: flask_security/templates/security/verify.html:6 +msgid "Please Enter Your Password" +msgstr "" + #: flask_security/templates/security/email/change_notice.html:1 msgid "Your password has been changed." msgstr "" @@ -558,7 +566,12 @@ msgid "click here to reset it" msgstr "" +#: flask_security/templates/security/email/change_notice.txt:3 +msgid "If you did not change your password, click the link below to reset it." +msgstr "" + #: flask_security/templates/security/email/confirmation_instructions.html:1 +#: flask_security/templates/security/email/confirmation_instructions.txt:1 msgid "Please confirm your email through the link below:" msgstr "" @@ -568,12 +581,15 @@ msgstr "" #: flask_security/templates/security/email/login_instructions.html:1 +#: flask_security/templates/security/email/login_instructions.txt:1 #: flask_security/templates/security/email/welcome.html:1 +#: flask_security/templates/security/email/welcome.txt:1 #, python-format msgid "Welcome %(email)s!" msgstr "" #: flask_security/templates/security/email/login_instructions.html:3 +#: flask_security/templates/security/email/login_instructions.txt:3 msgid "You can log into your account through the link below:" msgstr "" @@ -585,15 +601,22 @@ msgid "Click here to reset your password" msgstr "" +#: flask_security/templates/security/email/reset_instructions.txt:1 +msgid "Click the link below to reset your password:" +msgstr "" + #: flask_security/templates/security/email/two_factor_instructions.html:3 +#: flask_security/templates/security/email/two_factor_instructions.txt:3 msgid "You can log into your account using the following code:" msgstr "" #: flask_security/templates/security/email/two_factor_rescue.html:1 +#: flask_security/templates/security/email/two_factor_rescue.txt:1 msgid "can not access mail account" msgstr "" #: flask_security/templates/security/email/us_instructions.html:3 +#: flask_security/templates/security/email/us_instructions.txt:3 msgid "You can sign into your account using the following code:" msgstr "" @@ -601,7 +624,11 @@ msgid "Or use the the link below:" msgstr "" +#: flask_security/templates/security/email/us_instructions.txt:7 +msgid "Or use the link below:" +msgstr "" + #: flask_security/templates/security/email/welcome.html:4 +#: flask_security/templates/security/email/welcome.txt:4 msgid "You can confirm your email through the link below:" msgstr "" - Binary files /tmp/tmpl_LU2e/ak23Qk5OdH/flask-security-3.4.2/flask_security/translations/fr_FR/LC_MESSAGES/flask_security.mo and /tmp/tmpl_LU2e/U10RHAXkq7/flask-security-4.0.0/flask_security/translations/fr_FR/LC_MESSAGES/flask_security.mo differ diff -Nru flask-security-3.4.2/flask_security/translations/fr_FR/LC_MESSAGES/flask_security.po flask-security-4.0.0/flask_security/translations/fr_FR/LC_MESSAGES/flask_security.po --- flask-security-3.4.2/flask_security/translations/fr_FR/LC_MESSAGES/flask_security.po 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/translations/fr_FR/LC_MESSAGES/flask_security.po 2021-01-26 02:39:51.000000000 +0000 @@ -8,7 +8,7 @@ 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" +"POT-Creation-Date: 2021-01-22 14:48-0800\n" "PO-Revision-Date: 2017-06-08 10:13+0200\n" "Last-Translator: Alexandre Bulté \n" "Language: fr_FR\n" @@ -17,110 +17,122 @@ "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" +"Generated-By: Babel 2.9.0\n" -#: flask_security/core.py:207 +#: flask_security/core.py:209 msgid "Login Required" msgstr "Connexion requise" -#: flask_security/core.py:208 +#: flask_security/core.py:210 #: flask_security/templates/security/email/two_factor_instructions.html:1 +#: flask_security/templates/security/email/two_factor_instructions.txt:1 #: flask_security/templates/security/email/us_instructions.html:1 +#: flask_security/templates/security/email/us_instructions.txt:1 +#: tests/templates/_nav.html:2 msgid "Welcome" msgstr "Bienvenue" -#: flask_security/core.py:209 +#: flask_security/core.py:211 msgid "Please confirm your email" msgstr "Merci de confirmer votre adresse email" -#: flask_security/core.py:210 +#: flask_security/core.py:212 msgid "Login instructions" msgstr "Instructions de connexion" -#: flask_security/core.py:211 +#: flask_security/core.py:213 #: flask_security/templates/security/email/reset_notice.html:1 +#: flask_security/templates/security/email/reset_notice.txt:1 msgid "Your password has been reset" msgstr "Votre mot de passe a été réinitialisé" -#: flask_security/core.py:212 +#: flask_security/core.py:214 +#: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed" msgstr "Votre mot de passe a été changé" -#: flask_security/core.py:213 +#: flask_security/core.py:215 msgid "Password reset instructions" msgstr "Instructions de réinitialisation de votre mot de passe" -#: flask_security/core.py:216 +#: flask_security/core.py:218 msgid "Two-factor Login" msgstr "" -#: flask_security/core.py:217 +#: flask_security/core.py:219 msgid "Two-factor Rescue" msgstr "" -#: flask_security/core.py:266 +#: flask_security/core.py:263 msgid "Verification Code" msgstr "" -#: flask_security/core.py:282 +#: flask_security/core.py:278 msgid "Input not appropriate for requested API" msgstr "" -#: flask_security/core.py:283 +#: flask_security/core.py:279 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 +#: flask_security/core.py:281 msgid "You are not authenticated. Please supply the correct credentials." msgstr "" -#: flask_security/core.py:289 +#: flask_security/core.py:285 #, 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 +#: flask_security/core.py:289 #, 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 +#: flask_security/core.py:292 msgid "Thank you. Your email has been confirmed." msgstr "Merci. Votre adresse email a été confirmée." -#: flask_security/core.py:297 +#: flask_security/core.py:293 msgid "Your email has already been confirmed." msgstr "Votre adresse email a déjà été confirmée." -#: flask_security/core.py:298 +#: flask_security/core.py:294 msgid "Invalid confirmation token." msgstr "Token de confirmation non valide." -#: flask_security/core.py:300 +#: flask_security/core.py:296 #, 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 +#: flask_security/core.py:300 +#, python-format +msgid "" +"Identity attribute '%(attr)s' with value '%(value)s' is already " +"associated with an account." +msgstr "" + +#: flask_security/core.py:306 msgid "Password does not match" msgstr "Le mot de passe ne correspond pas" -#: flask_security/core.py:304 +#: flask_security/core.py:307 msgid "Passwords do not match" msgstr "Les mots de passe ne correspondent pas" -#: flask_security/core.py:305 +#: flask_security/core.py:308 msgid "Redirections outside the domain are forbidden" msgstr "Les redirections en dehors du domaine sont interdites" -#: flask_security/core.py:307 +#: flask_security/core.py:310 #, 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 +#: flask_security/core.py:314 #, python-format msgid "" "You did not reset your password within %(within)s. New instructions have " @@ -129,20 +141,20 @@ "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 +#: flask_security/core.py:320 msgid "Invalid reset password token." msgstr "Token de réinitialisation non valide." -#: flask_security/core.py:318 +#: flask_security/core.py:321 msgid "Email requires confirmation." msgstr "Une confirmation de l'adresse email est requise." -#: flask_security/core.py:320 +#: flask_security/core.py:323 #, 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 +#: flask_security/core.py:327 #, python-format msgid "" "You did not confirm your email within %(within)s. New instructions to " @@ -151,7 +163,7 @@ "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 +#: flask_security/core.py:335 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " @@ -160,82 +172,81 @@ "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 +#: flask_security/core.py:342 #, 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 +#: flask_security/core.py:345 msgid "Invalid login token." msgstr "Token de connexion non valide." -#: flask_security/core.py:343 +#: flask_security/core.py:346 msgid "Account is disabled." msgstr "Le compte est désactivé." -#: flask_security/core.py:344 +#: flask_security/core.py:347 msgid "Email not provided" msgstr "Merci d'indiquer une adresse email" -#: flask_security/core.py:345 +#: flask_security/core.py:348 msgid "Invalid email address" msgstr "Adresse email non valide" -#: flask_security/core.py:346 -#, fuzzy +#: flask_security/core.py:349 msgid "Invalid code" -msgstr "Mot de passe non valide" +msgstr "" -#: flask_security/core.py:347 +#: flask_security/core.py:350 msgid "Password not provided" msgstr "Merci d'indiquer un mot de passe" -#: flask_security/core.py:348 +#: flask_security/core.py:351 msgid "No password is set for this user" msgstr "Cet utilisateur n'a pas de mot de passe" -#: flask_security/core.py:350 +#: flask_security/core.py:353 #, fuzzy, python-format msgid "Password must be at least %(length)s characters" -msgstr "Le mot de passe doit comporter au moins 6 caractères" +msgstr "Le mot de passe doit comporter au moins %(length)s caractères" -#: flask_security/core.py:353 +#: flask_security/core.py:356 msgid "Password not complex enough" msgstr "" -#: flask_security/core.py:354 +#: flask_security/core.py:357 msgid "Password on breached list" msgstr "" -#: flask_security/core.py:356 +#: flask_security/core.py:359 msgid "Failed to contact breached passwords site" msgstr "" -#: flask_security/core.py:359 +#: flask_security/core.py:362 msgid "Phone number not valid e.g. missing country code" msgstr "" -#: flask_security/core.py:360 +#: flask_security/core.py:363 msgid "Specified user does not exist" msgstr "Cet utilisateur n'existe pas" -#: flask_security/core.py:361 +#: flask_security/core.py:364 msgid "Invalid password" msgstr "Mot de passe non valide" -#: flask_security/core.py:362 +#: flask_security/core.py:365 msgid "Password or code submitted is not valid" msgstr "" -#: flask_security/core.py:363 +#: flask_security/core.py:366 msgid "You have successfully logged in." msgstr "Vous êtes bien connecté." -#: flask_security/core.py:364 +#: flask_security/core.py:367 msgid "Forgot password?" msgstr "Mot de passe oublié ?" -#: flask_security/core.py:366 +#: flask_security/core.py:369 msgid "" "You successfully reset your password and you have been logged in " "automatically." @@ -243,212 +254,202 @@ "Vous avez bien réinitialisé votre mot de passe et avez été " "automatiquement connecté." -#: flask_security/core.py:373 +#: flask_security/core.py:376 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 +#: flask_security/core.py:379 msgid "You successfully changed your password." msgstr "Vous avez bien changé votre mot de passe." -#: flask_security/core.py:377 +#: flask_security/core.py:380 msgid "Please log in to access this page." msgstr "Merci de vous connecter pour accéder à cette page." -#: flask_security/core.py:378 +#: flask_security/core.py:381 msgid "Please reauthenticate to access this page." msgstr "Merci de vous reconnecter pour accéder à cette page." -#: flask_security/core.py:379 +#: flask_security/core.py:382 msgid "Reauthentication successful" msgstr "" -#: flask_security/core.py:381 +#: flask_security/core.py:384 msgid "You can only access this endpoint when not logged in." msgstr "" -#: flask_security/core.py:384 +#: flask_security/core.py:387 msgid "Failed to send code. Please try again later" msgstr "" -#: flask_security/core.py:385 +#: flask_security/core.py:388 msgid "Invalid Token" msgstr "" -#: flask_security/core.py:386 +#: flask_security/core.py:389 msgid "Your token has been confirmed" msgstr "" -#: flask_security/core.py:388 +#: flask_security/core.py:391 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 +#: flask_security/core.py:395 msgid "You currently do not have permissions to access this page" msgstr "" -#: flask_security/core.py:403 +#: flask_security/core.py:398 msgid "Marked method is not valid" msgstr "" -#: flask_security/core.py:405 +#: flask_security/core.py:400 msgid "You successfully disabled two factor authorization." msgstr "" -#: flask_security/core.py:408 +#: flask_security/core.py:403 msgid "Requested method is not valid" msgstr "" -#: flask_security/core.py:410 +#: flask_security/core.py:405 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "" -#: flask_security/core.py:413 +#: flask_security/core.py:408 msgid "Unified sign in setup successful" msgstr "" -#: flask_security/core.py:414 +#: flask_security/core.py:409 msgid "You must specify a valid identity to sign in" msgstr "" -#: flask_security/core.py:415 +#: flask_security/core.py:410 #, python-format msgid "Use this code to sign in: %(code)s." msgstr "" -#: flask_security/forms.py:50 +#: flask_security/forms.py:53 msgid "Email Address" msgstr "Adresse email" -#: flask_security/forms.py:51 +#: flask_security/forms.py:54 msgid "Password" msgstr "Mot de passe" -#: flask_security/forms.py:52 +#: flask_security/forms.py:55 msgid "Remember Me" msgstr "Se souvenir de moi" -#: flask_security/forms.py:53 flask_security/templates/security/_menu.html:5 +#: flask_security/forms.py:56 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/forms.py:57 #: 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/forms.py:58 flask_security/templates/security/_menu.html:11 #: flask_security/templates/security/register_user.html:6 msgid "Register" msgstr "Inscription" -#: flask_security/forms.py:56 +#: flask_security/forms.py:59 msgid "Resend Confirmation Instructions" msgstr "Renvoyer les instructions de confirmation" -#: flask_security/forms.py:57 +#: flask_security/forms.py:60 msgid "Recover Password" msgstr "Récupérer le mot de passe" -#: flask_security/forms.py:58 +#: flask_security/forms.py:61 msgid "Reset Password" msgstr "Réinitialiser le mot de passe" -#: flask_security/forms.py:59 +#: flask_security/forms.py:62 msgid "Retype Password" msgstr "Confirmer le mot de passe" -#: flask_security/forms.py:60 +#: flask_security/forms.py:63 msgid "New Password" msgstr "Nouveau mot de passe" -#: flask_security/forms.py:61 +#: flask_security/forms.py:64 msgid "Change Password" msgstr "Changer le mot de passe" -#: flask_security/forms.py:62 +#: flask_security/forms.py:65 msgid "Send Login Link" msgstr "Envoyer le lien de connexion" -#: flask_security/forms.py:63 +#: flask_security/forms.py:66 msgid "Verify Password" msgstr "" -#: flask_security/forms.py:64 +#: flask_security/forms.py:67 msgid "Change Method" msgstr "" -#: flask_security/forms.py:65 +#: flask_security/forms.py:68 msgid "Phone Number" msgstr "" -#: flask_security/forms.py:66 +#: flask_security/forms.py:69 msgid "Authentication Code" msgstr "" -#: flask_security/forms.py:67 +#: flask_security/forms.py:70 msgid "Submit" msgstr "" -#: flask_security/forms.py:68 +#: flask_security/forms.py:71 msgid "Submit Code" msgstr "" -#: flask_security/forms.py:69 +#: flask_security/forms.py:72 msgid "Error(s)" msgstr "" -#: flask_security/forms.py:70 +#: flask_security/forms.py:73 msgid "Identity" msgstr "" -#: flask_security/forms.py:71 +#: flask_security/forms.py:74 msgid "Send Code" msgstr "" -#: flask_security/forms.py:72 -#, fuzzy +#: flask_security/forms.py:75 msgid "Passcode" -msgstr "Mot de passe" +msgstr "" -#: flask_security/unified_signin.py:145 -#, fuzzy +#: flask_security/unified_signin.py:131 msgid "Code or Password" -msgstr "Récupérer le mot de passe" +msgstr "" -#: flask_security/unified_signin.py:150 flask_security/unified_signin.py:270 +#: flask_security/unified_signin.py:136 flask_security/unified_signin.py:262 msgid "Available Methods" msgstr "" -#: flask_security/unified_signin.py:151 +#: flask_security/unified_signin.py:137 msgid "Via email" msgstr "" -#: flask_security/unified_signin.py:151 +#: flask_security/unified_signin.py:137 msgid "Via SMS" msgstr "" -#: flask_security/unified_signin.py:272 +#: flask_security/unified_signin.py:264 msgid "Set up using email" msgstr "" -#: flask_security/unified_signin.py:275 +#: flask_security/unified_signin.py:267 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" -#: flask_security/unified_signin.py:277 +#: flask_security/unified_signin.py:269 msgid "Set up using SMS" msgstr "" @@ -484,32 +485,31 @@ msgid "Resend confirmation instructions" msgstr "Renvoyer les instructions de confirmation" -#: flask_security/templates/security/two_factor_setup.html:6 +#: flask_security/templates/security/two_factor_setup.html:31 msgid "Two-factor authentication adds an extra layer of security to your account" msgstr "" -#: flask_security/templates/security/two_factor_setup.html:7 +#: flask_security/templates/security/two_factor_setup.html:32 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 +#: flask_security/templates/security/two_factor_setup.html:43 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 +#: flask_security/templates/security/two_factor_setup.html:49 msgid "" -"Open your authenticator app on your device and scan the following qrcode " -"to start receiving codes:" +"Open an authenticator app on your device and scan the following QRcode " +"(or enter the code below manually) to start receiving codes:" msgstr "" -#: flask_security/templates/security/two_factor_setup.html:22 +#: flask_security/templates/security/two_factor_setup.html:52 msgid "Two factor authentication code" msgstr "" -#: flask_security/templates/security/two_factor_setup.html:25 +#: flask_security/templates/security/two_factor_setup.html:60 msgid "To Which Phone Number Should We Send Code To?" msgstr "" @@ -529,27 +529,27 @@ 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 +#: flask_security/templates/security/us_setup.html:36 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_setup.html:58 #: 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é" +msgstr "" + +#: flask_security/templates/security/us_setup.html:66 +msgid "" +"Open an authenticator app on your device and scan the following QRcode " +"(or enter the code below manually) to start receiving passcodes:" +msgstr "" + +#: flask_security/templates/security/us_setup.html:69 +msgid "Passwordless QRCode" +msgstr "" -#: flask_security/templates/security/us_setup.html:29 +#: flask_security/templates/security/us_setup.html:77 msgid "No methods have been enabled - nothing to setup" msgstr "" @@ -563,6 +563,10 @@ msgid "Please re-authenticate" msgstr "Merci de vous reconnecter pour accéder à cette page." +#: flask_security/templates/security/verify.html:6 +msgid "Please Enter Your Password" +msgstr "" + #: flask_security/templates/security/email/change_notice.html:1 msgid "Your password has been changed." msgstr "Votre mot de passe a été changé." @@ -575,7 +579,12 @@ msgid "click here to reset it" msgstr "cliquez ici pour le réinitialiser" +#: flask_security/templates/security/email/change_notice.txt:3 +msgid "If you did not change your password, click the link below to reset it." +msgstr "" + #: flask_security/templates/security/email/confirmation_instructions.html:1 +#: flask_security/templates/security/email/confirmation_instructions.txt:1 msgid "Please confirm your email through the link below:" msgstr "Merci de confirmer votre adresse email via le lien ci-dessous :" @@ -585,12 +594,15 @@ msgstr "Confirmer mon compte" #: flask_security/templates/security/email/login_instructions.html:1 +#: flask_security/templates/security/email/login_instructions.txt:1 #: flask_security/templates/security/email/welcome.html:1 +#: flask_security/templates/security/email/welcome.txt:1 #, python-format msgid "Welcome %(email)s!" msgstr "Bienvenue %(email)s !" #: flask_security/templates/security/email/login_instructions.html:3 +#: flask_security/templates/security/email/login_instructions.txt:3 msgid "You can log into your account through the link below:" msgstr "Vous pouvez vous connecter via le lien ci-dessous :" @@ -602,29 +614,48 @@ msgid "Click here to reset your password" msgstr "Cliquez pour réinitialiser votre mot de passe" +#: flask_security/templates/security/email/reset_instructions.txt:1 +msgid "Click the link below to reset your password:" +msgstr "" + #: flask_security/templates/security/email/two_factor_instructions.html:3 +#: flask_security/templates/security/email/two_factor_instructions.txt:3 msgid "You can log into your account using the following code:" msgstr "" #: flask_security/templates/security/email/two_factor_rescue.html:1 +#: flask_security/templates/security/email/two_factor_rescue.txt:1 msgid "can not access mail account" msgstr "" #: flask_security/templates/security/email/us_instructions.html:3 -#, fuzzy +#: flask_security/templates/security/email/us_instructions.txt:3 msgid "You can sign into your account using the following code:" -msgstr "Vous pouvez vous connecter via le lien ci-dessous :" +msgstr "" #: 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 :" + +#: flask_security/templates/security/email/us_instructions.txt:7 +msgid "Or use the link below:" +msgstr "" #: flask_security/templates/security/email/welcome.html:4 +#: flask_security/templates/security/email/welcome.txt:4 msgid "You can confirm your email through the link below:" msgstr "" "Vous pouvez confirmer votre votre adresse email via le lien ci-" "dessous :" +#~ msgid "You successfully confirmed password" +#~ msgstr "" + +#~ msgid "Password confirmation is needed in order to access page" +#~ msgstr "" + +#~ msgid "" +#~ "Open your authenticator app on your " +#~ "device and scan the following qrcode " +#~ "to start receiving codes:" +#~ msgstr "" Binary files /tmp/tmpl_LU2e/ak23Qk5OdH/flask-security-3.4.2/flask_security/translations/hy_AM/LC_MESSAGES/flask_security.mo and /tmp/tmpl_LU2e/U10RHAXkq7/flask-security-4.0.0/flask_security/translations/hy_AM/LC_MESSAGES/flask_security.mo differ diff -Nru flask-security-3.4.2/flask_security/translations/hy_AM/LC_MESSAGES/flask_security.po flask-security-4.0.0/flask_security/translations/hy_AM/LC_MESSAGES/flask_security.po --- flask-security-3.4.2/flask_security/translations/hy_AM/LC_MESSAGES/flask_security.po 1970-01-01 00:00:00.000000000 +0000 +++ flask-security-4.0.0/flask_security/translations/hy_AM/LC_MESSAGES/flask_security.po 2021-01-26 02:39:51.000000000 +0000 @@ -0,0 +1,661 @@ +# Armenian (Armenia) translations for Flask-Security. +# Copyright (C) 2020 ORGANIZATION +# This file is distributed under the same license as the Flask-Security +# project. +# FIRST AUTHOR , 2020. +# +msgid "" +msgstr "" +"Project-Id-Version: Flask-Security 4.0.0\n" +"Report-Msgid-Bugs-To: jwag956@github.com\n" +"POT-Creation-Date: 2021-01-22 14:48-0800\n" +"PO-Revision-Date: 2020-12-01 11:47+0400\n" +"Last-Translator: FULL NAME \n" +"Language: hy_AM\n" +"Language-Team: hy_AM \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.9.0\n" + +#: flask_security/core.py:209 +msgid "Login Required" +msgstr "Անհրաժեշտ է մուտք գործել" + +#: flask_security/core.py:210 +#: flask_security/templates/security/email/two_factor_instructions.html:1 +#: flask_security/templates/security/email/two_factor_instructions.txt:1 +#: flask_security/templates/security/email/us_instructions.html:1 +#: flask_security/templates/security/email/us_instructions.txt:1 +#: tests/templates/_nav.html:2 +msgid "Welcome" +msgstr "Բարի գալուստ" + +#: flask_security/core.py:211 +msgid "Please confirm your email" +msgstr "Հաստատեք Ձեր էլ․փոստի հասցեն" + +#: flask_security/core.py:212 +msgid "Login instructions" +msgstr "Մուտքի հրահանգներ" + +#: flask_security/core.py:213 +#: flask_security/templates/security/email/reset_notice.html:1 +#: flask_security/templates/security/email/reset_notice.txt:1 +msgid "Your password has been reset" +msgstr "Ձեր գաղտնաբառը վերականգնվել է" + +#: flask_security/core.py:214 +#: flask_security/templates/security/email/change_notice.txt:1 +msgid "Your password has been changed" +msgstr "Ձեր գաղտնաբառը փոխվեց" + +#: flask_security/core.py:215 +msgid "Password reset instructions" +msgstr "Գաղտնաբառի վերականգնման հրահանգներ" + +#: flask_security/core.py:218 +msgid "Two-factor Login" +msgstr "Երկու գործոնով մուտք" + +#: flask_security/core.py:219 +msgid "Two-factor Rescue" +msgstr "Երկու գործոնով վերականգնում" + +#: flask_security/core.py:263 +msgid "Verification Code" +msgstr "Վերահաստատման ծածկագիր" + +#: flask_security/core.py:278 +msgid "Input not appropriate for requested API" +msgstr "Մուտքը չի համապատասխանում հայցվող API֊ին" + +#: flask_security/core.py:279 +msgid "You do not have permission to view this resource." +msgstr "Դուք այս ռեսուրսը դիտելու թույլտվություն չունեք" + +#: flask_security/core.py:281 +msgid "You are not authenticated. Please supply the correct credentials." +msgstr "Դուք նույնականացված չեք: Խնդրում ենք մուտքագրել ճիշտ տվյալներ։" + +#: flask_security/core.py:285 +msgid "You must re-authenticate to access this endpoint" +msgstr "Դուք պետք է կրկին նույնականցվեք որպեսզի այստեղ մուտք գործեք" + +#: flask_security/core.py:289 +#, python-format +msgid "Thank you. Confirmation instructions have been sent to %(email)s." +msgstr "Շնորհակալություն. Հաստատման հրահանգներն ուղարկվել են %(email)s հասցեին։" + +#: flask_security/core.py:292 +msgid "Thank you. Your email has been confirmed." +msgstr "Շնորհակալություն. Ձեր էլ․փոստի հասցեն հաստատվել է։" + +#: flask_security/core.py:293 +msgid "Your email has already been confirmed." +msgstr "Ձեր էլ․փոստի հասցեն արդեն հաստատված է։" + +#: flask_security/core.py:294 +msgid "Invalid confirmation token." +msgstr "Հաստատման տոկենը անվավեր է։" + +#: flask_security/core.py:296 +#, python-format +msgid "%(email)s is already associated with an account." +msgstr "%(email)s արդեն կապված է այլ օգտահաշվի հետ։" + +#: flask_security/core.py:300 +#, python-format +msgid "" +"Identity attribute '%(attr)s' with value '%(value)s' is already " +"associated with an account." +msgstr "" + +#: flask_security/core.py:306 +msgid "Password does not match" +msgstr "Գաղտնաբառը չի համապատասխանում" + +#: flask_security/core.py:307 +msgid "Passwords do not match" +msgstr "Գաղտնաբառերը չեն համապատասխանում" + +#: flask_security/core.py:308 +msgid "Redirections outside the domain are forbidden" +msgstr "Դոմենից դուրս վերահղումներն արգելափակված են" + +#: flask_security/core.py:310 +#, python-format +msgid "Instructions to reset your password have been sent to %(email)s." +msgstr "Գաղտնաբառի վերականգնման հրահանգներն ուղարկվել են %(email)s հասցեին։" + +#: flask_security/core.py:314 +#, 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:320 +msgid "Invalid reset password token." +msgstr "Գաղտնաբառի վերականգնման տոկենը անվավեր է։" + +#: flask_security/core.py:321 +msgid "Email requires confirmation." +msgstr "Էլ․փոստի հասցեն պետք է հաստատել։" + +#: flask_security/core.py:323 +#, python-format +msgid "Confirmation instructions have been sent to %(email)s." +msgstr "Հաստատման հրահանգները ուղարկվել են %(email)s հասցեին։" + +#: flask_security/core.py:327 +#, 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:335 +#, 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:342 +#, python-format +msgid "Instructions to login have been sent to %(email)s." +msgstr "Մուտքի հրահանգները ուղարկվել են %(email)s հասցեին։" + +#: flask_security/core.py:345 +msgid "Invalid login token." +msgstr "Մուտքի անվավեր տոկեն։" + +#: flask_security/core.py:346 +msgid "Account is disabled." +msgstr "Օգտահաշիվն արգելափակված է։" + +#: flask_security/core.py:347 +msgid "Email not provided" +msgstr "Էլ․փոստի հասցեն տրամադրված չէ" + +#: flask_security/core.py:348 +msgid "Invalid email address" +msgstr "Անվավեր էլ․փոստի հասցե" + +#: flask_security/core.py:349 +msgid "Invalid code" +msgstr "Անվավեր ծածկագիր" + +#: flask_security/core.py:350 +msgid "Password not provided" +msgstr "Գաղտնաբառը տրամադրված չէ" + +#: flask_security/core.py:351 +msgid "No password is set for this user" +msgstr "Այս օգտատերը գաղնաբառ չունի" + +#: flask_security/core.py:353 +#, python-format +msgid "Password must be at least %(length)s characters" +msgstr "Գաղտնաբառը պետք է պարունակի առնվազն %(length)s նիշ" + +#: flask_security/core.py:356 +msgid "Password not complex enough" +msgstr "Գաղտնաբառը բավարար բարդ չէ" + +#: flask_security/core.py:357 +msgid "Password on breached list" +msgstr "Գաղտնաբառը բացված գաղտնաբառերի ցուցակում է" + +#: flask_security/core.py:359 +msgid "Failed to contact breached passwords site" +msgstr "Չհաջողվեց կապվել բացված գաղտնաբառերի կայքի հետ" + +#: flask_security/core.py:362 +msgid "Phone number not valid e.g. missing country code" +msgstr "Հեռախոսահամարը անվավեր է, օրինակ՝ բացակայում է երկրի կոդը" + +#: flask_security/core.py:363 +msgid "Specified user does not exist" +msgstr "Նշված օգտատերը գոյություն չունի" + +#: flask_security/core.py:364 +msgid "Invalid password" +msgstr "Անվավեր գաղտնաբառ" + +#: flask_security/core.py:365 +msgid "Password or code submitted is not valid" +msgstr "Ներկայացված գաղտնաբառը կամ ծածկագիրը վավեր չէ" + +#: flask_security/core.py:366 +msgid "You have successfully logged in." +msgstr "Դուք բարեհաջող մուտք էք գործել։" + +#: flask_security/core.py:367 +msgid "Forgot password?" +msgstr "Մոռացել ե՞ք գաղտնաբառը" + +#: flask_security/core.py:369 +msgid "" +"You successfully reset your password and you have been logged in " +"automatically." +msgstr "" +"Դուք հաջողությամբ վերականգնել եք ձեր գաղտնաբառը եւ մուտք էք գործել " +"համակարգ ինքնաբերաբար։" + +#: flask_security/core.py:376 +msgid "Your new password must be different than your previous password." +msgstr "Ձեր նոր գաղտնաբառը պետք է տարբերվի նախկինից" + +#: flask_security/core.py:379 +msgid "You successfully changed your password." +msgstr "Դուք հաջողությամբ փոխեցիք ձեր գաղտնաբառը։" + +#: flask_security/core.py:380 +msgid "Please log in to access this page." +msgstr "Մուտք գործեք այս էջից օգտվելու համար։" + +#: flask_security/core.py:381 +msgid "Please reauthenticate to access this page." +msgstr "Այս էջ մուտք գործելու համար անհրաժեշտ է նորից նույնականացվել:" + +#: flask_security/core.py:382 +msgid "Reauthentication successful" +msgstr "Նույնականացումը հաջողված է" + +#: flask_security/core.py:384 +msgid "You can only access this endpoint when not logged in." +msgstr "Դուք կարող եք մտնել այս վերջնակետ միայն այն ժամանակ, երբ մուտք չեք գործել" + +#: flask_security/core.py:387 +msgid "Failed to send code. Please try again later" +msgstr "Չհաջողվեց ուղարկել ծածկագիրը: Խնդրում ենք փորձել ավելի ուշ" + +#: flask_security/core.py:388 +msgid "Invalid Token" +msgstr "Անվավեր թոքեն" + +#: flask_security/core.py:389 +msgid "Your token has been confirmed" +msgstr "Ձեր թոքենը հաստատված է" + +#: flask_security/core.py:391 +msgid "You successfully changed your two-factor method." +msgstr "Դուք հաջողությամբ փոխեցիք ձեր երկու֊գործոն տարբերակը" + +#: flask_security/core.py:395 +msgid "You currently do not have permissions to access this page" +msgstr "Ներկայումս դուք չունեք այս էջ մուտք գործելու թույլտվություն" + +#: flask_security/core.py:398 +msgid "Marked method is not valid" +msgstr "Նշված տարբերակը վավեր չէ" + +#: flask_security/core.py:400 +msgid "You successfully disabled two factor authorization." +msgstr "Դուք հաջողությամբ անջատել եք երկու գործոնի թույլտվությունը" + +#: flask_security/core.py:403 +msgid "Requested method is not valid" +msgstr "Հայցվող մեթոդը վավեր չէ" + +#: flask_security/core.py:405 +#, python-format +msgid "Setup must be completed within %(within)s. Please start over." +msgstr "Կարգավորումը պետք է ավարտվի %(within)s ընթացքում։ Խնդրում ենք նորից սկսել։" + +#: flask_security/core.py:408 +msgid "Unified sign in setup successful" +msgstr "Միասնական մուտքի տեղադրումը հաջող է" + +#: flask_security/core.py:409 +msgid "You must specify a valid identity to sign in" +msgstr "Մուտք գործելու համար պետք է նշեք վավեր ինքնություն" + +#: flask_security/core.py:410 +#, python-format +msgid "Use this code to sign in: %(code)s." +msgstr "Մուտքի համար օգտագործեք այս ծածկագիրը․ %(code)s։" + +#: flask_security/forms.py:53 +msgid "Email Address" +msgstr "Էլեկտրոնային փոստի հասցե" + +#: flask_security/forms.py:54 +msgid "Password" +msgstr "Գաղտնաբառ" + +#: flask_security/forms.py:55 +msgid "Remember Me" +msgstr "Հիշիր ինձ" + +#: flask_security/forms.py:56 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:57 +#: 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:58 flask_security/templates/security/_menu.html:11 +#: flask_security/templates/security/register_user.html:6 +msgid "Register" +msgstr "Գրանցվել" + +#: flask_security/forms.py:59 +msgid "Resend Confirmation Instructions" +msgstr "Վերստին հաստատեք ցուցումները" + +#: flask_security/forms.py:60 +msgid "Recover Password" +msgstr "Վերականգնել գաղտնաբառը" + +#: flask_security/forms.py:61 +msgid "Reset Password" +msgstr "Զրոյացնել գաղտնաբառը" + +#: flask_security/forms.py:62 +msgid "Retype Password" +msgstr "Կրկին մուտքագրել գաղտնաբառը" + +#: flask_security/forms.py:63 +msgid "New Password" +msgstr "Նոր գաղտնաբառ" + +#: flask_security/forms.py:64 +msgid "Change Password" +msgstr "Փոխել գաղտնաբառը" + +#: flask_security/forms.py:65 +msgid "Send Login Link" +msgstr "Ուղարկել մուտքի հղումը" + +#: flask_security/forms.py:66 +msgid "Verify Password" +msgstr "Ստուգեք գաղտնաբառը" + +#: flask_security/forms.py:67 +msgid "Change Method" +msgstr "Փոխել տարբերակը" + +#: flask_security/forms.py:68 +msgid "Phone Number" +msgstr "Հեռախոսահամար" + +#: flask_security/forms.py:69 +msgid "Authentication Code" +msgstr "Նույնականացման ծածկագիր" + +#: flask_security/forms.py:70 +msgid "Submit" +msgstr "Ներկայացնել" + +#: flask_security/forms.py:71 +msgid "Submit Code" +msgstr "Ներկայացնել ծածկագիր" + +#: flask_security/forms.py:72 +msgid "Error(s)" +msgstr "Սխալ" + +#: flask_security/forms.py:73 +msgid "Identity" +msgstr "Ինքնություն" + +#: flask_security/forms.py:74 +msgid "Send Code" +msgstr "Ուղարկել ծածկագիր" + +#: flask_security/forms.py:75 +msgid "Passcode" +msgstr "Գաղտնագիր" + +#: flask_security/unified_signin.py:131 +msgid "Code or Password" +msgstr "Ծածկագիր կամ Գաղտնաբառ" + +#: flask_security/unified_signin.py:136 flask_security/unified_signin.py:262 +msgid "Available Methods" +msgstr "Առկա տարբերակներ" + +#: flask_security/unified_signin.py:137 +msgid "Via email" +msgstr "Էլ․ Փոստով" + +#: flask_security/unified_signin.py:137 +msgid "Via SMS" +msgstr "SMS հաղորդագրությամբ" + +#: flask_security/unified_signin.py:264 +msgid "Set up using email" +msgstr "Կարգավորեք էլ․փոստի միջոցով" + +#: flask_security/unified_signin.py:267 +msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" +msgstr "" +"Կարգավորեք օգտագործելով վավերացման ծրագիր (ինչպիսիք են՝ google, lastpass," +" authy)" + +#: flask_security/unified_signin.py:269 +msgid "Set up using SMS" +msgstr "Կարգավորեք օգտագործելով SMS" + +#: 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:31 +msgid "Two-factor authentication adds an extra layer of security to your account" +msgstr "" +"Երկու գործոնով նույնականացումը ձեր հաշվին ավելացնում է անվտանգության " +"լրացուցիչ շերտ" + +#: flask_security/templates/security/two_factor_setup.html:32 +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:43 +msgid "To complete logging in, please enter the code sent to your mail" +msgstr "" +"Մուտքն ավարտելու համար մուտքագրեք ձեր էլեկտրոնային հասցեին ուղարկված " +"ծածկագիրը" + +#: flask_security/templates/security/two_factor_setup.html:49 +msgid "" +"Open an authenticator app on your device and scan the following QRcode " +"(or enter the code below manually) to start receiving codes:" +msgstr "" + +#: flask_security/templates/security/two_factor_setup.html:52 +msgid "Two factor authentication code" +msgstr "Երկու գործոն նույնականացման ծածկագիր" + +#: flask_security/templates/security/two_factor_setup.html:60 +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/us_setup.html:36 +msgid "Setup Unified Sign In options" +msgstr "Կարգավորել Միասնական Մուտք գործելու տարբերակները" + +#: flask_security/templates/security/us_setup.html:58 +#: 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:66 +msgid "" +"Open an authenticator app on your device and scan the following QRcode " +"(or enter the code below manually) to start receiving passcodes:" +msgstr "" + +#: flask_security/templates/security/us_setup.html:69 +msgid "Passwordless QRCode" +msgstr "Առանց գաղտնաբառի QRcode" + +#: flask_security/templates/security/us_setup.html:77 +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/verify.html:6 +msgid "Please Enter Your Password" +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/change_notice.txt:3 +msgid "If you did not change your password, click the link below to reset it." +msgstr "Եթե դուք չեք փոխել Ձեր գաղտնաբառը, սեղմեք հղումին, որպեսզի փոխեք այն" + +#: flask_security/templates/security/email/confirmation_instructions.html:1 +#: flask_security/templates/security/email/confirmation_instructions.txt: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/login_instructions.txt:1 +#: flask_security/templates/security/email/welcome.html:1 +#: flask_security/templates/security/email/welcome.txt:1 +#, python-format +msgid "Welcome %(email)s!" +msgstr "Բարի գալուստ %(email)s!" + +#: flask_security/templates/security/email/login_instructions.html:3 +#: flask_security/templates/security/email/login_instructions.txt: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/reset_instructions.txt:1 +msgid "Click the link below to reset your password:" +msgstr "Ձեր գաղտնաբառը վերականգնելու համար սեղմեք սեղմեք հղումին" + +#: flask_security/templates/security/email/two_factor_instructions.html:3 +#: flask_security/templates/security/email/two_factor_instructions.txt:3 +msgid "You can log into your account using the following code:" +msgstr "Դուք կարող եք մտնել Ձեր օգտահաշիվ՝ օգտագործելով հետևյալ ծածկագիրը." + +#: flask_security/templates/security/email/two_factor_rescue.html:1 +#: flask_security/templates/security/email/two_factor_rescue.txt:1 +msgid "can not access mail account" +msgstr "Օգտահաշիվ հնարավոր չէ մուտք գործել" + +#: flask_security/templates/security/email/us_instructions.html:3 +#: flask_security/templates/security/email/us_instructions.txt: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/us_instructions.txt:7 +msgid "Or use the link below:" +msgstr "Կամ օգտագործեք ստորեւ նշված հղումը" + +#: flask_security/templates/security/email/welcome.html:4 +#: flask_security/templates/security/email/welcome.txt:4 +msgid "You can confirm your email through the link below:" +msgstr "Դուք կարող եք հաստատել ձեր էլ. փոստի հասցեն ստորև նշված հղումով." + +#~ msgid "" +#~ "Open your authenticator app on your " +#~ "device and scan the following qrcode " +#~ "to start receiving codes:" +#~ msgstr "" +#~ "Բացեք ձեր վավերականացման ծրագիրը ձեր " +#~ "սարքում և սկանավորեք հետևյալ QRcode-ը " +#~ "ծածկագրեր ստանալու համար:" Binary files /tmp/tmpl_LU2e/ak23Qk5OdH/flask-security-3.4.2/flask_security/translations/ja_JP/LC_MESSAGES/flask_security.mo and /tmp/tmpl_LU2e/U10RHAXkq7/flask-security-4.0.0/flask_security/translations/ja_JP/LC_MESSAGES/flask_security.mo differ diff -Nru flask-security-3.4.2/flask_security/translations/ja_JP/LC_MESSAGES/flask_security.po flask-security-4.0.0/flask_security/translations/ja_JP/LC_MESSAGES/flask_security.po --- flask-security-3.4.2/flask_security/translations/ja_JP/LC_MESSAGES/flask_security.po 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/translations/ja_JP/LC_MESSAGES/flask_security.po 2021-01-26 02:39:51.000000000 +0000 @@ -8,7 +8,7 @@ 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" +"POT-Creation-Date: 2021-01-22 14:48-0800\n" "PO-Revision-Date: 2018-01-25 14:12+0900\n" "Last-Translator: \n" "Language: ja\n" @@ -17,428 +17,429 @@ "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" +"Generated-By: Babel 2.9.0\n" -#: flask_security/core.py:207 +#: flask_security/core.py:209 msgid "Login Required" msgstr "ログインが必要です" -#: flask_security/core.py:208 +#: flask_security/core.py:210 #: flask_security/templates/security/email/two_factor_instructions.html:1 +#: flask_security/templates/security/email/two_factor_instructions.txt:1 #: flask_security/templates/security/email/us_instructions.html:1 +#: flask_security/templates/security/email/us_instructions.txt:1 +#: tests/templates/_nav.html:2 msgid "Welcome" msgstr "ようこそ" -#: flask_security/core.py:209 +#: flask_security/core.py:211 msgid "Please confirm your email" msgstr "メール アドレスの検証" -#: flask_security/core.py:210 +#: flask_security/core.py:212 msgid "Login instructions" msgstr "ログイン手順" -#: flask_security/core.py:211 +#: flask_security/core.py:213 #: flask_security/templates/security/email/reset_notice.html:1 +#: flask_security/templates/security/email/reset_notice.txt:1 msgid "Your password has been reset" msgstr "パスワード変更" -#: flask_security/core.py:212 +#: flask_security/core.py:214 +#: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed" msgstr "パスワードが変更されました。" -#: flask_security/core.py:213 +#: flask_security/core.py:215 msgid "Password reset instructions" msgstr "パスワード再設定手順" -#: flask_security/core.py:216 +#: flask_security/core.py:218 msgid "Two-factor Login" msgstr "" -#: flask_security/core.py:217 +#: flask_security/core.py:219 msgid "Two-factor Rescue" msgstr "" -#: flask_security/core.py:266 +#: flask_security/core.py:263 msgid "Verification Code" msgstr "" -#: flask_security/core.py:282 +#: flask_security/core.py:278 msgid "Input not appropriate for requested API" msgstr "" -#: flask_security/core.py:283 +#: flask_security/core.py:279 msgid "You do not have permission to view this resource." msgstr "アクセス権がありません" -#: flask_security/core.py:285 +#: flask_security/core.py:281 msgid "You are not authenticated. Please supply the correct credentials." msgstr "" -#: flask_security/core.py:289 +#: flask_security/core.py:285 #, fuzzy msgid "You must re-authenticate to access this endpoint" msgstr "再度ログインしてください" -#: flask_security/core.py:293 +#: flask_security/core.py:289 #, python-format msgid "Thank you. Confirmation instructions have been sent to %(email)s." msgstr "ご登録ありがとうございます。%(email)sにメール アドレス検証手順が送信されました。" -#: flask_security/core.py:296 +#: flask_security/core.py:292 msgid "Thank you. Your email has been confirmed." msgstr "ありがとうございます。メール アドレスが検証されました。" -#: flask_security/core.py:297 +#: flask_security/core.py:293 msgid "Your email has already been confirmed." msgstr "メール アドレスは検証済みです" -#: flask_security/core.py:298 +#: flask_security/core.py:294 msgid "Invalid confirmation token." msgstr "リンクが無効です" -#: flask_security/core.py:300 +#: flask_security/core.py:296 #, python-format msgid "%(email)s is already associated with an account." msgstr "%(email)s のアカウントは既に作成されています" -#: flask_security/core.py:303 +#: flask_security/core.py:300 +#, python-format +msgid "" +"Identity attribute '%(attr)s' with value '%(value)s' is already " +"associated with an account." +msgstr "" + +#: flask_security/core.py:306 msgid "Password does not match" msgstr "パスワードが一致しません" -#: flask_security/core.py:304 +#: flask_security/core.py:307 msgid "Passwords do not match" msgstr "入力したパスワードが一致していません" -#: flask_security/core.py:305 +#: flask_security/core.py:308 msgid "Redirections outside the domain are forbidden" msgstr "ドメイン外へのリダイレクトは禁止されています" -#: flask_security/core.py:307 +#: flask_security/core.py:310 #, python-format msgid "Instructions to reset your password have been sent to %(email)s." msgstr "パスワードの再設定手順が %(email)s に送信されました" -#: flask_security/core.py:311 +#: flask_security/core.py:314 #, 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 +#: flask_security/core.py:320 msgid "Invalid reset password token." msgstr "リンクが無効です" -#: flask_security/core.py:318 +#: flask_security/core.py:321 msgid "Email requires confirmation." msgstr "メール アドレスの検証が必要です" -#: flask_security/core.py:320 +#: flask_security/core.py:323 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "%(email)sにメール アドレス検証手順が再送信されました" -#: flask_security/core.py:324 +#: flask_security/core.py:327 #, 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 +#: flask_security/core.py:335 #, 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 +#: flask_security/core.py:342 #, python-format msgid "Instructions to login have been sent to %(email)s." msgstr "%(email)sにログイン手順が送信されました" -#: flask_security/core.py:342 +#: flask_security/core.py:345 msgid "Invalid login token." msgstr "リンクが無効です" -#: flask_security/core.py:343 +#: flask_security/core.py:346 msgid "Account is disabled." msgstr "アカウントが無効になっています" -#: flask_security/core.py:344 +#: flask_security/core.py:347 msgid "Email not provided" msgstr "メール アドレスを入力してください" -#: flask_security/core.py:345 +#: flask_security/core.py:348 msgid "Invalid email address" msgstr "正しいメール アドレスを入力してください" -#: flask_security/core.py:346 -#, fuzzy +#: flask_security/core.py:349 msgid "Invalid code" -msgstr "入力を確認してください" +msgstr "" -#: flask_security/core.py:347 +#: flask_security/core.py:350 msgid "Password not provided" msgstr "パスワードを入力してください" -#: flask_security/core.py:348 +#: flask_security/core.py:351 msgid "No password is set for this user" msgstr "パスワードが設定されていません" -#: flask_security/core.py:350 -#, fuzzy, python-format +#: flask_security/core.py:353 +#, python-format msgid "Password must be at least %(length)s characters" -msgstr "パスワードは6文字以上でなければなりません" +msgstr "" -#: flask_security/core.py:353 +#: flask_security/core.py:356 msgid "Password not complex enough" msgstr "" -#: flask_security/core.py:354 +#: flask_security/core.py:357 msgid "Password on breached list" msgstr "" -#: flask_security/core.py:356 +#: flask_security/core.py:359 msgid "Failed to contact breached passwords site" msgstr "" -#: flask_security/core.py:359 +#: flask_security/core.py:362 msgid "Phone number not valid e.g. missing country code" msgstr "" -#: flask_security/core.py:360 +#: flask_security/core.py:363 msgid "Specified user does not exist" msgstr "入力を確認してください" -#: flask_security/core.py:361 +#: flask_security/core.py:364 msgid "Invalid password" msgstr "入力を確認してください" -#: flask_security/core.py:362 +#: flask_security/core.py:365 msgid "Password or code submitted is not valid" msgstr "" -#: flask_security/core.py:363 +#: flask_security/core.py:366 msgid "You have successfully logged in." msgstr "ログインしました" -#: flask_security/core.py:364 +#: flask_security/core.py:367 msgid "Forgot password?" msgstr "パスワードを忘れた場合" -#: flask_security/core.py:366 +#: flask_security/core.py:369 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "パスワードの再設定が完了しました。" -#: flask_security/core.py:373 +#: flask_security/core.py:376 msgid "Your new password must be different than your previous password." msgstr "新旧パスワードが同じです" -#: flask_security/core.py:376 +#: flask_security/core.py:379 msgid "You successfully changed your password." msgstr "パスワードが変更されました" -#: flask_security/core.py:377 +#: flask_security/core.py:380 msgid "Please log in to access this page." msgstr "ログインしてください" -#: flask_security/core.py:378 +#: flask_security/core.py:381 msgid "Please reauthenticate to access this page." msgstr "再度ログインしてください" -#: flask_security/core.py:379 +#: flask_security/core.py:382 msgid "Reauthentication successful" msgstr "" -#: flask_security/core.py:381 +#: flask_security/core.py:384 msgid "You can only access this endpoint when not logged in." msgstr "" -#: flask_security/core.py:384 +#: flask_security/core.py:387 msgid "Failed to send code. Please try again later" msgstr "" -#: flask_security/core.py:385 +#: flask_security/core.py:388 msgid "Invalid Token" msgstr "" -#: flask_security/core.py:386 +#: flask_security/core.py:389 msgid "Your token has been confirmed" msgstr "" -#: flask_security/core.py:388 +#: flask_security/core.py:391 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 +#: flask_security/core.py:395 msgid "You currently do not have permissions to access this page" msgstr "" -#: flask_security/core.py:403 +#: flask_security/core.py:398 msgid "Marked method is not valid" msgstr "" -#: flask_security/core.py:405 +#: flask_security/core.py:400 msgid "You successfully disabled two factor authorization." msgstr "" -#: flask_security/core.py:408 +#: flask_security/core.py:403 msgid "Requested method is not valid" msgstr "" -#: flask_security/core.py:410 +#: flask_security/core.py:405 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "" -#: flask_security/core.py:413 +#: flask_security/core.py:408 msgid "Unified sign in setup successful" msgstr "" -#: flask_security/core.py:414 +#: flask_security/core.py:409 msgid "You must specify a valid identity to sign in" msgstr "" -#: flask_security/core.py:415 +#: flask_security/core.py:410 #, python-format msgid "Use this code to sign in: %(code)s." msgstr "" -#: flask_security/forms.py:50 +#: flask_security/forms.py:53 msgid "Email Address" msgstr "メール アドレス" -#: flask_security/forms.py:51 +#: flask_security/forms.py:54 msgid "Password" msgstr "パスワード" -#: flask_security/forms.py:52 +#: flask_security/forms.py:55 msgid "Remember Me" msgstr "次回以降ログインを省略する" -#: flask_security/forms.py:53 flask_security/templates/security/_menu.html:5 +#: flask_security/forms.py:56 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/forms.py:57 #: 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/forms.py:58 flask_security/templates/security/_menu.html:11 #: flask_security/templates/security/register_user.html:6 msgid "Register" msgstr "ユーザ登録" -#: flask_security/forms.py:56 +#: flask_security/forms.py:59 msgid "Resend Confirmation Instructions" msgstr "検証手順の再送信" -#: flask_security/forms.py:57 +#: flask_security/forms.py:60 msgid "Recover Password" msgstr "再設定手順を送信" -#: flask_security/forms.py:58 +#: flask_security/forms.py:61 msgid "Reset Password" msgstr "パスワード変更" -#: flask_security/forms.py:59 +#: flask_security/forms.py:62 msgid "Retype Password" msgstr "パスワード再入力" -#: flask_security/forms.py:60 +#: flask_security/forms.py:63 msgid "New Password" msgstr "新しいパスワード" -#: flask_security/forms.py:61 +#: flask_security/forms.py:64 msgid "Change Password" msgstr "変更" -#: flask_security/forms.py:62 +#: flask_security/forms.py:65 msgid "Send Login Link" msgstr "ログイン手順を送信" -#: flask_security/forms.py:63 +#: flask_security/forms.py:66 msgid "Verify Password" msgstr "" -#: flask_security/forms.py:64 +#: flask_security/forms.py:67 msgid "Change Method" msgstr "" -#: flask_security/forms.py:65 +#: flask_security/forms.py:68 msgid "Phone Number" msgstr "" -#: flask_security/forms.py:66 +#: flask_security/forms.py:69 msgid "Authentication Code" msgstr "" -#: flask_security/forms.py:67 +#: flask_security/forms.py:70 msgid "Submit" msgstr "" -#: flask_security/forms.py:68 +#: flask_security/forms.py:71 msgid "Submit Code" msgstr "" -#: flask_security/forms.py:69 +#: flask_security/forms.py:72 msgid "Error(s)" msgstr "" -#: flask_security/forms.py:70 +#: flask_security/forms.py:73 msgid "Identity" msgstr "" -#: flask_security/forms.py:71 +#: flask_security/forms.py:74 msgid "Send Code" msgstr "" -#: flask_security/forms.py:72 -#, fuzzy +#: flask_security/forms.py:75 msgid "Passcode" -msgstr "パスワード" +msgstr "" -#: flask_security/unified_signin.py:145 -#, fuzzy +#: flask_security/unified_signin.py:131 msgid "Code or Password" -msgstr "再設定手順を送信" +msgstr "" -#: flask_security/unified_signin.py:150 flask_security/unified_signin.py:270 +#: flask_security/unified_signin.py:136 flask_security/unified_signin.py:262 msgid "Available Methods" msgstr "" -#: flask_security/unified_signin.py:151 +#: flask_security/unified_signin.py:137 msgid "Via email" msgstr "" -#: flask_security/unified_signin.py:151 +#: flask_security/unified_signin.py:137 msgid "Via SMS" msgstr "" -#: flask_security/unified_signin.py:272 +#: flask_security/unified_signin.py:264 msgid "Set up using email" msgstr "" -#: flask_security/unified_signin.py:275 +#: flask_security/unified_signin.py:267 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" -#: flask_security/unified_signin.py:277 +#: flask_security/unified_signin.py:269 msgid "Set up using SMS" msgstr "" @@ -474,32 +475,31 @@ msgid "Resend confirmation instructions" msgstr "検証手順の再送信" -#: flask_security/templates/security/two_factor_setup.html:6 +#: flask_security/templates/security/two_factor_setup.html:31 msgid "Two-factor authentication adds an extra layer of security to your account" msgstr "" -#: flask_security/templates/security/two_factor_setup.html:7 +#: flask_security/templates/security/two_factor_setup.html:32 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 +#: flask_security/templates/security/two_factor_setup.html:43 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 +#: flask_security/templates/security/two_factor_setup.html:49 msgid "" -"Open your authenticator app on your device and scan the following qrcode " -"to start receiving codes:" +"Open an authenticator app on your device and scan the following QRcode " +"(or enter the code below manually) to start receiving codes:" msgstr "" -#: flask_security/templates/security/two_factor_setup.html:22 +#: flask_security/templates/security/two_factor_setup.html:52 msgid "Two factor authentication code" msgstr "" -#: flask_security/templates/security/two_factor_setup.html:25 +#: flask_security/templates/security/two_factor_setup.html:60 msgid "To Which Phone Number Should We Send Code To?" msgstr "" @@ -519,27 +519,27 @@ 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 +#: flask_security/templates/security/us_setup.html:36 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_setup.html:58 #: flask_security/templates/security/us_signin.html:23 #: flask_security/templates/security/us_verify.html:21 -#, fuzzy msgid "Code has been sent" -msgstr "パスワード変更" +msgstr "" + +#: flask_security/templates/security/us_setup.html:66 +msgid "" +"Open an authenticator app on your device and scan the following QRcode " +"(or enter the code below manually) to start receiving passcodes:" +msgstr "" + +#: flask_security/templates/security/us_setup.html:69 +msgid "Passwordless QRCode" +msgstr "" -#: flask_security/templates/security/us_setup.html:29 +#: flask_security/templates/security/us_setup.html:77 msgid "No methods have been enabled - nothing to setup" msgstr "" @@ -553,6 +553,10 @@ msgid "Please re-authenticate" msgstr "再度ログインしてください" +#: flask_security/templates/security/verify.html:6 +msgid "Please Enter Your Password" +msgstr "" + #: flask_security/templates/security/email/change_notice.html:1 msgid "Your password has been changed." msgstr "パスワードが変更されました。" @@ -565,7 +569,12 @@ msgid "click here to reset it" msgstr "このリンクを開いてください。" +#: flask_security/templates/security/email/change_notice.txt:3 +msgid "If you did not change your password, click the link below to reset it." +msgstr "" + #: flask_security/templates/security/email/confirmation_instructions.html:1 +#: flask_security/templates/security/email/confirmation_instructions.txt:1 msgid "Please confirm your email through the link below:" msgstr "以下のリンクからメール アドレスを検証してください:" @@ -575,12 +584,15 @@ msgstr "メール アドレスの検証" #: flask_security/templates/security/email/login_instructions.html:1 +#: flask_security/templates/security/email/login_instructions.txt:1 #: flask_security/templates/security/email/welcome.html:1 +#: flask_security/templates/security/email/welcome.txt:1 #, python-format msgid "Welcome %(email)s!" msgstr "ようこそ %(email)s !" #: flask_security/templates/security/email/login_instructions.html:3 +#: flask_security/templates/security/email/login_instructions.txt:3 msgid "You can log into your account through the link below:" msgstr "以下のリンクによりログインできます。" @@ -592,25 +604,46 @@ msgid "Click here to reset your password" msgstr "パスワードを再設定するためにこのリンクを開いてください。" +#: flask_security/templates/security/email/reset_instructions.txt:1 +msgid "Click the link below to reset your password:" +msgstr "" + #: flask_security/templates/security/email/two_factor_instructions.html:3 +#: flask_security/templates/security/email/two_factor_instructions.txt:3 msgid "You can log into your account using the following code:" msgstr "" #: flask_security/templates/security/email/two_factor_rescue.html:1 +#: flask_security/templates/security/email/two_factor_rescue.txt:1 msgid "can not access mail account" msgstr "" #: flask_security/templates/security/email/us_instructions.html:3 -#, fuzzy +#: flask_security/templates/security/email/us_instructions.txt:3 msgid "You can sign into your account using the following code:" -msgstr "以下のリンクによりログインできます。" +msgstr "" #: flask_security/templates/security/email/us_instructions.html:6 -#, fuzzy msgid "Or use the the link below:" -msgstr "以下のリンクによりメール アドレスを検証できます。" +msgstr "" + +#: flask_security/templates/security/email/us_instructions.txt:7 +msgid "Or use the link below:" +msgstr "" #: flask_security/templates/security/email/welcome.html:4 +#: flask_security/templates/security/email/welcome.txt:4 msgid "You can confirm your email through the link below:" msgstr "以下のリンクによりメール アドレスを検証できます。" +#~ msgid "You successfully confirmed password" +#~ msgstr "" + +#~ msgid "Password confirmation is needed in order to access page" +#~ msgstr "" + +#~ msgid "" +#~ "Open your authenticator app on your " +#~ "device and scan the following qrcode " +#~ "to start receiving codes:" +#~ msgstr "" Binary files /tmp/tmpl_LU2e/ak23Qk5OdH/flask-security-3.4.2/flask_security/translations/nl_NL/LC_MESSAGES/flask_security.mo and /tmp/tmpl_LU2e/U10RHAXkq7/flask-security-4.0.0/flask_security/translations/nl_NL/LC_MESSAGES/flask_security.mo differ diff -Nru flask-security-3.4.2/flask_security/translations/nl_NL/LC_MESSAGES/flask_security.po flask-security-4.0.0/flask_security/translations/nl_NL/LC_MESSAGES/flask_security.po --- flask-security-3.4.2/flask_security/translations/nl_NL/LC_MESSAGES/flask_security.po 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/translations/nl_NL/LC_MESSAGES/flask_security.po 2021-01-26 02:39:51.000000000 +0000 @@ -8,7 +8,7 @@ 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" +"POT-Creation-Date: 2021-01-22 14:48-0800\n" "PO-Revision-Date: 2017-05-01 17:52+0200\n" "Last-Translator: FULL NAME \n" "Language: nl_NL\n" @@ -17,109 +17,121 @@ "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" +"Generated-By: Babel 2.9.0\n" -#: flask_security/core.py:207 +#: flask_security/core.py:209 msgid "Login Required" msgstr "Inloggen Verplicht" -#: flask_security/core.py:208 +#: flask_security/core.py:210 #: flask_security/templates/security/email/two_factor_instructions.html:1 +#: flask_security/templates/security/email/two_factor_instructions.txt:1 #: flask_security/templates/security/email/us_instructions.html:1 +#: flask_security/templates/security/email/us_instructions.txt:1 +#: tests/templates/_nav.html:2 msgid "Welcome" msgstr "Welkom" -#: flask_security/core.py:209 +#: flask_security/core.py:211 msgid "Please confirm your email" msgstr "Gelieve uw e-mailadres te bevestigen" -#: flask_security/core.py:210 +#: flask_security/core.py:212 msgid "Login instructions" msgstr "Aanmeld instructies" -#: flask_security/core.py:211 +#: flask_security/core.py:213 #: flask_security/templates/security/email/reset_notice.html:1 +#: flask_security/templates/security/email/reset_notice.txt:1 msgid "Your password has been reset" msgstr "Uw wachtwoord werd gereset" -#: flask_security/core.py:212 +#: flask_security/core.py:214 +#: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed" msgstr "Uw wachtwoord werd gewijzigd" -#: flask_security/core.py:213 +#: flask_security/core.py:215 msgid "Password reset instructions" msgstr "Wachtwoord reset instructies" -#: flask_security/core.py:216 +#: flask_security/core.py:218 msgid "Two-factor Login" msgstr "Dubbele Authenticatie Aanmelding" -#: flask_security/core.py:217 +#: flask_security/core.py:219 msgid "Two-factor Rescue" msgstr "Dubbele Authenticatie Herstellen" -#: flask_security/core.py:266 +#: flask_security/core.py:263 #, fuzzy msgid "Verification Code" msgstr "Authenticatie Code" -#: flask_security/core.py:282 +#: flask_security/core.py:278 msgid "Input not appropriate for requested API" msgstr "" -#: flask_security/core.py:283 +#: flask_security/core.py:279 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 +#: flask_security/core.py:281 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 +#: flask_security/core.py:285 #, 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 +#: flask_security/core.py:289 #, 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 +#: flask_security/core.py:292 msgid "Thank you. Your email has been confirmed." msgstr "Bedankt. Uw e-mailadres werd bevestigd." -#: flask_security/core.py:297 +#: flask_security/core.py:293 msgid "Your email has already been confirmed." msgstr "Uw e-mailadres werd reeds bevestigd." -#: flask_security/core.py:298 +#: flask_security/core.py:294 msgid "Invalid confirmation token." msgstr "Ongeldige bevestiging token." -#: flask_security/core.py:300 +#: flask_security/core.py:296 #, 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 +#: flask_security/core.py:300 +#, python-format +msgid "" +"Identity attribute '%(attr)s' with value '%(value)s' is already " +"associated with an account." +msgstr "" + +#: flask_security/core.py:306 msgid "Password does not match" msgstr "Wachtwoord komt niet overeen" -#: flask_security/core.py:304 +#: flask_security/core.py:307 msgid "Passwords do not match" msgstr "Wachtwoorden komen niet overeen" -#: flask_security/core.py:305 +#: flask_security/core.py:308 msgid "Redirections outside the domain are forbidden" msgstr "Omleidingen buiten het domein zijn niet toegelaten" -#: flask_security/core.py:307 +#: flask_security/core.py:310 #, 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 +#: flask_security/core.py:314 #, python-format msgid "" "You did not reset your password within %(within)s. New instructions have " @@ -128,22 +140,22 @@ "U heeft uw wachtwoord niet gereset gedurende %(within)s. Nieuwe " "instructies werden verzonden naar %(email)s." -#: flask_security/core.py:317 +#: flask_security/core.py:320 msgid "Invalid reset password token." msgstr "Ongeldig wachtwoord reset token." -#: flask_security/core.py:318 +#: flask_security/core.py:321 msgid "Email requires confirmation." msgstr "E-mailadres moet bevestigd worden." -#: flask_security/core.py:320 +#: flask_security/core.py:323 #, 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 +#: flask_security/core.py:327 #, python-format msgid "" "You did not confirm your email within %(within)s. New instructions to " @@ -153,7 +165,7 @@ "instructies ter bevestiging van uw e-mailadres werden verzonden naar " "%(email)s." -#: flask_security/core.py:332 +#: flask_security/core.py:335 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " @@ -162,296 +174,285 @@ "Je bent niet ingelogd geweest gedurende %(within)s. Nieuwe instructies om" " in te loggen werden verzonden naar%(email)s." -#: flask_security/core.py:339 +#: flask_security/core.py:342 #, 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 +#: flask_security/core.py:345 msgid "Invalid login token." msgstr "Ongeldige aanmelding." -#: flask_security/core.py:343 +#: flask_security/core.py:346 msgid "Account is disabled." msgstr "Account is geblokkeerd." -#: flask_security/core.py:344 +#: flask_security/core.py:347 msgid "Email not provided" msgstr "Email niet ingevuld" -#: flask_security/core.py:345 +#: flask_security/core.py:348 msgid "Invalid email address" msgstr "Ongeldig e-mailadres" -#: flask_security/core.py:346 +#: flask_security/core.py:349 #, fuzzy msgid "Invalid code" msgstr "Niet valide token" -#: flask_security/core.py:347 +#: flask_security/core.py:350 msgid "Password not provided" msgstr "Wachtwoord niet ingevuld" -#: flask_security/core.py:348 +#: flask_security/core.py:351 msgid "No password is set for this user" msgstr "Er is geen wachtwoord gezet voor deze gebruiker" -#: flask_security/core.py:350 +#: flask_security/core.py:353 #, fuzzy, python-format msgid "Password must be at least %(length)s characters" -msgstr "Uw wachtwoord moet minstens 6 karakters bevatten" +msgstr "Uw wachtwoord moet minstens %(length)s karakters bevatten" -#: flask_security/core.py:353 +#: flask_security/core.py:356 msgid "Password not complex enough" msgstr "" -#: flask_security/core.py:354 +#: flask_security/core.py:357 msgid "Password on breached list" msgstr "" -#: flask_security/core.py:356 +#: flask_security/core.py:359 msgid "Failed to contact breached passwords site" msgstr "" -#: flask_security/core.py:359 +#: flask_security/core.py:362 msgid "Phone number not valid e.g. missing country code" msgstr "" -#: flask_security/core.py:360 +#: flask_security/core.py:363 msgid "Specified user does not exist" msgstr "Deze gebruiker bestaat niet" -#: flask_security/core.py:361 +#: flask_security/core.py:364 msgid "Invalid password" msgstr "Ongeldig wachtwoord" -#: flask_security/core.py:362 -#, fuzzy +#: flask_security/core.py:365 msgid "Password or code submitted is not valid" -msgstr "De gemarkeerde methode is niet valide" +msgstr "" -#: flask_security/core.py:363 +#: flask_security/core.py:366 msgid "You have successfully logged in." msgstr "U bent succesvol ingelogd." -#: flask_security/core.py:364 +#: flask_security/core.py:367 msgid "Forgot password?" msgstr "Wachtwoord vergeten?" -#: flask_security/core.py:366 +#: flask_security/core.py:369 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 +#: flask_security/core.py:376 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 +#: flask_security/core.py:379 msgid "You successfully changed your password." msgstr "Uw wachtwoord werd met succes gewijzigd." -#: flask_security/core.py:377 +#: flask_security/core.py:380 msgid "Please log in to access this page." msgstr "Gelieve in te loggen om deze pagina te zien." -#: flask_security/core.py:378 +#: flask_security/core.py:381 msgid "Please reauthenticate to access this page." msgstr "Gelieve opnieuw in te loggen om deze pagina te zien." -#: flask_security/core.py:379 -#, fuzzy +#: flask_security/core.py:382 msgid "Reauthentication successful" -msgstr "Authenticatie Code" +msgstr "" -#: flask_security/core.py:381 +#: flask_security/core.py:384 msgid "You can only access this endpoint when not logged in." msgstr "" -#: flask_security/core.py:384 +#: flask_security/core.py:387 msgid "Failed to send code. Please try again later" msgstr "" -#: flask_security/core.py:385 +#: flask_security/core.py:388 msgid "Invalid Token" msgstr "Niet valide token" -#: flask_security/core.py:386 +#: flask_security/core.py:389 msgid "Your token has been confirmed" msgstr "Uw token is bevestigd" -#: flask_security/core.py:388 +#: flask_security/core.py:391 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 +#: flask_security/core.py:395 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 +#: flask_security/core.py:398 msgid "Marked method is not valid" msgstr "De gemarkeerde methode is niet valide" -#: flask_security/core.py:405 +#: flask_security/core.py:400 msgid "You successfully disabled two factor authorization." msgstr "U heeft succesvol Dubbele Authenticatie uitgeschakeld." -#: flask_security/core.py:408 +#: flask_security/core.py:403 #, fuzzy msgid "Requested method is not valid" msgstr "De gemarkeerde methode is niet valide" -#: flask_security/core.py:410 +#: flask_security/core.py:405 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "" -#: flask_security/core.py:413 +#: flask_security/core.py:408 msgid "Unified sign in setup successful" msgstr "" -#: flask_security/core.py:414 +#: flask_security/core.py:409 msgid "You must specify a valid identity to sign in" msgstr "" -#: flask_security/core.py:415 +#: flask_security/core.py:410 #, python-format msgid "Use this code to sign in: %(code)s." msgstr "" -#: flask_security/forms.py:50 +#: flask_security/forms.py:53 msgid "Email Address" msgstr "E-mailadres" -#: flask_security/forms.py:51 +#: flask_security/forms.py:54 msgid "Password" msgstr "wachtwoord" -#: flask_security/forms.py:52 +#: flask_security/forms.py:55 msgid "Remember Me" msgstr "Ingelogd blijven" -#: flask_security/forms.py:53 flask_security/templates/security/_menu.html:5 +#: flask_security/forms.py:56 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/forms.py:57 #: 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/forms.py:58 flask_security/templates/security/_menu.html:11 #: flask_security/templates/security/register_user.html:6 msgid "Register" msgstr "Registreer" -#: flask_security/forms.py:56 +#: flask_security/forms.py:59 msgid "Resend Confirmation Instructions" msgstr "Verzend instructies om te bevestigen opnieuw" -#: flask_security/forms.py:57 +#: flask_security/forms.py:60 msgid "Recover Password" msgstr "Herstel wachtwoord" -#: flask_security/forms.py:58 +#: flask_security/forms.py:61 msgid "Reset Password" msgstr "reset wachtwoord" -#: flask_security/forms.py:59 +#: flask_security/forms.py:62 msgid "Retype Password" msgstr "Type wachtwoord opnieuw" -#: flask_security/forms.py:60 +#: flask_security/forms.py:63 msgid "New Password" msgstr "Nieuw wachtwoord" -#: flask_security/forms.py:61 +#: flask_security/forms.py:64 msgid "Change Password" msgstr "Verander wachtwoord" -#: flask_security/forms.py:62 +#: flask_security/forms.py:65 msgid "Send Login Link" msgstr "Verzend aanmeld link" -#: flask_security/forms.py:63 +#: flask_security/forms.py:66 msgid "Verify Password" msgstr "Wachtwoord Verificatie" -#: flask_security/forms.py:64 +#: flask_security/forms.py:67 msgid "Change Method" msgstr "Verander Methode" -#: flask_security/forms.py:65 +#: flask_security/forms.py:68 msgid "Phone Number" msgstr "Telefoonnummer" -#: flask_security/forms.py:66 +#: flask_security/forms.py:69 msgid "Authentication Code" msgstr "Authenticatie Code" -#: flask_security/forms.py:67 +#: flask_security/forms.py:70 msgid "Submit" msgstr "" -#: flask_security/forms.py:68 +#: flask_security/forms.py:71 msgid "Submit Code" msgstr "" -#: flask_security/forms.py:69 +#: flask_security/forms.py:72 msgid "Error(s)" msgstr "" -#: flask_security/forms.py:70 +#: flask_security/forms.py:73 msgid "Identity" msgstr "" -#: flask_security/forms.py:71 +#: flask_security/forms.py:74 msgid "Send Code" msgstr "" -#: flask_security/forms.py:72 +#: flask_security/forms.py:75 #, fuzzy msgid "Passcode" msgstr "wachtwoord" -#: flask_security/unified_signin.py:145 -#, fuzzy +#: flask_security/unified_signin.py:131 msgid "Code or Password" -msgstr "Herstel wachtwoord" +msgstr "" -#: flask_security/unified_signin.py:150 flask_security/unified_signin.py:270 +#: flask_security/unified_signin.py:136 flask_security/unified_signin.py:262 msgid "Available Methods" msgstr "" -#: flask_security/unified_signin.py:151 +#: flask_security/unified_signin.py:137 msgid "Via email" msgstr "" -#: flask_security/unified_signin.py:151 +#: flask_security/unified_signin.py:137 msgid "Via SMS" msgstr "" -#: flask_security/unified_signin.py:272 +#: flask_security/unified_signin.py:264 msgid "Set up using email" msgstr "" -#: flask_security/unified_signin.py:275 +#: flask_security/unified_signin.py:267 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" -#: flask_security/unified_signin.py:277 +#: flask_security/unified_signin.py:269 msgid "Set up using SMS" msgstr "" @@ -487,13 +488,13 @@ msgid "Resend confirmation instructions" msgstr "Verzend bevestiging instructies opnieuw" -#: flask_security/templates/security/two_factor_setup.html:6 +#: flask_security/templates/security/two_factor_setup.html:31 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 +#: flask_security/templates/security/two_factor_setup.html:32 msgid "" "In addition to your username and password, you'll need to use a code that" " we will send you" @@ -501,27 +502,23 @@ "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 +#: flask_security/templates/security/two_factor_setup.html:43 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 +#: flask_security/templates/security/two_factor_setup.html:49 msgid "" -"Open your authenticator app on your device and scan the following qrcode " -"to start receiving codes:" +"Open an authenticator app on your device and scan the following QRcode " +"(or enter the code below manually) 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 +#: flask_security/templates/security/two_factor_setup.html:52 msgid "Two factor authentication code" msgstr "Dubbele Authenticatie code" -#: flask_security/templates/security/two_factor_setup.html:25 +#: flask_security/templates/security/two_factor_setup.html:60 msgid "To Which Phone Number Should We Send Code To?" msgstr "Naar welk telefoonnummer kunnen we code verzenden?" @@ -541,27 +538,27 @@ 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 +#: flask_security/templates/security/us_setup.html:36 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_setup.html:58 #: 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" +msgstr "" + +#: flask_security/templates/security/us_setup.html:66 +msgid "" +"Open an authenticator app on your device and scan the following QRcode " +"(or enter the code below manually) to start receiving passcodes:" +msgstr "" + +#: flask_security/templates/security/us_setup.html:69 +msgid "Passwordless QRCode" +msgstr "" -#: flask_security/templates/security/us_setup.html:29 +#: flask_security/templates/security/us_setup.html:77 msgid "No methods have been enabled - nothing to setup" msgstr "" @@ -571,9 +568,12 @@ msgstr "" #: flask_security/templates/security/us_verify.html:6 -#, fuzzy msgid "Please re-authenticate" -msgstr "Voer uw authenticatie code in" +msgstr "" + +#: flask_security/templates/security/verify.html:6 +msgid "Please Enter Your Password" +msgstr "Voer uw wachtwoord in" #: flask_security/templates/security/email/change_notice.html:1 msgid "Your password has been changed." @@ -587,7 +587,12 @@ msgid "click here to reset it" msgstr "Klik hier om het te resetten" +#: flask_security/templates/security/email/change_notice.txt:3 +msgid "If you did not change your password, click the link below to reset it." +msgstr "" + #: flask_security/templates/security/email/confirmation_instructions.html:1 +#: flask_security/templates/security/email/confirmation_instructions.txt:1 msgid "Please confirm your email through the link below:" msgstr "Gelieve uw e-mailadres te bevestigen via onderstaande link:" @@ -597,12 +602,15 @@ msgstr "Bevestig mijn account" #: flask_security/templates/security/email/login_instructions.html:1 +#: flask_security/templates/security/email/login_instructions.txt:1 #: flask_security/templates/security/email/welcome.html:1 +#: flask_security/templates/security/email/welcome.txt:1 #, python-format msgid "Welcome %(email)s!" msgstr "Welkom, %(email)s!" #: flask_security/templates/security/email/login_instructions.html:3 +#: flask_security/templates/security/email/login_instructions.txt:3 msgid "You can log into your account through the link below:" msgstr "U kunt inloggen door onderstaande link te gebruiken:" @@ -614,25 +622,50 @@ msgid "Click here to reset your password" msgstr "Klik hier om uw wachtwoord te resetten" +#: flask_security/templates/security/email/reset_instructions.txt:1 +msgid "Click the link below to reset your password:" +msgstr "" + #: flask_security/templates/security/email/two_factor_instructions.html:3 +#: flask_security/templates/security/email/two_factor_instructions.txt: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 +#: flask_security/templates/security/email/two_factor_rescue.txt:1 msgid "can not access mail account" msgstr "kan niet in het e-mail account" #: flask_security/templates/security/email/us_instructions.html:3 +#: flask_security/templates/security/email/us_instructions.txt: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:" +msgstr "" + +#: flask_security/templates/security/email/us_instructions.txt:7 +msgid "Or use the link below:" +msgstr "" #: flask_security/templates/security/email/welcome.html:4 +#: flask_security/templates/security/email/welcome.txt:4 msgid "You can confirm your email through the link below:" msgstr "U kan uw e-mailadres bevestigen via de onderstaande link:" +#~ msgid "You successfully confirmed password" +#~ msgstr "U heeft succesvol uw wachtwoord aangepast" + +#~ msgid "Password confirmation is needed in order to access page" +#~ msgstr "Wachtwoord bevestiging is nodig voor we deze pagina kunnen laten zien" + +#~ 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:" Binary files /tmp/tmpl_LU2e/ak23Qk5OdH/flask-security-3.4.2/flask_security/translations/pl_PL/LC_MESSAGES/flask_security.mo and /tmp/tmpl_LU2e/U10RHAXkq7/flask-security-4.0.0/flask_security/translations/pl_PL/LC_MESSAGES/flask_security.mo differ diff -Nru flask-security-3.4.2/flask_security/translations/pl_PL/LC_MESSAGES/flask_security.po flask-security-4.0.0/flask_security/translations/pl_PL/LC_MESSAGES/flask_security.po --- flask-security-3.4.2/flask_security/translations/pl_PL/LC_MESSAGES/flask_security.po 1970-01-01 00:00:00.000000000 +0000 +++ flask-security-4.0.0/flask_security/translations/pl_PL/LC_MESSAGES/flask_security.po 2021-01-26 02:39:51.000000000 +0000 @@ -0,0 +1,672 @@ +# Polish translation for Flask-Security-Too +# Copyright (C) 2020 Kamil Daniewski +# This file is distributed under the same license as the Flask-Security +# project. +# +msgid "" +msgstr "" +"Project-Id-Version: Flask-Security 2.0.1\n" +"Report-Msgid-Bugs-To: info@inveniosoftware.org\n" +"POT-Creation-Date: 2021-01-22 14:48-0800\n" +"PO-Revision-Date: 2020-11-28 10:19+0100\n" +"Last-Translator: Kamil Daniewski \n" +"Language: pl_PL\n" +"Language-Team: pl_PL \n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && " +"(n%100<10 || n%100>=20) ? 1 : 2)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.9.0\n" + +#: flask_security/core.py:209 +msgid "Login Required" +msgstr "Logowanie jest wymagane" + +#: flask_security/core.py:210 +#: flask_security/templates/security/email/two_factor_instructions.html:1 +#: flask_security/templates/security/email/two_factor_instructions.txt:1 +#: flask_security/templates/security/email/us_instructions.html:1 +#: flask_security/templates/security/email/us_instructions.txt:1 +#: tests/templates/_nav.html:2 +msgid "Welcome" +msgstr "Witamy" + +#: flask_security/core.py:211 +msgid "Please confirm your email" +msgstr "Prosimy o potwierdzenie Twojego adresu e-mail" + +#: flask_security/core.py:212 +msgid "Login instructions" +msgstr "Instrukcje logowania" + +#: flask_security/core.py:213 +#: flask_security/templates/security/email/reset_notice.html:1 +#: flask_security/templates/security/email/reset_notice.txt:1 +msgid "Your password has been reset" +msgstr "Twoje hasło zostało zresetowane" + +#: flask_security/core.py:214 +#: flask_security/templates/security/email/change_notice.txt:1 +msgid "Your password has been changed" +msgstr "Twoje hasło zostało zmienione" + +#: flask_security/core.py:215 +msgid "Password reset instructions" +msgstr "Instrukcje zmiany hasła" + +#: flask_security/core.py:218 +msgid "Two-factor Login" +msgstr "Logowanie dwuskładnikowe" + +#: flask_security/core.py:219 +msgid "Two-factor Rescue" +msgstr "Pomoc w logowaniu dwuskładnikowym" + +#: flask_security/core.py:263 +msgid "Verification Code" +msgstr "Kod weryfikacyjny" + +#: flask_security/core.py:278 +msgid "Input not appropriate for requested API" +msgstr "Nieprawidłowe dane dla żądanego API" + +#: flask_security/core.py:279 +msgid "You do not have permission to view this resource." +msgstr "Nie posiadasz uprawnień, aby wyświetlić tę stronę." + +#: flask_security/core.py:281 +msgid "You are not authenticated. Please supply the correct credentials." +msgstr "" +"Nie jesteś zalogowany. Prosimy o przesłanie prawidłowych danych " +"uwierzytelniania." + +#: flask_security/core.py:285 +msgid "You must re-authenticate to access this endpoint" +msgstr "Musisz zalogować się ponownie, aby wyświetlić tę stronę" + +#: flask_security/core.py:289 +#, python-format +msgid "Thank you. Confirmation instructions have been sent to %(email)s." +msgstr "" +"Dziękujemy. Instrukcje potwierdzenia rejestracji zostały wysłane na adres" +" %(email)s." + +#: flask_security/core.py:292 +msgid "Thank you. Your email has been confirmed." +msgstr "Dziękujemy. Twój adres e-mail został potwierdzony." + +#: flask_security/core.py:293 +msgid "Your email has already been confirmed." +msgstr "Twój adres e-mail już został potwierdzony." + +#: flask_security/core.py:294 +msgid "Invalid confirmation token." +msgstr "Nieprawidłowy token potwierdzania adresu e-mail." + +#: flask_security/core.py:296 +#, python-format +msgid "%(email)s is already associated with an account." +msgstr "%(email)s jest już powiązany z kontem." + +#: flask_security/core.py:300 +#, python-format +msgid "" +"Identity attribute '%(attr)s' with value '%(value)s' is already " +"associated with an account." +msgstr "" +"Atrybut identyfikujący '%(attr)s' z wartością '%(value)s' jest już " +"powiązany z kontem." + +#: flask_security/core.py:306 +msgid "Password does not match" +msgstr "Hasło nie pasuje" + +#: flask_security/core.py:307 +msgid "Passwords do not match" +msgstr "Hasła nie pasują do siebie" + +#: flask_security/core.py:308 +msgid "Redirections outside the domain are forbidden" +msgstr "Przekierowania poza domenę są zabronione" + +#: flask_security/core.py:310 +#, python-format +msgid "Instructions to reset your password have been sent to %(email)s." +msgstr "Instrukcje resetowania hasła zostały wysłane na adres %(email)s." + +#: flask_security/core.py:314 +#, python-format +msgid "" +"You did not reset your password within %(within)s. New instructions have " +"been sent to %(email)s." +msgstr "" +"Nie ustawiłeś hasła w ciągu %(within)s. Nowe instrukcje zostały wysłane " +"na adres %(email)s." + +#: flask_security/core.py:320 +msgid "Invalid reset password token." +msgstr "Nieprawidłowy token resetowania hasła." + +#: flask_security/core.py:321 +msgid "Email requires confirmation." +msgstr "Wymagane jest potwierdzenie adresu e-mail." + +#: flask_security/core.py:323 +#, python-format +msgid "Confirmation instructions have been sent to %(email)s." +msgstr "Instrukcje potwierdzenia adresu e-mail zostały wysłane na adres %(email)s." + +#: flask_security/core.py:327 +#, 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 "" +"Nie potwierdziłeś adresu e-mail w ciągu %(within)s. Nowe instrukcje " +"zostały wysłane na adres %(email)s." + +#: flask_security/core.py:335 +#, python-format +msgid "" +"You did not login within %(within)s. New instructions to login have been " +"sent to %(email)s." +msgstr "" +"Nie zalogowałeś się w ciągu %(within)s. Nowe instrukcje logowania zostały" +" wysłane na adres %(email)s." + +#: flask_security/core.py:342 +#, python-format +msgid "Instructions to login have been sent to %(email)s." +msgstr "Instrukcje logowania zostały wysłane na adres %(email)s." + +#: flask_security/core.py:345 +msgid "Invalid login token." +msgstr "Nieprawidłowy token logowania." + +#: flask_security/core.py:346 +msgid "Account is disabled." +msgstr "Konto jest wyłączone." + +#: flask_security/core.py:347 +msgid "Email not provided" +msgstr "Adres e-mail nie został wprowadzony" + +#: flask_security/core.py:348 +msgid "Invalid email address" +msgstr "Nieprawidłowy adres e-mail" + +#: flask_security/core.py:349 +msgid "Invalid code" +msgstr "Nieprawidłowy kod" + +#: flask_security/core.py:350 +msgid "Password not provided" +msgstr "Hasło nie zostało wprowadzone" + +#: flask_security/core.py:351 +msgid "No password is set for this user" +msgstr "Hasło nie zostało ustawione przez tego użytkownika" + +#: flask_security/core.py:353 +#, python-format +msgid "Password must be at least %(length)s characters" +msgstr "Hasło musi zawierać co najmniej %(length)s znaków" + +#: flask_security/core.py:356 +msgid "Password not complex enough" +msgstr "Hasło nie jest wystarczająco złożone" + +#: flask_security/core.py:357 +msgid "Password on breached list" +msgstr "Hasło znajduje się na liście haseł wykradzionych" + +#: flask_security/core.py:359 +msgid "Failed to contact breached passwords site" +msgstr "" +"Nie udało się dotrzeć do podmiotu sprawdzającego hasło w bazie " +"wykradzionych haseł" + +#: flask_security/core.py:362 +msgid "Phone number not valid e.g. missing country code" +msgstr "Nieprawidłowiy numer telefonu (upewnij się, że zawiera kod kraju)" + +#: flask_security/core.py:363 +msgid "Specified user does not exist" +msgstr "Ten użytkownik nie istnieje" + +#: flask_security/core.py:364 +msgid "Invalid password" +msgstr "Nieprawidłowe hasło" + +#: flask_security/core.py:365 +msgid "Password or code submitted is not valid" +msgstr "Hasło lub wprowadzony kod są nieprawidłowe" + +#: flask_security/core.py:366 +msgid "You have successfully logged in." +msgstr "Zostałeś zalogowany pomyślnie." + +#: flask_security/core.py:367 +msgid "Forgot password?" +msgstr "Zapomniałeś hasło?" + +#: flask_security/core.py:369 +msgid "" +"You successfully reset your password and you have been logged in " +"automatically." +msgstr "Ustawiono nowe hasło i zostałeś zalogowany pomyślnie." + +#: flask_security/core.py:376 +msgid "Your new password must be different than your previous password." +msgstr "Twoje nowe hasło musi być inne, niż obecne hasło." + +#: flask_security/core.py:379 +msgid "You successfully changed your password." +msgstr "Pomyślnie zmieniłeś hasło." + +#: flask_security/core.py:380 +msgid "Please log in to access this page." +msgstr "Prosimy o zalogowanie się, aby móc odwiedzić tę stronę." + +#: flask_security/core.py:381 +msgid "Please reauthenticate to access this page." +msgstr "Prosimy o ponowne zalogowanie się, aby móc odwiedzić tę stronę." + +#: flask_security/core.py:382 +msgid "Reauthentication successful" +msgstr "Ponownie zalogowano" + +#: flask_security/core.py:384 +msgid "You can only access this endpoint when not logged in." +msgstr "Możesz odwiedzić tę stronę tylko będąc niezalogowanym." + +#: flask_security/core.py:387 +msgid "Failed to send code. Please try again later" +msgstr "Nie udało się wysłać kodu. Prosimy spróbować później" + +#: flask_security/core.py:388 +msgid "Invalid Token" +msgstr "Nieprawidłowy token" + +#: flask_security/core.py:389 +msgid "Your token has been confirmed" +msgstr "Twój token nie został potwierdzony" + +#: flask_security/core.py:391 +msgid "You successfully changed your two-factor method." +msgstr "Metoda logowania dwuskładnikowego została zmieniona pomyślnie." + +#: flask_security/core.py:395 +msgid "You currently do not have permissions to access this page" +msgstr "Nie posiadasz uprawnień, aby odwiedzić tę stronę" + +#: flask_security/core.py:398 +msgid "Marked method is not valid" +msgstr "Wybrana metoda jest niewłaściwa" + +#: flask_security/core.py:400 +msgid "You successfully disabled two factor authorization." +msgstr "Pomyślnie wyłączyłeś logowanie dwuskładnikowe." + +#: flask_security/core.py:403 +msgid "Requested method is not valid" +msgstr "Żądana metoda jest niewłaściwa" + +#: flask_security/core.py:405 +#, python-format +msgid "Setup must be completed within %(within)s. Please start over." +msgstr "" +"Ustawienie musi zostać ukończone w ciągu %(within)s. Prosimy zacząć " +"ponownie." + +#: flask_security/core.py:408 +msgid "Unified sign in setup successful" +msgstr "Ujednolicone logowanie przebiegło pomyślnie" + +#: flask_security/core.py:409 +msgid "You must specify a valid identity to sign in" +msgstr "Musisz ustawić prawidłowy identyfikator, aby się zalogować" + +#: flask_security/core.py:410 +#, python-format +msgid "Use this code to sign in: %(code)s." +msgstr "Użyj tego kodu, aby się zalogować: %(code)s." + +#: flask_security/forms.py:53 +msgid "Email Address" +msgstr "Adres e-mail" + +#: flask_security/forms.py:54 +msgid "Password" +msgstr "Hasło" + +#: flask_security/forms.py:55 +msgid "Remember Me" +msgstr "Zapamiętaj mnie" + +#: flask_security/forms.py:56 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 "Zaloguj" + +#: flask_security/forms.py:57 +#: flask_security/templates/security/email/us_instructions.html:8 +#: flask_security/templates/security/us_signin.html:6 +msgid "Sign In" +msgstr "Zaloguj" + +#: flask_security/forms.py:58 flask_security/templates/security/_menu.html:11 +#: flask_security/templates/security/register_user.html:6 +msgid "Register" +msgstr "Zarejestruj" + +#: flask_security/forms.py:59 +msgid "Resend Confirmation Instructions" +msgstr "Ponownie wyślij instrukcje potwierdzania adresu e-mail" + +#: flask_security/forms.py:60 +msgid "Recover Password" +msgstr "Odzyskaj hasło" + +#: flask_security/forms.py:61 +msgid "Reset Password" +msgstr "Zresetuj hasło" + +#: flask_security/forms.py:62 +msgid "Retype Password" +msgstr "Przepisz hasło" + +#: flask_security/forms.py:63 +msgid "New Password" +msgstr "Nowe hasło" + +#: flask_security/forms.py:64 +msgid "Change Password" +msgstr "Zmień hasło" + +#: flask_security/forms.py:65 +msgid "Send Login Link" +msgstr "Wyślij link logowania" + +#: flask_security/forms.py:66 +msgid "Verify Password" +msgstr "Potwierdź hasło" + +#: flask_security/forms.py:67 +msgid "Change Method" +msgstr "Zmień metodę" + +#: flask_security/forms.py:68 +msgid "Phone Number" +msgstr "Numer telefonu" + +#: flask_security/forms.py:69 +msgid "Authentication Code" +msgstr "Kod uwierzytelniania" + +#: flask_security/forms.py:70 +msgid "Submit" +msgstr "Wyślij" + +#: flask_security/forms.py:71 +msgid "Submit Code" +msgstr "Kod zatwierdzenia" + +#: flask_security/forms.py:72 +msgid "Error(s)" +msgstr "Błędy" + +#: flask_security/forms.py:73 +msgid "Identity" +msgstr "Identyfikator" + +#: flask_security/forms.py:74 +msgid "Send Code" +msgstr "Wyślij kod" + +#: flask_security/forms.py:75 +msgid "Passcode" +msgstr "Kod dostępu" + +#: flask_security/unified_signin.py:131 +msgid "Code or Password" +msgstr "Kod lub hasło" + +#: flask_security/unified_signin.py:136 flask_security/unified_signin.py:262 +msgid "Available Methods" +msgstr "Dostępne metody" + +#: flask_security/unified_signin.py:137 +msgid "Via email" +msgstr "Poprzez adres e-mail" + +#: flask_security/unified_signin.py:137 +msgid "Via SMS" +msgstr "Poprzez wiadomość SMS" + +#: flask_security/unified_signin.py:264 +msgid "Set up using email" +msgstr "Ustaw przy pomocy adresu e-mail" + +#: flask_security/unified_signin.py:267 +msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" +msgstr "" +"Ustaw przy pomocy zewnętrznej aplikacji uwierzytelniania (np. Google, " +"Lastpass, Authy)" + +#: flask_security/unified_signin.py:269 +msgid "Set up using SMS" +msgstr "Ustaw przy pomocy wiadomości SMS" + +#: flask_security/templates/security/_menu.html:2 +msgid "Menu" +msgstr "Menu" + +#: flask_security/templates/security/_menu.html:8 +msgid "Unified Sign In" +msgstr "Logowanie ujednolicone" + +#: flask_security/templates/security/_menu.html:14 +msgid "Forgot password" +msgstr "Zapomniałem hasło" + +#: flask_security/templates/security/_menu.html:17 +msgid "Confirm account" +msgstr "Potwierdź konto" + +#: flask_security/templates/security/change_password.html:6 +msgid "Change password" +msgstr "Zmień hasło" + +#: flask_security/templates/security/forgot_password.html:6 +msgid "Send password reset instructions" +msgstr "Wyślij instrukcje resetowania hasła" + +#: flask_security/templates/security/reset_password.html:6 +msgid "Reset password" +msgstr "Resetuj hasło" + +#: flask_security/templates/security/send_confirmation.html:6 +msgid "Resend confirmation instructions" +msgstr "Ponownie wyślij instrukcje potwierdzania rejestracji" + +#: flask_security/templates/security/two_factor_setup.html:31 +msgid "Two-factor authentication adds an extra layer of security to your account" +msgstr "" +"Uwierzytelnianie dwuskładnikowe jest dodatkową warstwą bezpieczeństwa dla" +" Twojego konta" + +#: flask_security/templates/security/two_factor_setup.html:32 +msgid "" +"In addition to your username and password, you'll need to use a code that" +" we will send you" +msgstr "" +"Oprócz Twojej nazwy użytkownika i hasła, będziesz musiał jeszcze użyć " +"kodu, który od nas otrzymasz" + +#: flask_security/templates/security/two_factor_setup.html:43 +msgid "To complete logging in, please enter the code sent to your mail" +msgstr "" +"Aby dokończyć proces logowania, prosimy wprowadzić kod, który został " +"wysłany na Twój adres e-mail" + +#: flask_security/templates/security/two_factor_setup.html:49 +msgid "" +"Open an authenticator app on your device and scan the following QRcode " +"(or enter the code below manually) to start receiving codes:" +msgstr "" + +#: flask_security/templates/security/two_factor_setup.html:52 +msgid "Two factor authentication code" +msgstr "Kod uwierzytelniania dwuskładnikowego" + +#: flask_security/templates/security/two_factor_setup.html:60 +msgid "To Which Phone Number Should We Send Code To?" +msgstr "Na jaki numer telefonu powinien zostać wysłany kod?" + +#: flask_security/templates/security/two_factor_verify_code.html:6 +msgid "Two-factor Authentication" +msgstr "Uwierzytelnianie dwuskładnikowe" + +#: flask_security/templates/security/two_factor_verify_code.html:7 +msgid "Please enter your authentication code" +msgstr "Prosimy o wprowadzenie Twojego kodu uwierzytelniania" + +#: flask_security/templates/security/two_factor_verify_code.html:18 +msgid "The code for authentication was sent to your email address" +msgstr "Kod uwierzytelniania został do Ciebie wysłany na adres e-mail" + +#: 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 "" +"Wiadomość e-mail została do nas wysłana w celu zresetowania Twojego konta" +" aplikacji" + +#: flask_security/templates/security/us_setup.html:36 +msgid "Setup Unified Sign In options" +msgstr "Ustaw opcje logowania ujednoliconego" + +#: flask_security/templates/security/us_setup.html:58 +#: flask_security/templates/security/us_signin.html:23 +#: flask_security/templates/security/us_verify.html:21 +msgid "Code has been sent" +msgstr "Kod został wysłany" + +#: flask_security/templates/security/us_setup.html:66 +msgid "" +"Open an authenticator app on your device and scan the following QRcode " +"(or enter the code below manually) to start receiving passcodes:" +msgstr "" + +#: flask_security/templates/security/us_setup.html:69 +msgid "Passwordless QRCode" +msgstr "Bezhasłowy kod QR" + +#: flask_security/templates/security/us_setup.html:77 +msgid "No methods have been enabled - nothing to setup" +msgstr "Żadna z metod nie została włączona" + +#: flask_security/templates/security/us_signin.html:15 +#: flask_security/templates/security/us_verify.html:13 +msgid "Request one-time code be sent" +msgstr "Zażądaj jednorazowego wysłania kodu" + +#: flask_security/templates/security/us_verify.html:6 +msgid "Please re-authenticate" +msgstr "Prosimy o ponowne zalogowanie" + +#: flask_security/templates/security/verify.html:6 +msgid "Please Enter Your Password" +msgstr "Prosimy o wprowadzenie hasła" + +#: flask_security/templates/security/email/change_notice.html:1 +msgid "Your password has been changed." +msgstr "Twoje hasło zostało zmienione." + +#: flask_security/templates/security/email/change_notice.html:3 +msgid "If you did not change your password," +msgstr "Jeśli nie zmieniłeś swojego hasła," + +#: flask_security/templates/security/email/change_notice.html:3 +msgid "click here to reset it" +msgstr "kliknij tutaj, aby je zresetować" + +#: flask_security/templates/security/email/change_notice.txt:3 +msgid "If you did not change your password, click the link below to reset it." +msgstr "" +"Jeśli nie zmieniłeś swojego hasła, kliknij w poniższy link, aby je " +"zresetować." + +#: flask_security/templates/security/email/confirmation_instructions.html:1 +#: flask_security/templates/security/email/confirmation_instructions.txt:1 +msgid "Please confirm your email through the link below:" +msgstr "Prosimy o potwierdzenie Twojego adresu e-mail poprzez poniższy link:" + +#: flask_security/templates/security/email/confirmation_instructions.html:3 +#: flask_security/templates/security/email/welcome.html:6 +msgid "Confirm my account" +msgstr "Potwierdź moje konto" + +#: flask_security/templates/security/email/login_instructions.html:1 +#: flask_security/templates/security/email/login_instructions.txt:1 +#: flask_security/templates/security/email/welcome.html:1 +#: flask_security/templates/security/email/welcome.txt:1 +#, python-format +msgid "Welcome %(email)s!" +msgstr "Witamy %(email)s!" + +#: flask_security/templates/security/email/login_instructions.html:3 +#: flask_security/templates/security/email/login_instructions.txt:3 +msgid "You can log into your account through the link below:" +msgstr "Możesz logować się na swoje konto poprzez poniższy link:" + +#: flask_security/templates/security/email/login_instructions.html:5 +msgid "Login now" +msgstr "Zaloguj teraz" + +#: flask_security/templates/security/email/reset_instructions.html:1 +msgid "Click here to reset your password" +msgstr "Kliknij tutaj, aby zresetować swoje hasło" + +#: flask_security/templates/security/email/reset_instructions.txt:1 +msgid "Click the link below to reset your password:" +msgstr "Kliknij na poniższy link, aby zresetować swoje hasło:" + +#: flask_security/templates/security/email/two_factor_instructions.html:3 +#: flask_security/templates/security/email/two_factor_instructions.txt:3 +msgid "You can log into your account using the following code:" +msgstr "Możesz logować się na swoje konto używając poniższego kodu:" + +#: flask_security/templates/security/email/two_factor_rescue.html:1 +#: flask_security/templates/security/email/two_factor_rescue.txt:1 +msgid "can not access mail account" +msgstr "brak dostępu do konta mailowego" + +#: flask_security/templates/security/email/us_instructions.html:3 +#: flask_security/templates/security/email/us_instructions.txt:3 +msgid "You can sign into your account using the following code:" +msgstr "Możesz logować się na swoje konto używając poniższego kodu:" + +#: flask_security/templates/security/email/us_instructions.html:6 +msgid "Or use the the link below:" +msgstr "Lub używając poniższego linku:" + +#: flask_security/templates/security/email/us_instructions.txt:7 +msgid "Or use the link below:" +msgstr "Lub używając poniższego linku:" + +#: flask_security/templates/security/email/welcome.html:4 +#: flask_security/templates/security/email/welcome.txt:4 +msgid "You can confirm your email through the link below:" +msgstr "Możesz potwierdzić swój adres e-mail poprzez poniższy link:" + +#~ msgid "" +#~ "Open your authenticator app on your " +#~ "device and scan the following qrcode " +#~ "to start receiving codes:" +#~ msgstr "" +#~ "Otwórz Twoją aplikację uwierzytelniania na " +#~ "swoim urządzeniu i zeskanuj poniższy kod" +#~ " QR, aby móc otrzymywać kolejne kody:" Binary files /tmp/tmpl_LU2e/ak23Qk5OdH/flask-security-3.4.2/flask_security/translations/pt_BR/LC_MESSAGES/flask_security.mo and /tmp/tmpl_LU2e/U10RHAXkq7/flask-security-4.0.0/flask_security/translations/pt_BR/LC_MESSAGES/flask_security.mo differ diff -Nru flask-security-3.4.2/flask_security/translations/pt_BR/LC_MESSAGES/flask_security.po flask-security-4.0.0/flask_security/translations/pt_BR/LC_MESSAGES/flask_security.po --- flask-security-3.4.2/flask_security/translations/pt_BR/LC_MESSAGES/flask_security.po 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/translations/pt_BR/LC_MESSAGES/flask_security.po 2021-01-26 02:39:51.000000000 +0000 @@ -8,7 +8,7 @@ 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" +"POT-Creation-Date: 2021-01-22 14:48-0800\n" "PO-Revision-Date: 2017-09-27 23:39-0300\n" "Last-Translator: José Neto \n" "Language: pt_BR\n" @@ -17,108 +17,120 @@ "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" +"Generated-By: Babel 2.9.0\n" -#: flask_security/core.py:207 +#: flask_security/core.py:209 msgid "Login Required" msgstr "Login obrigatório" -#: flask_security/core.py:208 +#: flask_security/core.py:210 #: flask_security/templates/security/email/two_factor_instructions.html:1 +#: flask_security/templates/security/email/two_factor_instructions.txt:1 #: flask_security/templates/security/email/us_instructions.html:1 +#: flask_security/templates/security/email/us_instructions.txt:1 +#: tests/templates/_nav.html:2 msgid "Welcome" msgstr "Bem-vindo" -#: flask_security/core.py:209 +#: flask_security/core.py:211 msgid "Please confirm your email" msgstr "Por favor, confirme seu email" -#: flask_security/core.py:210 +#: flask_security/core.py:212 msgid "Login instructions" msgstr "Instruções de login" -#: flask_security/core.py:211 +#: flask_security/core.py:213 #: flask_security/templates/security/email/reset_notice.html:1 +#: flask_security/templates/security/email/reset_notice.txt:1 msgid "Your password has been reset" msgstr "Sua senha foi redefinida" -#: flask_security/core.py:212 +#: flask_security/core.py:214 +#: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed" msgstr "Sua senha foi alterada" -#: flask_security/core.py:213 +#: flask_security/core.py:215 msgid "Password reset instructions" msgstr "Instruções para redfinir a senha" -#: flask_security/core.py:216 +#: flask_security/core.py:218 msgid "Two-factor Login" msgstr "" -#: flask_security/core.py:217 +#: flask_security/core.py:219 msgid "Two-factor Rescue" msgstr "" -#: flask_security/core.py:266 +#: flask_security/core.py:263 msgid "Verification Code" msgstr "" -#: flask_security/core.py:282 +#: flask_security/core.py:278 msgid "Input not appropriate for requested API" msgstr "" -#: flask_security/core.py:283 +#: flask_security/core.py:279 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 +#: flask_security/core.py:281 msgid "You are not authenticated. Please supply the correct credentials." msgstr "" -#: flask_security/core.py:289 +#: flask_security/core.py:285 #, 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 +#: flask_security/core.py:289 #, 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 +#: flask_security/core.py:292 msgid "Thank you. Your email has been confirmed." msgstr "Obrigado. Seu email foi confirmado." -#: flask_security/core.py:297 +#: flask_security/core.py:293 msgid "Your email has already been confirmed." msgstr "Seu email já foi confirmado." -#: flask_security/core.py:298 +#: flask_security/core.py:294 msgid "Invalid confirmation token." msgstr "Token de confirmação inválido." -#: flask_security/core.py:300 +#: flask_security/core.py:296 #, 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 +#: flask_security/core.py:300 +#, python-format +msgid "" +"Identity attribute '%(attr)s' with value '%(value)s' is already " +"associated with an account." +msgstr "" + +#: flask_security/core.py:306 msgid "Password does not match" msgstr "Senha não confere" -#: flask_security/core.py:304 +#: flask_security/core.py:307 msgid "Passwords do not match" msgstr "Senhas não conferem" -#: flask_security/core.py:305 +#: flask_security/core.py:308 msgid "Redirections outside the domain are forbidden" msgstr "Redirecionamentos para fora do domínio são proibidos" -#: flask_security/core.py:307 +#: flask_security/core.py:310 #, 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 +#: flask_security/core.py:314 #, python-format msgid "" "You did not reset your password within %(within)s. New instructions have " @@ -127,20 +139,20 @@ "Você não redefiniu sua senha dentro de %(within)s. Novas instruções foram" " enviadas para %(email)s." -#: flask_security/core.py:317 +#: flask_security/core.py:320 msgid "Invalid reset password token." msgstr "Token de redefinição de senha inválido." -#: flask_security/core.py:318 +#: flask_security/core.py:321 msgid "Email requires confirmation." msgstr "O email requer confirmação." -#: flask_security/core.py:320 +#: flask_security/core.py:323 #, 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 +#: flask_security/core.py:327 #, python-format msgid "" "You did not confirm your email within %(within)s. New instructions to " @@ -149,7 +161,7 @@ "Você não confirmou seu email dentro de %(within)s. Novas instruções foram" " enviadas para %(email)s." -#: flask_security/core.py:332 +#: flask_security/core.py:335 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " @@ -158,293 +170,283 @@ "Você não logou dentro de %(within)s. Novas instruções para logar foram " "enviadas para %(email)s." -#: flask_security/core.py:339 +#: flask_security/core.py:342 #, 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 +#: flask_security/core.py:345 msgid "Invalid login token." msgstr "Token de login inválido." -#: flask_security/core.py:343 +#: flask_security/core.py:346 msgid "Account is disabled." msgstr "Conta desabilitada." -#: flask_security/core.py:344 +#: flask_security/core.py:347 msgid "Email not provided" msgstr "Email não informado" -#: flask_security/core.py:345 +#: flask_security/core.py:348 msgid "Invalid email address" msgstr "Endereço de email inválido" -#: flask_security/core.py:346 +#: flask_security/core.py:349 #, fuzzy msgid "Invalid code" msgstr "Senha inválida" -#: flask_security/core.py:347 +#: flask_security/core.py:350 msgid "Password not provided" msgstr "Senha não informada" -#: flask_security/core.py:348 +#: flask_security/core.py:351 msgid "No password is set for this user" msgstr "Nenhuma senha definida para este usuário" -#: flask_security/core.py:350 +#: flask_security/core.py:353 #, 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 +#: flask_security/core.py:356 msgid "Password not complex enough" msgstr "" -#: flask_security/core.py:354 +#: flask_security/core.py:357 msgid "Password on breached list" msgstr "" -#: flask_security/core.py:356 +#: flask_security/core.py:359 msgid "Failed to contact breached passwords site" msgstr "" -#: flask_security/core.py:359 +#: flask_security/core.py:362 msgid "Phone number not valid e.g. missing country code" msgstr "" -#: flask_security/core.py:360 +#: flask_security/core.py:363 msgid "Specified user does not exist" msgstr "Usuário não existe" -#: flask_security/core.py:361 +#: flask_security/core.py:364 msgid "Invalid password" msgstr "Senha inválida" -#: flask_security/core.py:362 +#: flask_security/core.py:365 msgid "Password or code submitted is not valid" msgstr "" -#: flask_security/core.py:363 +#: flask_security/core.py:366 msgid "You have successfully logged in." msgstr "Você logou com sucesso." -#: flask_security/core.py:364 +#: flask_security/core.py:367 msgid "Forgot password?" msgstr "Esqueceu a senha?" -#: flask_security/core.py:366 +#: flask_security/core.py:369 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 +#: flask_security/core.py:376 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 +#: flask_security/core.py:379 msgid "You successfully changed your password." msgstr "Você alterou sua senha com sucesso." -#: flask_security/core.py:377 +#: flask_security/core.py:380 msgid "Please log in to access this page." msgstr "Por favor, logue para acessar esta página." -#: flask_security/core.py:378 +#: flask_security/core.py:381 msgid "Please reauthenticate to access this page." msgstr "Por favor, reautentique-se para acessar esta página." -#: flask_security/core.py:379 +#: flask_security/core.py:382 msgid "Reauthentication successful" msgstr "" -#: flask_security/core.py:381 +#: flask_security/core.py:384 msgid "You can only access this endpoint when not logged in." msgstr "" -#: flask_security/core.py:384 +#: flask_security/core.py:387 msgid "Failed to send code. Please try again later" msgstr "" -#: flask_security/core.py:385 +#: flask_security/core.py:388 msgid "Invalid Token" msgstr "" -#: flask_security/core.py:386 +#: flask_security/core.py:389 msgid "Your token has been confirmed" msgstr "" -#: flask_security/core.py:388 +#: flask_security/core.py:391 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 +#: flask_security/core.py:395 msgid "You currently do not have permissions to access this page" msgstr "" -#: flask_security/core.py:403 +#: flask_security/core.py:398 msgid "Marked method is not valid" msgstr "" -#: flask_security/core.py:405 +#: flask_security/core.py:400 msgid "You successfully disabled two factor authorization." msgstr "" -#: flask_security/core.py:408 +#: flask_security/core.py:403 msgid "Requested method is not valid" msgstr "" -#: flask_security/core.py:410 +#: flask_security/core.py:405 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "" -#: flask_security/core.py:413 +#: flask_security/core.py:408 msgid "Unified sign in setup successful" msgstr "" -#: flask_security/core.py:414 +#: flask_security/core.py:409 msgid "You must specify a valid identity to sign in" msgstr "" -#: flask_security/core.py:415 +#: flask_security/core.py:410 #, python-format msgid "Use this code to sign in: %(code)s." msgstr "" -#: flask_security/forms.py:50 +#: flask_security/forms.py:53 msgid "Email Address" msgstr "Endereço de email" -#: flask_security/forms.py:51 +#: flask_security/forms.py:54 msgid "Password" msgstr "Senha" -#: flask_security/forms.py:52 +#: flask_security/forms.py:55 msgid "Remember Me" msgstr "Lembre de mim" -#: flask_security/forms.py:53 flask_security/templates/security/_menu.html:5 +#: flask_security/forms.py:56 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/forms.py:57 #: 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/forms.py:58 flask_security/templates/security/_menu.html:11 #: flask_security/templates/security/register_user.html:6 msgid "Register" msgstr "Registro" -#: flask_security/forms.py:56 +#: flask_security/forms.py:59 msgid "Resend Confirmation Instructions" msgstr "Reenviar instruções de confirmação" -#: flask_security/forms.py:57 +#: flask_security/forms.py:60 msgid "Recover Password" msgstr "Recuperar senha" -#: flask_security/forms.py:58 +#: flask_security/forms.py:61 msgid "Reset Password" msgstr "Redefinir senha" -#: flask_security/forms.py:59 +#: flask_security/forms.py:62 msgid "Retype Password" msgstr "Reescreva a senha" -#: flask_security/forms.py:60 +#: flask_security/forms.py:63 msgid "New Password" msgstr "Nova senha" -#: flask_security/forms.py:61 +#: flask_security/forms.py:64 msgid "Change Password" msgstr "Alterar senha" -#: flask_security/forms.py:62 +#: flask_security/forms.py:65 msgid "Send Login Link" msgstr "Enviar link de login" -#: flask_security/forms.py:63 +#: flask_security/forms.py:66 msgid "Verify Password" msgstr "" -#: flask_security/forms.py:64 +#: flask_security/forms.py:67 msgid "Change Method" msgstr "" -#: flask_security/forms.py:65 +#: flask_security/forms.py:68 msgid "Phone Number" msgstr "" -#: flask_security/forms.py:66 +#: flask_security/forms.py:69 msgid "Authentication Code" msgstr "" -#: flask_security/forms.py:67 +#: flask_security/forms.py:70 msgid "Submit" msgstr "" -#: flask_security/forms.py:68 +#: flask_security/forms.py:71 msgid "Submit Code" msgstr "" -#: flask_security/forms.py:69 +#: flask_security/forms.py:72 msgid "Error(s)" msgstr "" -#: flask_security/forms.py:70 +#: flask_security/forms.py:73 msgid "Identity" msgstr "" -#: flask_security/forms.py:71 +#: flask_security/forms.py:74 msgid "Send Code" msgstr "" -#: flask_security/forms.py:72 -#, fuzzy +#: flask_security/forms.py:75 msgid "Passcode" -msgstr "Senha" +msgstr "" -#: flask_security/unified_signin.py:145 -#, fuzzy +#: flask_security/unified_signin.py:131 msgid "Code or Password" -msgstr "Recuperar senha" +msgstr "" -#: flask_security/unified_signin.py:150 flask_security/unified_signin.py:270 +#: flask_security/unified_signin.py:136 flask_security/unified_signin.py:262 msgid "Available Methods" msgstr "" -#: flask_security/unified_signin.py:151 +#: flask_security/unified_signin.py:137 msgid "Via email" msgstr "" -#: flask_security/unified_signin.py:151 +#: flask_security/unified_signin.py:137 msgid "Via SMS" msgstr "" -#: flask_security/unified_signin.py:272 +#: flask_security/unified_signin.py:264 msgid "Set up using email" msgstr "" -#: flask_security/unified_signin.py:275 +#: flask_security/unified_signin.py:267 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" -#: flask_security/unified_signin.py:277 +#: flask_security/unified_signin.py:269 msgid "Set up using SMS" msgstr "" @@ -480,32 +482,31 @@ msgid "Resend confirmation instructions" msgstr "Reenviar instruções de confirmação" -#: flask_security/templates/security/two_factor_setup.html:6 +#: flask_security/templates/security/two_factor_setup.html:31 msgid "Two-factor authentication adds an extra layer of security to your account" msgstr "" -#: flask_security/templates/security/two_factor_setup.html:7 +#: flask_security/templates/security/two_factor_setup.html:32 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 +#: flask_security/templates/security/two_factor_setup.html:43 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 +#: flask_security/templates/security/two_factor_setup.html:49 msgid "" -"Open your authenticator app on your device and scan the following qrcode " -"to start receiving codes:" +"Open an authenticator app on your device and scan the following QRcode " +"(or enter the code below manually) to start receiving codes:" msgstr "" -#: flask_security/templates/security/two_factor_setup.html:22 +#: flask_security/templates/security/two_factor_setup.html:52 msgid "Two factor authentication code" msgstr "" -#: flask_security/templates/security/two_factor_setup.html:25 +#: flask_security/templates/security/two_factor_setup.html:60 msgid "To Which Phone Number Should We Send Code To?" msgstr "" @@ -525,27 +526,27 @@ 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 +#: flask_security/templates/security/us_setup.html:36 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_setup.html:58 #: 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" +msgstr "" + +#: flask_security/templates/security/us_setup.html:66 +msgid "" +"Open an authenticator app on your device and scan the following QRcode " +"(or enter the code below manually) to start receiving passcodes:" +msgstr "" + +#: flask_security/templates/security/us_setup.html:69 +msgid "Passwordless QRCode" +msgstr "" -#: flask_security/templates/security/us_setup.html:29 +#: flask_security/templates/security/us_setup.html:77 msgid "No methods have been enabled - nothing to setup" msgstr "" @@ -559,6 +560,10 @@ msgid "Please re-authenticate" msgstr "Por favor, reautentique-se para acessar esta página." +#: flask_security/templates/security/verify.html:6 +msgid "Please Enter Your Password" +msgstr "" + #: flask_security/templates/security/email/change_notice.html:1 msgid "Your password has been changed." msgstr "Sua senha foi alterada." @@ -571,7 +576,12 @@ msgid "click here to reset it" msgstr "clique aqui para resetar" +#: flask_security/templates/security/email/change_notice.txt:3 +msgid "If you did not change your password, click the link below to reset it." +msgstr "" + #: flask_security/templates/security/email/confirmation_instructions.html:1 +#: flask_security/templates/security/email/confirmation_instructions.txt:1 msgid "Please confirm your email through the link below:" msgstr "Por favor, confirme seu email através do link abaixo:" @@ -581,12 +591,15 @@ msgstr "Confirmar minha conta" #: flask_security/templates/security/email/login_instructions.html:1 +#: flask_security/templates/security/email/login_instructions.txt:1 #: flask_security/templates/security/email/welcome.html:1 +#: flask_security/templates/security/email/welcome.txt:1 #, python-format msgid "Welcome %(email)s!" msgstr "Bem-vindo %(email)s!" #: flask_security/templates/security/email/login_instructions.html:3 +#: flask_security/templates/security/email/login_instructions.txt:3 msgid "You can log into your account through the link below:" msgstr "Você pode logar na sua conta através do link abaixo:" @@ -598,25 +611,46 @@ msgid "Click here to reset your password" msgstr "Clique aqui para redefinir sua senha" +#: flask_security/templates/security/email/reset_instructions.txt:1 +msgid "Click the link below to reset your password:" +msgstr "" + #: flask_security/templates/security/email/two_factor_instructions.html:3 +#: flask_security/templates/security/email/two_factor_instructions.txt:3 msgid "You can log into your account using the following code:" msgstr "" #: flask_security/templates/security/email/two_factor_rescue.html:1 +#: flask_security/templates/security/email/two_factor_rescue.txt:1 msgid "can not access mail account" msgstr "" #: flask_security/templates/security/email/us_instructions.html:3 -#, fuzzy +#: flask_security/templates/security/email/us_instructions.txt:3 msgid "You can sign into your account using the following code:" -msgstr "Você pode logar na sua conta através do link abaixo:" +msgstr "" #: 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:" +msgstr "" + +#: flask_security/templates/security/email/us_instructions.txt:7 +msgid "Or use the link below:" +msgstr "" #: flask_security/templates/security/email/welcome.html:4 +#: flask_security/templates/security/email/welcome.txt:4 msgid "You can confirm your email through the link below:" msgstr "Você pode confirmar seu email através do link abaixo:" +#~ msgid "You successfully confirmed password" +#~ msgstr "" + +#~ msgid "Password confirmation is needed in order to access page" +#~ msgstr "" + +#~ msgid "" +#~ "Open your authenticator app on your " +#~ "device and scan the following qrcode " +#~ "to start receiving codes:" +#~ msgstr "" Binary files /tmp/tmpl_LU2e/ak23Qk5OdH/flask-security-3.4.2/flask_security/translations/pt_PT/LC_MESSAGES/flask_security.mo and /tmp/tmpl_LU2e/U10RHAXkq7/flask-security-4.0.0/flask_security/translations/pt_PT/LC_MESSAGES/flask_security.mo differ diff -Nru flask-security-3.4.2/flask_security/translations/pt_PT/LC_MESSAGES/flask_security.po flask-security-4.0.0/flask_security/translations/pt_PT/LC_MESSAGES/flask_security.po --- flask-security-3.4.2/flask_security/translations/pt_PT/LC_MESSAGES/flask_security.po 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/translations/pt_PT/LC_MESSAGES/flask_security.po 2021-01-26 02:39:51.000000000 +0000 @@ -8,7 +8,7 @@ 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" +"POT-Creation-Date: 2021-01-22 14:48-0800\n" "PO-Revision-Date: 2018-04-27 14:00+0100\n" "Last-Translator: Micael Grilo \n" "Language: pt_PT\n" @@ -17,110 +17,122 @@ "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" +"Generated-By: Babel 2.9.0\n" -#: flask_security/core.py:207 +#: flask_security/core.py:209 msgid "Login Required" msgstr "Login obrigatório" -#: flask_security/core.py:208 +#: flask_security/core.py:210 #: flask_security/templates/security/email/two_factor_instructions.html:1 +#: flask_security/templates/security/email/two_factor_instructions.txt:1 #: flask_security/templates/security/email/us_instructions.html:1 +#: flask_security/templates/security/email/us_instructions.txt:1 +#: tests/templates/_nav.html:2 msgid "Welcome" msgstr "Bem-vindo" -#: flask_security/core.py:209 +#: flask_security/core.py:211 msgid "Please confirm your email" msgstr "Por favor, confirme o seu email" -#: flask_security/core.py:210 +#: flask_security/core.py:212 msgid "Login instructions" msgstr "Instruções de login" -#: flask_security/core.py:211 +#: flask_security/core.py:213 #: flask_security/templates/security/email/reset_notice.html:1 +#: flask_security/templates/security/email/reset_notice.txt:1 msgid "Your password has been reset" msgstr "A sua palavra-passe foi redefinida" -#: flask_security/core.py:212 +#: flask_security/core.py:214 +#: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed" msgstr "A sua palavra-passe foi alterada" -#: flask_security/core.py:213 +#: flask_security/core.py:215 msgid "Password reset instructions" msgstr "Instruções para redefinir a palavra-passe" -#: flask_security/core.py:216 +#: flask_security/core.py:218 msgid "Two-factor Login" msgstr "" -#: flask_security/core.py:217 +#: flask_security/core.py:219 msgid "Two-factor Rescue" msgstr "" -#: flask_security/core.py:266 +#: flask_security/core.py:263 msgid "Verification Code" msgstr "" -#: flask_security/core.py:282 +#: flask_security/core.py:278 msgid "Input not appropriate for requested API" msgstr "" -#: flask_security/core.py:283 +#: flask_security/core.py:279 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 +#: flask_security/core.py:281 msgid "You are not authenticated. Please supply the correct credentials." msgstr "" -#: flask_security/core.py:289 +#: flask_security/core.py:285 #, 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 +#: flask_security/core.py:289 #, 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 +#: flask_security/core.py:292 msgid "Thank you. Your email has been confirmed." msgstr "Obrigado. O seu email foi confirmado." -#: flask_security/core.py:297 +#: flask_security/core.py:293 msgid "Your email has already been confirmed." msgstr "O seu email já foi confirmado." -#: flask_security/core.py:298 +#: flask_security/core.py:294 msgid "Invalid confirmation token." msgstr "Token de confirmação inválido." -#: flask_security/core.py:300 +#: flask_security/core.py:296 #, 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 +#: flask_security/core.py:300 +#, python-format +msgid "" +"Identity attribute '%(attr)s' with value '%(value)s' is already " +"associated with an account." +msgstr "" + +#: flask_security/core.py:306 msgid "Password does not match" msgstr "Palavra-passe não coincide" -#: flask_security/core.py:304 +#: flask_security/core.py:307 msgid "Passwords do not match" msgstr "Palavras-passe não coincidem" -#: flask_security/core.py:305 +#: flask_security/core.py:308 msgid "Redirections outside the domain are forbidden" msgstr "Redirecionamentos para fora do domínio são proibidos" -#: flask_security/core.py:307 +#: flask_security/core.py:310 #, 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 +#: flask_security/core.py:314 #, python-format msgid "" "You did not reset your password within %(within)s. New instructions have " @@ -129,20 +141,20 @@ "Não redefiniu a sua palavra-passe dentro de %(within)s. Novas instruções " "foram enviadas para %(email)s." -#: flask_security/core.py:317 +#: flask_security/core.py:320 msgid "Invalid reset password token." msgstr "Token de redefinição de senha inválido." -#: flask_security/core.py:318 +#: flask_security/core.py:321 msgid "Email requires confirmation." msgstr "O email requer confirmação." -#: flask_security/core.py:320 +#: flask_security/core.py:323 #, 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 +#: flask_security/core.py:327 #, python-format msgid "" "You did not confirm your email within %(within)s. New instructions to " @@ -151,7 +163,7 @@ "Não confirmou o seu email dentro de %(within)s. Novas instruções foram " "enviadas para %(email)s." -#: flask_security/core.py:332 +#: flask_security/core.py:335 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " @@ -160,82 +172,81 @@ "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 +#: flask_security/core.py:342 #, 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 +#: flask_security/core.py:345 msgid "Invalid login token." msgstr "Token de login inválido." -#: flask_security/core.py:343 +#: flask_security/core.py:346 msgid "Account is disabled." msgstr "Conta desactivada." -#: flask_security/core.py:344 +#: flask_security/core.py:347 msgid "Email not provided" msgstr "Email em falta" -#: flask_security/core.py:345 +#: flask_security/core.py:348 msgid "Invalid email address" msgstr "Endereço de email inválido" -#: flask_security/core.py:346 -#, fuzzy +#: flask_security/core.py:349 msgid "Invalid code" -msgstr "Palavra-passe inválida" +msgstr "" -#: flask_security/core.py:347 +#: flask_security/core.py:350 msgid "Password not provided" msgstr "Palavra-passe em falta" -#: flask_security/core.py:348 +#: flask_security/core.py:351 msgid "No password is set for this user" msgstr "Nenhuma palavra-passe foi definida para este utilizador" -#: flask_security/core.py:350 +#: flask_security/core.py:353 #, fuzzy, python-format msgid "Password must be at least %(length)s characters" -msgstr "A palavra-passe deve ter pelo menos 6 caracteres" +msgstr "A palavra-passe deve ter pelo menos %(length)s caracteres" -#: flask_security/core.py:353 +#: flask_security/core.py:356 msgid "Password not complex enough" msgstr "" -#: flask_security/core.py:354 +#: flask_security/core.py:357 msgid "Password on breached list" msgstr "" -#: flask_security/core.py:356 +#: flask_security/core.py:359 msgid "Failed to contact breached passwords site" msgstr "" -#: flask_security/core.py:359 +#: flask_security/core.py:362 msgid "Phone number not valid e.g. missing country code" msgstr "" -#: flask_security/core.py:360 +#: flask_security/core.py:363 msgid "Specified user does not exist" msgstr "Utilizador não existe" -#: flask_security/core.py:361 +#: flask_security/core.py:364 msgid "Invalid password" msgstr "Palavra-passe inválida" -#: flask_security/core.py:362 +#: flask_security/core.py:365 msgid "Password or code submitted is not valid" msgstr "" -#: flask_security/core.py:363 +#: flask_security/core.py:366 msgid "You have successfully logged in." msgstr "Sessão iniciada com sucesso." -#: flask_security/core.py:364 +#: flask_security/core.py:367 msgid "Forgot password?" msgstr "Esqueceu a palavra-passe?" -#: flask_security/core.py:366 +#: flask_security/core.py:369 msgid "" "You successfully reset your password and you have been logged in " "automatically." @@ -243,212 +254,202 @@ "Redefiniu a sua palavra-passe com sucesso e iniciou sessão " "automaticamente." -#: flask_security/core.py:373 +#: flask_security/core.py:376 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 +#: flask_security/core.py:379 msgid "You successfully changed your password." msgstr "Alterou a sua palavra-passe com sucesso." -#: flask_security/core.py:377 +#: flask_security/core.py:380 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 +#: flask_security/core.py:381 msgid "Please reauthenticate to access this page." msgstr "Por favor, reautentique-se para aceder esta página." -#: flask_security/core.py:379 +#: flask_security/core.py:382 msgid "Reauthentication successful" msgstr "" -#: flask_security/core.py:381 +#: flask_security/core.py:384 msgid "You can only access this endpoint when not logged in." msgstr "" -#: flask_security/core.py:384 +#: flask_security/core.py:387 msgid "Failed to send code. Please try again later" msgstr "" -#: flask_security/core.py:385 +#: flask_security/core.py:388 msgid "Invalid Token" msgstr "" -#: flask_security/core.py:386 +#: flask_security/core.py:389 msgid "Your token has been confirmed" msgstr "" -#: flask_security/core.py:388 +#: flask_security/core.py:391 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 +#: flask_security/core.py:395 msgid "You currently do not have permissions to access this page" msgstr "" -#: flask_security/core.py:403 +#: flask_security/core.py:398 msgid "Marked method is not valid" msgstr "" -#: flask_security/core.py:405 +#: flask_security/core.py:400 msgid "You successfully disabled two factor authorization." msgstr "" -#: flask_security/core.py:408 +#: flask_security/core.py:403 msgid "Requested method is not valid" msgstr "" -#: flask_security/core.py:410 +#: flask_security/core.py:405 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "" -#: flask_security/core.py:413 +#: flask_security/core.py:408 msgid "Unified sign in setup successful" msgstr "" -#: flask_security/core.py:414 +#: flask_security/core.py:409 msgid "You must specify a valid identity to sign in" msgstr "" -#: flask_security/core.py:415 +#: flask_security/core.py:410 #, python-format msgid "Use this code to sign in: %(code)s." msgstr "" -#: flask_security/forms.py:50 +#: flask_security/forms.py:53 msgid "Email Address" msgstr "Endereço de email" -#: flask_security/forms.py:51 +#: flask_security/forms.py:54 msgid "Password" msgstr "Palavra-passe" -#: flask_security/forms.py:52 +#: flask_security/forms.py:55 msgid "Remember Me" msgstr "Lembrar-me" -#: flask_security/forms.py:53 flask_security/templates/security/_menu.html:5 +#: flask_security/forms.py:56 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/forms.py:57 #: 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/forms.py:58 flask_security/templates/security/_menu.html:11 #: flask_security/templates/security/register_user.html:6 msgid "Register" msgstr "Registo" -#: flask_security/forms.py:56 +#: flask_security/forms.py:59 msgid "Resend Confirmation Instructions" msgstr "Reenviar instruções de confirmação" -#: flask_security/forms.py:57 +#: flask_security/forms.py:60 msgid "Recover Password" msgstr "Recuperar palavra-passe" -#: flask_security/forms.py:58 +#: flask_security/forms.py:61 msgid "Reset Password" msgstr "Redefinir palavra-passe" -#: flask_security/forms.py:59 +#: flask_security/forms.py:62 msgid "Retype Password" msgstr "Reescreva a palavra-passe" -#: flask_security/forms.py:60 +#: flask_security/forms.py:63 msgid "New Password" msgstr "Nova palavra-passe" -#: flask_security/forms.py:61 +#: flask_security/forms.py:64 msgid "Change Password" msgstr "Alterar palavra-passe" -#: flask_security/forms.py:62 +#: flask_security/forms.py:65 msgid "Send Login Link" msgstr "Enviar endereço de login" -#: flask_security/forms.py:63 +#: flask_security/forms.py:66 msgid "Verify Password" msgstr "" -#: flask_security/forms.py:64 +#: flask_security/forms.py:67 msgid "Change Method" msgstr "" -#: flask_security/forms.py:65 +#: flask_security/forms.py:68 msgid "Phone Number" msgstr "" -#: flask_security/forms.py:66 +#: flask_security/forms.py:69 msgid "Authentication Code" msgstr "" -#: flask_security/forms.py:67 +#: flask_security/forms.py:70 msgid "Submit" msgstr "" -#: flask_security/forms.py:68 +#: flask_security/forms.py:71 msgid "Submit Code" msgstr "" -#: flask_security/forms.py:69 +#: flask_security/forms.py:72 msgid "Error(s)" msgstr "" -#: flask_security/forms.py:70 +#: flask_security/forms.py:73 msgid "Identity" msgstr "" -#: flask_security/forms.py:71 +#: flask_security/forms.py:74 msgid "Send Code" msgstr "" -#: flask_security/forms.py:72 -#, fuzzy +#: flask_security/forms.py:75 msgid "Passcode" -msgstr "Palavra-passe" +msgstr "" -#: flask_security/unified_signin.py:145 -#, fuzzy +#: flask_security/unified_signin.py:131 msgid "Code or Password" -msgstr "Recuperar palavra-passe" +msgstr "" -#: flask_security/unified_signin.py:150 flask_security/unified_signin.py:270 +#: flask_security/unified_signin.py:136 flask_security/unified_signin.py:262 msgid "Available Methods" msgstr "" -#: flask_security/unified_signin.py:151 +#: flask_security/unified_signin.py:137 msgid "Via email" msgstr "" -#: flask_security/unified_signin.py:151 +#: flask_security/unified_signin.py:137 msgid "Via SMS" msgstr "" -#: flask_security/unified_signin.py:272 +#: flask_security/unified_signin.py:264 msgid "Set up using email" msgstr "" -#: flask_security/unified_signin.py:275 +#: flask_security/unified_signin.py:267 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" -#: flask_security/unified_signin.py:277 +#: flask_security/unified_signin.py:269 msgid "Set up using SMS" msgstr "" @@ -484,32 +485,31 @@ msgid "Resend confirmation instructions" msgstr "Reenviar instruções de confirmação" -#: flask_security/templates/security/two_factor_setup.html:6 +#: flask_security/templates/security/two_factor_setup.html:31 msgid "Two-factor authentication adds an extra layer of security to your account" msgstr "" -#: flask_security/templates/security/two_factor_setup.html:7 +#: flask_security/templates/security/two_factor_setup.html:32 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 +#: flask_security/templates/security/two_factor_setup.html:43 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 +#: flask_security/templates/security/two_factor_setup.html:49 msgid "" -"Open your authenticator app on your device and scan the following qrcode " -"to start receiving codes:" +"Open an authenticator app on your device and scan the following QRcode " +"(or enter the code below manually) to start receiving codes:" msgstr "" -#: flask_security/templates/security/two_factor_setup.html:22 +#: flask_security/templates/security/two_factor_setup.html:52 msgid "Two factor authentication code" msgstr "" -#: flask_security/templates/security/two_factor_setup.html:25 +#: flask_security/templates/security/two_factor_setup.html:60 msgid "To Which Phone Number Should We Send Code To?" msgstr "" @@ -529,27 +529,27 @@ 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 +#: flask_security/templates/security/us_setup.html:36 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_setup.html:58 #: flask_security/templates/security/us_signin.html:23 #: flask_security/templates/security/us_verify.html:21 -#, fuzzy msgid "Code has been sent" -msgstr "A sua palavra-passe foi redefinida" +msgstr "" + +#: flask_security/templates/security/us_setup.html:66 +msgid "" +"Open an authenticator app on your device and scan the following QRcode " +"(or enter the code below manually) to start receiving passcodes:" +msgstr "" + +#: flask_security/templates/security/us_setup.html:69 +msgid "Passwordless QRCode" +msgstr "" -#: flask_security/templates/security/us_setup.html:29 +#: flask_security/templates/security/us_setup.html:77 msgid "No methods have been enabled - nothing to setup" msgstr "" @@ -563,6 +563,10 @@ msgid "Please re-authenticate" msgstr "Por favor, reautentique-se para aceder esta página." +#: flask_security/templates/security/verify.html:6 +msgid "Please Enter Your Password" +msgstr "" + #: flask_security/templates/security/email/change_notice.html:1 msgid "Your password has been changed." msgstr "A sua palavra-passe foi alterada." @@ -575,7 +579,12 @@ msgid "click here to reset it" msgstr "clique aqui para redefinir" +#: flask_security/templates/security/email/change_notice.txt:3 +msgid "If you did not change your password, click the link below to reset it." +msgstr "" + #: flask_security/templates/security/email/confirmation_instructions.html:1 +#: flask_security/templates/security/email/confirmation_instructions.txt:1 msgid "Please confirm your email through the link below:" msgstr "Por favor, confirme o seu email através do endereço abaixo:" @@ -585,12 +594,15 @@ msgstr "Confirmar minha conta" #: flask_security/templates/security/email/login_instructions.html:1 +#: flask_security/templates/security/email/login_instructions.txt:1 #: flask_security/templates/security/email/welcome.html:1 +#: flask_security/templates/security/email/welcome.txt:1 #, python-format msgid "Welcome %(email)s!" msgstr "Bem-vindo %(email)s!" #: flask_security/templates/security/email/login_instructions.html:3 +#: flask_security/templates/security/email/login_instructions.txt:3 msgid "You can log into your account through the link below:" msgstr "Você pode iniciar sessão na sua conta através do endereço abaixo:" @@ -602,25 +614,46 @@ msgid "Click here to reset your password" msgstr "Clique aqui para redefinir a sua palavra-passe" +#: flask_security/templates/security/email/reset_instructions.txt:1 +msgid "Click the link below to reset your password:" +msgstr "" + #: flask_security/templates/security/email/two_factor_instructions.html:3 +#: flask_security/templates/security/email/two_factor_instructions.txt:3 msgid "You can log into your account using the following code:" msgstr "" #: flask_security/templates/security/email/two_factor_rescue.html:1 +#: flask_security/templates/security/email/two_factor_rescue.txt:1 msgid "can not access mail account" msgstr "" #: flask_security/templates/security/email/us_instructions.html:3 -#, fuzzy +#: flask_security/templates/security/email/us_instructions.txt:3 msgid "You can sign into your account using the following code:" -msgstr "Você pode iniciar sessão na sua conta através do endereço abaixo:" +msgstr "" #: flask_security/templates/security/email/us_instructions.html:6 -#, fuzzy msgid "Or use the the link below:" -msgstr "Você pode confirmar o seu email através do endereço abaixo:" +msgstr "" + +#: flask_security/templates/security/email/us_instructions.txt:7 +msgid "Or use the link below:" +msgstr "" #: flask_security/templates/security/email/welcome.html:4 +#: flask_security/templates/security/email/welcome.txt:4 msgid "You can confirm your email through the link below:" msgstr "Você pode confirmar o seu email através do endereço abaixo:" +#~ msgid "You successfully confirmed password" +#~ msgstr "" + +#~ msgid "Password confirmation is needed in order to access page" +#~ msgstr "" + +#~ msgid "" +#~ "Open your authenticator app on your " +#~ "device and scan the following qrcode " +#~ "to start receiving codes:" +#~ msgstr "" diff -Nru flask-security-3.4.2/flask_security/translations/pwl.txt flask-security-4.0.0/flask_security/translations/pwl.txt --- flask-security-3.4.2/flask_security/translations/pwl.txt 1970-01-01 00:00:00.000000000 +0000 +++ flask-security-4.0.0/flask_security/translations/pwl.txt 2021-01-26 02:39:51.000000000 +0000 @@ -0,0 +1,5 @@ +token +email +thinsp +qrcode +authenticator Binary files /tmp/tmpl_LU2e/ak23Qk5OdH/flask-security-3.4.2/flask_security/translations/ru_RU/LC_MESSAGES/flask_security.mo and /tmp/tmpl_LU2e/U10RHAXkq7/flask-security-4.0.0/flask_security/translations/ru_RU/LC_MESSAGES/flask_security.mo differ diff -Nru flask-security-3.4.2/flask_security/translations/ru_RU/LC_MESSAGES/flask_security.po flask-security-4.0.0/flask_security/translations/ru_RU/LC_MESSAGES/flask_security.po --- flask-security-3.4.2/flask_security/translations/ru_RU/LC_MESSAGES/flask_security.po 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/translations/ru_RU/LC_MESSAGES/flask_security.po 2021-01-26 02:39:51.000000000 +0000 @@ -8,9 +8,9 @@ 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-04-15 15:15+0300\n" -"Last-Translator: FULL NAME \n" +"POT-Creation-Date: 2021-01-22 14:48-0800\n" +"PO-Revision-Date: 2020-09-12 22:21+0300\n" +"Last-Translator: Ivan Fedorov \n" "Language: ru_RU\n" "Language-Team: Leonid R. \n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " @@ -18,108 +18,121 @@ "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" +"Generated-By: Babel 2.9.0\n" -#: flask_security/core.py:207 +#: flask_security/core.py:209 msgid "Login Required" msgstr "Требуется авторизация" -#: flask_security/core.py:208 +#: flask_security/core.py:210 #: flask_security/templates/security/email/two_factor_instructions.html:1 +#: flask_security/templates/security/email/two_factor_instructions.txt:1 #: flask_security/templates/security/email/us_instructions.html:1 +#: flask_security/templates/security/email/us_instructions.txt:1 +#: tests/templates/_nav.html:2 msgid "Welcome" msgstr "Добро пожаловать" -#: flask_security/core.py:209 +#: flask_security/core.py:211 msgid "Please confirm your email" msgstr "Пожалуйста, подтвердите свой почтовый адрес" -#: flask_security/core.py:210 +#: flask_security/core.py:212 msgid "Login instructions" msgstr "Инструкция по входу" -#: flask_security/core.py:211 +#: flask_security/core.py:213 #: flask_security/templates/security/email/reset_notice.html:1 +#: flask_security/templates/security/email/reset_notice.txt:1 msgid "Your password has been reset" msgstr "Ваш пароль был сброшен" -#: flask_security/core.py:212 +#: flask_security/core.py:214 +#: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed" msgstr "Ваш пароль был изменён" -#: flask_security/core.py:213 +#: flask_security/core.py:215 msgid "Password reset instructions" msgstr "Инструкция по восстановлению пароля" -#: flask_security/core.py:216 +#: flask_security/core.py:218 msgid "Two-factor Login" -msgstr "" +msgstr "Двухфакторный вход" -#: flask_security/core.py:217 +#: flask_security/core.py:219 msgid "Two-factor Rescue" -msgstr "" +msgstr "Двухфакторное восстановление" -#: flask_security/core.py:266 +#: flask_security/core.py:263 msgid "Verification Code" -msgstr "" +msgstr "Код подтверждения" -#: flask_security/core.py:282 +#: flask_security/core.py:278 msgid "Input not appropriate for requested API" -msgstr "" +msgstr "Ввод некорректен для запрошенного API" -#: flask_security/core.py:283 +#: flask_security/core.py:279 msgid "You do not have permission to view this resource." msgstr "У вас нет прав доступа к этому ресурсу." -#: flask_security/core.py:285 +#: flask_security/core.py:281 msgid "You are not authenticated. Please supply the correct credentials." -msgstr "" +msgstr "Вы не аутентифицированы. Пожалуйста, укажите корректные учетные данные." -#: flask_security/core.py:289 -#, fuzzy +#: flask_security/core.py:285 msgid "You must re-authenticate to access this endpoint" -msgstr "Пожалуйста, войдите заново чтобы получить доступ к этой странице." +msgstr "Пожалуйста, войдите повторно чтобы получить доступ к этой странице." -#: flask_security/core.py:293 +#: flask_security/core.py:289 #, python-format msgid "Thank you. Confirmation instructions have been sent to %(email)s." msgstr "Спасибо. Инструкция по подтверждению аккаунта отправлена на %(email)s." -#: flask_security/core.py:296 +#: flask_security/core.py:292 msgid "Thank you. Your email has been confirmed." msgstr "Спасибо. Ваш почтовый адрес был подтверждён." -#: flask_security/core.py:297 +#: flask_security/core.py:293 msgid "Your email has already been confirmed." msgstr "Ваш почтовый адрес уже подтверждён." -#: flask_security/core.py:298 +#: flask_security/core.py:294 msgid "Invalid confirmation token." msgstr "Неверный токен для подтверждения аккаунта." -#: flask_security/core.py:300 +#: flask_security/core.py:296 #, python-format msgid "%(email)s is already associated with an account." msgstr "%(email)s уже привязан к другому аккаунту." -#: flask_security/core.py:303 +#: flask_security/core.py:300 +#, python-format +msgid "" +"Identity attribute '%(attr)s' with value '%(value)s' is already " +"associated with an account." +msgstr "" +"Идентификационный атрибут '%(attr)s' со значением '%(value)s' уже " +"ассоциирован с учетной записью." + +#: flask_security/core.py:306 msgid "Password does not match" msgstr "Пароль не подходит" -#: flask_security/core.py:304 +#: flask_security/core.py:307 msgid "Passwords do not match" msgstr "Пароли не совпадают" -#: flask_security/core.py:305 +#: flask_security/core.py:308 msgid "Redirections outside the domain are forbidden" msgstr "Перенаправления вне текущего домена запрещены" -#: flask_security/core.py:307 +#: flask_security/core.py:310 #, python-format msgid "Instructions to reset your password have been sent to %(email)s." msgstr "Инструкция по восстановлению пароля отправлена на %(email)s." -#: flask_security/core.py:311 +#: flask_security/core.py:314 #, python-format msgid "" "You did not reset your password within %(within)s. New instructions have " @@ -128,20 +141,20 @@ "Вы не восстановили пароль в течение %(within)s. Новая инструкция " "отправлена на %(email)s." -#: flask_security/core.py:317 +#: flask_security/core.py:320 msgid "Invalid reset password token." msgstr "Неверный токен для восстановления пароля." -#: flask_security/core.py:318 +#: flask_security/core.py:321 msgid "Email requires confirmation." msgstr "Почтовый адрес требует подтверждения." -#: flask_security/core.py:320 +#: flask_security/core.py:323 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "Инструкция по подтверждению аккаунта отправлена на %(email)s." -#: flask_security/core.py:324 +#: flask_security/core.py:327 #, python-format msgid "" "You did not confirm your email within %(within)s. New instructions to " @@ -150,7 +163,7 @@ "Вы не подтвердили свой почтовый адрес в течение %(within)s. Новая " "инструкция по подтверждению отправлена на %(email)s." -#: flask_security/core.py:332 +#: flask_security/core.py:335 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " @@ -159,303 +172,298 @@ "Вы не вошли в течение %(within)s. Новая инструкция по входу отправлена на" " %(email)s." -#: flask_security/core.py:339 +#: flask_security/core.py:342 #, python-format msgid "Instructions to login have been sent to %(email)s." msgstr "Инструкция по входу отправлена на %(email)s." -#: flask_security/core.py:342 +#: flask_security/core.py:345 msgid "Invalid login token." msgstr "Неверный токен для входа." -#: flask_security/core.py:343 +#: flask_security/core.py:346 msgid "Account is disabled." msgstr "Аккаунт отключён." -#: flask_security/core.py:344 +#: flask_security/core.py:347 msgid "Email not provided" msgstr "Почтовый адрес не введён" -#: flask_security/core.py:345 +#: flask_security/core.py:348 msgid "Invalid email address" msgstr "Неверный почтовый адрес" -#: flask_security/core.py:346 -#, fuzzy +#: flask_security/core.py:349 msgid "Invalid code" -msgstr "Неверный пароль" +msgstr "Код недействителен" -#: flask_security/core.py:347 +#: flask_security/core.py:350 msgid "Password not provided" msgstr "Пароль не введён" -#: flask_security/core.py:348 +#: flask_security/core.py:351 msgid "No password is set for this user" msgstr "У данного пользователя не установлен пароль" -#: flask_security/core.py:350 -#, fuzzy, python-format +#: flask_security/core.py:353 +#, python-format msgid "Password must be at least %(length)s characters" -msgstr "Пароль должен содержать как минимум 6 символов" +msgstr "Пароль должен содержать не менее %(length)s символов" -#: flask_security/core.py:353 +#: flask_security/core.py:356 msgid "Password not complex enough" -msgstr "" +msgstr "Пароль недостаточно сложный" -#: flask_security/core.py:354 +#: flask_security/core.py:357 msgid "Password on breached list" -msgstr "" +msgstr "Пароль в списке скомпрометированных" -#: flask_security/core.py:356 +#: flask_security/core.py:359 msgid "Failed to contact breached passwords site" -msgstr "" +msgstr "Не удалось соединиться с сайтом скомпрометированных паролей" -#: flask_security/core.py:359 +#: flask_security/core.py:362 msgid "Phone number not valid e.g. missing country code" -msgstr "" +msgstr "Номер телефона некорректен, например отсутствует код страны" -#: flask_security/core.py:360 +#: flask_security/core.py:363 msgid "Specified user does not exist" -msgstr "Данный пользователь не найден" +msgstr "Указанный пользователь не существует" -#: flask_security/core.py:361 +#: flask_security/core.py:364 msgid "Invalid password" msgstr "Неверный пароль" -#: flask_security/core.py:362 +#: flask_security/core.py:365 msgid "Password or code submitted is not valid" -msgstr "" +msgstr "Предоставленный пароль или код недействительны" -#: flask_security/core.py:363 +#: flask_security/core.py:366 msgid "You have successfully logged in." msgstr "Вы вошли." -#: flask_security/core.py:364 +#: flask_security/core.py:367 msgid "Forgot password?" msgstr "Забыли пароль?" -#: flask_security/core.py:366 +#: flask_security/core.py:369 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "Ваш пароль был восстановлен и вы автоматически вошли." -#: flask_security/core.py:373 +#: flask_security/core.py:376 msgid "Your new password must be different than your previous password." msgstr "Ваш новый пароль должен отличаться от предыдущего." -#: flask_security/core.py:376 +#: flask_security/core.py:379 msgid "You successfully changed your password." msgstr "Вы удачно сменили пароль." -#: flask_security/core.py:377 +#: flask_security/core.py:380 msgid "Please log in to access this page." msgstr "Пожалуйста, войдите чтобы получить доступ к этой странице." -#: flask_security/core.py:378 +#: flask_security/core.py:381 msgid "Please reauthenticate to access this page." msgstr "Пожалуйста, войдите заново чтобы получить доступ к этой странице." -#: flask_security/core.py:379 +#: flask_security/core.py:382 msgid "Reauthentication successful" -msgstr "" +msgstr "Повторный вход прошел успешно" -#: flask_security/core.py:381 +#: flask_security/core.py:384 msgid "You can only access this endpoint when not logged in." -msgstr "" +msgstr "Вы можете получить доступ к данной странице, только если не авторизованы." -#: flask_security/core.py:384 +#: flask_security/core.py:387 msgid "Failed to send code. Please try again later" -msgstr "" +msgstr "Не удалось отправить код. Пожалуйста, попробуйте позже" -#: flask_security/core.py:385 +#: flask_security/core.py:388 msgid "Invalid Token" -msgstr "" +msgstr "Токен недействителен" -#: flask_security/core.py:386 +#: flask_security/core.py:389 msgid "Your token has been confirmed" -msgstr "" +msgstr "Ваш токен был подтвержден" -#: flask_security/core.py:388 +#: flask_security/core.py:391 msgid "You successfully changed your two-factor method." -msgstr "" +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 +#: flask_security/core.py:395 msgid "You currently do not have permissions to access this page" -msgstr "" +msgstr "В настоящее время у вас нет доступа к данной странице" -#: flask_security/core.py:403 +#: flask_security/core.py:398 msgid "Marked method is not valid" -msgstr "" +msgstr "Отмеченный метод недействителен" -#: flask_security/core.py:405 +#: flask_security/core.py:400 msgid "You successfully disabled two factor authorization." -msgstr "" +msgstr "Вы успешно отключили двухфакторную авторизацию." -#: flask_security/core.py:408 +#: flask_security/core.py:403 msgid "Requested method is not valid" -msgstr "" +msgstr "Запрошенный метод недействителен" -#: flask_security/core.py:410 +#: flask_security/core.py:405 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "" +"Настройка должна быть завершена в течение %(within)s. Пожалуйста, начните" +" заново." -#: flask_security/core.py:413 +#: flask_security/core.py:408 msgid "Unified sign in setup successful" -msgstr "" +msgstr "Настройка единого способа входа прошла успешно" -#: flask_security/core.py:414 +#: flask_security/core.py:409 msgid "You must specify a valid identity to sign in" -msgstr "" +msgstr "Вы должны указать действительный идентификатор для входа" -#: flask_security/core.py:415 +#: flask_security/core.py:410 #, python-format msgid "Use this code to sign in: %(code)s." -msgstr "" +msgstr "Используйте данный код для входа: %(code)s." -#: flask_security/forms.py:50 +#: flask_security/forms.py:53 msgid "Email Address" -msgstr "Почтовый адрес" +msgstr "Адрес электронной почты" -#: flask_security/forms.py:51 +#: flask_security/forms.py:54 msgid "Password" msgstr "Пароль" -#: flask_security/forms.py:52 +#: flask_security/forms.py:55 msgid "Remember Me" msgstr "Запомнить меня" -#: flask_security/forms.py:53 flask_security/templates/security/_menu.html:5 +#: flask_security/forms.py:56 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/forms.py:57 #: flask_security/templates/security/email/us_instructions.html:8 #: flask_security/templates/security/us_signin.html:6 +#, fuzzy msgid "Sign In" -msgstr "" +msgstr "Войти" -#: flask_security/forms.py:55 flask_security/templates/security/_menu.html:11 +#: flask_security/forms.py:58 flask_security/templates/security/_menu.html:11 #: flask_security/templates/security/register_user.html:6 msgid "Register" msgstr "Зарегистрироваться" -#: flask_security/forms.py:56 +#: flask_security/forms.py:59 msgid "Resend Confirmation Instructions" msgstr "Заново отправить инструкцию по подтверждению аккаунта" -#: flask_security/forms.py:57 +#: flask_security/forms.py:60 msgid "Recover Password" msgstr "Восстановить пароль" -#: flask_security/forms.py:58 +#: flask_security/forms.py:61 msgid "Reset Password" msgstr "Сбросить пароль" -#: flask_security/forms.py:59 +#: flask_security/forms.py:62 msgid "Retype Password" msgstr "Подтверждение пароля" -#: flask_security/forms.py:60 +#: flask_security/forms.py:63 msgid "New Password" msgstr "Новый пароль" -#: flask_security/forms.py:61 +#: flask_security/forms.py:64 msgid "Change Password" msgstr "Сменить пароль" -#: flask_security/forms.py:62 +#: flask_security/forms.py:65 msgid "Send Login Link" msgstr "Отправить ссылку для входа" -#: flask_security/forms.py:63 +#: flask_security/forms.py:66 msgid "Verify Password" -msgstr "" +msgstr "Подтвердите пароль" -#: flask_security/forms.py:64 +#: flask_security/forms.py:67 msgid "Change Method" -msgstr "" +msgstr "Изменить метод" -#: flask_security/forms.py:65 +#: flask_security/forms.py:68 msgid "Phone Number" -msgstr "" +msgstr "Номер телефона" -#: flask_security/forms.py:66 +#: flask_security/forms.py:69 msgid "Authentication Code" -msgstr "" +msgstr "Код аутентификации" -#: flask_security/forms.py:67 +#: flask_security/forms.py:70 msgid "Submit" -msgstr "" +msgstr "Отправить" -#: flask_security/forms.py:68 +#: flask_security/forms.py:71 msgid "Submit Code" -msgstr "" +msgstr "Отправить код" -#: flask_security/forms.py:69 +#: flask_security/forms.py:72 msgid "Error(s)" -msgstr "" +msgstr "Ошибка" -#: flask_security/forms.py:70 +#: flask_security/forms.py:73 msgid "Identity" -msgstr "" +msgstr "Идентификатор" -#: flask_security/forms.py:71 +#: flask_security/forms.py:74 msgid "Send Code" -msgstr "" +msgstr "Отправить код" -#: flask_security/forms.py:72 -#, fuzzy +#: flask_security/forms.py:75 msgid "Passcode" -msgstr "Пароль" +msgstr "Код доступа" -#: flask_security/unified_signin.py:145 -#, fuzzy +#: flask_security/unified_signin.py:131 msgid "Code or Password" -msgstr "Восстановить пароль" +msgstr "Код или пароль" -#: flask_security/unified_signin.py:150 flask_security/unified_signin.py:270 +#: flask_security/unified_signin.py:136 flask_security/unified_signin.py:262 msgid "Available Methods" -msgstr "" +msgstr "Доступные методы" -#: flask_security/unified_signin.py:151 +#: flask_security/unified_signin.py:137 msgid "Via email" -msgstr "" +msgstr "По электронной почте" -#: flask_security/unified_signin.py:151 +#: flask_security/unified_signin.py:137 msgid "Via SMS" -msgstr "" +msgstr "По СМС" -#: flask_security/unified_signin.py:272 +#: flask_security/unified_signin.py:264 msgid "Set up using email" -msgstr "" +msgstr "Настроить с помощью электронной почты" -#: flask_security/unified_signin.py:275 +#: flask_security/unified_signin.py:267 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" +"Настроить с помощью приложения для аутентификации (например google, " +"lastpass, authy)" -#: flask_security/unified_signin.py:277 +#: flask_security/unified_signin.py:269 msgid "Set up using SMS" -msgstr "" +msgstr "Настроить с помощью СМС" #: flask_security/templates/security/_menu.html:2 msgid "Menu" msgstr "Меню" #: flask_security/templates/security/_menu.html:8 +#, fuzzy msgid "Unified Sign In" -msgstr "" +msgstr "Единый вход" #: flask_security/templates/security/_menu.html:14 msgid "Forgot password" @@ -481,84 +489,92 @@ msgid "Resend confirmation instructions" msgstr "Заново отправить инструкцию по подтверждению аккаунта" -#: flask_security/templates/security/two_factor_setup.html:6 +#: flask_security/templates/security/two_factor_setup.html:31 msgid "Two-factor authentication adds an extra layer of security to your account" msgstr "" +"Двухфакторная аутентификация добавляет дополнительный уровень " +"безопасности для вашей учетной записи." -#: flask_security/templates/security/two_factor_setup.html:7 +#: flask_security/templates/security/two_factor_setup.html:32 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 +#: flask_security/templates/security/two_factor_setup.html:43 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 +#: flask_security/templates/security/two_factor_setup.html:49 msgid "" -"Open your authenticator app on your device and scan the following qrcode " -"to start receiving codes:" +"Open an authenticator app on your device and scan the following QRcode " +"(or enter the code below manually) to start receiving codes:" msgstr "" -#: flask_security/templates/security/two_factor_setup.html:22 +#: flask_security/templates/security/two_factor_setup.html:52 msgid "Two factor authentication code" -msgstr "" +msgstr "Код двухфакторной аутентификации" -#: flask_security/templates/security/two_factor_setup.html:25 +#: flask_security/templates/security/two_factor_setup.html:60 msgid "To Which Phone Number Should We Send Code To?" -msgstr "" +msgstr "На какой номер телефона необходимо отправить код?" #: flask_security/templates/security/two_factor_verify_code.html:6 msgid "Two-factor Authentication" -msgstr "" +msgstr "Двухфакторная аутентификация" #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Please enter your authentication code" -msgstr "" +msgstr "Пожалуйста, введите ваш код аутентификации" #: flask_security/templates/security/two_factor_verify_code.html:18 msgid "The code for authentication was sent to your email address" -msgstr "" +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 +#: flask_security/templates/security/us_setup.html:36 msgid "Setup Unified Sign In options" -msgstr "" - -#: flask_security/templates/security/us_setup.html:22 -msgid "Passwordless QRCode" -msgstr "" +msgstr "Настроить параметры единого входа" -#: flask_security/templates/security/us_setup.html:25 +#: flask_security/templates/security/us_setup.html:58 #: flask_security/templates/security/us_signin.html:23 #: flask_security/templates/security/us_verify.html:21 -#, fuzzy msgid "Code has been sent" -msgstr "Ваш пароль был сброшен" +msgstr "Код отправлен" -#: flask_security/templates/security/us_setup.html:29 -msgid "No methods have been enabled - nothing to setup" +#: flask_security/templates/security/us_setup.html:66 +msgid "" +"Open an authenticator app on your device and scan the following QRcode " +"(or enter the code below manually) to start receiving passcodes:" msgstr "" +#: flask_security/templates/security/us_setup.html:69 +msgid "Passwordless QRCode" +msgstr "Беспарольный QR код" + +#: flask_security/templates/security/us_setup.html:77 +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 "" +msgstr "Запросить одноразовый код" #: flask_security/templates/security/us_verify.html:6 -#, fuzzy msgid "Please re-authenticate" -msgstr "Пожалуйста, войдите заново чтобы получить доступ к этой странице." +msgstr "Пожалуйста, войдите повторно чтобы получить доступ к этой странице." + +#: flask_security/templates/security/verify.html:6 +msgid "Please Enter Your Password" +msgstr "Пожалуйста, введите ваш пароль" #: flask_security/templates/security/email/change_notice.html:1 msgid "Your password has been changed." @@ -572,7 +588,12 @@ msgid "click here to reset it" msgstr "нажмите сюда чтобы сбросить его" +#: flask_security/templates/security/email/change_notice.txt:3 +msgid "If you did not change your password, click the link below to reset it." +msgstr "" + #: flask_security/templates/security/email/confirmation_instructions.html:1 +#: flask_security/templates/security/email/confirmation_instructions.txt:1 msgid "Please confirm your email through the link below:" msgstr "Пожалуйста, подтвердите свой почтовый адрес перейдя по ссылке:" @@ -582,12 +603,15 @@ msgstr "Подтвердить аккаунт" #: flask_security/templates/security/email/login_instructions.html:1 +#: flask_security/templates/security/email/login_instructions.txt:1 #: flask_security/templates/security/email/welcome.html:1 +#: flask_security/templates/security/email/welcome.txt:1 #, python-format msgid "Welcome %(email)s!" msgstr "Добро пожаловать, %(email)s!" #: flask_security/templates/security/email/login_instructions.html:3 +#: flask_security/templates/security/email/login_instructions.txt:3 msgid "You can log into your account through the link below:" msgstr "Вы можете войти по ссылке ниже:" @@ -599,25 +623,44 @@ msgid "Click here to reset your password" msgstr "Нажмите, чтобы сбросить свой пароль" +#: flask_security/templates/security/email/reset_instructions.txt:1 +msgid "Click the link below to reset your password:" +msgstr "" + #: flask_security/templates/security/email/two_factor_instructions.html:3 +#: flask_security/templates/security/email/two_factor_instructions.txt:3 msgid "You can log into your account using the following code:" -msgstr "" +msgstr "Вы можете войти в свою учетную запись с помощью следующего кода:" #: flask_security/templates/security/email/two_factor_rescue.html:1 +#: flask_security/templates/security/email/two_factor_rescue.txt:1 msgid "can not access mail account" -msgstr "" +msgstr "не может получить доступ к почтовой учетной записи" #: flask_security/templates/security/email/us_instructions.html:3 -#, fuzzy +#: flask_security/templates/security/email/us_instructions.txt:3 msgid "You can sign into your account using the following code:" -msgstr "Вы можете войти по ссылке ниже:" +msgstr "Вы можете войти в свою учетную запись с помощью следующего кода:" #: flask_security/templates/security/email/us_instructions.html:6 -#, fuzzy msgid "Or use the the link below:" -msgstr "Вы можете подтвердить свой почтовый адрес перейдя по ссылке:" +msgstr "Или используйте данную ссылку:" + +#: flask_security/templates/security/email/us_instructions.txt:7 +msgid "Or use the link below:" +msgstr "" #: flask_security/templates/security/email/welcome.html:4 +#: flask_security/templates/security/email/welcome.txt:4 msgid "You can confirm your email through the link below:" msgstr "Вы можете подтвердить свой почтовый адрес перейдя по ссылке:" +#~ msgid "" +#~ "Open your authenticator app on your " +#~ "device and scan the following qrcode " +#~ "to start receiving codes:" +#~ msgstr "" +#~ "Откройте ваше приложение для авторизации " +#~ "на вашем устройстве и просканируйте " +#~ "данный QR код чтобы начать получать " +#~ "коды:" Binary files /tmp/tmpl_LU2e/ak23Qk5OdH/flask-security-3.4.2/flask_security/translations/tr_TR/LC_MESSAGES/flask_security.mo and /tmp/tmpl_LU2e/U10RHAXkq7/flask-security-4.0.0/flask_security/translations/tr_TR/LC_MESSAGES/flask_security.mo differ diff -Nru flask-security-3.4.2/flask_security/translations/tr_TR/LC_MESSAGES/flask_security.po flask-security-4.0.0/flask_security/translations/tr_TR/LC_MESSAGES/flask_security.po --- flask-security-3.4.2/flask_security/translations/tr_TR/LC_MESSAGES/flask_security.po 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/translations/tr_TR/LC_MESSAGES/flask_security.po 2021-01-26 02:39:51.000000000 +0000 @@ -7,7 +7,7 @@ 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" +"POT-Creation-Date: 2021-01-22 14:48-0800\n" "PO-Revision-Date: 2018-12-20 18:48+0300\n" "Last-Translator: Ecmel B. Canlıer \n" "Language: tr_TR\n" @@ -16,108 +16,120 @@ "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" +"Generated-By: Babel 2.9.0\n" -#: flask_security/core.py:207 +#: flask_security/core.py:209 msgid "Login Required" msgstr "Giriş yapmanız gerekmektedir" -#: flask_security/core.py:208 +#: flask_security/core.py:210 #: flask_security/templates/security/email/two_factor_instructions.html:1 +#: flask_security/templates/security/email/two_factor_instructions.txt:1 #: flask_security/templates/security/email/us_instructions.html:1 +#: flask_security/templates/security/email/us_instructions.txt:1 +#: tests/templates/_nav.html:2 msgid "Welcome" msgstr "Hoş Geldiniz" -#: flask_security/core.py:209 +#: flask_security/core.py:211 msgid "Please confirm your email" msgstr "Lütfen e-posta adresinizi onaylayın" -#: flask_security/core.py:210 +#: flask_security/core.py:212 msgid "Login instructions" msgstr "Giriş talimatları" -#: flask_security/core.py:211 +#: flask_security/core.py:213 #: flask_security/templates/security/email/reset_notice.html:1 +#: flask_security/templates/security/email/reset_notice.txt:1 msgid "Your password has been reset" msgstr "Şifreniz yenilenmiştir" -#: flask_security/core.py:212 +#: flask_security/core.py:214 +#: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed" msgstr "Şifreniz değiştirilmiştir" -#: flask_security/core.py:213 +#: flask_security/core.py:215 msgid "Password reset instructions" msgstr "Şifre yenileme talimatları" -#: flask_security/core.py:216 +#: flask_security/core.py:218 msgid "Two-factor Login" msgstr "" -#: flask_security/core.py:217 +#: flask_security/core.py:219 msgid "Two-factor Rescue" msgstr "" -#: flask_security/core.py:266 +#: flask_security/core.py:263 msgid "Verification Code" msgstr "" -#: flask_security/core.py:282 +#: flask_security/core.py:278 msgid "Input not appropriate for requested API" msgstr "" -#: flask_security/core.py:283 +#: flask_security/core.py:279 msgid "You do not have permission to view this resource." msgstr "Bu maddeyi görmeye yetkiniz yoktur." -#: flask_security/core.py:285 +#: flask_security/core.py:281 msgid "You are not authenticated. Please supply the correct credentials." msgstr "" -#: flask_security/core.py:289 +#: flask_security/core.py:285 #, fuzzy msgid "You must re-authenticate to access this endpoint" msgstr "Bu sayfaya erişebilmek için lütfen tekrardan giriş yapın." -#: flask_security/core.py:293 +#: flask_security/core.py:289 #, python-format msgid "Thank you. Confirmation instructions have been sent to %(email)s." msgstr "Teşekkür ederiz. Onaylama talimatları %(email)s adresine gönderilmiştir." -#: flask_security/core.py:296 +#: flask_security/core.py:292 msgid "Thank you. Your email has been confirmed." msgstr "Teşekkür ederiz. E-posta adresiniz onaylanmıştır" -#: flask_security/core.py:297 +#: flask_security/core.py:293 msgid "Your email has already been confirmed." msgstr "E-posta adresiniz zaten onaylanmış." -#: flask_security/core.py:298 +#: flask_security/core.py:294 msgid "Invalid confirmation token." msgstr "Yanlış onaylama kodu." -#: flask_security/core.py:300 +#: flask_security/core.py:296 #, python-format msgid "%(email)s is already associated with an account." msgstr "%(email)s başka bir hesaba bağlı." -#: flask_security/core.py:303 +#: flask_security/core.py:300 +#, python-format +msgid "" +"Identity attribute '%(attr)s' with value '%(value)s' is already " +"associated with an account." +msgstr "" + +#: flask_security/core.py:306 msgid "Password does not match" msgstr "Şifre yanlış" -#: flask_security/core.py:304 +#: flask_security/core.py:307 msgid "Passwords do not match" msgstr "Şifreler uymuyor" -#: flask_security/core.py:305 +#: flask_security/core.py:308 msgid "Redirections outside the domain are forbidden" msgstr "Adres dışına yönlendirmeler yasaktır" -#: flask_security/core.py:307 +#: flask_security/core.py:310 #, python-format msgid "Instructions to reset your password have been sent to %(email)s." msgstr "Şifrenizi yenileme talimatları %(email)s adresine gönderilmiştir." -#: flask_security/core.py:311 +#: flask_security/core.py:314 #, python-format msgid "" "You did not reset your password within %(within)s. New instructions have " @@ -126,20 +138,20 @@ "Şifrenizi %(within)s içinde yenilemediniz. Yeni talimatlar %(email)s " "adresine gönderilmiştir." -#: flask_security/core.py:317 +#: flask_security/core.py:320 msgid "Invalid reset password token." msgstr "Yanlış şifre yenileme kodu." -#: flask_security/core.py:318 +#: flask_security/core.py:321 msgid "Email requires confirmation." msgstr "E-posta onayı gerekmektedir." -#: flask_security/core.py:320 +#: flask_security/core.py:323 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "Onaylama talimatları %(email)s adresine gönderilmiştir." -#: flask_security/core.py:324 +#: flask_security/core.py:327 #, python-format msgid "" "You did not confirm your email within %(within)s. New instructions to " @@ -148,7 +160,7 @@ "E-posta adresinizi %(within)s içinde onaylamadınız. Yeni onaylama " "talimatları %(email)s adresine gönderilmiştir." -#: flask_security/core.py:332 +#: flask_security/core.py:335 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " @@ -157,293 +169,282 @@ "%(within)s içinde giriş yapmadınız. Yeni giriş yapma talimatları " "%(email)s adresine gönderilmiştir." -#: flask_security/core.py:339 +#: flask_security/core.py:342 #, python-format msgid "Instructions to login have been sent to %(email)s." msgstr "Giriş yapma talimatları %(email)s adresine gönderilmiştir." -#: flask_security/core.py:342 +#: flask_security/core.py:345 msgid "Invalid login token." msgstr "Yanlış giriş kodu." -#: flask_security/core.py:343 +#: flask_security/core.py:346 msgid "Account is disabled." msgstr "Hesap kapalıdır." -#: flask_security/core.py:344 +#: flask_security/core.py:347 msgid "Email not provided" msgstr "E-posta verilmemiş" -#: flask_security/core.py:345 +#: flask_security/core.py:348 msgid "Invalid email address" msgstr "Yanlış e-posta adresi" -#: flask_security/core.py:346 -#, fuzzy +#: flask_security/core.py:349 msgid "Invalid code" -msgstr "Şifre yanlış" +msgstr "" -#: flask_security/core.py:347 +#: flask_security/core.py:350 msgid "Password not provided" msgstr "Şifre verilmemiş" -#: flask_security/core.py:348 +#: flask_security/core.py:351 msgid "No password is set for this user" msgstr "Bu kullanıcı için bir şifre yok" -#: flask_security/core.py:350 +#: flask_security/core.py:353 #, fuzzy, python-format msgid "Password must be at least %(length)s characters" -msgstr "Şifreniz en az 6 karakter olmalıdır" +msgstr "Şifreniz en az %(length)s karakter olmalıdır" -#: flask_security/core.py:353 +#: flask_security/core.py:356 msgid "Password not complex enough" msgstr "" -#: flask_security/core.py:354 +#: flask_security/core.py:357 msgid "Password on breached list" msgstr "" -#: flask_security/core.py:356 +#: flask_security/core.py:359 msgid "Failed to contact breached passwords site" msgstr "" -#: flask_security/core.py:359 +#: flask_security/core.py:362 msgid "Phone number not valid e.g. missing country code" msgstr "" -#: flask_security/core.py:360 +#: flask_security/core.py:363 msgid "Specified user does not exist" msgstr "Böyle bir kullanıcı yok" -#: flask_security/core.py:361 +#: flask_security/core.py:364 msgid "Invalid password" msgstr "Şifre yanlış" -#: flask_security/core.py:362 +#: flask_security/core.py:365 msgid "Password or code submitted is not valid" msgstr "" -#: flask_security/core.py:363 +#: flask_security/core.py:366 msgid "You have successfully logged in." msgstr "Başarıyla giriş yaptınız." -#: flask_security/core.py:364 +#: flask_security/core.py:367 msgid "Forgot password?" msgstr "Şifrenizi mi unuttunuz?" -#: flask_security/core.py:366 +#: flask_security/core.py:369 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "Şifreniz yenilenmiştir ve otomatik olarak giriş yapmış bulunmaktasınız." -#: flask_security/core.py:373 +#: flask_security/core.py:376 msgid "Your new password must be different than your previous password." msgstr "Yeni şifreniz eski şifrenizden farklı olmalıdır." -#: flask_security/core.py:376 +#: flask_security/core.py:379 msgid "You successfully changed your password." msgstr "Şifrenizi başarıyla değiştirdiniz." -#: flask_security/core.py:377 +#: flask_security/core.py:380 msgid "Please log in to access this page." msgstr "Bu sayfaya erişebilmek için lütfen giriş yapın." -#: flask_security/core.py:378 +#: flask_security/core.py:381 msgid "Please reauthenticate to access this page." msgstr "Bu sayfaya erişebilmek için lütfen tekrardan giriş yapın." -#: flask_security/core.py:379 +#: flask_security/core.py:382 msgid "Reauthentication successful" msgstr "" -#: flask_security/core.py:381 +#: flask_security/core.py:384 msgid "You can only access this endpoint when not logged in." msgstr "" -#: flask_security/core.py:384 +#: flask_security/core.py:387 msgid "Failed to send code. Please try again later" msgstr "" -#: flask_security/core.py:385 +#: flask_security/core.py:388 msgid "Invalid Token" msgstr "" -#: flask_security/core.py:386 +#: flask_security/core.py:389 msgid "Your token has been confirmed" msgstr "" -#: flask_security/core.py:388 +#: flask_security/core.py:391 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 +#: flask_security/core.py:395 msgid "You currently do not have permissions to access this page" msgstr "" -#: flask_security/core.py:403 +#: flask_security/core.py:398 msgid "Marked method is not valid" msgstr "" -#: flask_security/core.py:405 +#: flask_security/core.py:400 msgid "You successfully disabled two factor authorization." msgstr "" -#: flask_security/core.py:408 +#: flask_security/core.py:403 msgid "Requested method is not valid" msgstr "" -#: flask_security/core.py:410 +#: flask_security/core.py:405 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "" -#: flask_security/core.py:413 +#: flask_security/core.py:408 msgid "Unified sign in setup successful" msgstr "" -#: flask_security/core.py:414 +#: flask_security/core.py:409 msgid "You must specify a valid identity to sign in" msgstr "" -#: flask_security/core.py:415 +#: flask_security/core.py:410 #, python-format msgid "Use this code to sign in: %(code)s." msgstr "" -#: flask_security/forms.py:50 +#: flask_security/forms.py:53 msgid "Email Address" msgstr "E-posta Adresi" -#: flask_security/forms.py:51 +#: flask_security/forms.py:54 msgid "Password" msgstr "Şifre" -#: flask_security/forms.py:52 +#: flask_security/forms.py:55 msgid "Remember Me" msgstr "Beni Hatırla" -#: flask_security/forms.py:53 flask_security/templates/security/_menu.html:5 +#: flask_security/forms.py:56 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 "Giriş Yap" -#: flask_security/forms.py:54 +#: flask_security/forms.py:57 #: 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/forms.py:58 flask_security/templates/security/_menu.html:11 #: flask_security/templates/security/register_user.html:6 msgid "Register" msgstr "Kayıt Ol" -#: flask_security/forms.py:56 +#: flask_security/forms.py:59 msgid "Resend Confirmation Instructions" msgstr "Onaylama Talimatlarını Tekrar Gönder" -#: flask_security/forms.py:57 +#: flask_security/forms.py:60 msgid "Recover Password" msgstr "Şifre Kurtar" -#: flask_security/forms.py:58 +#: flask_security/forms.py:61 msgid "Reset Password" msgstr "Şifre Yenile" -#: flask_security/forms.py:59 +#: flask_security/forms.py:62 msgid "Retype Password" msgstr "Şifre Tekrarı" -#: flask_security/forms.py:60 +#: flask_security/forms.py:63 msgid "New Password" msgstr "Yeni Şifre" -#: flask_security/forms.py:61 +#: flask_security/forms.py:64 msgid "Change Password" msgstr "Şifre Değiştir" -#: flask_security/forms.py:62 +#: flask_security/forms.py:65 msgid "Send Login Link" msgstr "Giriş Linki Gönder" -#: flask_security/forms.py:63 +#: flask_security/forms.py:66 msgid "Verify Password" msgstr "" -#: flask_security/forms.py:64 +#: flask_security/forms.py:67 msgid "Change Method" msgstr "" -#: flask_security/forms.py:65 +#: flask_security/forms.py:68 msgid "Phone Number" msgstr "" -#: flask_security/forms.py:66 +#: flask_security/forms.py:69 msgid "Authentication Code" msgstr "" -#: flask_security/forms.py:67 +#: flask_security/forms.py:70 msgid "Submit" msgstr "" -#: flask_security/forms.py:68 +#: flask_security/forms.py:71 msgid "Submit Code" msgstr "" -#: flask_security/forms.py:69 +#: flask_security/forms.py:72 msgid "Error(s)" msgstr "" -#: flask_security/forms.py:70 +#: flask_security/forms.py:73 msgid "Identity" msgstr "" -#: flask_security/forms.py:71 +#: flask_security/forms.py:74 msgid "Send Code" msgstr "" -#: flask_security/forms.py:72 -#, fuzzy +#: flask_security/forms.py:75 msgid "Passcode" -msgstr "Şifre" +msgstr "" -#: flask_security/unified_signin.py:145 -#, fuzzy +#: flask_security/unified_signin.py:131 msgid "Code or Password" -msgstr "Şifre Kurtar" +msgstr "" -#: flask_security/unified_signin.py:150 flask_security/unified_signin.py:270 +#: flask_security/unified_signin.py:136 flask_security/unified_signin.py:262 msgid "Available Methods" msgstr "" -#: flask_security/unified_signin.py:151 +#: flask_security/unified_signin.py:137 msgid "Via email" msgstr "" -#: flask_security/unified_signin.py:151 +#: flask_security/unified_signin.py:137 msgid "Via SMS" msgstr "" -#: flask_security/unified_signin.py:272 +#: flask_security/unified_signin.py:264 msgid "Set up using email" msgstr "" -#: flask_security/unified_signin.py:275 +#: flask_security/unified_signin.py:267 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" -#: flask_security/unified_signin.py:277 +#: flask_security/unified_signin.py:269 msgid "Set up using SMS" msgstr "" @@ -479,32 +480,31 @@ msgid "Resend confirmation instructions" msgstr "Onaylama talimatlarını tekrar gönder" -#: flask_security/templates/security/two_factor_setup.html:6 +#: flask_security/templates/security/two_factor_setup.html:31 msgid "Two-factor authentication adds an extra layer of security to your account" msgstr "" -#: flask_security/templates/security/two_factor_setup.html:7 +#: flask_security/templates/security/two_factor_setup.html:32 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 +#: flask_security/templates/security/two_factor_setup.html:43 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 +#: flask_security/templates/security/two_factor_setup.html:49 msgid "" -"Open your authenticator app on your device and scan the following qrcode " -"to start receiving codes:" +"Open an authenticator app on your device and scan the following QRcode " +"(or enter the code below manually) to start receiving codes:" msgstr "" -#: flask_security/templates/security/two_factor_setup.html:22 +#: flask_security/templates/security/two_factor_setup.html:52 msgid "Two factor authentication code" msgstr "" -#: flask_security/templates/security/two_factor_setup.html:25 +#: flask_security/templates/security/two_factor_setup.html:60 msgid "To Which Phone Number Should We Send Code To?" msgstr "" @@ -524,27 +524,27 @@ 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 +#: flask_security/templates/security/us_setup.html:36 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_setup.html:58 #: flask_security/templates/security/us_signin.html:23 #: flask_security/templates/security/us_verify.html:21 -#, fuzzy msgid "Code has been sent" -msgstr "Şifreniz yenilenmiştir" +msgstr "" + +#: flask_security/templates/security/us_setup.html:66 +msgid "" +"Open an authenticator app on your device and scan the following QRcode " +"(or enter the code below manually) to start receiving passcodes:" +msgstr "" + +#: flask_security/templates/security/us_setup.html:69 +msgid "Passwordless QRCode" +msgstr "" -#: flask_security/templates/security/us_setup.html:29 +#: flask_security/templates/security/us_setup.html:77 msgid "No methods have been enabled - nothing to setup" msgstr "" @@ -558,6 +558,10 @@ msgid "Please re-authenticate" msgstr "Bu sayfaya erişebilmek için lütfen tekrardan giriş yapın." +#: flask_security/templates/security/verify.html:6 +msgid "Please Enter Your Password" +msgstr "" + #: flask_security/templates/security/email/change_notice.html:1 msgid "Your password has been changed." msgstr "Şifreniz değiştirilmiştir." @@ -570,7 +574,12 @@ msgid "click here to reset it" msgstr "buraya tıklayarak yenileyiniz" +#: flask_security/templates/security/email/change_notice.txt:3 +msgid "If you did not change your password, click the link below to reset it." +msgstr "" + #: flask_security/templates/security/email/confirmation_instructions.html:1 +#: flask_security/templates/security/email/confirmation_instructions.txt:1 msgid "Please confirm your email through the link below:" msgstr "Lütfen e-posta adresinizi aşağıdaki linkten onaylayınız:" @@ -580,12 +589,15 @@ msgstr "Hesabımı onayla" #: flask_security/templates/security/email/login_instructions.html:1 +#: flask_security/templates/security/email/login_instructions.txt:1 #: flask_security/templates/security/email/welcome.html:1 +#: flask_security/templates/security/email/welcome.txt:1 #, python-format msgid "Welcome %(email)s!" msgstr "Hoş Geldin %(email)s" #: flask_security/templates/security/email/login_instructions.html:3 +#: flask_security/templates/security/email/login_instructions.txt:3 msgid "You can log into your account through the link below:" msgstr "Hesabına aşağıdaki linkten giriş yapabilirsin:" @@ -597,25 +609,46 @@ msgid "Click here to reset your password" msgstr "Şifreni yenilemek için buraya tıkla" +#: flask_security/templates/security/email/reset_instructions.txt:1 +msgid "Click the link below to reset your password:" +msgstr "" + #: flask_security/templates/security/email/two_factor_instructions.html:3 +#: flask_security/templates/security/email/two_factor_instructions.txt:3 msgid "You can log into your account using the following code:" msgstr "" #: flask_security/templates/security/email/two_factor_rescue.html:1 +#: flask_security/templates/security/email/two_factor_rescue.txt:1 msgid "can not access mail account" msgstr "" #: flask_security/templates/security/email/us_instructions.html:3 -#, fuzzy +#: flask_security/templates/security/email/us_instructions.txt:3 msgid "You can sign into your account using the following code:" -msgstr "Hesabına aşağıdaki linkten giriş yapabilirsin:" +msgstr "" #: flask_security/templates/security/email/us_instructions.html:6 -#, fuzzy msgid "Or use the the link below:" -msgstr "E-posta adresinizi aşağıdaki linkten onaylayabilirsiniz:" +msgstr "" + +#: flask_security/templates/security/email/us_instructions.txt:7 +msgid "Or use the link below:" +msgstr "" #: flask_security/templates/security/email/welcome.html:4 +#: flask_security/templates/security/email/welcome.txt:4 msgid "You can confirm your email through the link below:" msgstr "E-posta adresinizi aşağıdaki linkten onaylayabilirsiniz:" +#~ msgid "You successfully confirmed password" +#~ msgstr "" + +#~ msgid "Password confirmation is needed in order to access page" +#~ msgstr "" + +#~ msgid "" +#~ "Open your authenticator app on your " +#~ "device and scan the following qrcode " +#~ "to start receiving codes:" +#~ msgstr "" Binary files /tmp/tmpl_LU2e/ak23Qk5OdH/flask-security-3.4.2/flask_security/translations/zh_Hans_CN/LC_MESSAGES/flask_security.mo and /tmp/tmpl_LU2e/U10RHAXkq7/flask-security-4.0.0/flask_security/translations/zh_Hans_CN/LC_MESSAGES/flask_security.mo differ diff -Nru flask-security-3.4.2/flask_security/translations/zh_Hans_CN/LC_MESSAGES/flask_security.po flask-security-4.0.0/flask_security/translations/zh_Hans_CN/LC_MESSAGES/flask_security.po --- flask-security-3.4.2/flask_security/translations/zh_Hans_CN/LC_MESSAGES/flask_security.po 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/translations/zh_Hans_CN/LC_MESSAGES/flask_security.po 2021-01-26 02:39:51.000000000 +0000 @@ -8,7 +8,7 @@ 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" +"POT-Creation-Date: 2021-01-22 14:48-0800\n" "PO-Revision-Date: 2018-08-02 19:55+0800\n" "Last-Translator: SteinKuo \n" "Language: zh_CN\n" @@ -17,428 +17,430 @@ "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" +"Generated-By: Babel 2.9.0\n" -#: flask_security/core.py:207 +#: flask_security/core.py:209 msgid "Login Required" msgstr "需要登录" -#: flask_security/core.py:208 +#: flask_security/core.py:210 #: flask_security/templates/security/email/two_factor_instructions.html:1 +#: flask_security/templates/security/email/two_factor_instructions.txt:1 #: flask_security/templates/security/email/us_instructions.html:1 +#: flask_security/templates/security/email/us_instructions.txt:1 +#: tests/templates/_nav.html:2 msgid "Welcome" msgstr "欢迎" -#: flask_security/core.py:209 +#: flask_security/core.py:211 msgid "Please confirm your email" msgstr "请激活你的电子邮箱" -#: flask_security/core.py:210 +#: flask_security/core.py:212 msgid "Login instructions" msgstr "登录邮件" -#: flask_security/core.py:211 +#: flask_security/core.py:213 #: flask_security/templates/security/email/reset_notice.html:1 +#: flask_security/templates/security/email/reset_notice.txt:1 msgid "Your password has been reset" msgstr "你的密码已重置" -#: flask_security/core.py:212 +#: flask_security/core.py:214 +#: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed" msgstr "你的密码已更改" -#: flask_security/core.py:213 +#: flask_security/core.py:215 msgid "Password reset instructions" msgstr "密码重置" -#: flask_security/core.py:216 +#: flask_security/core.py:218 msgid "Two-factor Login" msgstr "" -#: flask_security/core.py:217 +#: flask_security/core.py:219 msgid "Two-factor Rescue" msgstr "" -#: flask_security/core.py:266 +#: flask_security/core.py:263 msgid "Verification Code" msgstr "" -#: flask_security/core.py:282 +#: flask_security/core.py:278 msgid "Input not appropriate for requested API" msgstr "" -#: flask_security/core.py:283 +#: flask_security/core.py:279 msgid "You do not have permission to view this resource." msgstr "你无权查看此资源!" -#: flask_security/core.py:285 +#: flask_security/core.py:281 msgid "You are not authenticated. Please supply the correct credentials." msgstr "" -#: flask_security/core.py:289 +#: flask_security/core.py:285 #, fuzzy msgid "You must re-authenticate to access this endpoint" msgstr "请重新进行身份验证,以访问此页面。" -#: flask_security/core.py:293 +#: flask_security/core.py:289 #, python-format msgid "Thank you. Confirmation instructions have been sent to %(email)s." msgstr "谢谢你。已发送激活邮件到 %(email)s。" -#: flask_security/core.py:296 +#: flask_security/core.py:292 msgid "Thank you. Your email has been confirmed." msgstr "谢谢。你的邮箱已激活!" -#: flask_security/core.py:297 +#: flask_security/core.py:293 msgid "Your email has already been confirmed." msgstr "你的邮箱已激活!" -#: flask_security/core.py:298 +#: flask_security/core.py:294 msgid "Invalid confirmation token." msgstr "无效验证码!" -#: flask_security/core.py:300 +#: flask_security/core.py:296 #, python-format msgid "%(email)s is already associated with an account." msgstr "%(email)s 已关联账户。" -#: flask_security/core.py:303 +#: flask_security/core.py:300 +#, python-format +msgid "" +"Identity attribute '%(attr)s' with value '%(value)s' is already " +"associated with an account." +msgstr "" + +#: flask_security/core.py:306 msgid "Password does not match" msgstr "密码不匹配" -#: flask_security/core.py:304 +#: flask_security/core.py:307 msgid "Passwords do not match" msgstr "密码不匹配" -#: flask_security/core.py:305 +#: flask_security/core.py:308 msgid "Redirections outside the domain are forbidden" msgstr "禁止域名外重定向" -#: flask_security/core.py:307 +#: flask_security/core.py:310 #, python-format msgid "Instructions to reset your password have been sent to %(email)s." msgstr "重置密码邮件已发送到 %(email)s。" -#: flask_security/core.py:311 +#: flask_security/core.py:314 #, 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 +#: flask_security/core.py:320 msgid "Invalid reset password token." msgstr "密码重置验证码无效!" -#: flask_security/core.py:318 +#: flask_security/core.py:321 msgid "Email requires confirmation." msgstr "请先激活邮箱。" -#: flask_security/core.py:320 +#: flask_security/core.py:323 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "激活邮件已发送到 %(email)s。" -#: flask_security/core.py:324 +#: flask_security/core.py:327 #, 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 +#: flask_security/core.py:335 #, 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 +#: flask_security/core.py:342 #, python-format msgid "Instructions to login have been sent to %(email)s." msgstr "登录邮件已发送到 %(email)s。" -#: flask_security/core.py:342 +#: flask_security/core.py:345 msgid "Invalid login token." msgstr "无效登录验证码!" -#: flask_security/core.py:343 +#: flask_security/core.py:346 msgid "Account is disabled." msgstr "账户已被禁用!" -#: flask_security/core.py:344 +#: flask_security/core.py:347 msgid "Email not provided" msgstr "未填写电子邮箱" -#: flask_security/core.py:345 +#: flask_security/core.py:348 msgid "Invalid email address" msgstr "无效邮箱地址" -#: flask_security/core.py:346 +#: flask_security/core.py:349 #, fuzzy msgid "Invalid code" msgstr "密码不正确" -#: flask_security/core.py:347 +#: flask_security/core.py:350 msgid "Password not provided" msgstr "未填写密码" -#: flask_security/core.py:348 +#: flask_security/core.py:351 msgid "No password is set for this user" msgstr "此账户未设置密码" -#: flask_security/core.py:350 -#, fuzzy, python-format +#: flask_security/core.py:353 +#, python-format msgid "Password must be at least %(length)s characters" -msgstr "密码至少6个字符" +msgstr "" -#: flask_security/core.py:353 +#: flask_security/core.py:356 msgid "Password not complex enough" msgstr "" -#: flask_security/core.py:354 +#: flask_security/core.py:357 msgid "Password on breached list" msgstr "" -#: flask_security/core.py:356 +#: flask_security/core.py:359 msgid "Failed to contact breached passwords site" msgstr "" -#: flask_security/core.py:359 +#: flask_security/core.py:362 msgid "Phone number not valid e.g. missing country code" msgstr "" -#: flask_security/core.py:360 +#: flask_security/core.py:363 msgid "Specified user does not exist" msgstr "此用户不存在" -#: flask_security/core.py:361 +#: flask_security/core.py:364 msgid "Invalid password" msgstr "密码不正确" -#: flask_security/core.py:362 +#: flask_security/core.py:365 msgid "Password or code submitted is not valid" msgstr "" -#: flask_security/core.py:363 +#: flask_security/core.py:366 msgid "You have successfully logged in." msgstr "你已成功登录!" -#: flask_security/core.py:364 +#: flask_security/core.py:367 msgid "Forgot password?" msgstr "忘记密码?" -#: flask_security/core.py:366 +#: flask_security/core.py:369 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "你的密码已成功重置,并已自动登录。" -#: flask_security/core.py:373 +#: flask_security/core.py:376 msgid "Your new password must be different than your previous password." msgstr "你的新密码不能与当前密码相同。" -#: flask_security/core.py:376 +#: flask_security/core.py:379 msgid "You successfully changed your password." msgstr "你已成功更改密码!" -#: flask_security/core.py:377 +#: flask_security/core.py:380 msgid "Please log in to access this page." msgstr "请登录访问此页面。" -#: flask_security/core.py:378 +#: flask_security/core.py:381 msgid "Please reauthenticate to access this page." msgstr "请重新进行身份验证,以访问此页面。" -#: flask_security/core.py:379 +#: flask_security/core.py:382 msgid "Reauthentication successful" msgstr "" -#: flask_security/core.py:381 +#: flask_security/core.py:384 msgid "You can only access this endpoint when not logged in." msgstr "" -#: flask_security/core.py:384 +#: flask_security/core.py:387 msgid "Failed to send code. Please try again later" msgstr "" -#: flask_security/core.py:385 +#: flask_security/core.py:388 msgid "Invalid Token" msgstr "" -#: flask_security/core.py:386 +#: flask_security/core.py:389 msgid "Your token has been confirmed" msgstr "" -#: flask_security/core.py:388 +#: flask_security/core.py:391 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 +#: flask_security/core.py:395 msgid "You currently do not have permissions to access this page" msgstr "" -#: flask_security/core.py:403 +#: flask_security/core.py:398 msgid "Marked method is not valid" msgstr "" -#: flask_security/core.py:405 +#: flask_security/core.py:400 msgid "You successfully disabled two factor authorization." msgstr "" -#: flask_security/core.py:408 +#: flask_security/core.py:403 msgid "Requested method is not valid" msgstr "" -#: flask_security/core.py:410 +#: flask_security/core.py:405 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "" -#: flask_security/core.py:413 +#: flask_security/core.py:408 msgid "Unified sign in setup successful" msgstr "" -#: flask_security/core.py:414 +#: flask_security/core.py:409 msgid "You must specify a valid identity to sign in" msgstr "" -#: flask_security/core.py:415 +#: flask_security/core.py:410 #, python-format msgid "Use this code to sign in: %(code)s." msgstr "" -#: flask_security/forms.py:50 +#: flask_security/forms.py:53 msgid "Email Address" msgstr "邮箱地址" -#: flask_security/forms.py:51 +#: flask_security/forms.py:54 msgid "Password" msgstr "密码" -#: flask_security/forms.py:52 +#: flask_security/forms.py:55 msgid "Remember Me" msgstr "记住我" -#: flask_security/forms.py:53 flask_security/templates/security/_menu.html:5 +#: flask_security/forms.py:56 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/forms.py:57 #: 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/forms.py:58 flask_security/templates/security/_menu.html:11 #: flask_security/templates/security/register_user.html:6 msgid "Register" msgstr "注册" -#: flask_security/forms.py:56 +#: flask_security/forms.py:59 msgid "Resend Confirmation Instructions" msgstr "重新发送邮件验证" -#: flask_security/forms.py:57 +#: flask_security/forms.py:60 msgid "Recover Password" msgstr "恢复密码" -#: flask_security/forms.py:58 +#: flask_security/forms.py:61 msgid "Reset Password" msgstr "重置密码" -#: flask_security/forms.py:59 +#: flask_security/forms.py:62 msgid "Retype Password" msgstr "再次确认密码" -#: flask_security/forms.py:60 +#: flask_security/forms.py:63 msgid "New Password" msgstr "新密码" -#: flask_security/forms.py:61 +#: flask_security/forms.py:64 msgid "Change Password" msgstr "更改密码" -#: flask_security/forms.py:62 +#: flask_security/forms.py:65 msgid "Send Login Link" msgstr "发送登录链接" -#: flask_security/forms.py:63 +#: flask_security/forms.py:66 msgid "Verify Password" msgstr "" -#: flask_security/forms.py:64 +#: flask_security/forms.py:67 msgid "Change Method" msgstr "" -#: flask_security/forms.py:65 +#: flask_security/forms.py:68 msgid "Phone Number" msgstr "" -#: flask_security/forms.py:66 +#: flask_security/forms.py:69 msgid "Authentication Code" msgstr "" -#: flask_security/forms.py:67 +#: flask_security/forms.py:70 msgid "Submit" msgstr "" -#: flask_security/forms.py:68 +#: flask_security/forms.py:71 msgid "Submit Code" msgstr "" -#: flask_security/forms.py:69 +#: flask_security/forms.py:72 msgid "Error(s)" msgstr "" -#: flask_security/forms.py:70 +#: flask_security/forms.py:73 msgid "Identity" msgstr "" -#: flask_security/forms.py:71 +#: flask_security/forms.py:74 msgid "Send Code" msgstr "" -#: flask_security/forms.py:72 -#, fuzzy +#: flask_security/forms.py:75 msgid "Passcode" -msgstr "密码" +msgstr "" -#: flask_security/unified_signin.py:145 -#, fuzzy +#: flask_security/unified_signin.py:131 msgid "Code or Password" -msgstr "恢复密码" +msgstr "" -#: flask_security/unified_signin.py:150 flask_security/unified_signin.py:270 +#: flask_security/unified_signin.py:136 flask_security/unified_signin.py:262 msgid "Available Methods" msgstr "" -#: flask_security/unified_signin.py:151 +#: flask_security/unified_signin.py:137 msgid "Via email" msgstr "" -#: flask_security/unified_signin.py:151 +#: flask_security/unified_signin.py:137 msgid "Via SMS" msgstr "" -#: flask_security/unified_signin.py:272 +#: flask_security/unified_signin.py:264 msgid "Set up using email" msgstr "" -#: flask_security/unified_signin.py:275 +#: flask_security/unified_signin.py:267 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" -#: flask_security/unified_signin.py:277 +#: flask_security/unified_signin.py:269 msgid "Set up using SMS" msgstr "" @@ -474,32 +476,31 @@ msgid "Resend confirmation instructions" msgstr "重新发送激活邮件" -#: flask_security/templates/security/two_factor_setup.html:6 +#: flask_security/templates/security/two_factor_setup.html:31 msgid "Two-factor authentication adds an extra layer of security to your account" msgstr "" -#: flask_security/templates/security/two_factor_setup.html:7 +#: flask_security/templates/security/two_factor_setup.html:32 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 +#: flask_security/templates/security/two_factor_setup.html:43 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 +#: flask_security/templates/security/two_factor_setup.html:49 msgid "" -"Open your authenticator app on your device and scan the following qrcode " -"to start receiving codes:" +"Open an authenticator app on your device and scan the following QRcode " +"(or enter the code below manually) to start receiving codes:" msgstr "" -#: flask_security/templates/security/two_factor_setup.html:22 +#: flask_security/templates/security/two_factor_setup.html:52 msgid "Two factor authentication code" msgstr "" -#: flask_security/templates/security/two_factor_setup.html:25 +#: flask_security/templates/security/two_factor_setup.html:60 msgid "To Which Phone Number Should We Send Code To?" msgstr "" @@ -519,27 +520,27 @@ 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 +#: flask_security/templates/security/us_setup.html:36 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_setup.html:58 #: flask_security/templates/security/us_signin.html:23 #: flask_security/templates/security/us_verify.html:21 -#, fuzzy msgid "Code has been sent" -msgstr "你的密码已重置" +msgstr "" + +#: flask_security/templates/security/us_setup.html:66 +msgid "" +"Open an authenticator app on your device and scan the following QRcode " +"(or enter the code below manually) to start receiving passcodes:" +msgstr "" + +#: flask_security/templates/security/us_setup.html:69 +msgid "Passwordless QRCode" +msgstr "" -#: flask_security/templates/security/us_setup.html:29 +#: flask_security/templates/security/us_setup.html:77 msgid "No methods have been enabled - nothing to setup" msgstr "" @@ -553,6 +554,10 @@ msgid "Please re-authenticate" msgstr "请重新进行身份验证,以访问此页面。" +#: flask_security/templates/security/verify.html:6 +msgid "Please Enter Your Password" +msgstr "" + #: flask_security/templates/security/email/change_notice.html:1 msgid "Your password has been changed." msgstr "你的密码已更改。" @@ -565,7 +570,12 @@ msgid "click here to reset it" msgstr "点击这里重置密码" +#: flask_security/templates/security/email/change_notice.txt:3 +msgid "If you did not change your password, click the link below to reset it." +msgstr "" + #: flask_security/templates/security/email/confirmation_instructions.html:1 +#: flask_security/templates/security/email/confirmation_instructions.txt:1 msgid "Please confirm your email through the link below:" msgstr "请通过下面链接激活的你的邮箱:" @@ -575,12 +585,15 @@ msgstr "激活账户" #: flask_security/templates/security/email/login_instructions.html:1 +#: flask_security/templates/security/email/login_instructions.txt:1 #: flask_security/templates/security/email/welcome.html:1 +#: flask_security/templates/security/email/welcome.txt:1 #, python-format msgid "Welcome %(email)s!" msgstr "欢迎你,%(email)s!" #: flask_security/templates/security/email/login_instructions.html:3 +#: flask_security/templates/security/email/login_instructions.txt:3 msgid "You can log into your account through the link below:" msgstr "你可以通过下面链接登录的你的账户:" @@ -592,25 +605,46 @@ msgid "Click here to reset your password" msgstr "点击这里重置密码" +#: flask_security/templates/security/email/reset_instructions.txt:1 +msgid "Click the link below to reset your password:" +msgstr "" + #: flask_security/templates/security/email/two_factor_instructions.html:3 +#: flask_security/templates/security/email/two_factor_instructions.txt:3 msgid "You can log into your account using the following code:" msgstr "" #: flask_security/templates/security/email/two_factor_rescue.html:1 +#: flask_security/templates/security/email/two_factor_rescue.txt:1 msgid "can not access mail account" msgstr "" #: flask_security/templates/security/email/us_instructions.html:3 -#, fuzzy +#: flask_security/templates/security/email/us_instructions.txt:3 msgid "You can sign into your account using the following code:" -msgstr "你可以通过下面链接登录的你的账户:" +msgstr "" #: flask_security/templates/security/email/us_instructions.html:6 -#, fuzzy msgid "Or use the the link below:" -msgstr "你可以通过下面链接激活你的邮箱:" +msgstr "" + +#: flask_security/templates/security/email/us_instructions.txt:7 +msgid "Or use the link below:" +msgstr "" #: flask_security/templates/security/email/welcome.html:4 +#: flask_security/templates/security/email/welcome.txt:4 msgid "You can confirm your email through the link below:" msgstr "你可以通过下面链接激活你的邮箱:" +#~ msgid "You successfully confirmed password" +#~ msgstr "" + +#~ msgid "Password confirmation is needed in order to access page" +#~ msgstr "" + +#~ msgid "" +#~ "Open your authenticator app on your " +#~ "device and scan the following qrcode " +#~ "to start receiving codes:" +#~ msgstr "" diff -Nru flask-security-3.4.2/flask_security/twofactor.py flask-security-4.0.0/flask_security/twofactor.py --- flask-security-3.4.2/flask_security/twofactor.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/twofactor.py 2021-01-26 02:39:51.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ flask_security.two_factor ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -16,10 +15,13 @@ from .utils import ( SmsSenderFactory, base_render_json, + check_and_get_token_status, config_value, do_flash, + get_within_delta, login_user, json_error_response, + send_mail, url_for_security, ) from .signals import ( @@ -43,7 +45,6 @@ "tf_state", "tf_user_id", "tf_primary_method", - "tf_confirmed", "tf_remember_login", "tf_totp_secret", ]: @@ -67,7 +68,7 @@ """ token_to_be_sent = _security._totp_factory.generate_totp_password(totp_secret) if method == "email" or method == "mail": - _security._send_mail( + send_mail( config_value("EMAIL_SUBJECT_TWO_FACTOR"), user.email, "two_factor_instructions", @@ -98,7 +99,7 @@ user, primary_method, totp_secret, is_changing, remember_login=None ): """clean session according to process (login or changing two-factor method) - and perform action accordingly + and perform action accordingly """ _datastore.tf_set(user, primary_method, totp_secret=totp_secret) @@ -133,7 +134,7 @@ def tf_login(user, remember=None, primary_authn_via=None): - """ Helper for two-factor authentication login + """Helper for two-factor authentication login This is called only when login/password have already been validated. This can be from login, register, confirm, unified sign in, unified magic link. @@ -148,7 +149,7 @@ # until complete 2FA. tf_clean_session() - session["tf_user_id"] = user.id + session["tf_user_id"] = user.fs_uniquifier if "remember": session["tf_remember_login"] = remember @@ -170,7 +171,7 @@ msg = user.tf_send_security_token( method=user.tf_primary_method, totp_secret=user.tf_totp_secret, - phone_number=user.tf_phone_number, + phone_number=getattr(user, "tf_phone_number", None), ) if msg: # send code didn't work @@ -190,3 +191,64 @@ form.user = user return base_render_json(form, include_user=False, additional=json_response) + + +def generate_tf_validity_token(fs_uniqifier): + """Generates a unique token for the specified user. + + :param fs_uniqifier: The fs_uniqifier of a user to whom the token belongs to + """ + return _security.tf_validity_serializer.dumps(fs_uniqifier) + + +def tf_validity_token_status(token): + """Returns the expired status, invalid status, and user of a + Two-Factor Validity token. + For example:: + + expired, invalid, user = tf_validity_token_status('...') + + :param token: The Two-Factor Validity token + """ + return check_and_get_token_status( + token, "tf_validity", get_within_delta("TWO_FACTOR_LOGIN_VALIDITY") + ) + + +def tf_verify_validility_token(token, fs_uniquifier): + """Returns the status of the Two-Factor Validity token + + :param token: The Two-Factor Validity token + :param fs_uniquifier: The ``fs_uniquifier`` of the submitting user. + """ + if token is None: + return False + + expired, invalid, uniquifier = tf_validity_token_status(token) + + if expired or invalid or (fs_uniquifier != uniquifier): + + return False + + return True + + +def tf_set_validity_token_cookie(response, fs_uniquifier=None, remember=False): + """Sets the Two-Factor validity token for a specific user given that is + configured and the user selects remember me + + :param response: The response with which to set the set_cookie + :param fs_uniquifier: The ``fs_uniquifier`` of a user that has succcessfully + authenticated and validated with Two-Factor + authentication. + :param remember: Flag specifying if the tf_validity cookie should be set. + """ + if not config_value("TWO_FACTOR_ALWAYS_VALIDATE") and remember: + token = generate_tf_validity_token(fs_uniquifier) + cookie_kwargs = config_value("TWO_FACTOR_VALIDITY_COOKIE") + max_age = int(get_within_delta("TWO_FACTOR_LOGIN_VALIDITY").total_seconds()) + response.set_cookie( + "tf_validity", value=token, max_age=max_age, **cookie_kwargs + ) + + return response diff -Nru flask-security-3.4.2/flask_security/unified_signin.py flask-security-4.0.0/flask_security/unified_signin.py --- flask-security-3.4.2/flask_security/unified_signin.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/unified_signin.py 2021-01-26 02:39:51.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ flask_security.unified_signin ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -16,7 +15,6 @@ Finish up: - we should be able to add a phone number as part of setup even w/o any METHODS - i.e. to allow login with any identity (phone) and a password. - - add username as last IDENTITY_MAPPING and allow anything...?? or just in example? Consider/Questions: - Allow registering/confirming with just a phone number - this likely would require @@ -30,11 +28,10 @@ """ -import sys import time from flask import current_app as app -from flask import abort, after_this_request, redirect, request, session +from flask import after_this_request, request, session from flask_login import current_user from werkzeug.datastructures import MultiDict from werkzeug.local import LocalProxy @@ -45,7 +42,11 @@ from .forms import Form, Required, get_form_field_label from .quart_compat import get_quart_status from .signals import us_profile_changed, us_security_token_sent -from .twofactor import is_tf_setup, tf_login +from .twofactor import ( + is_tf_setup, + tf_login, + tf_verify_validility_token, +) from .utils import ( _, SmsSenderFactory, @@ -53,6 +54,8 @@ check_and_get_token_status, config_value, do_flash, + find_user, + get_identity_attributes, get_post_login_redirect, get_post_verify_redirect, get_message, @@ -61,23 +64,22 @@ json_error_response, login_user, propagate_next, + send_mail, suppress_form_csrf, url_for_security, + view_commit, ) # Convenient references _security = LocalProxy(lambda: app.extensions["security"]) _datastore = LocalProxy(lambda: _security.datastore) +if get_quart_status(): # pragma: no cover + from quart import redirect -PY3 = sys.version_info[0] == 3 -if PY3 and get_quart_status(): # pragma: no cover - from .async_compat import _commit # noqa: F401 -else: - def _commit(response=None): - _datastore.commit() - return response +else: + from flask import redirect def _compute_code_methods(): @@ -108,34 +110,18 @@ # Validate identity - we go in order to figure out which user attribute the # request gave us. Note that we give up on the first 'match' even if that # doesn't yield a user. Why? - for mapping in config_value("USER_IDENTITY_MAPPINGS"): - # What we want is an ordered dict - but those don't exist for py27 - - # so there is really just one element here. - for ua, mapper in mapping.items(): - # Make sure we don't validate on a column that application - # hasn't specifically configured as a unique/identity column - # In other words - might have a phone number for 2FA or unified - # but don't want the user to be able to use that as primary identity - if ua in config_value("USER_IDENTITY_ATTRIBUTES"): - # Allow mapper to alter (coerce) to type DB requires - idata = mapper(form.identity.data) - if idata is not None: - form.user = _datastore.find_user(**{ua: idata}) - if not form.user: - form.identity.errors.append( - get_message("US_SPECIFY_IDENTITY")[0] - ) - return False - if not form.user.is_active: - form.identity.errors.append(get_message("DISABLED_ACCOUNT")[0]) - return False - return True - return False + form.user = find_user(form.identity.data) + if not form.user: + form.identity.errors.append(get_message("US_SPECIFY_IDENTITY")[0]) + return False + if not form.user.is_active: + form.identity.errors.append(get_message("DISABLED_ACCOUNT")[0]) + return False + return True class _UnifiedPassCodeForm(Form): - """ Common form for signin and verify/reauthenticate. - """ + """Common form for signin and verify/reauthenticate.""" user = None authn_via = None @@ -154,10 +140,10 @@ submit_send_code = SubmitField(get_form_field_label("sendcode")) def __init__(self, *args, **kwargs): - super(_UnifiedPassCodeForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def validate(self): - if not super(_UnifiedPassCodeForm, self).validate(): + if not super().validate(): return False if not self.user: # This is sign-in case. @@ -179,6 +165,7 @@ ok = False for method in config_value("US_ENABLED_METHODS"): if method == "password": + passcode = _security._password_util.normalize(passcode) if self.user.verify_and_update_password(passcode): ok = True break @@ -222,35 +209,40 @@ class UnifiedSigninForm(_UnifiedPassCodeForm): - """ A unified login form + """A unified login form For either identity/password or request and enter code. """ user = None - identity = StringField(get_form_field_label("identity"), validators=[Required()],) + identity = StringField( + get_form_field_label("identity"), + validators=[Required()], + ) remember = BooleanField(get_form_field_label("remember_me")) def __init__(self, *args, **kwargs): - super(UnifiedSigninForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.remember.default = config_value("DEFAULT_REMEMBER_ME") + self.requires_confirmation = False def validate(self): self.user = None - if not super(UnifiedSigninForm, self).validate(): + if not super().validate(): return False if self.submit.data: # This is login # Only check this once authenticated to not give away info - if requires_confirmation(self.user): + self.requires_confirmation = requires_confirmation(self.user) + if self.requires_confirmation: self.identity.errors.append(get_message("CONFIRMATION_REQUIRED")[0]) return False return True class UnifiedVerifyForm(_UnifiedPassCodeForm): - """ Verify authentication. + """Verify authentication. This is for freshness 'reauthentication' required. """ @@ -258,7 +250,7 @@ def validate(self): self.user = current_user - if not super(UnifiedVerifyForm, self).validate(): + if not super().validate(): return False return True @@ -281,10 +273,10 @@ submit = SubmitField(get_form_field_label("submit")) def __init__(self, *args, **kwargs): - super(UnifiedSigninSetupForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def validate(self): - if not super(UnifiedSigninSetupForm, self).validate(): + if not super().validate(): return False if self.chosen_method.data not in config_value("US_ENABLED_METHODS"): self.chosen_method.errors.append(get_message("US_METHOD_NOT_AVAILABLE")[0]) @@ -310,10 +302,10 @@ submit = SubmitField(get_form_field_label("submitcode")) def __init__(self, *args, **kwargs): - super(UnifiedSigninSetupValidateForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def validate(self): - if not super(UnifiedSigninSetupValidateForm, self).validate(): + if not super().validate(): return False if not _security._totp_factory.verify_totp( @@ -337,14 +329,14 @@ # mechanisms of course don't work. We rely on the fact that the user went # through the 'confirmation' process to validate the email. if method == "email" and method not in totp_secrets: - after_this_request(_commit) + after_this_request(view_commit) totp_secrets[method] = _security._totp_factory.generate_totp_secret() _datastore.us_put_totp_secrets(user, totp_secrets) msg = user.us_send_security_token( method, totp_secret=totp_secrets[method], - phone_number=user.us_phone_number, + phone_number=getattr(user, "us_phone_number", None), send_magic_link=True, ) code_sent = True @@ -400,7 +392,7 @@ payload = { "available_methods": config_value("US_ENABLED_METHODS"), "code_methods": code_methods, - "identity_attributes": config_value("USER_IDENTITY_ATTRIBUTES"), + "identity_attributes": get_identity_attributes(), } return base_render_json(form, include_user=False, additional=payload) @@ -414,7 +406,7 @@ ) -@auth_required() +@auth_required(lambda: config_value("API_ENABLED_METHODS")) def us_verify_send_code(): """ Send code during verify. @@ -519,20 +511,34 @@ form.submit.data = True if form.validate_on_submit(): + # Require multi-factor is it is enabled, and the method # we authenticated with requires it and either user has requested MFA or it is # required. remember_me = form.remember.data if "remember" in form else None - if ( - config_value("TWO_FACTOR") - and form.authn_via in config_value("US_MFA_REQUIRED") - and (config_value("TWO_FACTOR_REQUIRED") or is_tf_setup(form.user)) + if config_value("TWO_FACTOR") and form.authn_via in config_value( + "US_MFA_REQUIRED" ): - return tf_login( - form.user, remember=remember_me, primary_authn_via=form.authn_via - ) + if request.is_json and request.content_length: + tf_validity_token = request.get_json().get("tf_validity_token", None) + else: + tf_validity_token = request.cookies.get("tf_validity", default=None) + + tf_validity_token_is_valid = tf_verify_validility_token( + tf_validity_token, form.user.fs_uniquifier + ) + if config_value("TWO_FACTOR_REQUIRED") or is_tf_setup(form.user): + if config_value("TWO_FACTOR_ALWAYS_VALIDATE") or ( + not tf_validity_token_is_valid + ): + + return tf_login( + form.user, + remember=remember_me, + primary_authn_via=form.authn_via, + ) - after_this_request(_commit) + after_this_request(view_commit) login_user(form.user, remember=remember_me, authn_via=[form.authn_via]) if _security._want_json(request): @@ -546,7 +552,7 @@ payload = { "available_methods": config_value("US_ENABLED_METHODS"), "code_methods": code_methods, - "identity_attributes": config_value("USER_IDENTITY_ATTRIBUTES"), + "identity_attributes": get_identity_attributes(), } return base_render_json(form, include_user=False, additional=payload) @@ -557,6 +563,11 @@ # On error - wipe code form.passcode.data = None + + if form.requires_confirmation and _security.requires_confirmation_error_view: + do_flash(*get_message("CONFIRMATION_REQUIRED")) + return redirect(get_url(_security.requires_confirmation_error_view)) + return _security.render_template( config_value("US_SIGNIN_TEMPLATE"), us_signin_form=form, @@ -567,7 +578,7 @@ ) -@auth_required() +@auth_required(lambda: config_value("API_ENABLED_METHODS")) def us_verify(): """ Re-authenticate to reset freshness time. @@ -681,7 +692,7 @@ return tf_login(user, primary_authn_via="email") login_user(user, authn_via=["email"]) - after_this_request(_commit) + after_this_request(view_commit) if _security.redirect_behavior == "spa": # We do NOT send the authentication token here since the only way to # send it would be via a query param and that isn't secure. (logging and @@ -697,6 +708,7 @@ @auth_required( + lambda: config_value("API_ENABLED_METHODS"), within=lambda: config_value("FRESHNESS"), grace=lambda: config_value("FRESHNESS_GRACE_PERIOD"), ) @@ -729,17 +741,20 @@ # N.B. totp (totp_secret) is actually encrypted - so it seems safe enough # to send it to the user. # Only check phone number if SMS (see form validate) + phone_number = ( + _security._phone_util.get_canonical_form(form.phone.data) + if method == "sms" + else None + ) state = { "totp_secret": totp, "chosen_method": method, - "phone_number": _security._phone_util.get_canonical_form(form.phone.data) - if method == "sms" - else None, + "phone_number": phone_number, } msg = current_user.us_send_security_token( method=method, - totp_secret=state["totp_secret"], - phone_number=state["phone_number"], + totp_secret=totp, + phone_number=phone_number, ) if msg: # sending didn't work. @@ -759,10 +774,31 @@ ) state_token = _security.us_setup_serializer.dumps(state) + json_response = dict( + chosen_method=form.chosen_method.data, + phone=phone_number, + state=state_token, + ) + qrcode_values = dict() + if form.chosen_method.data == "authenticator": + authr_setup_values = _security._totp_factory.fetch_setup_values( + totp, current_user + ) + + # Add all the values used in qrcode to json response + json_response["authr_key"] = authr_setup_values["key"] + json_response["authr_username"] = authr_setup_values["username"] + json_response["authr_issuer"] = authr_setup_values["issuer"] + + qrcode_values = dict( + authr_qrcode=authr_setup_values["image"], + authr_key=authr_setup_values["key"], + authr_username=authr_setup_values["username"], + authr_issuer=authr_setup_values["issuer"], + ) if _security._want_json(request): - payload = {"state": state_token, "chosen_method": form.chosen_method.data} - return base_render_json(form, include_user=False, additional=payload) + return base_render_json(form, include_user=False, additional=json_response) return _security.render_template( config_value("US_SETUP_TEMPLATE"), available_methods=config_value("US_ENABLED_METHODS"), @@ -772,6 +808,7 @@ chosen_method=form.chosen_method.data, us_setup_form=form, us_setup_validate_form=_security.us_setup_validate_form(), + **qrcode_values, state=state_token, **_security._run_ctx_processor("us_setup") ) @@ -780,7 +817,7 @@ # Or failure of POST if _security._want_json(request): payload = { - "identity_attributes": config_value("USER_IDENTITY_ATTRIBUTES"), + "identity_attributes": get_identity_attributes(), "available_methods": config_value("US_ENABLED_METHODS"), "active_methods": active_methods, "setup_methods": setup_methods, @@ -800,7 +837,7 @@ ) -@auth_required() +@auth_required(lambda: config_value("API_ENABLED_METHODS")) def us_setup_validate(token): """ Validate new setup. @@ -833,7 +870,7 @@ form.user = current_user if form.validate_on_submit(): - after_this_request(_commit) + after_this_request(view_commit) method = state["chosen_method"] phone = state["phone_number"] if method == "sms" else None _datastore.us_set(current_user, method, state["totp_secret"], phone) @@ -864,50 +901,10 @@ return redirect(url_for_security("us_setup")) -@auth_required() -def us_qrcode(token): - - if "authenticator" not in config_value("US_ENABLED_METHODS"): - return abort(404) - expired, invalid, state = check_and_get_token_status( - token, "us_setup", get_within_delta("US_SETUP_WITHIN") - ) - if expired or invalid: - return abort(400) - - try: - import pyqrcode - - # By convention, the URI should have the username that the user - # logs in with. - username = current_user.calc_username() - url = pyqrcode.create( - _security._totp_factory.get_totp_uri( - username if username else "Unknown", state["totp_secret"] - ) - ) - except ImportError: # pragma: no cover - raise - from io import BytesIO - - stream = BytesIO() - url.svg(stream, scale=3) - return ( - stream.getvalue(), - 200, - { - "Content-Type": "image/svg+xml", - "Cache-Control": "no-cache, no-store, must-revalidate", - "Pragma": "no-cache", - "Expires": "0", - }, - ) - - def us_send_security_token( user, method, totp_secret, phone_number, send_magic_link=False ): - """ Generate and send the security code. + """Generate and send the security code. :param user: The user to send the code to :param method: The method in which the code will be sent @@ -932,7 +929,7 @@ login_link = url_for_security( "us_verify_link", email=user.email, code=token, _external=True ) - _security._send_mail( + send_mail( config_value("US_EMAIL_SUBJECT"), user.email, "us_instructions", diff -Nru flask-security-3.4.2/flask_security/utils.py flask-security-4.0.0/flask_security/utils.py --- flask-security-3.4.2/flask_security/utils.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/utils.py 2021-01-26 02:39:51.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ flask_security.utils ~~~~~~~~~~~~~~~~~~~~ @@ -15,40 +14,38 @@ from functools import partial import hashlib import hmac -import sys import time +from typing import Dict, List import warnings -from contextlib import contextmanager from datetime import timedelta - -from flask import _request_ctx_stack, current_app, flash, g, request, session, url_for +from urllib.parse import parse_qsl, parse_qs, urlsplit, urlunsplit, urlencode +import urllib.request +import urllib.error + +from flask import ( + _request_ctx_stack, + after_this_request, + current_app, + flash, + g, + request, + session, + url_for, +) from flask.json import JSONEncoder -from flask.signals import message_flashed from flask_login import login_user as _login_user from flask_login import logout_user as _logout_user from flask_login import current_user from flask_login import COOKIE_NAME as REMEMBER_COOKIE_NAME -from flask_mail import Message from flask_principal import AnonymousIdentity, Identity, identity_changed, Need from flask_wtf import csrf -from wtforms import validators, ValidationError +from wtforms import ValidationError from itsdangerous import BadSignature, SignatureExpired -from speaklater import is_lazy_string from werkzeug.local import LocalProxy from werkzeug.datastructures import MultiDict -from .quart_compat import best -from .signals import ( - login_instructions_sent, - reset_password_instructions_sent, - user_authenticated, - user_registered, -) -try: # pragma: no cover - from urlparse import parse_qsl, parse_qs, urlsplit, urlunsplit - from urllib import urlencode -except ImportError: # pragma: no cover - from urllib.parse import parse_qsl, parse_qs, urlsplit, urlunsplit, urlencode +from .quart_compat import best, get_quart_status +from .signals import user_authenticated # Convenient references _security = LocalProxy(lambda: current_app.extensions["security"]) @@ -61,15 +58,6 @@ localize_callback = LocalProxy(lambda: _security.i18n_domain.gettext) -PY3 = sys.version_info[0] == 3 - -if PY3: # pragma: no cover - string_types = (str,) # noqa - text_type = str # noqa -else: # pragma: no cover - string_types = (basestring,) # noqa - text_type = unicode # noqa - FsPermNeed = partial(Need, "fsperm") FsPermNeed.__doc__ = """A need with the method preset to `"fsperm"`.""" @@ -79,6 +67,44 @@ return translate +def get_request_attr(name): + """Retrieve a request local attribute. + + Currently public attributes are: + + **fs_authn_via** + will be set to the authentication mechanism (session, token, basic) + that the current request was authenticated with. + + Returns None if attribute doesn't exist. + + .. versionadded:: 4.0.0 + """ + return getattr(_request_ctx_stack.top, name, None) + + +def set_request_attr(name, value): + return setattr(_request_ctx_stack.top, name, value) + + +""" +Most view functions that modify the DB will call ``after_this_request(view_commit)`` +Quart compatibility needs an async version +""" +if get_quart_status(): # pragma: no cover + + async def view_commit(response=None): + _datastore.commit() + return response + + +else: + + def view_commit(response=None): + _datastore.commit() + return response + + def find_csrf_field_name(): """ We need to clear it on logout (since that isn't being done by Flask-WTF). @@ -133,7 +159,9 @@ session["fs_cc"] = "set" # CSRF cookie session["fs_paa"] = time.time() # Primary authentication at - timestamp - identity_changed.send(current_app._get_current_object(), identity=Identity(user.id)) + identity_changed.send( + current_app._get_current_object(), identity=Identity(user.fs_uniquifier) + ) user_authenticated.send( current_app._get_current_object(), user=user, authn_via=authn_via @@ -169,12 +197,8 @@ _logout_user() -def _py2timestamp(dt): - return time.mktime(dt.timetuple()) + dt.microsecond / 1e6 - - -def check_and_update_authn_fresh(within, grace): - """ Check if user authenticated within specified time and update grace period. +def check_and_update_authn_fresh(within, grace, method=None): + """Check if user authenticated within specified time and update grace period. :param within: A timedelta specifying the maximum time in the past that the caller authenticated that is still considered 'fresh'. @@ -182,6 +206,8 @@ will set a grace period for which freshness won't be checked. The intent here is that the caller shouldn't get part-way though a set of operations and suddenly be required to authenticate again. + :param method: Optional - if set and == "basic" then will always return True. + (since basic-auth sends username/password on every request) If within.total_seconds() is negative, will always return True (always 'fresh'). This effectively just disables this entire mechanism. @@ -193,14 +219,21 @@ return False (not fresh). Be aware that for this to work, sessions and therefore session cookies - must be functioning and being sent as part of the request. + must be functioning and being sent as part of the request. If the required + state isn't in the session cookie then return False (not 'fresh'). .. warning:: Be sure the caller is already authenticated PRIOR to calling this method. .. versionadded:: 3.4.0 + + .. versionchanged:: 4.0.0 + Added `method` parameter. """ + if method == "basic": + return True + if within.total_seconds() < 0: # this means 'always fresh' return True @@ -211,13 +244,11 @@ now = datetime.datetime.utcnow() new_exp = now + grace - # grace_ts = int(new_exp.timestamp()) - grace_ts = int(_py2timestamp(new_exp)) + grace_ts = int(new_exp.timestamp()) fs_gexp = session.get("fs_gexp", None) if fs_gexp: - # if now.timestamp() < fs_gexp: - if _py2timestamp(now) < fs_gexp: + if now.timestamp() < fs_gexp: # Within grace period - extend it and we're good. session["fs_gexp"] = grace_ts return True @@ -262,6 +293,9 @@ :param password: A plaintext password to verify :param password_hash: The expected hash value of the password (usually from your database) + + .. note:: + Make sure that the password passed in has already been normalized. """ if use_double_hash(password_hash): password = get_hmac(password) @@ -340,7 +374,7 @@ password, **config_value("PASSWORD_HASH_OPTIONS", default={}).get( _security.password_hash, {} - ) + ), ) @@ -349,7 +383,7 @@ :param string: The string to encode""" - if isinstance(string, text_type): + if isinstance(string, str): string = string.encode("utf-8") return string @@ -369,8 +403,7 @@ If app doesn't want CSRF for unauth endpoints then check if caller is authenticated or not (many endpoints can be called either way). """ - ctx = _request_ctx_stack.top - if hasattr(ctx, "fs_ignore_csrf") and ctx.fs_ignore_csrf: + if get_request_attr("fs_ignore_csrf"): # This is the case where CsrfProtect was already called (e.g. @auth_required) return {"csrf": False} if ( @@ -426,7 +459,7 @@ def transform_url(url, qparams=None, **kwargs): - """ Modify url + """Modify url :param url: url to transform (can be relative) :param qparams: additional query params to add to end of url @@ -446,7 +479,7 @@ def get_security_endpoint_name(endpoint): - return "%s.%s" % (_security.blueprint_name, endpoint) + return f"{_security.blueprint_name}.{endpoint}" def url_for_security(endpoint, **values): @@ -470,7 +503,18 @@ url_next = urlsplit(url) url_base = urlsplit(request.host_url) if (url_next.netloc or url_next.scheme) and url_next.netloc != url_base.netloc: - return False + base_domain = current_app.config.get("SERVER_NAME") + if ( + config_value("REDIRECT_ALLOW_SUBDOMAINS") + and base_domain + and ( + url_next.netloc == base_domain + or url_next.netloc.endswith(f".{base_domain}") + ) + ): + return True + else: + return False return True @@ -511,7 +555,7 @@ rv = ( get_url(session.pop(key.lower(), None)) or get_url(current_app.config[key.upper()] or None) - or "/" + or current_app.config.get("APPLICATION_ROOT", "/") ) return rv @@ -535,7 +579,7 @@ prefix = "SECURITY_" def strip_prefix(tup): - return (tup[0].replace("SECURITY_", ""), tup[1]) + return tup[0].replace("SECURITY_", ""), tup[1] return dict([strip_prefix(i) for i in items if i[0].startswith(prefix)]) @@ -554,7 +598,11 @@ :param default: An optional default value if the value is not set """ app = app or current_app - return get_config(app).get(key.upper(), default) + # protect against spelling mistakes + config = get_config(app) + if key.upper() not in config: + raise ValueError(f"Key {key} doesn't exist") + return config.get(key.upper(), default) def get_max_age(key, app=None): @@ -583,35 +631,37 @@ def send_mail(subject, recipient, template, **context): - """Send an email via the Flask-Mail extension. + """Send an email. :param subject: Email subject :param recipient: Email recipient :param template: The name of the email template :param context: The context to render the template with + + This formats the email and passes off to :class:`.MailUtil` to actuall send the + message. """ context.setdefault("security", _security) context.update(_security._run_ctx_processor("mail")) - sender = _security.email_sender - if isinstance(sender, LocalProxy): - sender = sender._get_current_object() - - msg = Message(subject, sender=sender, recipients=[recipient]) - + body = None + html = None ctx = ("security/email", template) if config_value("EMAIL_PLAINTEXT"): - msg.body = _security.render_template("%s/%s.txt" % ctx, **context) + body = _security.render_template("%s/%s.txt" % ctx, **context) if config_value("EMAIL_HTML"): - msg.html = _security.render_template("%s/%s.html" % ctx, **context) + html = _security.render_template("%s/%s.html" % ctx, **context) - if _security._send_mail_task: - _security._send_mail_task(msg) - return + subject = localize_callback(subject) - mail = current_app.extensions.get("mail") - mail.send(msg) + sender = _security.email_sender + if isinstance(sender, LocalProxy): + sender = sender._get_current_object() + + _security._mail_util.send_mail( + template, subject, recipient, str(sender), body, html, context.get("user", None) + ) def get_token_status(token, serializer, max_age=None, return_data=False): @@ -638,7 +688,7 @@ invalid = True if data: - user = _datastore.find_user(id=data[0]) + user = _datastore.find_user(fs_uniquifier=data[0]) expired = expired and (user is not None) @@ -676,21 +726,55 @@ return expired, invalid, data -def get_identity_attributes(app=None): +def get_identity_attributes(app=None) -> List: + # Return list of keys of identity attributes + # Is it possible to not have any? app = app or current_app - attrs = app.config["SECURITY_USER_IDENTITY_ATTRIBUTES"] - try: - attrs = [f.strip() for f in attrs.split(",")] - except AttributeError: - pass - return attrs + iattrs = app.config["SECURITY_USER_IDENTITY_ATTRIBUTES"] + if iattrs: + return [[*f][0] for f in iattrs] + return [] + + +def get_identity_attribute(attr, app=None) -> Dict: + """Given an user_identity_attribute, return the defining dict. + A bit annoying since USER_IDENTITY_ATTRIBUTES is a list of dict + where each dict has just one key. + """ + app = app or current_app + iattrs = app.config["SECURITY_USER_IDENTITY_ATTRIBUTES"] + if iattrs: + details = [ + mapping[attr] for mapping in iattrs if list(mapping.keys())[0] == attr + ] + if details: + return details[0] + return {} + + +def find_user(identity): + """ + Validate identity - we go in order to figure out which user attribute the + request gave us. Note that we give up on the first 'match' even if that + doesn't yield a user. Why? + """ + for mapping in config_value("USER_IDENTITY_ATTRIBUTES"): + attr = list(mapping.keys())[0] + details = mapping[attr] + idata = details["mapper"](identity) + if idata: + user = _datastore.find_user( + case_insensitive=details.get("case_insensitive", False), **{attr: idata} + ) + return user + return None def uia_phone_mapper(identity): - """ Used to match identity as a phone number. This is a simple proxy + """Used to match identity as a phone number. This is a simple proxy to :py:class:`PhoneUtil` - See :py:data:`SECURITY_USER_IDENTITY_MAPPINGS`. + See :py:data:`SECURITY_USER_IDENTITY_ATTRIBUTES`. .. versionadded:: 3.4.0 """ @@ -699,25 +783,17 @@ def uia_email_mapper(identity): - """ Used to match identity as an email. + """Used to match identity as an email. - See :py:data:`SECURITY_USER_IDENTITY_MAPPINGS`. + See :py:data:`SECURITY_USER_IDENTITY_ATTRIBUTES`. .. versionadded:: 3.4.0 """ - # Fake up enough to invoke the WTforms email validator. - class FakeField(object): - pass - - email_validator = validators.Email(message="nothing") - field = FakeField() - setattr(field, "data", identity) try: - email_validator(None, field) - except ValidationError: + return _security._mail_util.normalize(identity) + except ValueError: return None - return identity def use_double_hash(password_hash=None): @@ -735,7 +811,7 @@ def csrf_cookie_handler(response): - """ Called at end of every request. + """Called at end of every request. Uses session to track state (set/clear) Ideally we just need to set this once - however by default @@ -812,6 +888,11 @@ additional=None, error_status_code=400, ): + """ + This method is called by all views that return JSON responses. + This fills in the response and then calls :meth:`.Security.render_json` + which can be overridden by the app. + """ has_errors = len(form.errors) > 0 user = form.user if hasattr(form, "user") else None @@ -827,12 +908,20 @@ payload["user"] = user.get_security_payload() if include_auth_token: - # view wants to return auth_token - check behavior config + # view willing to return auth_token - check behavior config if ( config_value("BACKWARDS_COMPAT_AUTH_TOKEN") or "include_auth_token" in request.args ): - token = user.get_auth_token() + try: + token = user.get_auth_token() + except ValueError: + # application has fs_token_uniquifier attribute but it + # hasn't been initialized. Since we are in a request context + # we can do that here. + _datastore.set_token_uniquifier(user) + after_this_request(view_commit) + token = user.get_auth_token() payload["user"]["authentication_token"] = token # Return csrf_token on each JSON response - just as every form @@ -845,7 +934,7 @@ def default_want_json(req): - """ Return True if response should be in json + """Return True if response should be in json N.B. do not call this directly - use security.want_json() :param req: Flask/Werkzeug Request @@ -857,23 +946,15 @@ if not hasattr(req.accept_mimetypes, "best"): # pragma: no cover # Alright. we dont have the best property, lets add it ourselves. # This is for quart compatibility - setattr(accept_mimetypes, "best", best) + accept_mimetypes.best = best if accept_mimetypes.best == "application/json": return True return False def json_error_response(errors): - """ Helper to create an error response that adheres to the openapi spec. - """ - # Python 2 and 3 compatibility for checking if something is a string. - try: # pragma: no cover - basestring - string_type_check = (basestring, unicode) - except NameError: # pragma: no cover - string_type_check = str - - if isinstance(errors, string_type_check): + """Helper to create an error response that adheres to the openapi spec.""" + if isinstance(errors, str): # When the errors is a string, use the response/error/message format response_json = dict(error=errors) elif isinstance(errors, dict): @@ -887,96 +968,28 @@ class FsJsonEncoder(JSONEncoder): - """ Flask-Security JSON encoder. + """Flask-Security JSON encoder. Extends Flask's JSONencoder to handle lazy-text. .. versionadded:: 3.3.0 """ def default(self, obj): + from .babel import is_lazy_string + if is_lazy_string(obj): return str(obj) else: return JSONEncoder.default(self, obj) -@contextmanager -def capture_passwordless_login_requests(): - login_requests = [] - - def _on(app, **data): - login_requests.append(data) - - login_instructions_sent.connect(_on) - - try: - yield login_requests - finally: - login_instructions_sent.disconnect(_on) - - -@contextmanager -def capture_registrations(): - """Testing utility for capturing registrations. - """ - registrations = [] - - def _on(app, **data): - registrations.append(data) - - user_registered.connect(_on) - - try: - yield registrations - finally: - user_registered.disconnect(_on) - - -@contextmanager -def capture_reset_password_requests(reset_password_sent_at=None): - """Testing utility for capturing password reset requests. - - :param reset_password_sent_at: An optional datetime object to set the - user's `reset_password_sent_at` to - """ - reset_requests = [] - - def _on(app, **data): - reset_requests.append(data) - - reset_password_instructions_sent.connect(_on) - - try: - yield reset_requests - finally: - reset_password_instructions_sent.disconnect(_on) - - -@contextmanager -def capture_flashes(): - """Testing utility for capturing flashes.""" - flashes = [] - - def _on(app, **data): - flashes.append(data) - - message_flashed.connect(_on) - - try: - yield flashes - finally: - message_flashed.disconnect(_on) - - -class SmsSenderBaseClass(object): - __metaclass__ = abc.ABCMeta - +class SmsSenderBaseClass(metaclass=abc.ABCMeta): def __init__(self): pass @abc.abstractmethod def send_sms(self, from_number, to_number, msg): # pragma: no cover - """ Abstract method for sending sms messages + """Abstract method for sending sms messages .. versionadded:: 3.2.0 """ @@ -989,12 +1002,12 @@ return -class SmsSenderFactory(object): +class SmsSenderFactory: senders = {"Dummy": DummySmsSender} @classmethod def createSender(cls, name, *args, **kwargs): - """ Initialize an SMS sender. + """Initialize an SMS sender. :param name: Name as registered in SmsSenderFactory:senders (e.g. 'Twilio') @@ -1022,7 +1035,7 @@ def password_length_validator(password): - """ Test password for length. + """Test password for length. :param password: Plain text password to check @@ -1042,7 +1055,7 @@ def password_complexity_validator(password, is_register, **kwargs): - """ Test password for complexity. + """Test password for complexity. Currently just supports 'zxcvbn'. @@ -1086,7 +1099,7 @@ def password_breached_validator(password): - """ Check if password on breached list. + """Check if password on breached list. Does nothing unless :py:data:`SECURITY_PASSWORD_CHECK_BREACHED` is set. If password is found on the breached list, return an error if the count is greater than or equal to :py:data:`SECURITY_PASSWORD_BREACHED_COUNT`. @@ -1109,24 +1122,6 @@ return None -def default_password_validator(password, is_register, **kwargs): - """ - Password validation. - Called in app/request context. - - N.B. do not call this directly - use security._password_validator - """ - notok = password_length_validator(password) - if notok: - return notok - - notok = password_breached_validator(password) - if notok: - return notok - - return password_complexity_validator(password, is_register, **kwargs) - - def pwned(password): """ Check password against pwnedpasswords API using k-Anonymity. @@ -1135,8 +1130,6 @@ :return: Count of password in DB (0 means hasn't been compromised) Can raise HTTPError - Only implemented for python 3 - .. versionadded:: 3.4.0 """ @@ -1146,21 +1139,15 @@ sha1 = hashlib.sha1(password.encode("utf8")).hexdigest() - if PY3: - import urllib.request - import urllib.error - - req = urllib.request.Request( - url="https://api.pwnedpasswords.com/range/{}".format(sha1[:5].upper()), - headers={"User-Agent": "Flask-Security (Python)"}, - ) - # Might raise HTTPError - with urllib.request.urlopen(req) as f: - response = f.read() - - raw = response.decode("utf-8-sig") + req = urllib.request.Request( + url="https://api.pwnedpasswords.com/range/{}".format(sha1[:5].upper()), + headers={"User-Agent": "Flask-Security (Python)"}, + ) + # Might raise HTTPError + with urllib.request.urlopen(req) as f: + response = f.read() - entries = dict(map(convert_password_tuple, raw.upper().split("\r\n"))) - return entries.get(sha1[5:].upper(), 0) + raw = response.decode("utf-8-sig") - raise NotImplementedError() + entries = dict(map(convert_password_tuple, raw.upper().split("\r\n"))) + return entries.get(sha1[5:].upper(), 0) diff -Nru flask-security-3.4.2/flask_security/views.py flask-security-4.0.0/flask_security/views.py --- flask-security-3.4.2/flask_security/views.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/flask_security/views.py 2021-01-26 02:39:51.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ flask_security.views ~~~~~~~~~~~~~~~~~~~~ @@ -31,12 +30,11 @@ authenticated (via session?) as well as unauthenticated access. """ -import sys +from functools import partial import time from flask import ( Blueprint, - abort, after_this_request, current_app, jsonify, @@ -59,7 +57,6 @@ from .unified_signin import ( us_signin, us_signin_send_code, - us_qrcode, us_setup, us_setup_validate, us_verify, @@ -74,12 +71,17 @@ from .registerable import register_user from .twofactor import ( complete_two_factor_process, + generate_tf_validity_token, + is_tf_setup, tf_clean_session, tf_disable, tf_login, + tf_set_validity_token_cookie, + tf_verify_validility_token, ) from .utils import ( base_render_json, + check_and_update_authn_fresh, config_value, do_flash, get_message, @@ -87,13 +89,16 @@ get_post_logout_redirect, get_post_register_redirect, get_post_verify_redirect, + get_request_attr, get_url, json_error_response, login_user, logout_user, + send_mail, slash_url_suffix, suppress_form_csrf, url_for_security, + view_commit, ) if get_quart_status(): # pragma: no cover @@ -101,14 +106,14 @@ else: from flask import make_response, redirect + # Convenient references _security = LocalProxy(lambda: current_app.extensions["security"]) _datastore = LocalProxy(lambda: _security.datastore) def default_render_json(payload, code, headers, user): - """ Default JSON response handler. - """ + """Default JSON response handler.""" # Force Content-Type header to json. if headers is None: headers = dict() @@ -117,16 +122,6 @@ return make_response(jsonify(payload), code, headers) -PY3 = sys.version_info[0] == 3 -if PY3 and get_quart_status(): # pragma: no cover - from .async_compat import _commit # noqa: F401 -else: - - def _commit(response=None): - _datastore.commit() - return response - - def _ctx(endpoint): return _security._run_ctx_processor(endpoint) @@ -142,21 +137,20 @@ """ if current_user.is_authenticated and request.method == "POST": - # Just redirect current_user to POST_LOGIN_VIEW (or next). + # Just redirect current_user to POST_LOGIN_VIEW. # While its tempting to try to logout the current user and login the # new requested user - that simply doesn't work with CSRF. - # While this is close to anonymous_user_required - it differs in that - # it uses get_post_login_redirect which correctly handles 'next'. - # TODO: consider changing anonymous_user_required to also call - # get_post_login_redirect - not sure why it never has? + # This does NOT use get_post_login_redirect() so that it doesn't look at + # 'next' - which can cause infinite redirect loops + # (see test_common::test_authenticated_loop) 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_post_login_redirect()) + return redirect(get_url(_security.post_login_view)) form_class = _security.login_form @@ -171,39 +165,56 @@ if form.validate_on_submit(): remember_me = form.remember.data if "remember" in form else None - if config_value("TWO_FACTOR") and ( - config_value("TWO_FACTOR_REQUIRED") - or (form.user.tf_totp_secret and form.user.tf_primary_method) - ): - return tf_login( - form.user, remember=remember_me, primary_authn_via="password" + if config_value("TWO_FACTOR"): + if request.is_json and request.content_length: + tf_validity_token = request.get_json().get("tf_validity_token", None) + else: + tf_validity_token = request.cookies.get("tf_validity", default=None) + + tf_validity_token_is_valid = tf_verify_validility_token( + tf_validity_token, form.user.fs_uniquifier ) + if config_value("TWO_FACTOR_REQUIRED") or (is_tf_setup(form.user)): + if config_value("TWO_FACTOR_ALWAYS_VALIDATE") or ( + not tf_validity_token_is_valid + ): + + return tf_login( + form.user, remember=remember_me, primary_authn_via="password" + ) + login_user(form.user, remember=remember_me, authn_via=["password"]) - after_this_request(_commit) + after_this_request(view_commit) - if not _security._want_json(request): - return redirect(get_post_login_redirect()) + if _security._want_json(request): + return base_render_json(form, include_auth_token=True) + return redirect(get_post_login_redirect()) if _security._want_json(request): if current_user.is_authenticated: form.user = current_user - return base_render_json(form, include_auth_token=True) + return base_render_json(form) if current_user.is_authenticated: - # Basically a no-op if authenticated - just perform the same - # post-login redirect as if user just logged in. - return redirect(get_post_login_redirect()) + return redirect(get_url(_security.post_login_view)) else: + if form.requires_confirmation and _security.requires_confirmation_error_view: + do_flash(*get_message("CONFIRMATION_REQUIRED")) + return redirect( + get_url( + _security.requires_confirmation_error_view, + qparams={"email": form.email.data}, + ) + ) return _security.render_template( config_value("LOGIN_USER_TEMPLATE"), login_user_form=form, **_ctx("login") ) -@auth_required() +@auth_required(lambda: config_value("API_ENABLED_METHODS")) def verify(): - """View function which handles a authentication verification request. - """ + """View function which handles a authentication verification request.""" form_class = _security.verify_form if request.is_json: @@ -213,7 +224,7 @@ if form.validate_on_submit(): # form may have called verify_and_update_password() - after_this_request(_commit) + after_this_request(view_commit) # verified - so set freshness time. session["fs_paa"] = time.time() @@ -277,7 +288,7 @@ if not _security.confirmable or _security.login_without_confirmation: if config_value("TWO_FACTOR") and config_value("TWO_FACTOR_REQUIRED"): return tf_login(user, primary_authn_via="register") - after_this_request(_commit) + after_this_request(view_commit) login_user(user, authn_via=["register"]) did_login = True @@ -292,7 +303,7 @@ return _security.render_template( config_value("REGISTER_USER_TEMPLATE"), register_user_form=form, - **_ctx("register") + **_ctx("register"), ) @@ -351,7 +362,7 @@ return redirect(url_for_security("login")) login_user(user, authn_via=["token"]) - after_this_request(_commit) + after_this_request(view_commit) if _security.redirect_behavior == "spa": return redirect( get_url(_security.post_login_view, qparams=user.get_redirect_qparams()) @@ -384,7 +395,7 @@ return _security.render_template( config_value("SEND_CONFIRMATION_TEMPLATE"), send_confirmation_form=form, - **_ctx("send_confirmation") + **_ctx("send_confirmation"), ) @@ -431,7 +442,7 @@ ) confirm_user(user) - after_this_request(_commit) + after_this_request(view_commit) if user != current_user: logout_user() @@ -482,10 +493,19 @@ if _security._want_json(request): return base_render_json(form, include_user=False) + if form.requires_confirmation and _security.requires_confirmation_error_view: + do_flash(*get_message("CONFIRMATION_REQUIRED")) + return redirect( + get_url( + _security.requires_confirmation_error_view, + qparams={"email": form.email.data}, + ) + ) + return _security.render_template( config_value("FORGOT_PASSWORD_TEMPLATE"), forgot_password_form=form, - **_ctx("forgot_password") + **_ctx("forgot_password"), ) @@ -553,7 +573,7 @@ config_value("RESET_PASSWORD_TEMPLATE"), reset_password_form=form, reset_password_token=token, - **_ctx("reset_password") + **_ctx("reset_password"), ) # This is the POST case. @@ -581,7 +601,7 @@ return redirect(url_for_security("forgot_password")) if form.validate_on_submit(): - after_this_request(_commit) + after_this_request(view_commit) update_password(user, form.password.data) if config_value("TWO_FACTOR") and ( config_value("TWO_FACTOR_REQUIRED") @@ -590,8 +610,8 @@ return tf_login(user, primary_authn_via="reset") login_user(user, authn_via=["reset"]) if _security._want_json(request): - login_form = _security.login_form(MultiDict({"email": user.email})) - setattr(login_form, "user", user) + login_form = _security.login_form() + login_form.user = user return base_render_json(login_form, include_auth_token=True) else: do_flash(*get_message("PASSWORD_RESET")) @@ -607,11 +627,11 @@ config_value("RESET_PASSWORD_TEMPLATE"), reset_password_form=form, reset_password_token=token, - **_ctx("reset_password") + **_ctx("reset_password"), ) -@auth_required("basic", "token", "session") +@auth_required(lambda: config_value("API_ENABLED_METHODS")) def change_password(): """View function which handles a change password request.""" @@ -623,23 +643,25 @@ form = form_class(meta=suppress_form_csrf()) if form.validate_on_submit(): - after_this_request(_commit) + after_this_request(view_commit) change_user_password(current_user._get_current_object(), form.new_password.data) - if not _security._want_json(request): - do_flash(*get_message("PASSWORD_CHANGE")) - return redirect( - get_url(_security.post_change_view) - or get_url(_security.post_login_view) - ) + if _security._want_json(request): + form.user = current_user + return base_render_json(form, include_auth_token=True) + + do_flash(*get_message("PASSWORD_CHANGE")) + return redirect( + get_url(_security.post_change_view) or get_url(_security.post_login_view) + ) if _security._want_json(request): form.user = current_user - return base_render_json(form, include_auth_token=True) + return base_render_json(form) return _security.render_template( config_value("CHANGE_PASSWORD_TEMPLATE"), change_password_form=form, - **_ctx("change_password") + **_ctx("change_password"), ) @@ -656,8 +678,7 @@ 3) user wanting to enable or disable 2FA (assuming application doesn't require it) In order to CHANGE/ENABLE/DISABLE a 2FA information, user must be properly logged in - AND must perform a fresh password validation by - calling POST /tf-confirm (which sets 'tf_confirmed' in the session). + AND have a 'fresh' authentication. For initial login when 2FA required of course user can't be logged in - in this case we need to have been sent some @@ -667,13 +688,16 @@ form_class = _security.two_factor_setup_form if request.is_json: - form = form_class(MultiDict(request.get_json()), meta=suppress_form_csrf()) + if request.content_length: + form = form_class(MultiDict(request.get_json()), meta=suppress_form_csrf()) + else: + form = form_class(formdata=None, meta=suppress_form_csrf()) else: form = form_class(meta=suppress_form_csrf()) if not current_user.is_authenticated: # This is the initial login case - # We can also get here from setup if they want to change + # We can also get here from setup if they want to change TODO: how? if not all(k in session for k in ["tf_user_id", "tf_state"]) or session[ "tf_state" ] not in ["setup_from_login", "validating_profile"]: @@ -681,18 +705,21 @@ tf_clean_session() return _tf_illegal_state(form, _security.login_url) - user = _datastore.get_user(session["tf_user_id"]) + user = _datastore.find_user(fs_uniquifier=session["tf_user_id"]) if not user: tf_clean_session() return _tf_illegal_state(form, _security.login_url) else: - # all other cases require user to be logged in and have performed - # additional password verification as signified by 'tf_confirmed' - # in the session. - if "tf_confirmed" not in session: - tf_clean_session() - return _tf_illegal_state(form, _security.two_factor_confirm_url) + # Caller is changing their TFA profile. This requires a 'fresh' authentication + if not check_and_update_authn_fresh( + config_value("FRESHNESS"), + config_value("FRESHNESS_GRACE_PERIOD"), + get_request_attr("fs_authn_via"), + ): + return _security._reauthn_handler( + config_value("FRESHNESS"), config_value("FRESHNESS_GRACE_PERIOD") + ) user = current_user if form.validate_on_submit(): @@ -706,9 +733,9 @@ pm = form.setup.data if pm == "disable": tf_disable(user) - after_this_request(_commit) - do_flash(*get_message("TWO_FACTOR_DISABLED")) + after_this_request(view_commit) if not _security._want_json(request): + do_flash(*get_message("TWO_FACTOR_DISABLED")) return redirect(get_url(_security.post_login_view)) else: return base_render_json(form) @@ -720,11 +747,15 @@ session["tf_primary_method"] = pm session["tf_state"] = "validating_profile" + json_response = { + "tf_state": "validating_profile", + "tf_primary_method": pm, + } new_phone = form.phone.data if len(form.phone.data) > 0 else None if new_phone: user.tf_phone_number = new_phone _datastore.put(user) - after_this_request(_commit) + after_this_request(view_commit) # This form is sort of bizarre - for SMS and authenticator # you select, then get more info, and submit again. @@ -734,7 +765,7 @@ msg = user.tf_send_security_token( method=pm, totp_secret=session["tf_totp_secret"], - phone_number=user.tf_phone_number, + phone_number=getattr(user, "tf_phone_number", None), ) if msg: # send code didn't work @@ -744,6 +775,23 @@ return base_render_json( form, include_user=False, error_status_code=500 ) + + qrcode_values = dict() + if pm == "authenticator": + authr_setup_values = _security._totp_factory.fetch_setup_values( + session["tf_totp_secret"], user + ) + # Add all the values used in qrcode to json response + json_response["tf_authr_key"] = authr_setup_values["key"] + json_response["tf_authr_username"] = authr_setup_values["username"] + json_response["tf_authr_issuer"] = authr_setup_values["issuer"] + + qrcode_values = dict( + authr_qrcode=authr_setup_values["image"], + authr_key=authr_setup_values["key"], + authr_username=authr_setup_values["username"], + authr_issuer=authr_setup_values["issuer"], + ) code_form = _security.two_factor_verify_code_form() if not _security._want_json(request): return _security.render_template( @@ -752,20 +800,29 @@ two_factor_verify_code_form=code_form, choices=config_value("TWO_FACTOR_ENABLED_METHODS"), chosen_method=pm, - **_ctx("tf_setup") + **qrcode_values, + **_ctx("tf_setup"), ) + return base_render_json(form, include_user=False, additional=json_response) # We get here on GET and POST with failed validation. # For things like phone number - we've already done one POST - # that succeeded and now if failed - so retain the initial info - if _security._want_json(request): - return base_render_json(form, include_user=False) - - code_form = _security.two_factor_verify_code_form() + # that succeeded and now it failed - so retain the initial info choices = config_value("TWO_FACTOR_ENABLED_METHODS") if not config_value("TWO_FACTOR_REQUIRED"): choices.append("disable") + if _security._want_json(request): + # Provide information application/UI might need to render their own form/input + json_response = { + "tf_required": config_value("TWO_FACTOR_REQUIRED"), + "tf_primary_method": getattr(user, "tf_primary_method", None), + "tf_phone_number": getattr(user, "tf_phone_number", None), + "tf_available_methods": choices, + } + return base_render_json(form, include_user=False, additional=json_response) + + code_form = _security.two_factor_verify_code_form() return _security.render_template( config_value("TWO_FACTOR_SETUP_TEMPLATE"), two_factor_setup_form=form, @@ -773,7 +830,7 @@ choices=choices, chosen_method=form.setup.data, two_factor_required=config_value("TWO_FACTOR_REQUIRED"), - **_ctx("tf_setup") + **_ctx("tf_setup"), ) @@ -786,7 +843,6 @@ In this case - user not logged in - but 'tf_state' == 'ready' or 'validating_profile' 2) validating after CHANGE/ENABLE 2FA. In this case user logged in/authenticated - they must have 'tf_confirmed' set meaning they re-entered their passwd """ @@ -799,20 +855,22 @@ changing = current_user.is_authenticated if not changing: - # This is the normal login case + # This is the normal login case OR initial setup if ( not all(k in session for k in ["tf_user_id", "tf_state"]) or session["tf_state"] not in ["ready", "validating_profile"] or ( session["tf_state"] == "validating_profile" - and "tf_primary_method" not in session + and not all( + k in session for k in ["tf_primary_method", "tf_totp_secret"] + ) ) ): # illegal call on this endpoint tf_clean_session() return _tf_illegal_state(form, _security.login_url) - user = _datastore.get_user(session["tf_user_id"]) + user = _datastore.find_user(fs_uniquifier=session["tf_user_id"]) form.user = user if not user: tf_clean_session() @@ -825,10 +883,9 @@ pm = session["tf_primary_method"] totp_secret = session["tf_totp_secret"] else: + # Changing TFA profile - user is already authenticated. if ( - not all( - k in session for k in ["tf_confirmed", "tf_state", "tf_primary_method"] - ) + not all(k in session for k in ["tf_state", "tf_primary_method"]) or session["tf_state"] != "validating_profile" ): tf_clean_session() @@ -839,18 +896,35 @@ totp_secret = session["tf_totp_secret"] form.user = current_user - setattr(form, "primary_method", pm) - setattr(form, "tf_totp_secret", totp_secret) + form.primary_method = pm + form.tf_totp_secret = totp_secret if form.validate_on_submit(): # Success - log in user and clear all session variables + remember = session.pop("tf_remember_login", None) completion_message = complete_two_factor_process( - form.user, pm, totp_secret, changing, session.pop("tf_remember_login", None) + form.user, pm, totp_secret, changing, remember ) - after_this_request(_commit) + + after_this_request(view_commit) + if not _security._want_json(request): + after_this_request( + partial( + tf_set_validity_token_cookie, + fs_uniquifier=form.user.fs_uniquifier, + remember=remember, + ) + ) do_flash(*get_message(completion_message)) + return redirect(get_post_login_redirect()) + if ( + not config_value("TWO_FACTOR_ALWAYS_VALIDATE") and remember + ) and _security._want_json(request): + token = generate_tf_validity_token(form.user.fs_uniquifier) + json_response = {"tf_validity_token": token} + return base_render_json(form, additional=json_response) # GET or not successful POST if _security._want_json(request): return base_render_json(form) @@ -864,7 +938,7 @@ two_factor_setup_form=setup_form, two_factor_verify_code_form=form, choices=config_value("TWO_FACTOR_ENABLED_METHODS"), - **_ctx("tf_setup") + **_ctx("tf_setup"), ) # if we were trying to validate an existing method @@ -876,14 +950,14 @@ two_factor_rescue_form=rescue_form, two_factor_verify_code_form=form, problem=None, - **_ctx("tf_token_validation") + **_ctx("tf_token_validation"), ) @anonymous_user_required @unauth_csrf(fall_through=True) def two_factor_rescue(): - """ Function that handles a situation where user can't + """Function that handles a situation where user can't enter his two-factor validation code User must have already provided valid username/password. @@ -905,7 +979,7 @@ tf_clean_session() return _tf_illegal_state(form, _security.login_url) - user = _datastore.get_user(session["tf_user_id"]) + user = _datastore.find_user(fs_uniquifier=session["tf_user_id"]) form.user = user if not user: tf_clean_session() @@ -921,7 +995,7 @@ msg = form.user.tf_send_security_token( method="email", totp_secret=form.user.tf_totp_secret, - phone_number=form.user.tf_phone_number, + phone_number=getattr(form.user, "tf_phone_number", None), ) if msg: rproblem = "" @@ -932,7 +1006,7 @@ ) # send app provider a mail message regarding trouble elif problem == "no_mail_access": - _security._send_mail( + send_mail( config_value("EMAIL_SUBJECT_TWO_FACTOR_RESCUE"), config_value("TWO_FACTOR_RESCUE_MAIL"), "two_factor_rescue", @@ -951,94 +1025,7 @@ two_factor_rescue_form=form, rescue_mail=config_value("TWO_FACTOR_RESCUE_MAIL"), problem=rproblem, - **_ctx("tf_token_validation") - ) - - -@auth_required("basic", "session", "token") -def two_factor_verify_password(): - """View function which handles a password verification request.""" - form_class = _security.two_factor_verify_password_form - - if request.is_json: - form = form_class(MultiDict(request.get_json()), meta=suppress_form_csrf()) - else: - form = form_class(meta=suppress_form_csrf()) - - if form.validate_on_submit(): - # form called verify_and_update_password() - after_this_request(_commit) - session["tf_confirmed"] = True - m, c = get_message("TWO_FACTOR_PASSWORD_CONFIRMATION_DONE") - if not _security._want_json(request): - do_flash(m, c) - return redirect(url_for_security("two_factor_setup")) - else: - return _security._render_json(json_error_response(m), 400, None, None) - - if _security._want_json(request): - assert form.user == current_user - # form.user = current_user - return base_render_json(form) - - return _security.render_template( - config_value("TWO_FACTOR_VERIFY_PASSWORD_TEMPLATE"), - two_factor_verify_password_form=form, - **_ctx("tf_verify_password") - ) - - -@unauth_csrf(fall_through=True) -def two_factor_qrcode(): - if current_user.is_authenticated: - user = current_user - else: - if "tf_user_id" not in session: - abort(404) - user = _datastore.get_user(session["tf_user_id"]) - if not user: - # Seems like we should be careful here if user_id is gone. - tf_clean_session() - abort(404) - - if "authenticator" not in config_value("TWO_FACTOR_ENABLED_METHODS"): - return abort(404) - if ( - "tf_primary_method" not in session - or session["tf_primary_method"] != "authenticator" - ): - return abort(404) - - totp = user.tf_totp_secret - if "tf_totp_secret" in session: - totp = session["tf_totp_secret"] - try: - import pyqrcode - - # By convention, the URI should have the username that the user - # logs in with. - username = user.calc_username() - url = pyqrcode.create( - _security._totp_factory.get_totp_uri( - username if username else "Unknown", totp - ) - ) - except ImportError: - # For TWO_FACTOR - this should have been checked at app init. - raise - from io import BytesIO - - stream = BytesIO() - url.svg(stream, scale=3) - return ( - stream.getvalue(), - 200, - { - "Content-Type": "image/svg+xml", - "Cache-Control": "no-cache, no-store, must-revalidate", - "Pragma": "no-cache", - "Expires": "0", - }, + **_ctx("tf_token_validation"), ) @@ -1081,7 +1068,8 @@ else: bp.route(state.login_url, methods=["GET", "POST"], endpoint="login")(login) - bp.route(state.verify_url, methods=["GET", "POST"], endpoint="verify")(verify) + if config_value("FRESHNESS", app=app).total_seconds() >= 0: + bp.route(state.verify_url, methods=["GET", "POST"], endpoint="verify")(verify) if state.unified_signin: bp.route(state.us_signin_url, methods=["GET", "POST"], endpoint="us_signin")( @@ -1115,14 +1103,9 @@ bp.route(state.us_verify_link_url, methods=["GET"], endpoint="us_verify_link")( us_verify_link ) - bp.route( - state.us_qrcode_url + slash_url_suffix(state.us_setup_url, ""), - endpoint="us_qrcode", - )(us_qrcode) if state.two_factor: tf_token_validation = "two_factor_token_validation" - tf_qrcode = "two_factor_qrcode" bp.route( state.two_factor_setup_url, methods=["GET", "POST"], @@ -1133,17 +1116,11 @@ methods=["GET", "POST"], endpoint=tf_token_validation, )(two_factor_token_validation) - bp.route(state.two_factor_qrcode_url, endpoint=tf_qrcode)(two_factor_qrcode) bp.route( state.two_factor_rescue_url, methods=["GET", "POST"], endpoint="two_factor_rescue", )(two_factor_rescue) - bp.route( - state.two_factor_confirm_url, - methods=["GET", "POST"], - endpoint="two_factor_verify_password", - )(two_factor_verify_password) if state.registerable: bp.route(state.register_url, methods=["GET", "POST"], endpoint="register")( diff -Nru flask-security-3.4.2/Flask_Security_Too.egg-info/PKG-INFO flask-security-4.0.0/Flask_Security_Too.egg-info/PKG-INFO --- flask-security-3.4.2/Flask_Security_Too.egg-info/PKG-INFO 2020-05-03 01:49:26.000000000 +0000 +++ flask-security-4.0.0/Flask_Security_Too.egg-info/PKG-INFO 2021-01-26 02:40:43.000000000 +0000 @@ -1,6 +1,6 @@ -Metadata-Version: 2.1 +Metadata-Version: 1.2 Name: Flask-Security-Too -Version: 3.4.2 +Version: 4.0.0 Summary: Simple security for Flask apps. Home-page: https://github.com/Flask-Middleware/flask-security Author: Matt Wright & Chris Wagner @@ -13,11 +13,12 @@ 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://github.com/Flask-Middleware/flask-security/workflows/tests/badge.svg?branch=master&event=push + :target: https://github.com/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://codecov.io/gh/Flask-Middleware/flask-security/branch/master/graph/badge.svg?token=U02MUQJ7BM + :target: https://codecov.io/gh/Flask-Middleware/flask-security + :alt: Coverage! .. image:: https://img.shields.io/github/tag/Flask-Middleware/flask-security.svg :target: https://github.com/Flask-Middleware/flask-security/releases @@ -37,6 +38,10 @@ .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/python/black + .. image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white + :target: https://github.com/pre-commit/pre-commit + :alt: pre-commit + Quickly add security features to your Flask application. Notes on this repo @@ -54,7 +59,6 @@ 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 `_ 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 `_ lead on supported versions, documentation @@ -96,17 +100,12 @@ 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 :: 3.9 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 +Requires-Python: >=3.6 diff -Nru flask-security-3.4.2/Flask_Security_Too.egg-info/requires.txt flask-security-4.0.0/Flask_Security_Too.egg-info/requires.txt --- flask-security-3.4.2/Flask_Security_Too.egg-info/requires.txt 2020-05-03 01:49:26.000000000 +0000 +++ flask-security-4.0.0/Flask_Security_Too.egg-info/requires.txt 2021-01-26 02:40:43.000000000 +0000 @@ -1,114 +1,7 @@ -Flask>=1.0.2 +Flask>=1.1.1 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 +Flask-WTF>=0.14.3 +email-validator>=1.1.1 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 +passlib>=1.7.2 diff -Nru flask-security-3.4.2/Flask_Security_Too.egg-info/SOURCES.txt flask-security-4.0.0/Flask_Security_Too.egg-info/SOURCES.txt --- flask-security-3.4.2/Flask_Security_Too.egg-info/SOURCES.txt 2020-05-03 01:49:26.000000000 +0000 +++ flask-security-4.0.0/Flask_Security_Too.egg-info/SOURCES.txt 2021-01-26 02:40:43.000000000 +0000 @@ -7,7 +7,6 @@ README.rst babel.ini pytest.ini -requirements.txt setup.cfg setup.py tox.ini @@ -17,6 +16,7 @@ Flask_Security_Too.egg-info/not-zip-safe Flask_Security_Too.egg-info/requires.txt Flask_Security_Too.egg-info/top_level.txt +docs/.gitignore docs/Makefile docs/api.rst docs/authors.rst @@ -28,6 +28,7 @@ docs/features.rst docs/index.rst docs/models.rst +docs/openapi.yaml docs/patterns.rst docs/quickstart.rst docs/requirements.txt @@ -39,9 +40,7 @@ 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 @@ -49,6 +48,8 @@ flask_security/datastore.py flask_security/decorators.py flask_security/forms.py +flask_security/mail_util.py +flask_security/password_util.py flask_security/passwordless.py flask_security/phone_util.py flask_security/quart_compat.py @@ -76,7 +77,6 @@ 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 @@ -100,6 +100,7 @@ flask_security/templates/security/email/welcome.html flask_security/templates/security/email/welcome.txt flask_security/translations/flask_security.pot +flask_security/translations/pwl.txt 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 @@ -108,12 +109,18 @@ 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/eu_ES/LC_MESSAGES/flask_security.mo +flask_security/translations/eu_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/hy_AM/LC_MESSAGES/flask_security.mo +flask_security/translations/hy_AM/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/pl_PL/LC_MESSAGES/flask_security.mo +flask_security/translations/pl_PL/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 @@ -124,8 +131,11 @@ 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 +requirements/dev.txt +requirements/docs.txt +requirements/tests.txt +tests/__init__.py tests/conftest.py -tests/test_cache.py tests/test_changeable.py tests/test_cli.py tests/test_common.py @@ -144,13 +154,12 @@ tests/test_trackable.py tests/test_two_factor.py tests/test_unified_signin.py -tests/utils.py +tests/test_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 @@ -160,7 +169,6 @@ 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 diff -Nru flask-security-3.4.2/Flask_Security_Too.egg-info/top_level.txt flask-security-4.0.0/Flask_Security_Too.egg-info/top_level.txt --- flask-security-3.4.2/Flask_Security_Too.egg-info/top_level.txt 2020-05-03 01:49:26.000000000 +0000 +++ flask-security-4.0.0/Flask_Security_Too.egg-info/top_level.txt 2021-01-26 02:40:43.000000000 +0000 @@ -1 +1,2 @@ flask_security +tests diff -Nru flask-security-3.4.2/MANIFEST.in flask-security-4.0.0/MANIFEST.in --- flask-security-3.4.2/MANIFEST.in 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/MANIFEST.in 2021-01-26 02:39:51.000000000 +0000 @@ -7,20 +7,16 @@ 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 +include requirements/*.txt +graft docs +graft flask_security/templates +graft flask_security/translations +graft tests +prune tests/.pytest_cache +prune tests/.DS_Store +recursive-exclude tests/.pytest_cache * exclude .coverage tests/.coverage -recursive-exclude docs/_build * -global-exclude *.pyc .DS_Store +prune docs/_build prune scripts prune examples +global-exclude *.pyc diff -Nru flask-security-3.4.2/PKG-INFO flask-security-4.0.0/PKG-INFO --- flask-security-3.4.2/PKG-INFO 2020-05-03 01:49:26.000000000 +0000 +++ flask-security-4.0.0/PKG-INFO 2021-01-26 02:40:43.595904000 +0000 @@ -1,6 +1,6 @@ -Metadata-Version: 2.1 +Metadata-Version: 1.2 Name: Flask-Security-Too -Version: 3.4.2 +Version: 4.0.0 Summary: Simple security for Flask apps. Home-page: https://github.com/Flask-Middleware/flask-security Author: Matt Wright & Chris Wagner @@ -13,11 +13,12 @@ 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://github.com/Flask-Middleware/flask-security/workflows/tests/badge.svg?branch=master&event=push + :target: https://github.com/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://codecov.io/gh/Flask-Middleware/flask-security/branch/master/graph/badge.svg?token=U02MUQJ7BM + :target: https://codecov.io/gh/Flask-Middleware/flask-security + :alt: Coverage! .. image:: https://img.shields.io/github/tag/Flask-Middleware/flask-security.svg :target: https://github.com/Flask-Middleware/flask-security/releases @@ -37,6 +38,10 @@ .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/python/black + .. image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white + :target: https://github.com/pre-commit/pre-commit + :alt: pre-commit + Quickly add security features to your Flask application. Notes on this repo @@ -54,7 +59,6 @@ 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 `_ 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 `_ lead on supported versions, documentation @@ -96,17 +100,12 @@ 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 :: 3.9 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 +Requires-Python: >=3.6 diff -Nru flask-security-3.4.2/pytest.ini flask-security-4.0.0/pytest.ini --- flask-security-3.4.2/pytest.ini 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/pytest.ini 2021-01-26 02:39:51.000000000 +0000 @@ -1,6 +1,13 @@ [pytest] -addopts = -rs --cov flask_security --cov-report term-missing --black --flake8 --cache-clear -flake8-max-line-length = 88 -flake8-ignore = - tests/view_scaffold.py E402 - async_compat.py ALL +addopts = -rs --cache-clear --strict-markers +markers = + settings + babel + changeable + confirmable + registerable + two_factor + recoverable + passwordless + trackable + unified_signin diff -Nru flask-security-3.4.2/README.rst flask-security-4.0.0/README.rst --- flask-security-3.4.2/README.rst 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/README.rst 2021-01-26 02:39:51.000000000 +0000 @@ -1,11 +1,12 @@ 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://github.com/Flask-Middleware/flask-security/workflows/tests/badge.svg?branch=master&event=push + :target: https://github.com/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://codecov.io/gh/Flask-Middleware/flask-security/branch/master/graph/badge.svg?token=U02MUQJ7BM + :target: https://codecov.io/gh/Flask-Middleware/flask-security + :alt: Coverage! .. image:: https://img.shields.io/github/tag/Flask-Middleware/flask-security.svg :target: https://github.com/Flask-Middleware/flask-security/releases @@ -25,6 +26,10 @@ .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/python/black +.. image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white + :target: https://github.com/pre-commit/pre-commit + :alt: pre-commit + Quickly add security features to your Flask application. Notes on this repo @@ -42,7 +47,6 @@ 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 `_ 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 `_ lead on supported versions, documentation diff -Nru flask-security-3.4.2/requirements/dev.txt flask-security-4.0.0/requirements/dev.txt --- flask-security-3.4.2/requirements/dev.txt 1970-01-01 00:00:00.000000000 +0000 +++ flask-security-4.0.0/requirements/dev.txt 2021-01-26 02:39:51.000000000 +0000 @@ -0,0 +1,6 @@ +-r docs.txt +-r tests.txt +psycopg2 +pymysql +pre-commit +tox diff -Nru flask-security-3.4.2/requirements/docs.txt flask-security-4.0.0/requirements/docs.txt --- flask-security-3.4.2/requirements/docs.txt 1970-01-01 00:00:00.000000000 +0000 +++ flask-security-4.0.0/requirements/docs.txt 2021-01-26 02:39:51.000000000 +0000 @@ -0,0 +1,3 @@ +Pallets-Sphinx-Themes +Sphinx +sphinx-issues diff -Nru flask-security-3.4.2/requirements/tests.txt flask-security-4.0.0/requirements/tests.txt --- flask-security-3.4.2/requirements/tests.txt 1970-01-01 00:00:00.000000000 +0000 +++ flask-security-4.0.0/requirements/tests.txt 2021-01-26 02:39:51.000000000 +0000 @@ -0,0 +1,25 @@ +Flask-Babel +Babel +Flask-Mail +Flask-Mongoengine +peewee +Flask-SQLAlchemy +argon2_cffi +bcrypt +check-manifest +coverage +cryptography +mongoengine +mongomock +msgcheck +pony>=0.7.11;python_version<'3.9' +phonenumberslite +pydocstyle +pyqrcode +pytest-cache +pytest-cov +pytest +sqlalchemy +sqlalchemy-utils +werkzeug +zxcvbn diff -Nru flask-security-3.4.2/requirements.txt flask-security-4.0.0/requirements.txt --- flask-security-3.4.2/requirements.txt 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/requirements.txt 1970-01-01 00:00:00.000000000 +0000 @@ -1,3 +0,0 @@ -# Trick for ReadTheDocs to install all requirements: - --e .[all] diff -Nru flask-security-3.4.2/setup.cfg flask-security-4.0.0/setup.cfg --- flask-security-3.4.2/setup.cfg 2020-05-03 01:49:26.000000000 +0000 +++ flask-security-4.0.0/setup.cfg 2021-01-26 02:40:43.595904000 +0000 @@ -18,11 +18,12 @@ [extract_messages] project = Flask-Security -version = 3.4.0 +version = 4.0.0 msgid_bugs_address = jwag956@github.com mapping-file = babel.ini output-file = flask_security/translations/flask_security.pot add-comments = NOTE +keywords = _fsdomain [init_catalog] domain = flask_security @@ -33,6 +34,12 @@ domain = flask_security input-file = flask_security/translations/flask_security.pot output-dir = flask_security/translations/ +no-fuzzy-matching = yes + +[flake8] +max-line-length = 88 +per-file-ignores = + tests/view_scaffold.py: E402 [egg_info] tag_build = diff -Nru flask-security-3.4.2/setup.py flask-security-4.0.0/setup.py --- flask-security-3.4.2/setup.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/setup.py 2021-01-26 02:39:51.000000000 +0000 @@ -1,71 +1,22 @@ -# -*- coding: utf-8 -*- - """Simple security for Flask apps.""" -import io import re from setuptools import find_packages, setup -with io.open("README.rst", "rt", encoding="utf8") as f: +with open("README.rst", encoding="utf8") as f: readme = f.read() -with io.open("flask_security/__init__.py", "rt", encoding="utf8") as f: +with open("flask_security/__init__.py", encoding="utf8") as f: version = re.search(r'__version__ = "(.*?)"', f.read()).group(1) -tests_require = [ - "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", -] - -extras_require = { - "docs": ["Pallets-Sphinx-Themes>=1.2.0", "Sphinx>=1.8.5", "sphinx-issues>=1.2.0"], - "tests": tests_require, -} - -extras_require["all"] = [] -for reqs in extras_require.values(): - extras_require["all"].extend(reqs) - -setup_requires = ["Babel>=1.3", "pytest-runner>=2.6.2", "twine", "wheel"] - install_requires = [ - "Flask>=1.0.2", + "Flask>=1.1.1", "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", + "Flask-WTF>=0.14.3", + "email-validator>=1.1.1", "itsdangerous>=1.1.0", - "passlib>=1.7.1", + "passlib>=1.7.2", ] packages = find_packages() @@ -90,11 +41,8 @@ zip_safe=False, include_package_data=True, platforms="any", - python_requires=">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*", - extras_require=extras_require, + python_requires=">=3.6", install_requires=install_requires, - setup_requires=setup_requires, - tests_require=tests_require, classifiers=[ "Environment :: Web Environment", "Framework :: Flask", @@ -104,13 +52,11 @@ "Programming Language :: Python", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Software Development :: Libraries :: Python Modules", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Development Status :: 4 - Beta", diff -Nru flask-security-3.4.2/tests/conftest.py flask-security-4.0.0/tests/conftest.py --- flask-security-3.4.2/tests/conftest.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/tests/conftest.py 2021-01-26 02:39:51.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ conftest ~~~~~~~~ @@ -13,20 +12,14 @@ import tempfile import time from datetime import datetime - -try: - from urlparse import urlsplit -except ImportError: # pragma: no cover - from urllib.parse import urlsplit +from urllib.parse import urlsplit import pytest from flask import Flask, Response, render_template from flask import jsonify from flask import request as flask_request from flask.json import JSONEncoder -from flask_babelex import Babel from flask_mail import Mail -from utils import populate_data from flask_security import ( MongoEngineUserDatastore, @@ -40,12 +33,26 @@ auth_required, auth_token_required, http_auth_required, + get_request_attr, login_required, roles_accepted, roles_required, permissions_accepted, permissions_required, + uia_email_mapper, ) +from flask_security.utils import localize_callback + +from tests.test_utils import populate_data + +NO_BABEL = False +try: + from flask_babel import Babel +except ImportError: + try: + from flask_babelex import Babel + except ImportError: + NO_BABEL = True @pytest.fixture() @@ -57,6 +64,8 @@ app.config["TESTING"] = True app.config["LOGIN_DISABLED"] = False app.config["WTF_CSRF_ENABLED"] = False + # Our test emails/domain isn't necessarily valid + app.config["SECURITY_EMAIL_VALIDATOR_ARGS"] = {"check_deliverability": False} app.config["SECURITY_TWO_FACTOR_SECRET"] = { "1": "TjQ9Qa31VOrfEzuPy4VHQWPCTmRzCnFzMKLxXYiZu9B" } @@ -94,7 +103,7 @@ app.config["SECURITY_" + key.upper()] = value mail = Mail(app) - if babel is None or babel.args[0]: + if not NO_BABEL and (babel is None or babel.args[0]): Babel(app) app.json_encoder = JSONEncoder app.mail = mail @@ -126,16 +135,19 @@ @http_auth_required @permissions_required("admin") def http_admin_required(): + assert get_request_attr("fs_authn_via") == "basic" return "HTTP Authentication" @app.route("/http_custom_realm") @http_auth_required("My Realm") def http_custom_realm(): + assert get_request_attr("fs_authn_via") == "basic" return render_template("index.html", content="HTTP Authentication") @app.route("/token", methods=["GET", "POST"]) @auth_token_required def token(): + assert get_request_attr("fs_authn_via") == "token" return render_template("index.html", content="Token Authentication") @app.route("/multi_auth") @@ -158,6 +170,7 @@ @app.route("/admin") @roles_required("admin") def admin(): + assert get_request_attr("fs_authn_via") == "session" return render_template("index.html", content="Admin Page") @app.route("/admin_and_editor") @@ -187,10 +200,6 @@ def admin_perm_required(): return render_template("index.html", content="Admin Page required") - @app.route("/unauthorized") - def unauthorized(): - return render_template("unauthorized.html") - @app.route("/page1") def page_1(): return "Page 1" @@ -220,7 +229,16 @@ def mongoengine_setup(request, app, tmpdir, realdburl): + pytest.importorskip("flask_mongoengine") from flask_mongoengine import MongoEngine + from mongoengine.fields import ( + BooleanField, + DateTimeField, + IntField, + ListField, + ReferenceField, + StringField, + ) db_name = "flask_security_test_%s" % str(time.time()).replace(".", "_") app.config["MONGODB_SETTINGS"] = { @@ -233,28 +251,30 @@ db = MongoEngine(app) class Role(db.Document, RoleMixin): - name = db.StringField(required=True, unique=True, max_length=80) - description = db.StringField(max_length=255) + name = StringField(required=True, unique=True, max_length=80) + description = StringField(max_length=255) + permissions = StringField(max_length=255) meta = {"db_alias": db_name} class User(db.Document, UserMixin): - email = db.StringField(unique=True, max_length=255) - username = db.StringField(unique=True, max_length=255) - password = db.StringField(required=False, max_length=255) - security_number = db.IntField(unique=True) - last_login_at = db.DateTimeField() - current_login_at = db.DateTimeField() - tf_primary_method = db.StringField(max_length=255) - tf_totp_secret = db.StringField(max_length=255) - tf_phone_number = db.StringField(max_length=255) - us_totp_secrets = db.StringField() - us_phone_number = db.StringField(max_length=255) - last_login_ip = db.StringField(max_length=100) - current_login_ip = db.StringField(max_length=100) - login_count = db.IntField() - active = db.BooleanField(default=True) - confirmed_at = db.DateTimeField() - roles = db.ListField(db.ReferenceField(Role), default=[]) + email = StringField(unique=True, max_length=255) + fs_uniquifier = StringField(unique=True, max_length=64, required=True) + username = StringField(unique=True, required=False, sparse=True, max_length=255) + password = StringField(required=False, max_length=255) + security_number = IntField(unique=True, required=False, sparse=True) + last_login_at = DateTimeField() + current_login_at = DateTimeField() + tf_primary_method = StringField(max_length=255) + tf_totp_secret = StringField(max_length=255) + tf_phone_number = StringField(max_length=255) + us_totp_secrets = StringField() + us_phone_number = StringField(max_length=255) + last_login_ip = StringField(max_length=100) + current_login_ip = StringField(max_length=100) + login_count = IntField() + active = BooleanField(default=True) + confirmed_at = DateTimeField() + roles = ListField(ReferenceField(Role), default=[]) meta = {"db_alias": db_name} def tear_down(): @@ -274,6 +294,7 @@ def sqlalchemy_setup(request, app, tmpdir, realdburl): + pytest.importorskip("flask_sqlalchemy") from flask_sqlalchemy import SQLAlchemy from flask_security.models import fsqla_v2 as fsqla @@ -297,7 +318,7 @@ def get_security_payload(self): # Make sure we still properly hook up to flask JSONEncoder - return {"id": str(self.id), "last_update": self.update_datetime} + return {"email": str(self.email), "last_update": self.update_datetime} with app.app_context(): db.create_all() @@ -318,10 +339,21 @@ def sqlalchemy_session_setup(request, app, tmpdir, realdburl): + pytest.importorskip("sqlalchemy") from sqlalchemy import create_engine from sqlalchemy.orm import scoped_session, sessionmaker, relationship, backref from sqlalchemy.ext.declarative import declarative_base - from sqlalchemy import Boolean, DateTime, Column, Integer, String, Text, ForeignKey + from sqlalchemy.sql import func + from sqlalchemy import ( + Boolean, + DateTime, + Column, + Integer, + String, + Text, + ForeignKey, + UnicodeText, + ) f, path = tempfile.mkstemp( prefix="flask-security-test-db", suffix=".db", dir=str(tmpdir) @@ -329,7 +361,7 @@ app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///" + path - engine = create_engine(app.config["SQLALCHEMY_DATABASE_URI"], convert_unicode=True) + engine = create_engine(app.config["SQLALCHEMY_DATABASE_URI"]) db_session = scoped_session( sessionmaker(autocommit=False, autoflush=False, bind=engine) ) @@ -339,20 +371,28 @@ 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")) + user_id = Column("user_id", Integer(), ForeignKey("user.myuserid")) + role_id = Column("role_id", Integer(), ForeignKey("role.myroleid")) class Role(Base, RoleMixin): __tablename__ = "role" - id = Column(Integer(), primary_key=True) + myroleid = Column(Integer(), primary_key=True) name = Column(String(80), unique=True) description = Column(String(255)) + permissions = Column(UnicodeText, nullable=True) + update_datetime = Column( + DateTime, + nullable=False, + server_default=func.now(), + onupdate=datetime.utcnow, + ) class User(Base, UserMixin): __tablename__ = "user" - id = Column(Integer, primary_key=True) + myuserid = Column(Integer, primary_key=True) + fs_uniquifier = Column(String(64), unique=True, nullable=False) email = Column(String(255), unique=True) - username = Column(String(255)) + username = Column(String(255), unique=True, nullable=True) password = Column(String(255)) security_number = Column(Integer, unique=True) last_login_at = Column(DateTime()) @@ -370,6 +410,16 @@ roles = relationship( "Role", secondary="roles_users", backref=backref("users", lazy="dynamic") ) + update_datetime = Column( + DateTime, + nullable=False, + server_default=func.now(), + onupdate=datetime.utcnow, + ) + + def get_security_payload(self): + # Make sure we still properly hook up to flask JSONEncoder + return {"email": str(self.email), "last_update": self.update_datetime} with app.app_context(): Base.metadata.create_all(bind=engine) @@ -390,6 +440,7 @@ def peewee_setup(request, app, tmpdir, realdburl): + pytest.importorskip("peewee") from peewee import ( TextField, DateTimeField, @@ -411,8 +462,9 @@ "name": pieces.path[1:], "engine": engine_mapper[pieces.scheme.split("+")[0]], "user": pieces.username, - "passwd": pieces.password, + "password": pieces.password, "host": pieces.hostname, + "port": pieces.port, } else: f, path = tempfile.mkstemp( @@ -424,13 +476,15 @@ db = FlaskDB(app) - class Role(db.Model, RoleMixin): + class Role(RoleMixin, db.Model): name = CharField(unique=True, max_length=80) description = TextField(null=True) + permissions = TextField(null=True) - class User(db.Model, UserMixin): - email = TextField() - username = TextField() + class User(UserMixin, db.Model): + email = TextField(unique=True, null=False) + fs_uniquifier = TextField(unique=True, null=False) + username = TextField(unique=True, null=True) security_number = IntegerField(null=True) password = TextField(null=True) last_login_at = DateTimeField(null=True) @@ -447,7 +501,7 @@ confirmed_at = DateTimeField(null=True) class UserRoles(db.Model): - """ Peewee does not have built-in many-to-many support, so we have to + """Peewee does not have built-in many-to-many support, so we have to create this mapping class to link users to roles.""" user = ForeignKeyField(User, backref="roles") @@ -455,6 +509,9 @@ name = property(lambda self: self.role.name) description = property(lambda self: self.role.description) + def get_permissions(self): + return self.role.get_permissions() + with app.app_context(): for Model in (Role, User, UserRoles): Model.drop_table() @@ -481,6 +538,7 @@ def pony_setup(request, app, tmpdir, realdburl): + pytest.importorskip("pony") from pony.orm import Database, Optional, Required, Set from pony.orm.core import SetInstance @@ -494,6 +552,7 @@ class User(db.Entity): email = Required(str) + fs_uniquifier = Required(str, nullable=False) username = Optional(str) security_number = Optional(int) password = Optional(str, nullable=True) @@ -522,6 +581,7 @@ user=pieces.username, password=pieces.password, host=pieces.hostname, + port=pieces.port, database=pieces.path[1:], ) else: @@ -600,7 +660,28 @@ return app.test_client(use_cookies=False) -@pytest.yield_fixture() +@pytest.fixture(params=["cl-sqlalchemy", "c2", "cl-mongo", "cl-peewee"]) +def clients(request, app, tmpdir, realdburl): + if request.param == "cl-sqlalchemy": + ds = sqlalchemy_setup(request, app, tmpdir, realdburl) + elif request.param == "c2": + ds = sqlalchemy_session_setup(request, app, tmpdir, realdburl) + elif request.param == "cl-mongo": + ds = mongoengine_setup(request, app, tmpdir, realdburl) + elif request.param == "cl-peewee": + ds = peewee_setup(request, app, tmpdir, realdburl) + elif request.param == "cl-pony": + # Not working yet. + ds = pony_setup(request, app, tmpdir, realdburl) + app.security = Security(app, datastore=ds) + populate_data(app) + if request.param == "cl-peewee": + # peewee is insistent on a single connection? + ds.db.close_db(None) + return app.test_client() + + +@pytest.fixture() def in_app_context(request, sqlalchemy_app): app = sqlalchemy_app() with app.app_context(): @@ -616,6 +697,14 @@ return fn +@pytest.fixture() +def get_message_local(app): + def fn(key, **kwargs): + return localize_callback(app.config["SECURITY_MSG_" + key][0], **kwargs) + + return fn + + @pytest.fixture( params=["sqlalchemy", "sqlalchemy-session", "mongoengine", "peewee", "pony"] ) @@ -634,17 +723,18 @@ @pytest.fixture() -def script_info(app, datastore): - try: - from flask.cli import ScriptInfo - except ImportError: - from flask_cli import ScriptInfo +# def script_info(app, datastore): # Fix me when pony works +def script_info(app, sqlalchemy_datastore): + from flask.cli import ScriptInfo def create_app(info): - app.config.update( - **{"SECURITY_USER_IDENTITY_ATTRIBUTES": ("email", "username")} - ) - app.security = Security(app, datastore=datastore) + uia = [ + {"email": {"mapper": uia_email_mapper}}, + {"username": {"mapper": lambda x: x}}, + ] + + app.config.update(**{"SECURITY_USER_IDENTITY_ATTRIBUTES": uia}) + app.security = Security(app, datastore=sqlalchemy_datastore) return app return ScriptInfo(create_app=create_app) diff -Nru flask-security-3.4.2/tests/templates/custom_security/tfc.html flask-security-4.0.0/tests/templates/custom_security/tfc.html --- flask-security-3.4.2/tests/templates/custom_security/tfc.html 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/tests/templates/custom_security/tfc.html 1970-01-01 00:00:00.000000000 +0000 @@ -1,3 +0,0 @@ -CUSTOM TWO FACTOR CHANGE -{{ global }} -{{ foo }} diff -Nru flask-security-3.4.2/tests/templates/index.html flask-security-4.0.0/tests/templates/index.html --- flask-security-3.4.2/tests/templates/index.html 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/tests/templates/index.html 2021-01-26 02:39:51.000000000 +0000 @@ -1,3 +1,3 @@ {% include "_messages.html" %} {% include "_nav.html" %} -

{{ content }}

\ No newline at end of file +

{{ content }}

diff -Nru flask-security-3.4.2/tests/templates/_messages.html flask-security-4.0.0/tests/templates/_messages.html --- flask-security-3.4.2/tests/templates/_messages.html 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/tests/templates/_messages.html 2021-01-26 02:39:51.000000000 +0000 @@ -6,4 +6,4 @@ {% endfor %} {% endif %} -{%- endwith %} \ No newline at end of file +{%- endwith %} diff -Nru flask-security-3.4.2/tests/templates/_nav.html flask-security-4.0.0/tests/templates/_nav.html --- flask-security-3.4.2/tests/templates/_nav.html 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/tests/templates/_nav.html 2021-01-26 02:39:51.000000000 +0000 @@ -1,5 +1,5 @@ {%- if current_user.is_authenticated -%} -

{{ _fsdomain('Welcome') }} {{ current_user.email }}

+

{{ _fsdomain('Welcome') }} {{ current_user.calc_username() }}

{%- endif %}
  • Index
  • diff -Nru flask-security-3.4.2/tests/templates/unauthorized.html flask-security-4.0.0/tests/templates/unauthorized.html --- flask-security-3.4.2/tests/templates/unauthorized.html 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/tests/templates/unauthorized.html 1970-01-01 00:00:00.000000000 +0000 @@ -1,3 +0,0 @@ -{% include "_messages.html" %} -{% include "_nav.html" %} -

    You are not allowed to access the requested resouce

    diff -Nru flask-security-3.4.2/tests/test_cache.py flask-security-4.0.0/tests/test_cache.py --- flask-security-3.4.2/tests/test_cache.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/tests/test_cache.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,133 +0,0 @@ -# -*- coding: utf-8 -*- -""" - test_cache - ~~~~~~~~~~~~ - - verify hash cache tests -""" - -from flask_security.cache import VerifyHashCache -from flask_security.core import _request_loader, local_cache - - -class MockRequest: - @property - def headers(self): - return {"token-header": "mock-token-header"} - - @property - def args(self): - return {} - - @property - def is_json(self): - return True - - def get_json(self, silent=None): - return {} - - -class MockUser: - def __init__(self, id, password): - self.id = id - self.password = password - self.active = True - - def verify_auth_token(self, data): - return True - - -class MockExtensionSecurity: - @property - def token_authentication_header(self): - return "token-header" - - @property - def token_authentication_key(self): - return "token-key" - - @property - def login_manager(self): - class MockLoginManager: - def anonymous_user(self): - return None - - return MockLoginManager() - - @property - def remember_token_serializer(self): - class MockLoader: - def loads(self, token, max_age): - return [1, "token"] - - return MockLoader() - - @property - def token_max_age(self): - return 1 - - @property - def datastore(self): - class MockDataStore: - def find_user(self, id=None): - return MockUser(id, "token") - - return MockDataStore() - - @property - def hashing_context(self): - class MockHashingContext: - def verify(self, encoded_data, hashed_data): - return encoded_data.decode() == hashed_data - - return MockHashingContext() - - -def test_verify_password_cache_init(app): - with app.app_context(): - vhc = VerifyHashCache() - assert len(vhc._cache) == 0 - assert vhc._cache.ttl == 60 * 5 - assert vhc._cache.maxsize == 500 - app.config["SECURITY_VERIFY_HASH_CACHE_TTL"] = 10 - app.config["SECURITY_VERIFY_HASH_CACHE_MAX_SIZE"] = 10 - vhc = VerifyHashCache() - assert vhc._cache.ttl == 10 - assert vhc._cache.maxsize == 10 - - -def test_verify_password_cache_set_get(app): - class MockUser: - def __init__(self, id): - self.id = id - - user = MockUser(1) - with app.app_context(): - vhc = VerifyHashCache() - assert vhc.has_verify_hash_cache(user) is None - vhc.set_cache(user) - assert len(vhc._cache) == 1 - assert vhc.has_verify_hash_cache(user) - vhc.clear() - assert vhc.has_verify_hash_cache(user) is None - - -def test_request_loader_not_using_cache(app): - with app.app_context(): - app.extensions["security"] = MockExtensionSecurity() - with app.test_request_context("/"): - _request_loader(MockRequest()) - assert getattr(local_cache, "verify_hash_cache", None) is None - - -def test_request_loader_using_cache(app): - with app.app_context(): - app.config["SECURITY_USE_VERIFY_PASSWORD_CACHE"] = True - app.config["SECURITY_BACKWARDS_COMPAT_AUTH_TOKEN"] = True - app.extensions["security"] = MockExtensionSecurity() - with app.test_request_context("/"): - _request_loader(MockRequest()) - assert local_cache.verify_hash_cache is not None - assert local_cache.verify_hash_cache.has_verify_hash_cache( - MockUser(1, "token") - ) diff -Nru flask-security-3.4.2/tests/test_changeable.py flask-security-4.0.0/tests/test_changeable.py --- flask-security-3.4.2/tests/test_changeable.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/tests/test_changeable.py 2021-01-26 02:39:51.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ test_changeable ~~~~~~~~~~~~~~~ @@ -9,14 +8,26 @@ :license: MIT, see LICENSE for more details. """ -import sys +import base64 import pytest from flask import Flask -from utils import authenticate, json_authenticate, verify_token +import jinja2 from flask_security.core import UserMixin -from flask_security.signals import password_changed +from flask_security.forms import _default_field_labels +from flask_security.password_util import PasswordUtil +from flask_security.signals import password_changed, user_authenticated +from flask_security.utils import localize_callback +from tests.test_utils import ( + authenticate, + check_xlation, + get_session, + hash_password, + init_app_with_options, + json_authenticate, + logout, +) pytestmark = pytest.mark.changeable() @@ -141,6 +152,204 @@ ] +def test_change_invalidates_session(app, client): + # Make sure that if we change our password - prior sessions are invalidated. + + # changing password effectively re-logs in user - verify the signal + auths = [] + + @user_authenticated.connect_via(app) + def authned(myapp, user, **extra_args): + auths.append((user.email, extra_args["authn_via"])) + + # No remember cookie since that also be reset and auto-login. + data = dict(email="matt@lp.com", password="password", remember="") + response = client.post("/login", data=data) + sess = get_session(response) + cur_user_id = sess.get("_user_id", sess.get("user_id")) + + response = client.post( + "/change", + data={ + "password": "password", + "new_password": "new strong password", + "new_password_confirm": "new strong password", + }, + follow_redirects=True, + ) + # First auth was the initial login above - second should be from /change + assert auths[1][0] == "matt@lp.com" + assert "change" in auths[1][1] + + # Should have received a new session cookie - so should still be logged in + response = client.get("/profile", follow_redirects=True) + assert b"Profile Page" in response.data + + # Now use old session - shouldn't work. + with client.session_transaction() as oldsess: + oldsess["_user_id"] = cur_user_id + oldsess["user_id"] = cur_user_id + + # try to access protected endpoint - shouldn't work + response = client.get("/profile") + assert response.status_code == 302 + assert response.headers["Location"] == "http://localhost/login?next=%2Fprofile" + + +def test_change_updates_remember(app, client): + # Test that on change password - remember cookie updated + authenticate(client) + + response = client.post( + "/change", + data={ + "password": "password", + "new_password": "new strong password", + "new_password_confirm": "new strong password", + }, + follow_redirects=True, + ) + + # Should have received a new session cookie - so should still be logged in + response = client.get("/profile", follow_redirects=True) + assert b"Profile Page" in response.data + + assert "remember_token" in [c.name for c in client.cookie_jar] + client.cookie_jar.clear_session_cookies() + response = client.get("/profile", follow_redirects=True) + assert b"Profile Page" in response.data + + +def test_change_invalidates_auth_token(app, client): + # if change password, by default that should invalidate auth tokens + response = json_authenticate(client) + token = response.json["response"]["user"]["authentication_token"] + headers = {"Authentication-Token": token} + # make sure can access restricted page + response = client.get("/token", headers=headers) + assert b"Token Authentication" in response.data + + response = client.post( + "/change", + data={ + "password": "password", + "new_password": "new strong password", + "new_password_confirm": "new strong password", + }, + follow_redirects=True, + ) + assert response.status_code == 200 + + # authtoken should now be invalid + response = client.get("/token", headers=headers) + assert response.status_code == 302 + assert response.headers["Location"] == "http://localhost/login?next=%2Ftoken" + + +def test_auth_uniquifier(app): + # If add fs_token_uniquifier to user model - change password shouldn't invalidate + # auth tokens. + from sqlalchemy import Column, String + from flask_sqlalchemy import SQLAlchemy + from flask_security.models import fsqla_v2 as fsqla + from flask_security import Security, SQLAlchemyUserDatastore + + app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:" + db = SQLAlchemy(app) + + fsqla.FsModels.set_db_info(db) + + class Role(db.Model, fsqla.FsRoleMixin): + pass + + class User(db.Model, fsqla.FsUserMixin): + fs_token_uniquifier = Column(String(64), unique=True, nullable=False) + + with app.app_context(): + db.create_all() + + ds = SQLAlchemyUserDatastore(db, User, Role) + app.security = Security(app, datastore=ds) + + with app.app_context(): + ds.create_user( + email="matt@lp.com", + password=hash_password("password"), + ) + ds.commit() + + client = app.test_client() + + # standard login with auth token + response = json_authenticate(client) + token = response.json["response"]["user"]["authentication_token"] + headers = {"Authentication-Token": token} + # make sure can access restricted page + response = client.get("/token", headers=headers) + assert b"Token Authentication" in response.data + + # change password + response = client.post( + "/change", + data={ + "password": "password", + "new_password": "new strong password", + "new_password_confirm": "new strong password", + }, + follow_redirects=True, + ) + assert response.status_code == 200 + + # authtoken should still be valid + response = client.get("/token", headers=headers) + assert response.status_code == 200 + + +def test_xlation(app, client, get_message_local): + # Test form and email translation + app.config["BABEL_DEFAULT_LOCALE"] = "fr_FR" + assert check_xlation(app, "fr_FR"), "You must run python setup.py compile_catalog" + + authenticate(client) + + response = client.get("/change", follow_redirects=True) + with app.app_context(): + # Check header + assert ( + f'

    {localize_callback("Change password")}

    '.encode("utf-8") + in response.data + ) + submit = localize_callback(_default_field_labels["change_password"]) + assert f'value="{submit}"'.encode("utf-8") in response.data + + with app.mail.record_messages() as outbox: + response = client.post( + "/change", + data={ + "password": "password", + "new_password": "new strong password", + "new_password_confirm": "new strong password", + }, + follow_redirects=True, + ) + + with app.app_context(): + assert get_message_local("PASSWORD_CHANGE").encode("utf-8") in response.data + assert b"Home Page" in response.data + assert len(outbox) == 1 + assert ( + localize_callback( + app.config["SECURITY_EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE"] + ) + in outbox[0].subject + ) + assert ( + str(jinja2.escape(localize_callback("Your password has been changed."))) + in outbox[0].html + ) + assert localize_callback("Your password has been changed") in outbox[0].body + + @pytest.mark.settings(change_url="/custom_change") def test_custom_change_url(client): authenticate(client) @@ -205,32 +414,30 @@ assert "authentication_token" in response.json["response"]["user"] -@pytest.mark.settings(backwards_compat_auth_token_invalid=True) -def test_bc_password(app, client_nc): - # Test behavior of BACKWARDS_COMPAT_AUTH_TOKEN_INVALID - response = json_authenticate(client_nc) - token = response.json["response"]["user"]["authentication_token"] - verify_token(client_nc, token) - +@pytest.mark.settings(api_enabled_methods=["basic"]) +def test_basic_change(app, client_nc, get_message): + # Verify can change password using basic auth data = dict( password="password", new_password="new strong password", new_password_confirm="new strong password", ) + response = client_nc.post("/change", data=data) + assert b"You are not authenticated" in response.data + assert "WWW-Authenticate" in response.headers + response = client_nc.post( - "/change?include_auth_token=1", - json=data, - headers={"Content-Type": "application/json", "Authentication-Token": token}, + "/change", + data=data, + headers={ + "Authorization": "Basic %s" + % base64.b64encode(b"matt@lp.com:password").decode("utf-8") + }, + follow_redirects=True, ) assert response.status_code == 200 - assert "authentication_token" in response.json["response"]["user"] - - # changing password should have rendered existing auth tokens invalid - verify_token(client_nc, token, status=401) - - # but new auth token should work - token = response.json["response"]["user"]["authentication_token"] - verify_token(client_nc, token) + # No session so no flashing + assert b"Home Page" in response.data @pytest.mark.settings(password_complexity_checker="zxcvbn") @@ -251,13 +458,18 @@ assert "Repeats like" in response.json["response"]["errors"]["new_password"][0] -def test_my_validator(app, client): - @app.security.password_validator - def pwval(password, is_register, **kwargs): - user = kwargs["user"] - # This is setup in createusers for matt. - assert user.security_number == 123456 - return ["Are you crazy?"] +def test_my_validator(app, sqlalchemy_datastore): + class MyPwUtil(PasswordUtil): + def validate(self, password, is_register, **kwargs): + user = kwargs["user"] + # This is setup in createusers for matt. + assert user.security_number == 123456 + return ["Are you crazy?"], password + + init_app_with_options( + app, sqlalchemy_datastore, **{"security_args": {"password_util_cls": MyPwUtil}} + ) + client = app.test_client() authenticate(client) @@ -310,7 +522,6 @@ assert response.status_code == 200 -@pytest.mark.skipif(sys.version_info < (3, 0), reason="requires python3 or higher") def test_unicode_invalid_length(app, client, get_message): # From NIST and OWASP - each unicode code point should count as a character. authenticate(client) @@ -327,3 +538,111 @@ assert response.headers["Content-Type"] == "application/json" assert response.status_code == 400 assert get_message("PASSWORD_INVALID_LENGTH", length=8) in response.data + + +def test_pwd_normalize(app, client): + """ Verify that can log in with both original and normalized pwd """ + authenticate(client) + + data = dict( + password="password", + new_password="new strong password\N{ROMAN NUMERAL ONE}", + new_password_confirm="new strong password\N{ROMAN NUMERAL ONE}", + ) + response = client.post( + "/change", + json=data, + headers={"Content-Type": "application/json"}, + ) + assert response.status_code == 200 + logout(client) + + # use original typed-in pwd + response = client.post( + "/login", + json=dict( + email="matt@lp.com", password="new strong password\N{ROMAN NUMERAL ONE}" + ), + headers={"Content-Type": "application/json"}, + ) + assert response.status_code == 200 + logout(client) + + # try with normalized password + response = client.post( + "/login", + json=dict( + email="matt@lp.com", + password="new strong password\N{LATIN CAPITAL LETTER I}", + ), + headers={"Content-Type": "application/json"}, + ) + assert response.status_code == 200 + + # Verify can change password using original password + data = dict( + password="new strong password\N{ROMAN NUMERAL ONE}", + new_password="new strong password\N{ROMAN NUMERAL TWO}", + new_password_confirm="new strong password\N{ROMAN NUMERAL TWO}", + ) + response = client.post( + "/change", + json=data, + headers={"Content-Type": "application/json"}, + ) + assert response.status_code == 200 + + +@pytest.mark.settings(password_normalize_form=None) +def test_pwd_no_normalize(app, client): + """Verify that can log in with original but not normalized if have + disabled normalization + """ + authenticate(client) + + data = dict( + password="password", + new_password="new strong password\N{ROMAN NUMERAL ONE}", + new_password_confirm="new strong password\N{ROMAN NUMERAL ONE}", + ) + response = client.post( + "/change", + json=data, + headers={"Content-Type": "application/json"}, + ) + assert response.status_code == 200 + logout(client) + + # try with normalized password - should fail + response = client.post( + "/login", + json=dict( + email="matt@lp.com", + password="new strong password\N{LATIN CAPITAL LETTER I}", + ), + headers={"Content-Type": "application/json"}, + ) + assert response.status_code == 400 + + # use original typed-in pwd + response = client.post( + "/login", + json=dict( + email="matt@lp.com", password="new strong password\N{ROMAN NUMERAL ONE}" + ), + headers={"Content-Type": "application/json"}, + ) + assert response.status_code == 200 + + # Verify can change password using original password + data = dict( + password="new strong password\N{ROMAN NUMERAL ONE}", + new_password="new strong password\N{ROMAN NUMERAL TWO}", + new_password_confirm="new strong password\N{ROMAN NUMERAL TWO}", + ) + response = client.post( + "/change", + json=data, + headers={"Content-Type": "application/json"}, + ) + assert response.status_code == 200 diff -Nru flask-security-3.4.2/tests/test_cli.py flask-security-4.0.0/tests/test_cli.py --- flask-security-3.4.2/tests/test_cli.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/tests/test_cli.py 2021-01-26 02:39:51.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ test_cli ~~~~~~~~ @@ -12,10 +11,14 @@ roles_add, roles_create, roles_remove, + roles_add_permissions, + roles_remove_permissions, users_activate, users_create, users_deactivate, + users_reset_access, ) +from flask_security import verify_password def test_cli_createuser(script_info): @@ -40,6 +43,59 @@ ) assert result.exit_code == 0 + # create user with email and username + result = runner.invoke( + users_create, + ["email1@example.org", "username:lookatme!", "--password", "battery staple"], + obj=script_info, + ) + assert result.exit_code == 0 + + # try to activate using username + result = runner.invoke(users_activate, "lookatme!", obj=script_info) + assert result.exit_code == 0 + + +def test_cli_createuser_extraargs(script_info): + # Test that passing attributes that aren't part of registration form + # are passed to create_user + runner = CliRunner() + result = runner.invoke( + users_create, + [ + "email1@example.org", + "security_number:666", + "--password", + "battery staple", + "--active", + ], + obj=script_info, + ) + assert result.exit_code == 0 + result = runner.invoke(users_activate, ["email1@example.org"], obj=script_info) + assert result.exit_code == 0 + assert "was already activated" in result.output + + +def test_cli_createuser_normalize(script_info): + """Test create user CLI that is properly normalizes email and password.""" + runner = CliRunner() + + result = runner.invoke( + users_create, + ["email@EXAMPLE.org", "--password", "battery staple\N{ROMAN NUMERAL ONE}"], + obj=script_info, + ) + assert result.exit_code == 0 + assert "email@example.org" in result.stdout + + app = script_info.load_app() + with app.app_context(): + user = app.security.datastore.find_user(email="email@example.org") + assert verify_password( + "battery staple\N{LATIN CAPITAL LETTER I}", user.password + ) + def test_cli_createrole(script_info): """Test create user CLI.""" @@ -125,6 +181,44 @@ assert result.exit_code == 0 +def test_cli_addremove_permissions(script_info): + """Test add/remove permissions.""" + runner = CliRunner() + + result = runner.invoke( + roles_create, ["superusers", "-d", "Test description"], obj=script_info + ) + assert result.exit_code == 0 + + # add permission to non-existent role + result = runner.invoke( + roles_add_permissions, ["whatrole", "read, write"], obj=script_info + ) + assert "Cannot find role" in result.output + + result = runner.invoke( + roles_add_permissions, ["superusers", "read, write"], obj=script_info + ) + assert all(p in result.output for p in ["read", "write", "superusers"]) + + # remove permission to non-existent role + result = runner.invoke( + roles_remove_permissions, ["whatrole", "read, write"], obj=script_info + ) + assert "Cannot find role" in result.output + + result = runner.invoke( + roles_remove_permissions, ["superusers", "write"], obj=script_info + ) + assert all(p in result.output for p in ["write", "superusers"]) + + result = runner.invoke( + roles_remove_permissions, ["superusers", "whatever, read"], obj=script_info + ) + # remove permissions doesn't check if existing or not. + assert all(p in result.output for p in ["read", "superusers"]) + + def test_cli_activate_deactivate(script_info): """Test create user CLI.""" runner = CliRunner() @@ -151,3 +245,20 @@ assert result.exit_code == 0 result = runner.invoke(users_deactivate, ["a@example.org"], obj=script_info) assert result.exit_code == 0 + + +def test_cli_reset_user(script_info): + runner = CliRunner() + result = runner.invoke( + users_create, + ["email1@example.org", "username:lookatme!", "--password", "battery staple"], + obj=script_info, + ) + + result = runner.invoke(users_reset_access, ["lookatme!"], obj=script_info) + assert result.exit_code == 0 + result = runner.invoke(users_reset_access, ["lookatme!"], obj=script_info) + assert result.exit_code == 0 + + result = runner.invoke(users_reset_access, ["whoami"], obj=script_info) + assert "User not found" in result.output diff -Nru flask-security-3.4.2/tests/test_common.py flask-security-4.0.0/tests/test_common.py --- flask-security-3.4.2/tests/test_common.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/tests/test_common.py 2021-01-26 02:39:51.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ test_common ~~~~~~~~~~~ @@ -11,24 +10,24 @@ import base64 import json +import re +from http.cookiejar import Cookie import pytest from flask import Blueprint -from utils import ( +from flask_security import uia_email_mapper + +from tests.test_utils import ( authenticate, json_authenticate, get_num_queries, + hash_password, logout, populate_data, verify_token, ) -try: - from cookielib import Cookie -except ImportError: - from http.cookiejar import Cookie - def test_login_view(client): response = client.get("/login") @@ -74,6 +73,38 @@ assert get_message("INVALID_REDIRECT") in response.data +def test_authenticate_with_subdomain_next(app, client, get_message): + app.config["SERVER_NAME"] = "lp.com" + app.config["SECURITY_REDIRECT_ALLOW_SUBDOMAINS"] = True + data = dict(email="matt@lp.com", password="password") + response = client.post("/login?next=http://sub.lp.com", data=data) + assert response.status_code == 302 + + +def test_authenticate_with_root_domain_next(app, client, get_message): + app.config["SERVER_NAME"] = "lp.com" + app.config["SECURITY_SUBDOMAIN"] = "auth" + app.config["SECURITY_REDIRECT_ALLOW_SUBDOMAINS"] = True + data = dict(email="matt@lp.com", password="password") + response = client.post("/login?next=http://lp.com", data=data) + assert response.status_code == 302 + + +def test_authenticate_with_invalid_subdomain_next(app, client, get_message): + app.config["SERVER_NAME"] = "lp.com" + app.config["SECURITY_REDIRECT_ALLOW_SUBDOMAINS"] = True + data = dict(email="matt@lp.com", password="password") + response = client.post("/login?next=http://sub.lp.net", data=data) + assert get_message("INVALID_REDIRECT") in response.data + + +def test_authenticate_with_subdomain_next_default_config(app, client, get_message): + app.config["SERVER_NAME"] = "lp.com" + data = dict(email="matt@lp.com", password="password") + response = client.post("/login?next=http://sub.lp.com", data=data) + assert get_message("INVALID_REDIRECT") in response.data + + def test_authenticate_case_insensitive_email(app, client): response = authenticate(client, "MATT@lp.com", follow_redirects=True) assert b"Welcome matt@lp.com" in response.data @@ -98,9 +129,9 @@ def test_get_already_authenticated_next(client): response = authenticate(client, follow_redirects=True) assert b"Welcome matt@lp.com" in response.data - # This should override post_login_view + # This should NOT override post_login_view due to potential redirect loops. response = client.get("/login?next=/page1", follow_redirects=True) - assert b"Page 1" in response.data + assert b"Post Login" in response.data @pytest.mark.settings(post_login_view="/post_login") @@ -110,13 +141,15 @@ data = dict(email="matt@lp.com", password="password") response = client.post("/login", data=data, follow_redirects=True) assert b"Post Login" in response.data + # This should NOT override post_login_view due to potential redirect loops. response = client.post("/login?next=/page1", data=data, follow_redirects=True) - assert b"Page 1" in response.data + assert b"Post Login" in response.data def test_login_form(client): response = client.post("/login", data={"email": "matt@lp.com"}) assert b"matt@lp.com" in response.data + assert re.search(b']*type="email"[^>]*>', response.data) def test_unprovided_username(client, get_message): @@ -145,7 +178,7 @@ def test_inactive_forbids(app, client, get_message): - """ Make sure that existing session doesn't work after + """Make sure that existing session doesn't work after user marked inactive """ response = authenticate(client, follow_redirects=True) @@ -168,7 +201,7 @@ @pytest.mark.settings(unauthorized_view=None) def test_inactive_forbids_token(app, client_nc, get_message): - """ Make sure that existing token doesn't work after + """Make sure that existing token doesn't work after user marked inactive """ response = json_authenticate(client_nc) @@ -189,6 +222,35 @@ assert response.status_code == 401 +def test_inactive_forbids_basic(app, client, get_message): + """Make sure that basic auth doesn't work if user deactivated""" + + # Should properly work. + response = client.get( + "/multi_auth", + headers={ + "Authorization": "Basic %s" + % base64.b64encode(b"joe@lp.com:password").decode("utf-8") + }, + ) + assert b"Session, Token, Basic" in response.data + + # deactivate joe + with app.test_request_context("/"): + user = app.security.datastore.find_user(email="joe@lp.com") + app.security.datastore.deactivate_user(user) + app.security.datastore.commit() + + response = client.get( + "/multi_auth", + headers={ + "Authorization": "Basic %s" + % base64.b64encode(b"joe@lp.com:password").decode("utf-8") + }, + ) + assert b"You are not authenticated" in response.data + + def test_unset_password(client, get_message): response = authenticate(client, "jess@lp.com", "password") assert get_message("PASSWORD_NOT_SET") in response.data @@ -268,42 +330,42 @@ @pytest.mark.settings(unauthorized_view="/unauthz") -def test_roles_accepted(client): +def test_roles_accepted(clients): # This specificaly tests that we can pass a URL for unauthorized_view. for user in ("matt@lp.com", "joe@lp.com"): - authenticate(client, user) - response = client.get("/admin_or_editor") + authenticate(clients, user) + response = clients.get("/admin_or_editor") assert b"Admin or Editor Page" in response.data - logout(client) + logout(clients) - authenticate(client, "jill@lp.com") - response = client.get("/admin_or_editor", follow_redirects=True) + authenticate(clients, "jill@lp.com") + response = clients.get("/admin_or_editor", follow_redirects=True) assert b"Unauthorized" in response.data @pytest.mark.settings(unauthorized_view="unauthz") -def test_permissions_accepted(client): +def test_permissions_accepted(clients): for user in ("matt@lp.com", "joe@lp.com"): - authenticate(client, user) - response = client.get("/admin_perm") + authenticate(clients, user) + response = clients.get("/admin_perm") assert b"Admin Page with full-write or super" in response.data - logout(client) + logout(clients) - authenticate(client, "jill@lp.com") - response = client.get("/admin_perm", follow_redirects=True) + authenticate(clients, "jill@lp.com") + response = clients.get("/admin_perm", follow_redirects=True) assert b"Unauthorized" in response.data @pytest.mark.settings(unauthorized_view="unauthz") -def test_permissions_required(client): +def test_permissions_required(clients): for user in ["matt@lp.com"]: - authenticate(client, user) - response = client.get("/admin_perm_required") + authenticate(clients, user) + response = clients.get("/admin_perm_required") assert b"Admin Page required" in response.data - logout(client) + logout(clients) - authenticate(client, "joe@lp.com") - response = client.get("/admin_perm_required", follow_redirects=True) + authenticate(clients, "joe@lp.com") + response = clients.get("/admin_perm_required", follow_redirects=True) assert b"Unauthorized" in response.data @@ -314,15 +376,15 @@ @pytest.mark.settings(unauthorized_view="unauthz") -def test_multiple_role_required(client): +def test_multiple_role_required(clients): for user in ("matt@lp.com", "joe@lp.com"): - authenticate(client, user) - response = client.get("/admin_and_editor", follow_redirects=True) + authenticate(clients, user) + response = clients.get("/admin_and_editor", follow_redirects=True) assert b"Unauthorized" in response.data - client.get("/logout") + clients.get("/logout") - authenticate(client, "dave@lp.com") - response = client.get("/admin_and_editor", follow_redirects=True) + authenticate(clients, "dave@lp.com") + response = clients.get("/admin_and_editor", follow_redirects=True) assert b"Admin and Editor Page" in response.data @@ -365,6 +427,15 @@ def test_http_auth(client): + # browsers expect 401 response with WWW-Authenticate header - which will prompt + # them to pop up a login form. + response = client.get("/http", headers={}) + assert response.status_code == 401 + assert b"You are not authenticated" in response.data + assert "WWW-Authenticate" in response.headers + assert 'Basic realm="Login Required"' == response.headers["WWW-Authenticate"] + + # Now provide correct credentials response = client.get( "/http", headers={ @@ -375,7 +446,12 @@ assert b"HTTP Authentication" in response.data -@pytest.mark.settings(USER_IDENTITY_ATTRIBUTES=("email", "username")) +@pytest.mark.settings( + USER_IDENTITY_ATTRIBUTES=[ + {"email": {"mapper": uia_email_mapper}}, + {"username": {"mapper": lambda x: x}}, + ] +) def test_http_auth_username(client): response = client.get( "/http", @@ -428,7 +504,6 @@ "UNAUTHENTICATED" ) assert response.headers["Content-Type"] == "application/json" - assert "WWW-Authenticate" not in response.headers @pytest.mark.settings(backwards_compat_unauthn=True) @@ -447,9 +522,7 @@ @pytest.mark.settings(backwards_compat_unauthn=False) def test_invalid_http_auth_invalid_username_json(client, get_message): - # While Basic auth is allowed with JSON - we never expect a WWW-Authenticate - # header - since that is captured by most browsers and they pop up a - # login form. + # Even with JSON - Basic Auth required a WWW-Authenticate header response. response = client.get( "/http", headers={ @@ -463,7 +536,7 @@ "UNAUTHENTICATED" ) assert response.headers["Content-Type"] == "application/json" - assert "WWW-Authenticate" not in response.headers + assert "WWW-Authenticate" in response.headers @pytest.mark.settings(backwards_compat_unauthn=True) @@ -505,8 +578,10 @@ assert b"Basic" in response.data response = client.get("/multi_auth") - # Default unauthn is to redirect - assert response.status_code == 302 + # Default unauthn with basic is to return 401 with WWW-Authenticate Header + # so that browser pops up a username/password dialog + assert response.status_code == 401 + assert "WWW-Authenticate" in response.headers @pytest.mark.settings(backwards_compat_unauthn=True) @@ -523,7 +598,6 @@ assert 'Basic realm="Login Required"' == response.headers["WWW-Authenticate"] response = client.get("/multi_auth") - print(response.headers) assert response.status_code == 401 @@ -540,6 +614,20 @@ assert b"Session" in response.data +def test_authenticated_loop(client): + # If user is already authenticated say via session, and then hits an endpoint + # protected with @auth_token_required() - then they will be redirected to the login + # page which will simply note the current user is already logged in and redirect + # to POST_LOGIN_VIEW. Between 3.3.0 and 3.4.4 - this redirect would honor the 'next' + # parameter - thus redirecting back to the endpoint that caused the redirect in the + # first place - thus an infinite loop. + authenticate(client) + + response = client.get("/token", follow_redirects=True) + assert response.status_code == 200 + assert b"Home Page" in response.data + + def test_user_deleted_during_session_reverts_to_anonymous_user(app, client): authenticate(client) @@ -610,12 +698,12 @@ json_authenticate(client) response = client.get("/login", headers={"Content-Type": "application/json"}) assert response.status_code == 200 - assert response.json["response"]["user"]["id"] == "1" + assert response.json["response"]["user"]["email"] == "matt@lp.com" assert "last_update" in response.json["response"]["user"] response = client.get("/login", headers={"Accept": "application/json"}) assert response.status_code == 200 - assert response.json["response"]["user"]["id"] == "1" + assert response.json["response"]["user"]["email"] == "matt@lp.com" assert "last_update" in response.json["response"]["user"] @@ -641,27 +729,6 @@ ) -@pytest.mark.settings(security_hashing_schemes=["sha256_crypt"]) -@pytest.mark.skip -def test_auth_token_speed(app, client_nc): - # To run with old algorithm you have to comment out fs_uniquifier check in UserMixin - import timeit - - response = json_authenticate(client_nc) - token = response.json["response"]["user"]["authentication_token"] - - def time_get(): - rp = client_nc.get( - "/login", - data={}, - headers={"Content-Type": "application/json", "Authentication-Token": token}, - ) - assert rp.status_code == 200 - - t = timeit.timeit(time_get, number=50) - print("Time for 50 iterations: ", t) - - def test_change_uniquifier(app, client_nc): # make sure that existing token no longer works once we change the uniquifier @@ -683,6 +750,101 @@ verify_token(client_nc, token) +def test_change_token_uniquifier(app): + # make sure that existing token no longer works once we change the token uniquifier + from sqlalchemy import Column, String + from flask_sqlalchemy import SQLAlchemy + from flask_security.models import fsqla_v2 as fsqla + from flask_security import Security, SQLAlchemyUserDatastore + + app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:" + db = SQLAlchemy(app) + + fsqla.FsModels.set_db_info(db) + + class Role(db.Model, fsqla.FsRoleMixin): + pass + + class User(db.Model, fsqla.FsUserMixin): + fs_token_uniquifier = Column(String(64), unique=True, nullable=False) + + with app.app_context(): + db.create_all() + + ds = SQLAlchemyUserDatastore(db, User, Role) + app.security = Security(app, datastore=ds) + + with app.app_context(): + ds.create_user( + email="matt@lp.com", + password=hash_password("password"), + ) + ds.commit() + + client_nc = app.test_client(use_cookies=False) + + response = json_authenticate(client_nc) + token = response.json["response"]["user"]["authentication_token"] + verify_token(client_nc, token) + + # now change uniquifier + with app.test_request_context("/"): + user = app.security.datastore.find_user(email="matt@lp.com") + app.security.datastore.reset_user_access(user) + app.security.datastore.commit() + + verify_token(client_nc, token, status=401) + + # get new token and verify it works + response = json_authenticate(client_nc) + token = response.json["response"]["user"]["authentication_token"] + verify_token(client_nc, token) + + +def test_null_token_uniquifier(app): + # If existing record has a null fs_token_uniquifier, should be set on first use. + from sqlalchemy import Column, String + from flask_sqlalchemy import SQLAlchemy + from flask_security.models import fsqla_v2 as fsqla + from flask_security import Security, SQLAlchemyUserDatastore + + app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:" + db = SQLAlchemy(app) + + fsqla.FsModels.set_db_info(db) + + class Role(db.Model, fsqla.FsRoleMixin): + pass + + class User(db.Model, fsqla.FsUserMixin): + fs_token_uniquifier = Column(String(64), unique=True, nullable=True) + + with app.app_context(): + db.create_all() + + ds = SQLAlchemyUserDatastore(db, User, Role) + app.security = Security(app, datastore=ds) + + with app.app_context(): + ds.create_user( + email="matt@lp.com", + password=hash_password("password"), + ) + ds.commit() + + # manually null out fs_token_uniquifier + user = ds.find_user(email="matt@lp.com") + user.fs_token_uniquifier = None + ds.put(user) + ds.commit() + + client_nc = app.test_client(use_cookies=False) + + response = json_authenticate(client_nc) + token = response.json["response"]["user"]["authentication_token"] + verify_token(client_nc, token) + + def test_token_query(in_app_context): # Verify that when authenticating with auth token (and not session) # that there is just one DB query to get user. @@ -724,3 +886,26 @@ assert response.status_code == 200 end_nqueries = get_num_queries(app.security.datastore) assert current_nqueries is None or end_nqueries == (current_nqueries + 2) + + +@pytest.mark.changeable() +def test_no_get_auth_token(app, client): + # Test that GETs don't return an auth token. This is a security issue since + # GETs aren't protected with CSRF + authenticate(client) + response = client.get( + "/login?include_auth_token", headers={"Content-Type": "application/json"} + ) + assert "authentication_token" not in response.json["response"]["user"] + + data = dict( + password="password", + new_password="new strong password", + new_password_confirm="new strong password", + ) + response = client.get( + "/change?include_auth_token", + json=data, + headers={"Content-Type": "application/json"}, + ) + assert "authentication_token" not in response.json["response"]["user"] diff -Nru flask-security-3.4.2/tests/test_configuration.py flask-security-4.0.0/tests/test_configuration.py --- flask-security-3.4.2/tests/test_configuration.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/tests/test_configuration.py 2021-01-26 02:39:51.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ test_configuration ~~~~~~~~~~~~~~~~~~ @@ -9,7 +8,7 @@ import base64 import pytest -from utils import authenticate, logout +from tests.test_utils import authenticate, logout @pytest.mark.settings( @@ -35,8 +34,10 @@ "/http", headers={"Authorization": "Basic %s" % base64.b64encode(b"joe@lp.com:bogus")}, ) - assert response.status_code == 302 - assert response.headers["Location"] == "http://localhost/custom_login?next=%2Fhttp" + assert response.status_code == 401 + assert b"You are not authenticated" in response.data + assert "WWW-Authenticate" in response.headers + assert 'Basic realm="Custom Realm"' == response.headers["WWW-Authenticate"] @pytest.mark.settings(login_user_template="custom_security/login_user.html") diff -Nru flask-security-3.4.2/tests/test_confirmable.py flask-security-4.0.0/tests/test_confirmable.py --- flask-security-3.4.2/tests/test_confirmable.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/tests/test_confirmable.py 2021-01-26 02:39:51.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ test_confirmable ~~~~~~~~~~~~~~~~ @@ -7,26 +6,27 @@ """ import time +from urllib.parse import parse_qsl, urlsplit import pytest from flask import Flask -from utils import authenticate, logout from flask_security.core import UserMixin from flask_security.confirmable import generate_confirmation_token from flask_security.signals import confirm_instructions_sent, user_confirmed -from flask_security.utils import capture_flashes, capture_registrations, string_types -try: - from urlparse import parse_qsl, urlsplit -except ImportError: # pragma: no cover - from urllib.parse import parse_qsl, urlsplit +from tests.test_utils import ( + authenticate, + capture_flashes, + capture_registrations, + logout, +) pytestmark = pytest.mark.confirmable() @pytest.mark.registerable() -def test_confirmable_flag(app, client, sqlalchemy_datastore, get_message): +def test_confirmable_flag(app, clients, get_message): recorded_confirms = [] recorded_instructions_sent = [] @@ -40,7 +40,7 @@ def on_instructions_sent(app, user, token): assert isinstance(app, Flask) assert isinstance(user, UserMixin) - assert isinstance(token, string_types) + assert isinstance(token, str) recorded_instructions_sent.append(user) # Test login before confirmation @@ -48,19 +48,19 @@ with capture_registrations() as registrations: data = dict(email=email, password="awesome sunset", next="") - response = client.post("/register", data=data) + response = clients.post("/register", data=data) assert response.status_code == 302 - response = authenticate(client, email=email, password="awesome sunset") + response = authenticate(clients, email=email, password="awesome sunset") assert get_message("CONFIRMATION_REQUIRED") in response.data # Test invalid token - response = client.get("/confirm/bogus", follow_redirects=True) + response = clients.get("/confirm/bogus", follow_redirects=True) assert get_message("INVALID_CONFIRMATION_TOKEN") in response.data # Test JSON - response = client.post( + response = clients.post( "/confirm", json=dict(email="matt@lp.com"), headers={"Content-Type": "application/json"}, @@ -71,22 +71,22 @@ assert len(recorded_instructions_sent) == 1 # Test ask for instructions with invalid email - response = client.post("/confirm", data=dict(email="bogus@bogus.com")) + response = clients.post("/confirm", data=dict(email="bogus@bogus.com")) assert get_message("USER_DOES_NOT_EXIST") in response.data # Test resend instructions - response = client.post("/confirm", data=dict(email=email)) + response = clients.post("/confirm", data=dict(email=email)) assert get_message("CONFIRMATION_REQUEST", email=email) in response.data assert len(recorded_instructions_sent) == 2 # Test confirm token = registrations[0]["confirm_token"] - response = client.get("/confirm/" + token, follow_redirects=True) + response = clients.get("/confirm/" + token, follow_redirects=True) assert get_message("EMAIL_CONFIRMED") in response.data assert len(recorded_confirms) == 1 # Test already confirmed - response = client.get("/confirm/" + token, follow_redirects=True) + response = clients.get("/confirm/" + token, follow_redirects=True) assert get_message("ALREADY_CONFIRMED") in response.data assert len(recorded_instructions_sent) == 2 @@ -95,36 +95,49 @@ with app.app_context(): user = registrations[0]["user"] expired_token = generate_confirmation_token(user) - response = client.get("/confirm/" + expired_token, follow_redirects=True) + response = clients.get("/confirm/" + expired_token, follow_redirects=True) assert get_message("ALREADY_CONFIRMED") in response.data assert len(recorded_instructions_sent) == 2 # Test already confirmed when asking for confirmation instructions - logout(client) + logout(clients) - response = client.get("/confirm") + response = clients.get("/confirm") assert response.status_code == 200 - response = client.post("/confirm", data=dict(email=email)) + response = clients.post("/confirm", data=dict(email=email)) assert get_message("ALREADY_CONFIRMED") in response.data - # Test user was deleted before confirmation + # Test if user was deleted before confirmation with capture_registrations() as registrations: - data = dict(email="mary@lp.com", password="awesome sunset", next="") - client.post("/register", data=data) + data = dict(email="mary27@lp.com", password="awesome sunset", next="") + clients.post("/register", data=data) user = registrations[0]["user"] token = registrations[0]["confirm_token"] with app.app_context(): - sqlalchemy_datastore.delete(user) - sqlalchemy_datastore.commit() + app.security.datastore.delete(user) + app.security.datastore.commit() + if hasattr(app.security.datastore.db, "close_db"): + app.security.datastore.db.close_db(None) - response = client.get("/confirm/" + token, follow_redirects=True) + response = clients.get("/confirm/" + token, follow_redirects=True) assert get_message("INVALID_CONFIRMATION_TOKEN") in response.data @pytest.mark.registerable() +@pytest.mark.settings(requires_confirmation_error_view="/confirm") +def test_requires_confirmation_error_redirect(app, clients): + data = dict(email="jyl@lp.com", password="awesome sunset") + response = clients.post("/register", data=data) + + response = authenticate(clients, **data, follow_redirects=True) + assert b"send_confirmation_form" in response.data + assert b"jyl@lp.com" in response.data + + +@pytest.mark.registerable() @pytest.mark.settings(confirm_email_within="1 milliseconds") def test_expired_confirmation_token(client, get_message): with capture_registrations() as registrations: @@ -173,7 +186,7 @@ @pytest.mark.registerable() def test_no_auth_token(client_nc): - """ Make sure that register doesn't return Authentication Token + """Make sure that register doesn't return Authentication Token if user isn't confirmed. """ response = client_nc.post( @@ -183,13 +196,13 @@ ) assert response.status_code == 200 user = response.json["response"]["user"] - assert len(user) == 2 and all(k in user for k in ["id", "last_update"]) + assert len(user) == 2 and all(k in user for k in ["email", "last_update"]) @pytest.mark.registerable() @pytest.mark.settings(login_without_confirmation=True) def test_auth_token_unconfirmed(client_nc): - """ Make sure that register returns Authentication Token + """Make sure that register returns Authentication Token if user isn't confirmed, but the 'login_without_confirmation' flag is set. """ response = client_nc.post( @@ -200,7 +213,7 @@ assert response.status_code == 200 user = response.json["response"]["user"] assert len(user) == 3 and all( - k in user for k in ["id", "last_update", "authentication_token"] + k in user for k in ["email", "last_update", "authentication_token"] ) @@ -358,8 +371,8 @@ assert "localhost:8081" == split.netloc assert "/confirm-error" == split.path qparams = dict(parse_qsl(split.query)) - assert len(qparams) == 2 - assert all(k in qparams for k in ["email", "error"]) + assert all(k in qparams for k in ["email", "error", "identity"]) + assert qparams["identity"] == "dude@lp.com" msg = get_message( "CONFIRMATION_EXPIRED", within="1 milliseconds", email="dude@lp.com" @@ -390,7 +403,7 @@ @pytest.mark.registerable() @pytest.mark.settings(two_factor_required=True) def test_two_factor(app, client): - """ If two-factor is enabled, the confirm shouldn't login, but start the + """If two-factor is enabled, the confirm shouldn't login, but start the 2-factor setup. """ with capture_registrations() as registrations: @@ -432,3 +445,54 @@ assert response.status_code == 200 assert response.json["response"]["tf_required"] assert response.json["response"]["tf_state"] == "setup_from_login" + + +@pytest.mark.registerable() +@pytest.mark.settings( + user_identity_attributes=[{"username": {"mapper": lambda x: x}}], +) +def test_email_not_identity(app, sqlalchemy_datastore, get_message): + # Test that can register/confirm with email even if it isn't an IDENTITY_ATTRIBUTE + from flask_security import ConfirmRegisterForm, Security, unique_identity_attribute + from wtforms import StringField, validators + + class MyRegisterForm(ConfirmRegisterForm): + username = StringField( + "Username", + validators=[validators.data_required(), unique_identity_attribute], + ) + + app.config["SECURITY_CONFIRM_REGISTER_FORM"] = MyRegisterForm + security = Security(datastore=sqlalchemy_datastore) + security.init_app(app) + + client = app.test_client() + + with capture_registrations() as registrations: + data = dict(email="mary2@lp.com", username="mary", password="awesome sunset") + response = client.post("/register", data=data, follow_redirects=True) + assert b"mary2@lp.com" in response.data + + token = registrations[0]["confirm_token"] + response = client.get("/confirm/" + token, headers={"Accept": "application/json"}) + assert response.status_code == 302 + assert response.location == "http://localhost/" + + logout(client) + + # check that username must be unique + data = dict(email="mary4@lp.com", username="mary", password="awesome sunset") + response = client.post( + "/register", data=data, headers={"Accept": "application/json"} + ) + assert response.status_code == 400 + assert "is already associated" in response.json["response"]["errors"]["username"][0] + + # log in with username - this uses the age-old hack that although the form's + # input label says "email" - it in fact will accept any identity attribute. + response = client.post( + "/login", + data=dict(email="mary", password="awesome sunset"), + follow_redirects=True, + ) + assert b"

    Welcome mary

    " in response.data diff -Nru flask-security-3.4.2/tests/test_context_processors.py flask-security-4.0.0/tests/test_context_processors.py --- flask-security-3.4.2/tests/test_context_processors.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/tests/test_context_processors.py 2021-01-26 02:39:51.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ test_context_processors ~~~~~~~~~~~~~~~~~~~~~~~ @@ -7,9 +6,9 @@ """ import pytest -from test_two_factor import tf_authenticate -from test_unified_signin import authenticate as us_authenticate -from utils import authenticate, logout +from tests.test_two_factor import tf_authenticate +from tests.test_unified_signin import authenticate as us_authenticate +from tests.test_utils import authenticate, capture_reset_password_requests, logout @pytest.mark.recoverable() @@ -33,67 +32,73 @@ @app.security.forgot_password_context_processor def forgot_password(): - return {"foo": "bar"} + return {"foo": "bar-forgot"} response = client.get("/reset") assert b"global" in response.data - assert b"bar" in response.data + assert b"bar-forgot" in response.data @app.security.login_context_processor def login(): - return {"foo": "bar"} + return {"foo": "bar-login"} response = client.get("/login") assert b"global" in response.data - assert b"bar" in response.data + assert b"bar-login" in response.data @app.security.verify_context_processor def verify(): - return {"foo": "bar"} + return {"foo": "bar-verify"} authenticate(client) response = client.get("/verify") assert b"CUSTOM VERIFY USER" in response.data assert b"global" in response.data - assert b"bar" in response.data + assert b"bar-verify" in response.data logout(client) @app.security.register_context_processor def register(): - return {"foo": "bar"} + return {"foo": "bar-register"} response = client.get("/register") assert b"global" in response.data - assert b"bar" in response.data + assert b"bar-register" in response.data @app.security.reset_password_context_processor def reset_password(): - return {"foo": "bar"} + return {"foo": "bar-reset"} - response = client.get("/reset") + # /reset/token - need to generate a token + with capture_reset_password_requests() as requests: + response = client.post( + "/reset", data=dict(email="joe@lp.com"), follow_redirects=True + ) + token = requests[0]["token"] + response = client.get(f"/reset/{token}") assert b"global" in response.data - assert b"bar" in response.data + assert b"bar-reset" in response.data @app.security.change_password_context_processor def change_password(): - return {"foo": "bar"} + return {"foo": "bar-change"} authenticate(client) response = client.get("/change") assert b"global" in response.data - assert b"bar" in response.data + assert b"bar-change" in response.data @app.security.send_confirmation_context_processor def send_confirmation(): - return {"foo": "bar"} + return {"foo": "bar-confirm"} response = client.get("/confirm") assert b"global" in response.data - assert b"bar" in response.data + assert b"bar-confirm" in response.data @app.security.mail_context_processor def mail(): - return {"foo": "bar"} + return {"foo": "bar-mail"} client.get("/logout") @@ -102,7 +107,7 @@ email = outbox[0] assert "global" in email.html - assert "bar" in email.html + assert "bar-mail" in email.html @pytest.mark.passwordless() @@ -110,17 +115,16 @@ def test_passwordless_login_context_processor(app, client): @app.security.send_login_context_processor def send_login(): - return {"foo": "bar"} + return {"foo": "bar-send-login"} response = client.get("/login") - assert b"bar" in response.data + assert b"bar-send-login" in response.data @pytest.mark.two_factor() @pytest.mark.settings( two_factor_required=True, login_user_template="custom_security/login_user.html", - two_factor_verify_password_template="custom_security/tfc.html", two_factor_setup_template="custom_security/tf_setup.html", two_factor_verify_code_template="custom_security/tf_verify.html", ) @@ -130,35 +134,25 @@ def default_ctx_processor(): return {"global": "global"} - @app.security.tf_verify_password_context_processor - def send_two_factor_confirm(): - return {"foo": "bar"} - - tf_authenticate(app, client) - response = client.get("/tf-confirm") - assert b"global" in response.data - assert b"bar" in response.data - logout(client) - @app.security.tf_setup_context_processor def send_two_factor_setup(): - return {"foo": "bar"} + return {"foo": "bar-tfsetup"} # Note this just does initial login on a user that hasn't setup 2FA yet. authenticate(client) response = client.get("/tf-setup") assert b"global" in response.data - assert b"bar" in response.data + assert b"bar-tfsetup" in response.data logout(client) @app.security.tf_token_validation_context_processor def send_two_factor_token_validation(): - return {"foo": "bar"} + return {"foo": "bar-tfvalidate"} tf_authenticate(app, client, validate=False) response = client.get("/tf-rescue") assert b"global" in response.data - assert b"bar" in response.data + assert b"bar-tfvalidate" in response.data logout(client) diff -Nru flask-security-3.4.2/tests/test_csrf.py flask-security-4.0.0/tests/test_csrf.py --- flask-security-3.4.2/tests/test_csrf.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/tests/test_csrf.py 2021-01-26 02:39:51.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ test_csrf ~~~~~~~~~~~~~~~~~ @@ -16,7 +15,7 @@ import pytest from flask_wtf import CSRFProtect -from utils import get_session, logout +from tests.test_utils import get_session, logout REAL_VALIDATE_CSRF = None @@ -34,7 +33,7 @@ flask_wtf.csrf.validate_csrf = orig_validate_csrf -class MpValidateCsrf(object): +class MpValidateCsrf: success = 0 failure = 0 @@ -70,7 +69,7 @@ use_header=False, remember=None, ): - """ Return tuple (auth_token, csrf_token) + """Return tuple (auth_token, csrf_token) Note that since this is sent as JSON rather than form that csrfProtect won't find token value (since it looks in request.form). """ @@ -84,7 +83,7 @@ data["csrf_token"] = csrf_token response = client.post( - endpoint or "/login" + "?include_auth_token", + endpoint or "/login?include_auth_token", content_type="application/json", json=data, headers=headers, @@ -222,7 +221,7 @@ @pytest.mark.recoverable() def test_cp_reset(app, client): - """ Test that header based CSRF works for /reset when + """Test that header based CSRF works for /reset when using WTF_CSRF_CHECK_DEFAULT=False. """ app.config["WTF_CSRF_ENABLED"] = True @@ -425,7 +424,7 @@ @pytest.mark.settings(CSRF_COOKIE={"key": "X-XSRF-Token"}) @pytest.mark.changeable() def test_cp_with_token_cookie(app, client): - # Make sure can use returned CSRF-Token cookie in Header. + # Make sure can use returned CSRF-Token cookie in Header when changing password. app.config["WTF_CSRF_ENABLED"] = True CSRFProtect(app) @@ -446,8 +445,7 @@ headers={"X-XSRF-Token": csrf_token}, ) assert response.status_code == 200 - # 2 successes since the utils:csrf_cookie_handler will check - assert mp.success == 2 and mp.failure == 0 + assert mp.success == 1 and mp.failure == 0 json_logout(client) assert "X-XSRF-Token" not in [c.name for c in client.cookie_jar] @@ -526,6 +524,15 @@ csrf_cookie = [c for c in client.cookie_jar if c.name == "X-XSRF-Token"][0] assert csrf_cookie assert mp.success == 1 and mp.failure == 0 + + # delete cookie again, do a 'GET' - the REFRESH_COOKIE_ON_EACH_REQUEST should + # send us a new one + client.delete_cookie(csrf_cookie.domain, csrf_cookie.name) + response = client.get("/change") + assert response.status_code == 200 + csrf_cookie = [c for c in client.cookie_jar if c.name == "X-XSRF-Token"][0] + assert csrf_cookie + json_logout(client) assert "X-XSRF-Token" not in [c.name for c in client.cookie_jar] diff -Nru flask-security-3.4.2/tests/test_datastore.py flask-security-4.0.0/tests/test_datastore.py --- flask-security-3.4.2/tests/test_datastore.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/tests/test_datastore.py 2021-01-26 02:39:51.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ test_datastore ~~~~~~~~~~~~~~ @@ -12,7 +11,7 @@ import datetime from pytest import raises, skip -from utils import init_app_with_options, get_num_queries, is_sqlalchemy +from tests.test_utils import init_app_with_options, get_num_queries, is_sqlalchemy from flask_security import RoleMixin, Security, UserMixin from flask_security.datastore import Datastore, UserDatastore @@ -50,8 +49,6 @@ datastore.find_user(None) with raises(NotImplementedError): datastore.find_role(None) - with raises(NotImplementedError): - datastore.get_user(None) def test_toggle_active(): @@ -94,60 +91,31 @@ assert not datastore.activate_user(user) -def test_get_user(app, datastore): - # The order of identity attributes is important for testing. - # drivers like psycopg2 will abort the transaction if they throw an - # error and not continue on so we want to check that case of passing in - # a string for a numeric field and being able to move onto the next - # column. - init_app_with_options( - app, - datastore, - **{ - "SECURITY_USER_IDENTITY_ATTRIBUTES": ( - "email", - "security_number", - "username", - ) - } - ) - - with app.app_context(): - user_id = datastore.find_user(email="matt@lp.com").id - - user = datastore.get_user(user_id) - assert user is not None - - user = datastore.get_user("matt@lp.com") - assert user is not None - - user = datastore.get_user("matt") - assert user is not None - - # Regression check (make sure we don't match wildcards) - user = datastore.get_user("%lp.com") - assert user is None - - # Verify that numeric non PK works - user = datastore.get_user(123456) - assert user is not None - - def test_find_user(app, datastore): init_app_with_options(app, datastore) with app.app_context(): - user_id = datastore.find_user(email="gene@lp.com").id + user_id = datastore.find_user(email="gene@lp.com").fs_uniquifier current_nqueries = get_num_queries(datastore) - assert user_id == datastore.find_user(security_number=889900).id + assert user_id == datastore.find_user(security_number=889900).fs_uniquifier end_nqueries = get_num_queries(datastore) if current_nqueries is not None: if is_sqlalchemy(datastore): # This should have done just 1 query across all attrs. assert end_nqueries == (current_nqueries + 1) - assert user_id == datastore.find_user(username="gene").id + assert user_id == datastore.find_user(username="gene").fs_uniquifier + + +def test_find_user_multikey(app, datastore): + init_app_with_options(app, datastore) + + with app.app_context(): + with raises(ValueError): + datastore.find_user( + case_insensitive=True, email="gene@lp.com", security_number=889900 + ) def test_find_role(app, datastore): @@ -172,11 +140,6 @@ assert datastore.add_role_to_user(user, "editor") is False assert user.has_role("editor") is True - # Test with email - assert datastore.add_role_to_user("jill@lp.com", "editor") is True - user = datastore.find_user(email="jill@lp.com") - assert user.has_role("editor") is True - # Test remove role assert datastore.remove_role_from_user(user, "editor") is True assert datastore.remove_role_from_user(user, "editor") is False @@ -258,6 +221,7 @@ user = datastore.find_user(email="dude@lp.com") assert user.has_role("test1") is True assert user.has_permission("read") is True + assert user.has_permission("write") is False def test_permissions_strings(app, datastore): @@ -305,20 +269,22 @@ t1 = ds.find_role("test1") assert perms == t1.get_permissions() - orig_update_time = t1.update_datetime - assert t1.update_datetime <= datetime.datetime.utcnow() + if hasattr(t1, "update_datetime"): + orig_update_time = t1.update_datetime + assert t1.update_datetime <= datetime.datetime.utcnow() - t1.add_permissions("execute") + ds.add_permissions_to_role(t1, "execute") ds.commit() t1 = ds.find_role("test1") assert perms.union({"execute"}) == t1.get_permissions() - t1.remove_permissions("read") + ds.remove_permissions_from_role(t1, "read") ds.commit() t1 = ds.find_role("test1") assert {"write", "execute"} == t1.get_permissions() - assert t1.update_datetime > orig_update_time + if hasattr(t1, "update_datetime"): + assert t1.update_datetime > orig_update_time def test_get_permissions(app, datastore): @@ -350,13 +316,13 @@ assert {"read", "write"} == t1.get_permissions() # send in a list - t1.add_permissions(["execute", "whatever"]) + ds.add_permissions_to_role(t1, ["execute", "whatever"]) ds.commit() t1 = ds.find_role("test1") assert {"read", "write", "execute", "whatever"} == t1.get_permissions() - t1.remove_permissions(["read", "whatever"]) + ds.remove_permissions_from_role(t1, ["read", "whatever"]) ds.commit() assert {"write", "execute"} == t1.get_permissions() @@ -366,19 +332,19 @@ ds.commit() t2 = ds.find_role("test2") - t2.add_permissions({"execute", "whatever"}) + ds.add_permissions_to_role(t2, {"execute", "whatever"}) ds.commit() t2 = ds.find_role("test2") assert {"read", "write", "execute", "whatever"} == t2.get_permissions() - t2.remove_permissions({"read", "whatever"}) + ds.remove_permissions_from_role(t2, {"read", "whatever"}) ds.commit() assert {"write", "execute"} == t2.get_permissions() def test_modify_permissions_unsupported(app, datastore): - from conftest import PonyUserDatastore + from tests.conftest import PonyUserDatastore ds = datastore if hasattr(datastore.role_model, "permissions"): @@ -410,7 +376,7 @@ from sqlalchemy.orm import relationship, backref from flask_security import SQLAlchemyUserDatastore - from conftest import _setup_realdb, _teardown_realdb + from tests.conftest import _setup_realdb, _teardown_realdb # UUID type only supported by postgres - not sqlite. if not realdburl or "postgres" not in realdburl: @@ -432,9 +398,10 @@ UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True ) email = Column(String(255), unique=True) + fs_uniquifier = Column(String(64), unique=True, nullable=False) first_name = Column(String(255), index=True) last_name = Column(String(255), index=True) - username = Column(String(255), unique=True) + username = Column(String(255), unique=True, nullable=True) password = Column(String(255)) active = Column(Boolean()) created_at = Column(DateTime, default=datetime.datetime.utcnow) @@ -469,41 +436,5 @@ app.security = Security(app, datastore=ds) with app.app_context(): - user = ds.get_user("matt@lp.com") - assert not user - - -def test_user_loader(app, sqlalchemy_datastore): - # user_loader now tries first to match fs_uniquifier then match user.id - # While we know that fs_uniquifier is a string - we don't know what user.id is - # and we know that psycopg2 is really finicky about this. - from flask_security.core import _user_loader - - init_app_with_options(app, sqlalchemy_datastore) - with app.app_context(): - jill = sqlalchemy_datastore.find_user(email="jill@lp.com") - - # normal case - user = _user_loader(jill.fs_uniquifier) - assert user.email == "jill@lp.com" - - # send in an int - make sure it is cast to string for check against - # fs_uniquifier - user = _user_loader(1000) + user = ds.find_user(email="matt@lp.com") assert not user - # with psycopg2 this will lock up DB but since we pass exceptions we can - # check this by trying some other DB operation - jill = sqlalchemy_datastore.find_user(email="jill@lp.com") - assert jill - - # This works since DBs seem to try to cast to underlying type - user = _user_loader("10") - jill = sqlalchemy_datastore.find_user(email="jill@lp.com") - assert jill - - # Since this doesn't match an existing fs_uniqifier, and user.id is an int - # this will make pyscopg2 angry. This test verifies that we don't in fact - # try to take a string and use it to lookup an int. - user = _user_loader("someunknown") - jill = sqlalchemy_datastore.find_user(email="jill@lp.com") - assert jill diff -Nru flask-security-3.4.2/tests/test_entities.py flask-security-4.0.0/tests/test_entities.py --- flask-security-3.4.2/tests/test_entities.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/tests/test_entities.py 2021-01-26 02:39:51.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ test_entities ~~~~~~~~~~~~~ @@ -76,40 +75,28 @@ "tf_primary_method", "tf_totp_secret", } - attrs = set( - [ - a[0] - for a in get_user_attributes(fsqla.FsUserMixin) - if isinstance(a[1], Column) - ] - ) + attrs = { + a[0] for a in get_user_attributes(fsqla.FsUserMixin) if isinstance(a[1], Column) + } assert attrs == v1_user_attrs v2_user_attrs = {"us_totp_secrets", "us_phone_number"} - attrs = set( - [ - a[0] - for a in get_user_attributes(fsqla_v2.FsUserMixin) - if isinstance(a[1], Column) - ] - ) + attrs = { + a[0] + for a in get_user_attributes(fsqla_v2.FsUserMixin) + if isinstance(a[1], Column) + } assert attrs == v1_user_attrs.union(v2_user_attrs) v1_role_attrs = {"id", "name", "description", "permissions", "update_datetime"} - attrs = set( - [ - a[0] - for a in get_user_attributes(fsqla.FsRoleMixin) - if isinstance(a[1], Column) - ] - ) + attrs = { + a[0] for a in get_user_attributes(fsqla.FsRoleMixin) if isinstance(a[1], Column) + } assert attrs == v1_role_attrs - attrs = set( - [ - a[0] - for a in get_user_attributes(fsqla_v2.FsRoleMixin) - if isinstance(a[1], Column) - ] - ) + attrs = { + a[0] + for a in get_user_attributes(fsqla_v2.FsRoleMixin) + if isinstance(a[1], Column) + } assert attrs == v1_role_attrs diff -Nru flask-security-3.4.2/tests/test_hashing.py flask-security-4.0.0/tests/test_hashing.py --- flask-security-3.4.2/tests/test_hashing.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/tests/test_hashing.py 2021-01-26 02:39:51.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ test_hashing ~~~~~~~~~~~~ @@ -10,7 +9,7 @@ import pytest from pytest import raises -from utils import authenticate, init_app_with_options +from tests.test_utils import authenticate, init_app_with_options from passlib.hash import argon2, pbkdf2_sha256, django_pbkdf2_sha256, plaintext from flask_security.utils import hash_password, verify_password, get_hmac diff -Nru flask-security-3.4.2/tests/test_misc.py flask-security-4.0.0/tests/test_misc.py --- flask-security-3.4.2/tests/test_misc.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/tests/test_misc.py 2021-01-26 02:39:51.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ test_misc ~~~~~~~~~~~ @@ -12,18 +11,21 @@ from datetime import timedelta import hashlib -import mock +from unittest import mock import re import sys import time import pytest -from utils import ( +from tests.test_utils import ( authenticate, + capture_flashes, + capture_reset_password_requests, check_xlation, init_app_with_options, json_authenticate, + logout, populate_data, ) @@ -32,6 +34,7 @@ from flask_security.forms import ( ChangePasswordForm, ConfirmRegisterForm, + EmailField, ForgotPasswordForm, LoginForm, PasswordField, @@ -45,60 +48,39 @@ email_validator, valid_user_email, ) -from flask_security import auth_required +from flask_security import auth_required, roles_required from flask_security.utils import ( - capture_flashes, - capture_reset_password_requests, encode_string, json_error_response, + get_request_attr, hash_data, send_mail, - string_types, uia_phone_mapper, verify_hash, ) @pytest.mark.recoverable() -def test_async_email_task(app, client): - app.mail_sent = False +def test_my_mail_util(app, sqlalchemy_datastore): + from flask_security import MailUtil - @app.security.send_mail_task - def send_email(msg): - app.mail_sent = True - - client.post("/reset", data=dict(email="matt@lp.com")) - assert app.mail_sent is True - - -@pytest.mark.recoverable() -def test_alt_send_mail(app, sqlalchemy_datastore): - """ Verify that can override the send_mail method. """ - app.mail_sent = False - - def send_email(subject, email, template, **kwargs): - app.mail_sent = True + class MyMailUtil(MailUtil): + def send_mail( + self, template, subject, recipient, sender, body, html, user, **kwargs + ): + assert template == "reset_instructions" + assert subject == app.config["SECURITY_EMAIL_SUBJECT_PASSWORD_RESET"] + assert recipient == "matt@lp.com" + assert user.email == "matt@lp.com" + assert sender == "no-reply@localhost" + assert isinstance(sender, str) init_app_with_options( - app, sqlalchemy_datastore, **{"security_args": {"send_mail": send_email}} + app, sqlalchemy_datastore, **{"security_args": {"mail_util_cls": MyMailUtil}} ) - client = app.test_client() - - client.post("/reset", data=dict(email="matt@lp.com")) - assert app.mail_sent is True - - -@pytest.mark.recoverable() -def test_alt_send_mail_decorator(app, client): - """ Verify that can override the send_mail method. """ - app.mail_sent = False - - @app.security.send_mail - def send_email(subject, email, template, **kwargs): - app.mail_sent = True + client = app.test_client() client.post("/reset", data=dict(email="matt@lp.com")) - assert app.mail_sent is True def test_register_blueprint_flag(app, sqlalchemy_datastore): @@ -113,13 +95,13 @@ @pytest.mark.changeable() def test_basic_custom_forms(app, sqlalchemy_datastore): class MyLoginForm(LoginForm): - email = StringField("My Login Email Address Field") + email = EmailField("My Login Email Address Field") class MyRegisterForm(RegisterForm): - email = StringField("My Register Email Address Field") + email = EmailField("My Register Email Address Field") class MyForgotPasswordForm(ForgotPasswordForm): - email = StringField( + email = EmailField( "My Forgot Email Address Field", validators=[email_required, email_validator, valid_user_email], ) @@ -172,10 +154,10 @@ app.config["SECURITY_CONFIRMABLE"] = True class MyRegisterForm(ConfirmRegisterForm): - email = StringField("My Register Email Address Field") + email = EmailField("My Register Email Address Field") class MySendConfirmationForm(SendConfirmationForm): - email = StringField("My Send Confirmation Email Address Field") + email = EmailField("My Send Confirmation Email Address Field") app.security = Security( app, @@ -197,7 +179,7 @@ app.config["SECURITY_PASSWORDLESS"] = True class MyPasswordlessLoginForm(PasswordlessLoginForm): - email = StringField("My Passwordless Email Address Field") + email = EmailField("My Passwordless Email Address Field") app.security = Security( app, @@ -258,17 +240,6 @@ assert response.status_code == 404 -def test_addition_identity_attributes(app, sqlalchemy_datastore): - init_app_with_options( - app, - sqlalchemy_datastore, - **{"SECURITY_USER_IDENTITY_ATTRIBUTES": ("email", "username")} - ) - client = app.test_client() - response = authenticate(client, email="matt", follow_redirects=True) - assert b"Welcome matt@lp.com" in response.data - - def test_passwordless_and_two_factor_configuration_mismatch(app, sqlalchemy_datastore): with pytest.raises(ValueError): init_app_with_options( @@ -328,7 +299,7 @@ @pytest.mark.settings(hashing_schemes=["hex_md5"], deprecated_hashing_schemes=[]) -@pytest.mark.parametrize("data", [u"hellö", b"hello"]) +@pytest.mark.parametrize("data", ["hellö", b"hello"]) def test_legacy_hash(in_app_context, data): legacy_hash = hashlib.md5(encode_string(data)).hexdigest() new_hash = hash_data(data) @@ -337,23 +308,23 @@ def test_hash_data(in_app_context): data = hash_data(b"hello") - assert isinstance(data, string_types) - data = hash_data(u"hellö") - assert isinstance(data, string_types) + assert isinstance(data, str) + data = hash_data("hellö") + assert isinstance(data, str) def test_verify_hash(in_app_context): - data = hash_data(u"hellö") - assert verify_hash(data, u"hellö") is True - assert verify_hash(data, u"hello") is False - - legacy_data = hashlib.md5(encode_string(u"hellö")).hexdigest() - assert verify_hash(legacy_data, u"hellö") is True - assert verify_hash(legacy_data, u"hello") is False + data = hash_data("hellö") + assert verify_hash(data, "hellö") is True + assert verify_hash(data, "hello") is False + + legacy_data = hashlib.md5(encode_string("hellö")).hexdigest() + assert verify_hash(legacy_data, "hellö") is True + assert verify_hash(legacy_data, "hello") is False @pytest.mark.settings( - password_salt=u"öööööööööööööööööööööööööööööööööö", password_hash="bcrypt" + password_salt="öööööööööööööööööööööööööööööööööö", password_hash="bcrypt" ) def test_password_unicode_password_salt(client): response = authenticate(client) @@ -437,17 +408,18 @@ @pytest.mark.babel(False) def test_without_babel(client): - response = client.get("/login") - assert b"Login" in response.data + # This isn't really 'without' babel - it is without initializing babel + with pytest.raises(ValueError): + client.get("/login") def test_no_email_sender(app): - """ Verify that if SECURITY_EMAIL_SENDER is default - (which is a local proxy) that send_mail picks up MAIL_DEFAULT_SENDER. + """Verify that if SECURITY_EMAIL_SENDER is default + (which is a local proxy) that send_mail picks up MAIL_DEFAULT_SENDER. """ app.config["MAIL_DEFAULT_SENDER"] = "test@testme.com" - class TestUser(object): + class TestUser: def __init__(self, email): self.email = email @@ -455,6 +427,7 @@ security.init_app(app) with app.app_context(): + app.try_trigger_before_first_request_functions() user = TestUser("matt@lp.com") with app.mail.record_messages() as outbox: send_mail("Test Default Sender", user.email, "welcome", user=user) @@ -520,6 +493,8 @@ response = client.get("/login", headers=[("Accept-Language", "fr")]) assert b'' in response.data + # make sure template contents get xlated (not just form). + assert b"

    Connexion

    " in response.data data = dict(email="matt@lp.com", password="", remember="y") response = client.post("/login", data=data, headers=[("Accept-Language", "fr")]) @@ -539,6 +514,7 @@ response = client.get("/change", follow_redirects=True) assert response.status_code == 200 assert b"Nouveau mot de passe" in response.data + assert b"

    Changer de mot de passe

    " in response.data # try JSON response = client.post( @@ -566,8 +542,8 @@ with app.test_request_context(): user = TestUser("jwag@notme.com") - result = app.security._password_validator("simple", False, user=user) - print(result) + pbad, pnorm = app.security._password_util.validate("simple", False, user=user) + print(pbad) """ @@ -576,15 +552,13 @@ def test_breached(app): # partial response from: https://api.pwnedpasswords.com/range/07003 - pwned_response = "AF5A73CD3CBCFDCD12B0B68CB7930F3E888:2\r\n\ + pwned_response = b"AF5A73CD3CBCFDCD12B0B68CB7930F3E888:2\r\n\ AFD8AA47E6FD782ADDC11D89744769F7354:2\r\n\ B04334E179537C975D0B3C72DA2E5B68E44:15\r\n\ B118F58C2373FDF97ACF93BD3339684D1EB:2\r\n\ B1ED5D27429EDF77EFD84F4EA9BDA5013FB:4\r\n\ B25C03CFBE4CBF19E0F4889711C9A488E5D:2\r\n\ -B3902FD808DCA504AAAD30F3C14BD3ACE7C:10".encode( - "utf-8" - ) +B3902FD808DCA504AAAD30F3C14BD3ACE7C:10" app.security = Security() app.security.init_app(app) @@ -593,7 +567,7 @@ mock_urlopen.return_value.__enter__.return_value.read.return_value = ( pwned_response ) - pbad = app.security._password_validator("flaskflask", False) + pbad, pnorm = app.security._password_util.validate("flaskflask", False) assert len(pbad) == 1 assert app.config["SECURITY_MSG_PASSWORD_BREACHED"][0] in pbad[0] @@ -607,15 +581,13 @@ def test_breached_cnt(app): # partial response from: https://api.pwnedpasswords.com/range/07003 - pwned_response = "AF5A73CD3CBCFDCD12B0B68CB7930F3E888:2\r\n\ + pwned_response = b"AF5A73CD3CBCFDCD12B0B68CB7930F3E888:2\r\n\ AFD8AA47E6FD782ADDC11D89744769F7354:2\r\n\ B04334E179537C975D0B3C72DA2E5B68E44:15\r\n\ B118F58C2373FDF97ACF93BD3339684D1EB:2\r\n\ B1ED5D27429EDF77EFD84F4EA9BDA5013FB:4\r\n\ B25C03CFBE4CBF19E0F4889711C9A488E5D:2\r\n\ -B3902FD808DCA504AAAD30F3C14BD3ACE7C:10".encode( - "utf-8" - ) +B3902FD808DCA504AAAD30F3C14BD3ACE7C:10" app.security = Security() app.security.init_app(app) @@ -624,7 +596,7 @@ mock_urlopen.return_value.__enter__.return_value.read.return_value = ( pwned_response ) - pbad = app.security._password_validator("flaskflask", True) + pbad, pnorm = app.security._password_util.validate("flaskflask", True) # Still weak password, just not pwned enough. Should fail complexity assert len(pbad) == 1 assert "Repeats like" in pbad[0] @@ -638,7 +610,7 @@ app.security = Security() app.security.init_app(app) with app.test_request_context(): - pbad = app.security._password_validator("flaskflask", True) + pbad, pnorm = app.security._password_util.validate("flaskflask", True) assert len(pbad) == 1 assert app.config["SECURITY_MSG_PASSWORD_BREACHED"][0] in pbad[0] @@ -698,7 +670,10 @@ def test_phone_util_override(app): - class MyPhoneUtil(object): + class MyPhoneUtil: + def __init__(self, app): + pass + def validate_phone_number(self, input_data): return "call-me" @@ -709,9 +684,6 @@ app.security.init_app(app, phone_util_cls=MyPhoneUtil) with app.app_context(): - client = app.test_client() - # trigger @before first request - client.get("/login") assert uia_phone_mapper("55") == "very-canonical" @@ -911,3 +883,131 @@ response = client.get("/fresh", headers=headers) assert response.status_code == 200 assert response.json["title"] == "Fresh Only" + + +@pytest.mark.changeable() +def test_verify_pwd_json(app, client, get_message): + # Make sure verify accepts a normalized and original password. + authenticate(client) + headers = {"Accept": "application/json", "Content-Type": "application/json"} + data = dict( + password="password", + new_password="new strong password\N{ROMAN NUMERAL ONE}", + new_password_confirm="new strong password\N{ROMAN NUMERAL ONE}", + ) + response = client.post( + "/change", + json=data, + headers={"Content-Type": "application/json"}, + ) + assert response.status_code == 200 + + response = client.post( + "/verify", + json=dict(password="new strong password\N{ROMAN NUMERAL ONE}"), + headers=headers, + ) + assert response.status_code == 200 + + response = client.post( + "/verify", + json=dict(password="new strong password\N{LATIN CAPITAL LETTER I}"), + headers=headers, + ) + assert response.status_code == 200 + + +@pytest.mark.settings(verify_url="/auth/") +def test_verify_next(app, client, get_message): + authenticate(client) + response = client.post( + "/auth/?next=http://localhost/mynext", + data=dict(password="password"), + follow_redirects=False, + ) + assert response.location == "http://localhost/mynext" + + response = client.post( + "/auth/?next=http%3A%2F%2F127.0.0.1%3A5000%2Fdashboard%2Fsettings%2F", + data=dict(password="password"), + follow_redirects=False, + base_url="http://127.0.0.1:5000", + ) + assert response.location == "http://127.0.0.1:5000/dashboard/settings/" + + +def test_direct_decorator(app, client, get_message): + """ Test/show calling the auth_required decorator directly """ + headers = {"Accept": "application/json", "Content-Type": "application/json"} + + def myview(): + return roles_required("author")(domyview)() + + def domyview(): + return Response(status=200) + + app.add_url_rule("/myview", view_func=myview, methods=["GET"]) + + authenticate(client) + response = client.get("/myview", headers=headers) + assert response.status_code == 403 + + logout(client) + + authenticate(client, email="jill@lp.com") + response = client.get("/myview", headers=headers) + assert response.status_code == 200 + + +def test_authn_via(app, client, get_message): + """ Test that we get correct fs_authn_via set in request """ + + @auth_required(within=30, grace=0) + def myview(): + assert get_request_attr("fs_authn_via") == "session" + return Response(status=200) + + app.add_url_rule("/myview", view_func=myview, methods=["GET"]) + authenticate(client) + + # This should work and not be redirected + response = client.get("/myview", follow_redirects=False) + assert response.status_code == 200 + + +def test_post_security_with_application_root(app, sqlalchemy_datastore): + init_app_with_options(app, sqlalchemy_datastore, **{"APPLICATION_ROOT": "/root"}) + client = app.test_client() + + response = client.post( + "/login", data=dict(email="matt@lp.com", password="password") + ) + assert response.status_code == 302 + assert response.headers["Location"] == "http://localhost/root" + + response = client.get("/logout") + assert response.status_code == 302 + assert response.headers["Location"] == "http://localhost/root" + + +def test_post_security_with_application_root_and_views(app, sqlalchemy_datastore): + init_app_with_options( + app, + sqlalchemy_datastore, + **{ + "APPLICATION_ROOT": "/root", + "SECURITY_POST_LOGIN_VIEW": "/post_login", + "SECURITY_POST_LOGOUT_VIEW": "/post_logout", + } + ) + client = app.test_client() + + response = client.post( + "/login", data=dict(email="matt@lp.com", password="password") + ) + assert response.status_code == 302 + assert response.headers["Location"] == "http://localhost/post_login" + + response = client.get("/logout") + assert response.status_code == 302 + assert response.headers["Location"] == "http://localhost/post_logout" diff -Nru flask-security-3.4.2/tests/test_passwordless.py flask-security-4.0.0/tests/test_passwordless.py --- flask-security-3.4.2/tests/test_passwordless.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/tests/test_passwordless.py 2021-01-26 02:39:51.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ test_passwordless ~~~~~~~~~~~~~~~~~ @@ -7,23 +6,18 @@ """ import time +from urllib.parse import parse_qsl, urlsplit import pytest from flask import Flask -from utils import logout - -from flask_security.core import UserMixin -from flask_security.signals import login_instructions_sent -from flask_security.utils import ( +from tests.test_utils import ( capture_flashes, capture_passwordless_login_requests, - string_types, + logout, ) -try: - from urlparse import parse_qsl, urlsplit -except ImportError: # pragma: no cover - from urllib.parse import parse_qsl, urlsplit +from flask_security.core import UserMixin +from flask_security.signals import login_instructions_sent pytestmark = pytest.mark.passwordless() @@ -35,7 +29,7 @@ def on_instructions_sent(app, user, login_token): assert isinstance(app, Flask) assert isinstance(user, UserMixin) - assert isinstance(login_token, string_types) + assert isinstance(login_token, str) recorded.append(user) # Test disabled account @@ -168,8 +162,7 @@ assert "localhost:8081" == split.netloc assert "/login-error" == split.path qparams = dict(parse_qsl(split.query)) - assert len(qparams) == 2 - assert all(k in qparams for k in ["email", "error"]) + assert all(k in qparams for k in ["email", "error", "identity"]) msg = get_message("LOGIN_EXPIRED", within="1 milliseconds", email="matt@lp.com") assert msg == qparams["error"].encode("utf-8") diff -Nru flask-security-3.4.2/tests/test_recoverable.py flask-security-4.0.0/tests/test_recoverable.py --- flask-security-3.4.2/tests/test_recoverable.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/tests/test_recoverable.py 2021-01-26 02:39:51.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ test_recoverable ~~~~~~~~~~~~~~~~ @@ -6,30 +5,27 @@ Recoverable functionality tests """ +import re import time +from urllib.parse import parse_qsl, urlsplit import pytest from flask import Flask -from utils import authenticate, json_authenticate, json_logout, logout, verify_token - -from flask_security.core import UserMixin -from flask_security.forms import LoginForm -from flask_security.signals import password_reset, reset_password_instructions_sent -from flask_security.utils import ( +from tests.test_utils import ( + authenticate, capture_flashes, capture_reset_password_requests, - string_types, + logout, ) -try: - from urlparse import parse_qsl, urlsplit -except ImportError: # pragma: no cover - from urllib.parse import parse_qsl, urlsplit +from flask_security.core import UserMixin +from flask_security.forms import LoginForm +from flask_security.signals import password_reset, reset_password_instructions_sent pytestmark = pytest.mark.recoverable() -def test_recoverable_flag(app, client, get_message): +def test_recoverable_flag(app, clients, get_message): recorded_resets = [] recorded_instructions_sent = [] @@ -41,17 +37,18 @@ def on_instructions_sent(app, user, token): assert isinstance(app, Flask) assert isinstance(user, UserMixin) - assert isinstance(token, string_types) + assert isinstance(token, str) recorded_instructions_sent.append(user) # Test the reset view - response = client.get("/reset") + response = clients.get("/reset") assert b"

    Send password reset instructions

    " in response.data + assert re.search(b']*type="email"[^>]*>', response.data) # Test submitting email to reset password creates a token and sends email with capture_reset_password_requests() as requests: with app.mail.record_messages() as outbox: - response = client.post( + response = clients.post( "/reset", data=dict(email="joe@lp.com"), follow_redirects=True ) @@ -62,18 +59,18 @@ token = requests[0]["token"] # Test view for reset token - response = client.get("/reset/" + token) + response = clients.get("/reset/" + token) assert b"

    Reset password

    " in response.data # Test submitting a new password but leave out confirm - response = client.post( + response = clients.post( "/reset/" + token, data={"password": "newpassword"}, follow_redirects=True ) assert get_message("PASSWORD_NOT_PROVIDED") in response.data assert len(recorded_resets) == 0 # Test submitting a new password - response = client.post( + response = clients.post( "/reset/" + token, data={"password": "awesome sunset", "password_confirm": "awesome sunset"}, follow_redirects=True, @@ -82,26 +79,26 @@ assert get_message("PASSWORD_RESET") in response.data assert len(recorded_resets) == 1 - logout(client) + logout(clients) # Test logging in with the new password response = authenticate( - client, "joe@lp.com", "awesome sunset", follow_redirects=True + clients, "joe@lp.com", "awesome sunset", follow_redirects=True ) assert b"Welcome joe@lp.com" in response.data - logout(client) + logout(clients) # Test invalid email - response = client.post( + response = clients.post( "/reset", data=dict(email="bogus@lp.com"), follow_redirects=True ) assert get_message("USER_DOES_NOT_EXIST") in response.data - logout(client) + logout(clients) # Test invalid token - response = client.post( + response = clients.post( "/reset/bogus", data={"password": "awesome sunset", "password_confirm": "awesome sunset"}, follow_redirects=True, @@ -114,7 +111,7 @@ "BZEw_Q.lQyo3npdPZtcJ_sNHVHP103syjM" "&url_id=fbb89a8328e58c181ea7d064c2987874bc54a23d" ) - response = client.post( + response = clients.post( "/reset/" + token, data={"password": "newpassword", "password_confirm": "newpassword"}, follow_redirects=True, @@ -122,6 +119,20 @@ assert get_message("INVALID_RESET_PASSWORD_TOKEN") in response.data +@pytest.mark.confirmable() +@pytest.mark.registerable() +@pytest.mark.settings(requires_confirmation_error_view="/confirm") +def test_requires_confirmation_error_redirect(app, clients): + data = dict(email="jyl@lp.com", password="awesome sunset") + clients.post("/register", data=data) + + response = clients.post( + "/reset", data=dict(email="jyl@lp.com"), follow_redirects=True + ) + assert b"send_confirmation_form" in response.data + assert b"jyl@lp.com" in response.data + + @pytest.mark.settings() def test_recoverable_json(app, client, get_message): recorded_resets = [] @@ -181,7 +192,7 @@ ) assert all( k in response.json["response"]["user"] - for k in ["id", "authentication_token"] + for k in ["email", "authentication_token"] ) assert len(recorded_resets) == 1 @@ -196,7 +207,7 @@ ) assert all( k in response.json["response"]["user"] - for k in ["id", "authentication_token"] + for k in ["email", "authentication_token"] ) logout(client) @@ -222,6 +233,43 @@ assert len(flashes) == 0 +def test_recover_invalidates_session(app, client): + # Make sure that if we reset our password - prior sessions are invalidated. + + other_client = app.test_client() + authenticate(other_client) + response = other_client.get("/profile", follow_redirects=True) + assert b"Profile Page" in response.data + + # use normal client to reset password + with capture_reset_password_requests() as requests: + response = client.post( + "/reset", + json=dict(email="matt@lp.com"), + headers={"Content-Type": "application/json"}, + ) + assert response.headers["Content-Type"] == "application/json" + + assert response.status_code == 200 + token = requests[0]["token"] + + # Test submitting a new password + response = client.post( + "/reset/" + token + "?include_auth_token", + json=dict(password="awesome sunset", password_confirm="awesome sunset"), + headers={"Content-Type": "application/json"}, + ) + assert all( + k in response.json["response"]["user"] + for k in ["email", "authentication_token"] + ) + + # try to access protected endpoint with old session - shouldn't work + response = other_client.get("/profile") + assert response.status_code == 302 + assert response.headers["Location"] == "http://localhost/login?next=%2Fprofile" + + def test_login_form_description(sqlalchemy_app): app = sqlalchemy_app() with app.test_request_context("/login"): @@ -432,8 +480,7 @@ assert "localhost:8081" == split.netloc assert "/reset-error" == split.path qparams = dict(parse_qsl(split.query)) - assert len(qparams) == 2 - assert all(k in qparams for k in ["email", "error"]) + assert all(k in qparams for k in ["email", "error", "identity"]) msg = get_message( "PASSWORD_RESET_EXPIRED", within="1 milliseconds", email="joe@lp.com" @@ -460,41 +507,6 @@ assert len(flashes) == 0 -@pytest.mark.settings(backwards_compat_auth_token_invalid=True) -def test_bc_password(app, client_nc): - # Test behavior of BACKWARDS_COMPAT_AUTH_TOKEN_INVALID - response = json_authenticate(client_nc, email="joe@lp.com") - token = response.json["response"]["user"]["authentication_token"] - verify_token(client_nc, token) - json_logout(client_nc, token) - - with capture_reset_password_requests() as requests: - response = client_nc.post( - "/reset", - json=dict(email="joe@lp.com"), - headers={"Content-Type": "application/json"}, - ) - assert response.status_code == 200 - - reset_token = requests[0]["token"] - - data = dict(password="awesome sunset", password_confirm="awesome sunset") - response = client_nc.post( - "/reset/" + reset_token + "?include_auth_token=1", - json=data, - headers={"Content-Type": "application/json"}, - ) - assert response.status_code == 200 - assert "authentication_token" in response.json["response"]["user"] - - # changing password should have rendered existing auth tokens invalid - verify_token(client_nc, token, status=401) - - # but new auth token should work - token = response.json["response"]["user"]["authentication_token"] - verify_token(client_nc, token) - - @pytest.mark.settings(password_complexity_checker="zxcvbn") def test_easy_password(client, get_message): with capture_reset_password_requests() as requests: @@ -524,3 +536,11 @@ headers={"Content-Type": "application/json"}, ) assert response.status_code == 400 + + +def test_email_normalization(client, get_message): + response = client.post( + "/reset", data=dict(email="joe@LP.COM"), follow_redirects=True + ) + assert response.status_code == 200 + assert get_message("PASSWORD_RESET_REQUEST", email="joe@lp.com") in response.data diff -Nru flask-security-3.4.2/tests/test_registerable.py flask-security-4.0.0/tests/test_registerable.py --- flask-security-3.4.2/tests/test_registerable.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/tests/test_registerable.py 2021-01-26 02:39:51.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ test_registerable ~~~~~~~~~~~~~~~~~ @@ -7,24 +6,33 @@ """ import pytest +import re from flask import Flask -from utils import authenticate, logout +import jinja2 +from tests.test_utils import authenticate, check_xlation, logout from flask_security import Security from flask_security.core import UserMixin -from flask_security.forms import ConfirmRegisterForm, RegisterForm, StringField +from flask_security.forms import ( + ConfirmRegisterForm, + RegisterForm, + StringField, + _default_field_labels, +) from flask_security.signals import user_registered +from flask_security.utils import localize_callback pytestmark = pytest.mark.registerable() @pytest.mark.settings(post_register_view="/post_register") -def test_registerable_flag(client, app, get_message): +def test_registerable_flag(clients, app, get_message): recorded = [] # Test the register view - response = client.get("/register") + response = clients.get("/register") assert b"

    Register

    " in response.data + assert re.search(b']*type="email"[^>]*>', response.data) # Test registering is successful, sends email, and fires signal @user_registered.connect_via(app) @@ -44,37 +52,37 @@ next="", ) with app.mail.record_messages() as outbox: - response = client.post("/register", data=data, follow_redirects=True) + response = clients.post("/register", data=data, follow_redirects=True) assert len(recorded) == 1 assert len(outbox) == 1 assert b"Post Register" in response.data - logout(client) + logout(clients) # Test user can login after registering - response = authenticate(client, email="dude@lp.com", password="battery staple") + response = authenticate(clients, email="dude@lp.com", password="battery staple") assert response.status_code == 302 - logout(client) + logout(clients) # Test registering with an existing email data = dict( email="dude@lp.com", password="password", password_confirm="password", next="" ) - response = client.post("/register", data=data, follow_redirects=True) + response = clients.post("/register", data=data, follow_redirects=True) assert get_message("EMAIL_ALREADY_ASSOCIATED", email="dude@lp.com") in response.data # Test registering with an existing email but case insensitive data = dict( email="Dude@lp.com", password="password", password_confirm="password", next="" ) - response = client.post("/register", data=data, follow_redirects=True) + response = clients.post("/register", data=data, follow_redirects=True) assert get_message("EMAIL_ALREADY_ASSOCIATED", email="Dude@lp.com") in response.data # Test registering with JSON data = dict(email="dude2@lp.com", password="horse battery") - response = client.post( + response = clients.post( "/register", json=data, headers={"Content-Type": "application/json"} ) @@ -83,17 +91,17 @@ assert len(response.json["response"]) == 2 assert all(k in response.json["response"] for k in ["csrf_token", "user"]) - logout(client) + logout(clients) # Test registering with invalid JSON data = dict(email="bogus", password="password") - response = client.post( + response = clients.post( "/register", json=data, headers={"Content-Type": "application/json"} ) assert response.headers["content-type"] == "application/json" assert response.json["meta"]["code"] == 400 - logout(client) + logout(clients) # Test ?next param data = dict( @@ -103,11 +111,64 @@ next="", ) - response = client.post("/register?next=/page1", data=data, follow_redirects=True) + response = clients.post("/register?next=/page1", data=data, follow_redirects=True) assert b"Page 1" in response.data @pytest.mark.confirmable() +def test_xlation(app, client, get_message_local): + # Test form and email translation + app.config["BABEL_DEFAULT_LOCALE"] = "fr_FR" + assert check_xlation(app, "fr_FR"), "You must run python setup.py compile_catalog" + + response = client.get("/register", follow_redirects=True) + with app.app_context(): + # Check header + assert ( + f'

    {localize_callback("Register")}

    '.encode("utf-8") in response.data + ) + submit = localize_callback(_default_field_labels["register"]) + assert f'value="{submit}"'.encode("utf-8") in response.data + + with app.mail.record_messages() as outbox: + response = client.post( + "/register", + data={ + "email": "me@fr.com", + "password": "new strong password", + "password_confirm": "new strong password", + }, + follow_redirects=True, + ) + + with app.app_context(): + assert ( + get_message_local("CONFIRM_REGISTRATION", email="me@fr.com").encode("utf-8") + in response.data + ) + assert b"Home Page" in response.data + assert len(outbox) == 1 + assert ( + localize_callback(app.config["SECURITY_EMAIL_SUBJECT_REGISTER"]) + in outbox[0].subject + ) + assert ( + str( + jinja2.escape( + localize_callback( + "You can confirm your email through the link below:" + ) + ) + ) + in outbox[0].html + ) + assert ( + localize_callback("You can confirm your email through the link below:") + in outbox[0].body + ) + + +@pytest.mark.confirmable() def test_required_password(client, get_message): # when confirm required - should not require confirm_password - but should # require a password @@ -204,7 +265,7 @@ @pytest.mark.two_factor() @pytest.mark.settings(two_factor_required=True) def test_two_factor(app, client): - """ If two-factor is enabled, the register shouldn't login, but start the + """If two-factor is enabled, the register shouldn't login, but start the 2-factor setup. """ data = dict(email="dude@lp.com", password="password", password_confirm="password") @@ -345,3 +406,54 @@ "/register", json=data, headers={"Content-Type": "application/json"} ) assert response.status_code == 200 + + +def test_email_normalization(app, client): + # should be able to login either as LP.com or lp.com + data = dict( + email="\N{BLACK SCISSORS}@LP.com", + password="battery staple", + password_confirm="battery staple", + ) + + response = client.post("/register", data=data, follow_redirects=True) + assert b"Home Page" in response.data + logout(client) + + # Test user can login after registering + response = authenticate( + client, email="\N{BLACK SCISSORS}@lp.com", password="battery staple" + ) + assert response.status_code == 302 + + logout(client) + # Test user can login after registering using original non-canonical email + response = authenticate( + client, email="\N{BLACK SCISSORS}@LP.com", password="battery staple" + ) + assert response.status_code == 302 + + +def test_email_normalization_options(app, client, get_message): + # verify can set options for email_validator + data = dict( + email="\N{BLACK SCISSORS}@LP.com", + password="battery staple", + password_confirm="battery staple", + ) + + response = client.post("/register", data=data, follow_redirects=True) + assert b"Home Page" in response.data + logout(client) + + # turn off allowing 'local' part unicode. + app.config["SECURITY_EMAIL_VALIDATOR_ARGS"] = {"allow_smtputf8": False} + data = dict( + email="\N{WHITE SCISSORS}@LP.com", + password="battery staple", + password_confirm="battery staple", + ) + + response = client.post("/register", data=data, follow_redirects=True) + assert response.status_code == 200 + assert get_message("INVALID_EMAIL_ADDRESS") in response.data diff -Nru flask-security-3.4.2/tests/test_response.py flask-security-4.0.0/tests/test_response.py --- flask-security-3.4.2/tests/test_response.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/tests/test_response.py 2021-01-26 02:39:51.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ test_response ~~~~~~~~~~~~~~~~~ @@ -13,7 +12,7 @@ from flask import jsonify -from utils import authenticate +from tests.test_utils import authenticate def test_render_json(app, client): @@ -51,11 +50,11 @@ def test_default_unauthn(app, client): """ Test default unauthn handler with and without json """ - response = client.get("/multi_auth") + response = client.get("/profile") assert response.status_code == 302 - assert response.headers["Location"] == "http://localhost/login?next=%2Fmulti_auth" + assert response.headers["Location"] == "http://localhost/login?next=%2Fprofile" - response = client.get("/multi_auth", headers={"Accept": "application/json"}) + response = client.get("/profile", headers={"Accept": "application/json"}) assert response.status_code == 401 assert response.json["meta"]["code"] == 401 # While "Basic" is acceptable, we never get a WWW-Authenticate header back since @@ -67,11 +66,11 @@ def test_default_unauthn_bp(app, client): """ Test default unauthn handler with blueprint prefix and login url """ - response = client.get("/multi_auth") + response = client.get("/profile") assert response.status_code == 302 assert ( response.headers["Location"] - == "http://localhost/myprefix/mylogin?next=%2Fmulti_auth" + == "http://localhost/myprefix/mylogin?next=%2Fprofile" ) diff -Nru flask-security-3.4.2/tests/test_trackable.py flask-security-4.0.0/tests/test_trackable.py --- flask-security-3.4.2/tests/test_trackable.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/tests/test_trackable.py 2021-01-26 02:39:51.000000000 +0000 @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ test_trackable ~~~~~~~~~~~~~~ @@ -8,11 +7,12 @@ import pytest from flask import after_this_request, redirect -from utils import authenticate, logout from werkzeug.middleware.proxy_fix import ProxyFix from flask_security import login_user +from tests.test_utils import authenticate, logout + pytestmark = pytest.mark.trackable() diff -Nru flask-security-3.4.2/tests/test_two_factor.py flask-security-4.0.0/tests/test_two_factor.py --- flask-security-3.4.2/tests/test_two_factor.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/tests/test_two_factor.py 2021-01-26 02:39:51.000000000 +0000 @@ -1,29 +1,33 @@ -# -*- coding: utf-8 -*- """ test_two_factor ~~~~~~~~~~~~~~~~~ two_factor tests - :copyright: (c) 2019-2020 by J. Christopher Wagner (jwag). + :copyright: (c) 2019-2021 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ -try: - from unittest.mock import Mock -except ImportError: - from mock import Mock +from datetime import timedelta +import re +from passlib.totp import TOTP import pytest - -from utils import SmsBadSender, SmsTestSender, authenticate, get_session, logout from flask_principal import identity_changed from flask_security import ( SQLAlchemyUserDatastore, SmsSenderFactory, reset_password_instructions_sent, + uia_email_mapper, +) +from tests.test_utils import ( + SmsBadSender, + SmsTestSender, + authenticate, + capture_flashes, + get_session, + logout, ) -from flask_security.utils import capture_flashes pytestmark = pytest.mark.two_factor() @@ -32,36 +36,37 @@ SmsSenderFactory.senders["bad"] = SmsBadSender -class TestMail(object): - def __init__(self): - self.count = 0 - self.msg = None - - def send(self, msg): - self.msg = msg - self.count += 1 - - -def tf_authenticate(app, client, validate=True): - """ Login/Authenticate using two factor. +def tf_authenticate(app, client, json=False, validate=True, remember=False): + """Login/Authenticate using two factor. This is the equivalent of utils:authenticate """ prev_sms = app.config["SECURITY_SMS_SERVICE"] app.config["SECURITY_SMS_SERVICE"] = "test" sms_sender = SmsSenderFactory.createSender("test") - json_data = dict(email="gal@lp.com", password="password") + json_data = dict(email="gal@lp.com", password="password", remember=remember) response = client.post( "/login", json=json_data, headers={"Content-Type": "application/json"} ) + assert b'"code": 200' in response.data app.config["SECURITY_SMS_SERVICE"] = prev_sms if validate: code = sms_sender.messages[0].split()[-1] - response = client.post( - "/tf-validate", data=dict(code=code), follow_redirects=True - ) - assert response.status_code == 200 + if json: + response = client.post( + "/tf-validate", + json=dict(code=code), + headers={"Content-Type": "application/json"}, + ) + assert b'"code": 200' in response.data + return response.json["response"].get("tf_validity_token", None) + else: + response = client.post( + "/tf-validate", data=dict(code=code), follow_redirects=True + ) + + assert response.status_code == 200 def tf_in_session(session): @@ -71,13 +76,189 @@ "tf_state", "tf_primary_method", "tf_user_id", - "tf_confirmed", "tf_remember_login", "tf_totp_secret", ] ) +@pytest.mark.settings(two_factor_always_validate=False) +def test_always_validate(app, client): + tf_authenticate(app, client, remember=True) + cookie = next( + (cookie for cookie in client.cookie_jar if cookie.name == "tf_validity"), None + ) + assert cookie is not None + + logout(client) + + data = dict(email="gal@lp.com", password="password") + response = client.post("/login", data=data, follow_redirects=True) + assert b"Welcome gal@lp.com" in response.data + assert response.status_code == 200 + + logout(client) + data = dict(email="gal2@lp.com", password="password") + response = client.post("/login", data=data, follow_redirects=True) + assert b"Please enter your authentication code" in response.data + + # make sure the cookie doesn't affect the JSON request + client.cookie_jar.clear("localhost.local", "/", "tf_validity") + # Test JSON + token = tf_authenticate(app, client, json=True, remember=True) + logout(client) + data = dict(email="gal@lp.com", password="password", tf_validity_token=token) + response = client.post( + "/login", + json=data, + follow_redirects=True, + headers={"Content-Type": "application/json"}, + ) + assert response.status_code == 200 + # verify logged in + response = client.get("/profile", follow_redirects=False) + assert response.status_code == 200 + + logout(client) + + data["email"] = "gal2@lp.com" + response = client.post( + "/login", + json=data, + follow_redirects=True, + headers={"Content-Type": "application/json"}, + ) + assert response.status_code == 200 + assert response.json["response"]["tf_required"] + assert response.json["response"]["tf_state"] == "ready" + assert response.json["response"]["tf_primary_method"] == "authenticator" + + +@pytest.mark.settings(two_factor_always_validate=False) +def test_do_not_remember_tf_validity(app, client): + tf_authenticate(app, client) + logout(client) + + data = dict(email="gal@lp.com", password="password") + response = client.post("/login", data=data, follow_redirects=True) + assert b"Please enter your authentication code" in response.data + + # Test JSON + token = tf_authenticate(app, client, json=True) + logout(client) + assert token is None + + data = dict(email="gal@lp.com", password="password", tf_validity_token=token) + response = client.post( + "/login", + json=data, + follow_redirects=True, + headers={"Content-Type": "application/json"}, + ) + + assert response.status_code == 200 + assert response.json["response"]["tf_required"] + assert response.json["response"]["tf_state"] == "ready" + assert response.json["response"]["tf_primary_method"] == "sms" + + +@pytest.mark.settings( + two_factor_always_validate=False, two_factor_login_validity="-1 minutes" +) +def test_tf_expired_cookie(app, client): + tf_authenticate(app, client, remember=True) + logout(client) + + data = dict(email="gal@lp.com", password="password") + response = client.post("/login", data=data, follow_redirects=True) + + assert b"Please enter your authentication code" in response.data + + # Test JSON + token = tf_authenticate(app, client, json=True, remember=True) + logout(client) + data = dict(email="gal@lp.com", password="password", tf_validity_token=token) + response = client.post( + "/login", + json=data, + follow_redirects=True, + headers={"Content-Type": "application/json"}, + ) + assert response.status_code == 200 + assert response.json["response"]["tf_required"] + assert response.json["response"]["tf_state"] == "ready" + assert response.json["response"]["tf_primary_method"] == "sms" + + +@pytest.mark.settings(two_factor_always_validate=False) +def test_change_uniquifier_invalidates_cookie(app, client): + tf_authenticate(app, client, remember=True) + logout(client) + with app.app_context(): + user = app.security.datastore.find_user(email="gal@lp.com") + app.security.datastore.set_uniquifier(user) + app.security.datastore.commit() + + data = dict(email="gal@lp.com", password="password") + response = client.post("/login", data=data, follow_redirects=True) + + assert b"Please enter your authentication code" in response.data + + client.cookie_jar.clear("localhost.local", "/", "tf_validity") + # Test JSON + token = tf_authenticate(app, client, json=True, remember=True) + logout(client) + with app.app_context(): + user = app.security.datastore.find_user(email="gal@lp.com") + app.security.datastore.set_uniquifier(user) + app.security.datastore.commit() + data = dict(email="gal@lp.com", password="password", tf_validity_token=token) + response = client.post( + "/login", + json=data, + follow_redirects=True, + headers={"Content-Type": "application/json"}, + ) + assert response.status_code == 200 + assert response.json["response"]["tf_required"] + assert response.json["response"]["tf_state"] == "ready" + assert response.json["response"]["tf_primary_method"] == "sms" + + +@pytest.mark.settings(two_factor_always_validate=False, two_factor_required=True) +def test_tf_reset_invalidates_cookie(app, client): + tf_authenticate(app, client, remember=True) + logout(client) + with app.app_context(): + user = app.security.datastore.find_user(email="gal@lp.com") + app.security.datastore.reset_user_access(user) + app.security.datastore.commit() + + data = dict(email="gal@lp.com", password="password") + response = client.post("/login", data=data, follow_redirects=True) + + assert b"Two-factor authentication adds an extra layer of security" in response.data + + client.cookie_jar.clear("localhost.local", "/", "tf_validity") + # Test JSON + token = tf_authenticate(app, client, json=True, remember=True, validate=False) + logout(client) + with app.app_context(): + user = app.security.datastore.find_user(email="gal@lp.com") + app.security.datastore.reset_user_access(user) + app.security.datastore.commit() + data = dict(email="gal@lp.com", password="password", tf_validity_token=token) + response = client.post( + "/login", + json=data, + follow_redirects=True, + headers={"Content-Type": "application/json"}, + ) + assert response.status_code == 200 + assert response.json["response"]["tf_required"] + assert response.json["response"]["tf_state"] == "setup_from_login" + + @pytest.mark.settings(two_factor_required=True) def test_two_factor_two_factor_setup_anonymous(app, client, get_message): @@ -188,77 +369,39 @@ # Upon completion, session cookie shouldnt have any two factor stuff in it. assert not tf_in_session(get_session(response)) - # try confirming password with a wrong one - response = client.post("/tf-confirm", data=dict(password=""), follow_redirects=True) - assert b"Password not provided" in response.data + # Test change two_factor view to from sms to mail + with app.mail.record_messages() as outbox: + setup_data = dict(setup="email") + response = client.post("/tf-setup", data=setup_data, follow_redirects=True) + msg = b"To complete logging in, please enter the code sent to your mail" + assert msg in response.data - # try confirming password with a wrong one + json - data = dict(password="wrong_password") - response = client.post( - "/tf-confirm", - json=data, - headers={"Content-Type": "application/json"}, - follow_redirects=True, - ) - - assert response.json["meta"]["code"] == 400 - - # Test change two_factor password confirmation view to mail - password = "password" - response = client.post( - "/tf-confirm", data=dict(password=password), follow_redirects=True - ) - - assert b"You successfully confirmed password" in response.data - message = b"Two-factor authentication adds an extra layer of security" - assert message in response.data - - # change method (from sms to mail) - setup_data = dict(setup="email") - testMail = TestMail() - app.extensions["mail"] = testMail - response = client.post("/tf-setup", data=setup_data, follow_redirects=True) - msg = b"To complete logging in, please enter the code sent to your mail" - assert msg in response.data - - # Fetch token validate form - response = client.get("/tf-validate") - assert response.status_code == 200 - assert b'name="code"' in response.data + # Fetch token validate form + response = client.get("/tf-validate") + assert response.status_code == 200 + assert b'name="code"' in response.data - code = testMail.msg.body.split()[-1] + code = outbox[0].body.split()[-1] # sumbit right token and show appropriate response response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert b"You successfully changed your two-factor method" in response.data - # Test change two_factor password confirmation view to google authenticator - password = "password" - response = client.post( - "/tf-confirm", data=dict(password=password), follow_redirects=True - ) - assert b"You successfully confirmed password" in response.data - message = b"Two-factor authentication adds an extra layer of security" - assert message in response.data - + # Test change two_factor password confirmation view to authenticator # Setup authenticator setup_data = dict(setup="authenticator") response = client.post("/tf-setup", data=setup_data, follow_redirects=True) - assert b"Open your authenticator app on your device" in response.data - - # Now request code. We can't test the qrcode easily - but we can get the totp_secret - # that goes into the qrcode and make sure that works - mtf = Mock(wraps=app.security._totp_factory) - app.security.totp_factory(mtf) - qrcode_page_response = client.get( - "/tf-qrcode", data=setup_data, follow_redirects=True - ) - assert mtf.get_totp_uri.call_count == 1 - (username, totp_secret), _ = mtf.get_totp_uri.call_args - assert username == "gal@lp.com" - assert b"svg" in qrcode_page_response.data + assert b"Open an authenticator app on your device" in response.data + # verify png QRcode is present + assert b"data:image/svg+xml;base64," in response.data + + # parse out key + rd = response.data.decode("utf-8") + matcher = re.match(r".*((?:\S{4}-){7}\S{4}).*", rd, re.DOTALL) + totp_secret = matcher.group(1) # Generate token from passed totp_secret and confirm setup - code = app.security._totp_factory.generate_totp_password(totp_secret) + totp = TOTP(totp_secret) + code = totp.generate().token response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert b"You successfully changed your two-factor method" in response.data @@ -275,7 +418,7 @@ ) # Generate token from passed totp_secret - code = app.security._totp_factory.generate_totp_password(totp_secret) + code = totp.generate().token response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert b"Your token has been confirmed" in response.data @@ -297,20 +440,14 @@ message = b"Two-factor authentication adds an extra layer of security" assert message in response.data - # check availability of qrcode page when this option is not picked - qrcode_page_response = client.get("/two_factor_qrcode/", follow_redirects=False) - assert qrcode_page_response.status_code == 404 + # check availability of qrcode when this option is not picked + assert b"data:image/png;base64," not in response.data # check availability of qrcode page when this option is picked setup_data = dict(setup="authenticator") response = client.post("/tf-setup", data=setup_data, follow_redirects=True) - assert b"Open your authenticator app on your device" in response.data - - qrcode_page_response = client.get( - "/tf-qrcode", data=setup_data, follow_redirects=True - ) - print(qrcode_page_response) - assert b"svg" in qrcode_page_response.data + assert b"Open an authenticator app on your device" in response.data + assert b"data:image/svg+xml;base64," in response.data # check appearence of setup page when sms picked and phone number entered sms_sender = SmsSenderFactory.createSender("test") @@ -345,7 +482,7 @@ assert message in response.data rescue_data = dict(help_setup="no_mail_access") response = client.post("/tf-rescue", data=rescue_data, follow_redirects=True) - message = b"A mail was sent to us in order" + b" to reset your application account" + message = b"A mail was sent to us in order to reset your application account" assert message in response.data @@ -362,37 +499,37 @@ assert b"Phone number not valid" in response.data assert sms_sender.get_count() == 0 - client.post( + # Now setup good phone + response = client.post( "/tf-setup", data=dict(setup="sms", phone="650-555-1212"), follow_redirects=True ) assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] + # shouldn't get authenticator stuff when setting up SMS + assert b"data:image/png;base64," not in response.data response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert b"Your token has been confirmed" in response.data assert not tf_in_session(get_session(response)) + headers = {"Accept": "application/json", "Content-Type": "application/json"} + response = client.get("/tf-setup", headers=headers) + # N.B. right now for tfa - we don't canonicalize phone number (since user + # never has to type it in). + assert response.json["response"]["tf_phone_number"] == "650-555-1212" + @pytest.mark.settings(two_factor_required=True) def test_json(app, client): """ - Test all endpoints using JSON. (eventually) + Test login/setup using JSON. """ - - # Test that user not yet setup for 2FA gets correct response. - data = dict(email="matt@lp.com", password="password") - response = client.post( - "/login", json=data, headers={"Content-Type": "application/json"} - ) - assert response.json["response"]["tf_required"] - assert response.json["response"]["tf_state"] == "setup_from_login" + headers = {"Accept": "application/json", "Content-Type": "application/json"} # Login with someone already setup. sms_sender = SmsSenderFactory.createSender("test") data = dict(email="gal@lp.com", password="password") - response = client.post( - "/login", json=data, headers={"Content-Type": "application/json"} - ) + response = client.post("/login", json=data, headers=headers) assert response.status_code == 200 assert response.json["response"]["tf_required"] assert response.json["response"]["tf_state"] == "ready" @@ -400,14 +537,105 @@ # Verify SMS sent assert sms_sender.get_count() == 1 + code = sms_sender.messages[0].split()[-1] + response = client.post("/tf-validate", json=dict(code=code), headers=headers) + assert response.status_code == 200 + # verify logged in + response = client.get("/profile", follow_redirects=False) + assert response.status_code == 200 + logout(client) + + # Test that user not yet setup for 2FA gets correct response. + data = dict(email="matt@lp.com", password="password") + response = client.post("/login", json=data, headers=headers) + assert response.json["response"]["tf_required"] + assert response.json["response"]["tf_state"] == "setup_from_login" + + # Start setup process. + response = client.get("/tf-setup", headers=headers) + assert response.json["response"]["tf_required"] + assert "sms" in response.json["response"]["tf_available_methods"] + + # Now try to setup + data = dict(setup="sms", phone="+442083661177") + response = client.post("/tf-setup", json=data, headers=headers) + assert response.status_code == 200 + assert response.json["response"]["tf_state"] == "validating_profile" + assert response.json["response"]["tf_primary_method"] == "sms" + code = sms_sender.messages[0].split()[-1] + response = client.post("/tf-validate", json=dict(code=code), headers=headers) + assert response.status_code == 200 + assert "csrf_token" in response.json["response"] + assert response.json["response"]["user"]["email"] == "matt@lp.com" + logout(client) + + # Verify tf is now setup and can directly get code + data = dict(email="matt@lp.com", password="password") + response = client.post("/login", json=data, headers=headers) + assert response.json["response"]["tf_required"] + assert response.json["response"]["tf_state"] == "ready" code = sms_sender.messages[0].split()[-1] + response = client.post("/tf-validate", json=dict(code=code), headers=headers) + assert response.status_code == 200 + # verify logged in + response = client.get("/profile", follow_redirects=False) + assert response.status_code == 200 + + # tf-setup should provide existing info + response = client.get("/tf-setup", headers=headers) + assert response.json["response"]["tf_required"] + assert "sms" in response.json["response"]["tf_available_methods"] + assert "disable" not in response.json["response"]["tf_available_methods"] + assert response.json["response"]["tf_primary_method"] == "sms" + assert response.json["response"]["tf_phone_number"] == "+442083661177" + assert not tf_in_session(get_session(response)) + + +@pytest.mark.settings(two_factor_rescue_mail="helpme@myapp.com") +def test_rescue_json(app, client): + headers = {"Accept": "application/json", "Content-Type": "application/json"} + + # it's an error if not logged in. + rescue_data_json = dict(help_setup="lost_device") response = client.post( - "/tf-validate", - json=dict(code=code), - headers={"Content-Type": "application/json"}, + "/tf-rescue", + json=rescue_data_json, + headers=headers, + ) + assert response.status_code == 400 + + # check when two_factor_rescue function should appear + data = dict(email="gal2@lp.com", password="password") + response = client.post("/login", json=data, headers=headers) + assert response.json["response"]["tf_required"] + + with app.mail.record_messages() as outbox: + rescue_data = dict(help_setup="lost_device") + response = client.post("/tf-rescue", json=rescue_data, headers=headers) + assert response.status_code == 200 + + assert outbox[0].recipients == ["gal2@lp.com"] + assert outbox[0].sender == "no-reply@localhost" + assert outbox[0].subject == "Two-factor Login" + matcher = re.match(r".*code: ([0-9]+).*", outbox[0].body, re.IGNORECASE | re.DOTALL) + response = client.post( + "/tf-validate", json=dict(code=matcher.group(1)), headers=headers ) assert response.status_code == 200 + logout(client) + + # Try rescue with no email (should send email to admin) + client.post("/login", json=data, headers=headers) + with app.mail.record_messages() as outbox: + rescue_data = dict(help_setup="no_mail_access") + response = client.post("/tf-rescue", json=rescue_data, headers=headers) + assert response.status_code == 200 + + assert outbox[0].recipients == ["helpme@myapp.com"] + assert outbox[0].sender == "no-reply@localhost" + assert outbox[0].subject == "Two-factor Rescue" + assert "gal2@lp.com" in outbox[0].body @pytest.mark.settings(two_factor_required=True) @@ -426,11 +654,6 @@ response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert b"Your token has been confirmed" in response.data - response = client.post( - "/tf-confirm", data=dict(password="password"), follow_redirects=True - ) - assert b"You successfully confirmed password" in response.data - response = client.get("/tf-setup", follow_redirects=True) assert b"Disable two factor" not in response.data @@ -466,8 +689,9 @@ response = authenticate(client, "jill@lp.com") session = get_session(response) assert "tf_state" not in session - # Jill is 4th user to be added in utils.py - assert signalled_identity[0] == 4 + with app.app_context(): + user = app.security.datastore.find_user(email="jill@lp.com") + assert signalled_identity[0] == user.fs_uniquifier del signalled_identity[:] # try to validate @@ -493,24 +717,15 @@ response = authenticate(client, "jill@lp.com") session = get_session(response) assert "tf_state" not in session - # Jill is 4th user to be added in utils.py - assert signalled_identity[0] == 4 + with app.app_context(): + user = app.security.datastore.find_user(email="jill@lp.com") + assert signalled_identity[0] == user.fs_uniquifier del signalled_identity[:] - # opt-in for SMS 2FA - but we haven't re-verified password + # opt-in for SMS 2FA sms_sender = SmsSenderFactory.createSender("test") data = dict(setup="sms", phone="+442083661177") response = client.post("/tf-setup", data=data, follow_redirects=True) - message = b"You currently do not have permissions to access this page" - assert message in response.data - - # Confirm password - then opt-in - password = "password" - response = client.post( - "/tf-confirm", data=dict(password=password), follow_redirects=True - ) - data = dict(setup="sms", phone="+442083661177") - response = client.post("/tf-setup", data=data, follow_redirects=True) assert b"To Which Phone Number Should We Send Code To" in response.data assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] @@ -540,17 +755,12 @@ response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert b"Your token has been confirmed" in response.data # Verify now logged in - assert signalled_identity[0] == 4 + with app.app_context(): + user = app.security.datastore.find_user(email="jill@lp.com") + assert signalled_identity[0] == user.fs_uniquifier del signalled_identity[:] # Now opt back out. - # as before must reconfirm password first - response = client.get("/tf-setup", data=data, follow_redirects=True) - message = b"You currently do not have permissions to access this page" - assert message in response.data - - password = "password" - client.post("/tf-confirm", data=dict(password=password), follow_redirects=True) data = dict(setup="disable") response = client.post("/tf-setup", data=data, follow_redirects=True) assert b"You successfully disabled two factor authorization." in response.data @@ -564,8 +774,29 @@ response = authenticate(client, "jill@lp.com") session = get_session(response) assert "tf_state" not in session - # Jill is 4th user to be added in utils.py - assert signalled_identity[0] == 4 + with app.app_context(): + user = app.security.datastore.find_user(email="jill@lp.com") + assert signalled_identity[0] == user.fs_uniquifier + + +def test_opt_out_json(app, client): + headers = {"Accept": "application/json", "Content-Type": "application/json"} + + tf_authenticate(app, client) + response = client.get("tf-setup", headers=headers) + assert "disable" in response.json["response"]["tf_available_methods"] + + response = client.post("tf-setup", json=dict(setup="disable"), headers=headers) + assert response.status_code == 200 + logout(client) + + # Should be able to log in with just user/pass + response = authenticate(client, "gal@lp.com") + session = get_session(response) + assert "tf_state" not in session + # verify logged in + response = client.get("/profile", follow_redirects=False) + assert response.status_code == 200 @pytest.mark.recoverable() @@ -719,16 +950,12 @@ response = authenticate(client, "jill@lp.com") session = get_session(response) assert "tf_state" not in session - # Jill is 4th user to be added in utils.py - assert signalled_identity[0] == 4 + with app.app_context(): + user = app.security.datastore.find_user(email="jill@lp.com") + assert signalled_identity[0] == user.fs_uniquifier del signalled_identity[:] - # Confirm password sms_sender = SmsSenderFactory.createSender("test") - password = "password" - response = client.post( - "/tf-confirm", data=dict(password=password), follow_redirects=True - ) # Select sms method but do not send a phone number just yet (regenerates secret) data = dict(setup="sms") response = client.post("/tf-setup", data=data, follow_redirects=True) @@ -760,12 +987,6 @@ assert generated_secret == user.tf_totp_secret # Finally opt back out and check that tf_totp_secret is None - response = client.get("/tf-setup", data=data, follow_redirects=True) - message = b"You currently do not have permissions to access this page" - assert message in response.data - - password = "password" - client.post("/tf-confirm", data=dict(password=password), follow_redirects=True) data = dict(setup="disable") response = client.post("/tf-setup", data=data, follow_redirects=True) assert b"You successfully disabled two factor authorization." in response.data @@ -782,8 +1003,6 @@ @pytest.mark.settings(two_factor_enabled_methods=["authenticator"]) def test_just_authenticator(app, client): authenticate(client, email="jill@lp.com") - password = "password" - client.post("/tf-confirm", data=dict(password=password), follow_redirects=True) response = client.get("/tf-setup", follow_redirects=True) assert b"Set up using SMS" not in response.data @@ -797,83 +1016,82 @@ assert response.status_code == 200 -@pytest.mark.settings(USER_IDENTITY_ATTRIBUTES=("username", "email")) -def test_qrcode_identity(app, client): +@pytest.mark.settings( + USER_IDENTITY_ATTRIBUTES=[ + {"username": {"mapper": lambda x: "@" not in x}}, + {"email": {"mapper": uia_email_mapper}}, + ] +) +def test_authr_identity(app, client): # Setup authenticator + headers = {"Accept": "application/json", "Content-Type": "application/json"} authenticate(client, email="jill@lp.com") - client.post("/tf-confirm", data=dict(password="password"), follow_redirects=True) setup_data = dict(setup="authenticator") - response = client.post("/tf-setup", data=setup_data, follow_redirects=True) - assert b"Open your authenticator app on your device" in response.data - - # Now request code. Verify that we get 'username' not email. - mtf = Mock(wraps=app.security._totp_factory) - app.security.totp_factory(mtf) - qrcode_page_response = client.get( - "/tf-qrcode", data=setup_data, follow_redirects=True - ) - assert mtf.get_totp_uri.call_count == 1 - (username, totp_secret), _ = mtf.get_totp_uri.call_args - assert username == "jill" - assert b"svg" in qrcode_page_response.data + response = client.post("/tf-setup", json=setup_data, headers=headers) + assert response.json["response"]["tf_authr_issuer"] == "service_name" + assert response.json["response"]["tf_authr_username"] == "jill" + assert response.json["response"]["tf_state"] == "validating_profile" + assert "tf_authr_key" in response.json["response"] -@pytest.mark.settings(USER_IDENTITY_ATTRIBUTES=("security_number", "email")) -def test_qrcode_identity_num(app, client): - # Setup authenticator +@pytest.mark.settings( + USER_IDENTITY_ATTRIBUTES=[ + {"security_number": {"mapper": lambda x: x.isdigit()}}, + {"email": {"mapper": uia_email_mapper}}, + ] +) +def test_authr_identity_num(app, client): + # Test that response to setup has 'security_number' as the 'username' + # since it is listed first. + headers = {"Accept": "application/json", "Content-Type": "application/json"} authenticate(client, email="jill@lp.com") - client.post("/tf-confirm", data=dict(password="password"), follow_redirects=True) setup_data = dict(setup="authenticator") - response = client.post("/tf-setup", data=setup_data, follow_redirects=True) - assert b"Open your authenticator app on your device" in response.data - - # Now request code. Verify that we get 'security_number' not email. - mtf = Mock(wraps=app.security._totp_factory) - app.security.totp_factory(mtf) - qrcode_page_response = client.get( - "/tf-qrcode", data=setup_data, follow_redirects=True - ) - assert mtf.get_totp_uri.call_count == 1 - (username, totp_secret), _ = mtf.get_totp_uri.call_args - assert username == "456789" - assert b"svg" in qrcode_page_response.data + response = client.post("/tf-setup", json=setup_data, headers=headers) + assert response.json["response"]["tf_authr_username"] == "456789" + assert "tf_authr_key" in response.json["response"] -@pytest.mark.settings(USER_IDENTITY_ATTRIBUTES=("email", "username")) +@pytest.mark.settings( + USER_IDENTITY_ATTRIBUTES=[ + {"email": {"mapper": uia_email_mapper}}, + {"username": {"mapper": lambda x: x}}, + ] +) def test_email_salutation(app, client): - authenticate(client, email="jill@lp.com") - client.post("/tf-confirm", data=dict(password="password"), follow_redirects=True) - - test_mail = TestMail() - app.extensions["mail"] = test_mail - response = client.post("/tf-setup", data=dict(setup="email"), follow_redirects=True) - msg = b"To complete logging in, please enter the code sent to your mail" - assert msg in response.data + with app.mail.record_messages() as outbox: + response = client.post( + "/tf-setup", data=dict(setup="email"), follow_redirects=True + ) + msg = b"To complete logging in, please enter the code sent to your mail" + assert msg in response.data - assert "jill@lp.com" in test_mail.msg.send_to - assert "jill@lp.com" in test_mail.msg.body - assert "jill@lp.com" in test_mail.msg.html + assert "jill@lp.com" in outbox[0].send_to + assert "jill@lp.com" in outbox[0].body + assert "jill@lp.com" in outbox[0].html -@pytest.mark.settings(USER_IDENTITY_ATTRIBUTES=("username", "email")) +@pytest.mark.settings( + USER_IDENTITY_ATTRIBUTES=[ + {"username": {"mapper": lambda x: "@" not in x}}, + {"email": {"mapper": uia_email_mapper}}, + ] +) def test_username_salutation(app, client): - authenticate(client, email="jill@lp.com") - client.post("/tf-confirm", data=dict(password="password"), follow_redirects=True) + with app.mail.record_messages() as outbox: + response = client.post( + "/tf-setup", data=dict(setup="email"), follow_redirects=True + ) + msg = b"To complete logging in, please enter the code sent to your mail" + assert msg in response.data - test_mail = TestMail() - app.extensions["mail"] = test_mail - response = client.post("/tf-setup", data=dict(setup="email"), follow_redirects=True) - msg = b"To complete logging in, please enter the code sent to your mail" - assert msg in response.data - - assert "jill@lp.com" in test_mail.msg.send_to - assert "jill@lp.com" not in test_mail.msg.body - assert "jill@lp.com" not in test_mail.msg.html - assert "jill" in test_mail.msg.body + assert "jill@lp.com" in outbox[0].send_to + assert "jill@lp.com" not in outbox[0].body + assert "jill@lp.com" not in outbox[0].html + assert "jill" in outbox[0].body @pytest.mark.settings(sms_service="bad") @@ -900,7 +1118,6 @@ # Now test setup tf_authenticate(app, client) - client.post("/tf-confirm", data=dict(password="password"), follow_redirects=True) data = dict(setup="sms", phone="+442083661188") response = client.post("tf-setup", data=data) assert get_message("FAILED_TO_SEND_CODE") in response.data @@ -962,3 +1179,153 @@ response = client.post("/tf-rescue", json=rescue_data, headers=headers) assert response.status_code == 500 assert response.json["response"]["errors"]["help_setup"][0] == "Failed Again" + + +@pytest.mark.settings(freshness=timedelta(minutes=0)) +def test_verify(app, client, get_message): + # Test setup when re-authenticate required + authenticate(client) + response = client.get("tf-setup", follow_redirects=False) + verify_url = response.location + assert ( + verify_url == "http://localhost/verify?next=http%3A%2F%2Flocalhost%2Ftf-setup" + ) + logout(client) + + # Now try again - follow redirects to get to verify form + # This call should require re-verify + authenticate(client) + response = client.get("tf-setup", follow_redirects=True) + form_response = response.data.decode("utf-8") + assert get_message("REAUTHENTICATION_REQUIRED") in response.data + matcher = re.match( + r'.*form action="([^"]*)".*', form_response, re.IGNORECASE | re.DOTALL + ) + verify_password_url = matcher.group(1) + + # Send wrong password + response = client.post( + verify_password_url, + data=dict(password="iforgot"), + follow_redirects=True, + ) + assert response.status_code == 200 + assert get_message("INVALID_PASSWORD") in response.data + + # Verify with correct password + with capture_flashes() as flashes: + response = client.post( + verify_password_url, + data=dict(password="password"), + follow_redirects=False, + ) + assert response.status_code == 302 + assert response.location == "http://localhost/tf-setup" + assert get_message("REAUTHENTICATION_SUCCESSFUL") == flashes[0]["message"].encode( + "utf-8" + ) + + +def test_verify_json(app, client, get_message): + # Test setup when re-authenticate required + # N.B. with freshness=0 we never set a grace period and should never be able to + # get to /tf-setup + authenticate(client) + headers = {"Accept": "application/json", "Content-Type": "application/json"} + + app.config["SECURITY_FRESHNESS"] = timedelta(minutes=0) + response = client.get("tf-setup", headers=headers) + assert response.status_code == 401 + assert response.json["response"]["reauth_required"] + + response = client.post("verify", json=dict(password="notmine"), headers=headers) + assert response.status_code == 400 + assert response.json["response"]["errors"]["password"][0].encode( + "utf-8" + ) == get_message("INVALID_PASSWORD") + + response = client.post("verify", json=dict(password="password"), headers=headers) + assert response.status_code == 200 + + app.config["SECURITY_FRESHNESS"] = timedelta(minutes=60) + response = client.get("tf-setup", headers=headers) + assert response.status_code == 200 + + +@pytest.mark.settings(freshness=timedelta(minutes=-1)) +def test_setup_nofresh(app, client, get_message): + authenticate(client) + response = client.get("tf-setup", follow_redirects=False) + assert response.status_code == 200 + + +@pytest.mark.settings(two_factor_enabled_methods=["email"]) +def test_no_sms(app, get_message): + # Make sure that don't require tf_phone_number if SMS isn't an option. + from sqlalchemy import ( + Boolean, + Column, + Integer, + String, + ) + from sqlalchemy.orm import relationship, backref + from flask_sqlalchemy import SQLAlchemy + from flask_security.models import fsqla_v2 as fsqla + from flask_security import Security, UserMixin, hash_password + + app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:" + db = SQLAlchemy(app) + + fsqla.FsModels.set_db_info(db) + + class Role(db.Model, fsqla.FsRoleMixin): + pass + + class User(db.Model, UserMixin): + id = Column(Integer, primary_key=True) + email = Column(String(255), unique=True, nullable=False) + password = Column(String(255), nullable=False) + active = Column(Boolean(), nullable=False) + + # Faster token checking + fs_uniquifier = Column(String(64), unique=True, nullable=False) + + # 2FA + tf_primary_method = Column(String(64), nullable=True) + tf_totp_secret = Column(String(255), nullable=True) + + roles = relationship( + "Role", secondary="roles_users", backref=backref("users", lazy="dynamic") + ) + + with app.app_context(): + db.create_all() + + ds = SQLAlchemyUserDatastore(db, User, Role) + app.security = Security(app, datastore=ds) + + with app.app_context(): + client = app.test_client() + + ds.create_user( + email="trp@lp.com", + password=hash_password("password"), + ) + ds.commit() + + data = dict(email="trp@lp.com", password="password") + client.post("/login", data=data, follow_redirects=True) + + with app.mail.record_messages() as outbox: + response = client.post( + "/tf-setup", data=dict(setup="email"), follow_redirects=True + ) + msg = b"To complete logging in, please enter the code sent to your mail" + assert msg in response.data + + code = outbox[0].body.split()[-1] + # sumbit right token and show appropriate response + response = client.post( + "/tf-validate", data=dict(code=code), follow_redirects=True + ) + assert b"You successfully changed your two-factor method" in response.data diff -Nru flask-security-3.4.2/tests/test_unified_signin.py flask-security-4.0.0/tests/test_unified_signin.py --- flask-security-3.4.2/tests/test_unified_signin.py 2020-05-03 01:41:32.000000000 +0000 +++ flask-security-4.0.0/tests/test_unified_signin.py 2021-01-26 02:39:51.000000000 +0000 @@ -1,50 +1,56 @@ -# -*- coding: utf-8 -*- """ test_unified_signin ~~~~~~~~~~~~~~~~~~~ Unified signin tests - :copyright: (c) 2019-2020 by J. Christopher Wagner (jwag). + :copyright: (c) 2019-2021 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ +import base64 from contextlib import contextmanager from datetime import timedelta - -try: - from unittest.mock import Mock -except ImportError: - from mock import Mock +from passlib.totp import TOTP import re import time +from urllib.parse import parse_qsl, urlsplit import pytest from flask import Flask -from utils import SmsBadSender, SmsTestSender, authenticate, logout +from tests.test_utils import ( + SmsBadSender, + SmsTestSender, + authenticate, + capture_flashes, + capture_reset_password_requests, + logout, +) from flask_security import ( SmsSenderFactory, SQLAlchemyUserDatastore, UserMixin, uia_email_mapper, + uia_phone_mapper, us_profile_changed, us_security_token_sent, user_authenticated, ) -from flask_security.utils import capture_flashes, capture_reset_password_requests -try: - from urlparse import parse_qsl, urlsplit -except ImportError: # pragma: no cover - from urllib.parse import parse_qsl, urlsplit +from flask_security.utils import get_identity_attributes pytestmark = pytest.mark.unified_signin() SmsSenderFactory.senders["test"] = SmsTestSender SmsSenderFactory.senders["bad"] = SmsBadSender +UIA_EMAIL_PHONE = [ + {"email": {"mapper": uia_email_mapper, "case_insensitive": True}}, + {"us_phone_number": {"mapper": uia_phone_mapper}}, +] + @contextmanager def capture_send_code_requests(): @@ -82,6 +88,39 @@ return response.json["response"]["user"]["authentication_token"] +def us_tf_authenticate(app, client, json=False, validate=True, remember=False): + """Login/Authenticate using two factor and unified signin + This is the equivalent of utils:authenticate + """ + prev_sms = app.config["SECURITY_SMS_SERVICE"] + app.config["SECURITY_SMS_SERVICE"] = "test" + sms_sender = SmsSenderFactory.createSender("test") + json_data = dict(identity="gal@lp.com", passcode="password", remember=remember) + response = client.post( + "/us-signin", json=json_data, headers={"Content-Type": "application/json"} + ) + + assert b'"code": 200' in response.data + app.config["SECURITY_SMS_SERVICE"] = prev_sms + + if validate: + code = sms_sender.messages[0].split()[-1] + if json: + response = client.post( + "/tf-validate", + json=dict(code=code), + headers={"Content-Type": "application/json"}, + ) + assert b'"code": 200' in response.data + return response.json["response"].get("tf_validity_token", None) + else: + response = client.post( + "/tf-validate", data=dict(code=code), follow_redirects=True + ) + + assert response.status_code == 200 + + def set_phone(app, email="matt@lp.com", phone="650-273-3780"): # A quick way to 'setup' SMS with app.test_request_context("/"): @@ -91,7 +130,7 @@ app.security.datastore.commit() -def test_simple_signin(app, client, get_message): +def test_simple_signin(app, clients, get_message): auths = [] @user_authenticated.connect_via(app) @@ -100,23 +139,23 @@ # Test missing choice data = dict(identity="matt@lp.com") - response = client.post("/us-signin/send-code", data=data, follow_redirects=True) + response = clients.post("/us-signin/send-code", data=data, follow_redirects=True) assert get_message("US_METHOD_NOT_AVAILABLE") in response.data # Test login using invalid email data = dict(identity="nobody@lp.com", chosen_method="email") - response = client.post("/us-signin/send-code", data=data, follow_redirects=True) + response = clients.post("/us-signin/send-code", data=data, follow_redirects=True) assert get_message("US_SPECIFY_IDENTITY") in response.data # test disabled account data = dict(identity="tiya@lp.com", chosen_method="email") - response = client.post("/us-signin/send-code", data=data, follow_redirects=True) + response = clients.post("/us-signin/send-code", data=data, follow_redirects=True) assert b"Code has been sent" not in response.data assert get_message("DISABLED_ACCOUNT") in response.data with capture_send_code_requests() as requests: with app.mail.record_messages() as outbox: - response = client.post( + response = clients.post( "/us-signin/send-code", data=dict(identity="matt@lp.com", chosen_method="email"), follow_redirects=True, @@ -127,7 +166,7 @@ assert len(outbox) == 1 # try bad code - response = client.post( + response = clients.post( "/us-signin", data=dict(identity="matt@lp.com", passcode="blahblah"), follow_redirects=True, @@ -135,27 +174,27 @@ assert get_message("INVALID_PASSWORD_CODE") in response.data # Correct code - assert "remember_token" not in [c.name for c in client.cookie_jar] - assert "session" not in [c.name for c in client.cookie_jar] - response = client.post( + assert "remember_token" not in [c.name for c in clients.cookie_jar] + assert "session" not in [c.name for c in clients.cookie_jar] + response = clients.post( "/us-signin", data=dict(identity="matt@lp.com", passcode=requests[0]["token"]), follow_redirects=False, ) - assert "remember_token" not in [c.name for c in client.cookie_jar] + assert "remember_token" not in [c.name for c in clients.cookie_jar] assert "email" in auths[0][1] - response = client.get("/profile", follow_redirects=False) + response = clients.get("/profile", follow_redirects=False) assert response.status_code == 200 - logout(client) - response = client.get("/profile", follow_redirects=False) + logout(clients) + response = clients.get("/profile", follow_redirects=False) assert "/login?next=%2Fprofile" in response.location # login via SMS sms_sender = SmsSenderFactory.createSender("test") set_phone(app) - response = client.post( + response = clients.post( "/us-signin/send-code", data=dict(identity="matt@lp.com", chosen_method="sms"), follow_redirects=True, @@ -164,20 +203,20 @@ assert b"Sign In" in response.data code = sms_sender.messages[0].split()[-1].strip(".") - response = client.post( + response = clients.post( "/us-signin", data=dict(identity="matt@lp.com", passcode=code, remember=True), follow_redirects=True, ) assert response.status_code == 200 - assert "remember_token" in [c.name for c in client.cookie_jar] + assert "remember_token" in [c.name for c in clients.cookie_jar] assert "sms" in auths[1][1] - response = client.get("/profile", follow_redirects=False) + response = clients.get("/profile", follow_redirects=False) assert response.status_code == 200 - logout(client) - assert "remember_token" not in [c.name for c in client.cookie_jar] + logout(clients) + assert "remember_token" not in [c.name for c in clients.cookie_jar] def test_simple_signin_json(app, client_nc, get_message): @@ -196,10 +235,7 @@ assert ( jresponse["available_methods"] == app.config["SECURITY_US_ENABLED_METHODS"] ) - assert ( - jresponse["identity_attributes"] - == app.config["SECURITY_USER_IDENTITY_ATTRIBUTES"] - ) + assert jresponse["identity_attributes"] == get_identity_attributes(app=app) assert set(jresponse["code_methods"]) == {"email", "sms"} with capture_send_code_requests() as requests: @@ -266,6 +302,47 @@ assert len(flashes) == 0 +@pytest.mark.changeable() +def test_signin_pwd_json(app, client, get_message): + # Make sure us-signin accepts a normalized and original password. + authenticate(client) + headers = {"Accept": "application/json", "Content-Type": "application/json"} + data = dict( + password="password", + new_password="new strong password\N{ROMAN NUMERAL ONE}", + new_password_confirm="new strong password\N{ROMAN NUMERAL ONE}", + ) + response = client.post( + "/change", + json=data, + headers={"Content-Type": "application/json"}, + ) + assert response.status_code == 200 + logout(client) + + response = client.post( + "/us-signin", + json=dict( + identity="matt@lp.com", passcode="new strong password\N{ROMAN NUMERAL ONE}" + ), + headers=headers, + follow_redirects=False, + ) + assert response.status_code == 200 + logout(client) + + response = client.post( + "/us-signin", + json=dict( + identity="matt@lp.com", + passcode="new strong password\N{LATIN CAPITAL LETTER I}", + ), + headers=headers, + follow_redirects=False, + ) + assert response.status_code == 200 + + def test_admin_setup_user_reset(app, client_nc, get_message): # Test that we can setup SMS using datastore admin method, and that # the datastore admin reset (reset_user_access) disables it. @@ -382,6 +459,7 @@ ) +@pytest.mark.settings(us_email_subject="Code For You") def test_verify_link(app, client, get_message): auths = [] @@ -398,6 +476,9 @@ assert response.status_code == 200 assert b"Sign In" in response.data + assert outbox[0].recipients == ["matt@lp.com"] + assert outbox[0].sender == "no-reply@localhost" + assert outbox[0].subject == "Code For You" matcher = re.match( r".*(http://[^\s*]*).*", outbox[0].body, re.IGNORECASE | re.DOTALL ) @@ -571,7 +652,7 @@ @pytest.mark.settings( us_enabled_methods=["email", "sms"], - user_identity_attributes=["email", "us_phone_number"], + user_identity_attributes=UIA_EMAIL_PHONE, freshness=timedelta(hours=-1), ) def test_setup_json(app, client_nc, get_message): @@ -646,7 +727,7 @@ @pytest.mark.settings( us_enabled_methods=["email", "sms"], - user_identity_attributes=["email", "us_phone_number"], + user_identity_attributes=UIA_EMAIL_PHONE, ) def test_setup_json_no_session(app, client_nc, get_message): # Test that with normal config freshness is required so must have session. @@ -658,6 +739,21 @@ } response = client_nc.get("/us-setup", headers=headers) assert response.status_code == 401 + assert response.json["response"]["reauth_required"] + assert "WWW-Authenticate" not in response.headers + + +@pytest.mark.settings(api_enabled_methods=["basic"]) +def test_setup_basic(app, client, get_message): + # If using Basic Auth - always fresh so should be able to setup (not sure the + # use case but...) + headers = { + "Authorization": "Basic %s" + % base64.b64encode(b"matt@lp.com:password").decode("utf-8") + } + response = client.get("/us-setup", headers=headers) + assert response.status_code == 200 + assert b"Setup Unified Sign In options" in response.data def test_setup_bad_token(app, client, get_message): @@ -666,7 +762,7 @@ # bogus state response = client.post( - "/us-setup/" + "not a token", json=dict(passcode=12345), headers=headers + "/us-setup/not a token", json=dict(passcode=12345), headers=headers ) assert response.status_code == 400 assert response.json["response"]["error"].encode("utf-8") == get_message( @@ -675,7 +771,7 @@ # same w/o json response = client.post( - "/us-setup/" + "not a token", data=dict(passcode=12345), follow_redirects=True + "/us-setup/not a token", data=dict(passcode=12345), follow_redirects=True ) assert get_message("API_ERROR") in response.data @@ -780,7 +876,9 @@ } response = client.post( - "us-verify/send-code", json=dict(chosen_method="orb"), headers=headers, + "us-verify/send-code", + json=dict(chosen_method="orb"), + headers=headers, ) assert response.status_code == 400 @@ -788,7 +886,9 @@ sms_sender = SmsSenderFactory.createSender("test") set_phone(app) response = client.post( - "us-verify/send-code", json=dict(chosen_method="sms"), headers=headers, + "us-verify/send-code", + json=dict(chosen_method="sms"), + headers=headers, ) assert response.status_code == 200 @@ -885,7 +985,7 @@ def test_setup_new_totp(app, client, get_message): # us-setup should generate a new totp secret for each setup - # Verify existing cods no longer work + # Verify existing codes no longer work us_authenticate(client) headers = {"Accept": "application/json", "Content-Type": "application/json"} @@ -898,6 +998,7 @@ headers=headers, ) assert response.status_code == 200 + assert "authr_key" not in response.json["response"] code = sms_sender.messages[0].split()[-1].strip(".") # Now start setup again - it should generate a new totp - so the previous 'code' @@ -927,31 +1028,34 @@ def test_qrcode(app, client, get_message): - headers = {"Accept": "application/json", "Content-Type": "application/json"} + # Test forms based authenticator setup - can't really parse QRcode - but can use + # the key sent as part of the response. us_authenticate(client, identity="gal@lp.com") - response = client.post( - "us-setup", json=dict(chosen_method="authenticator"), headers=headers - ) + response = client.post("us-setup", data=dict(chosen_method="authenticator")) assert response.status_code == 200 - state = response.json["response"]["state"] + # verify png QRcode is present + assert b"data:image/svg+xml;base64," in response.data - # Now request code. We can't test the qrcode easily - but we can get the totp_secret - # that goes into the qrcode and make sure that works - mtf = Mock(wraps=app.security._totp_factory) - app.security.totp_factory(mtf) - qrcode_page_response = client.get("/us-qrcode/" + state, follow_redirects=True) - assert mtf.get_totp_uri.call_count == 1 - (username, totp_secret), _ = mtf.get_totp_uri.call_args - assert username == "gal@lp.com" - assert b"svg" in qrcode_page_response.data + # parse out key + rd = response.data.decode("utf-8") + matcher = re.match(r".*((?:\S{4}-){7}\S{4}).*", rd, re.DOTALL) + totp_secret = matcher.group(1) # Generate token from passed totp_secret and confirm setup - code = app.security._totp_factory.generate_totp_password(totp_secret) - response = client.post( - "/us-setup/" + state, json=dict(passcode=code), headers=headers + totp = TOTP(totp_secret) + code = totp.generate().token + + # get verify link e.g. /us-setup/{state} + matcher = re.match( + r'.*